1833 คำ
9 นาที
Database 101 EP.07 — Index + Query Optimization: ทำไม query ที่เคยเร็ว ช้าลง 1000 เท่า
สารบัญ

Series: Database 101 — เข้าใจห้องสมุดของเมืองดิจิทัล (ภาษาคน)

Part 0 — WHY: ทำไมต้องมีห้องสมุด

  • EP.01 — ทำไมต้องมี Database: โลกก่อนมีห้องสมุด

Part 1 — ประวัติ: 4 ยุคของ Database

Part 2 — How: ภายใน Database ทำงานยังไง

  • EP.06 — Schema, Normalization, Keys
  • EP.07 — Index + Query Optimization ← คุณอยู่ตรงนี้
  • EP.08 — Transaction + Concurrency Control (เร็วๆ นี้)

Part 3 — เลือก Storage ตามขนาด

Part 4 — Operations

  • EP.13 — DBA Role + Privileged Access
  • EP.14 — Database Security + Encryption

Part 5 — Future

  • EP.15 — Vector Database + AI Era
  • EP.16 — Wrap: Decision Tree + 5 Trends

EP.06 เราจบที่ schema สวยงามครับ table normalize ดี key ครบ foreign key ผูกถูก โครงสร้างเป็นระเบียบเหมือนห้องสมุดที่จัดชั้นเรียบร้อย แต่พอเอาเข้าใช้งานจริง อยู่ดีๆ ระบบที่เคยเร็วก็ช้าลงเรื่อยๆ ไม่มีใครเปลี่ยน code ไม่มีใครเปลี่ยน server schema ก็ยังเหมือนเดิม ลูกค้าเริ่มบ่น ทีม dev เริ่มถูกถาม

แล้วคำตอบที่ผู้บริหารส่วนใหญ่ได้ยินคือ “ระบบช้าเพราะ data เยอะครับ ต้องซื้อ server ใหม่” ฟังดูสมเหตุสมผลนะ แต่ในความจริง คำตอบนี้ผิด 95% ของเวลา คำตอบที่ถูกในเกือบทุกเคสคือ index ผิด หรือ query เขียนผิด

EP.07 จะพาไปดูว่า index คืออะไร (สั้นๆ คือสารบัญหลังหนังสือ) ทำไม query ที่ไม่ได้ใช้ index ถึงช้าลงเป็น 1000 เท่าได้ แล้วทำไมการ “ซื้อ server แรงขึ้น” ควรเป็นทางสุดท้าย ไม่ใช่ทางแรก

ฉากเปิด: ระบบที่ slow down 1000 เท่าโดยไม่มีใครเปลี่ยน code#

ลองนึก scenario นี้ครับ — บริษัทค้าปลีกแห่งหนึ่งเปิดระบบ CRM ใหม่ หน้า “ค้นหาลูกค้า” มี search box เล็กๆ พิมพ์เบอร์โทร แล้วได้ข้อมูลลูกค้ากลับมา

ปีแรก ระบบเปิดใช้ มีลูกค้าในระบบ 10,000 คน กดค้นหา ได้ผลใน 0.01 วินาที พนักงานหน้าร้านชอบมาก ผู้บริหารชอบมาก ทุกคน happy

ปีที่สอง ลูกค้าเพิ่มเป็น 1,000,000 คน (โต 100 เท่า เพราะเปิดสาขาเพิ่ม + ทำ marketing) พนักงานหน้าร้านเริ่มบ่น — กดค้นหาแล้วต้องรอ 30 วินาที ลูกค้าที่ยืนหน้า counter ก็เริ่มหงุดหงิด

ผู้บริหารเรียกทีม IT มาคุย ทีมตอบว่า “data เยอะขึ้น 100 เท่าครับ ต้องอัพเกรด server” ผู้บริหารเซ็นอนุมัติงบ 500,000 บาท ซื้อ server ใหม่ที่แรงกว่าเดิม 4 เท่า ติดตั้งเสร็จ กดค้นหาใหม่ ได้ผลใน 27 วินาที แทบไม่ต่างเลย

อ้าว แล้วทำไมล่ะ?

