DDIA · Martin Kleppmann · Chương 3DDIA · Martin Kleppmann · Chương 3DDIA · Martin Kleppmann · Chapter 3
DDIA · Chương 3 · Lưu trữ & truy xuấtDDIA · Chương 3 · Lưu trữ & truy xuấtDDIA · Chapter 3 · Storage and Retrieval

Cỗ máy cất và tìm nằm dưới mọi truy vấnMáy cất dữ liệu ở đâu, tìm lại kiểu gì?The store-and-find engine under every query

Chương 2 hỏi dữ liệu trông như thế nào; Chương 3 hỏi một câu thấp hơn hẳn: khi bạn đưa dữ liệu cho cơ sở dữ liệu, nó cất ở đâu, và khi bạn hỏi lại, nó tìm bằng cách nào?1 Kleppmann nói rõ rằng phần lớn lập trình viên ứng dụng sẽ không tự viết một storage engine, nhưng gần như chắc chắn sẽ phải chọn một cái — và chọn đúng thì cần hình dung được nó làm gì bên dưới12.

Bạn đưa cho máy một thứ và bảo "nhớ giùm". Lát sau bạn hỏi lại. Câu hỏi của chương này rất đơn giản: máy cất thứ đó vào đâu, và làm sao nó tìm lại nhanh đến thế? Bạn không cần tự làm ra cái tủ ấy, nhưng bạn sẽ phải chọn tủ nào — nên cũng nên biết bên trong nó xếp đồ ra sao.

Chapter 2 asked what data looks like; Chapter 3 asks something far lower down: when you hand data to a database, where does it put it, and how does it find it again?1 Kleppmann is explicit that most application developers won't implement a storage engine from scratch, but almost certainly will have to pick one — and picking well means having a rough picture of what it does underneath12.

∴
Luận đề của chương Điều quan trọng nhất The thesis of the chapter

Mọi engine lưu trữ đều đứng trên cùng một đánh đổi: chỉ mục làm việc đọc nhanh lên và luôn làm việc ghi chậm đi, vì chỉ mục là cấu trúc phái sinh phải được cập nhật ở mỗi lần ghi1. Từ đánh đổi ấy mọc ra hai họ engine — log-structured (ghi nối đuôi rồi hợp nhất sau) và page-oriented kiểu B-tree (sửa tại chỗ trên các trang cố định) — rồi một trục thứ hai cắt ngang: hệ phục vụ giao dịch và hệ phục vụ phân tích muốn hai cách bố trí dữ liệu khác hẳn nhau.

Muốn tìm nhanh thì phải làm thêm "mục lục". Nhưng mục lục không tự có: mỗi lần ghi thêm một dòng, máy phải sửa cả mục lục nữa — nên ghi sẽ chậm đi một chút. Nhớ nhé: đọc nhanh hơn thì ghi phải trả tiền, không có bữa trưa miễn phí.

Every storage engine rests on the same trade-off: an index speeds up reads and always slows down writes, because an index is a derived structure that must be updated on every write1. Out of that trade-off grow two families of engine — log-structured (append, then merge later) and page-oriented B-trees (update in place on fixed-size pages) — and then a second axis cuts across: systems serving transactions and systems serving analytics want quite different data layouts.

Nền tảng 1 · Log & hash indexFoundation 1 · Log & hash index

Ghi thì nối đuôi, đọc mới là chỗ phải trả tiềnAppending is cheap; reading is where you pay

Kleppmann mở chương bằng cơ sở dữ liệu đơn giản nhất có thể tưởng tượng: một tệp mà mỗi lần ghi chỉ nối thêm một dòng vào cuối, còn mỗi lần đọc thì quét cả tệp để lấy giá trị mới nhất1. Ghi rẻ đến mức khó tin, nhưng chi phí đọc tăng theo số bản ghi. Muốn đọc nhanh thì phải dựng chỉ mục — một cấu trúc phái sinh từ dữ liệu gốc, không thêm thông tin mới mà chỉ thêm một lối đi ngắn hơn. Và vì lối đi ấy phải được bảo trì, mọi chỉ mục đều bắt việc ghi gánh thêm chi phí — đó là đánh đổi nền tảng mà cả chương xoay quanh1.

Hãy tưởng tượng một cuốn sổ mà bạn chỉ được viết thêm vào dòng cuối, không được tẩy xóa. Viết thì nhanh lắm. Nhưng muốn tìm lại một điều đã ghi thì phải lật từ đầu đến cuối. Vậy nên người ta làm thêm một trang "mục lục" ghi sẵn mỗi thứ nằm ở dòng nào. Tìm nhanh hẳn — đổi lại, mỗi lần viết thêm một dòng, bạn phải sửa mục lục nữa.

Kleppmann opens the chapter with the simplest database imaginable: a file where every write just appends one line to the end, and every read scans the whole file for the latest value1. Writing is absurdly cheap, but read cost grows with the number of records. To read fast you need an index — a derived structure built from the primary data, adding no new information, only a shorter path to it. And because that path must be maintained, every index makes writes pay more — the foundational trade-off the whole chapter turns on1.

An index is derived data — you choose what it costs you

Chỉ mục không phải là dữ liệu; nó là ý kiến của bạn về cách dữ liệu sẽ được hỏi. Chọn chỉ mục là chọn trước xem bạn muốn trả chi phí ở lúc ghi hay ở lúc đọc — và không có lựa chọn nào miễn phí cả hai đầu.

Mục lục không phải là nội dung cuốn sách; nó chỉ là cách bạn đoán trước người ta sẽ tra gì. Nhớ nhé: làm mục lục là trả tiền trước cho lần tìm sau.

An index isn't data; it's your opinion about how the data will be asked for. Choosing an index is choosing in advance whether to pay at write time or at read time — and no choice is free at both ends.

Lý do việc nối đuôi lại nhanh nằm ở dạng truy cập chứ không ở tốc độ thiết bị: ghi tuần tự tránh được việc dịch chuyển đầu đọc. Bài giới thiệu Bitcask nói thẳng rằng tệp đang hoạt động "chỉ được ghi bằng cách nối thêm, nghĩa là các lần ghi tuần tự không cần disk seeking"3. Điều đáng nhớ là kết luận này không dựa trên việc phần cứng nhanh hay chậm: đó là lý do vì sao cùng một ý tưởng vẫn được dùng lại trên ổ SSD, nơi người ta muốn ghi thành khối lớn liên tục thay vì sửa lắt nhắt.

