คู่มือ SQL ฉบับเร่งรัด

จากศูนย์สู่มืออาชีพ — เนื้อหาครบทั้งทฤษฎีและปฏิบัติ พร้อม Cheatsheet อ้างอิงครบทุกคำสั่ง

4 บทเรียน SQLite / MySQL / PostgreSQL ภาษาไทย
1

พื้นฐาน SQL คืออะไร?

แนวคิดหลัก, ประเภทข้อมูล, Key, NULL

SQL คืออะไร?

SQL ย่อมาจาก Structured Query Language — ภาษาสำหรับ "คุยกับฐานข้อมูล" ลองนึกว่าฐานข้อมูลคือ Excel ขนาดยักษ์ที่มีหลายแผ่น (หลายตาราง) SQL คือสิ่งที่ช่วยให้เรา:

สิ่งที่ทำได้ตัวอย่าง
ดึงข้อมูลแสดงรายชื่อลูกค้าทั้งหมด
เพิ่มข้อมูลเพิ่มลูกค้าใหม่เข้าระบบ
แก้ไขข้อมูลเปลี่ยนที่อยู่ของลูกค้า
ลบข้อมูลลบออเดอร์ที่ยกเลิกแล้ว
วิเคราะห์ข้อมูลยอดขายรวมเดือนนี้เท่าไร?
ใช้กับฐานข้อมูลอะไรได้บ้าง?
SQL ใช้ได้กับ MySQL, PostgreSQL, SQLite, SQL Server, Oracle — ไวยากรณ์เหมือนกันมากกว่า 90% คู่มือนี้ใช้ SQLite เป็นหลัก

แนวคิดหลัก — ตาราง, แถว, คอลัมน์

ข้อมูลใน SQL จัดเก็บในรูป ตาราง (Table) เหมือน Excel:

employee_idnamedepartmentsalary
1สมชาย ใจดีIT55000
2สมหญิง รักHR48000
3วิทยา เก่งIT62000
องค์ประกอบอธิบายตัวอย่าง
ตาราง (Table)กล่องเก็บข้อมูลemployees
คอลัมน์ (Column)หัวข้อ / ประเภทข้อมูลname, salary
แถว (Row)ข้อมูล 1 รายการพนักงาน 1 คน

ประเภทคำสั่ง SQL

กลุ่มย่อว่าคำสั่งหลักใช้ทำอะไร
Data Query LanguageDQLSELECTดึงข้อมูล
Data Definition LanguageDDLCREATE, ALTER, DROPสร้าง/แก้/ลบโครงสร้าง
Data Manipulation LanguageDMLINSERT, UPDATE, DELETEเพิ่ม/แก้/ลบข้อมูล
Data Control LanguageDCLGRANT, REVOKEจัดการสิทธิ์

ประเภทข้อมูล (Data Types)

ประเภทตัวอย่างใช้เก็บอะไร
INTEGER1, 42, -5เลขจำนวนเต็ม (รหัส, จำนวน)
REAL / FLOAT3.14, 99.99เลขทศนิยม (ราคา, น้ำหนัก)
TEXT / VARCHAR(n)"สมชาย", "BKK"ข้อความ
DATE2024-01-15วันที่
DATETIME2024-01-15 09:30:00วันที่และเวลา
BOOLEANTRUE / FALSE (หรือ 1/0)ค่าจริง/เท็จ

Key — ตัวระบุข้อมูล

Primary Key (PK) — ระบุตัวตนของแต่ละแถวอย่างเฉพาะเจาะจง ห้ามซ้ำ ห้ามว่าง เช่น รหัสพนักงาน

employee_id INTEGER PRIMARY KEY AUTOINCREMENT

Foreign Key (FK) — เชื่อมโยงตารางหนึ่งไปยังอีกตาราง ป้องกันข้อมูลอ้างอิงที่ไม่มีอยู่จริง

customer_id INTEGER REFERENCES customers(customer_id)

NULL คืออะไร?

NULL หมายถึง "ไม่มีค่า" หรือ "ยังไม่รู้" — ไม่เหมือน เลขศูนย์ (0) หรือสตริงว่าง ("")

ระวัง!
เมื่อเปรียบเทียบกับ NULL ต้องใช้ IS NULL หรือ IS NOT NULL เท่านั้น — ห้ามใช้ = NULL
-- ถูก
WHERE phone IS NULL

-- ผิด (ไม่ได้ผลลัพธ์ที่ต้องการ)
WHERE phone = NULL

🎯 แบบทดสอบส่วนที่ 1

ข้อ 1. SQL ย่อมาจากอะไร? ใช้ทำอะไร?
ข้อ 2. ตาราง, คอลัมน์, แถว — แต่ละอย่างคืออะไร?
ข้อ 3. PRIMARY KEY คืออะไร? ต่างจาก UNIQUE อย่างไร?
ข้อ 4. ต้องการเก็บ "ยอดขาย 12500.50 บาท" ควรใช้ประเภทข้อมูลใด?
(ก) TEXT   (ข) INTEGER   (ค) REAL   (ง) DATE
ข้อ 5. คำสั่งใดถูกต้อง?
(ก) WHERE phone = NULL   (ข) WHERE phone IS NULL   (ค) WHERE phone == NULL
ดูเฉลย

1. Structured Query Language — ภาษาสำหรับสื่อสารกับฐานข้อมูล ทั้งดึง เพิ่ม แก้ไข และลบข้อมูล

2. ตาราง = กล่องเก็บข้อมูล, คอลัมน์ = หัวข้อ/ประเภท, แถว = ข้อมูล 1 รายการ

3. PRIMARY KEY ระบุตัวตนเฉพาะ ห้ามซ้ำ ห้าม NULL มีได้คอลัมน์เดียว / UNIQUE ห้ามซ้ำแต่อนุญาต NULL มีได้หลายคอลัมน์

4. (ค) REAL

5. (ข) WHERE phone IS NULL


2

สร้างตารางข้อมูล (CREATE TABLE)

ฐานข้อมูลร้านอาหาร — 6 ตารางพร้อมข้อมูลตัวอย่าง

โครงสร้างคำสั่ง CREATE TABLE

CREATE TABLE ชื่อตาราง (
    ชื่อคอลัมน์1  ประเภทข้อมูล  [ข้อจำกัด],
    ชื่อคอลัมน์2  ประเภทข้อมูล  [ข้อจำกัด],
    ...
);

ข้อจำกัด (Constraints)

Constraintความหมาย
PRIMARY KEYระบุตัวตนเฉพาะ ห้ามซ้ำ ห้ามว่าง
NOT NULLห้ามว่าง ต้องมีค่าเสมอ
UNIQUEห้ามซ้ำ (อนุญาต NULL ได้)
DEFAULT valueค่าเริ่มต้นถ้าไม่ระบุ
CHECK (เงื่อนไข)ตรวจสอบค่าที่ใส่เข้ามา
REFERENCES table(col)Foreign Key — เชื่อมกับตารางอื่น
AUTOINCREMENTเพิ่มตัวเลขอัตโนมัติ

ตัวอย่างตาราง — ระบบร้านอาหาร

ตาราง 1 — พนักงาน (employees)