คำตอบจริงคือ ปัญหาไม่ใช่ server ปัญหาคือ database table ของลูกค้าไม่มี index บน column “เบอร์โทร” ทุกครั้งที่ค้นหา database ต้องเปิดทุกแถว 1 ล้านแถว เพื่อหาเบอร์โทรที่ตรง ซื้อ server แรงขึ้น 4 เท่า ก็แค่ทำให้ “เปิดทุกแถว” เร็วขึ้น 4 เท่า แทนที่จะใช้ 30 วินาที กลายเป็น 27 วินาที

วิธีแก้จริงๆ ใช้คำสั่ง SQL บรรทัดเดียว — CREATE INDEX idx_phone ON customers(phone) หลังรันคำสั่งนี้ กดค้นหาใหม่ ได้ผลใน 0.001 วินาที เร็วกว่าเดิม 30,000 เท่า โดยไม่ต้องซื้อ server เลย

นี่แหละครับเหตุผลที่ EP.07 มีอยู่ มาเริ่มที่หัวใจของเรื่องกันก่อน — ทำไมการ “เปิดทุกหน้า” ถึงช้ามาก

Full Table Scan vs Index Scan — สมมติฐานพื้นฐาน#

ลองนึกถึงหนังสือเล่มหนึ่งหนาๆ ครับ สมมติเป็นพจนานุกรม 1,000 หน้า คุณอยากหาคำว่า “วราภรณ์”

วิธีที่ 1: เปิดทุกหน้า — เริ่มจากหน้า 1 อ่านทุกคำ ถ้าไม่ใช่ก็ไปหน้า 2 อ่านทุกคำ ไปหน้า 3 … ไปจนกว่าจะเจอ ถ้าโชคดีคำอยู่หน้า 50 ก็จบเร็ว ถ้าโชคร้ายอยู่หน้า 999 ใช้เวลาเกือบทั้งวัน ในภาษา database เราเรียกวิธีนี้ว่า Full Table Scan เปิดทุก row ในตารางเพื่อหา row ที่ตรงเงื่อนไข

วิธีที่ 2: เปิดสารบัญก่อน — ที่หลังหนังสือมี “สารบัญคำ” เรียงตามตัวอักษร คุณเปิดสารบัญ → หาคำว่า “วราภรณ์” → เห็นว่าอยู่หน้า 743 → เปิดไปที่หน้า 743 ทันที ในภาษา database เราเรียกวิธีนี้ว่า Index Scan เปิดสารบัญ (index) ก่อนเพื่อหาว่า row ที่ต้องการอยู่ตรงไหน แล้วค่อยเปิดเฉพาะ row นั้น

Index ใน database คือสารบัญที่ database สร้างขึ้นเองอัตโนมัติ เก็บ “ค่าใน column” + “ตำแหน่งของ row นั้นใน disk” เวลา query มา database ก็เปิดสารบัญก่อน หาตำแหน่ง แล้วกระโดดไปอ่าน row โดยตรง

ความต่างของสองวิธีนี้ในภาษาคณิตศาสตร์เรียกว่า O(n) vs O(log n) ครับ ฟังดูซับซ้อน แต่จริงๆ ง่ายมาก

จำนวน rowFull Table Scan (O(n))Index Scan (O(log n))
1,0001,000 operations~10 operations
1,000,0001,000,000 operations~20 operations
1,000,000,0001,000,000,000 operations~30 operations

สังเกตคอลัมน์ขวาครับ — ต่อให้ data เพิ่มจาก 1 ล้าน เป็น 1 พันล้าน index scan แค่เพิ่มจาก 20 ครั้ง เป็น 30 ครั้ง แต่ full table scan เพิ่มจาก 1 ล้าน เป็น 1 พันล้าน โต 1,000 เท่า

นี่แหละครับเหตุผลที่ระบบในฉากเปิด พอ data โต 100 เท่า full table scan ก็ช้าลง 100 เท่า + extra penalty อีกหน่อย รวมเป็น ~1,000 เท่า ถ้ามี index ความเร็วแทบไม่ต่างเลย

ทีนี้ index มีหลายแบบ แต่ละแบบเหมาะกับงานคนละแบบ มาเริ่มที่ตัวที่ใช้บ่อยสุดในโลก

B-tree Index — ค้นหาที่นิยมที่สุด#

B-tree (อ่านว่า “บี-ทรี”) คือ index แบบ default ของ Postgres, MySQL, SQL Server, Oracle ทุกค่ายเลือกใช้ตัวนี้เป็นอันดับแรก

