โหมดมืด
บทที่ 1 — SQL พื้นฐาน (DDL + DML)
หลังจบบท คุณจะ:
- สร้าง/ลบ table ได้
- INSERT, UPDATE, DELETE, SELECT ได้คล่อง
- ใช้ WHERE, ORDER BY, LIMIT, DISTINCT
- เข้าใจ data type ของ PostgreSQL
- ตั้ง constraint (NOT NULL, UNIQUE, CHECK, DEFAULT)
📖 Glossary คำอ่าน — ศัพท์ DDL ในบทนี้
คำ คำอ่าน สั้น ๆ SQL เอส-คิว-แอล / ซีเควล ภาษา query DDL ดี-ดี-แอล Data Definition Language DML ดี-เอ็ม-แอล Data Manipulation Language SERIAL ซี-เรียล auto-increment integer (legacy) IDENTITY ไอ-เดน-ทิ-ที auto-increment แบบใหม่ตาม SQL standard VARCHAR วาร์-ชาร์ variable-length string NUMERIC นู-เม-ริก ตัวเลขทศนิยมแม่นยำ BOOLEAN บูล-เลียน TRUE/FALSE TEXT เท็กซ์ท string ไม่จำกัด JSONB เจ-สัน-บี JSON binary TIMESTAMPTZ ไทม์-สแตมป์-ที-ซี timestamp + timezone UUID ยู-ยู-ไอ-ดี unique 128-bit identifier UPSERT อัพ-เซิร์ท insert + update if exists constraint คอน-สเตรนท์ กฎที่ DB บังคับ idempotent ไอ-เด็ม-โพ-เทนต์ ทำซ้ำได้ผลเหมือนเดิม
1. DDL vs DML
| DDL (Data Definition Language) | DML (Data Manipulation Language) | |
|---|---|---|
| ทำอะไร | กำหนด schema | จัดการข้อมูล |
| คำสั่ง | CREATE, ALTER, DROP, TRUNCATE | SELECT, INSERT, UPDATE, DELETE |
2. CREATE TABLE — สร้างตาราง
เริ่มจากตารางง่ายสุดก่อน — แค่ id, name, age 3 column:
sql
-- minimal CREATE TABLE — เริ่มจากนี้ก่อน
CREATE TABLE products_minimal (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(200) NOT NULL,
age INTEGER
);อธิบาย:
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY— เลขที่ DB นับเองอัตโนมัติ (1, 2, 3, ...) เป็น primary key (ระบุตัวตน 1 row)VARCHAR(200) NOT NULL— string ยาวไม่เกิน 200 ตัวอักษร, ห้ามว่างINTEGER— ตัวเลขจำนวนเต็ม
ตอนนี้ลองเพิ่ม column ทีละชนิด สำหรับตาราง products จริง:
sql
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0,
category VARCHAR(50),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
description TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);อธิบายทุกบรรทัด:
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY— auto-increment integer + primary key (SQL standard ตั้งแต่ PG 10 — แนะนำใช้แทนSERIALที่เป็น legacy)VARCHAR(200) NOT NULL— string max 200 chars, ห้าม nullNUMERIC(10, 2)— ตัวเลขแม่นยำ 10 หลัก ทศนิยม 2 ตำแหน่ง (เหมาะกับเงิน/ราคา — ไม่ปัดเศษ)CHECK (price >= 0)— บังคับว่า price ต้อง ≥ 0 (ถ้าใส่ค่าติดลบ DB จะ reject)DEFAULT 0— ถ้าไม่ใส่ค่า → ใช้ค่า defaultBOOLEAN— true/false (จริง/เท็จ)TEXT— string ยาวไม่จำกัด (เหมาะกับ description)JSONB— JSON ที่เก็บเป็น binary format (ภายในเก็บเป็น byte ไม่ใช่ text) ทำให้ index ได้, query เร็ว (Postgres feature)TIMESTAMPTZ— timestamp with timezone (เก็บเวลาเป็น UTC + จำ timezone — ไม่เพี้ยนข้าม region)
💡 SERIAL vs GENERATED ALWAYS AS IDENTITY — ก่อน PG 10 ใช้
SERIAL(legacy: behind-the-scenes ใช้ sequence + permission ซับซ้อน) ตั้งแต่ PG 10 มีGENERATED ALWAYS AS IDENTITYเป็น SQL standard แนะนำใช้แบบใหม่สำหรับตาราง ใหม่ —SERIALยังพบเห็นในโค้ดเก่าเยอะ
3. Data Types — PostgreSQL
Numeric
sql
SMALLINT -- -32K ถึง 32K (2 bytes)
INTEGER / INT -- -2.1B ถึง 2.1B (4 bytes)
BIGINT -- ใหญ่มาก (8 bytes)
-- Auto-increment (เลขที่ DB นับเองทีละ 1)
BIGINT GENERATED ALWAYS AS IDENTITY -- ⭐ แนะนำ (SQL standard ตั้งแต่ PG 10)
SERIAL -- legacy — INT + auto-increment
BIGSERIAL -- legacy — BIGINT + auto-increment
NUMERIC(10, 2) -- ตัวเลขแม่นยำ (money)
DECIMAL(10, 2) -- alias ของ NUMERIC
REAL -- float (4 bytes, ไม่แม่นยำ)
DOUBLE PRECISION -- float (8 bytes, ไม่แม่นยำ)⚠️ อย่าใช้ FLOAT/REAL กับเงิน — FLOAT เก็บเป็น binary ทำให้ทศนิยมบางตัวเก็บไม่ตรงเป๊ะ เช่น
0.1 + 0.2 = 0.30000000000000004(ไม่ใช่ 0.3 พอดี) → ใช้กับเงินผิดทันที → ใช้ NUMERIC แทน (เก็บแบบ decimal แม่นยำ)
String
sql
VARCHAR(n) -- string ≤ n chars
CHAR(n) -- string เสมอ n chars (pad space)
TEXT -- ไม่จำกัด — ใช้ในทั่วไป (เร็วเท่ากัน)ใน PostgreSQL —
TEXTกับVARCHAR(n)ความเร็วเท่ากัน — ใช้ TEXT + CHECK constraint ก็ได้💡 มี
CITEXT(case-insensitive text — extensioncitext) สำหรับ column ที่ต้องเทียบโดยไม่สนตัวพิมพ์เล็ก/ใหญ่ เช่น email/username — เก็บแบบเดิมแต่ compare แบบ lowercase
Date/Time
sql
DATE -- 2026-05-18
TIME -- 14:30:25
TIMESTAMP -- 2026-05-18 14:30:25 (ไม่มี timezone — เสี่ยง bug)
TIMESTAMPTZ -- 2026-05-18 14:30:25+07 (มี timezone — ใช้ตัวนี้!)
INTERVAL -- ช่วงเวลา '2 hours 30 minutes'Boolean
sql
BOOLEAN -- TRUE / FALSE / NULLJSON
sql
JSON -- เก็บ raw text — ingest เร็ว, query ช้า (ไม่ index ได้, ต้อง parse ทุกครั้ง) + เก็บ key order/whitespace/key ซ้ำตามที่ใส่
JSONB -- binary format — query เร็ว / index ได้, ingest ช้ากว่าเล็กน้อย (ต้อง parse + เรียบเรียงตอน insert) ⭐↪️ JSONB ทำได้มากกว่านี้เยอะ (query เข้าไปใน object, index, full-text) — เจาะลึกใน บทที่ 7
UUID
UUID (Universally Unique Identifier) = ตัวเลขสุ่ม unique 128-bit ที่เกือบเป็นไปไม่ได้ที่จะชนกัน — ใช้แทน id แบบเลขเรียง (1, 2, 3) เมื่อ:
- ต้องรวม data ข้ามระบบ (id เรียงคนละชุดจะชนกัน)
- ไม่อยาก expose จำนวน record ออกไป (เห็น id = 1234 รู้เลยว่ามี user ~1234 คน)
sql
UUID -- 550e8400-e29b-41d4-a716-446655440000 (รูปแบบ string 36 ตัวอักษร)
-- สร้าง UUID (PG 13+) — ใช้ฟังก์ชัน built-in ไม่ต้องลง extension
SELECT gen_random_uuid();💡 gen_random_uuid() vs uuid_generate_v4() — รุ่นเก่าใช้
CREATE EXTENSION "uuid-ossp"; SELECT uuid_generate_v4();ตั้งแต่ PG 13 มีgen_random_uuid()built-in ไม่ต้องลง extension แล้ว — ใช้ตัวนี้เป็นหลัก💡 UUIDv7 (RFC 9562, May 2024) — UUID รุ่นใหม่ที่ฝัง timestamp ไว้ในตัว ทำให้ "เรียงตามเวลา" ได้ → index ใน B-tree เร็วกว่า UUIDv4 (ที่สุ่มล้วน) — สำหรับระบบใหม่ที่สร้าง UUID เยอะ พิจารณา UUIDv7 (PG ยังไม่มี built-in function ต้องใช้ extension เช่น
pg_uuidv7หรือสร้างจากฝั่ง app)
Array
sql
INTEGER[] -- array ของ integer
TEXT[] -- array ของ text
-- ตัวอย่างต้องมี table ก่อน — สร้าง tags ที่มี column ชื่อ tag_values (เก็บ array ของ integer)
CREATE TABLE tags (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_values INTEGER[] -- column ชื่อ tag_values เก็บ array
);
-- ใช้
INSERT INTO tags (tag_values) VALUES ('{1, 2, 3}'); -- '{1,2,3}' = array literal ของ Postgres
SELECT * FROM tags WHERE 2 = ANY(tag_values); -- ANY(tag_values) = "มีค่า 2 อยู่ใน array ไหม"💡 PostgreSQL array literal = string ที่ขึ้นต้นด้วย
{และปิดด้วย}คั่นด้วย comma — เช่น'{1,2,3}'= array 3 element,'{"a","b"}'= text array 2 element⚠️ อย่าตั้งชื่อ column ทับ reserved word — เช่น
values,user,order,selectเป็นต้น Postgres อาจอนุญาตในบาง position แต่ fragile + งง —INSERT INTO tags (values) VALUES (...)จริง ๆ parse ไม่ผ่านเพราะvaluesติด keywordVALUES(ถ้าจำเป็นต้องใช้ ต้อง quote:"values") ตั้งชื่ออื่นดีกว่า เช่นtag_values
Enum
sql
CREATE TYPE order_status AS ENUM ('PENDING', 'PAID', 'SHIPPED', 'CANCELLED');
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
status order_status NOT NULL DEFAULT 'PENDING'
);4. Constraints — บังคับ rule
sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER CHECK (age >= 13 AND age <= 120),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
parent_id INTEGER REFERENCES users(id),
CONSTRAINT email_lowercase CHECK (email = LOWER(email))
);| Constraint | ทำอะไร |
|---|---|
NOT NULL | ห้าม null |
UNIQUE | ห้ามซ้ำ |
PRIMARY KEY | unique + not null + เป็น index |
DEFAULT x | ค่าถ้าไม่ใส่ |
CHECK (...) | condition ที่ต้อง true |
REFERENCES other(col) | foreign key |
Named Constraint
sql
ALTER TABLE users
ADD CONSTRAINT users_email_lowercase CHECK (email = LOWER(email));
ALTER TABLE users DROP CONSTRAINT users_email_lowercase;5. ALTER TABLE — เปลี่ยน schema
sql
-- เพิ่ม column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- เปลี่ยน type
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
-- เพิ่ม default
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'ACTIVE';
-- ลบ default
ALTER TABLE users ALTER COLUMN status DROP DEFAULT;
-- Rename column
ALTER TABLE users RENAME COLUMN phone TO phone_number;
-- Rename table
ALTER TABLE users RENAME TO accounts;
-- ลบ column
ALTER TABLE users DROP COLUMN phone_number;
-- เพิ่ม constraint
ALTER TABLE users ADD CONSTRAINT age_positive CHECK (age > 0);⚠️ Production: เปลี่ยน schema ระวัง — table ใหญ่ ALTER อาจ lock นาน → ใช้ migration tool (Flyway, Liquibase)
6. DROP — ลบ
DROP ลบทั้ง table (schema + data) ส่วน TRUNCATE ลบแค่ data เหลือ schema — ⚠️ ทั้งคู่ไม่มี undo ดังนั้นใน production ต้องระวังมาก (ใช้ IF EXISTS กัน error และเข้าใจผลของ CASCADE ก่อนรัน):
sql
DROP TABLE users; -- ลบ table
DROP TABLE IF EXISTS users; -- ไม่ error ถ้าไม่มี
DROP TABLE users CASCADE; -- ลบ + ลบ FK ที่อ้างถึง
TRUNCATE TABLE users; -- ลบ data ทั้งหมด (เก็บ schema)
TRUNCATE TABLE users RESTART IDENTITY; -- reset SERIAL counter ด้วย⚠️ DROP/TRUNCATE ไม่มี undo — ใช้ใน production ระวัง
7. INSERT — เพิ่มข้อมูล
INSERT เพิ่มข้อมูลลง table — ควรระบุ column เสมอ (ไม่พังเมื่อเพิ่ม column ใหม่), insert หลาย row ในคำสั่งเดียวเพื่อความเร็ว, RETURNING เพื่อดูค่าที่ DB สร้าง (เช่น id) และ ON CONFLICT (upsert) สำหรับ "เพิ่มหรืออัปเดตถ้ามีอยู่แล้ว":
sql
-- แบบ 1: insert ทุก column ตามลำดับ (ไม่แนะนำ — เปราะ + เสี่ยงชน sequence)
INSERT INTO users VALUES (1, 'a@b.com', 'Anna', 25, ...);
-- ⚠️ ระบุ id = 1 เองทับ SERIAL/IDENTITY — sequence ไม่รู้เรื่อง ครั้งต่อไป insert จะพยายามให้ id = 1 อีก
-- เกิด unique violation → ปล่อยให้ DB gen เอง (ละ id ทิ้งจาก column list) ดีกว่า
-- แบบ 2: ระบุ column (แนะนำ — ไม่พังถ้าเพิ่ม column ใหม่ + ปล่อย id ให้ DB gen)
INSERT INTO users (email, name, age) VALUES ('a@b.com', 'Anna', 25);
-- หลาย row พร้อมกัน
INSERT INTO users (email, name, age) VALUES
('a@b.com', 'Anna', 25),
('b@c.com', 'Ben', 30),
('c@d.com', 'Carol', 28);
-- Insert + RETURNING (ดู id ที่สร้าง)
INSERT INTO users (email, name, age) VALUES ('d@e.com', 'Dan', 22)
RETURNING id, created_at;
-- Insert จากผล SELECT (คัดลอกข้อมูลจาก query มาใส่)
INSERT INTO users_archive (email, name)
SELECT email, name FROM users WHERE created_at < '2024-01-01';
-- Upsert (อัพ-เซิร์ท = insert + update ถ้ามีอยู่แล้วก็ update แทน)
INSERT INTO users (email, name) VALUES ('a@b.com', 'Anna Updated')
ON CONFLICT (email) -- "ถ้าชน unique constraint ที่ email"
DO UPDATE SET name = EXCLUDED.name; -- EXCLUDED = "แถวที่พยายาม insert เข้ามา (ที่ชน)"
-- ถ้ามี → ไม่ทำอะไร
INSERT INTO users (email, name) VALUES ('a@b.com', 'Anna')
ON CONFLICT (email) DO NOTHING;⚠️
ON CONFLICT (col)ต้องมี UNIQUE/PRIMARY KEY constraint บนcol— ถ้าตารางไม่มี unique constraint บน column ที่ระบุ จะ errorthere is no unique or exclusion constraint matching the ON CONFLICT specificationตัวอย่างusers.emailในบทนี้มีUNIQUEแล้ว → ใช้ได้
8. SELECT — ดึงข้อมูล
SELECT คือคำสั่งที่ใช้บ่อยที่สุด — เลือก column, ตั้ง alias, คำนวณ และที่สำคัญคือ WHERE สำหรับกรองแถว เริ่มจากพื้นฐานนี้ให้แม่นก่อน เพราะทุกอย่างต่อจากนี้ (JOIN, aggregate) ล้วนต่อยอดจาก SELECT:
Basic
sql
-- ทุก column ทุก row
SELECT * FROM users;
-- เลือก column
SELECT id, email, name FROM users;
-- เปลี่ยนชื่อ (alias)
SELECT id, name AS full_name FROM users;
-- คำนวณ
SELECT name, age, age + 10 AS age_in_10_years FROM users;
SELECT name, EXTRACT(YEAR FROM created_at) AS join_year FROM users;WHERE — filter
sql
SELECT * FROM users WHERE age > 18;
SELECT * FROM users WHERE email = 'a@b.com';
SELECT * FROM users WHERE name LIKE 'A%'; -- ขึ้นต้นด้วย A
SELECT * FROM users WHERE name ILIKE '%anna%'; -- ไม่สนตัวพิมพ์เล็ก/ใหญ่ (case-insensitive)
SELECT * FROM users WHERE age BETWEEN 20 AND 30;
SELECT * FROM users WHERE age IN (20, 25, 30);
SELECT * FROM users WHERE name IS NULL;
SELECT * FROM users WHERE name IS NOT NULL;
SELECT * FROM users WHERE age > 18 AND status = 'ACTIVE';
SELECT * FROM users WHERE age < 18 OR age > 65;
SELECT * FROM users WHERE NOT (status = 'INACTIVE');Operators
sql
= != <> < > <= >=
LIKE / ILIKE -- จับคู่ pattern (รูปแบบข้อความ)
% -- แทนตัวอักษร 0 ตัวขึ้นไป
_ -- แทนตัวอักษร 1 ตัวพอดี
IN (...) -- อยู่ใน list ที่กำหนด
BETWEEN a AND b
IS NULL / IS NOT NULL
AND / OR / NOT⚠️ NULL ใช้ IS NULL ไม่ใช่ =
sql
WHERE name = NULL -- ❌ ผิดเสมอ (return NULL/unknown → WHERE ถือว่า "ไม่ match" → ผลลัพธ์ว่าง)
WHERE name IS NULL -- ✅ทำไม? — NULL = "ไม่รู้ค่า" → unknown = unknown ผลก็ unknown (ไม่ใช่ true ไม่ใช่ false) — เปรียบเทียบไทย: "ไม่รู้ = ไม่รู้ → เรารู้ไหมว่าเท่ากัน? ก็ไม่รู้" WHERE จะ keep เฉพาะ row ที่เป็น TRUE เท่านั้น — unknown ถือว่า "ไม่ match" → ผลลัพธ์ว่าง
9. ORDER BY + LIMIT + OFFSET
sql
-- เรียง
SELECT * FROM users ORDER BY age; -- น้อย → มาก (default)
SELECT * FROM users ORDER BY age DESC; -- มาก → น้อย
SELECT * FROM users ORDER BY age DESC, name; -- multi-column
-- Top N
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- Pagination (page 1-indexed, size 10 — page 3 = ข้าม 2 page แรก = OFFSET 20)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20; -- page 3 (1-indexed, size 10)⚠️ OFFSET ใหญ่ ๆ ช้า —
OFFSET 1000000ต้อง scan + ทิ้ง 1M row → ใช้ keyset pagination แทน (บทที่ 4)
10. DISTINCT — ลบซ้ำ
DISTINCT กรองแถวที่ซ้ำกันออก เหลือเฉพาะค่าที่ไม่ซ้ำ — ใช้กับ column เดียว (ค่า unique) หรือหลาย column (combination ที่ไม่ซ้ำ) มีประโยชน์ตอนอยากรู้ว่ามีค่าอะไรบ้างในคอลัมน์:
sql
-- unique values
SELECT DISTINCT category FROM products;
-- unique combination
SELECT DISTINCT category, brand FROM products;11. UPDATE — แก้ไข
UPDATE แก้ข้อมูลที่มีอยู่ — ⚠️ กฎเหล็กคือ "อย่าลืม WHERE" เพราะลืมเมื่อไหร่จะอัปเดตทุกแถวในตาราง! ใน production ควรครอบด้วย transaction (BEGIN → ดู row count → COMMIT/ROLLBACK) เพื่อกันพลาด:
sql
-- ⚠️ มี WHERE = update เฉพาะ row ที่ตรงเงื่อนไข (ลืม WHERE = โดนทุก row!)
UPDATE users SET status = 'INACTIVE' WHERE last_login < '2024-01-01';
-- หลาย column
UPDATE users
SET name = 'Anna B.',
age = 26,
updated_at = NOW()
WHERE id = 1;
-- จาก expression
UPDATE products SET price = price * 1.1 WHERE category = 'BOOK';
-- RETURNING
UPDATE orders SET status = 'PAID' WHERE id = 10
RETURNING id, total;
-- จาก subquery
UPDATE users
SET status = 'PREMIUM'
WHERE id IN (
SELECT user_id FROM orders WHERE total > 1000
);⚠️ Safety — ใช้ Transaction
💡 คำสั่ง transaction (บทที่ 5 จะลึก) — ที่นี่แค่เป็น "safety trick" ก่อน:
BEGIN= เริ่ม transaction (กลุ่มคำสั่งที่ทำพร้อมกัน)COMMIT= ยืนยัน — บันทึกผลถาวรROLLBACK= ยกเลิก — ย้อนทุกการเปลี่ยนแปลงตั้งแต่ BEGIN
sql
BEGIN;
UPDATE products SET price = price * 1.1 WHERE category = 'BOOK';
-- ดู row count, ถ้าผิด:
ROLLBACK;
-- ถ้าถูก:
COMMIT;12. DELETE — ลบ
DELETE ลบแถวตามเงื่อนไข — เหมือน UPDATE คือลืม WHERE แล้วลบหมดทั้งตาราง การลบทั้งตารางด้วย DELETE ช้า (ทำทีละแถว + log) ถ้าต้องการ clear data ทั้งหมดให้ใช้ TRUNCATE ที่เร็วกว่ามาก:
sql
-- ลบเฉพาะที่
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE status = 'INACTIVE' AND last_login < '2020-01-01';
-- RETURNING
DELETE FROM users WHERE id = 1 RETURNING *;
-- ⚠️ ลบทั้ง table (slow — ทำ row by row + log)
DELETE FROM users;
-- เร็วกว่า (skip log + reset auto-increment) — ใช้เมื่อ clear test data
TRUNCATE TABLE users RESTART IDENTITY;13. ตัวอย่างจริง — Bookstore
มารวมทุกคำสั่งในบท (CREATE/INSERT/SELECT/UPDATE/DELETE) เป็นตัวอย่างจริง — ร้านหนังสือที่มีตาราง authors/books พร้อมข้อมูลและ query ใช้งาน ลองพิมพ์ตามทีละคำสั่งจะเข้าใจ SQL พื้นฐานครบ:
sql
-- Schema
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
country VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title VARCHAR(300) NOT NULL,
author_id INTEGER NOT NULL REFERENCES authors(id),
isbn VARCHAR(20) UNIQUE,
price NUMERIC(8, 2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
published_year INTEGER,
category VARCHAR(50),
is_available BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Data
INSERT INTO authors (name, country) VALUES
('George Orwell', 'UK'),
('Haruki Murakami', 'Japan'),
('Margaret Atwood', 'Canada');
INSERT INTO books (title, author_id, isbn, price, stock, published_year, category) VALUES
('1984', 1, '978-0-452-28423-4', 12.99, 50, 1949, 'FICTION'),
('Animal Farm', 1, '978-0-452-28424-1', 9.99, 30, 1945, 'FICTION'),
('Norwegian Wood', 2, '978-0-375-70402-4', 15.99, 20, 1987, 'FICTION'),
('Kafka on the Shore', 2, '978-1-400-07927-8', 17.99, 0, 2002, 'FICTION'),
('The Handmaid''s Tale', 3, '978-0-385-49081-8', 14.99, 15, 1985, 'FICTION');
-- Queries
-- 1. หนังสือทั้งหมดที่ราคา < 15
SELECT title, price FROM books WHERE price < 15 ORDER BY price;
-- 2. หนังสือที่ในสต็อก + เรียงตามปีพิมพ์
SELECT title, published_year, stock
FROM books
WHERE stock > 0
ORDER BY published_year DESC;
-- 3. ค้นชื่อมี "the"
SELECT title FROM books WHERE title ILIKE '%the%';
-- 4. หนังสือที่หมดสต็อก
SELECT title FROM books WHERE stock = 0;
-- 5. อัพเดทราคา +10% สำหรับหนังสือก่อนปี 1990
UPDATE books SET price = ROUND(price * 1.10, 2) WHERE published_year < 1990;
-- 6. ลบหนังสือ unavailable + stock 0
DELETE FROM books WHERE is_available = FALSE AND stock = 0;14. ⚠️ จุดที่พลาดบ่อย (Common Pitfalls)
"Pitfall" (พิทฟอลล์) = หลุมพราง / จุดที่คนทั่วไปพลาดบ่อย — ในเล่มนี้จะใช้คำนี้สม่ำเสมอ
| จุดที่พลาดบ่อย (Pitfall) | แก้ |
|---|---|
WHERE name = NULL | WHERE name IS NULL |
| UPDATE/DELETE ไม่มี WHERE | ใช้ transaction + ดู count ก่อน |
SELECT * ทุกที่ | ระบุ column — ลด data + clear intent |
String concat (ต่อ string — ออกเสียง "คอน-แค็ท" ย่อจาก concatenate) ผ่าน + | PostgreSQL ใช้ || หรือ CONCAT() — หมายเหตุ: ใน SQL || คือต่อ string ไม่ใช่ OR (OR ใช้คำว่า OR ตรง ๆ) |
เทียบ String แบบ case-sensitive ('Anna' ≠ 'anna') | ใช้ ILIKE หรือ LOWER() |
| ใช้ TIMESTAMP ไม่มี timezone | ใช้ TIMESTAMPTZ |
| ใช้ FLOAT กับเงิน | ใช้ NUMERIC |
| ลืม PRIMARY KEY | ทุก table ควรมี PK |
| Forget RETURNING | INSERT/UPDATE/DELETE มี RETURNING ใช้ได้ |
15. Checkpoint
🛠️ Checkpoint 1.1 — Library Schema
ออกแบบ + สร้าง schema สำหรับห้องสมุด:
members(id, email, name, joined_at)books(id, title, author, isbn, copies_total, copies_available)loans(id, member_id, book_id, borrowed_at, returned_at)
ใส่ constraint ที่เหมาะสม
🛠️ Checkpoint 1.2 — CRUD Practice
ใน table books:
- Insert 20 books (หลาย category, ราคาต่าง)
- Update ราคา discount 20% สำหรับ category 'FICTION'
- Delete books ที่ stock = 0 และอายุ > 30 ปี
- Find: 10 books ราคาแพงสุด + ชื่อมี "the"
🛠️ Checkpoint 1.3 — Tricky NULL
ลอง:
sql
SELECT * FROM books WHERE published_year != 2020;ถ้า published_year มี NULL — เกิดอะไรขึ้น? อธิบาย + แก้
16. สรุปบท
✅ DDL = CREATE / ALTER / DROP / TRUNCATE
✅ DML = SELECT / INSERT / UPDATE / DELETE
✅ Data types ที่ใช้บ่อย: SERIAL, VARCHAR, TEXT, INTEGER, NUMERIC, BOOLEAN, TIMESTAMPTZ, JSONB
✅ Constraints: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY KEY, REFERENCES
✅ NULL handling: IS NULL ไม่ใช่ = NULL
✅ INSERT ระบุ column เสมอ + ใช้ RETURNING
✅ UPDATE/DELETE ใช้ transaction safety
✅ TIMESTAMPTZ (with timezone) > TIMESTAMP
✅ NUMERIC > FLOAT สำหรับเงิน