ทุก App ที่คุณใช้ทุกวันมี Database อยู่ข้างหลัง — LINE (messages, contacts), Shopee (products, orders), YouTube (videos, comments), Banking App (accounts, transactions) 🏦
💡 "App without a database is just a pretty picture" — Code เปลี่ยนได้ทุกวัน, Framework เปลี่ยนได้ทุกปี แต่ Database schema ที่ออกแบบผิด → ปวดหัวไปตลอดชีวิต
📊 Database Types — เลือกอะไรดี?
ก่อนที่เราจะเจาะลึกเรื่อง SQL เราต้องเข้าใจก่อนว่ามี Database หลายประเภท แต่ละแบบมีจุดเด่นและเหมาะกับงานแบบไหน การเลือก Database ที่ถูกต้องตั้งแต่เริ่มต้นจะช่วยให้โปรเจคของคุณเติบโตได้อย่างมีประสิทธิภาพ
ในโลกของ Database มี 2 กลุ่มใหญ่ที่เราต้องเลือก: Relational Database (SQL) ที่ข้อมูลมีโครงสร้างและความสัมพันธ์ที่ชัดเจน กับ NoSQL Database ที่ยืดหยุ่นในเรื่องโครงสร้างมากกว่า แต่ละแบบมีเหตุผลในการใช้งานที่แตกต่างกัน
- ข้อมูลมีความสัมพันธ์กัน
- ต้องการ ACID
- Complex queries
- Transaction สำคัญ
- 90% ของ apps
- Schema ไม่แน่นอน
- ต้องการ scale แนวนอน
- High throughput
- Real-time / Cache
- Specific use cases
จากการเปรียบเทียบข้างต้น คุณจะเห็นว่า Relational Database ยังคงเป็นตัวเลือกหลักสำหรับแอปพลิเคชันส่วนใหญ่ เพราะข้อมูลในแอปทั่วไปมักมีความสัมพันธ์กัน เช่น ลูกค้า-คำสั่งซื้อ, โพสต์-ความคิดเห็น, ผู้ใช้-เพื่อน
ในกลุ่ม Relational Database เอง PostgreSQL เป็นตัวเลือกที่แนะนำที่สุดในปัจจุบัน ด้วยความสามารถที่หลากหลายและฟีเจอร์ที่ทันสมัย แต่หลายๆ องค์กรยังใช้ MySQL เป็นหลัก มาดูเปรียบเทียบกันว่าต่างกันอย่างไร
PostgreSQL vs MySQL — เปรียบเทียบ
| PostgreSQL ⭐ | MySQL | |
|---|---|---|
| License | BSD (เสรีจริงๆ) | GPL (Oracle) |
| ACID | ✅ Full | ✅ (InnoDB) |
| JSON support | ⭐ JSONB (เร็ว) | JSON (ช้ากว่า) |
| Full-text search | ✅ Built-in | ✅ Basic |
| Extensions | PostGIS, pg_trgm, pgvector, timescale | Limited |
| Complex queries | ⭐ CTE, Window functions, lateral | ✅ OK |
| Performance | Complex queries ดีกว่า | Simple reads ดีกว่า |
| ใช้โดย | Instagram, Stripe, Discord, Reddit | Facebook, Uber, Airbnb, Twitter |
จากตารางเปรียบเทียบข้างต้น คุณจะเห็นว่า PostgreSQL มีความได้เปรียบในหลายๆ ด้าน โดยเฉพาะการรองรับ JSONB ที่ทำให้สามารถเก็บข้อมูลแบบ NoSQL ได้ภายใน Relational Database เดียวกัน รวมถึง Extensions ที่ทรงพลังเช่น PostGIS สำหรับข้อมูลเชิงพื้นที่
MySQL ก็ยังมีจุดเด่นในเรื่องความเร็วของ Simple Reads และการมี Community ที่ใหญ่ แต่สำหรับโปรเจคใหม่ PostgreSQL มักจะเป็นทางเลือกที่ดีกว่าเพราะความยืดหยุ่นและฟีเจอร์ที่ครบครัน
💡 แนะนำ: เริ่มต้นที่ PostgreSQL — ทำได้ทุกอย่าง
SQLite สำหรับ prototype / mobile / embedded
🏗️ Database Design — ออกแบบยังไงให้ดี?
การออกแบบ Database ที่ดีคือรากฐานของแอปพลิเคชันที่มีประสิทธิภาพ การออกแบบที่ผิดพลาดจะทำให้เราต้องแก้ไขในภายหลังด้วยความยากลำบาก ซึ่งเป็นเรื่องที่ค่อนข้างเสี่ยงและใช้เวลามาก
หลักการสำคัญในการออกแบบคือการลด Data Redundancy (ข้อมูลซ้ำซ้อน) และรักษา Data Integrity (ความถูกต้องของข้อมูล) มาดูตัวอย่างระบบ E-Commerce แบบง่ายๆ กันว่าการออกแบบที่ถูกและผิดต่างกันอย่างไร
ตัวอย่าง: ระบบ E-Commerce
สมมติว่าเราต้องเก็บข้อมูลลูกค้า สินค้า และคำสั่งซื้อ หลายๆ คนเริ่มต้นด้วยการยัดข้อมูลทุกอย่างลงใน table เดียว ซึ่งดูง่ายแต่จะสร้างปัญหาใหญ่ในอนาคต
orders table (🤮 ทุกอย่างอยู่ที่เดียว):
id | customer_name | customer_email | customer_phone | product_name | product_price | product_category | quantity | total | order_date | shipping_address | shipping_city | ...
ปัญหา:
- ข้อมูลซ้ำ: customer_name ซ้ำทุก order
- แก้ยาก: เปลี่ยนเบอร์โทร → ต้องแก้ทุก row
- Inconsistent: บาง row ชื่อ "สมชาย" บาง row "Somchai"
- ช้า: table ใหญ่ขึ้นเรื่อยๆ query ช้าลง
ตัวอย่างข้างต้นแสดงให้เห็นความแตกต่างอย่างชัดเจนระหว่างการออกแบบแบบผิดและถูก แบบผิดจะทำให้ข้อมูลซ้ำซ้อน แก้ไขยาก และเกิดความไม่สอดคล้องได้ง่าย ขณะที่แบบถูกจะแยกข้อมูลออกเป็นหลาย tables และใช้ Foreign Keys เชื่อมความสัมพันธ์
การแยก tables ด้วย Primary Keys และ Foreign Keys ทำให้:
- ข้อมูลลูกค้าเก็บครั้งเดียว → แก้ที่เดียวส่งผลทั่วระบบ
- สินค้าหมดสต็อก → อัพเดตที่เดียวแสดงผลทุกที่
- เพิ่มฟีเจอร์ใหม่ → ไม่ต้องแก้ table เดิม
- Query เร็วขึ้น → table เล็กลง index มีประสิทธิภาพมากขึ้น
หลักการนี้เรียกว่า Normalization ซึ่งเป็นกระบวนการลดความซ้ำซ้อนของข้อมูลอย่างเป็นระบบ
Normalization — ลดความซ้ำซ้อน
❌ phone: "081-xxx, 082-xxx"
✅ แยกเป็น phone_numbers table
❌ order_items(order_id, product_id, product_name)
❌ orders(customer_id, customer_name, customer_city)
Normalization แต่ละระดับมีจุดประสงค์และข้อดีที่ชัดเจน 1NF ทำให้ข้อมูลมีโครงสร้างพื้นฐาน 2NF ลดการเก็บข้อมูลซ้ำที่เกิดจาก Composite Keys และ 3NF ตัดการพึ่งพาทางอ้อมออกไป
ในการทำงานจริง การออกแบบถึง 3NF มักจะเพียงพอแล้ว ถ้าไปไกลกว่านี้จะซับซ้อนเกินความจำเป็น ส่วน Denormalization (การรวม tables กลับเข้าด้วยกัน) ควรทำเฉพาะเมื่อมี Performance Issue จริงๆ และวัดผลได้ชัดเจน
💡 ในทางปฏิบัติ: ออกแบบถึง 3NF ก็เพียงพอ
Denormalize เฉพาะเมื่อ performance ต้องการจริงๆ
เมื่อออกแบบ Database เสร็จแล้ว ขั้นตอนต่อไปคือการเรียนรู้ SQL (Structured Query Language) ซึ่งเป็นภาษาสำหรับจัดการข้อมูลใน Relational Database ทุกระบบ SQL มีพื้นฐานคล้ายๆ กัน แต่แต่ละตัวก็มี Extensions และฟีเจอร์พิเศษของตัวเอง
📝 SQL Essentials — คำสั่งที่ต้องรู้
CREATE TABLE — สร้าง Table
การสร้าง Table เป็นขั้นตอนแรกของการทำงานกับ Database คำสั่ง CREATE TABLE ใน SQL ใช้ในการกำหนดโครงสร้างของ Table รวมถึง Data Types, Constraints, และความสัมพันธ์ต่างๆ
ตัวอย่างนี้จะแสดงการสร้าง Tables สำหรับระบบ E-Commerce ที่เรากล่าวถึงก่อนหน้านี้ โดยใช้ PostgreSQL Syntax โปรดสังเกต Constraints ต่างๆ เช่น PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE ที่ช่วยรักษาความถูกต้องของข้อมูล
-- ═══════════════════════════════
-- สร้าง Tables สำหรับ E-Commerce
-- ═══════════════════════════════
-- Categories
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
parent_id INT REFERENCES categories(id), -- self-reference
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Products
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE, -- URL-friendly name
description TEXT,
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
category_id INT REFERENCES categories(id),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
จากตัวอย่างข้างต้น คุณจะเห็นการใช้ Constraints ต่างๆ ที่สำคัญ:
- SERIAL PRIMARY KEY → สร้าง auto-increment ID
- NOT NULL → บังคับไม่ให้เป็นค่าว่าง
- UNIQUE → ไม่ให้มีค่าซ้ำ (เช่น email, slug)
- CHECK → กำหนดเงื่อนไข (เช่น price >= 0)
- REFERENCES → สร้าง Foreign Key เชื่อม Tables
- DEFAULT → ค่าเริ่มต้นถ้าไม่ได้ใส่ข้อมูล
การเลือก Data Type ที่เหมาะสมสำคัญมาก เพราะส่งผลต่อ Storage Space, Performance และความถูกต้องของข้อมูล มาดู Data Types ที่ใช้บ่อยในแต่ละหมวดหมู่
Data Types ที่ใช้บ่อย
การเลือก Data Type ที่เหมาะสมจะช่วยประหยัด Storage และเพิ่ม Performance VARCHAR เหมาะกับข้อมูลที่มีความยาวไม่แน่นอน TEXT สำหรับข้อมูลยาวๆ เช่น บทความ INT สำหรับตัวเลขจำนวนเต็ม และ DECIMAL สำหรับการคำนวณที่ต้องการความแม่นยำ
โดยเฉพาะ TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ) ใน PostgreSQL ซึ่งแนะนำมากเพราะจัดการ Time Zone ให้อัตโนมัติ ไม่ต้องไปปวดหัวกับปัญหาเวลาที่แตกต่างกันในแต่ละประเทศ
💡 เงิน → DECIMAL ไม่ใช่ FLOAT!
FLOAT: 0.1 + 0.2 = 0.30000000000000004
DECIMAL: 0.1 + 0.2 = 0.3 ✅
เมื่อสร้าง Tables เสร็จแล้ว ขั้นตอนต่อไปคือการทำงานกับข้อมูล หรือที่เราเรียกว่า CRUD Operations ซึ่งครอบคลุมการทำงานพื้นฐานทั้งหมดที่เราต้องทำกับ Database
CRUD — Create, Read, Update, Delete
-- ═══════════════════════════════
-- INSERT — เพิ่มข้อมูล
-- ═══════════════════════════════
-- เพิ่ม 1 record
INSERT INTO customers (name, email, phone, password_hash)
VALUES ('สมชาย ใจดี', '[email protected]', '081-234-5678', '$2b$10$hash...');
-- เพิ่มหลาย records
INSERT INTO products (name, slug, price, stock, category_id) VALUES
('iPhone 16 Pro', 'iphone-16-pro', 45900.00, 50, 1),
('MacBook Air M4', 'macbook-air-m4', 42900.00, 30, 2),
('AirPods Pro 3', 'airpods-pro-3', 8900.00, 100, 3);
-- INSERT + RETURNING (ได้ค่ากลับมา)
INSERT INTO orders (customer_id, status, total)
VALUES (1, 'pending', 54800.00)
RETURNING id, ordered_at;
คำสั่ง INSERT ข้างต้นแสดงวิธีการเพิ่มข้อมูลทั้งแบบ Single Record และ Multiple Records พร้อมกัน การใช้ INSERT ... RETURNING เป็นฟีเจอร์ที่มีประโยชน์มาก เพราะจะได้ค่า ID หรือค่าอื่นๆ ที่ Database สร้างให้กลับมาทันที
แต่การใส่ข้อมูลเพียงอย่างเดียวไม่เพียงพอ เราต้องสามารถ Query ข้อมูลจากหลาย Tables พร้อมกันด้วย นี่คือจุดที่ JOIN เข้ามามีบทบาทสำคัญ
JOIN — เชื่อมข้อมูลจากหลาย Tables
-- ═══════════════════════════════
-- INNER JOIN — เอาเฉพาะที่ match ทั้ง 2 ฝั่ง
-- ═══════════════════════════════
SELECT c.name, o.id AS order_id, o.total, o.status
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
-- LEFT JOIN — เอาทุก row จากซ้าย + match จากขวา (ถ้ามี)
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name;
-- → ได้ลูกค้าทุกคน แม้ยังไม่เคยสั่ง (order_count = 0)
JOIN เป็นหัวใจสำคัญของ Relational Database ที่ทำให้เราสามารถเชื่อมข้อมูลจากหลาย Tables มาแสดงผลร่วมกัน INNER JOIN จะได้เฉพาะข้อมูลที่ match ทั้งสองฝั่ง ส่วน LEFT JOIN จะได้ข้อมูลทั้งหมดจากฝั่งซ้าย และข้อมูลจากฝั่งขวาเฉพาะที่ match
การเข้าใจ JOIN Types ต่างๆ เป็นสิ่งสำคัญ เพราะจะส่งผลต่อผลลัพธ์ที่ได้ เช่น การนับจำนวน Orders ของลูกค้าแต่ละคน ถ้าใช้ INNER JOIN จะได้เฉพาะลูกค้าที่เคยสั่งซื้อ แต่ถ้าใช้ LEFT JOIN จะได้ลูกค้าทุกคน รวมถึงคนที่ยังไม่เคยสั่งซื้อ (แสดงเป็น 0)
แต่เมื่อข้อมูลมากขึ้น Query จะช้าลง ซึ่งนี่คือจุดที่ Indexing เข้ามาช่วยทำให้ Query เร็วขึ้นอย่างมหาศาล
⚡ Indexing — ทำให้ Query เร็วขึ้น 100x
📄📄📄📄📄📄📄📄📄📄 → 1,000,000 rows
🔍 → 📄 → found!
การเปรียบเทียบข้างต้นแสดงให้เห็นพลังของ Index อย่างชัดเจน การที่ Database ต้อง Scan ข้อมูล 1 ล้าน Records ใช้เวลา 2.3 วินาที แต่เมื่อมี Index แล้วใช้เวลาแค่ 0.002 วินาที นั่นคือเร็วขึ้น 1000 เท่า!
Index = สารบัญหนังสือ — ไม่มี index = ต้องอ่านหนังสือทั้งเล่มเพื่อหาหน้าที่ต้องการ
แต่ Index ไม่ใช่ยาวิเศษที่ใส่ได้ไม่จำกัด Index แต่ละตัวใช้ Storage และทำให้ INSERT/UPDATE ช้าลง เพราะต้องไปอัพเดต Index ด้วย ดังนั้นต้องสร้าง Index อย่างชาญฉลาดตามการใช้งานจริง
-- ═══════════════════════════════
-- สร้าง Index
-- ═══════════════════════════════
-- B-tree (default) — ใช้ได้กับ =, <, >, BETWEEN, ORDER BY
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_orders_status ON orders(status);
-- Unique index — ป้องกันค่าซ้ำ + เร็ว
CREATE UNIQUE INDEX idx_customers_email ON customers(email);
-- Composite index — หลาย columns
CREATE INDEX idx_orders_customer_status
ON orders(customer_id, status);
-- Partial index — index เฉพาะบาง rows
CREATE INDEX idx_active_products
ON products(name, price)
WHERE is_active = true;
ตัวอย่างการสร้าง Index ข้างต้นแสดงหลายแบบที่ใช้ในงานจริง:
- B-tree Index (default) → เหมาะกับ equality และ range queries
- Unique Index → ป้องกันข้อมูลซ้ำและเร็วกว่า Index ธรรมดา
- Composite Index → Index หลาย columns พร้อมกัน เหมาะกับ Query ที่ใช้หลาก column ใน WHERE
- Partial Index → Index เฉพาะ rows ที่ตรงเงื่อนไข ประหยัด space และเร็วกว่า
หลังจากสร้าง Index แล้ว เราจำเป็นต้องตรวจสอบว่า Database ใช้ Index หรือไม่ ผ่านคำสั่ง EXPLAIN ANALYZE ที่เป็นเครื่องมือสำคัญสำหรับ Performance Tuning
EXPLAIN ANALYZE — วิเคราะห์ Performance
EXPLAIN ANALYZE
SELECT p.name, p.price
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.name = 'Electronics'
AND p.price < 50000
AND p.is_active = true
ORDER BY p.price DESC
LIMIT 10;
-- ผลลัพธ์:
-- Limit (cost=12.45..12.48 rows=10) (actual time=0.089..0.091 rows=10 loops=1)
-- -> Sort (cost=12.45..12.58 rows=50) (actual time=0.088..0.089 rows=10)
-- Sort Key: p.price DESC
-- -> Nested Loop (cost=0.43..11.89 rows=50) (actual time=0.031..0.072)
-- -> Index Scan using idx_categories_name on categories c ← ✅ ใช้ index!
-- -> Index Scan using idx_products_category on products p ← ✅ ใช้ index!
ผลลัพธ์จาก EXPLAIN ANALYZE แสดงให้เห็น Query Plan ที่ Database เลือกใช้ การที่เห็น "Index Scan" แสดงว่า Database ใช้ Index เราได้สำเร็จ ส่วนถ้าเห็น "Seq Scan" (Sequential Scan) แสดงว่าไม่มี Index ที่เหมาะสม จำเป็นต้องไปสร้างเพิ่มเติม
ค่า "cost" และ "actual time" ที่แสดงออกมาช่วยให้เราเปรียบเทียบ Performance ได้ และค่า "rows" บอกจำนวน Records ที่ประมวลผล ข้อมูลเหล่านี้ช่วยให้เราตัดสินใจได้ว่าควรสร้าง Index เพิ่มหรือไม่
แต่การสร้าง Index เป็นเพียงส่วนหนึ่งของการจัดการ Database เราต้องมีวิธีจัดการการเปลี่ยนแปลง Schema อย่างเป็นระบบด้วย ซึ่งนั่นคือจุดที่ Database Migrations เข้ามามีบทบาท
🔄 Migrations — จัดการ Schema Changes
ทำไมต้อง Migrations? → ทุก schema change เป็น code file, version controlled (git), รันซ้ำได้, rollback ได้, ทุกคนใน team ได้ schema เดียวกัน
Migration ด้วย Knex.js (Node.js)
// migrations/20260307_001_create_products.js
exports.up = function(knex) {
return knex.schema.createTable('products', (table) => {
table.increments('id');
table.string('name', 255).notNullable();
table.string('slug', 255).notNullable().unique();
table.text('description');
table.decimal('price', 10, 2).notNullable();
table.integer('stock').defaultTo(0);
table.integer('category_id')
.references('id').inTable('categories');
table.boolean('is_active').defaultTo(true);
table.timestamps(true, true); // created_at, updated_at
// Indexes
table.index('category_id');
table.index('price');
table.index(['is_active', 'price']); // composite
});
};
exports.down = function(knex) {
return knex.schema.dropTable('products');
};
ตัวอย่าง Migration ข้างต้นแสดงโครงสร้างที่ดี มีทั้ง up() function สำหรับการเปลี่ยนแปลง และ down() function สำหรับการ rollback ถ้าเกิดปัญหา การที่ Migration เป็น Code ทำให้เราสามารถใส่ไว้ใน Git, Review, และทำงานเป็น Team ได้
ข้อดีของ Migrations:
- Version Control → Schema changes ติดตาม Git ได้
- Reproducible → รันบน Dev, Staging, Production ได้เหมือนกัน
- Rollback → ย้อนกลับได้ถ้าเกิดปัญหา
- Team Sync → ทุกคนใน team ได้ Schema เดียวกัน
- Automation → รัน CI/CD ได้อัตโนมัติ
หลังจากที่เราจัดการ Schema และ Performance แล้ว ยังมีอีกเรื่องสำคัญที่ห้ามลืม นั่นคือ Security โดยเฉพาะการป้องกัน SQL Injection ที่เป็นช่องโหว่อันตรายที่สุดอันดับต้นๆ
🛡️ Security — ป้องกัน SQL Injection
SQL Injection — อันตรายที่สุดของ web apps
// ❌ String concatenation (อย่าทำ!!!)
// User input: ' OR '1'='1' --
const query = `SELECT * FROM users
WHERE email = '${email}'
AND password = '${password}'`;
// กลายเป็น:
SELECT * FROM users
WHERE email = '' OR '1'='1' --'
AND password = 'anything'
// → Login ได้โดยไม่ต้องรู้ password!
// ✅ Parameterized queries (ทำแบบนี้เสมอ!)
const query = 'SELECT * FROM users WHERE email = $1 AND password = $2';
db.query(query, [email, password]);
ตัวอย่างข้างต้นแสดงความแตกต่างระหว่าง String Concatenation ที่อันตรายกับ Parameterized Queries ที่ปลอดภัย เมื่อใช้ Parameterized Queries Database จะแยก SQL Structure กับ Data ออกจากกัน ทำให้ไม่สามารถ Inject คำสั่ง SQL เพิ่มเติมได้
กฎทองของการป้องกัน SQL Injection:
- ไม่เอา User Input มา Concat กับ SQL โดยตรง → ไม่มีข้อยกเว้น
- ใช้ Parameterized Queries เสมอ → $1, $2, ? หรือ :name
- Validate Input → ตรวจสอบ Type, Range, Format
- ใช้ ORM/Query Builder → มี Protection built-in
- Least Privilege → DB User มีสิทธิ์เฉพาะที่จำเป็น
นอกจาก Security แล้ว เรายังต้องรู้วิธีจัดการกับ Operations ที่ซับซ้อน ที่จำเป็นต้อง "ทำสำเร็จทั้งหมดหรือไม่ทำเลย" ซึ่งนั่นคือแนวคิดของ Database Transactions
🔄 Transactions — All or Nothing
-- Transaction — การสั่งซื้อต้อง atomic
-- ถ้า step ไหน fail → rollback ทั้งหมด!
BEGIN;
-- 1. สร้าง order
INSERT INTO orders (customer_id, status, total)
VALUES (1, 'confirmed', 54800.00)
RETURNING id INTO order_id;
-- 2. เพิ่ม order items
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
(order_id, 1, 1, 45900.00),
(order_id, 3, 1, 8900.00);
-- 3. ลด stock
UPDATE products SET stock = stock - 1 WHERE id = 1;
UPDATE products SET stock = stock - 1 WHERE id = 3;
COMMIT;
-- ถ้าทุกอย่าง OK → COMMIT
-- ถ้ามี error → ROLLBACK อัตโนมัติ
ACID Properties:
- A - Atomicity → ทำทั้งหมดหรือไม่ทำเลย
- C - Consistency → ข้อมูลถูกต้องเสมอ
- I - Isolation → Transaction แต่ละตัวไม่เห็นกัน
- D - Durability → COMMIT แล้ว = บันทึกถาวร
ตัวอย่าง Transaction ข้างต้นแสดงการสั่งซื้อสินค้าที่ต้องทำหลายขั้นตอนพร้อมกัน ถ้าขั้นตอนใดล้มเหลว (เช่น สินค้าหมด, ข้อมูลผิด) ทุกอย่างจะถูก Rollback กลับสู่สถานะเดิม ป้องกันไม่ให้เกิด Inconsistent Data
ACID Properties ทำให้ Database เชื่อถือได้:
- Atomicity → ป้องกันข้อมูลครึ่งๆ กลางๆ
- Consistency → รักษา Business Rules
- Isolation → Transactions ไม่รบกวนกัน
- Durability → ข้อมูล Commit แล้วไม่หาย
เมื่อเราเข้าใจ SQL พื้นฐานแล้ว คำถามต่อไปคือควรเขียน Raw SQL หรือใช้เครื่องมือช่วยเช่น ORM หรือ Query Builder มาดูเปรียบเทียบกันว่าแต่ละแบบเหมาะกับสถานการณ์ไหน
🏗️ ORM vs Query Builder vs Raw SQL
| ORM | Query Builder | Raw SQL | |
|---|---|---|---|
| ตัวอย่าง | Prisma, SQLAlchemy, TypeORM | Knex.js, Kysely | pg, psycopg2 |
| Abstraction | สูง | กลาง | ต่ำ |
| เรียนรู้ | ง่าย | กลาง | ต้องรู้ SQL |
| Complex queries | ยากขึ้น | ยืดหยุ่น | ⭐ ยืดหยุ่นสุด |
| Type safety | ⭐ ดีมาก | ✅ OK | ❌ ไม่มี |
| Performance | ดี (แต่ N+1 problem) | ⭐ ใกล้ raw | ⭐ เร็วสุด |
| เหมาะกับ | CRUD apps, rapid dev | Complex queries | Analytics, optimization |
การเปรียบเทียบข้างต้นแสดงให้เห็นว่าแต่ละ Approach มีจุดเด่นต่างกัน ORM เหมาะกับ Developer ที่ต้องการ Type Safety และ Productivity สูง แต่อาจมีข้อจำกัดใน Complex Queries และมีความเสี่ยงของ N+1 Query Problem
Query Builder เป็น Middle Ground ที่ดี มีความยืดหยุ่นใกล้เคียง Raw SQL แต่ยังมี Type Safety บางส่วน เหมาะกับ Team ที่มี SQL Skills ดีและต้องการควบคุม Queries มากขึ้น
Raw SQL ให้ Performance และความยืดหยุ่นสูงสุด เหมาะกับ Analytics, Reporting, หรือ Optimization ขั้นสูง แต่ต้องระวังเรื่อง SQL Injection และการ Maintain
💡 แนะนำ: Prisma/SQLAlchemy ORM สำหรับ CRUD ทั่วไป + Raw SQL สำหรับ complex analytics queries
เมื่อเราเข้าใจเทคนิคต่างๆ แล้ว สิ่งสำคัญคือการนำไปใช้อย่างเป็นระบบ Checklist ต่อไปนี้จะช่วยให้คุณออกแบบและจัดการ Database อย่างมืออาชีพ
📋 Database Design Checklist
Checklist ข้างต้นครอบคลุมจุดสำคัญทุกด้านของการทำงานกับ Database ตั้งแต่การออกแบบ Performance การรักษาความปลอดภัย และการ Operations ที่ดี การทำตาม Checklist เหล่านี้จะช่วยให้ Database ของคุณมีคุณภาพและพร้อมรองรับการเติบโตของแอปพลิเคชัน
การออกแบบ Database ที่ดีไม่ใช่เรื่องที่ทำได้ในคืนเดียว แต่เป็นทักษะที่พัฒนาได้ผ่านการฝึกฝนและประสบการณ์ สิ่งสำคัญคือการเริ่มต้นด้วยหลักการที่ถูกต้อง และค่อยๆ ปรับปรุงเมื่อมีความเข้าใจมากขึ้น
จุดสำคัญที่ควรจำไว้คือ Database เป็น Foundation ของแอปพลิเคชัน การลงทุนเวลาทำความเข้าใจและออกแบบให้ดีตั้งแต่เริ่มต้นจะประหยัดเวลาและลดปัญหาในอนาคตได้มากมาย
🔑 Key Takeaways
- PostgreSQL → แนะนำเป็นตัวเลือกแรก — ทำได้ทุกอย่าง, JSONB, extensions, free
- Normalize ถึง 3NF → ลดข้อมูลซ้ำ, denormalize เมื่อ performance ต้องการ
- Index ทุก FK + WHERE columns → EXPLAIN ANALYZE เพื่อตรวจสอบ
- Parameterized queries เสมอ → ป้องกัน SQL Injection (ห้าม concat string!)
- Transactions → ACID = all or nothing, ใช้สำหรับ multi-step operations
- DECIMAL สำหรับเงิน → ห้ามใช้ FLOAT! (0.1 + 0.2 ≠ 0.3)
- Migrations → ทุก schema change เป็น code, version controlled
- ORM + Raw SQL → ORM สำหรับ CRUD, Raw SQL สำหรับ analytics
- Window Functions → RANK, SUM OVER, ROW_NUMBER — analytics ขั้นสูง
- Soft delete → อย่า DELETE จริง, ใช้ is_active = false