Analogy: ลองนึกถึงต้นไม้ที่มีโครงสร้างแบบนี้ครับ — จากราก (root) แตกเป็นกิ่ง 2-3 กิ่ง แต่ละกิ่งแตกเป็นกิ่งย่อยอีก 2-3 กิ่ง ลงไปเรื่อยๆ จนถึงใบ (leaf) ที่เก็บข้อมูลจริง ทุกใบอยู่ลึกเท่ากันหมด เริ่มจาก root เดินไปใบไหนก็ใช้จำนวน hop เท่ากัน

[50]
/ \
[20,35] [70,90]
/ | \ / | \
... ... ... ... ... ← leaf เก็บข้อมูลจริง

เวลาหาเลข 73 — เริ่มจาก root เห็น 50 → 73 มากกว่า 50 ไปขวา → เห็น 70,90 → 73 อยู่ระหว่าง 70 กับ 90 → ลงกิ่งกลาง → เจอ leaf → ได้คำตอบ แค่ 3-4 hop ก็เจอใน data ที่มีล้าน row

B-tree เก่งกับงานพวกนี้:

  • EqualityWHERE id = 5 (หาเป๊ะ)
  • RangeWHERE age > 30 AND age < 40 (หาช่วง)
  • OrderingORDER BY name (เรียงลำดับ)
  • Prefix matchWHERE name LIKE 'วร%' (ขึ้นต้นด้วย — สังเกตว่า LIKE '%วร' ที่ลงท้ายแบบนี้ใช้ B-tree ไม่ได้นะครับ)

ทำไม B-tree ครองตลาด? เพราะมันทำได้หมดเลย ทั้ง equality, range, ordering เป็น tool ที่ “เก่งหลายอย่างพอประมาณ” ครอบคลุม use case 80% ของระบบทั่วไป ถ้าไม่รู้จะเลือก index แบบไหน เลือก B-tree ไว้ก่อน ไม่ผิด

Hash Index — เร็วที่สุดสำหรับ exact match#

Hash Index เป็น index อีกแบบที่เก่งคนละมุม เร็วกว่า B-tree สำหรับงานเฉพาะอย่าง แต่ทำได้น้อยกว่ามาก

Analogy: ลองนึกถึงโรงแรม ถ้าคุณรู้เลขห้อง 305 คุณเดินไปห้อง 305 ตรงๆ ไม่ต้องเปิดสารบัญ ไม่ต้องไล่จากห้อง 1, 2, 3… แค่ดูเลขห้องแล้วไป ใช้เวลาเท่ากันไม่ว่าโรงแรมจะมี 10 ห้องหรือ 10,000 ห้อง

นี่คือ hash — O(1) constant time กี่ row ก็ใช้เวลาเท่าเดิม มัน hash key ที่คุณป้อนเข้ามาเป็นเลข แล้วกระโดดไปตำแหน่งนั้นบน disk โดยตรง

ข้อจำกัดของ Hash:

  • ใช้ได้แค่ exact equality (WHERE id = 5) — เร็วมาก
  • ใช้กับ range query ไม่ได้ (WHERE age > 30 — hash ทำไม่ได้)
  • ใช้กับ ORDER BY ไม่ได้ — เพราะ hash ไม่เก็บลำดับ
  • ใช้กับ prefix match ไม่ได้ (LIKE 'วร%' — hash ทำไม่ได้)

ในวงการ hash index เห็นเด่นใน Redis (key-value store ที่เราคุยใน EP.04) และ Memcached ใน relational database อย่าง Postgres ก็มี hash index ให้เลือก แต่คนใช้น้อย เพราะ B-tree เก่งกว่าในเกือบทุกเคส (B-tree ทำงานของ hash ได้ + ทำงานของ range/order ได้ด้วย)

สรุป: ถ้าระบบของคุณมี query แบบ “หา key เป๊ะๆ” เท่านั้น และไม่เคย range/sort เลย hash เร็วกว่า แต่ใน relational database ส่วนใหญ่ ใช้ B-tree พอ

Composite Index — ดัชนีหลาย column รวมกัน#

