Skip to content
Tayakorn
System Design Handbook

System Design Handbook

การเลือกฐานข้อมูล — SQL vs NoSQL

Database Selection · ACID · BASE · CAP · ▶ ในคลิป 0:06:20 – 0:12:40

ฐานข้อมูลคือการตัดสินใจที่ เปลี่ยนทีหลังแพงที่สุด ในระบบ — โค้ดแอป refactor ได้ทุกสัปดาห์ แต่การย้ายข้อมูล production ข้ามชนิดฐานข้อมูลคือโปรเจกต์ระดับไตรมาส คำถามหลักของบทนี้คือ: งานของเราต้องการ "ความถูกต้องเคร่งครัด" หรือ "ความยืดหยุ่นและสเกล" มากกว่ากัน

2.1 Relational Database (SQL) — โครงสร้างคือจุดแข็ง

ฐานข้อมูลเชิงสัมพันธ์ (PostgreSQL, MySQL) เก็บข้อมูลเป็น ตารางที่มี schema ตายตัว — ทุกแถวมีคอลัมน์เหมือนกัน และตารางเชื่อมโยงกันผ่าน foreign key ทำให้ JOIN ข้อมูลข้ามตารางได้ในคำสั่งเดียว

จุดขายที่แท้จริงของ SQL คือ ACID — สัญญาว่าธุรกรรม (transaction) จะถูกต้องเสมอ:

  • A — Atomicity: ทุกขั้นในธุรกรรมสำเร็จทั้งหมด หรือไม่เกิดขึ้นเลย (โอนเงิน: หักบัญชี A และเข้าบัญชี B ต้องเกิดคู่กัน ห้ามค้างครึ่งทาง)
  • C — Consistency: ข้อมูลไม่ละเมิดกฎที่ตั้งไว้ (ยอดเงินติดลบไม่ได้, FK ต้องชี้ไปที่แถวที่มีจริง)
  • I — Isolation: ธุรกรรมที่รันพร้อมกันไม่เห็นข้อมูลครึ่ง ๆ กลาง ๆ ของกันและกัน
  • D — Durability: commit แล้วข้อมูลไม่หาย แม้ไฟดับวินาทีถัดมา
SQL — ตาราง + ความสัมพันธ์usersid │ name │ email42 │ Anya │ a@x.co43 │ Boon │ b@x.coordersid │ user_id │ total90 │ 42 │ 350.091 │ 42 │ 120.5foreign key → JOIN ได้NoSQL (Document) — เอกสารจบในตัว{"id": 42,"name": "Anya","email": "a@x.co","orders": [{"id":90,"total":350.0},{"id":91,"total":120.5}]}
FIG 2.1 ข้อมูลเดียวกันในสองโลก: SQL กระจายเป็นตารางเชื่อมด้วย key ส่วน document เก็บทุกอย่างที่เกี่ยวข้องไว้ในเอกสารเดียว อ่านครั้งเดียวจบแต่ทำสำเนาซ้ำซ้อนง่าย

2.2 NoSQL — สี่ตระกูล สี่โจทย์

"NoSQL" ไม่ใช่ฐานข้อมูลชนิดเดียว แต่เป็นคำเรียกรวมของฐานข้อมูลที่ สละความเคร่งครัดของ schema และ JOIN เพื่อแลกกับความยืดหยุ่นและการสเกลแนวนอน (scale-out) แบ่งเป็น 4 ตระกูลหลัก:

Key-ValueRedis · DynamoDB · Memcached"session:9f2c" → {user:42,...}"cart:42" → [sku1, sku2]เหมาะกับ: session, cache, ตะกร้าสินค้า — เร็วสุดDocumentMongoDB · CouchDB · Firestore{title:"...", tags:[...], comments:[{...},{...}]}เหมาะกับ: คอนเทนต์, แคตตาล็อก, ข้อมูลโครงสร้างยืดหยุ่นWide-ColumnCassandra · HBase · ScyllaDBrow: device42 → ts1:val ts2:val device43 → ts1:val ...เหมาะกับ: time-series, log, เขียนหนักมหาศาลGraphNeo4j · Neptuneเหมาะกับ: social graph,ระบบแนะนำ, fraud detection
FIG 2.2 สี่ตระกูล NoSQL: เลือกตามรูปร่างของข้อมูลและรูปแบบการอ่าน/เขียน ไม่ใช่ตามกระแส

2.3 Consistency ที่ต้องแลก — BASE และ CAP

NoSQL ส่วนใหญ่สเกลแนวนอนได้ดีเพราะยอมผ่อนความ consistent ลง จากโมเดล ACID ไปเป็น BASE (Basically Available, Soft state, Eventually consistent) — เขียนข้อมูลที่ node หนึ่งแล้ว node อื่นอาจเห็นค่าใหม่ "ในอีกครู่หนึ่ง" ไม่ใช่ทันที

