บทที่ 8 · Part 2 — Building Blocks
Datastores
3 แกนที่อธิบายทุก datastore, ทั้ง 9 ตระกูล และของที่เป็นกระแสปี 2026 (ClickHouse, DuckDB, Iceberg, TigerBeetle) พร้อมราคาที่ต้องจ่าย
คำถาม "ควรใช้ฐานข้อมูลอะไร" ตอบไม่ได้ถ้ายังไม่มี access pattern table บทนี้ไม่ตอบคำถามนั้น — บทนี้บอกว่า แต่ละตระกูลคืออะไร เก่งอะไร เก่งเพราะกลไกอะไร และล้มแบบไหน เพื่อให้ตอนคุณกลับไปดู pattern table ในบทที่ 14 คุณเลือกได้เอง
และเพราะปี 2026 มีของใหม่ที่ทีมคุณจะโดนถามในห้องประชุมแน่ๆ ("ทำไมเราไม่ใช้ ClickHouse", "TigerBeetle มันดีจริงไหม", "Iceberg คือ database หรือเปล่า") บทนี้จึงมีหัวข้อที่ไล่ของที่กำลังเป็นกระแสทีละตัว พร้อมกลไกที่ทำให้มันเก่งและราคาที่ต้องจ่าย ไม่ใช่แค่ชื่อกับคำโฆษณา
จบบทนี้คุณจะ
- อธิบายทุก datastore ด้วย 3 แกน เดียวกันได้ แทนที่จะท่องชื่อเป็นรายตัว
- แยกจุดแข็ง/จุดอ่อนของ datastore ทั้ง 9 ตระกูลได้
- รู้ว่า datastore ที่เป็นกระแสในปี 2026 แต่ละตัวเก่งเพราะ กลไกอะไร และแลกอะไรไป
- รู้ว่า search index และ read model ไม่ใช่ source of truth เด็ดขาด
- จัดการ object storage ให้ไม่กลายเป็นข่าวข้อมูลรั่ว
- มี rubric ประเมิน datastore ใหม่ที่ใช้ได้กับตัวที่ยังไม่เกิดในปีหน้าด้วย
3 แกนที่อธิบายทุก datastore
ก่อนจะจำชื่อ ให้จำ 3 แกนนี้ก่อน — เกือบทุกคุณสมบัติของ datastore เป็นผลพวงจาก 3 ข้อนี้ และถ้าคุณอ่าน datastore ใหม่ที่ยังไม่เคยเห็นด้วย 3 แกนนี้ คุณจะเดาจุดแข็งกับจุดพังของมันได้เองก่อนอ่าน benchmark
| แกน | ตัวเลือก | สิ่งที่ตามมาโดยอัตโนมัติ |
|---|---|---|
| 1. Physical layout — เก็บข้อมูลเรียงยังไงบนดิสก์ | Row-oriented: ค่าทุกคอลัมน์ของแถวเดียวกันอยู่ติดกัน | อ่านแถวเดียวครบทุกคอลัมน์ = 1 I/O · แต่ scan คอลัมน์เดียวของ 1 พันล้านแถวต้องอ่านคอลัมน์ที่ไม่ได้ใช้ไปด้วยทั้งหมด |
| Column-oriented: ค่าคอลัมน์เดียวกันของหลายแถวอยู่ติดกัน | คอลัมน์เดียวกันมีชนิดเดียวกัน → compression ratio สูงกว่ามาก และ CPU ทำงานแบบ vectorized ได้ · แต่การประกอบแถวเดียวกลับคืนต้องอ่านทุกคอลัมน์แยกกัน | |
| 2. Write path — เขียนแล้วเกิดอะไร | Update-in-place (B-tree) | อ่านเร็วและคาดเดาได้, รองรับ range scan ตาม index ตรงๆ · write amplification จากการแก้ page และต้องจัดการ page split |
| Append + background merge (LSM-tree) | write throughput สูงมากเพราะเขียนแบบ sequential · แต่ read ต้องดูหลาย level, มี compaction ที่กิน I/O และ tombstone ที่ค้างได้ | |
| Immutable file + manifest (lakehouse) | ไฟล์ไม่เคยถูกแก้ การเปลี่ยนแปลงคือ snapshot ใหม่ → time travel ได้ฟรี · แต่ต้องมี compaction job และเจอ small-file problem | |
| 3. Storage coupling — compute กับ storage ผูกกันแค่ไหน | Local disk (shared-nothing) | latency ต่ำสุดและคาดเดาได้ · แต่ scale = ย้ายข้อมูล และ replication ข้าม AZ คือค่า network จริง |
| Shared disk / shared storage | เพิ่ม read replica ได้เร็วเพราะไม่ต้อง copy ข้อมูล · แต่ storage layer กลายเป็นคอขวดและ single point ที่ต้องดูแล | |
| Object-storage-native ("diskless") | durability ยกให้ object storage, scale-to-zero ได้, ไม่มีค่า inter-AZ replication · แต่ latency พื้นคือ latency ของ object storage ต้องพึ่ง cache ทุกชั้น |
รายละเอียดของ B-tree กับ LSM-tree อยู่ในบทที่ 14 — บทนี้จะใช้คำพวกนี้ต่อโดยไม่อธิบายซ้ำ
ทำไมแกนที่ 1 ทำให้ columnar เร็วแบบก้าวกระโดด
ไม่ใช่เพราะ "อ่านน้อยคอลัมน์" อย่างเดียว แต่เพราะค่าที่อยู่ติดกันเป็นชนิดเดียวกัน จึงบีบอัดด้วย codec เฉพาะทางได้ (delta encoding สำหรับ timestamp ที่เรียงกัน, dictionary encoding สำหรับ enum, run-length สำหรับค่าที่ซ้ำ) ข้อมูลที่เล็กลง 10 เท่าคือ I/O ที่น้อยลง 10 เท่า ก่อนที่ CPU จะเริ่มทำงานด้วยซ้ำ แล้ว CPU ยังทำงานทีละ batch บน array ที่ชนิดเดียวกันได้ (vectorized) แทนที่จะ dispatch ทีละแถว
ตระกูลของ datastore
| ตระกูล | เก่งอะไร | ไม่เก่งอะไร | หยิบมาใช้เมื่อ |
|---|---|---|---|
| Relational — Postgres, MySQL | ACID transaction, join, ad-hoc query, เครื่องมือครบ, constraint ที่ปกป้องข้อมูลจาก bug ของคุณเอง | เพดานการ scale ฝั่ง write (single writer), sharding ต้องทำมือ, การแก้ schema บนตารางใหญ่ต้องระวัง | default โดยเฉพาะอะไรที่เกี่ยวกับเงิน, สิทธิ์, หรือ invariant ข้ามหลายแถว |
| Document — MongoDB, DynamoDB | รูปร่างยืดหยุ่น, scale ตามแนวนอน, เข้าถึงด้วย key ได้เร็ว, latency คาดเดาได้ | transaction ข้าม document จำกัด; query ที่ไม่อยู่ในการออกแบบ key จะช้าหรือทำไม่ได้; schema drift ไปอยู่ในโค้ดคุณ | การเข้าถึงถูกครอบด้วย key ตัวเดียว, รูปร่างต่างกันต่อ record, และต้อง scale เกิน 1 node |
| Wide-column — Cassandra, ScyllaDB, HBase | write throughput มหาศาล, scale เป็นเส้นตรง, write ได้หลาย region, เหมาะกับ time-series | ต้องรู้ query ล่วงหน้า; ไม่มี join; tombstone และ compaction เป็นทักษะ operational จริงจัง | ข้อมูล append-mostly ปริมาณสูงมากที่มี partition ชัด: message, event, metric, feed |
| Key-value / cache — Redis, Valkey, Memcached | latency ต่ำกว่า 1 ms, มีโครงสร้างรวย (sorted set, stream, HyperLogLog) | ผูกกับ memory, single-thread ต่อ shard, durability เป็น config ที่คุณต้องคิดเอง | cache, counter, leaderboard, rate limiting, state ชั่วคราว, queue ขนาดเล็ก |
| Search — Elasticsearch, OpenSearch, Quickwit | full-text relevance, faceting, aggregation บนข้อความจำนวนมาก | near-real-time ไม่ใช่ real-time; ไม่ใช่ source of truth; การดูแล cluster เป็นวิชาเฉพาะ | ค้นหาข้อความอิสระ และ dashboard วิเคราะห์ — populate จาก source of truth เสมอ |
| Analytical / columnar — ClickHouse, BigQuery, Snowflake, DuckDB | scan 1 พันล้านแถว, aggregate ราคาถูก, compress ดี | ไม่เหมาะกับ point lookup แถวเดียว; update/delete เป็นปฏิบัติการราคาแพงไม่ใช่เรื่องปกติ; การโหลดข้อมูลมี latency | รายงาน, BI, reconciliation, fraud analytics, observability — อะไรที่ scan ไม่ใช่ seek |
| Object storage — S3, GCS | ใหญ่ได้ไม่จำกัด, ถูก, durable มาก, มี versioning และ lifecycle rule ในตัว | ไม่ใช่ filesystem ไม่ใช่ database; latency ต่อ object; LIST ไม่ใช่ query engine (ราคาและเวลาโตตามจำนวน object ใน prefix) — ระดับ consistency ต่างกันตาม provider ให้ยืนยันกับเอกสารของตัวที่ใช้ | เอกสาร, รูป, ไฟล์ KYC, backup, data lake, archive tier |
| Time-series — Prometheus, Mimir, Timescale, InfluxDB | เก็บ metric ได้มีประสิทธิภาพ, มี downsampling และ retention ในตัว | cardinality ระเบิดแล้วตาย (ห้าม label ด้วย user_id) | metric และ monitoring — ไม่ใช่ business record |
| Graph — Neo4j, Neptune | query แบบเดินหลาย hop ที่เขียนด้วย SQL เจ็บปวด | วุฒิภาวะ operational, คนน้อย, scale write ยาก | fraud ring, เครือข่ายความเป็นเจ้าของ, recommendation — เมื่อความสัมพันธ์คือข้อมูล |
Real case — Discord ย้ายที่เก็บ message 2 ครั้ง
Discord เผยแพร่เรื่องการย้าย 2 ครั้งห่างกันไม่กี่ปี
ครั้งแรก ย้าย message จาก MongoDB ไป Cassandra โดยเลือก partition key เป็น
(channel_id, bucket) เพื่อให้ message ของ channel เดียวกันอยู่ด้วยกัน
และ time bucket ทำให้ขนาด partition มีขอบเขต — เป็น wide-column model ตามตำรา
ที่ถูกขับด้วย access pattern เดียวคือ "อ่าน message ล่าสุดใน channel นี้"
ครั้งที่ 2 ตอนมี message เป็น 1 ล้านล้าน พวกเขาย้ายจาก Cassandra ไป ScyllaDB และ — ที่สำคัญกว่า — เพิ่ม "data service" เขียนด้วย Rust ไว้หน้าฐานข้อมูล ที่รวม request ที่ขอ partition เดียวกันพร้อมกันให้เป็น query เดียว เพราะ channel ที่ร้อนทำให้เกิดงานซ้ำและ latency spike
บทเรียน 3 ข้อ: partition key คือ design, การ bucket ทำให้ขนาด partition มีขอบเขตตามเวลา, และการวาง service ที่ฉลาดไว้หน้าฐานข้อมูลมักถูกกว่าการเปลี่ยนฐานข้อมูล — How Discord Stores Billions of Messages · …Trillions of Messages
Datastore ที่เป็นกระแสในปี 2026
หัวข้อนี้ไม่ใช่รายการ "ของที่ควรใช้" — เป็นรายการ "ของที่คุณจะโดนถามถึง และควรตอบได้ว่ามันเก่งเพราะอะไร" สังเกตว่าเกือบทุกตัวในตารางนี้ไม่ได้ชนะด้วยการเป็นฐานข้อมูลที่ดีกว่าในทุกด้าน แต่ชนะด้วยการตัดความสามารถบางอย่างทิ้งอย่างจงใจ เพื่อแลกกับความเก่งเฉพาะทางที่ชัดมาก นั่นคือสิ่งที่คุณควรมองหาเวลาประเมินของใหม่
| ตัว | หมวด | จุดแข็งที่จับต้องได้ | ราคาที่จ่าย | ลิงก์ |
|---|---|---|---|---|
| ClickHouse | real-time OLAP | sparse primary index + columnar + vectorized → scan 1 พันล้านแถวได้ในเวลาระดับวินาที บนฮาร์ดแวร์ที่ไม่ต้องมหาศาล | update/delete แพงและไม่ใช่ปฏิบัติการปกติ; ไม่มี transaction แบบ OLTP | clickhouse.com/docs |
| DuckDB | embedded OLAP | analytics engine ที่รันในโปรเซสเดียวกับแอป ไม่มี server ให้ดูแล อ่าน Parquet/Iceberg บน S3 ตรงๆ | โปรเซสเดียว ไม่ได้ออกแบบมาเป็น multi-user server | duckdb.org |
| Apache Iceberg | open table format | ตารางเดียวกันถูกอ่านด้วย engine หลายตัว, snapshot ทำให้ time travel และ rollback เป็นของที่มีอยู่แล้ว | ต้องมี catalog + งาน compaction; ไม่ใช่ database มันคือ "รูปแบบตาราง" | iceberg.apache.org/spec |
| TigerBeetle | OLTP เฉพาะทางเรื่องเงิน | debit/credit อยู่ในฐานข้อมูล ไม่ต้องถือ lock ข้าม network; batch ได้ถึง 8,190 transfer ต่อ request | schema ตายตัว (มีแค่ account/transfer); single leader; ต้องมี Postgres คู่ไปด้วย | docs.tigerbeetle.com |
| Aurora DSQL | distributed SQL | active-active หลาย region พร้อม strong consistency โดยไม่ต้องดูแล node | ไม่มี foreign key/trigger/PL-pgSQL, จำกัด 3,000 แถวต่อ transaction, ต้องเขียน retry เอง | aws.amazon.com/rds/aurora/dsql |
| Valkey | key-value | fork ของ Redis 7.2.4 ภายใต้ Linux Foundation ด้วย BSD-3 — เป็น default ของ managed service หลายเจ้าแล้ว | ecosystem ของ module แยกทางกับ Redis ตั้งแต่จุด fork | valkey.io |
| Postgres + extension | relational ที่กลืนงานคนอื่น | pgvector, PostGIS, TimescaleDB ทำให้ store เดียวครอบ 3–4 use case ได้จริง | ทุก extension คือ dependency ที่ต้องรอดตอน major upgrade | postgresql.org |
| turbopuffer / SlateDB | object-storage-native | ยก durability ให้ object storage → ไม่มีค่า replication ข้าม AZ และ scale ลงเหลือเกือบศูนย์ได้ | latency พื้นคือ latency ของ object storage ต้องพึ่ง cache ทุกชั้น | turbopuffer.com · slatedb.io |
| ScyllaDB | wide-column | shard-per-core เขียนด้วย C++ ไม่มี GC pause → tail latency นิ่งกว่า Cassandra ที่โหลดเท่ากัน | ยังเป็น wide-column อยู่ดี — partition key ผิดก็จบเหมือนกัน | scylladb.com |
| CockroachDB / TiDB | distributed SQL | SQL แบบกระจายที่ shard/rebalance ให้เอง, survive การเสีย node หรือ AZ ได้โดยไม่ต้อง failover ด้วยมือ | latency ต่อ transaction สูงกว่า single-node เพราะต้อง coordinate; ราคาและความซับซ้อนของ ops | cockroachlabs.com · pingcap.com |
| StarRocks / Apache Doris | real-time OLAP | คู่แข่งสาย MPP ของ ClickHouse ที่เน้น join หลายตารางและ query แบบ BI มากกว่า | ecosystem และคนในตลาดยังเล็กกว่า | starrocks.io · doris.apache.org |
อ่านตารางนี้ให้ถูกวิธี
คอลัมน์ "ราคาที่จ่าย" สำคัญกว่าคอลัมน์ "จุดแข็ง" เพราะจุดแข็งคือสิ่งที่ vendor พูดให้ฟรีอยู่แล้ว แต่ราคาคือสิ่งที่คุณจะเจอในเดือนที่ 6 เวลานำเสนอในห้องประชุม ให้พูดราคาก่อน — คนจะเชื่อจุดแข็งที่คุณพูดต่อจากนั้นมากขึ้น
ClickHouse — OLAP ที่ออกแบบให้ scan ไม่ใช่ seek
กลไกที่ทำให้มันเก่ง — ไม่ใช่เวทมนตร์ มันคือ 3 อย่างที่ทำงานทับกัน
- Sparse primary index — ClickHouse ไม่ทำ index ทีละแถว มันแบ่งข้อมูลเป็น granule ขนาด 8,192 แถวโดยค่าเริ่มต้น แล้วเก็บ index 1 entry ต่อ granule ผลคือ index ของตารางระดับหลายล้านแถวมีขนาดเล็กพอที่จะโหลดเข้า memory ได้ทั้งก้อน query จึงทำ binary search บน index ใน memory เพื่อตัด granule ที่ไม่เกี่ยวออกก่อนแตะดิสก์ — เอกสารของ ClickHouse ยกตัวอย่างตาราง 8.87 ล้านแถวที่ได้ประมาณ 1,083 granule และ primary index ขนาดระดับร้อยกิโลไบต์
- Columnar + compression codec เฉพาะชนิด — คอลัมน์ timestamp ที่เรียงกันใช้ delta encoding, คอลัมน์ enum ใช้ dictionary, แล้วทับด้วย LZ4 หรือ ZSTD อีกชั้น
- MergeTree — write เข้ามาเป็น part ที่ immutable แล้วมี background merge รวม part (เป็น LSM ในรูปแบบของ column store) → write path เป็น sequential ล้วน
ราคาที่จ่าย และนี่คือส่วนที่คนข้ามบ่อยที่สุด
ALTER TABLE ... UPDATE/DELETEคือ mutation ไม่ใช่ UPDATE แบบ OLTP — มันเขียน part ใหม่ทั้งก้อน และห้ามแก้คอลัมน์ที่อยู่ใน primary key หรือ sorting key เอกสารของ ClickHouse เองแนะนำให้ใช้ deduplication หรือ lightweight delete แทนการ mutate บ่อยๆ- ไม่มี transaction ข้ามตารางแบบที่คุณใช้ป้องกัน invariant เรื่องเงิน
ORDER BYkey ของตารางคือ design เหมือน partition key ของ Cassandra — เลือกผิดแล้ว query ที่ไม่ตรงกับมันจะกลาย full scan
ในบริบท fintech ควรวางมันไว้ตรงไหน — fraud analytics, product analytics, observability, และงาน reconciliation ที่ต้อง scan ธุรกรรมทั้งวัน ไม่ใช่ ledger และไม่ใช่ที่ที่คุณตัดสินใจหักเงิน — ตรงนั้นดูบทที่ 16
ClickHouse ไม่ทดแทน Postgres และ Postgres ไม่ทดแทน ClickHouse
ข้อผิดพลาดที่เห็นบ่อยคือทีมเอา ClickHouse มาเป็น operational database เพราะ benchmark สวย แล้วไปเจอกำแพงตอนต้องแก้แถวเดียวหรือต้องการ unique constraint ทางกลับกันก็มี: ทีมยัด analytical query ลง read replica ของ Postgres จนกิน I/O จนกระทบ production 2 ระบบนี้อยู่คนละแกนของ "physical layout" ที่อธิบายไว้ข้างบน วิธีเชื่อมที่ถูกคือ CDC จาก Postgres ไป ClickHouse ไม่ใช่เลือกอันใดอันหนึ่ง
DuckDB และ DuckLake — analytics ที่ไม่ต้องมี cluster
กลไก — DuckDB เป็น analytical database ที่รัน ในโปรเซสเดียวกับแอปของคุณ (คนมักเรียกว่า "SQLite ของฝั่ง analytics") ไม่มี server ไม่มี port ไม่มี daemon แต่ข้างในเป็น columnar + vectorized execution เต็มรูปแบบ และมันอ่าน Parquet, CSV, JSON รวมถึงตาราง Iceberg บน object storage ได้โดยตรง โดยไม่ต้อง import เข้ามาก่อน
จุดแข็งที่จับต้องได้
- งาน reconciliation ปลายวันที่เคยต้องยก cluster ขึ้นมา กลายเป็น job เดียวในคอนเทนเนอร์เดียว
- Data engineer ทดสอบ query บนข้อมูลจริงบนเครื่องตัวเองได้ ไม่ต้องรอ warehouse quota
- ใช้เป็น query engine ฝังใน service เพื่อทำ aggregation บนไฟล์ที่ดึงมาจาก S3
ราคาที่จ่าย — โปรเซสเดียว ไม่ใช่ multi-user database server, ไม่มี concurrency control สำหรับผู้เขียนหลายคนพร้อมกันแบบ Postgres, และขนาดข้อมูลที่ทำงานได้ผูกกับเครื่อง 1 เครื่อง (แม้จะไปได้ไกลกว่าที่คนคิดมาก)
DuckLake คือส่วนขยายที่เก็บ metadata ของ lakehouse ไว้ใน SQL database แทนที่จะเป็น ไฟล์ catalog บน object storage — ผลคือ metadata lookup และ partition pruning เร็วขึ้น และได้ multi-table transaction กับ time travel มาในอินเทอร์เฟซ SQL เดียว — ducklake.select · DuckDB Iceberg extension
Apache Iceberg — table format ที่แยก storage ออกจาก engine
นี่คือของที่คนสับสนมากที่สุดในตารางนี้ Iceberg ไม่ใช่ database มันคือ สเปกว่าไฟล์ Parquet กองหนึ่งบน object storage จะถูกเรียกว่า "ตาราง" ได้ยังไง — มี manifest บอกว่าไฟล์ไหนอยู่ในตาราง, มี snapshot บอกว่าสถานะ ณ เวลาหนึ่งคืออะไร, และมี schema evolution ที่ไม่ต้องเขียนข้อมูลใหม่
จุดแข็งที่จับต้องได้ — เป็นเรื่อง lock-in มากกว่าเรื่องความเร็ว
- ตารางเดียวกันถูกอ่าน/เขียนได้จากหลาย engine (Spark, Trino, Flink, DuckDB, ClickHouse, Snowflake, Databricks) → คุณเปลี่ยน query engine ได้โดยไม่ต้องย้ายข้อมูล ซึ่งเป็น exit path ที่ประเมินค่าเป็นตัวเงินได้ในการเจรจากับ vendor
- Snapshot ทำให้ time travel และ rollback เป็นของที่มีอยู่แล้ว ไม่ต้องสร้างเอง — มีค่ามากตอนตรวจสอบย้อนหลังและตอน pipeline เขียนข้อมูลผิด
- Optimistic concurrency + serializable isolation ตามสเปก โดย reader ไม่ต้องถือ lock
สถานะสเปก (ยืนยันจากสเปกโดยตรง) — เวอร์ชัน 1, 2 และ 3 เสร็จและถูกรับรองโดยชุมชนแล้ว ส่วนเวอร์ชัน 4 ยังอยู่ระหว่างพัฒนาและยังไม่ถูกรับรองอย่างเป็นทางการ
| Spec version | เพิ่มอะไร |
|---|---|
| v1 | ตาราง analytic บนไฟล์ immutable (Parquet, Avro, ORC) |
| v2 | row-level delete ผ่าน delete file — ลบ/แก้ทีละแถวได้โดยไม่ต้องเขียนไฟล์ใหม่ทั้งไฟล์ |
| v3 | ชนิดข้อมูลใหม่ (nanosecond timestamp, variant, geometry, geography, unknown), default value ของคอลัมน์, multi-argument transform, row lineage, binary deletion vector, table encryption key |
| v4 | (กำลังพัฒนา) ปรับโครงสร้าง metadata และรองรับ relative location |
ราคาที่จ่าย — ต้องมี catalog (ตัวที่บอกว่าตารางล่าสุดอยู่ที่ metadata ไฟล์ไหน), ต้องมี maintenance job สำหรับ compaction และลบ snapshot เก่า และถ้า pipeline เขียนไฟล์เล็กๆ ถี่ๆ คุณจะเจอ small-file problem ที่ทำให้ query ช้าลงเรื่อยๆ
ฝั่ง AWS มี S3 Tables ที่ทำ Iceberg ให้เป็น storage primitive ที่จัดการ compaction ให้ ซึ่งลดงาน maintenance ข้างต้นไปได้ส่วนหนึ่ง — แลกกับการผูกกับ managed service ของเจ้าเดียว
TigerBeetle — OLTP ที่ตัดทุกอย่างทิ้งเพื่อ debit/credit
ตัวนี้เกี่ยวกับงานที่พวกเราทำโดยตรงที่สุดในรายการทั้งหมด จึงควรเข้าใจให้ละเอียดกว่าตัวอื่น
กลไกที่ทำให้มันเก่ง — จุดสำคัญคือเรื่อง interface ไม่ใช่เรื่อง engine
ฐานข้อมูลทั่วไปให้ business logic อยู่ที่แอป แล้วแอปต้องถือ lock ข้ามเน็ตเวิร์ก ระหว่างที่คิดว่าจะหักเงินเท่าไหร่ — พอ contention สูง (บัญชีร้อนไม่กี่บัญชีอยู่ในธุรกรรมส่วนใหญ่) ทั้ง latency และ throughput พังพร้อมกันตาม Little's Law TigerBeetle ย้าย debit/credit เข้าไปอยู่ในฐานข้อมูลเอง จึงไม่ต้องมี lock ข้ามเน็ตเวิร์กเลย
เมื่อ interface ถูกแล้ว วิศวกรรมที่เหลือถึงจะมีผล — และของพวกนี้เป็นรูปธรรมมาก
| การตัดสินใจ | ผลที่จับต้องได้ |
|---|---|
| ทุก request คือ batch สูงสุด 8,190 transfer | ต้นทุนของ consensus จ่ายครั้งเดียวต่อ batch ไม่ใช่ต่อ transfer · และตอนโหลดเบา batch จะเล็กลงเองเพื่อแลกกลับเป็น latency ที่ดีขึ้น |
Transfer มีขนาด 128 ไบต์ และ cache-line aligned | การประมวลผล 1 batch กลายเป็น CPU loop เดียวที่แน่น |
| จองหน่วยความจำแบบ static ทั้งหมด | ไม่มี GC pause, ไม่มี memory fragmentation, ไม่มีเคส out-of-memory กลางทาง |
เขียนด้วย Zig, ไม่มี dependency ภายนอก, ออกแบบรอบ io_uring | ทุกชั้นถูกออกแบบร่วมกันเพื่อ workload เดียว |
| Viewstamped Replication + strict serializability | มีรายงานวิเคราะห์จาก Jepsen ให้อ่านเป็นหลักฐานอิสระ |
การสร้าง account/transfer เป็น idempotent ตาม id | สร้างซ้ำด้วย id เดิมได้ผลลัพธ์ exists ไม่ใช่ยอดซ้ำ — ตรงกับ idempotency key ในบทที่ 4 |
ราคาที่จ่าย และมันแพงจริง
- Schema ตายตัว — เก็บได้แค่
AccountกับTransferไม่มีชื่อลูกค้า ไม่มีสถานะ KYC ไม่มี metadata ของธุรกรรม แปลว่าคุณยังต้องมี Postgres คู่ไปด้วยเสมอ และคุณเพิ่งสร้างปัญหา dual-write ให้ตัวเอง (วิธีแก้อยู่ในบทที่ 16) - Single-threaded และ single leader โดยตั้งใจ — เอกสารระบุตรงๆ ว่าการเพิ่ม node เพิ่มความน่าเชื่อถือ ไม่ใช่ throughput เหตุผลคือการ shard ฐานข้อมูลการเงินทำได้ยาก เพราะบัญชีร้อนไม่กี่บัญชีจะทำให้ shard นั้นเป็นคอขวดอยู่ดี
- ไม่มี SQL, ไม่มี ad-hoc query, เครื่องมือ ops รอบตัวยังน้อยกว่า Postgres มาก
- ทีมต้องเรียนรู้ data model แบบ debit/credit ให้แน่นก่อน ไม่ใช่หลัง
เกณฑ์ตัดสินใจที่ตรงไปตรงมา
ถ้า ledger ของคุณยังอยู่ในระดับที่ Postgres รับไหวโดย p99 ยังอยู่ในงบ ใช้ Postgres ต่อไป — double-entry ledger ที่ถูกต้องใน Postgres มีค่ามากกว่า ledger ที่เร็วในฐานข้อมูลที่ทีมยังไม่เคย operate จริง
TigerBeetle เริ่มคุ้มเมื่อคุณวัดได้แล้วว่า contention บนบัญชีร้อน (บัญชี settlement, บัญชี merchant รายใหญ่, บัญชีตัวกลางของ scheme) เป็นคอขวดจริง ไม่ใช่แค่กลัวว่าจะเป็น — และการตัดสินใจย้าย ledger core ต้องมีเจ้าของที่รับผิดชอบเซ็นชื่อพร้อมวันที่ ไม่ใช่การตัดสินใจของ engineer คนเดียว
Aurora DSQL และตระกูล distributed SQL
สัญญาของหมวดนี้ — SQL ที่ scale ตามแนวนอนและทน node/AZ ตายได้ โดยที่คุณยังเขียน SQL เหมือนเดิม Aurora DSQL (GA เมื่อ 27 พ.ค. 2025) เป็นตัวที่ผลักไปไกลสุดฝั่ง serverless: active-active หลาย region พร้อม strong consistency โดยไม่มี node ให้คุณดูแลเลย
แต่ "PostgreSQL-compatible" ไม่ได้แปลว่า drop-in และนี่คือบทเรียนที่ใช้ได้กับทั้งหมวด ข้อจำกัดต่อไปนี้มาจากเอกสารของ AWS โดยตรง
| ข้อจำกัด | ผลต่อโค้ดที่คุณมีอยู่ |
|---|---|
| ไม่มี foreign key — ต้องตรวจ referential integrity ที่ชั้นแอป | invariant ที่คุณเคยฝากไว้กับ DB ย้ายมาอยู่ในโค้ดที่มี bug ได้ |
| ไม่มี trigger และไม่มี PL/pgSQL (มีแต่ SQL function) | logic ที่อยู่ใน trigger ต้องย้ายไป application หรือ event-driven |
| Optimistic concurrency control — ไม่มี lock, ชนกันแล้วได้ serialization error ตอน commit | ทุก transaction ต้องมี retry loop และต้อง idempotent |
| Isolation คงที่ที่ Repeatable Read เท่านั้น | ปรับ isolation ต่อ transaction ไม่ได้ |
| 1 transaction แก้ได้ไม่เกิน 3,000 แถว (นับรวมทุก DML) | batch job ที่เคยอัปเดต 10,000 แถวใน transaction เดียวต้องเขียนใหม่ |
| DDL กับ DML ต้องแยก transaction และ 1 DDL ต่อ 1 transaction | migration script ส่วนใหญ่ต้องรื้อ |
1 database ต่อ cluster, ไม่มี temp table, collation C, timezone UTC, connection หมดอายุใน 1 ชั่วโมง | connection pool และ query ที่พึ่ง collation ต้องตรวจใหม่ |
วิธีอ่านข้อจำกัดพวกนี้ให้เป็นประโยชน์ — มันไม่ใช่ "ข้อบกพร่อง" แต่คือราคาของ active-active ข้าม region ที่ต้องมีคนจ่าย ระบบกระจายที่ไม่ต้องการ coordinator กลางจะไม่มีทางให้ FK แบบที่ตรวจข้ามทั้งคลัสเตอร์ได้ฟรี ทางเลือกอื่นในหมวดนี้ (CockroachDB, TiDB, Spanner) เลือกจ่ายราคาคนละจุดกัน — ตัวที่ให้ FK มักจ่ายเป็น latency ต่อ transaction แทน
คำถามที่ต้องถามก่อนเลือก distributed SQL
"เราต้องการ write หลาย region พร้อมกัน จริงหรือ หรือแค่ต้องการ survive การเสีย 1 region" คำตอบ 2 ข้อนี้นำไปสู่ architecture คนละแบบและราคาคนละระดับ — ส่วนใหญ่ต้องการอย่างหลัง ซึ่ง Postgres แบบ multi-AZ + DR ที่ซ้อมแล้วตอบได้ ดูบทที่ 13
Valkey กับ Redis — เรื่อง license ที่กลายเป็นเรื่อง architecture
ลำดับเหตุการณ์ที่ควรรู้ เพราะมันกระทบว่า managed service ที่คุณใช้จะเป็นตัวไหนในอีก 3 ปี
- มี.ค. 2024 — Redis เปลี่ยนจาก BSD ไปเป็น license แบบ source-available (RSALv2 / SSPLv1)
- มี.ค. 2024 — Linux Foundation ประกาศ Valkey ซึ่ง fork มาจาก Redis 7.2.4 ภายใต้ BSD-3 โดยมีผู้ร่วมก่อตั้งเป็นผู้ให้บริการคลาวด์รายใหญ่หลายเจ้า
- พ.ค. 2025 — Redis เพิ่มทางเลือก AGPLv3 ให้ Redis 8 (ประกาศจาก Redis)
ทำไม engineer ต้องสนใจ — เพราะ managed service คือสิ่งที่คุณใช้จริง ไม่ใช่ซอร์สโค้ด เมื่อผู้ให้บริการคลาวด์ย้ายค่าเริ่มต้นไปทาง Valkey สิ่งที่ตามมาคือ roadmap ของฟีเจอร์, โครงสร้างราคา, และชุด module ที่ใช้ได้ เดินแยกทางกันตั้งแต่จุด fork
สิ่งที่ต้องตรวจก่อนเลือกข้าง (เป็นรูปธรรม)
- คุณใช้ module อะไรอยู่ (search, JSON, time-series, bloom) และมันมีในฝั่งที่คุณจะไปไหม
- client library ที่ทีมใช้ ประกาศรองรับเวอร์ชันไหนของฝั่งไหน
- ถ้าเป็นงานที่ regulated ทีม legal ต้องรู้ว่าคุณใช้ AGPLv3 หรือ BSD-3 — นี่เป็นคำถามที่ต้องส่งให้เจ้าของด้าน legal ตอบ ไม่ใช่ engineer ตัดสินเอง
Postgres ที่กลืนงานของ store อื่นไปเรื่อยๆ
กระแสที่แรงที่สุดในปี 2026 อาจไม่ใช่ฐานข้อมูลใหม่เลย แต่คือการที่ Postgres ทำงานที่เคยต้องใช้ store แยกได้ "ดีพอ" มากขึ้นทุกปี จนเกณฑ์ที่จะเพิ่ม store ใหม่สูงขึ้นเรื่อยๆ
| งาน | เคยต้องใช้ | ตอนนี้ทำใน Postgres ได้ด้วย | เพดานที่ควรรู้ |
|---|---|---|---|
| semantic search | vector database แยก | pgvector (HNSW / IVFFlat) | ระดับหลายสิบล้าน vector ขึ้นไปเริ่มคุ้มที่จะแยก |
| spatial query | ระบบ GIS แยก | PostGIS | polygon จริงและ index เชิงพื้นที่ครบ |
| time-series | InfluxDB | TimescaleDB / partition ตามเวลา | cardinality ยังเป็นข้อจำกัดเหมือนเดิม |
| queue เบาๆ | Redis / SQS | SELECT ... FOR UPDATE SKIP LOCKED | throughput สูงมากๆ ยังควรใช้ broker จริง |
| JSON document | MongoDB | jsonb + GIN index | schema drift ยังเป็นปัญหาเดิม |
ฝั่งตัวเครื่องยนต์เองก็ขยับ — PostgreSQL 18
เพิ่มระบบ asynchronous I/O (เลือก method ได้ทั้งแบบ worker และ io_uring),
uuidv7() ที่ให้ id เรียงตามเวลาจึงเป็นมิตรกับ B-tree index มากกว่า UUIDv4,
virtual generated column, skip scan สำหรับ B-tree และ OAuth 2.0 authentication
`uuidv7()` แก้ปัญหาที่เจอบ่อยในระบบเงิน
UUIDv4 สุ่มทั้งก้อน → การ insert กระจายทั่ว B-tree ทำให้ page ร้อนกระจายและ index โตเร็ว UUIDv7 มี timestamp เป็น prefix → insert ไปตกใกล้ๆ กันตามเวลา ได้ locality ที่ดีกว่าโดยยังไม่เดา id ถัดไปได้ง่ายเท่า sequential integer รายละเอียดเรื่องการเลือก ID อยู่ในบทที่ 10
[!WARNING] ทุก extension คือหนี้ตอน major upgrade "ใช้ Postgres ตัวเดียวจบ" ฟังดูเรียบง่าย แต่ Postgres ที่มี 6 extension คือ 6 โครงการที่ต้องออกเวอร์ชันรองรับ major version ใหม่ ก่อน ที่คุณจะอัปเกรดได้ ก่อนเพิ่ม extension ให้ถามว่า "ถ้าตัวนี้ตามไม่ทัน Postgres เวอร์ชันหน้า เราจะทำยังไง"
Object-storage-native — เมื่อ S3 กลายเป็นดิสก์หลัก
แนวคิด — แทนที่จะเก็บ state บนดิสก์ของ node แล้ว replicate ข้าม AZ เอง ระบบกลุ่มนี้ยก durability ทั้งหมดให้ object storage แล้วใช้ NVMe กับ RAM เป็น cache เท่านั้น node จึงกลายเป็นของที่ทิ้งได้จริง ไม่มี state ที่ต้องกู้
จุดแข็งที่จับต้องได้
- ไม่มีค่า replication ข้าม AZ เพราะ object storage จัดการ durability ให้แล้วในราคาของมัน
- scale ลงเหลือเกือบศูนย์ได้ — workload ที่ query เป็นช่วงๆ ไม่ต้องจ่ายค่า cluster ที่เปิดค้างไว้
- ไม่มี state ที่ต้อง rebalance ตอนเพิ่ม/ลด node ทำให้ operational model ง่ายลงมาก
- ต้นทุนต่อ GB ของ object storage ต่ำกว่า memory มาก — เหมาะกับข้อมูล cold ที่ต้องค้นได้เป็นครั้งคราว (ตัวเลขเปรียบเทียบที่ vendor เผยแพร่เป็น illustrative ให้ยืนยันกับ pricing ของ region คุณเอง)
ราคาที่จ่าย — latency พื้นของ object storage อยู่ระดับ 10 มิลลิวินาทีขึ้นไปต่อ request ระบบกลุ่มนี้จึงต้องมี cache ทุกชั้นและ p99 จะแย่ลงชัดเจนตอน cache miss ไม่เหมาะกับ write ที่ต้อง ack ในระดับมิลลิวินาทีเดียว หรือ read ที่ต้องนิ่งทุก request
ตัวอย่างที่อ่านได้จริง — turbopuffer (search + vector), SlateDB (LSM ที่เขียนลง object storage ตรง), Quickwit (log search) และฝั่ง streaming ก็มี WarpStream ที่ทำ Kafka protocol บน object storage ล้วน (Confluent ประกาศเข้าซื้อ) — รูปแบบเดียวกันโผล่ในหลายหมวดพร้อมกัน นั่นคือสัญญาณว่ามันเป็นแนวสถาปัตยกรรม ไม่ใช่ผลิตภัณฑ์เดี่ยว
วิธีประเมิน datastore ที่กำลังมาแรง
ของในหัวข้อที่แล้วจะเปลี่ยนไปในอีก 2 ปี แต่วิธีประเมินไม่เปลี่ยน ใช้ตารางนี้กับ datastore ตัวไหนก็ได้ รวมถึงตัวที่ยังไม่เกิด
| คำถาม | หลักฐานที่ยอมรับได้ | ธงแดง |
|---|---|---|
| มันเก่งเพราะกลไกอะไร | อธิบายได้ด้วย 3 แกนข้างบน และมีเอกสารสถาปัตยกรรมของตัวเอง | ตอบได้แค่ "เร็วกว่า X เท่า" จาก benchmark ของ vendor เอง |
| ความถูกต้องถูกตรวจโดยคนนอกหรือยัง | รายงาน Jepsen, formal verification, deterministic simulation testing ที่เผยแพร่ | "เรามี test ครบ" โดยไม่มีอะไรให้อ่าน |
| ตอนพังมันพังยังไง | เอกสารเรื่อง failure mode, การ recover, พฤติกรรมตอน network partition | เอกสารมีแต่ happy path |
| License และ governance | license ที่ทีม legal อนุมัติแล้ว + governance ที่ไม่ได้ขึ้นกับบริษัทเดียว | license ที่เพิ่งเปลี่ยน หรือเปลี่ยนได้ฝ่ายเดียว |
| ทางออกคืออะไร | export ออกเป็น format มาตรฐานได้ (Parquet, Iceberg, logical dump) | ข้อมูลออกได้ทางเดียวคือ API ของเจ้าของ |
| ใครดูแลตอนตีสาม | มีคนในทีมอย่างน้อย 2 คนที่เคย operate จริง หรือมี managed service ที่มี SLA | "เดี๋ยวเราเรียนรู้ระหว่างทาง" กับระบบที่มีเงินอยู่ในนั้น |
| backup กู้ได้จริงไหม | เคยซ้อม restore ลงระบบเปล่าและจับเวลา | มี backup แต่ไม่เคยกู้ |
| มันแทน store เดิมหรือมาเพิ่ม | มีแผนปลดของเดิมพร้อมวันที่ | คำตอบคือ "เพิ่ม" ทุกครั้ง |
Deterministic simulation testing — คำที่จะได้ยินบ่อยขึ้น
ฐานข้อมูลรุ่นใหม่หลายตัวโฆษณาว่าใช้ deterministic simulation testing: รันทั้งระบบในโลกจำลองที่เวลา, network, และดิสก์ถูกควบคุมด้วย seed แล้วยิงความผิดพลาด (packet หาย, ดิสก์คืนข้อมูลเพี้ยน, node ตายกลาง commit) เป็นล้านรอบ จุดสำคัญคือเมื่อเจอ bug มัน reproduce ได้ด้วย seed เดิมเสมอ
มันเป็นสัญญาณที่ดี แต่ไม่ใช่หลักฐานว่าถูกต้อง — สิ่งที่ควรถามคือ "จำลองความผิดพลาดของดิสก์ด้วยหรือเปล่า" เพราะข้อมูลเสียหายเงียบๆ ระดับ storage คือหมวดที่ระบบส่วนใหญ่ทดสอบน้อยที่สุด
Search index — derived เสมอ ไม่เคยเป็น authoritative
- Populate จากฐานข้อมูลผ่าน CDC หรือ outbox เพื่อให้สร้างใหม่จากศูนย์ได้เสมอ
- ตั้งงบเวลาสำหรับ full reindex — คุณจะต้องทำทุกครั้งที่แก้ mapping
- คาดหวัง near-real-time (~1 s refresh) และออกแบบ UX ให้รองรับ ("เอกสารของคุณกำลังถูกจัดทำดัชนี")
- คืนแค่ id จาก search แล้วไปดึงรายละเอียดจาก source of truth ป้องกันการแสดงข้อมูลเก่าและป้องกัน PII ค้างใน index
ตัวเลือกในหมวดนี้ปี 2026 กว้างขึ้นกว่าเดิม: Elasticsearch กับ OpenSearch เป็นสาย cluster เต็มรูปแบบ, ส่วน Quickwit และ turbopuffer เป็นสาย object-storage-native ที่แลก latency พื้น กับต้นทุนที่ต่ำลงมากสำหรับข้อมูล cold — เลือกจากรูปร่างของ query ไม่ใช่จากความใหม่
อย่าใส่ข้อมูลที่ต้องลบได้ลง search index โดยไม่มีแผน
ภายใต้ PDPA คำขอลบข้อมูลต้องไปถึงทุกที่ที่มีสำเนา ถ้า search index มีชื่อ-นามสกุล-เบอร์โทร คุณต้องมีกลไกลบที่นั่นด้วย และต้องพิสูจน์ได้ว่าลบแล้ว ทางที่ง่ายกว่าคือ index เฉพาะ id กับ field ที่ต้องใช้ค้นจริง
ข้อนี้ใช้กับ derived store ทุกตัว ไม่ใช่แค่ search — ClickHouse ที่รับ CDC มาจาก Postgres ก็เป็นสำเนาที่ต้องลบได้เหมือนกัน และการลบใน columnar store แพงกว่าที่คิดมาก ให้ออกแบบตั้งแต่วันแรกว่า จะไม่ส่ง field ที่ระบุตัวบุคคลเข้าไป แทนที่จะไปหาวิธีลบทีหลัง
Object storage — ที่ที่ข้อมูลรั่วบ่อยที่สุด
| กฎ | เหตุผล |
|---|---|
| ใช้ pre-signed URL เสมอ | byte ไม่ต้องผ่าน application server เลย — ประหยัด bandwidth และ CPU |
| Validate content type และขนาดที่ server หลัง upload | content type ที่ client ประกาศมาคือคำโกหกที่รอวันเกิด |
| Scan malware ถ้าผู้ใช้ดาวน์โหลดไฟล์ของกันได้ | ไม่อย่างนั้นคุณกลายเป็น CDN แจกมัลแวร์ |
| เปิด versioning และ lifecycle rule วันแรก | undelete ได้ และย้ายไป cold tier อัตโนมัติ |
| เปิด block public access และตรวจสอบว่ามันเปิดจริง | bucket ที่ config ผิดยังเป็นสาเหตุข้อมูลรั่วที่พบบ่อยที่สุดในโลกจริง |
| แยก bucket ตาม data classification | ไฟล์ KYC ไม่ควรอยู่ bucket เดียวกับ asset ของหน้าเว็บ |
| เปิด server-side encryption ด้วย key ที่คุณควบคุม สำหรับ bucket ที่มีข้อมูลลูกค้า | ทำให้การเพิกถอน key เป็นกลไกควบคุมได้จริง ไม่ใช่แค่บรรทัดในนโยบาย |
Upload flow ที่ถูก
1. Client -> API: POST /v1/documents (ประกาศ type, ขนาด, purpose)
2. API: ตรวจสิทธิ์, สร้าง record (status = AWAITING_UPLOAD),
คืน pre-signed PUT URL ที่ผูก key + content-length + หมดอายุ 5 นาที
3. Client -> S3: PUT byte ตรงเข้า object storage
4. S3 -> API: event notification ว่า object มาถึง
5. API: อ่าน metadata จริง (magic bytes, ขนาด), scan, แล้ว
set status = READY หรือ REJECTED
--> ห้ามเชื่อขั้นที่ 1 เลย ทุกอย่างต้องยืนยันที่ขั้นที่ 5
Pre-signed URL ที่ผูกไม่แน่น
pre-signed URL ที่ไม่จำกัด Content-Length ทำให้ใครอัปโหลดไฟล์ 50 GB ก็ได้
และที่ไม่จำกัด key prefix ทำให้เขียนทับ object ของคนอื่นได้
ผูก key ที่แน่นอน + content-length range + expiry สั้น ทุกครั้ง
Geo / spatial
| เทคนิค | แนวคิด | เหมาะกับ |
|---|---|---|
| Geohash | encode พิกัดเป็น string — prefix ที่ตรงกัน = พื้นที่เดียวกัน | index ได้ในทุก store แม้ไม่มีความสามารถ spatial |
| S2 cell (Google) | แบ่งโลกเป็น cell บนลูกบาศก์แล้ว map ลงเส้น Hilbert | ต้องการความแม่นของพื้นที่และการ cover region |
| H3 hexagon (Uber) | hexagon แบบลำดับชั้น — ระยะถึงเพื่อนบ้านสม่ำเสมอกว่า grid สี่เหลี่ยม · ข้อจำกัดที่ต้องรู้: grid สร้างบน icosahedron จึงมี pentagon 12 เซลล์ และ cell ลูกไม่ซ้อนทับ cell แม่แบบพอดี | จับคู่ผู้ใช้-คนขับ, การรวมสถิติเชิงพื้นที่ |
| PostGIS | spatial extension บน Postgres รองรับ polygon จริง | ครอบคลุมความต้องการส่วนใหญ่ได้ และคุณมี Postgres อยู่แล้ว |
ข่าวดีของปี 2026 คือฝั่ง analytics ตามมาแล้ว — Iceberg spec v3 เพิ่มชนิด geometry และ geography
เข้ามาในตัว แปลว่าข้อมูลเชิงพื้นที่บน lakehouse ไม่ต้องเก็บเป็น WKT string แล้วแปลงเองอีกต่อไป
รายละเอียดการออกแบบ nearby search อยู่ในบทที่ 19 · อ้างอิง: H3 — Uber's hexagonal hierarchical spatial index
Time-series — ระวัง cardinality
Label ที่ทำให้ Prometheus ตาย
ห้าม: http_requests_total{user_id="u_918273", path="/v1/transfers/trf_01J9XYZ"}
--> 1 ล้านผู้ใช้ x 500,000 transfer = time series หลายพันล้านเส้น
ควร: http_requests_total{route="/v1/transfers/:id", method="POST", status="201"}
--> route ที่ normalize แล้ว x method x status = หลักร้อยเส้น
กฎ: label ต้องมีค่าที่เป็นไปได้จำกัดและรู้ล่วงหน้า ถ้าต้องการสืบย้อนถึง request รายตัว นั่นคืองานของ trace และ log ไม่ใช่ metric — Prometheus metric and label naming
ทำไม cardinality ถึงฆ่าระบบได้ — แต่ละชุด label ที่ไม่ซ้ำกันคือ time series 1 เส้น ที่ต้องมี index, มี memory ค้างไว้สำหรับ chunk ล่าสุด และมีต้นทุนตอน query ที่ต้องรวมเส้นเหล่านี้ การเพิ่ม label 1 ตัวที่มี 10,000 ค่าเป็นการคูณ จำนวนเส้นทั้งหมดด้วย 10,000 ไม่ใช่บวก นี่คือเหตุผลที่มันพังแบบฉับพลันไม่ใช่ค่อยๆ ช้าลง
ถ้าต้องเก็บ metric ระยะยาวหลายทีมรวมกัน ฝั่ง long-term storage อย่าง Grafana Mimir หรือ Thanos ช่วยเรื่อง retention และ multi-tenant ได้ — แต่ไม่ได้แก้ปัญหา cardinality ให้ ปัญหานั้นแก้ได้ที่ตอนตั้งชื่อ label เท่านั้น
Vector store — pgvector ก่อน แล้วค่อยคิดเรื่องอื่น
ใช้เมื่อทำ semantic search หรือ retrieval สำหรับฟีเจอร์ AI เท่านั้น ถ้า query ของคุณเป็นการหาคำที่ตรงกัน search index ธรรมดายังเป็นคำตอบที่ถูกกว่าและอธิบายผลลัพธ์ได้ง่ายกว่า
ลำดับการตัดสินใจที่เสียหายน้อยที่สุด
- เริ่มที่
pgvectorใน Postgres ที่คุณมีอยู่แล้ว — ได้ transaction, backup, สิทธิ์ และ join กับข้อมูลจริงมาฟรีทั้งหมด - เลือก index ให้ตรงกับงาน — HNSW ให้ recall กับ latency ที่ดีกว่าแต่กิน memory และสร้างช้ากว่า ส่วน IVFFlat สร้างเร็วและกิน memory น้อยกว่าแต่ต้องมีข้อมูลก่อนถึงจะ train ได้
- วัด recall ไม่ใช่แค่ latency — vector index เป็น approximate ระบบที่ตอบเร็วแต่พลาดผลลัพธ์ที่ถูกต้องคือระบบที่ผิด และตัวเลขนี้ไม่โผล่ใน dashboard latency
- แยกออกเป็น vector database ต่างหากเมื่อวัดแล้วว่าไม่ไหว — ปกติคือเรื่องจำนวน vector ระดับหลายสิบล้านขึ้นไป, ความต้องการ filter ซับซ้อนพร้อม ANN, หรือการกระจายหลาย region ตัวเลือกในหมวดนี้เช่น Qdrant, Milvus, turbopuffer
Vector store กับ PDPA
embedding ที่สร้างจากข้อความของลูกค้า ยังเป็นข้อมูลที่มาจากบุคคล อย่าคิดว่าการแปลงเป็นตัวเลขทำให้มันหลุดจากขอบเขตการคุ้มครอง ต้องมีเส้นทางลบและต้องผูกกับ id ที่ตามไปลบต้นทางได้ ให้ตัดสินใจเรื่องนี้กับเจ้าของด้าน data protection พร้อมชื่อและวันที่ ไม่ใช่ให้ทีม engineer สรุปเอง
ตารางสรุป: จับคู่ pattern กับ store
| Access pattern | Store ที่เหมาะ |
|---|---|
| point lookup ด้วย primary key + transaction | relational |
| invariant เรื่องยอดเงินที่ contention สูงมาก | relational ก่อน · ledger เฉพาะทาง (TigerBeetle) เมื่อวัดแล้วว่าไม่ไหว |
| point lookup ด้วย key เดียว, ปริมาณมหาศาล, ไม่ต้อง join | document / key-value |
| append หนักมาก อ่านตามช่วงเวลาใน partition | wide-column |
| full-text + faceting | search (derived) |
| scan + aggregate หลายพันล้านแถว แบบ real-time | ClickHouse / StarRocks / Doris |
| scan + aggregate ที่รอได้เป็นชั่วโมง หลาย engine ต้องอ่านร่วมกัน | lakehouse บน Iceberg |
| analytics บนไฟล์ที่มีอยู่แล้ว โดยไม่อยากตั้ง cluster | DuckDB |
| SQL ที่ต้องรอดการเสียทั้ง region และ write ได้หลาย region | distributed SQL (Aurora DSQL, CockroachDB, TiDB) |
| byte ก้อนใหญ่, immutable | object storage |
| ค้นข้อมูล cold ปริมาณมากเป็นครั้งคราว จ่ายตามการใช้จริง | object-storage-native (turbopuffer, Quickwit) |
| metric ที่ label จำกัด | time-series |
| เดินความสัมพันธ์หลาย hop | graph |
| ค้นความคล้ายเชิงความหมาย | pgvector ก่อน แล้วค่อยคิดเรื่องอื่น |
Polyglot persistence มีต้นทุนที่คนไม่นับ
ทุก store ที่เพิ่มคือ: backup อีกชุด, การกู้คืนที่ต้องทดสอบอีกแบบ, monitoring อีกชุด, ทักษะที่ทีมต้องมีอีกอย่าง, เส้นทาง upgrade อีกเส้น, และผิวสัมผัสด้าน security อีก 1 ชุด (credential, network policy, การเข้ารหัส, audit log) พร้อมกับปัญหาใหม่คือ ข้อมูลชุดเดียวกันไม่ตรงกันระหว่าง store
เกณฑ์ที่ควรผ่านก่อนเพิ่ม store: มี access pattern ที่ store ที่มีอยู่ทำไม่ได้จริง และคุณวัดแล้วว่ามันทำไม่ได้ ไม่ใช่แค่คิดว่าทำไม่ได้ และคุณเขียนได้ว่าใครดูแลมันตอนตีสาม
Checklist ก่อนตัดสินใจเพิ่มหรือเปลี่ยน datastore
- มี access pattern table จากบทที่ 14 แล้ว ไม่ใช่เริ่มจากชื่อผลิตภัณฑ์
- อธิบายได้ว่าตัวที่เลือกอยู่ตรงไหนของ 3 แกน และทำไมแกนนั้นตรงกับ pattern ของเรา
- เขียน ราคาที่ต้องจ่าย ของตัวเลือกนั้นไว้ใน design doc อย่างน้อย 3 ข้อ
- วัดแล้วว่า store ที่มีอยู่ทำไม่ได้จริง พร้อมตัวเลขและวันที่ที่วัด
- ถ้าเป็น derived store: มีเส้นทาง rebuild จาก source of truth และเคยทดสอบ rebuild แล้ว
- ถ้าเป็น derived store: มีเส้นทางลบข้อมูลส่วนบุคคล และพิสูจน์ได้ว่าลบแล้ว
- มีคนในทีมอย่างน้อย 2 คนที่ operate ตัวนี้ได้ หรือมี managed service พร้อม SLA ที่อ่านแล้ว
- เคยซ้อม restore จาก backup ลงระบบเปล่าและจับเวลาไว้
- license ผ่านการตรวจจากเจ้าของด้าน legal แล้ว พร้อมชื่อและวันที่
- มีทางออก — export เป็น format มาตรฐานได้ และประเมินเวลาไว้แล้ว
- มีแผนว่าจะปลด store เดิมเมื่อไหร่ หรือระบุชัดว่าจะอยู่คู่กันถาวรและใครดูแล
สรุปบทนี้
อธิบายทุก datastore ด้วย 3 แกน: physical layout, write path, storage coupling · Relational เป็น default โดยเฉพาะที่เกี่ยวกับเงิน · partition key คือ design ของ wide-column · ของที่เป็นกระแสในปี 2026 เกือบทุกตัวเก่งเพราะ ตัดความสามารถบางอย่างทิ้งอย่างจงใจ — จุดแข็งคือสิ่งที่ vendor บอกฟรี ราคาคือสิ่งที่คุณต้องไปหาเอง · search index และ read model เป็น derived ที่ต้อง rebuild ได้ · object storage ต้องปิด public access และ validate ทุกอย่างฝั่ง server · ห้าม label metric ด้วยค่าที่ cardinality ไม่จำกัด · และทุก store ที่เพิ่มคือ backup, monitoring, security surface, ทักษะ และปัญหาข้อมูลไม่ตรงกันอีก 1 ชุด