จนถึงตอนนี้เราคุยกันเรื่อง index บน 1 column ครับ แต่ในความจริง query ส่วนใหญ่กรองด้วยหลาย column พร้อมกัน เช่น WHERE last_name = 'สมิธ' AND first_name = 'จอห์น'

Composite Index คือการสร้าง index ที่ครอบคลุมหลาย column พร้อมกัน เช่น CREATE INDEX idx_name ON people(last_name, first_name) index ทั้ง 2 column ในลำดับนี้

Analogy: ลองนึกถึงสมุดโทรศัพท์ที่จัดเรียงตามนามสกุลก่อน ถ้านามสกุลเดียวกันก็เรียงตามชื่อต่อ นี่คือ composite index ลำดับสำคัญมาก

ทีนี้กฎสำคัญที่ต้องจำคือ Leftmost Prefix Rule — composite index จะถูกใช้ก็ต่อเมื่อ query เริ่มกรองจาก column ซ้ายสุด ของ index ก่อน

ลอง trace ตัวอย่างนี้ — index คือ (last_name, first_name):

Queryใช้ index ไหม?
WHERE last_name = 'สมิธ'✅ ใช้ได้ (เริ่มจากซ้ายสุด)
WHERE last_name = 'สมิธ' AND first_name = 'จอห์น'✅ ใช้ได้เต็มที่
WHERE first_name = 'จอห์น'❌ ใช้ไม่ได้ (skip ซ้ายสุดไป)
WHERE first_name = 'จอห์น' AND last_name = 'สมิธ'✅ ใช้ได้ (database จัดลำดับเอง — เพราะ AND เปลี่ยนลำดับได้)

เคสที่พลาดบ่อยคือเคสที่ 3 ครับ dev สร้าง composite index ไว้ คิดว่าจะใช้ได้กับทุก query ที่แตะ column นั้น แต่ถ้า query ไม่ได้กรองจาก column ซ้ายสุด index นี้เปล่าประโยชน์ database จะ fallback ไป full table scan เงียบๆ

วิธีออกแบบ composite index ที่ดี — เอา column ที่กรองบ่อยสุดและตัดข้อมูลได้เยอะสุดไว้ซ้าย เช่น ถ้า 90% ของ query กรอง last_name ก็เอา last_name ไว้ซ้าย

Index Trade-off — ทำไมไม่สร้าง index ทุก column#

ฟังมาถึงตรงนี้ ผู้บริหารหลายคนน่าจะคิดว่า “งั้นสร้าง index ทุก column เลยสิ ระบบจะเร็วเสมอ” จริงๆ ไม่ใช่ครับ index มีต้นทุนของมัน

Trade-off 3 ข้อของ index:

1. Read เร็วขึ้น — query ที่ใช้ index วิ่งเร็วเป็นพันเท่า นี่คือเหตุผลที่เราสร้าง index

2. Write ช้าลง — ทุกครั้งที่ INSERT, UPDATE, DELETE database ต้องไปอัพเดต index ทุกตัวที่เกี่ยวข้องด้วย ถ้าตารางมี index 10 ตัว INSERT 1 row จริงๆ คือเขียน 1 row ลง table + อัพเดต 10 index = ทำงาน 11 ครั้ง

3. Storage บวม — index กิน disk space บางตารางที่ index เยอะ ขนาดของ index รวมกันใหญ่กว่า data จริงเสียอีก เปลือง storage + เปลือง memory ตอน database โหลด

Rule of thumb:

  • Column ที่ อ่านบ่อย เขียนน้อย (เช่น เบอร์โทร, อีเมล, ID) → สร้าง index คุ้ม
  • Column ที่ เขียนบ่อย อ่านน้อย (เช่น last_updated_at ที่อัพเดตทุก request แต่แทบไม่มีใคร query) → สร้าง index ก็เป็นภาระ
  • Table ที่ เขียนเยอะมาก เช่น log, event, IoT data → สร้าง index แต่น้อยที่สุด เพราะการเขียนคือ workload หลัก

นี่คือเหตุผลที่ DBA มืออาชีพไม่ “สร้าง index ทุก column” เขาจะดูว่า query ไหนถูกใช้บ่อย แล้วสร้าง index ที่ตรงกับ query นั้น ส่วน column ที่ไม่มีใคร query ไม่สร้างเลย