CREATE TABLE employees (
    employee_id   INTEGER PRIMARY KEY AUTOINCREMENT,
    first_name    TEXT    NOT NULL,
    last_name     TEXT    NOT NULL,
    email         TEXT    UNIQUE NOT NULL,
    phone         TEXT,                           -- ไม่บังคับ → อนุญาต NULL
    department    TEXT    NOT NULL DEFAULT 'General',
    position      TEXT    NOT NULL,
    salary        REAL    NOT NULL CHECK (salary > 0),
    hire_date     DATE    NOT NULL,
    is_active     INTEGER DEFAULT 1               -- 1=ทำงานอยู่, 0=ลาออก
);

ตาราง 2 — เมนูอาหาร (menu_items)

CREATE TABLE menu_items (
    item_id       INTEGER PRIMARY KEY AUTOINCREMENT,
    item_name     TEXT    NOT NULL,
    category      TEXT    NOT NULL,
    price         REAL    NOT NULL CHECK (price >= 0),
    cost          REAL    NOT NULL CHECK (cost >= 0),
    is_available  INTEGER DEFAULT 1,
    description   TEXT
);

ตาราง 3 — ลูกค้า (customers)

CREATE TABLE customers (
    customer_id   INTEGER PRIMARY KEY AUTOINCREMENT,
    first_name    TEXT    NOT NULL,
    last_name     TEXT    NOT NULL,
    email         TEXT    UNIQUE,
    phone         TEXT,
    birth_date    DATE,
    member_since  DATE    NOT NULL,
    member_level  TEXT    DEFAULT 'Bronze'
                          CHECK (member_level IN ('Bronze', 'Silver', 'Gold')),
    total_spent   REAL    DEFAULT 0
);

ตาราง 4 — ออเดอร์ (orders) พร้อม Foreign Key

CREATE TABLE orders (
    order_id      INTEGER PRIMARY KEY AUTOINCREMENT,
    customer_id   INTEGER NOT NULL REFERENCES customers(customer_id),
    employee_id   INTEGER NOT NULL REFERENCES employees(employee_id),
    order_date    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    table_number  INTEGER,
    status        TEXT    DEFAULT 'Pending'
                          CHECK (status IN ('Pending','Preparing','Served','Cancelled')),
    total_amount  REAL    DEFAULT 0,
    notes         TEXT
);

ตาราง 5 — รายการในออเดอร์ (order_items)

CREATE TABLE order_items (
    order_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id      INTEGER NOT NULL REFERENCES orders(order_id),
    item_id       INTEGER NOT NULL REFERENCES menu_items(item_id),
    quantity      INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0),
    unit_price    REAL    NOT NULL CHECK (unit_price >= 0)
);

INSERT — เพิ่มข้อมูล

-- รูปแบบมาตรฐาน (แนะนำ — ระบุคอลัมน์ชัดเจน)
INSERT INTO ชื่อตาราง (คอลัมน์1, คอลัมน์2, ...)
VALUES (ค่า1, ค่า2, ...);

-- เพิ่มหลายแถวพร้อมกัน
INSERT INTO ชื่อตาราง (คอลัมน์1, คอลัมน์2)
VALUES
    (ค่า1a, ค่า2a),
    (ค่า1b, ค่า2b),
    (ค่า1c, ค่า2c);

เพิ่มพนักงานตัวอย่าง

INSERT INTO employees (first_name, last_name, email, phone, department, position, salary, hire_date)
VALUES
    ('สมชาย',  'ใจดี',    'somchai@rest.com',  '081-111-1111', 'Kitchen',    'เชฟ',          35000, '2020-03-15'),
    ('สมหญิง', 'รักงาน',  'somying@rest.com',  '082-222-2222', 'Service',    'พนักงานเสิร์ฟ', 22000, '2021-06-01'),
    ('วิทยา',  'เก่งมาก', 'wittaya@rest.com',  '083-333-3333', 'Kitchen',    'ผู้ช่วยเชฟ',   28000, '2021-09-10'),
    ('นิดา',   'สวยงาม',  'nida@rest.com',     '084-444-4444', 'Service',    'พนักงานเสิร์ฟ', 22000, '2022-01-20'),
    ('มานะ',   'บากบั่น', 'mana@rest.com',     NULL,           'Management', 'ผู้จัดการ',     55000, '2019-07-01'),
    ('กมล',    'เหนียวแน่','kamol@rest.com',   '086-666-6666', 'Service',    'แคชเชียร์',    25000, '2022-04-15'),
    ('ประภา',  'ฉลาด',    'prapa@rest.com',    '087-777-7777', 'Kitchen',    'เชฟ',          38000, '2020-11-01');

เพิ่มเมนูอาหาร

INSERT INTO menu_items (item_name, category, price, cost, description)
VALUES
    ('ข้าวผัดกุ้ง',       'อาหารจานหลัก',     120, 45, 'ข้าวผัดกุ้งสดกับไข่ดาว'),
    ('ต้มยำกุ้ง',         'อาหารจานหลัก',     180, 70, 'ต้มยำน้ำข้นรสเข้มข้น'),
    ('ผัดไทยกุ้งสด',     'อาหารจานหลัก',     150, 55, 'ผัดไทยเส้นเล็กกุ้งสด'),
    ('ส้มตำไทย',           'อาหารเรียกน้ำย่อย', 60, 20, 'ส้มตำรสจัดจ้าน'),
    ('น้ำเปล่า',           'เครื่องดื่ม',        15, 3,  NULL),
    ('น้ำอัดลม',           'เครื่องดื่ม',        35, 12, NULL),
    ('ชาเย็น',             'เครื่องดื่ม',        45, 15, 'ชาไทยใส่นมข้นหวาน'),
    ('ไอศกรีมกะทิ',       'ของหวาน',           65, 22, 'ไอศกรีมกะทิราดน้ำตาลมะพร้าว'),
    ('มะม่วงข้าวเหนียว', 'ของหวาน',           120, 45, 'มะม่วงน้ำดอกไม้ข้าวเหนียวมูน'),
    ('ลาบหมู',             'อาหารจานหลัก',     110, 42, 'ลาบหมูสับสมุนไพรหอม');

แก้ไขโครงสร้างตาราง (ALTER TABLE)

-- เพิ่มคอลัมน์ใหม่
ALTER TABLE customers ADD COLUMN line_id TEXT;

-- เปลี่ยนชื่อคอลัมน์
ALTER TABLE employees RENAME COLUMN phone TO phone_number;

-- เปลี่ยนชื่อตาราง
ALTER TABLE old_name RENAME TO new_name;

-- ลบตาราง (ระวัง! ถาวร)
DROP TABLE IF EXISTS table_name;

-- ล้างข้อมูลแต่เก็บโครงสร้าง
DELETE FROM table_name;

🎯 แบบทดสอบส่วนที่ 2

