สรุปสั้น
- เขียน
UPDATE/DELETEต้องมีWHEREเสมอ และลองรันเป็นSELECTก่อนNULLไม่เท่ากับอะไรเลย แม้แต่NULLด้วยกัน → ใช้IS NULLLEFT JOINเก็บแถวฝั่งซ้ายไว้ทั้งหมดINNER JOINเก็บเฉพาะแถวที่จับคู่ได้- index ทำให้ค้นเร็ว แต่ใช้ไม่ได้ถ้าเอาฟังก์ชันไปครอบคอลัมน์
- ส่งค่าจากผู้ใช้ผ่าน prepared statement เสมอ (ดูตัวอย่างใน พื้นฐาน PHP)
ตัวอย่างในโน้ตนี้ใช้ MySQL และตาราง customers, orders
1. SELECT พื้นฐาน
SELECT id, name, email
FROM customers
WHERE status = 'active'
AND created_at >= '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;- ระบุคอลัมน์ที่ต้องใช้ แทน
SELECT *จะเร็วกว่าและไม่พังเมื่อตารางเพิ่มคอลัมน์ LIKE 'สม%'ใช้ index ได้ แต่LIKE '%ชาย'(มี % ข้างหน้า) ใช้ไม่ได้
ลำดับที่ฐานข้อมูลประมวลผลจริง
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
ลำดับนี้เป็นเหตุผลที่ใน WHERE ใช้ชื่อเล่น (alias) ที่ตั้งใน SELECT ไม่ได้
2. NULL
WHERE phone = NULL -- ❌ ไม่เจออะไรเลย
WHERE phone IS NULL -- ✅
WHERE phone IS NOT NULL
SELECT COALESCE(phone, '-') FROM customers; -- แทน NULL ด้วยค่าอื่นCOUNT(*)นับทุกแถวCOUNT(phone)นับเฉพาะแถวที่ phone ไม่เป็น NULLWHERE status != 'cancelled'จะไม่รวมแถวที่ status เป็น NULL
3. JOIN
-- ลูกค้าที่มีออเดอร์ (ไม่มีออเดอร์ = ไม่แสดง)
SELECT c.name, o.id AS order_id, o.total
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;
-- ลูกค้าทุกคน ถ้าไม่มีออเดอร์ คอลัมน์ของ orders จะเป็น NULL
SELECT c.name, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
-- ลูกค้าที่ยังไม่เคยสั่งซื้อ
SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;เงื่อนไขของตารางขวาใน LEFT JOIN
ถ้าเขียน
WHERE o.status = 'paid'แถวที่ไม่มีออเดอร์จะหายไป กลายเป็น INNER JOIN โดยไม่รู้ตัว ถ้าอยากเก็บลูกค้าทุกคนไว้ ให้ย้ายเงื่อนไขไปไว้ในON ... AND o.status = 'paid'
4. GROUP BY และ HAVING
SELECT customer_id,
COUNT(*) AS order_count,
SUM(total) AS revenue
FROM orders
WHERE status = 'paid' -- กรองก่อนจัดกลุ่ม
GROUP BY customer_id
HAVING SUM(total) > 10000 -- กรองหลังจัดกลุ่ม
ORDER BY revenue DESC;คอลัมน์ใน SELECT ต้องอยู่ใน GROUP BY หรืออยู่ในฟังก์ชันรวม (COUNT, SUM, MAX ฯลฯ) เท่านั้น
5. เพิ่ม แก้ ลบ ข้อมูล
INSERT INTO customers (name, email) VALUES ('Somchai', 'som@example.com');
UPDATE customers SET status = 'inactive' WHERE id = 42;
DELETE FROM customers WHERE id = 42;ก่อนรัน UPDATE หรือ DELETE
- เปลี่ยนเป็น
SELECT COUNT(*) FROM ... WHERE ...ก่อน ดูว่าจำนวนแถวตรงที่คาดไหม- ลืม
WHERE= แก้หรือลบทั้งตาราง
6. Transaction
ทำหลายคำสั่งให้ “สำเร็จทั้งหมด หรือไม่เกิดอะไรเลย” เช่น โอนเงิน
START TRANSACTION;
UPDATE wallets SET balance = balance - 500 WHERE user_id = 1;
UPDATE wallets SET balance = balance + 500 WHERE user_id = 2;
COMMIT; -- หรือ ROLLBACK; ถ้ามีอะไรผิดพลาดอัปเดตยอดแบบ balance = balance - 500 ในคำสั่งเดียว ปลอดภัยกว่าอ่านค่ามาคำนวณในโค้ดแล้วเขียนกลับ เพราะถ้ามีสองคำขอเข้ามาพร้อมกัน แบบหลังจะเขียนทับกันจนยอดผิด
7. Index
index เหมือนสารบัญท้ายหนังสือ ช่วยให้หาแถวได้เร็วโดยไม่ต้องไล่ทั้งตาราง
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at);
EXPLAIN SELECT * FROM orders WHERE customer_id = 42; -- ดูว่าใช้ index ไหม- index หลายคอลัมน์ใช้ได้ตามลำดับจากซ้าย: index
(customer_id, created_at)ช่วยค้นcustomer_idอย่างเดียวได้ แต่ช่วยค้นcreated_atอย่างเดียวไม่ได้ - ครอบคอลัมน์ด้วยฟังก์ชันแล้วใช้ index ไม่ได้
WHERE DATE(created_at) = '2026-09-29' -- ❌ ใช้ index ไม่ได้
WHERE created_at >= '2026-09-29' AND created_at < '2026-09-30' -- ✅- index ทำให้อ่านเร็ว แต่เขียนช้าลงเล็กน้อย ไม่ต้องใส่ทุกคอลัมน์
8. Key และ Constraint
- PRIMARY KEY: ตัวระบุแถวที่ไม่ซ้ำ
- FOREIGN KEY: บังคับว่า
orders.customer_idต้องมีอยู่จริงในcustomers - UNIQUE: กันข้อมูลซ้ำ เช่น email
- NOT NULL และ DEFAULT: กันค่าว่างที่ไม่ควรว่าง
ให้ฐานข้อมูลบังคับกฎเหล่านี้ อย่าหวังพึ่งการเช็กในโค้ดอย่างเดียว
9. เครื่องมือขั้นกลางที่ควรรู้จัก
CTE (WITH): ตั้งชื่อ query ย่อยให้อ่านง่าย
WITH paid AS (
SELECT customer_id, SUM(total) AS revenue
FROM orders WHERE status = 'paid'
GROUP BY customer_id
)
SELECT c.name, p.revenue
FROM paid p JOIN customers c ON c.id = p.customer_id;Window function: จัดอันดับโดยไม่ยุบแถว (MySQL 8+)
-- ออเดอร์ล่าสุดของลูกค้าแต่ละคน
SELECT * FROM (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders o
) t
WHERE rn = 1;10. ปัญหา N+1
ดึงรายการมา 1 ครั้ง แล้ววน loop query ทีละแถวอีก N ครั้ง เช่น ดึงลูกค้า 100 คน แล้ว query ออเดอร์ทีละคน = 101 query
-- แทนที่จะวน loop ให้ดึงทีเดียว
SELECT * FROM orders WHERE customer_id IN (1, 2, 3, ...);ใน Laravel ใช้ Customer::with('orders')->get() (eager loading) แทน
11. แบ่งหน้า (Pagination)
-- OFFSET: ง่าย แต่ยิ่งหน้าลึกยิ่งช้า เพราะต้องข้ามแถวทิ้ง
SELECT * FROM orders ORDER BY id DESC LIMIT 20 OFFSET 10000;
-- Keyset: เร็วคงที่ จำ id ตัวสุดท้ายของหน้าก่อนไว้
SELECT * FROM orders WHERE id < 98765 ORDER BY id DESC LIMIT 20;จุดที่มักพลาด
- ลืม
WHEREในUPDATE/DELETE - เทียบ
NULLด้วย= - เงื่อนไขของตารางขวาใส่ใน
WHEREทำให้LEFT JOINกลายเป็นINNER JOIN - เก็บเงินเป็น
FLOAT→ ใช้DECIMAL(12,2)แทน - เวลาไม่ตรงเพราะ timezone ของ server, database และแอปไม่ตรงกัน → ตกลงกันให้ชัด เช่น เก็บเป็น UTC
- ต่อ string SQL จาก input ผู้ใช้ → SQL injection
แหล่งอ้างอิง
- MySQL 8.0 Reference Manual · https://dev.mysql.com/doc/refman/8.0/en/
- Use The Index, Luke: คู่มือเรื่อง index · https://use-the-index-luke.com/