แล้ว DBA รู้ได้ยังไงว่า query ไหนใช้ index ไหน? คำตอบคือเครื่องมือที่ชื่อว่า EXPLAIN

EXPLAIN PLAN — เครื่องมือดูว่า query ใช้ index ไหน#

EXPLAIN คือคำสั่งของ database ที่บอกว่า ถ้า run query นี้ database จะทำงานยังไง อ่าน table แบบไหน ใช้ index ตัวไหน คาดว่าใช้เวลาเท่าไหร่

ตัวอย่างของ Postgres เวลา query ที่ ไม่มี index:

EXPLAIN SELECT * FROM customers WHERE phone = '0812345678';
Seq Scan on customers (cost=0.00..18334.00 rows=1 width=64)
Filter: (phone = '0812345678'::text)

คำที่ต้องระวังคือ Seq Scan ครับ ย่อมาจาก Sequential Scan = full table scan = เปิดทุก row เห็นคำนี้บน table ใหญ่ = สัญญาณว่า index ขาด

ตัวอย่างเดียวกัน หลังสร้าง index แล้ว

EXPLAIN SELECT * FROM customers WHERE phone = '0812345678';
Index Scan using idx_phone on customers (cost=0.42..8.44 rows=1 width=64)
Index Cond: (phone = '0812345678'::text)

เห็นคำว่า Index Scan + ชื่อ index ที่ใช้ แปลว่า database เปิดสารบัญแล้วกระโดดไป row โดยตรง cost ลดจาก 18,334 เหลือ 8.44 เร็วกว่าพันเท่า

คำที่ควรเข้าใจในผล EXPLAIN:

  • Seq Scan = full table scan (เปิดทุก row) ระวังบน table ใหญ่
  • Index Scan = ใช้ index แล้วไปอ่าน row จาก table — ดี
  • Index Only Scan = ใช้ index แล้วได้คำตอบจาก index เลย ไม่ต้องไปอ่าน table — ดีที่สุด

มุมผู้บริหาร: ทุกครั้งที่ทีม dev บ่นว่า “ระบบช้าครับ” คำถามแรกที่ผู้บริหารควรถาม (โดยไม่ต้องเข้าใจรายละเอียดเทคนิค) คือ “EXPLAIN query นั้นแล้วยัง? เห็น Seq Scan บน table ใหญ่ไหม?” ถ้าทีมตอบว่ายังไม่ได้ดู แปลว่าเขายังไม่ได้ไปดู root cause การ “ซื้อ server แรงขึ้น” ก่อนดู EXPLAIN คือการเผาเงินเปล่า

N+1 Query Problem — เคสคลาสสิคที่ระบบ slow โดยไม่รู้ตัว#

จนถึงตรงนี้เราคุยกันที่ระดับ “1 query ต่อ 1 หน้า” แต่ในความจริง หน้าเว็บ 1 หน้าอาจจะ trigger query 100 หรือ 1,000 ตัวโดยที่ dev ไม่รู้ตัว นี่คือ pattern ที่วงการเรียกว่า N+1 Query Problem ระบบช้าโดยที่ดู code ครั้งแรกไม่เห็นปัญหาเลย

Scenario: สมมติคุณทำหน้า “รายการ order ล่าสุด 100 รายการ” ของระบบ e-commerce แต่ละ order ต้องโชว์ชื่อลูกค้าด้วย

วิธีคิดของ dev (ที่ดูสมเหตุสมผล):

  1. Query 1 ตัว — ดึง order 100 ตัวล่าสุดจาก table orders
  2. Loop ผ่าน order ทีละตัว แต่ละตัว query ดึงชื่อลูกค้าจาก table customers

ผลลัพธ์ — 1 + 100 = 101 query ต่อ 1 page load ถ้าหน้าเว็บนี้มีคนเปิดวันละ 10,000 ครั้ง database ต้องประมวลผล 1,010,000 query ต่อวัน server ระเบิดแน่นอน

ที่ตลกคือ code ที่เขียนแบบนี้ดูสะอาดมากครับ ส่วนใหญ่เกิดจาก ORM (Object-Relational Mapper) เครื่องมือที่ทำให้ dev เขียน query เป็น object แทนที่จะเขียน SQL ตรงๆ ตัวที่ดังๆ คือ Rails ActiveRecord, Django ORM, Sequelize, Prisma ORM มันสะดวก แต่ก็ซ่อน query ไว้ dev เขียน for order in orders: print(order.customer.name) ดูสวย แต่เบื้องหลังคือ 100 query