ข้อ 1. เขียนคำสั่งสร้างตาราง suppliers ที่มี: supplier_id (PK, auto), company_name (ไม่ซ้ำ, ห้ามว่าง), contact_name, phone, rating (1–5, default 3)
ข้อ 2. UNIQUE ต่างจาก PRIMARY KEY อย่างไร?
ข้อ 3. เขียน INSERT เพิ่มเมนู "กาแฟร้อน" ราคา 55 บาท ต้นทุน 18 บาท หมวด "เครื่องดื่ม"
ข้อ 4. ทำไม orders ต้องมี REFERENCES customers(customer_id)?
ดูเฉลย
-- ข้อ 1
CREATE TABLE suppliers (
    supplier_id   INTEGER PRIMARY KEY AUTOINCREMENT,
    company_name  TEXT    NOT NULL UNIQUE,
    contact_name  TEXT,
    phone         TEXT,
    rating        INTEGER DEFAULT 3 CHECK (rating BETWEEN 1 AND 5)
);

ข้อ 2. UNIQUE ห้ามซ้ำแต่อนุญาต NULL ได้หลายแถว, มีได้หลายคอลัมน์ต่อตาราง / PRIMARY KEY ห้ามซ้ำและห้าม NULL เด็ดขาด มีได้คอลัมน์เดียว

-- ข้อ 3
INSERT INTO menu_items (item_name, category, price, cost)
VALUES ('กาแฟร้อน', 'เครื่องดื่ม', 55, 18);

ข้อ 4. เพื่อป้องกันสร้างออเดอร์ให้ลูกค้าที่ไม่มีในระบบ — Foreign Key บังคับให้ customer_id ใน orders ต้องมีอยู่จริงในตาราง customers


3

ดึงและวิเคราะห์ข้อมูล (SELECT)

WHERE, JOIN, GROUP BY, HAVING, Subquery, Functions

SELECT พื้นฐาน

-- ดึงทุกคอลัมน์
SELECT * FROM employees;

-- เลือกเฉพาะคอลัมน์
SELECT first_name, last_name, position, salary
FROM employees;

-- ตั้งชื่อเล่น (Alias)
SELECT
    first_name AS ชื่อ,
    last_name  AS นามสกุล,
    salary     AS เงินเดือน
FROM employees;

-- คำนวณในคอลัมน์
SELECT
    first_name,
    salary,
    salary * 12   AS เงินเดือนต่อปี,
    salary * 0.05 AS โบนัส5เปอร์เซ็น
FROM employees;

WHERE — กรองข้อมูล

ตัวดำเนินการความหมายตัวอย่าง
=เท่ากับdepartment = 'Kitchen'
<> หรือ !=ไม่เท่ากับstatus != 'Cancelled'
> < >= <=เปรียบเทียบsalary > 30000
IN (...)อยู่ใน listdepartment IN ('IT','HR')
BETWEEN a AND bอยู่ในช่วงsalary BETWEEN 25000 AND 50000
LIKE 'pattern'ค้นหาด้วยรูปแบบname LIKE 'สม%'
IS NULL / IS NOT NULLตรวจ NULLphone IS NULL
AND / OR / NOTรวมเงื่อนไขdept='IT' AND salary>50000
-- AND — เงื่อนไขทั้งหมดต้องเป็นจริง
SELECT first_name, department, salary
FROM employees
WHERE department = 'Kitchen' AND salary > 30000;

-- OR — อย่างน้อยหนึ่งเงื่อนไข
SELECT first_name, department
FROM employees
WHERE department = 'Kitchen' OR department = 'Management';

-- IN — เขียนสั้นกว่า OR หลายตัว
SELECT first_name, department
FROM employees
WHERE department IN ('Kitchen', 'Management');

-- BETWEEN — ช่วงค่า (รวมค่าขอบเขตทั้งสอง)
SELECT first_name, salary
FROM employees
WHERE salary BETWEEN 25000 AND 40000;

-- LIKE — % = อักษรใดก็ได้กี่ตัวก็ได้, _ = 1 ตัว
SELECT item_name FROM menu_items WHERE item_name LIKE '%กุ้ง%';
SELECT first_name FROM employees WHERE first_name LIKE 'สม%';
SELECT email FROM employees WHERE email LIKE '%@rest.com';

ORDER BY & LIMIT

-- เรียงจากมากไปน้อย (DESC) หรือน้อยไปมาก (ASC)
SELECT first_name, salary
FROM employees
ORDER BY salary DESC;

-- เรียงหลายคอลัมน์
SELECT first_name, department, salary
FROM employees
ORDER BY department ASC, salary DESC;

-- จำกัดจำนวนผลลัพธ์
SELECT item_name, price
FROM menu_items
ORDER BY price DESC
LIMIT 5;

-- ข้าม N แถวแรก (Pagination)
SELECT item_name, price
FROM menu_items
ORDER BY price DESC
LIMIT 5 OFFSET 5;  -- หน้า 2: แถวที่ 6-10

Aggregate Functions — สรุปข้อมูล

ฟังก์ชันใช้ทำอะไร
COUNT(*)นับจำนวนแถวทั้งหมด
COUNT(col)นับแถวที่ไม่ใช่ NULL
SUM(col)รวมทั้งหมด
AVG(col)ค่าเฉลี่ย
MAX(col)ค่าสูงสุด
MIN(col)ค่าต่ำสุด
SELECT
    COUNT(*)    AS จำนวนพนักงาน,
    AVG(salary) AS เงินเดือนเฉลี่ย,
    MAX(salary) AS เงินเดือนสูงสุด,
    MIN(salary) AS เงินเดือนต่ำสุด,
    SUM(salary) AS ค่าใช้จ่ายรวม
FROM employees;

GROUP BY & HAVING

GROUP BY แยกข้อมูลเป็นกลุ่ม แล้ว Aggregate แต่ละกลุ่ม — เหมือน Pivot Table ใน Excel

ลำดับประมวลผล SQL (สำคัญมาก!)
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
-- จำนวนและเงินเดือนเฉลี่ยแต่ละแผนก
SELECT
    department,
    COUNT(*)    AS จำนวนพนักงาน,
    AVG(salary) AS เงินเดือนเฉลี่ย,
    SUM(salary) AS ค่าใช้จ่ายรวม
FROM employees
GROUP BY department
ORDER BY เงินเดือนเฉลี่ย DESC;

-- ยอดขายแต่ละวัน
SELECT
    DATE(order_date) AS วันที่,
    COUNT(*)          AS จำนวนออเดอร์,
    SUM(total_amount) AS ยอดขายรวม
FROM orders
WHERE status = 'Served'
GROUP BY DATE(order_date)
ORDER BY วันที่;

-- HAVING — กรองหลัง GROUP BY (ต่างจาก WHERE ที่กรองก่อน)
SELECT department, COUNT(*) AS จำนวนพนักงาน
FROM employees
GROUP BY department
HAVING COUNT(*) > 1;   -- เฉพาะแผนกที่มีมากกว่า 1 คน

JOIN — รวมข้อมูลหลายตาราง

-- INNER JOIN — เฉพาะแถวที่มีคู่ทั้งสองตาราง
SELECT
    o.order_id,
    c.first_name || ' ' || c.last_name AS ชื่อลูกค้า,
    o.order_date,
    o.total_amount,
    o.status
FROM orders AS o
INNER JOIN customers AS c ON o.customer_id = c.customer_id;