Ghi = nối đuôi Thao tác rẻ nhất là thêm vào cuối; không đụng tới thứ đã ghi nên không cần tìm chỗ.
Đọc = quét Không có chỉ mục, tìm một khóa phải đi hết tệp; chi phí tăng theo số bản ghi.
Chỉ mục = trả trước Cấu trúc phái sinh, phải cập nhật ở mỗi lần ghi: đọc nhanh lên, ghi chậm đi.
Viết thêm vào cuối Cách ghi nhanh nhất là không đụng gì tới phần đã viết.
Không mục lục thì phải lật hết Sổ càng dày, tìm càng lâu.
Mục lục tốn công Mỗi lần viết thêm, phải sửa mục lục theo.
Write = append The cheapest operation is adding to the end; nothing already written is touched, so nothing must be found.
Read = scan With no index, finding a key walks the whole file; cost grows with the number of records.
Index = pay up front A derived structure, updated on every write: reads get faster, writes get slower.
  1. Append-only log

    Cuốn sổ chỉ được viết thêmA book you can only add to

    Trong một kho ghi theo log, mỗi thao tác ghi là một bản ghi mới nối vào cuối tệp đang hoạt động; khi tệp đủ lớn, nó được đóng lại và trở thành bất biến, một tệp mới mở ra thay thế3. Xóa cũng là một lần ghi: người ta ghi một dấu tombstone đặc biệt, và giá trị cũ chỉ thật sự biến mất ở lần hợp nhất sau3.

    Cuốn sổ này có luật lạ: không được tẩy, không được sửa, chỉ được viết thêm xuống dưới. Muốn xóa một điều? Bạn viết thêm một dòng "điều kia bỏ đi nhé". Dòng cũ vẫn còn đó cho tới hôm tổng dọn. Nhớ nhé: ở đây, xóa cũng là một lần viết.

    In a log-structured store, every write is a new record appended to the active file; once that file is big enough it is closed and becomes immutable, and a new one takes its place3. Deletion is also a write: a special tombstone is appended, and the old value only really disappears at the next merge3.

    Bạn gặp nó ở đâuBất cứ chỗ nào ưu tiên thông lượng ghi: nhật ký sự kiện, hàng đợi bền, các kho khóa–giá trị dòng LSM, và cả write-ahead log của cơ sở dữ liệu quan hệ.
    Thấy ở đâuỞ mọi chỗ cần ghi thật nhiều, thật nhanh — như sổ nhật ký ghi liên tục cả ngày.
    Where you see itAnywhere write throughput matters: event logs, durable queues, LSM-family key–value stores, and the write-ahead log of a relational database.
  2. Hash index & the in-memory keydir

    Bản đồ khóa → vị trí byte, nằm trong RAMA key-to-byte-offset map, held in RAM

    Chỉ mục đơn giản nhất cho một log như thế là một bảng băm trong bộ nhớ trỏ từ khóa tới vị trí byte trong tệp. Bitcask — engine cục bộ ra đời từ kiến trúc lưu trữ cắm-thay-được của Riak — gọi nó là keydir: "một bảng băm ánh xạ mọi khóa trong Bitcask tới một cấu trúc kích thước cố định cho biết tệp, vị trí và kích thước của bản ghi được ghi gần nhất cho khóa đó"3. Nhờ vậy, đọc một giá trị không bao giờ cần quá một lần disk seek3.

    Mục lục ở đây là một tờ giấy để trên bàn (chứ không cất trong sổ), ghi: "món A ở trang 12, dòng 3". Cần tìm món A? Liếc tờ giấy rồi mở đúng một chỗ. Nhớ nhé: mục lục để ngoài thì tra cực nhanh, nhưng nó chiếm chỗ trên bàn.

    The simplest index over such a log is an in-memory hash map from key to byte offset. Bitcask — the local engine that grew out of Riak's pluggable storage — calls it the keydir: "a hash table that maps every key in a Bitcask to a fixed-size structure giving the file, offset, and size of the most recently written entry for that key"3. Because of it, reading a value never requires more than a single disk seek3.

    Bạn gặp nó ở đâuỞ những kho khóa–giá trị mà tập khóa nhỏ nhưng bị ghi đè rất nhiều — ví dụ đếm lượt xem theo URL, nơi số khóa có hạn còn số lần cập nhật thì không.
    Thấy ở đâuKhi có ít món nhưng phải cập nhật liên tục — như bảng đếm lượt xem của từng video.
    Where you see itKey–value stores where the key set is small but overwritten constantly — counting views per URL, say, where the number of keys is bounded but the number of updates is not.
  3. Segments, compaction & hint files

    Cắt khúc rồi hợp nhất để lấy lại chỗSegment, then merge to win space back

    Ghi mãi mà không xóa thì đĩa đầy. Lời giải là hợp nhất (Bitcask gọi là merging, giới LSM gọi là compaction): duyệt qua các tệp bất biến và sinh ra một bộ tệp mới "chỉ chứa bản mới nhất, còn sống, của mỗi khóa có mặt"3. Kèm theo mỗi tệp dữ liệu là một hint file chứa vị trí và kích thước thay vì giá trị, để lần khởi động sau dựng lại keydir nhanh hơn nhiều3.

    Sổ viết mãi thì dày. Nên thỉnh thoảng người ta chép lại: mỗi món chỉ giữ dòng mới nhất, những dòng cũ và những dòng "bỏ đi" thì không chép nữa. Cuốn sổ mới mỏng hơn hẳn. Nhớ nhé: dọn dẹp không phải để đẹp, mà để còn chỗ viết tiếp.

    Append forever and the disk fills up. The answer is merging (Bitcask's word; the LSM world says compaction): walk the immutable files and produce a new set containing "only the 'live' or latest versions of each present key"3. Alongside each data file sits a hint file holding positions and sizes instead of values, so the next startup can rebuild the keydir far faster3.

    Bạn gặp nó ở đâuMỗi khi một hệ "ghi nhanh" bất ngờ phình dung lượng: gần như luôn là vì tiến trình hợp nhất đang chậm hơn tốc độ ghi mới.
    Thấy ở đâuKhi ổ đĩa đầy lên nhanh bất thường — thường là vì chưa kịp dọn.
    Where you see itWhenever a "fast-write" system suddenly balloons on disk: almost always the merge process is falling behind the incoming writes.
  4. Where the hash index stops

    Hai bức tường của chỉ mục bămThe two walls a hash index hits

    Bức tường thứ nhất: vì keydir ánh xạ mọi khóa và nằm trong bộ nhớ3, tập khóa bị chặn bởi RAM — trong khi phần giá trị thì không, và chính bài Bitcask đặt mục tiêu "xử lý được tập dữ liệu lớn hơn RAM rất nhiều mà không suy giảm"3. Bức tường thứ hai: bảng băm không có thứ tự, nên truy vấn theo khoảng — "mọi khóa từ A đến B" — lại phải quay về quét1.

    Cách này có hai chỗ kẹt. Một: tờ mục lục phải để trên bàn, mà bàn thì bé — quá nhiều món là hết chỗ. Hai: tờ giấy ghi lộn xộn, nên nếu bạn hỏi "cho xem tất cả món từ A đến B" thì vẫn phải dò từng dòng. Nhớ nhé: mục lục không xếp thứ tự thì không tra theo khoảng được.

    Wall one: because the keydir maps every key and lives in memory3, the key set is bounded by RAM — while the values are not, and the Bitcask paper's own goal was the "ability to handle datasets much larger than RAM w/o degradation"3. Wall two: a hash map has no order, so a range query — "every key from A to B" — falls back to scanning1.

    Bạn gặp nó ở đâuKhi mô hình dữ liệu của bạn đầy khóa dạng chuỗi thời gian hoặc tiền tố phân cấp, và câu hỏi thường trực là "lấy tất cả trong khoảng này" — đó là lúc chỉ mục băm hết đất dùng.
    Thấy ở đâuKhi bạn hay hỏi "cho xem từ ngày này đến ngày kia" — kiểu mục lục lộn xộn sẽ không giúp được.
    Where you see itWhen your keys are time series or hierarchical prefixes and the standing question is "give me everything in this range" — that's where the hash index runs out of road.

    Đọc con số hiệu năng cho đúng

    Bài Bitcask có nêu thông lượng "5000–6000 lần ghi mỗi giây", nhưng kèm điều kiện rất cụ thể: đó là thử nghiệm ban đầu trên một máy xách tay với đĩa chậm, trong tài liệu năm 20103. Con số ấy nói lên hình dạng của thiết kế chứ không phải giới hạn của phần cứng hôm nay — và nó là ví dụ tốt cho thói quen nên có: mọi benchmark chỉ có nghĩa khi đi kèm điều kiện đo.

    Vì sao hai bức tường lại đi cùng nhau

    Cả hai đều bắt nguồn từ một chọn lựa duy nhất: dùng băm làm cấu trúc tra cứu. Băm cho thời gian tra cứu gần như hằng số nhưng phá bỏ thứ tự, và nó bắt buộc phải giữ trọn bộ khóa ở nơi truy cập ngẫu nhiên rẻ, tức bộ nhớ. Thay băm bằng một cấu trúc có thứ tự sẽ gỡ được cả hai — đó chính là đường dẫn sang phần tiếp theo.

Nền tảng 2 · LSM-tree vs B-treeFoundation 2 · LSM-tree vs B-tree

Hai họ engine, hai chỗ để trả giáTwo families of engine, two places to pay

Bắt các đoạn log phải sắp xếp theo khóa, ta được SSTable; cho các bản ghi mới đi vào một cây trong bộ nhớ rồi định kỳ đổ xuống đĩa, ta được LSM-tree. Tên gọi đến từ bài của O'Neil và cộng sự năm 1996, mô tả một cấu trúc "hoãn lại và gom lô các thay đổi chỉ mục, dồn thay đổi từ một thành phần trong bộ nhớ xuống một hoặc nhiều thành phần trên đĩa theo cách gợi nhớ merge sort"4. Ở phía đối diện là B-tree, cấu trúc chỉ mục tiêu chuẩn của cơ sở dữ liệu quan hệ — phổ biến tới mức năm 1979 Douglas Comer đã đặt tên bài tổng quan của mình là "The Ubiquitous B-Tree"9. Nó chia dữ liệu thành các trang kích thước cố định và sửa tại chỗ.

Có hai kiểu tủ đựng đồ. Tủ thứ nhất: bạn cứ ném đồ mới vào một cái hộp nhỏ trên bàn, đầy thì đổ cả hộp xuống ngăn dưới, rồi thỉnh thoảng gộp các ngăn lại cho gọn. Tủ thứ hai: mỗi món có sẵn một ô cố định, đổi gì thì mở đúng ô đó ra sửa. Kiểu một ghi nhanh nhưng dọn nhiều; kiểu hai gọn gàng nhưng mỗi lần sửa phải đi tìm ô.

Require the log segments to be sorted by key and you get SSTables; let new records land in an in-memory tree and spill to disk periodically and you get the LSM-tree. The name comes from O'Neil and colleagues in 1996, describing a structure that "defers and batches index changes, cascading the changes from a memory-based component through one or more disk components in an efficient manner reminiscent of merge sort"4. Facing it is the B-tree, the standard index of relational databases — so standard that in 1979 Douglas Comer titled his survey "The Ubiquitous B-Tree"9. It breaks data into fixed-size pages and updates in place.

Defer and merge, or edit in place

LSM-tree ghi bằng cách hoãn: nhận nhanh, sắp xếp trong bộ nhớ, rồi trộn dần xuống đĩa. B-tree ghi bằng cách sửa: tìm đúng trang, thay đúng chỗ, và ghi nhật ký trước để còn cứu được nếu mất điện. Cả hai đều đúng; câu hỏi duy nhất là bạn muốn trả giá ở đâu.

Kiểu một: cứ nhận đã, dọn sau. Kiểu hai: xếp đúng chỗ ngay từ đầu. Nhớ nhé: không có kiểu nào "tốt hơn" — chỉ có kiểu hợp với việc bạn hay làm.

An LSM-tree writes by deferring: accept fast, sort in memory, merge down to disk over time. A B-tree writes by editing: find the right page, change it in place, and log first so a crash can be recovered. Both are correct; the only question is where you want to pay.

Điểm tinh tế mà Kleppmann nhấn mạnh là đánh đổi này phụ thuộc workload, không phải một thứ hạng cố định1. Chính bài LSM gốc đã tự giới hạn phạm vi: cấu trúc này "hữu ích nhất trong các ứng dụng mà việc chèn vào chỉ mục xảy ra thường xuyên hơn các lần tìm để lấy bản ghi ra", và ngược lại "các lần tìm cần trả lời ngay sẽ mất hiệu quả I/O trong một số trường hợp"4. Ai trích một con số hiệu năng mà bỏ vế sau là đã kể nửa câu chuyện.

Hoãn & trộn LSM nhận ghi vào bộ nhớ, đổ xuống đĩa thành tệp bất biến, rồi hợp nhất nền.
Sửa tại chỗ B-tree ghi đè đúng trang chứa khóa; nhật ký ghi trước lo phần an toàn khi sập.
Không có kẻ thắng chung Ưu thế đảo chiều theo tỉ lệ đọc/ghi, kích thước bản ghi và yêu cầu độ trễ.
Nhận trước, dọn sau Ghi rất nhanh, nhưng phải dọn dẹp đều đặn.
Xếp đúng ô ngay Tìm lại rất gọn, nhưng mỗi lần sửa phải đi tìm ô.
Tùy việc mà chọn Hay ghi thì chọn kiểu một; hay tra thì kiểu hai thường hợp hơn.
Defer & merge LSM takes writes into memory, spills immutable files to disk, then compacts in the background.
Edit in place A B-tree overwrites the page holding the key; a write-ahead log handles crash safety.
No overall winner The advantage flips with read/write ratio, record size and latency requirements.
  1. SSTable & memtable

    Sắp xếp trước, rồi mới chạm đĩaSort first, touch disk later

    Bản ghi mới không đi thẳng xuống đĩa: chúng vào một cấu trúc cây trong bộ nhớ — memtable — nơi thứ tự khóa được giữ sẵn. Tài liệu hiện thực của LevelDB mô tả đúng vòng đời ấy: "một bản sao của tệp log hiện thời được giữ trong một cấu trúc trong bộ nhớ (memtable)", và khi tệp log đạt khoảng 4 MB thì nó được chuyển thành một bảng đã sắp xếp, đồng thời một tệp log mới được tạo5. Vì mỗi tệp đã sắp xếp, chỉ mục trong bộ nhớ chỉ cần thưa — vài khóa mốc là đủ để nhảy tới đúng vùng.

    Đồ mới không bị ném thẳng vào tủ. Chúng nằm tạm trong một khay trên bàn, và khay này luôn được xếp theo thứ tự chữ cái. Khay đầy thì bê nguyên xuống tủ, vẫn đúng thứ tự. Nhờ vậy, mục lục không cần ghi từng món — chỉ cần ghi "A đến F ở ngăn một" là đủ. Nhớ nhé: xếp trước thì mục lục nhẹ đi rất nhiều.

    New records don't go straight to disk: they land in an in-memory tree — the memtable — where key order is maintained for free. LevelDB's implementation notes describe exactly that lifecycle: "a copy of the current log file is kept in an in-memory structure (the memtable)", and when the log file reaches about 4 MB it is converted to a sorted table and a fresh log file is started5. Because each file is sorted, the in-memory index need only be sparse — a few landmark keys are enough to jump to the right region.

    Bạn gặp nó ở đâuỞ mọi engine họ LSM: LevelDB, RocksDB, và các kho phân tán dựng trên ý tưởng SSTable. Truy vấn theo khoảng khóa trở lại rẻ vì dữ liệu đã nằm đúng thứ tự trên đĩa.
    Thấy ở đâuỞ những hệ cần vừa ghi nhiều vừa tra được theo khoảng — như dữ liệu theo thời gian.
    Where you see itIn every LSM-family engine: LevelDB, RocksDB, and the distributed stores built on the SSTable idea. Range queries get cheap again because the data already sits in order on disk.
  2. Compaction strategies: leveled vs size-tiered

    Hai lối hợp nhất, hai kiểu hóa đơnTwo ways to compact, two kinds of bill

    Lối leveled xếp các tệp thành tầng có kích thước tăng dần. LevelDB: khi tổng kích thước tệp ở tầng L vượt 10L MB (10 MB cho tầng 1, 100 MB cho tầng 2…), một tệp ở tầng L cùng mọi tệp giao khoảng ở tầng L+1 được trộn lại; tệp đầu ra có kích thước mục tiêu 2 MB5. RocksDB nêu cùng công thức: Target_Size(Ln+1) = Target_Size(Ln) × max_bytes_for_level_multiplier, mặc định nhân 106. Lối size-tiered thì gom các tệp xấp xỉ bằng nhau lại: tài liệu Cassandra cho biết STCS khởi động khi tích đủ một số tệp cùng cỡ (mặc định 4) và trộn chúng thành tệp lớn hơn7.

    Dọn tủ có hai cách. Cách một: chia tủ thành nhiều ngăn, ngăn dưới to gấp mười ngăn trên; đầy ngăn nào thì gộp ngăn đó xuống dưới. Cách hai: cứ thấy bốn hộp cỡ bằng nhau thì gộp thành một hộp to. Nhớ nhé: cách một giữ tủ gọn hơn nhưng phải dọn nhiều lần hơn; cách hai dọn ít hơn nhưng có lúc tủ phình ra.

    The leveled approach arranges files into levels of growing size. LevelDB: when the combined size of files at level L exceeds 10L MB (10 MB for level-1, 100 MB for level-2…), one file at L plus all overlapping files at L+1 are merged; output files target 2 MB5. RocksDB states the same formula: Target_Size(Ln+1) = Target_Size(Ln) × max_bytes_for_level_multiplier, default 106. The size-tiered approach instead merges files of roughly equal size: Cassandra's docs say STCS fires once a set number of similarly-sized SSTables accumulate (default 4) and merges them into a larger one7.

    Bạn gặp nó ở đâuTrong các nút chỉnh cấu hình mà bạn tưởng là vô hại. Đổi chiến lược hợp nhất là đổi thẳng hóa đơn I/O và dung lượng dư của cụm — thường đáng làm trước khi nghĩ tới việc thêm máy.
    Thấy ở đâuỞ phần cài đặt của cơ sở dữ liệu — một dòng cấu hình đổi hẳn chuyện tủ có gọn hay không.
    Where you see itIn config knobs that look harmless. Changing the compaction strategy changes your cluster's I/O and spare-capacity bill directly — often worth trying before adding machines.

    Cái giá của size-tiered

    Tài liệu Cassandra nói rõ hai nhược điểm: đọc chậm hơn "vì quá trình trộn theo kích thước không nhóm dữ liệu theo hàng", làm các phiên bản của một hàng rải ra nhiều SSTable; và khuếch đại không gian — dung lượng cần cho cả tệp mới lẫn tệp cũ trong lúc hợp nhất có thể vượt quá chỗ trống thường có trên một nút7.

    Sách viết năm 2017, phần mềm thì không đứng yên

    Tài liệu hiện thời của Cassandra vẫn ghi SizeTieredCompactionStrategy là chiến lược mặc định, nhưng đồng thời khuyến nghị Unified Compaction Strategy (UCS) cho phần lớn workload từ Cassandra 5.0 trở đi7. Nói cách khác, cặp "LevelDB/RocksDB leveled — Cassandra/HBase size-tiered" là cách phân đôi đúng với thời điểm cuốn sách, chứ không phải một sự thật vĩnh viễn: hãy kiểm tra tài liệu phiên bản bạn đang chạy.

  3. Bloom filter

    Trả lời "chắc chắn không có" mà không cần mở đĩaSaying "definitely not here" without touching disk

    Điểm yếu cố hữu của LSM là truy vấn một khóa không tồn tại: phải hỏi lần lượt từng tệp mới dám kết luận. Bloom filter chặn đúng chỗ đó. Tài liệu RocksDB mô tả nó như một cấu trúc cho biết một khóa có thể tồn tại hay chắc chắn không tồn tại trong tập khóa của một tệp SST, nhờ vậy tránh được những lần đọc đĩa vô ích8. Nó có thể báo nhầm "có" nhưng không bao giờ báo nhầm "không"8.

    Trước khi lục tung cả tủ để tìm một món, bạn hỏi người giữ kho: "trong ngăn này có món X không?". Người ấy có thể trả lời "chắc là có" (rồi mở ra thấy không có thật), nhưng nếu bảo "chắc chắn không có" thì đúng là không có. Nhớ nhé: một câu "chắc chắn không" tiết kiệm được cả một lần lục tủ.

    The inherent weakness of an LSM is a query for a key that doesn't exist: you must ask every file before you can say so. The bloom filter blocks exactly that. RocksDB's docs describe it as a structure that tells you whether a key may exist or definitely does not exist in an SST file's key set, avoiding wasted disk reads8. It can wrongly say "yes", but never wrongly says "no"8.

    Bạn gặp nó ở đâuTrong mọi tra cứu "khóa này đã tồn tại chưa" ở quy mô lớn — kiểm tra trùng, tra cache, lọc khóa trước khi đọc. Đây cũng là cấu trúc đầu tiên nên nghĩ tới khi phần lớn truy vấn của bạn là câu trả lời "không có".
    Thấy ở đâuKhi phần lớn câu hỏi của bạn là "cái này có chưa?" và câu trả lời thường là "chưa".
    Where you see itIn any large-scale "does this key exist yet" lookup — dedup checks, cache probes, filtering before a read. It's the first structure to reach for when most of your queries answer "not here".

    Đổi bộ nhớ lấy độ chính xác

    Quan hệ giữa chỗ chiếm và tỉ lệ báo nhầm không tuyến tính. Tài liệu RocksDB đưa các mốc: khoảng 4,9 bit mỗi khóa cho tỉ lệ dương tính giả ~10%, 9,9 bit cho ~1%, và 15,5 bit cho ~0,1%8. Gấp đôi số bit không làm tỉ lệ nhầm giảm một nửa — đó là lý do việc chỉnh tham số này nên bám vào tỉ lệ truy vấn trượt thật của bạn.

    Vì sao sai một chiều lại chấp nhận được

    Vì hậu quả của hai loại sai không cân xứng. Báo nhầm "có" chỉ tốn thêm một lần đọc đĩa rồi phát hiện không có. Báo nhầm "không" sẽ khiến hệ trả lời sai. Thiết kế của bloom filter cố ý dồn toàn bộ rủi ro về phía loại sai rẻ tiền hơn.

  4. B-tree: fixed pages + write-ahead log

    Trang cố định, và cuốn nhật ký ghi trướcFixed pages, and the log written first

    B-tree chia không gian khóa thành các trang kích thước cố định; mỗi trang chứa khóa và con trỏ tới trang con, cây được giữ cân bằng nên độ sâu tăng rất chậm theo dữ liệu9. Nhưng "sửa tại chỗ" đặt ra một rủi ro mà log không có: mất điện giữa chừng có thể để lại một trang hỏng. Lời giải là nhật ký ghi trước. Tài liệu PostgreSQL phát biểu nguyên tắc rất gọn: thay đổi lên các tệp dữ liệu "chỉ được ghi sau khi những thay đổi đó đã được ghi nhật ký, tức là sau khi các bản ghi WAL mô tả thay đổi đã được đẩy xuống lưu trữ bền"10.

    Tủ kiểu hai chia sẵn thành ô, mỗi ô một kích cỡ, và mỗi ô ghi sẵn "muốn tìm chữ A thì mở ngăn bên trái". Nhưng sửa ngay vào ô cũ thì nguy hiểm: đang sửa mà mất điện là hỏng cả ô. Nên trước khi sửa, người ta ghi vào một cuốn nhật ký: "tôi sắp đổi ô này thành thế này". Nhớ nhé: ghi nhật ký trước, sửa thật sau — mất điện vẫn làm lại được.

    A B-tree splits the key space into fixed-size pages; each holds keys and pointers to child pages, and the tree stays balanced so depth grows very slowly with data9. But updating in place carries a risk a log doesn't: losing power mid-write can leave a corrupted page. The answer is the write-ahead log. PostgreSQL's docs put the principle crisply: changes to data files "must be written only after those changes have been logged, that is, after WAL records describing the changes have been flushed to permanent storage"10.

    Bạn gặp nó ở đâuTrong gần như mọi cơ sở dữ liệu quan hệ bạn đang chạy, và trong mọi cuộc điều tra sự cố có câu hỏi "sau khi sập, hệ khôi phục lại từ đâu". Câu trả lời gần như luôn là: phát lại nhật ký.
    Thấy ở đâuỞ hầu hết cơ sở dữ liệu quen thuộc — và mỗi lần máy sập rồi bật lại mà dữ liệu vẫn còn.
    Where you see itIn nearly every relational database you run, and in every incident review that asks "after the crash, where did it recover from". The answer is almost always: replaying the log.

    WAL còn giúp nhanh hơn, không chỉ an toàn hơn

    PostgreSQL ghi nhận rằng WAL làm giảm đáng kể số lần ghi đĩa, vì chỉ cần đẩy tệp WAL xuống đĩa để bảo đảm một giao dịch đã commit, thay vì đẩy mọi tệp dữ liệu mà giao dịch đó chạm tới; WAL được ghi tuần tự nên rẻ hơn nhiều so với việc đồng bộ các trang dữ liệu rải rác10. Khi có nhiều giao dịch nhỏ chạy song song, một lần fsync tệp WAL có thể đủ để xác nhận nhiều giao dịch cùng lúc10.

    Ghi trước — một ý tưởng, hai họ engine

    Điều thú vị là B-tree phải mượn chính cơ chế log để an toàn, còn LSM thì mượn chính cơ chế cây có thứ tự để nhận ghi. Hai họ engine không đối lập tuyệt đối; chúng vay của nhau đúng phần mình thiếu, và khác nhau chủ yếu ở chỗ cấu trúc nào được coi là nguồn sự thật chính.

  5. Write amplification & tail latency

    Đọc bảng đánh đổi cho đúngReading the trade-off table honestly

    Cả hai họ đều ghi dữ liệu xuống đĩa nhiều lần hơn số lần ứng dụng ghi — hiện tượng khuếch đại ghi. Với hợp nhất theo tầng, wiki RocksDB nói thẳng rằng khuếch đại ghi "thường lớn hơn 10"6. Đổi lại, dữ liệu đã sắp xếp và nén theo tệp thường chiếm ít chỗ hơn các trang B-tree có phân mảnh bên trong. Mặt trái nguy hiểm hơn nằm ở độ trễ đuôi: hợp nhất chạy nền và giành băng thông đĩa với truy vấn đang phục vụ — tài liệu Cassandra mô tả rõ tình huống dung lượng cần cho cả tệp cũ lẫn mới trong lúc hợp nhất có thể vượt chỗ trống của một nút7. B-tree thì có một ưu thế khác hẳn: mỗi khóa tồn tại ở đúng một chỗ trong cây, nên khóa và ngữ nghĩa giao dịch dễ gắn vào hơn1.

    Cả hai kiểu tủ đều có lúc phải chép đi chép lại đồ đạc, nên thật ra chúng "viết" nhiều hơn bạn tưởng. Kiểu một dọn nền, mà lúc dọn thì người đang tìm đồ phải chờ lâu hơn bình thường. Kiểu hai không phải dọn, nhưng mỗi món chỉ nằm ở một ô duy nhất nên dễ khóa lại khi cần. Nhớ nhé: hỏi "chậm ở lúc nào" chứ đừng hỏi "cái nào nhanh hơn".

    Both families write to disk more times than the application wrote — write amplification. For leveled compaction, the RocksDB wiki states plainly that write amplification is "often larger than 10"6. In exchange, sorted-and-compressed files usually take less space than B-tree pages with their internal fragmentation. The nastier flip side is tail latency: compaction runs in the background and competes for disk bandwidth with live queries — Cassandra's docs describe exactly the situation where the space needed for old and new SSTables during compaction can outstrip a node's free space7. B-trees have a different edge: each key exists in exactly one place in the tree, so locks and transactional semantics attach more naturally1.

    Bạn gặp nó ở đâuMỗi khi biểu đồ p99 của bạn có những gai nhọn không giải thích được bằng lưu lượng. Trước khi đổ cho mạng, hãy xem lịch hợp nhất — và nhớ rằng con số trung bình sẽ giấu đúng phần đó đi.
    Thấy ở đâuKhi hệ bình thường vẫn nhanh nhưng thỉnh thoảng lại khựng một nhịp — thường là lúc nó đang dọn dẹp.
    Where you see itWhenever your p99 chart shows spikes traffic can't explain. Before blaming the network, look at the compaction schedule — and remember the average will hide precisely that.

    Điều bài LSM gốc tự nói về giới hạn của nó

    O'Neil và cộng sự viết rằng LSM-tree "hữu ích nhất trong các ứng dụng mà việc chèn vào chỉ mục thường xuyên hơn các lần tìm để lấy bản ghi ra", và rằng "các lần tìm cần trả lời ngay sẽ mất hiệu quả I/O trong một số trường hợp"4. Họ cũng nêu bối cảnh cụ thể mà cấu trúc này nhắm tới: bảng lịch sử và tệp nhật ký — nơi ghi nhiều hơn đọc4.

    Vì sao không nên trích một con số duy nhất

    Kleppmann trình bày đây là một đánh đổi phụ thuộc workload, và nhấn rằng kết quả đo phụ thuộc vào chi tiết của workload cụ thể, nên cách duy nhất đáng tin là tự đo trên chính workload của mình1. Một câu như "LSM luôn nhanh hơn B-tree" không có nội dung kỹ thuật cho tới khi nói rõ: nhanh ở thao tác nào, tỉ lệ đọc/ghi bao nhiêu, kích thước bản ghi thế nào, và đo ở percentile nào.

  6. In-memory storage

    Nếu bỏ hẳn đĩa đi thì saoWhat if we drop the disk entirely

    Một lối thoát thứ ba: giữ toàn bộ dữ liệu trong RAM. Kleppmann lưu ý một điều dễ hiểu sai — lợi thế tốc độ ở đây không chủ yếu đến từ việc khỏi đọc đĩa (hệ điều hành vốn đã đệm các trang nóng trong bộ nhớ), mà từ việc không phải mã hóa cấu trúc dữ liệu trong bộ nhớ sang một dạng ghi được xuống đĩa1. Lợi ích thứ hai là mô hình dữ liệu: nằm hẳn trong RAM cho phép cung cấp những cấu trúc vốn vụng về nếu phải bày lên đĩa — Redis công bố danh mục kiểu dữ liệu gồm danh sách, tập hợp, tập hợp có thứ tự, hash và stream17. Ở đầu thuần cache, Memcached tự mô tả mình là hệ cache đối tượng trong bộ nhớ, phân tán, dùng để giảm tải cho cơ sở dữ liệu18.

    Cách thứ ba: không dùng tủ nữa, để hết đồ trên bàn. Nhanh thì khỏi nói. Điều bất ngờ là cái nhanh ấy không hẳn vì "khỏi mở tủ", mà vì khỏi phải gói đồ lại cho vừa hộp mỗi lần cất. Nhớ nhé: để trên bàn thì lấy liền, nhưng bàn nhỏ và dọn nhà là mất.

    A third way out: keep the whole dataset in RAM. Kleppmann flags a common misreading — the speed advantage comes not mainly from skipping disk reads (the OS already caches hot pages in memory) but from not having to encode in-memory data structures into a form that can be written to disk1. The second benefit is the data model: living in RAM lets an engine offer structures that would be awkward on disk — Redis documents lists, sets, sorted sets, hashes and streams among its types17. At the pure-cache end, Memcached describes itself as a distributed in-memory object caching system for taking load off a database18.

    Bạn gặp nó ở đâuỞ lớp cache, hàng đợi ưu tiên, bảng xếp hạng, đếm theo thời gian thực — những chỗ mà cấu trúc dữ liệu quan trọng hơn độ bền, và mất dữ liệu là chấp nhận được vì dựng lại được từ nguồn khác.
    Thấy ở đâuỞ bảng xếp hạng, giỏ hàng tạm, số lượt thích đang chạy — những thứ cần nhanh và mất thì làm lại được.
    Where you see itIn caches, priority queues, leaderboards, real-time counters — places where the data structure matters more than durability, and losing it is acceptable because it can be rebuilt.
Nền tảng 3 · Index shapesFoundation 3 · Index shapes

Chỉ mục không chỉ có một hình dạngAn index has more than one shape

Cả LSM-tree lẫn B-tree đều có thể dùng làm chỉ mục cho khóa chính lẫn chỉ mục phụ. Khác biệt nằm ở chỗ: khóa của chỉ mục phụ không nhất thiết duy nhất — nhiều bản ghi có thể cùng một giá trị. Kleppmann nêu hai cách xử lý: cho mỗi mục trong chỉ mục trỏ tới một danh sách các bản ghi khớp, hoặc thêm định danh bản ghi vào khóa để khóa trở nên duy nhất1. Từ nền đó mọc ra một họ hình dạng: chỉ mục gom cụm, chỉ mục phủ, chỉ mục nhiều cột, chỉ mục đa chiều, và chỉ mục cho tìm kiếm mờ.

Mục lục không nhất thiết phải theo tên. Bạn có thể làm mục lục theo màu, theo năm, theo chỗ để. Nhưng có một chỗ khác: tên thì mỗi món một tên, còn màu thì nhiều món cùng màu — nên mục lục theo màu phải ghi được nhiều dòng cho một mục.

Both LSM-trees and B-trees can index a primary key or a secondary one. The difference: a secondary index key is not necessarily unique — many records can share a value. Kleppmann gives two ways to handle it: make each index entry point to a list of matching records, or append a record identifier to the key to make it unique1. From that base grows a family of shapes: clustered indexes, covering indexes, multi-column indexes, multi-dimensional indexes, and indexes for fuzzy search.

The index decides which questions are cheap

Chọn hình dạng chỉ mục là chọn trước câu hỏi nào sẽ rẻ. Một chỉ mục tuyệt vời cho truy vấn này có thể vô dụng hoàn toàn với truy vấn kia — vì vấn đề không nằm ở dữ liệu, mà ở việc thứ tự bạn đã chọn có khớp với cách bạn hỏi hay không.

Mục lục xếp theo tên thì tra tên rất nhanh, nhưng hỏi "món nào màu đỏ" thì chịu. Nhớ nhé: mục lục chỉ giúp đúng những câu hỏi mà nó được làm ra để trả lời.

Choosing an index shape is choosing which questions will be cheap. An index that is perfect for one query can be useless for another — the issue isn't the data, it's whether the order you picked matches the way you ask.

Đây cũng là chỗ khái niệm "cấu trúc phái sinh" ở phần đầu chương trả về lợi tức. Vì chỉ mục không chứa thông tin mới, bạn có quyền dựng nhiều chỉ mục trên cùng một dữ liệu, mỗi cái phục vụ một dạng câu hỏi — và cái giá phải trả luôn là cùng một loại: thêm chỗ trên đĩa và thêm việc ở mỗi lần ghi1. Câu hỏi thiết kế vì thế không phải "có nên đánh chỉ mục không" mà là "những câu hỏi nào xứng đáng để mình trả tiền trước".

Khóa phụ không duy nhất Một giá trị ứng nhiều bản ghi: trỏ tới danh sách, hoặc gắn định danh vào khóa.
Giữ hàng hay giữ con trỏ Gom cụm thì lưu luôn hàng trong chỉ mục; ngược lại chỉ lưu con trỏ tới hàng.
Thứ tự quyết định câu hỏi Một chiều thì dùng B-tree; hai chiều trở lên cần cấu trúc khác hẳn.
Nhiều món cùng màu Mục lục theo màu phải ghi được nhiều món trong một dòng.
Chép luôn hay chỉ ghi chỗ Mục lục có thể ghi sẵn nội dung, hoặc chỉ ghi "ở trang mấy".
Bản đồ cần mục lục khác Tìm theo vị trí trên bản đồ không xếp hàng dọc được.
Secondary keys repeat One value, many records: point at a list, or append a record id to the key.
Row or pointer A clustered index stores the row itself; otherwise the entry only points at it.
Order decides the question One dimension suits a B-tree; two or more needs a different structure entirely.
  1. Clustered vs non-clustered

    Chỉ mục giữ luôn hàng, hay chỉ giữ chỗHolding the row, or just its address

    Một chỉ mục có thể chỉ trỏ tới hàng, hoặc chứa luôn hàng bên trong. Tài liệu SQL Server mô tả ranh giới này rất gọn: chỉ mục gom cụm "sắp xếp và lưu các hàng dữ liệu trong bảng hoặc khung nhìn theo giá trị khóa của chúng", và vì bản thân các hàng chỉ có thể được lưu theo một thứ tự, mỗi bảng chỉ có một chỉ mục gom cụm11. Ngược lại, chỉ mục không gom cụm "có cấu trúc tách rời khỏi các hàng dữ liệu", mỗi mục khóa kèm một con trỏ tới hàng chứa giá trị đó11.

    Có hai kiểu mục lục. Kiểu một chép luôn cả nội dung vào mục lục — tra xong là có ngay, khỏi lật. Kiểu hai chỉ ghi "ở trang 42" — tra xong vẫn phải lật thêm một lần. Nhớ nhé: chép luôn thì đọc nhanh nhưng tốn giấy, và mỗi cuốn chỉ chép luôn được theo một thứ tự thôi.

    An index can merely point at a row, or hold the row itself. SQL Server's docs draw the line crisply: a clustered index "sorts and stores the data rows in the table or view based on their key values", and since the rows themselves can only be stored in one order, a table can have just one clustered index11. A nonclustered index, by contrast, "has a structure separate from the data rows", each key entry carrying a pointer to the row that holds the value11.

    Bạn gặp nó ở đâuKhi chọn khóa chính. Thứ tự gom cụm quyết định những truy vấn khoảng nào sẽ đọc tuần tự và những truy vấn nào phải nhảy khắp đĩa — một quyết định rất khó đảo sau này.
    Thấy ở đâuKhi quyết định "xếp theo gì" — vì cuốn sổ chỉ xếp được theo một thứ tự duy nhất.
    Where you see itWhen choosing a primary key. The clustering order decides which range queries read sequentially and which jump all over the disk — a decision that's painful to reverse later.
  2. Covering index

    Chỉ mục phủ: trả lời xong mà không cần mở bảngA covering index answers without opening the table

    Giữa hai thái cực trên có một lối giữa: để chỉ mục mang theo một vài cột mà truy vấn cần, đủ để trả lời ngay trong chỉ mục. PostgreSQL gọi kết quả là index-only scan — những lần quét "có thể trả lời truy vấn chỉ từ chỉ mục, không cần chạm tới heap", bằng cách trả giá trị thẳng từ mỗi mục chỉ mục thay vì tra bản ghi tương ứng12. Tài liệu gọi đúng cấu trúc phục vụ việc này là covering index: một chỉ mục được thiết kế riêng để chứa các cột mà một dạng truy vấn hay chạy cần tới12.

    Mục lục thường chỉ ghi "ở trang 42". Nhưng nếu bạn hay hỏi đúng ba thứ, sao không ghi luôn ba thứ đó vào mục lục? Tra một phát là xong, khỏi lật trang. Nhớ nhé: ghi thêm vào mục lục làm nó dày hơn, nhưng đổi lại bạn khỏi phải lật sách.

    Between those extremes lies a middle path: let the index carry a few columns the query needs, enough to answer inside the index. PostgreSQL calls the result an index-only scan — a scan that "can answer queries from an index alone without any heap access", returning values directly from each index entry instead of consulting the heap12. The structure built for it is the covering index: an index designed specifically to include the columns a frequently-run query needs12.

    Bạn gặp nó ở đâuỞ những truy vấn nóng, lặp lại nhiều lần với đúng một hình dạng. Thêm một cột vào chỉ mục có thể cắt hẳn một lượt đọc bảng — nhưng cũng làm mọi lần ghi nặng thêm, đúng theo đánh đổi mở đầu chương.
    Thấy ở đâuỞ những câu hỏi bạn lặp lại hoài, ngày nào cũng hỏi đúng kiểu đó.
    Where you see itIn hot queries repeated many times in exactly one shape. Adding one column to the index can remove a whole table read — while making every write heavier, exactly per the chapter's opening trade-off.
  3. Multi-column (concatenated) index

    Nhiều cột, nhưng thứ tự trái–phải là luậtMany columns, but left-to-right is law

    Cách phổ biến nhất để đánh chỉ mục nhiều cột là nối chúng lại thành một khóa theo thứ tự định trước. Hệ quả rất thực dụng và hay bị quên: PostgreSQL ghi rõ rằng một chỉ mục B-tree nhiều cột có thể dùng với điều kiện truy vấn liên quan tới bất kỳ tập con nào của các cột trong chỉ mục, nhưng nó hiệu quả nhất khi ràng buộc nằm ở các cột dẫn đầu (ngoài cùng bên trái)13.

    Giống cách xếp danh bạ: xếp theo họ trước, rồi mới tới tên. Biết họ thì tìm nhanh. Nhưng chỉ biết mỗi tên mà không biết họ thì gần như phải lật cả cuốn. Nhớ nhé: thứ tự các cột trong mục lục quan trọng không kém việc có mục lục.

    The common way to index several columns is to concatenate them into one key in a fixed order. The practical consequence is easy to forget: PostgreSQL states that a multicolumn B-tree index can be used with query conditions involving any subset of its columns, but is most efficient when the constraints are on the leading (leftmost) columns13.

    Bạn gặp nó ở đâuMỗi khi một truy vấn "đã có chỉ mục rồi mà vẫn chậm". Rất thường là vì điều kiện lọc rơi vào cột đứng sau, còn cột dẫn đầu lại để trống.
    Thấy ở đâuKhi bạn có mục lục rồi mà tìm vẫn lâu — thường vì hỏi sai thứ tự.
    Where you see itEvery time a query is "already indexed but still slow". Usually the filter lands on a trailing column while the leading one is left unconstrained.
  4. Multi-dimensional index

    Bản đồ không xếp thành một hàng dọc đượcA map does not flatten into one queue

    Chỉ mục nối cột vẫn chỉ là một chiều: nó sắp mọi thứ thành một hàng. Với truy vấn "mọi điểm nằm trong hình chữ nhật kinh độ–vĩ độ này", một hàng dọc không giúp được, vì hai điều kiện phải thỏa đồng thời theo hai trục1. Giới không gian dùng cấu trúc khác: tài liệu PostGIS cho biết "cả PostGIS lẫn Oracle Spatial đều dùng chung cấu trúc chỉ mục không gian 'R-Tree'", và rằng chỉ mục không gian không đánh chỉ mục trực tiếp hình học mà đánh chỉ mục hộp bao của chúng14.

    Danh bạ xếp theo tên là một hàng dọc. Nhưng nếu bạn hỏi "quán nào nằm trong ô vuông này trên bản đồ", hàng dọc không giúp gì. Người ta phải chia bản đồ thành ô to, rồi ô nhỏ, rồi ô nhỏ hơn nữa. Nhớ nhé: câu hỏi hai chiều cần mục lục hai chiều.

    A concatenated index is still one-dimensional: it lines everything up in a single queue. For "every point inside this longitude–latitude rectangle", a queue doesn't help, because two conditions must hold simultaneously on two axes1. The spatial world uses a different structure: PostGIS documents that "both PostGIS and Oracle Spatial share the same 'R-Tree' spatial index structure", and that spatial indexes don't index the geometries themselves but their bounding boxes14.

    Bạn gặp nó ở đâuKhông chỉ ở bản đồ. Bất kỳ truy vấn nào lọc đồng thời trên hai trục liên tục — ví dụ "sản phẩm trong khoảng giá này và khoảng ngày này" — cũng là một bài toán đa chiều trá hình.
    Thấy ở đâuỞ bản đồ, và cả khi bạn lọc "giá từ đây tới đây, ngày từ đây tới đây" cùng lúc.
    Where you see itNot just maps. Any query filtering two continuous axes at once — "products in this price range and this date range" — is a multi-dimensional problem in disguise.
  5. Fuzzy index

    Tìm được cả khi người dùng gõ saiFinding it even when the user mistypes

    Mọi chỉ mục nói tới đến giờ đều giả định bạn biết chính xác thứ mình tìm. Tìm kiếm toàn văn phá vỡ giả định ấy. Lucene giữ chỉ mục từ vựng của mình dưới dạng finite state transducer — một cấu trúc hoạt động như một ánh xạ có thứ tự từ chuỗi byte sang giá trị, tốn ít RAM hơn hẳn các hiện thực bản đồ thông thường, đổi lại tốn CPU hơn khi tra15. Trên nền đó, truy vấn mờ được định nghĩa theo khoảng cách sửa: tài liệu API của Lucene cho biết FuzzyQuery đo tương đồng dựa trên thuật toán Damerau–Levenshtein, và các từ được thu thập rồi chấm điểm theo khoảng cách sửa của chúng16.

    Nếu bạn gõ "quyến sách" thay vì "quyển sách", mục lục thường sẽ bảo "không có". Công cụ tìm kiếm thì khác: nó biết đếm xem hai chữ khác nhau mấy nét, và vẫn đưa ra thứ gần giống nhất. Nhớ nhé: có loại mục lục được làm ra để chịu được lỗi gõ.

    Every index so far assumes you know exactly what you're looking for. Full-text search breaks that assumption. Lucene keeps its term index as a finite state transducer — a structure behaving like a sorted map from byte sequences to values, needing far less RAM than ordinary map implementations at a higher CPU cost per lookup15. On that base, fuzzy queries are defined by edit distance: Lucene's API docs state that FuzzyQuery measures similarity with the Damerau–Levenshtein algorithm, with terms collected and scored by their edit distance16.

    Bạn gặp nó ở đâuỞ ô tìm kiếm của mọi sản phẩm có người thật gõ vào. Đây cũng là ranh giới nơi "cơ sở dữ liệu" và "công cụ tìm kiếm" tách làm hai, vì chúng tối ưu cho hai định nghĩa khác nhau của chữ "khớp".
    Thấy ở đâuỞ ô tìm kiếm — chỗ mà ai cũng có lúc gõ sai chính tả.
    Where you see itIn the search box of any product real people type into. It's also where "database" and "search engine" part ways, because they optimise for two different definitions of "match".
Nền tảng 4 · OLTP vs OLAPFoundation 4 · OLTP vs OLAP

Cùng gọi là cơ sở dữ liệu, nhưng là hai thế giớiBoth are called databases, but they are two worlds

Ranh giới không nằm ở công nghệ mà ở dạng truy cập. Hệ xử lý giao dịch (OLTP) lấy vài bản ghi mỗi truy vấn, tìm bằng khóa, và làm vậy rất nhiều lần mỗi giây; hệ phân tích (OLAP) quét hàng triệu bản ghi để trả về vài con số tổng hợp1. Bài C-Store phát biểu cùng sự phân đôi ấy từ phía thiết kế: kho theo hàng là hệ tối ưu cho ghi và "đặc biệt hiệu quả với các ứng dụng kiểu OLTP", còn "các hệ hướng tới truy vấn tùy biến trên lượng dữ liệu lớn thì nên được tối ưu cho đọc"19.

Có hai loại việc rất khác nhau. Loại một: "cho tôi xem đơn hàng số 4821" — chỉ cần lấy đúng một tờ. Loại hai: "cả năm nay bán được bao nhiêu tiền" — phải đếm hết cả kho. Cùng là một kho giấy tờ, nhưng cách xếp giấy cho hai việc này phải khác nhau hoàn toàn.

The line isn't drawn by technology but by access pattern. Transaction processing (OLTP) fetches a few records per query, found by key, very many times per second; analytics (OLAP) scans millions of records to return a handful of aggregates1. The C-Store paper states the same split from the design side: row stores are write-optimized and "especially effective on OLTP-style applications", while "systems oriented toward ad-hoc querying of large amounts of data should be read-optimized"19.

Rows for transactions, columns for questions

Kho theo hàng gom một bản ghi lại gần nhau — hợp với việc lấy trọn một đơn hàng. Kho theo cột gom một thuộc tính của mọi bản ghi lại gần nhau — hợp với việc cộng một cột trên trăm triệu dòng mà không phải đọc những cột không hỏi tới.

Xếp theo hàng là mỗi tờ đơn một chỗ. Xếp theo cột là gom tất cả ô "số tiền" của mọi tờ vào cùng một trang. Nhớ nhé: muốn cộng tiền thì gom cột tiền lại là nhanh nhất.

A row store keeps one record together — good for pulling a whole order. A column store keeps one attribute of every record together — good for summing one column over a hundred million rows without reading the columns nobody asked about.

Hệ quả về nút thắt đi theo ngay: OLTP thường nghẽn ở việc tìm đúng chỗ trên đĩa, còn OLAP nghẽn ở băng thông — số byte phải kéo lên để trả lời1. Vì thế mọi kỹ thuật của phần này đều quy về một mục tiêu duy nhất: giảm số byte phải đọc. Bố trí theo cột giảm byte bằng cách không đọc cột thừa; nén giảm byte bằng cách biểu diễn gọn hơn; sắp xếp lại giảm byte bằng cách làm cho nén ăn hơn. Ba kỹ thuật, một ý đồ.

Ít dòng vs nhiều dòng OLTP chạm vài bản ghi theo khóa; OLAP quét hàng triệu bản ghi để tổng hợp.
Bản sao riêng Phân tích chạy trên một kho đọc-là-chính, nạp theo lô, tách khỏi hệ vận hành.
Đích đến: bớt byte Theo cột, nén, và sắp xếp lại đều chỉ nhằm giảm lượng byte phải kéo lên.
Lấy một tờ vs đếm cả kho Hai việc khác hẳn nhau nên cách xếp giấy cũng khác.
Chép ra kho riêng Đếm cả kho thì làm trên bản sao, đừng làm phiền chỗ đang bán hàng.
Đọc càng ít càng nhanh Mọi mẹo ở đây đều để phải lật ít giấy hơn.
Few rows vs many OLTP touches a few records by key; OLAP scans millions to aggregate.
A separate copy Analytics runs on a read-oriented store, bulk-loaded, kept away from the operational system.
The goal: fewer bytes Columns, compression and re-sorting all exist to reduce the bytes pulled off disk.
  1. Data warehouse & ETL

    Một bản sao riêng để hỏi thoải máiA separate copy you may interrogate freely

    Vì một truy vấn phân tích có thể quét cạn tài nguyên, người ta không chạy nó thẳng trên hệ vận hành mà trích, biến đổi và nạp dữ liệu sang một kho dữ liệu riêng1. Bài C-Store mô tả đúng nhịp làm việc ấy: kho dữ liệu là "một lớp hệ tối ưu cho đọc, trong đó định kỳ thực hiện một lần nạp hàng loạt dữ liệu mới, rồi tiếp theo là một quãng tương đối dài các truy vấn tùy biến"19.

    Bạn không mang cả kho hàng ra giữa cửa tiệm để đếm trong giờ cao điểm. Bạn chép sổ sang một cuốn khác, để trong phòng sau, rồi tha hồ đếm. Nhớ nhé: đếm trên bản sao, đừng đếm trên bản đang dùng.

    Because one analytic query can exhaust a system, you don't run it on the operational database but extract, transform and load into a separate data warehouse1. The C-Store paper describes exactly that rhythm: a data warehouse is "one class of read-optimized system, in which periodically a bulk load of new data is performed, followed by a relatively long period of ad-hoc queries"19.

    Bạn gặp nó ở đâuNgay lần đầu một truy vấn báo cáo làm chậm trang thanh toán. Việc tách kho không phải là bước "trưởng thành" hình thức — nó là cách giữ cho hai dạng tải không giẫm lên nhau.
    Thấy ở đâuKhi một báo cáo chạy làm cả web chậm theo.
    Where you see itThe first time a reporting query slows down the checkout page. Splitting the warehouse off isn't a maturity ritual — it's how you stop two workloads trampling each other.
  2. Star schema & snowflake schema

    Bảng sự kiện ở giữa, các chiều vây quanhA fact table at the centre, dimensions around it

    Kho phân tích hiếm khi dùng lược đồ chuẩn hóa của hệ vận hành. Nó dùng mô hình chiều, mà Kimball Group mô tả: mô hình chiều có thể hiện thực trong cơ sở dữ liệu quan hệ — gọi là lược đồ sao — hoặc trong cơ sở dữ liệu đa chiều, gọi là khối OLAP; lược đồ sao "đặc trưng gồm các bảng sự kiện nối với các bảng chiều liên quan qua quan hệ khóa chính/khóa ngoại"23. Khi các phân cấp bên trong một bảng chiều lại được chuẩn hóa tiếp thành các bảng phụ, ta có lược đồ bông tuyết24.

    Ở giữa là một bảng ghi lại từng việc đã xảy ra: hôm nào, ai mua, mua gì, bao nhiêu tiền. Quanh nó là các bảng nhỏ giải thích: "mã 17 là bánh mì", "cửa hàng 3 ở quận 1". Vẽ ra thì giống một ngôi sao. Nhớ nhé: bảng sự kiện ở giữa, các bảng giải thích vây quanh.

    Analytic stores rarely use the operational system's normalised schema. They use dimensional modelling, which the Kimball Group describes: dimensional models can be instantiated in relational databases — called star schemas — or in multidimensional databases, known as OLAP cubes; star schemas "characteristically consist of fact tables linked to associated dimension tables via primary/foreign key relationships"23. When the hierarchies inside a dimension table are themselves normalised into secondary tables, you get a snowflake schema24.

    Bạn gặp nó ở đâuTrong mọi mô hình báo cáo mà bảng trung tâm có rất nhiều dòng và rất nhiều khóa ngoại, còn các bảng vệ tinh thì nhỏ nhưng nhiều cột mô tả.
    Thấy ở đâuTrong các bảng báo cáo: một bảng rất dài ở giữa, vài bảng ngắn giải thích quanh nó.
    Where you see itIn any reporting model where the central table has enormous row counts and many foreign keys, while the satellites are small but rich in descriptive columns.

    Vì sao Kimball khuyên tránh bông tuyết

    Tài liệu Kimball nói thẳng: dù bông tuyết biểu diễn dữ liệu phân cấp một cách chính xác, bạn nên tránh nó vì người dùng nghiệp vụ khó hiểu và khó lần theo, và nó cũng có thể làm giảm hiệu năng truy vấn; một bảng chiều phi chuẩn hóa đã được làm phẳng chứa đúng cùng một lượng thông tin như bảng bông tuyết24. Đây là một ví dụ đẹp cho luận điểm chung của chương: chuẩn hóa không phải luôn đúng — nó là một đánh đổi, và ở kho phân tích thì cán cân nghiêng về phía đọc.

    Khối OLAP đứng ở đâu trong bức tranh

    Kimball mô tả khối OLAP là thứ "có thể tương đương về nội dung với, hoặc thường gặp hơn là được dẫn xuất từ, một lược đồ sao quan hệ", và nó thường là bước triển khai cuối cùng của một hệ kho dữ liệu, hoặc tồn tại như một cấu trúc tổng hợp dựng trên lược đồ sao nguyên tử hơn23. Nói cách khác, khối không thay thế kho — nó là lớp kết quả đã tính sẵn nằm trên kho.

  3. Column-oriented storage

    Đừng lưu theo hàng, hãy lưu theo cộtDon't store by row; store by column

    Ý tưởng cốt lõi rất gọn: thay vì để mọi giá trị của một bản ghi cạnh nhau, hãy để mọi giá trị của một cột cạnh nhau. Bài C-Store nêu đây là khác biệt đầu tiên trong thiết kế của nó — "lưu dữ liệu theo cột thay vì theo hàng"19. Ngày nay ý tưởng ấy thành định dạng tệp phổ thông: Apache Parquet tự mô tả là "định dạng tệp dữ liệu hướng cột, mã nguồn mở, thiết kế cho việc lưu trữ và truy xuất hiệu quả", cung cấp các lược đồ nén và mã hóa hiệu năng cao20.

    Thay vì cất mỗi tờ đơn hàng một chỗ, người ta chép tất cả ô "ngày" vào một trang, tất cả ô "số tiền" vào một trang khác. Khi sếp hỏi "tổng tiền cả năm", bạn chỉ cần mở đúng trang tiền. Nhớ nhé: không đọc thứ mình không hỏi tới.

    The core idea is compact: instead of keeping all values of one record together, keep all values of one column together. The C-Store paper lists this as the first difference in its design — "storage of data by column rather than by row"19. Today the idea is an everyday file format: Apache Parquet describes itself as "an open source, column-oriented data file format designed for efficient data storage and retrieval", providing high-performance compression and encoding schemes20.

    Bạn gặp nó ở đâuTrong hầu hết các data lake hiện đại, nơi dữ liệu nằm dưới dạng tệp Parquet và động cơ truy vấn chỉ đọc đúng những cột mà câu lệnh nhắc tới.
    Thấy ở đâuỞ các kho dữ liệu lớn — nơi máy chỉ đọc đúng cột nó cần.
    Where you see itIn most modern data lakes, where data sits as Parquet files and the query engine reads only the columns the statement mentions.

    Dữ liệu lồng nhau thì sao

    Bố trí theo cột có vẻ chỉ hợp với bảng phẳng. Parquet giải quyết bằng cách mượn một kỹ thuật cụ thể: tài liệu định dạng ghi rằng "Parquet dùng mã hóa Dremel với các mức định nghĩa và mức lặp"21. Dremel là hệ của Google được mô tả trong bài cùng tên như "một hệ truy vấn tùy biến, tương tác, có khả năng co giãn để phân tích dữ liệu lồng nhau chỉ-đọc", kết hợp cây thực thi nhiều tầng với bố trí dữ liệu theo cột22.

    Vì sao "chỉ đọc cột cần" lại quan trọng đến thế

    Vì nút thắt của phân tích là băng thông đĩa1. Một bảng sự kiện thực tế có thể có cả trăm cột, còn một truy vấn báo cáo điển hình chỉ chạm tới vài cột. Trong kho theo hàng, đọc vài cột ấy vẫn buộc phải kéo lên toàn bộ bề rộng của mỗi dòng; trong kho theo cột thì không. Đây là nguồn gốc của phần lớn chênh lệch hiệu năng, trước cả khi bàn tới nén.

  4. Column compression: bitmap & run-length

    Xếp theo cột rồi thì nén rất ănOnce grouped by column, compression bites hard

    Một cột thường lặp đi lặp lại rất nhiều giá trị giống nhau — và đó chính là điều kiện lý tưởng để nén. Bài C-Store mô tả một lược đồ mã hóa cụ thể: biểu diễn cột thành các cặp (giá trị, bitmap đánh dấu những vị trí chứa giá trị đó); ví dụ cột số nguyên 0, 0, 1, 1, 2, 1, 0, 2, 1 được mã hóa thành ba cặp, một cặp cho mỗi giá trị phân biệt. Và vì mỗi bitmap đều thưa, nó lại được mã hóa theo độ dài chạy (run-length encoding) để tiết kiệm chỗ19.

    Một trang toàn ô "ngày" thì rất nhiều ô giống nhau. Thay vì chép "thứ Hai" tám trăm lần, bạn ghi "thứ Hai, tám trăm lần". Gọn đi bao nhiêu lần. Nhớ nhé: xếp những thứ giống nhau cạnh nhau thì viết tắt được.

    A column typically repeats the same values over and over — ideal conditions for compression. The C-Store paper describes one concrete encoding: represent a column as (value, bitmap marking the positions holding that value) pairs; a column of integers 0, 0, 1, 1, 2, 1, 0, 2, 1, for instance, becomes three pairs, one per distinct value. And since each bitmap is sparse, it is in turn run-length encoded to save space19.

    Bạn gặp nó ở đâuỞ mọi cột có ít giá trị phân biệt nhưng rất nhiều dòng — mã cửa hàng, mã sản phẩm, trạng thái đơn. Đây cũng là lý do một bảng sự kiện khổng lồ có thể nén xuống nhỏ đến mức bất ngờ.
    Thấy ở đâuỞ những cột lặp đi lặp lại — mã cửa hàng, trạng thái, ngày trong tuần.
    Where you see itIn every column with few distinct values but very many rows — store code, product code, order status. It's also why a gigantic fact table can compress down surprisingly small.
  5. Sort order — and several of them

    Cùng một cột, lưu lại vài lần theo vài thứ tựThe same column, stored several times in several orders

    Các cột trong kho không nằm lộn xộn: C-Store "lưu vật lý một tập hợp các cột, mỗi cột được sắp xếp theo một hoặc vài thuộc tính nào đó", và gọi nhóm các cột cùng thứ tự sắp xếp là projection; cùng một cột có thể tồn tại trong nhiều projection, mỗi nơi sắp theo một thuộc tính khác19. Một trong các tính năng được bài liệt kê chính là "lưu trữ dư thừa các phần tử của một bảng trong nhiều projection chồng lấn theo các thứ tự khác nhau, để một truy vấn có thể được giải bằng projection thuận lợi nhất"19.

    Nếu hôm nay bạn hay tra theo ngày, mai lại hay tra theo cửa hàng, thì sao? Người ta chép kho ra vài bản, mỗi bản xếp theo một thứ tự. Hỏi kiểu nào thì mở bản hợp kiểu đó. Nhớ nhé: dữ liệu đã nén thì chép thêm vài bản cũng không quá đắt.

    Columns in a warehouse don't sit in arbitrary order: C-Store "physically stores a collection of columns, each sorted on some attribute(s)", calling a group of columns sharing a sort order a projection; the same column may exist in several projections, each sorted on a different attribute19. One of the features the paper lists is "redundant storage of elements of a table in several overlapping projections in different orders, so that a query can be solved using the most advantageous projection"19.

    Bạn gặp nó ở đâuKhi bạn phải chọn khóa phân vùng và thứ tự sắp xếp cho một bảng phân tích. Chọn đúng thứ tự không chỉ giúp lọc nhanh — nó còn làm việc nén ăn hơn hẳn, vì các giá trị giống nhau nằm liền nhau.
    Thấy ở đâuKhi bạn quyết định "xếp kho theo ngày hay theo cửa hàng" — và câu trả lời có thể là "cả hai".
    Where you see itWhen choosing partition key and sort order for an analytic table. The right order doesn't just speed up filtering — it makes compression bite harder, because equal values end up adjacent.
  6. Writing to a column store · materialized views

    Kho đã nén chặt thì ghi vào kiểu gìHow do you write into something packed that tight

    Cột đã sắp xếp và nén thì không thể chèn một dòng vào giữa. Lời giải giống hệt LSM-tree: nhận ghi vào một tầng đệm rồi định kỳ trộn xuống. C-Store làm đúng thế bằng cách ghép trong một phần mềm duy nhất "một kho cột tối ưu cho đọc và một kho ghi được, hướng cập nhật/chèn, nối với nhau bởi một tuple mover": thành phần Writeable Store nhỏ lo chèn và cập nhật tốc độ cao, thành phần Read-optimized Store lớn hơn nhiều chỉ nhận dữ liệu qua các đợt chuyển hàng loạt do tuple mover thực hiện19. Ở tầng trên, kết quả hay dùng được tính sẵn và lưu lại: PostgreSQL mô tả khung nhìn vật chất hóa như một khung nhìn lưu lại kết quả truy vấn một cách vật lý và phải được làm mới định kỳ25 — còn khối OLAP của Kimball chính là dạng tổng hợp nhiều chiều của cùng ý tưởng ấy23.

    Kho đã xếp gọn và buộc chặt thì không nhét thêm một tờ vào giữa được. Nên hàng mới được để tạm ở một ngăn nhỏ ngoài cửa, đến kỳ thì gom lại và xếp vào kho một thể. Còn những con số hay bị hỏi thì tính sẵn, ghi ra một tờ riêng. Nhớ nhé: tính trước một lần còn hơn tính lại mỗi lần.

    Sorted, compressed columns can't take a row inserted in the middle. The answer looks just like an LSM-tree: buffer writes, then merge down periodically. C-Store does exactly that, combining in one piece of software "both a read-optimized column store and an update/insert-oriented writeable store, connected by a tuple mover": a small Writeable Store architected for high-performance inserts and updates, and a much larger Read-optimized Store that accepts data only through the batch movement the tuple mover performs19. A layer up, frequently-used results are precomputed and kept: PostgreSQL describes a materialized view as one that physically stores the query result and must be refreshed25 — and Kimball's OLAP cube is the multi-dimensional aggregate form of the same idea23.

    Bạn gặp nó ở đâuỞ mọi bảng tổng hợp chạy đêm, mọi khung nhìn vật chất hóa, mọi lớp "gold" trong kiến trúc dữ liệu nhiều tầng. Cái giá luôn là cùng một thứ: kết quả tính sẵn thì nhanh nhưng cũ, và bạn phải chọn được mức cũ chấp nhận được.
    Thấy ở đâuỞ những báo cáo "tính sẵn từ đêm qua" — mở ra là có ngay, nhưng số liệu là của hôm qua.
    Where you see itIn every nightly rollup table, every materialized view, every "gold" layer of a tiered data architecture. The price is always the same: precomputed results are fast but stale, and you must pick a staleness you can live with.

Ba ngộ nhận thường gặp về engine lưu trữThree common misreadings about storage engines

“LSM luôn nhanh hơn B-tree”"LSM is always faster than a B-tree"

Sai — và chính bài LSM gốc đã nói ngược lại: cấu trúc này hữu ích nhất khi số lần chèn nhiều hơn số lần tìm, còn "các lần tìm cần trả lời ngay sẽ mất hiệu quả I/O trong một số trường hợp"4. Kleppmann trình bày đây là đánh đổi phụ thuộc workload, và khuyên đo trên chính workload của mình1.

Wrong — and the original LSM paper says the opposite: the structure is most useful where inserts outnumber finds, while "indexed finds requiring immediate response will lose I/O efficiency in some cases"4. Kleppmann frames it as a workload-dependent trade-off and advises measuring on your own workload1.

“Hợp nhất là việc nền, không ảnh hưởng ai”"Compaction is background work, it bothers nobody"

Sai: nó tiêu thụ chính băng thông đĩa mà truy vấn đang cần. Tài liệu Cassandra mô tả tình huống dung lượng cho cả tệp cũ lẫn mới trong lúc hợp nhất vượt quá chỗ trống của một nút7, còn wiki RocksDB ghi khuếch đại ghi của hợp nhất theo tầng "thường lớn hơn 10"6.

Wrong: it consumes the very disk bandwidth live queries need. Cassandra's docs describe the space for old and new SSTables during compaction outstripping a node's free space7, and the RocksDB wiki puts leveled compaction's write amplification at "often larger than 10"6.

“Kho cột chỉ là bảng xoay ngang”"A column store is just a table turned sideways"

Sai: lợi thế đến từ việc các cột được sắp xếp và nén cùng nhau. C-Store lưu "một tập hợp các projection hướng cột chồng lấn, thay cho kiểu bảng-và-chỉ-mục thường thấy", và dùng rộng rãi chỉ mục bitmap để bổ trợ cấu trúc B-tree19.

Wrong: the advantage comes from columns being sorted and compressed together. C-Store stores "an overlapping collection of column-oriented projections, rather than the current fare of tables and indexes", and makes extensive use of bitmap indexes to complement B-tree structures19.

Tự đọc mìnhRead yourself

Soi cái engine đang đỡ hệ của bạnLook at the engine holding your system up

Chương 3 không bắt bạn viết một engine; nó cho bạn đủ từ vựng để hỏi đúng câu về cái engine mình đang chạy. Những câu dưới đây không đo hệ của bạn lớn tới đâu — chúng soi xem bạn đã biết mình đang trả giá ở đâu hay chưa.

Thử soi một hệ thật bằng mấy câu dưới đây. Không phải để xem hệ to hay nhỏ, mà để xem bạn có biết nó đang tốn công ở chỗ nào không.

Chapter 3 doesn't ask you to write an engine; it gives you enough vocabulary to ask the right questions about the one you run. These don't measure how big your system is — they check whether you know where you are paying.

Engine dưới hệ của bạn thuộc họ nào — log-structured hay page-oriented?Câu này quyết định hình dạng của mọi vấn đề hiệu năng bạn sẽ gặp. Họ LSM sẽ cho bạn những cơn gai độ trễ lúc hợp nhất và hóa đơn khuếch đại ghi; họ B-tree sẽ cho bạn phân mảnh trang và tranh chấp khóa. Nếu chưa trả lời được, đây là thứ nên tra trước tiên trong tài liệu phiên bản bạn đang chạy.
Bạn có bao nhiêu chỉ mục trên bảng ghi nhiều nhất, và mỗi cái phục vụ truy vấn nào?Chỉ mục là cấu trúc phái sinh: mỗi cái đều bắt mọi lần ghi gánh thêm việc. Hãy liệt kê từng chỉ mục cùng truy vấn nó phục vụ. Cái nào không gọi tên được truy vấn tương ứng thì gần như chắc chắn bạn đang trả tiền cho một câu hỏi không ai hỏi.
Toàn bộ tập khóa của bạn có cần vừa bộ nhớ không — và bạn có biết nó đang chiếm bao nhiêu?Một số thiết kế chỉ mục giữ mọi khóa trong RAM, và khi đó tăng trưởng khóa chứ không phải tăng trưởng dữ liệu mới là cái tường bạn sẽ đâm vào. Hãy đo số khóa phân biệt, không chỉ dung lượng đĩa.
Truy vấn theo khoảng có quan trọng với bạn không, và chỉ mục hiện tại có phục vụ được không?Chỉ mục băm không giữ thứ tự nên "mọi khóa từ A đến B" sẽ phải quét. Nếu phần lớn câu hỏi của bạn có dạng khoảng — theo thời gian, theo tiền tố — thì một cấu trúc có thứ tự mới là chỗ đúng để đầu tư.
Lịch hợp nhất của bạn chạy khi nào, và bạn có nhìn thấy nó trên biểu đồ độ trễ không?Hợp nhất giành băng thông đĩa với truy vấn đang phục vụ, nên nó thường hiện ra ở phần đuôi p99 chứ không ở số trung bình. Nếu bạn chưa từng vẽ chồng lịch hợp nhất lên biểu đồ độ trễ, rất có thể bạn đang đổ lỗi nhầm cho mạng.
Bạn còn dư bao nhiêu dung lượng cho lúc hợp nhất cần cả tệp cũ lẫn tệp mới?Đây là dạng sự cố im lặng đến bất ngờ: đĩa còn 30% trống nghe rất an toàn cho tới khi một lần hợp nhất lớn cần chỗ cho cả hai bản. Hãy tính chỗ dư theo kích thước tệp lớn nhất, không theo tỉ lệ phần trăm chung chung.
Truy vấn phân tích của bạn đang chạy trên hệ vận hành hay trên một bản sao riêng?Một truy vấn quét toàn bảng có thể hút sạch băng thông mà giao dịch đang cần. Nếu báo cáo và thanh toán dùng chung một đĩa, thì mỗi kỳ chốt sổ là một sự cố hiệu năng được lên lịch sẵn.
Bảng phân tích lớn nhất của bạn lưu theo hàng hay theo cột — và nó đang được sắp xếp theo gì?Hai câu này đi liền nhau: bố trí theo cột giúp bạn không đọc cột thừa, còn thứ tự sắp xếp quyết định việc nén ăn tới đâu và bộ lọc nào cắt được sớm. Nếu bạn chưa từng chọn thứ tự sắp xếp một cách có chủ ý, thì hệ đang chọn hộ bạn.
Hệ của bạn ghi nhiều hơn hay đọc nhiều hơn?Biết điều này mới chọn được kiểu tủ phù hợp.
Bạn có bao nhiêu "mục lục", và mỗi cái để trả lời câu hỏi gì?Mục lục nào không ai dùng thì chỉ làm việc ghi nặng thêm.
Có lúc nào hệ bỗng khựng một nhịp rồi lại bình thường không?Rất có thể đó là lúc nó đang dọn dẹp bên trong.
Khi cần đếm cả kho, bạn đếm trên bản chính hay bản sao?Đếm trên bản chính thì người đang mua hàng phải chờ.
Which family is the engine under your system — log-structured or page-oriented?This shapes every performance problem you will meet. LSM gives you latency spikes during compaction and a write-amplification bill; B-trees give you page fragmentation and lock contention. If you can't answer, that's the first thing to look up for the version you run.
How many indexes sit on your most-written table, and which query does each serve?An index is derived data: each one makes every write do more. List them with the query they serve. Any index you can't name a query for is money paid for a question nobody asks.
Does your whole key set need to fit in memory — and do you know how big it is?Some index designs hold every key in RAM, and then key growth, not data growth, is the wall you hit. Measure distinct keys, not just disk usage.
Do range queries matter to you, and does your current index serve them?A hash index keeps no order, so "every key from A to B" degrades to a scan. If most of your questions are range-shaped — by time, by prefix — a sorted structure is the right place to invest.
When does compaction run, and can you see it on your latency chart?Compaction competes for disk bandwidth with live queries, so it shows up in the p99 tail rather than the average. If you've never overlaid the compaction schedule on latency, you may be blaming the network unfairly.
How much spare disk do you keep for the moment compaction needs old and new files at once?This failure is surprisingly quiet: 30% free sounds safe right up until one big merge needs room for both copies. Size the headroom against your largest file, not a blanket percentage.
Do your analytic queries run on the operational system or on a separate copy?One full-table scan can drain the bandwidth transactions need. If reporting and checkout share a disk, every close-of-books is a scheduled performance incident.
Is your largest analytic table stored by row or by column — and what is it sorted on?The two go together: a column layout saves you from reading columns you didn't ask for, and the sort order decides how well compression bites and which filters cut early. If you never chose a sort order deliberately, the system chose one for you.
Kết luận cô đọngThe conclusion, distilled
Không có engine lưu trữ nào nhanh hơn engine khác một cách tuyệt đối — chỉ có những engine trả giá ở chỗ khác nhau. Ghi nối đuôi đẩy chi phí về phía hợp nhất; sửa tại chỗ đẩy chi phí về phía tìm trang và nhật ký; chỉ mục đẩy chi phí từ lúc đọc sang lúc ghi; kho cột đẩy chi phí từ lúc hỏi sang lúc nạp. Vì vậy câu hỏi đáng giá không phải "cái nào tốt nhất", mà "tôi đang trả giá ở đâu, và đó có phải chỗ tôi muốn trả không" — và câu trả lời chỉ có nghĩa khi gắn với workload cụ thể của bạn.Không có cái tủ nào "tốt nhất". Chỉ có tủ tốn công ở chỗ này và tủ tốn công ở chỗ kia. Nhớ nhé: đừng hỏi "cái nào nhanh hơn", hãy hỏi "mình hay làm việc gì nhất" — rồi chọn cái tủ hợp với việc đó.No storage engine is absolutely faster than another — there are only engines that pay in different places. Appending pushes cost into compaction; updating in place pushes it into page lookups and logging; an index pushes cost from read time to write time; a column store pushes it from query time to load time. So the question worth asking isn't "which is best", but "where am I paying, and is that where I want to pay" — and the answer only means something tied to your specific workload.