วิธีแก้คือ Eager Loading หรือ JOIN — ดึงข้อมูลทั้งหมดในรอบเดียว

SELECT orders.*, customers.name
FROM orders
JOIN customers ON orders.customer_id = customers.id
ORDER BY orders.created_at DESC
LIMIT 100;

จาก 101 query เหลือ 1 query เร็วกว่า 100 เท่า

มุมผู้บริหาร: ถ้าหน้าเว็บที่ดู “เรียบง่าย” (รายการ list ธรรมดา) ใช้เวลา load 5 วินาที ส่วนใหญ่คือ N+1 problem ถามทีมว่า “หน้านี้ trigger query กี่ตัว?” ถ้าตอบไม่ได้ แปลว่ายังไม่ได้ตรวจ เปิด log ของ database ดู ถ้าเห็น query ซ้ำๆ pattern เดียวกัน 100 ครั้งติด เจอแล้วครับ

Query Optimizer — สมองของ database#

ทุก query ที่คุณส่งให้ database มันไม่ได้รันทันทีนะครับ database จะเอา query นั้นไปให้ “สมอง” ของมันคิดก่อนว่าจะรันยังไงให้เร็วที่สุด สมองนี้เรียกว่า Query Optimizer

หน้าที่ของ optimizer:

  • ตัดสินว่าจะใช้ index ตัวไหน (ถ้ามีหลายตัวเลือก)
  • ตัดสินว่าจะ JOIN ตารางในลำดับไหน (JOIN A กับ B ก่อน หรือ B กับ C ก่อน ผลลัพธ์เท่ากัน แต่เร็วต่างกัน 100 เท่า)
  • ตัดสินว่าจะใช้ JOIN strategy แบบไหน (Nested Loop, Hash Join, Merge Join แต่ละแบบเหมาะกับ data คนละขนาด)

วิธีที่ optimizer ตัดสินเรียกว่า Cost-Based Optimization มันคำนวณ “ต้นทุน” ของแต่ละ plan แล้วเลือกตัวที่ต้นทุนต่ำสุด ต้นทุนมาจาก statistics ที่ database เก็บไว้เกี่ยวกับ table เช่น “table นี้มีกี่ row” “column นี้มีค่า unique กี่ค่า” “ค่าใน column นี้กระจายตัวยังไง”

ตรงนี้สำคัญครับ ถ้า statistics outdated optimizer จะตัดสินใจผิด เช่น optimizer คิดว่า table มี 1,000 row (จากตอน import data ครั้งแรก) แต่จริงๆ ตอนนี้มี 10 ล้าน row แล้ว optimizer จะเลือก plan ที่เหมาะกับ 1,000 row ซึ่งช้ามากกับ 10 ล้าน row

นี่คือเหตุผลที่ Postgres มีคำสั่ง ANALYZE (และ VACUUM สำหรับงานเก็บกวาด) เพื่ออัพเดต statistics ให้ optimizer มีข้อมูลที่ทันสมัย ใน production ส่วนใหญ่ตั้งให้รันอัตโนมัติทุกคืน ถ้าลืม ระบบที่เคยเร็วก็ช้าลงเงียบๆ

บางครั้ง optimizer ก็ยังตัดสินใจผิดอยู่ดี เช่น data กระจายตัวแบบแปลกๆ ที่ statistics จับไม่ได้ ในเคสนั้น dev สามารถใช้ query hint บังคับให้ database ใช้ index ตัวที่ต้องการ แต่นี่คือทางแก้สุดท้าย ถ้าใช้บ่อยแปลว่ามีอะไรผิดพลาดที่ระดับลึกกว่า

มุมผู้บริหาร — “ระบบช้าเพราะ data เยอะ” คือคำตอบที่ผิด 95% ของเวลา#

มาถึงข้อสรุปที่ EP.07 อยากให้ผู้บริหารจำให้ขึ้นใจครับ — เวลาทีม IT บอกว่า “ระบบช้าเพราะ data เยอะครับ ต้องอัพเกรด server” คำตอบนี้ผิด 95% ของเวลา