-- JOIN สามตาราง
SELECT
    o.order_id,
    c.first_name AS ลูกค้า,
    m.item_name  AS เมนู,
    oi.quantity  AS จำนวน,
    oi.quantity * oi.unit_price AS ยอดรวม
FROM order_items AS oi
INNER JOIN orders     AS o ON oi.order_id = o.order_id
INNER JOIN customers  AS c ON o.customer_id = c.customer_id
INNER JOIN menu_items AS m ON oi.item_id = m.item_id;

-- LEFT JOIN — ทุกแถวจากซ้าย แม้ไม่มีคู่ในตารางขวา
SELECT
    c.first_name,
    c.last_name,
    COUNT(o.order_id) AS จำนวนออเดอร์
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
ORDER BY จำนวนออเดอร์ DESC;

Subquery — Query ซ้อน Query

-- ใน WHERE
SELECT first_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- ใน WHERE ด้วย IN
SELECT item_name, price
FROM menu_items
WHERE item_id IN (
    SELECT item_id FROM order_items
    GROUP BY item_id
    HAVING COUNT(*) > 2
);

-- เป็น Derived Table ใน FROM
SELECT dept, avg_sal
FROM (
    SELECT department AS dept, AVG(salary) AS avg_sal
    FROM employees
    GROUP BY department
) AS dept_summary
WHERE avg_sal > 30000;

ฟังก์ชันที่ใช้บ่อย

ข้อความ

SELECT
    UPPER(first_name)                       AS ตัวพิมพ์ใหญ่,
    LOWER(email)                            AS ตัวพิมพ์เล็ก,
    LENGTH(first_name)                      AS ความยาว,
    first_name || ' ' || last_name          AS ชื่อเต็ม,
    SUBSTR(phone, 1, 3)                     AS รหัสต้น,
    TRIM('  text  ')                        AS ตัดช่องว่าง,
    REPLACE(department, 'Kitchen', 'ครัว') AS แปลคำ
FROM employees;

ตัวเลข

SELECT
    ROUND(salary * 1.1, 2) AS หลังขึ้นเงิน,
    ABS(-500)               AS ค่าสมบูรณ์,
    CEIL(3.2)               AS ปัดขึ้น,   -- 4
    FLOOR(3.9)              AS ปัดลง      -- 3
FROM employees;

วันที่ (SQLite)

SELECT
    DATE('now')                        AS วันนี้,
    DATE(order_date)                   AS วันที่ออเดอร์,
    strftime('%Y', order_date)        AS ปี,
    strftime('%m', order_date)        AS เดือน,
    strftime('%Y-%m', order_date)     AS ปี_เดือน,
    DATE('now', '-30 days')            AS 30วันที่แล้ว
FROM orders;

CASE WHEN — เงื่อนไขแบบ if-else

SELECT
    first_name,
    salary,
    CASE
        WHEN salary >= 50000 THEN 'สูง'
        WHEN salary >= 30000 THEN 'กลาง'
        ELSE 'ต่ำ'
    END AS ระดับเงินเดือน
FROM employees;

-- นับจำนวนแบบมีเงื่อนไข (Conditional Aggregation)
SELECT
    COUNT(*)                                            AS ออเดอร์ทั้งหมด,
    COUNT(CASE WHEN status = 'Served'    THEN 1 END)   AS สำเร็จ,
    COUNT(CASE WHEN status = 'Cancelled' THEN 1 END)   AS ยกเลิก,
    SUM(CASE WHEN status = 'Served' THEN total_amount ELSE 0 END) AS ยอดขายรวม
FROM orders;

🎯 แบบทดสอบส่วนที่ 3

ข้อ 1. เขียน query ดูชื่อและราคาเมนูหมวด "ของหวาน" เรียงจากราคามากไปน้อย
ข้อ 2. เขียน query หาพนักงานที่เงินเดือน 20,000–35,000 บาท ในแผนก Service หรือ Kitchen
ข้อ 3. WHERE vs HAVING ต่างกันอย่างไร?
ข้อ 4. เขียน query แสดงชื่อพนักงานพร้อมจำนวนออเดอร์ที่รับผิดชอบ (รวมคนที่ยังไม่มีออเดอร์)
ข้อ 5. INNER JOIN vs LEFT JOIN ต่างกันอย่างไร?
ดูเฉลย
-- ข้อ 1
SELECT item_name, price FROM menu_items
WHERE category = 'ของหวาน' ORDER BY price DESC;

-- ข้อ 2
SELECT first_name, department, salary FROM employees
WHERE salary BETWEEN 20000 AND 35000
  AND department IN ('Service', 'Kitchen');

-- ข้อ 4
SELECT e.first_name, e.last_name, COUNT(o.order_id) AS จำนวนออเดอร์
FROM employees AS e
LEFT JOIN orders AS o ON e.employee_id = o.employee_id
GROUP BY e.employee_id, e.first_name, e.last_name
ORDER BY จำนวนออเดอร์ DESC;

ข้อ 3. WHERE กรองแถวก่อน GROUP BY (กรองข้อมูลดิบ) / HAVING กรองหลัง GROUP BY (กรองผลสรุป)

ข้อ 5. INNER JOIN แสดงเฉพาะแถวที่มีคู่ทั้งสองตาราง / LEFT JOIN แสดงทุกแถวจากตารางซ้าย แม้ไม่มีคู่ในตารางขวา (ค่าจากขวาจะเป็น NULL)


4

จัดการข้อมูลขั้นสูง

UPDATE, DELETE, Transaction, View, Index, Window Functions, CTE

UPDATE & DELETE

คำเตือน!
ถ้าลืม WHERE ใน UPDATE หรือ DELETE จะแก้ไข/ลบ ทุกแถว ในตาราง ควรทำ SELECT ดูก่อนเสมอ
-- UPDATE — แก้ไขข้อมูล
UPDATE employees
SET salary = salary * 1.10
WHERE department = 'Kitchen';

-- อัปเดตหลายคอลัมน์พร้อมกัน
UPDATE customers
SET member_level = 'Gold',
    total_spent  = total_spent + 500
WHERE customer_id = 1;

-- DELETE — ลบข้อมูล
DELETE FROM orders
WHERE status = 'Cancelled';

-- ลบด้วย Subquery
DELETE FROM customers
WHERE customer_id NOT IN (
    SELECT DISTINCT customer_id FROM orders
);

Transaction — ทำหลายคำสั่งเป็นชุดเดียว

Transaction มัดรวมหลายคำสั่งให้ "สำเร็จทั้งหมดหรือยกเลิกทั้งหมด" เหมือนโอนเงิน — ถ้าหักบัญชีต้นทางแล้วแต่โอนไม่สำเร็จ ต้องคืนเงิน

BEGIN;

    UPDATE customers
    SET total_spent = total_spent + 425
    WHERE customer_id = 1;

    UPDATE orders
    SET status = 'Served'
    WHERE order_id = 1;

COMMIT;    -- ยืนยันทุกอย่าง

-- หากเกิดข้อผิดพลาด:
ROLLBACK;  -- ยกเลิกทุกอย่างกลับสู่สถานะก่อน BEGIN

View — ตารางเสมือน