เบื้องหลังคือ CAP Theorem: ในระบบกระจาย (distributed) เมื่อ network ขาดจากกัน (Partition) เราต้องเลือกอย่างใดอย่างหนึ่งระหว่าง

  • C — Consistency: ทุก node ตอบค่าล่าสุดเสมอ (ยอมตอบ error ถ้าไม่ชัวร์)
  • A — Availability: ตอบได้เสมอ (ยอมตอบค่าที่อาจเก่า)

ระบบธนาคารเลือก C (ยอดเงินผิดไม่ได้) ระบบ feed โซเชียลเลือก A (เห็นโพสต์ช้าไป 2 วินาทีไม่มีใครเดือดร้อน) — ไม่มีคำตอบถูกสากล มีแต่คำตอบที่ถูกกับโจทย์

2.4 ตารางตัดสินใจ

มิติSQL (Relational)NoSQL
โครงสร้างข้อมูลSchema ตายตัว ตรวจสอบเข้มยืดหยุ่น เปลี่ยนรูปร่างได้ต่อแถว
ความสัมพันธ์JOIN ได้ในตัว เหมาะข้อมูลโยงกันมากไม่มี JOIN จริง ต้อง denormalize (ทำซ้ำข้อมูล)
TransactionACID เต็มรูปแบบส่วนใหญ่ BASE / eventual consistency
การสเกลถนัด scale-up (เครื่องใหญ่ขึ้น) — scale-out ทำได้แต่ยากออกแบบมาเพื่อ scale-out ตั้งแต่ต้น
QuerySQL ทรงพลัง วิเคราะห์เฉพาะหน้า (ad-hoc) ได้ดีQuery ตามรูปแบบที่ออกแบบไว้ล่วงหน้า
เหมาะกับการเงิน, คำสั่งซื้อ, ระบบที่ผิดไม่ได้Feed, log, IoT, แคตตาล็อก, ทราฟฟิกมหาศาล

🧭 หลักเลือกในชีวิตจริงเริ่มที่ PostgreSQL ไว้ก่อนถ้าไม่แน่ใจ — ระบบส่วนใหญ่ในโลกไม่เคยโตถึงจุดที่ SQL สเกลไม่ไหว และ Postgres ยุคใหม่ทำงาน document (JSONB) ได้ด้วย ค่อยแยกบางส่วนไป NoSQL เมื่อเจอโจทย์เฉพาะจริง ๆ เช่น cache (Redis), เขียน log มหาศาล (Cassandra) — ระบบใหญ่จริงมักใช้ หลายฐานข้อมูลผสมกัน (polyglot persistence) ตามจุดแข็งของแต่ละตัว

2.5 Transaction ข้ามหลายระบบ — Saga, 2PC และ Outbox

ACID ที่เล่าไปข้างต้นใช้ได้เมื่อข้อมูลทั้งหมดอยู่ใน ฐานข้อมูลเดียว แต่ระบบแบบ microservices มักตัดขั้นตอนหนึ่ง (เช่น "สั่งซื้อ") ออกเป็นหลายบริการที่ต่างคนต่างมีฐานข้อมูลของตัวเอง — สั่งซื้อสำเร็จต้อง "สร้างออเดอร์" + "ตัดเงิน" + "จองสต็อก" ให้ครบทั้งสามที่ ถ้าที่ใดที่หนึ่งล้มเหลว ทำอย่างไรไม่ให้ข้อมูลค้างครึ่ง ๆ กลาง ๆ ข้ามระบบ

Two-Phase Commit (2PC) — ความถูกต้องเข้มแต่บล็อกทั้งระบบ

วิธีตรงไปตรงมาคือยกเครื่องมือของ ACID ข้ามระบบ: coordinator ถาม Phase 1 (Prepare) ให้ทุกฝ่าย "เตรียมพร้อม" และล็อกทรัพยากรไว้ก่อน ถ้าทุกฝ่ายตอบ "ได้" coordinator จึงสั่ง Phase 2 (Commit) พร้อมกัน — ได้ความถูกต้องระดับเดียวกับ ACID แต่ราคาที่จ่ายคือ blocking: ทุกฝ่ายต้องถือล็อกรอจนกว่า coordinator จะตัดสินใจ ถ้า coordinator ตายกลางคันหรือฝ่ายใดฝ่ายหนึ่งตอบช้า ทุกระบบที่เกี่ยวข้องค้างพร้อมกันหมด — ยิ่งมีบริการเยอะ โอกาสค้างยิ่งสูง จึงแทบไม่มีใครใช้ 2PC ข้าม microservices ในระบบที่ต้องการ availability สูง

Saga Pattern — ทำสำเร็จทีละขั้น แล้ว "แก้คืน" ถ้าพลาด