คำตอบจริงในเกือบทุกเคส มี 5 อย่าง:

  1. Index ขาด หรือ index ไม่ตรงกับ query — query กรอง column A แต่ index อยู่บน column B
  2. N+1 problem — หน้าเว็บ trigger query เป็นร้อย โดยที่ดู code ครั้งแรกไม่เห็น
  3. Statistics outdated — optimizer ตัดสินใจผิด เพราะข้อมูลเก่า
  4. Bad schema design — table ที่ขาด normalization (เคยคุยใน EP.06) ทำให้ data ซ้ำเยอะ + JOIN แพง
  5. ขาด caching layer — query ที่ผลลัพธ์ไม่ค่อยเปลี่ยน ควร cache ใน Redis แทนที่จะถาม database ทุกครั้ง

ลำดับการแก้ที่ถูก:

  1. EXPLAIN ดูก่อน — เห็น Seq Scan บน table ใหญ่ไหม → สร้าง index
  2. ดู query log — มี query ซ้ำ pattern เดียวกัน 100 ครั้งไหม → แก้ N+1
  3. ดู statisticsANALYZE ครั้งสุดท้ายเมื่อไหร่ → รันใหม่
  4. ดู schema — ตารางสำคัญ normalize ดีไหม → refactor ถ้าจำเป็น
  5. เพิ่ม cache layer — Redis สำหรับ query ที่ผลลัพธ์ไม่ค่อยเปลี่ยน
  6. สุดท้ายจริงๆ — ถ้าทำทุกข้อบนแล้วยังช้า → ค่อยพิจารณาซื้อ server แรงขึ้น

ที่เห็นใน pattern ของวงการ — ระบบที่ “ซื้อ server แรงขึ้น” เป็นวิธีแก้ปัญหาความช้า มักช้าซ้ำอีกใน 6-12 เดือน เพราะ root cause ไม่ได้ถูกแก้ data โตต่อ problem โตต่อ แล้วก็ต้องซื้อ server ใหม่อีก วงจรนี้แพงมากครับ การจ้าง DBA ที่เก่งคนเดียวมาดูครั้งเดียว มักจะถูกกว่าการซื้อ server หลายรอบ

ปิดบท + tease EP.08#

EP.07 จบที่นี่ครับ สรุปสั้นๆ — index ที่ดี = ระบบที่ scale ได้ Full table scan คือศัตรูเงียบที่ทำให้ระบบช้าลงเรื่อยๆ ตามขนาด data B-tree เก่งหลายอย่าง เป็น default ที่ดี Hash เร็วสุดสำหรับ exact match Composite index มีกฎ leftmost prefix ที่ห้ามลืม EXPLAIN PLAN คือเครื่องมือที่ผู้บริหารควรรู้จัก N+1 problem คือ pattern ที่ทำให้หน้าเว็บง่ายๆ ใช้เวลา 5 วินาที Query optimizer คือสมองของ database ที่ต้องเลี้ยงด้วย statistics ที่ทันสมัย

แต่ index ทำให้ “อ่าน” เร็วเท่านั้นนะครับ มันไม่ได้แก้ปัญหาอีกชุดหนึ่งที่เกิดเมื่อ หลายคนพยายาม “เขียน” ข้อมูลตัวเดียวกันพร้อมกัน ลองนึกภาพระบบจองที่นั่งสายการบิน — ที่นั่ง 12A เหลือตัวสุดท้าย มีคน 3 คนกดจองพร้อมกันในวินาทีเดียวกัน database ควรปล่อยให้ใคร “ชนะ”? อีก 2 คนควรเห็นข้อความอะไร? ถ้าตัดสินใจผิด จะมีคน 2 คนซื้อตั๋วใบเดียวกัน

ปัญหานี้เรียกว่า concurrency และวิธีที่ database จัดการเรียกว่า transaction + locking EP.08 จะพาไปดูกันว่าทำไม ACID จาก EP.03 ต้องลงรายละเอียดเพิ่ม lock มีกี่แบบ deadlock คืออะไร แล้วทำไมระบบที่ “ดูเร็ว” บางทีกลับล่มตอนคน 1,000 คนใช้พร้อมกัน

→ EP.08 — Transaction + Concurrency Control (เร็วๆ นี้)