View คือการบันทึก Query ที่ซับซ้อนไว้เป็นชื่อ เรียกใช้ง่ายเหมือนตารางปกติ แต่ไม่ได้เก็บข้อมูลจริง (คำนวณใหม่ทุกครั้งที่เรียก)

-- สร้าง View
CREATE VIEW vw_order_summary AS
SELECT
    o.order_id,
    c.first_name || ' ' || c.last_name AS ลูกค้า,
    e.first_name || ' ' || e.last_name AS พนักงาน,
    o.order_date,
    o.total_amount,
    o.status
FROM orders AS o
INNER JOIN customers AS c ON o.customer_id = c.customer_id
INNER JOIN employees AS e ON o.employee_id = e.employee_id;

-- ใช้งาน View เหมือนตารางปกติ
SELECT * FROM vw_order_summary WHERE status = 'Served';

-- ลบ View
DROP VIEW IF EXISTS vw_order_summary;

Index — เพิ่มความเร็วการค้นหา

Index คือ "สารบัญ" ของตาราง ช่วยให้ค้นข้อมูลเร็วขึ้นมาก แต่ใช้พื้นที่เพิ่มเล็กน้อยและช้าลงเล็กน้อยตอน INSERT/UPDATE/DELETE

-- สร้าง Index บนคอลัมน์ที่ค้นหาบ่อย
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_status   ON orders(status);
CREATE INDEX idx_emp_dept        ON employees(department);

-- Index หลายคอลัมน์ (Composite Index)
CREATE INDEX idx_orders_date_status ON orders(order_date, status);

-- ลบ Index
DROP INDEX IF EXISTS idx_orders_status;
เมื่อไรควรสร้าง Index
คอลัมน์ที่ใช้ใน WHERE, JOIN, ORDER BY บ่อยๆ — อย่าสร้างทุกคอลัมน์ เพราะช้าตอน write

Window Functions — วิเคราะห์ข้อมูลขั้นสูง

คำนวณข้อมูลโดยยังเก็บแถวเดิมไว้ (ต่างจาก GROUP BY ที่รวมเป็นแถวเดียว)

-- ROW_NUMBER() — ลำดับที่ไม่ซ้ำ
SELECT
    first_name, department, salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS อันดับในแผนก
FROM employees;

-- RANK() — อันดับ (ค่าซ้ำข้ามอันดับ: 1,1,3)
-- DENSE_RANK() — อันดับ (ค่าซ้ำไม่ข้าม: 1,1,2)
SELECT
    first_name, salary,
    RANK()       OVER (ORDER BY salary DESC) AS rank,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;

-- SUM() OVER — ยอดสะสม (Running Total)
SELECT
    DATE(order_date)    AS วันที่,
    SUM(total_amount)   AS ยอดวันนี้,
    SUM(SUM(total_amount)) OVER (ORDER BY DATE(order_date)) AS ยอดสะสม
FROM orders
WHERE status = 'Served'
GROUP BY DATE(order_date);

-- LAG() / LEAD() — ค่าแถวก่อนหน้า / ถัดไป
SELECT
    DATE(order_date) AS วันที่,
    SUM(total_amount) AS ยอดขาย,
    LAG(SUM(total_amount)) OVER (ORDER BY DATE(order_date)) AS ยอดเมื่อวาน
FROM orders WHERE status='Served'
GROUP BY DATE(order_date);

CTE — Common Table Expression

ตั้งชื่อ Query ชั่วคราวด้วย WITH ทำให้โค้ดอ่านง่ายและนำกลับมาใช้ใหม่ได้ในคำสั่งเดียวกัน

WITH high_value_customers AS (
    SELECT customer_id, first_name, last_name, total_spent
    FROM customers
    WHERE total_spent >= 10000
),
their_orders AS (
    SELECT customer_id, COUNT(*) AS จำนวนออเดอร์, SUM(total_amount) AS ยอดสั่งรวม
    FROM orders
    WHERE status = 'Served'
    GROUP BY customer_id
)
SELECT
    hvc.first_name,
    hvc.last_name,
    hvc.total_spent,
    COALESCE(to2.จำนวนออเดอร์, 0) AS จำนวนออเดอร์,
    COALESCE(to2.ยอดสั่งรวม,   0) AS ยอดสั่งรวม
FROM high_value_customers AS hvc
LEFT JOIN their_orders AS to2 ON hvc.customer_id = to2.customer_id;

Practical Queries — ใช้จริงในธุรกิจ

Dashboard KPI ประจำวัน

SELECT
    COUNT(*)                                                           AS ออเดอร์ทั้งหมด,
    COUNT(CASE WHEN status='Served'    THEN 1 END)                    AS สำเร็จ,
    COUNT(CASE WHEN status='Cancelled' THEN 1 END)                    AS ยกเลิก,
    COUNT(CASE WHEN status IN ('Pending','Preparing') THEN 1 END)     AS รอดำเนินการ,
    COALESCE(SUM(CASE WHEN status='Served' THEN total_amount END), 0) AS ยอดขายวันนี้
FROM orders
WHERE DATE(order_date) = DATE('now');

วิเคราะห์กำไรแต่ละเมนู

SELECT
    m.item_name,
    m.category,
    SUM(oi.quantity)                          AS จำนวนที่ขาย,
    SUM(oi.quantity * oi.unit_price)          AS รายได้รวม,
    SUM(oi.quantity * m.cost)                 AS ต้นทุนรวม,
    SUM(oi.quantity * (oi.unit_price - m.cost)) AS กำไรรวม
FROM order_items AS oi
INNER JOIN orders     AS o ON oi.order_id = o.order_id
INNER JOIN menu_items AS m ON oi.item_id  = m.item_id
WHERE o.status = 'Served'
GROUP BY m.item_id, m.item_name, m.category
ORDER BY กำไรรวม DESC;

ลูกค้าที่ไม่มาใช้บริการ > 30 วัน

SELECT
    c.first_name, c.last_name, c.member_level,
    MAX(o.order_date) AS ออเดอร์ล่าสุด
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id AND o.status = 'Served'
GROUP BY c.customer_id, c.first_name, c.last_name, c.member_level
HAVING MAX(o.order_date) < DATE('now', '-30 days')
    OR MAX(o.order_date) IS NULL
ORDER BY ออเดอร์ล่าสุด;

🎯 แบบทดสอบส่วนที่ 4

ข้อ 1. เขียน query ขึ้นราคาเมนูทุกรายการในหมวด "เครื่องดื่ม" 5 บาท
ข้อ 2. Transaction คืออะไร? ยกตัวอย่างสถานการณ์ที่ต้องใช้
ข้อ 3. View vs Table ต่างกันอย่างไร?
ข้อ 4. เขียน query แสดงอันดับพนักงานตามเงินเดือน (มากไปน้อย) ในแต่ละแผนก
ข้อ 5. CTE มีประโยชน์อะไรเหนือ Subquery?
ดูเฉลย
-- ข้อ 1
UPDATE menu_items SET price = price + 5 WHERE category = 'เครื่องดื่ม';

-- ข้อ 4
SELECT first_name, department, salary,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS อันดับในแผนก
FROM employees;