Saga เปลี่ยนโจทย์จาก "ทำทุกที่พร้อมกันแบบ atomic" เป็น ลำดับ local transaction ทีละขั้น — แต่ละขั้น commit จริงในฐานข้อมูลของตัวเอง แล้วส่ง event บอกขั้นถัดไปให้ทำงานต่อ ถ้าขั้นใดขั้นหนึ่งล้มเหลว จะไม่มีการ "ROLLBACK ข้ามระบบ" แต่ใช้ compensating transaction — ธุรกรรมที่ทำหน้าที่ "แก้คืน" ผลของขั้นที่สำเร็จไปแล้วย้อนตามลำดับ (เช่น ตัดเงินไปแล้วแต่จองสต็อกไม่ได้ → สั่ง "คืนเงิน" แทนการ rollback)

มีสองสไตล์การจัดลำดับ:

  • Choreography — ไม่มีตัวกลาง แต่ละ service ฟัง event แล้วตัดสินใจทำงานต่อเอง (event-driven ล้วน) เหมาะ flow สั้น ๆ ไม่กี่ขั้น แต่พอ flow ซับซ้อนขึ้นจะไม่มีที่เดียวที่เห็น "ภาพรวมทั้งหมด" — debug ยาก
  • Orchestration — มี "Saga Orchestrator" กลางคอยสั่งทีละขั้นและรู้ทั้ง flow ชัดเจน debug ง่ายกว่า แต่ orchestrator เองก็กลายเป็นจุดที่ต้องดูแลเรื่อง SPOF และความพร้อมใช้งาน (บทที่ 3)
Saga OrchestratorOrder ServicePayment ServiceInventory Service1. Create Order (local tx)✓ สำเร็จ2. Charge Payment (local tx)✓ สำเร็จ3. Reserve Inventory (local tx)✗ สต็อกหมด — ล้มเหลว4. Compensate: Refund Payment↩ คืนเงิน5. Compensate: Cancel Order↩ ยกเลิกออเดอร์
FIG 2.3 Saga แบบ orchestration: เมื่อขั้นตอนหนึ่งล้มเหลว (จองสต็อกไม่ได้) orchestrator สั่ง compensating transaction ย้อนกลับทุกขั้นที่สำเร็จไปแล้วตามลำดับย้อน ไม่ใช่ ROLLBACK ของฐานข้อมูลเดียว

Transactional Outbox — แก้ปัญหา "เขียน DB สำเร็จ แต่ event หลุด"

ทุกแพตเทิร์นข้างต้นพึ่งพา "ส่ง event ให้บริการถัดไปรู้" แต่การเขียนลง database กับการส่ง message ไปคนละระบบกัน — เขียนสำเร็จแต่ส่ง event ไม่สำเร็จ (หรือกลับกัน) คือ dual-write problem ทางแก้มาตรฐานคือ Outbox Pattern: เขียนทั้ง "ข้อมูลจริง" และ "event ที่จะส่ง" ลงตาราง outbox ใน transaction เดียวกัน (atomic เพราะเป็น DB เดียวกัน) แล้วให้ process แยกต่างหากอ่านตาราง outbox แล้วค่อยส่งเข้า message queue จริง — event จะถูกส่งก็ต่อเมื่อ transaction หลักสำเร็จเท่านั้น ไม่มีทางหลุดครึ่งทาง (รายละเอียดของ message queue ที่รับช่วงต่อ อยู่ในบทที่ 9)

มิติ2PCSaga
ConsistencyStrong — atomic ข้ามระบบจริงEventual — ถูกต้องในที่สุด ระหว่างทางอาจเห็นสถานะค้าง
Availabilityต่ำ — บล็อกรอทุกฝ่ายสูง — แต่ละ service ทำงานอิสระ
ความซับซ้อนตรรกะง่าย แต่ operational เสี่ยง (deadlock/ค้าง)ต้องออกแบบ compensating tx + รับมือ retry/idempotency เอง
เหมาะกับระบบปิดที่ควบคุมทุกฝ่ายเอง ทนบล็อกได้Microservices ที่ต้องการ availability สูง

สรุปบทที่ 2 — SQL = โครงสร้าง + ACID + JOIN เหมาะกับข้อมูลที่ผิดไม่ได้ · NoSQL = ยืดหยุ่น + scale-out + eventual consistency เหมาะกับปริมาณและความเร็ว · CAP บอกว่าตอน network มีปัญหา ต้องเลือกระหว่างตอบถูกเสมอ (C) กับตอบได้เสมอ (A) · ธุรกรรมข้ามหลาย service เลือก Saga (เร็ว+พร้อมใช้งานสูง) เหนือ 2PC (บล็อก) เกือบทุกกรณีใน microservices · คำถามแรกเวลาสัมภาษณ์: "ข้อมูลนี้ยอมเก่าได้ไหม และอ่าน-เขียนหนักด้านไหน"

Read the full book