ข้อ 2. Transaction มัดรวมหลายคำสั่งให้ทำสำเร็จทั้งหมดหรือยกเลิกทั้งหมด ตัวอย่าง: โอนเงินต้องหักต้นทาง+เพิ่มปลายทางพร้อมกัน ถ้าขั้นตอนใดล้มเหลวต้อง ROLLBACK ทั้งชุด

ข้อ 3. View ไม่เก็บข้อมูลจริง คำนวณใหม่ทุกครั้งที่เรียก ข้อมูลเสมอ up-to-date / Table เก็บข้อมูลจริงในดิสก์

ข้อ 5. CTE ตั้งชื่อได้และนำกลับมาใช้ซ้ำได้ในคำสั่งเดียวกัน อ่านง่ายกว่า Subquery ซ้อนกันหลายชั้น และสามารถมีหลาย CTE ในคำสั่งเดียว


Cheatsheet — ครบทุกคำสั่ง SQL

Template + โครงสร้าง + ตัวอย่างอ้างอิงเร็ว

DDL — สร้าง/แก้ไข/ลบโครงสร้าง

DDLCREATE TABLE
CREATE TABLE [IF NOT EXISTS] table_name (
    col1  TYPE  [CONSTRAINT],
    col2  TYPE  [CONSTRAINT],
    ...
    [TABLE CONSTRAINT]
);
สร้างตารางใหม่ — IF NOT EXISTS ป้องกัน error ถ้ามีอยู่แล้ว
DDLColumn Constraints
col  INTEGER  PRIMARY KEY AUTOINCREMENT
col  TEXT     NOT NULL
col  TEXT     UNIQUE
col  TEXT     UNIQUE NOT NULL
col  REAL     DEFAULT 0
col  TEXT     DEFAULT 'value'
col  REAL     CHECK (col > 0)
col  TEXT     CHECK (col IN ('a','b','c'))
col  INTEGER  REFERENCES other(id)
ข้อจำกัดใส่ต่อท้ายประเภทข้อมูลของคอลัมน์
DDLALTER TABLE
-- เพิ่มคอลัมน์
ALTER TABLE t ADD COLUMN col TYPE [CONSTRAINT];

-- เปลี่ยนชื่อคอลัมน์
ALTER TABLE t RENAME COLUMN old TO new;

-- เปลี่ยนชื่อตาราง
ALTER TABLE old_name RENAME TO new_name;
SQLite ไม่รองรับ DROP COLUMN (ก่อน v3.35) และ MODIFY COLUMN
DDLDROP & TRUNCATE
-- ลบตาราง (ถาวร รวมข้อมูล)
DROP TABLE [IF EXISTS] table_name;

-- ลบ View
DROP VIEW [IF EXISTS] view_name;

-- ลบ Index
DROP INDEX [IF EXISTS] index_name;

-- ล้างข้อมูลทั้งตาราง (SQLite ใช้ DELETE)
DELETE FROM table_name;
IF EXISTS ป้องกัน error หากไม่มีอยู่จริง
DDLCREATE INDEX
-- Index คอลัมน์เดียว
CREATE INDEX idx_name ON table(col);

-- Index ไม่ซ้ำ
CREATE UNIQUE INDEX idx_name ON table(col);

-- Index หลายคอลัมน์
CREATE INDEX idx_name ON table(col1, col2);

-- ลบ Index
DROP INDEX [IF EXISTS] idx_name;
ใช้กับคอลัมน์ใน WHERE, JOIN, ORDER BY ที่ query บ่อย
DDLCREATE VIEW
-- สร้าง View
CREATE VIEW view_name AS
SELECT ...;

-- สร้างหรือแทนที่ (MySQL/PostgreSQL)
CREATE OR REPLACE VIEW view_name AS
SELECT ...;

-- ลบ View
DROP VIEW [IF EXISTS] view_name;
View ไม่เก็บข้อมูลจริง คำนวณใหม่ทุกครั้งที่เรียก

DML — เพิ่ม/แก้ไข/ลบข้อมูล

DMLINSERT INTO
-- แถวเดียว (ระบุคอลัมน์)
INSERT INTO table (col1, col2, col3)
VALUES (val1, val2, val3);

-- หลายแถวพร้อมกัน
INSERT INTO table (col1, col2)
VALUES
    (v1a, v2a),
    (v1b, v2b),
    (v1c, v2c);

-- แทรกจาก SELECT
INSERT INTO table (col1, col2)
SELECT col1, col2 FROM other_table
WHERE condition;

-- แทรกหรืออัปเดตถ้ามีอยู่แล้ว
INSERT OR REPLACE INTO table (col1, col2)
VALUES (v1, v2);
แนะนำให้ระบุชื่อคอลัมน์เสมอ ป้องกันปัญหาเมื่อโครงสร้างตารางเปลี่ยน
DMLUPDATE
-- พื้นฐาน
UPDATE table
SET col1 = val1
WHERE condition;

-- อัปเดตหลายคอลัมน์
UPDATE table
SET col1 = val1,
    col2 = val2,
    col3 = col3 + 1
WHERE condition;

-- อัปเดตด้วยค่าจากตารางอื่น (Subquery)
UPDATE employees
SET salary = salary * 1.1
WHERE department = (
    SELECT department FROM departments
    WHERE department_name = 'Kitchen'
);
⚠️ ลืม WHERE = แก้ทุกแถว ทำ SELECT ดูก่อนเสมอ
DMLDELETE
-- ลบแถวที่ตรงเงื่อนไข
DELETE FROM table
WHERE condition;

-- ลบด้วย Subquery
DELETE FROM table
WHERE col IN (SELECT col FROM other WHERE ...);

-- ลบทุกแถว (เก็บโครงสร้าง)
DELETE FROM table;
⚠️ ลืม WHERE = ลบทุกแถว ไม่มี undo! ใช้ Transaction ห่อไว้
DMLTRANSACTION
BEGIN;              -- เริ่ม Transaction

    INSERT INTO ...;
    UPDATE ...;
    DELETE FROM ...;

COMMIT;             -- ยืนยันทุกคำสั่ง

-- หรือยกเลิกถ้าเกิดข้อผิดพลาด
ROLLBACK;           -- คืนสู่สถานะก่อน BEGIN

-- SAVEPOINT — จุดบันทึกย่อย
SAVEPOINT sp1;
-- ... คำสั่ง ...
ROLLBACK TO sp1;    -- ยกเลิกถึงจุดนี้เท่านั้น
RELEASE sp1;
ใช้เมื่อคำสั่งหลายตัวต้องสำเร็จพร้อมกัน เช่น โอนเงิน, สร้างออเดอร์+อัปเดตสต็อก

DQL — ดึงข้อมูล (SELECT)

DQLSELECT โครงสร้างเต็ม
SELECT [DISTINCT] col1 [AS alias], col2, ...
FROM   table1 [AS t1]
[JOIN  table2 AS t2 ON t1.id = t2.fk]
[WHERE condition]
[GROUP BY col1, col2]
[HAVING group_condition]
[ORDER BY col [ASC|DESC]]
[LIMIT n [OFFSET m]];
ลำดับประมวลผล: FROM→JOIN→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT
DQLWHERE Operators
WHERE col = 'value'
WHERE col <> 'value'        -- ไม่เท่ากับ
WHERE col > 100
WHERE col BETWEEN 10 AND 20 -- รวม 10 และ 20
WHERE col IN ('a', 'b', 'c')
WHERE col NOT IN ('x', 'y')
WHERE col LIKE 'prefix%'    -- ขึ้นต้นด้วย
WHERE col LIKE '%suffix'    -- ลงท้ายด้วย
WHERE col LIKE '%contains%' -- มีคำนี้อยู่
WHERE col LIKE 'a_c'        -- _ = 1 ตัวอักษร
WHERE col IS NULL
WHERE col IS NOT NULL
WHERE cond1 AND cond2
WHERE cond1 OR  cond2
WHERE NOT condition
ใช้วงเล็บ () เมื่อรวม AND และ OR เพื่อควบคุมลำดับ
DQLJOIN Types
-- INNER JOIN (ค่าเริ่มต้น)
FROM a INNER JOIN b ON a.id = b.fk
FROM a JOIN b ON a.id = b.fk       -- เหมือนกัน

-- LEFT JOIN — ทุกแถวจาก a
FROM a LEFT JOIN b ON a.id = b.fk
FROM a LEFT OUTER JOIN b ON ...    -- เหมือนกัน

-- CROSS JOIN — ทุกคู่ผสม (Cartesian Product)
FROM a CROSS JOIN b

-- Self JOIN — ตารางกับตัวเอง
FROM employees AS e
JOIN employees AS mgr ON e.manager_id = mgr.employee_id
SQLite ไม่รองรับ RIGHT JOIN และ FULL OUTER JOIN (ใช้ LEFT JOIN + UNION แทน)
DQLGROUP BY & HAVING
-- GROUP BY พื้นฐาน
SELECT col, COUNT(*), SUM(val), AVG(val)
FROM table
GROUP BY col;

-- GROUP BY หลายคอลัมน์
SELECT col1, col2, COUNT(*)
FROM table
GROUP BY col1, col2;

-- HAVING กรองหลัง GROUP BY
SELECT col, COUNT(*) AS cnt
FROM table
GROUP BY col
HAVING cnt > 5;

-- WHERE + GROUP BY + HAVING ร่วมกัน
SELECT dept, AVG(salary) AS avg_sal
FROM employees
WHERE is_active = 1         -- กรองก่อน group
GROUP BY dept
HAVING avg_sal > 30000;    -- กรองหลัง group
HAVING ใช้ชื่อ alias จาก SELECT ได้ใน SQLite
DQLAggregate Functions
COUNT(*)           -- นับทุกแถว
COUNT(col)         -- นับแถวที่ไม่ใช่ NULL
COUNT(DISTINCT col) -- นับค่าไม่ซ้ำ
SUM(col)           -- รวม
AVG(col)           -- เฉลี่ย
MAX(col)           -- สูงสุด
MIN(col)           -- ต่ำสุด

-- Conditional Aggregation
COUNT(CASE WHEN cond THEN 1 END)
SUM(CASE WHEN cond THEN col ELSE 0 END)
AVG(CASE WHEN cond THEN col END)
Aggregate ใช้ใน SELECT และ HAVING เท่านั้น ไม่ใช้ใน WHERE
DQLSubquery
-- ใน WHERE (Scalar subquery)
WHERE col > (SELECT AVG(col) FROM t)

-- ใน WHERE ด้วย IN
WHERE id IN (SELECT id FROM t WHERE ...)

-- ใน WHERE ด้วย EXISTS
WHERE EXISTS (SELECT 1 FROM t WHERE t.fk = outer.pk)

-- เป็น Derived Table ใน FROM
FROM (SELECT ... FROM t WHERE ...) AS alias

-- Correlated Subquery (อ้างอิงตารางนอก)
SELECT * FROM employees AS e
WHERE salary > (
    SELECT AVG(salary) FROM employees
    WHERE department = e.department  -- อ้าง e จากนอก
)
Correlated subquery ช้ากว่า JOIN — ใช้ CTE หรือ JOIN แทนเมื่อเป็นไปได้

Advanced SQL

ADVCTE — WITH
-- CTE เดียว
WITH cte_name AS (
    SELECT ...
)
SELECT * FROM cte_name WHERE ...;

-- หลาย CTE
WITH
cte1 AS (SELECT ...),
cte2 AS (SELECT ... FROM cte1)
SELECT * FROM cte2;

-- Recursive CTE (สำหรับ hierarchy)
WITH RECURSIVE cte AS (
    SELECT id, name, parent_id FROM categories WHERE parent_id IS NULL
    UNION ALL
    SELECT c.id, c.name, c.parent_id
    FROM categories AS c
    JOIN cte ON c.parent_id = cte.id
)
SELECT * FROM cte;
CTE อ่านง่ายกว่า Subquery ซ้อนกัน และนำกลับมาใช้ซ้ำในคำสั่งเดียวกันได้
ADVWindow Functions
func() OVER (
    [PARTITION BY col1, col2]  -- แบ่งกลุ่ม
    [ORDER BY col [ASC|DESC]]  -- เรียงลำดับ
    [ROWS/RANGE BETWEEN ...]   -- frame
)

-- Ranking Functions
ROW_NUMBER() OVER (...)    -- 1,2,3,4 (ไม่ซ้ำ)
RANK()       OVER (...)    -- 1,1,3,4 (ซ้ำข้ามอันดับ)
DENSE_RANK() OVER (...)    -- 1,1,2,3 (ซ้ำไม่ข้าม)
NTILE(n)     OVER (...)    -- แบ่งเป็น n กลุ่ม

-- Value Functions
LAG(col, n, default) OVER (...)   -- ค่าแถว n ก่อนหน้า
LEAD(col, n, default) OVER (...)  -- ค่าแถว n ถัดไป
FIRST_VALUE(col) OVER (...)       -- ค่าแรกในกลุ่ม
LAST_VALUE(col)  OVER (...)       -- ค่าสุดท้ายในกลุ่ม

-- Aggregate Window
SUM(col)   OVER (ORDER BY col2)   -- Running total
AVG(col)   OVER (PARTITION BY g)  -- เฉลี่ยในกลุ่ม
COUNT(*)   OVER ()                -- นับทั้งหมด
Window Functions ต่างจาก GROUP BY ตรงที่ไม่รวมแถว — แต่ละแถวยังคงอยู่
ADVCASE WHEN
-- Simple CASE
CASE col
    WHEN 'a' THEN 'result_a'
    WHEN 'b' THEN 'result_b'
    ELSE 'other'
END

-- Searched CASE (ยืดหยุ่นกว่า)
CASE
    WHEN col >= 90 THEN 'A'
    WHEN col >= 80 THEN 'B'
    WHEN col >= 70 THEN 'C'
    ELSE 'F'
END AS grade

-- ใน Aggregate (Conditional Count/Sum)
COUNT(CASE WHEN status='Active' THEN 1 END)
SUM(CASE WHEN type='income' THEN amount
         WHEN type='expense' THEN -amount
         ELSE 0 END) AS net
CASE WHEN ใช้ได้ในทุกที่ที่ expression ใช้ได้: SELECT, WHERE, ORDER BY, GROUP BY
ADVSET Operations
-- UNION — รวม ตัดซ้ำออก
SELECT col FROM t1
UNION
SELECT col FROM t2;

-- UNION ALL — รวม เก็บซ้ำ (เร็วกว่า)
SELECT col FROM t1
UNION ALL
SELECT col FROM t2;

-- INTERSECT — เฉพาะที่มีทั้งสองชุด
SELECT col FROM t1
INTERSECT
SELECT col FROM t2;

-- EXCEPT / MINUS — อยู่ใน t1 แต่ไม่อยู่ใน t2
SELECT col FROM t1
EXCEPT
SELECT col FROM t2;
ทุก SELECT ต้องมีจำนวนและชนิดคอลัมน์เหมือนกัน
ADVEXISTS & ANY / ALL
-- EXISTS — ตรวจว่ามีผลลัพธ์หรือไม่
SELECT * FROM customers AS c
WHERE EXISTS (
    SELECT 1 FROM orders AS o
    WHERE o.customer_id = c.customer_id
);

-- NOT EXISTS
WHERE NOT EXISTS (SELECT 1 FROM t WHERE ...);

-- ANY / SOME — ตรงกับค่าใดค่าหนึ่ง
WHERE salary > ANY (SELECT salary FROM t WHERE dept='IT')

-- ALL — ตรงกับทุกค่า
WHERE salary > ALL (SELECT salary FROM t WHERE dept='IT')
EXISTS มักเร็วกว่า IN สำหรับ subquery ขนาดใหญ่
ADVNULL Handling
-- ตรวจ NULL
WHERE col IS NULL
WHERE col IS NOT NULL

-- แทน NULL ด้วยค่าอื่น
COALESCE(col, 'ค่าแทน')        -- ค่าแรกที่ไม่ใช่ NULL
IFNULL(col, 'ค่าแทน')          -- SQLite specific
NULLIF(col, 0)                  -- คืน NULL ถ้า col = 0

-- NULL ใน Aggregate (ถูก ignore อัตโนมัติ)
-- COUNT(col) ไม่นับ NULL แต่ COUNT(*) นับทุกแถว
-- AVG(col) คำนวณเฉพาะ non-NULL

-- Conditional NULL
CASE WHEN col IS NULL THEN 'N/A' ELSE col END
NULL ไม่เท่ากับ NULL — ทุก NULL เปรียบเทียบกันจะได้ UNKNOWN ไม่ใช่ TRUE

Functions Reference

FNString Functions
LENGTH(str)                    -- ความยาว
UPPER(str)                     -- ตัวพิมพ์ใหญ่
LOWER(str)                     -- ตัวพิมพ์เล็ก
TRIM(str)                      -- ตัดช่องว่างหัวท้าย
LTRIM(str) / RTRIM(str)       -- ตัดซ้าย / ขวา
SUBSTR(str, start, len)        -- ตัดสตริง
REPLACE(str, old, new)         -- แทนที่
INSTR(str, substr)             -- หาตำแหน่ง (0 = ไม่พบ)
str1 || str2                   -- ต่อสตริง (SQLite)
CONCAT(str1, str2)             -- ต่อสตริง (MySQL)
LIKE 'pattern'                 -- จับคู่รูปแบบ
GLOB 'pattern'                 -- จับคู่ case-sensitive (SQLite)
FNNumeric Functions
ROUND(n, decimals)    -- ปัดเศษ
CEIL(n) / CEILING(n)  -- ปัดขึ้น
FLOOR(n)              -- ปัดลง
ABS(n)                -- ค่าสมบูรณ์
MOD(n, m)             -- เศษจากหาร (หรือ n % m)
POWER(n, exp)         -- n ยกกำลัง exp
SQRT(n)               -- รากที่สอง
MAX(a, b)             -- ค่าสูงสุดในหลายค่า
MIN(a, b)             -- ค่าต่ำสุดในหลายค่า
CAST(val AS TYPE)     -- แปลงประเภทข้อมูล
FNDate Functions (SQLite)
DATE('now')                    -- วันนี้ YYYY-MM-DD
TIME('now')                    -- เวลาตอนนี้ HH:MM:SS
DATETIME('now')                -- วันที่+เวลา
DATE('now', '+7 days')         -- บวก 7 วัน
DATE('now', '-1 month')        -- ลบ 1 เดือน
DATE('now', 'start of month')  -- วันแรกของเดือน
DATE('now', 'start of year')   -- วันแรกของปี

strftime('%Y', date)           -- ปี
strftime('%m', date)           -- เดือน (01-12)
strftime('%d', date)           -- วัน (01-31)
strftime('%H', datetime)       -- ชั่วโมง
strftime('%Y-%m', date)        -- ปี-เดือน
strftime('%w', date)           -- วันในสัปดาห์ (0=อาทิตย์)
SQLite เก็บวันที่เป็น TEXT — ใช้รูปแบบ ISO 8601: YYYY-MM-DD
FNType Conversion
-- CAST — แปลงประเภท
CAST('123' AS INTEGER)         -- TEXT → INTEGER
CAST(123 AS TEXT)              -- INTEGER → TEXT
CAST('3.14' AS REAL)          -- TEXT → REAL
CAST(price AS INTEGER)        -- ตัดทศนิยม

-- SQLite type affinity (อัตโนมัติ)
'123' + 0                      -- ได้ 123 (integer)
123 || ''                      -- ได้ '123' (text)

-- TYPEOF — ตรวจประเภท
TYPEOF(col)  -- 'integer', 'real', 'text', 'null', 'blob'
FNConditional Functions
-- COALESCE — ค่าแรกที่ไม่ใช่ NULL
COALESCE(col1, col2, 'default')

-- IFNULL — แทน NULL (SQLite / MySQL)
IFNULL(col, 'ค่าแทน')

-- NULLIF — คืน NULL ถ้าเท่ากัน
NULLIF(col, 0)    -- ถ้า col=0 คืน NULL

-- IIF — if-else สั้น (SQLite 3.32+)
IIF(condition, true_val, false_val)

-- CASE WHEN (ใช้ได้ทุกที่)
CASE WHEN cond THEN val ELSE other END
ADVUseful Patterns
-- Pagination
SELECT * FROM t ORDER BY id LIMIT 10 OFFSET (page-1)*10;

-- Upsert (Insert or Update)
INSERT INTO t (id, val) VALUES (1, 'x')
ON CONFLICT(id) DO UPDATE SET val = excluded.val;

-- Top-N ต่อกลุ่ม
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER
        (PARTITION BY dept ORDER BY salary DESC) AS rn
    FROM employees
) WHERE rn <= 3;

-- Running Total
SUM(amount) OVER (ORDER BY date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

-- 7-Day Moving Average
AVG(amount) OVER (ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
ON CONFLICT รองรับใน SQLite 3.24+ และ PostgreSQL

คู่มือ SQL ฉบับเร่งรัด — ครอบคลุม SQLite / MySQL / PostgreSQL
ขั้นตอนต่อไป: ฝึกกับ DB Browser for SQLite หรือ sqliteonline.com