DDIA·مطالعهSTUDY

نقشه راهRoadmap/ فصل ۴Chapter 4

ذخیره‌سازی و بازیابیStorage and retrieval

این فصل به داخلِ موتورِ ذخیره‌سازی می‌رود: داده کجا نوشته می‌شود و موقع خواندن چطور پیدا می‌شود؟ لاگ، ایندکس، B-tree، LSM و ذخیره‌سازی ستونی، هزینهٔ خواندن و نوشتن را به شکل‌های متفاوت تقسیم می‌کنند. From the “world’s simplest database” through LSM and B-tree engines to warehouse columnar storage and text/vector indexes — with 6 interactive (Archify) and 3 concept diagrams, a teacher narration for each, and an interview question pack.

۹9 نمودار (۶ تعاملی + ۳ مفهومی)diagrams (6 interactive + 3 concept)· محور: log-structured · B-tree · columnar · indexesTheme: log-structured · B-tree · columnar · indexes· زمان مطالعه: حدود ۶۰ دقیقهReading time: ~60 min

۰0نقشهٔ راه فصلChapter roadmap

ایدهٔ کلیدی فصلChapter thesis

این فصل به داخلِ موتورِ ذخیره‌سازی می‌رود: داده کجا نوشته می‌شود و موقع خواندن چطور پیدا می‌شود؟ لاگ، ایندکس، B-tree، LSM و ذخیره‌سازی ستونی، هزینهٔ خواندن و نوشتن را به شکل‌های متفاوت تقسیم می‌کنند.

مثال فنی: یک داده، دو مسیر خواندن

وضعیت اولیه: جدول سفارش با id خوانده می‌شود و گزارش فروش فقط ستون amount را جمع می‌زند.

اجرای مرحله‌به‌مرحله: lookup نقطه‌ای از ایندکس استفاده می‌کند؛ اسکن تحلیلی از چیدمان ستونی سود می‌برد.

نتیجه و دلیل: موتور ذخیره‌سازی را باید با نوع خواندن و نوشتن سنجید؛ سریع بودن یک lookup دربارهٔ سرعت اسکن میلیاردی کافی نیست.

مثال‌های این فصل سناریوهای آموزشیِ برگرفته از کارهای روزمره‌اند. عددهای انتخاب‌شده برای توضیح‌اند؛ معماریِ داخلیِ یک شرکت مشخص را توصیف نمی‌کنند.

At the lowest level every database must do just two things: keep the data you give it and hand it back when you ask later. Chapter 3 was about the “shape” of data and query languages; this chapter looks through the engine’s eyes: how data is laid out on disk and how it is found at query time.

چرا توسعه‌دهنده باید این را بداند؟ چون باید از میان موتورهای فراوان، موتورِ مناسب الگوی بارکاریِ خودت را انتخاب کنی و پارامترهایش را درست بچینی. برای هر دو کار، داشتن نقشهٔ ذهنی از زیر کاپوت لازم است.

Why should a developer know this? Because you must pick the right engine for your workload pattern out of dozens of options and set its parameters correctly. Both jobs need a mental map of what happens under the hood.

مهم‌ترین خطِ تقسیم این فصل، همان تفکیک OLTP در برابر OLAP از فصل ۱ است:

This chapter’s most important dividing line is the same OLTP vs OLAP split from Chapter 1:

  • موتورهای تراکنشی (OLTP): تعداد زیادی درخواست، هرکدام چند رکورد کوچک، پاسخ سریع. دسترسی معمولاً با کلید اصلی یا ایندکس ثانویه؛ ساختارهای اصلی: ایندکس‌های مرتب از کلید به رکورد که range query هم بدهند.
  • موتورهای تحلیلی (OLAP/انبارداده): چند پرس‌وجوی پیچیده که میلیون‌ها رکورد را می‌پیمایند. ساختار اصلی: ذخیره‌سازی ستونی با فشرده‌سازی (برای کمینه کردن دادهٔ خوانده‌شده از دیسک) و اجرای JIT یا برداری (برای کمینه کردن زمان CPU).
  • Transactional (OLTP) engines: many requests, each touching a few small records, with fast responses. Access is usually by primary key or secondary index; the core structures are sorted key-to-record indexes that also serve range queries.
  • Analytical (OLAP/warehouse) engines: a few complex queries scanning millions of records. The core structure is columnar storage with compression (to minimize bytes read from disk) plus JIT or vectorized execution (to minimize CPU time).
در سمت OLTP هم دو مکتب موتور داریم که بقیهٔ فصل حول آن‌ها می‌چرخد:

On the OLTP side there are two engine schools that the rest of the chapter revolves around:

  • ساخت‌یافته / Log-Structured: فقط افزودن به فایل و حذف فایل‌های کهنه؛ هرگز بازنویسی درجا. SSTable، LSM-tree، RocksDB، Cassandra، HBase، ScyllaDB و حتی Lucene در این خانواده‌اند. معمولاً بازده نوشتن بالا.
  • بازنویسی درجا / Update-in-place: دیسک مجموعه‌ای از صفحات با اندازهٔ ثابت است که درجا بازنویسی می‌شوند. B-treeها نمونهٔ برجسته‌اند و در تقریباً همهٔ پایگاه‌های داده رابطه‌ای هستند. معمولاً خواندن سریع‌تر و قابل‌پیش‌بینی‌تر.
  • Log-structured: only append to files and delete stale files; never rewrite in place. SSTable, LSM-tree, RocksDB, Cassandra, HBase, ScyllaDB and even Lucene belong to this family. Usually high write throughput.
  • Update-in-place: the disk is a set of fixed-size pages rewritten in place. B-trees are the flagship example and live in almost every relational database. Usually faster, more predictable reads.
نکتهٔ مصاحبهInterview tip
کلِ مفهوم «ایندکس» یک تریدآف است: ساختاری مشتق‌شده از دادهٔ اصلی که خواندن را سریع می‌کند، اما هر نوشتن باید آن را هم به‌روز کند؛ یعنی نوشتن کندتر و فضای بیشتر. به‌همین‌دلیل پایگاه داده‌ها همه‌جا ایندکس نمی‌سازند و انتخاب ایندکس با دانستن الگوی کوئری‌های تو انجام می‌شود.

The whole idea of an “index” is a trade-off: a structure derived from the primary data that speeds up reads, but every write must update it too — meaning slower writes and more space. That is why databases don’t index everything, and why index choice is made by someone who knows your query patterns: you.

۱1ساده‌ترین پایگاه داده جهانThe world’s simplest database

ساده‌ترین ذخیره‌ساز می‌تواند هر مقدار تازه را به انتهای یک فایل اضافه کند. نوشتن ساده است، اما برای پیدا کردن آخرین مقدارِ یک کلید باید فایل را جست‌وجو کنیم.

مثال فنی: آخرین مقدار در لاگ

وضعیت اولیه: فایل به‌ترتیب P7=100 و P7=120 دارد.

اجرای مرحله‌به‌مرحله: نوشتن فقط به انتهای فایل اضافه می‌کند؛ خواندن بدون ایندکس باید آخرین رکورد P7 را در فایل پیدا کند.

نتیجه و دلیل: append ساده است، ولی خواندن با رشد فایل گران می‌شود؛ ایندکس نشانی آخرین مقدار را نگه می‌دارد.

The book opens with two bash functions that form a complete key-value store — and every lesson of this chapter grows out of that simplicity:

#!/bin/bash
db_set () {
  echo "$1,$2" >> database
}
db_get () {
  grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

$ db_set 12 '{"name":"London","attractions":["Big Ben"]}'
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate"]}'
$ db_get 42
هر فراخوانیِ db_set یک خط به انتهای فایل افزوده می‌شود؛ مقدارهای قدیمیِ هر کلید بازنویسی نمی‌شوند و آخرین مقدار، آخرین رخدادِ همان کلید در فایل است (به‌همین‌دلیل tail -n 1). این دقیقاً همان کاری است که پایگاه‌های داده واقعی با لاگ می‌کنند.

Each db_set call appends one line to the end of the file; old values of a key are never rewritten, and the latest value is the key’s last occurrence in the file (hence tail -n 1). That is exactly what real databases do with a log.

۱.۱ لاگ، ایندکس و تریدآف1.1 Log, index and trade-off

  • لاگ (log) در این کتاب یعنی: دنباله‌ای فقط‌افزودنی از رکوردها روی دیسک. لازم نیست انسان‌خوان باشد؛ می‌تواند باینری و فقط داخلی باشد. افزودن به فایل، ساده‌ترین و معمولاً سریع‌ترین نوع نوشتن است.
  • مسئله در خواندن است: db_get باید کل فایل را بگردد؛ هزینهٔ هر جست‌وجو O(n) است — با دو برابر شدن رکوردها، دو برابر کندتر.
  • پاسخ: ایندکس — ساختاری اضافی و مشتق‌شده از دادهٔ اصلی (مثلاً مرتب‌شده بر اساس کلید) که یافتن داده را سریع می‌کند. افزودن و حذف ایندکس محتوای پایگاه داده را عوض نمی‌کند؛ فقط کارایی را.
  • هر ایندکس نوشتن را کند می‌کند (چون با هر نوشتن باید او هم به‌روز شود) و فضا می‌گیرد. چون هیچ ایندکسی رایگان نیست، پایگاه داده‌ها به‌طور پیش‌فرض روی همه‌چیز ایندکس نمی‌زنند؛ تو با دانستن الگوی کوئری‌ها انتخاب می‌کنی.
  • Log in this book means: an append-only sequence of records on disk. It need not be human-readable; it can be binary and purely internal. Appending to a file is the simplest and usually the fastest kind of write.
  • The problem is reading: db_get must scan the whole file; every lookup costs O(n) — twice the records, twice as slow.
  • The answer: an index — an extra structure derived from the primary data (e.g. sorted by key) that finds data fast. Adding or dropping an index never changes the database’s content; only its performance.
  • Every index slows writes (each write must update it too) and takes space. Since no index is free, databases don’t index everything by default; you choose, knowing your query patterns.
زیر کاپوتUnder the hood

لاگ تاریخِ نوشته‌ها را نگه می‌دارد؛ ایندکس مسیر کوتاهی برای پیدا کردن داده می‌سازد. این مسیر اضافی، فضای ذخیره‌سازی و کارِ هنگام نوشتن می‌خواهد.

مثال فنی: بهای ایندکسِ اضافه

وضعیت اولیه: روی جدول سفارش، ایندکس customer_id ساخته‌ایم.

اجرای مرحله‌به‌مرحله: خواندن سفارش‌های مشتری ۱۷ دیگر کل جدول را اسکن نمی‌کند؛ هر درج یا تغییر مشتری باید ایندکس را هم اصلاح کند.

نتیجه و دلیل: کاهش هزینهٔ خواندن با فضای بیشتر و کار اضافهٔ نوشتن همراه است؛ ایندکسِ بلااستفاده هزینهٔ رایگان نیست.

A real database has three more problems: concurrent writes, reclaiming space so the log doesn’t grow forever, and half-written records during crash recovery (all covered in Chapter 8). But the “append to the end of the file” principle is the same.

نمودار مفهومی — تریدآف لاگ و ایندکسConcept diagram — the log/index trade-off

نمودار مفهومی · از لاگ فقط‌افزودنی تا ایندکس: تریدآف اصلیConcept diagram · From append-only log to index: the core trade-off باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: بهای ایندکسِ اضافه

وضعیت اولیه: روی جدول سفارش، ایندکس customer_id ساخته‌ایم.

اجرای مرحله‌به‌مرحله: خواندن سفارش‌های مشتری ۱۷ دیگر کل جدول را اسکن نمی‌کند؛ هر درج یا تغییر مشتری باید ایندکس را هم اصلاح کند.

نتیجه و دلیل: کاهش هزینهٔ خواندن با فضای بیشتر و کار اضافهٔ نوشتن همراه است؛ ایندکسِ بلااستفاده هزینهٔ رایگان نیست.

شکل را از چپ بخوانید: جعبهٔ «لاگ فقط‌افزودنی» (هر db_set به انتها اضافه می‌شود؛ نوشتن O(1) و ترتیبی) با فلش «نوشتن ارزان» به جعبهٔ «مشکل خواندن» می‌رسد (db_get همهٔ خط‌ها را می‌گردد؛ خواندن O(n)). از آنجا فلش «خواندن کند» به جعبهٔ «افزودن ایندکس مشتق‌شده» می‌رود (ساختار مرتب بر اساس کلید؛ خواندن O(log n)). پنل پایین حکم را می‌گوید: هر ایندکس خواندن را سریع می‌کند، اما هر نوشتن باید او را هم به‌روز کند و فضا می‌گیرد — «ایندکس رایگان نداریم؛ با دانستن الگوی کوئری انتخاب کن».

Read left to right: “Append-only log” (every db_set appends; O(1) sequential writes) leads via “cheap writes” to “The read problem” (db_get scans every line; O(n) reads), then via “slow reads” to “Add a derived index” (a key-sorted structure; O(log n) reads). The bottom panel states the verdict: every index speeds reads but must be updated on each write and takes space — “no free index; choose by query pattern.”

۲2ذخیره‌سازی Log-StructuredLog-structured storage

در ذخیره‌سازی لاگ‌محور، تغییرات تازه به ساختارهای جدید افزوده می‌شوند. در پس‌زمینه، بخش‌های قدیمی ادغام می‌شوند تا دادهٔ تکراری و فضای بلااستفاده کمتر شود.

مثال فنی: نسخه‌های تازه در LSM

وضعیت اولیه: P7=100 در SSTable قدیمی است و P7=120 در memtable نوشته می‌شود.

اجرای مرحله‌به‌مرحله: نوشتن، WAL و حافظه را به‌روز می‌کند؛ flush فایل مرتب تازه می‌سازد و compaction نسخه‌های لازم را ادغام می‌کند.

نتیجه و دلیل: خواننده باید نسخهٔ قابل‌مشاهدهٔ تازه را انتخاب کند؛ پیدا شدن کلید در یک فایل قدیمی برای توقف کافی نیست.

Suppose you want to keep the same append-only file and only make reads fast. First idea: an in-memory hash map from each key to the byte offset of its latest value in the file. Writes = append to the file + update the hash; reads = jump to the offset and read (no I/O at all if it sits in the OS cache).

اما این طرح سه مشکل جدی دارد:

But this design has three serious problems:

  • فضای رکوردهای بازنویسی‌شده هرگز آزاد نمی‌شود؛ با نوشتن مداوم، دیسک پر می‌شود.
  • hash map پایدار نیست و بعد از restart باید با اسکن کل لاگ بازسازی شود (restart کند).
  • hash map باید در حافظه جا شود؛ hash روی دیسک هم بدقلق است (دسترسی تصادفی زیاد، بزرگ‌سازی گران، برخورد کلیدها).
  • range query ندارد: نمی‌توانی ساده بگویی «همهٔ کلیدهای ۱۰۰۰۰ تا ۱۹۹۹۹»؛ باید تک‌تک را جداگانه بیابی.
  • Space of overwritten records is never reclaimed; with constant writes the disk fills up.
  • The hash map is not durable and must be rebuilt by scanning the whole log after a restart (slow restarts).
  • The hash map must fit in memory; a hash on disk is awkward (lots of random access, expensive growth, key collisions).
  • No range queries: you can’t simply ask for “keys 10000 to 19999”; each must be looked up separately.

از لاگ تا SSTableFrom log to SSTable

در LSM، تغییرات ابتدا در حافظه جمع می‌شوند و سپس به فایل‌های مرتب‌شدهٔ SSTable می‌روند. لاگِ بازیابی کمک می‌کند خرابی، تغییراتِ هنوز منتقل‌نشده را از بین نبرد.

مثال فنی: بلوکِ مناسب به‌جای کل فایل

وضعیت اولیه: SSTable کلیدهای مرتب 100…999 دارد و ایندکس پراکنده شروع هر بلوک را می‌شناسد.

اجرای مرحله‌به‌مرحله: برای کلید 740 بلوکِ شروع‌شونده در 700 پیدا و همان بخش خوانده می‌شود.

نتیجه و دلیل: مرتب بودن هم جست‌وجوی بلوک و هم اسکن بازهٔ 740…760 را ممکن می‌کند؛ هش به‌تنهایی ترتیب بازه را نمی‌دهد.

  • در عمل، به‌جای hash، داده را مرتب بر اساس کلید نگه می‌دارند. نمونه: SSTable (Sorted Strings Table) — جفت‌کلیدمقدارهای مرتب که هر کلید فقط یک‌بار می‌آید.
  • ایندکس پراکنده (sparse index): به‌جای همهٔ کلیدها، فقط اولین کلیدِ هر بلوکِ چند کیلوبایتی را نگه می‌دارند. مثال کتاب: برای یافتن handiwork، چون مرتب است می‌دانی بین handbag و handsome است؛ به offset بلوک اول می‌پری و همان چند کیلوبایت را می‌گردی.
  • هر بلوک قابل فشرده‌سازی است: فضای کمتر + پهنای باند I/O کمتر، به بهای اندکی CPU.
  • ولی نوشتن سخت می‌شود: نمی‌توان ساده اضافه کرد (چون ترتیب می‌شکند) و بازنویسی کل فایل هم بسیار گران است. راه‌حل، رویکرد log-structured است:
  • memtable: هر نوشته اول به یک نقشهٔ مرتب در حافظه می‌رود (red–black tree، skip list یا trie) — درج با هر ترتیبی، جست‌وجوی کارآمد، بازخوانی مرتب.
  • flush: وقتی memtable از چند مگابایت بزرگ‌تر شد، مرتب روی دیسک به‌صورت یک SSTableِ جدا (جدیدترین سگمنت) نوشته می‌شود؛ هم‌زمان memtable تازه کار را تحویل می‌گیرد.
  • خواندن: اول memtable، بعد سگمنت‌ها از جدید به قدیم؛ اگر کلید هیچ‌جا نبود، وجود ندارد.
  • merge و compaction: مثل mergesort — چند سگمنت کنار هم خوانده می‌شوند، کوچک‌ترین کلید به خروجی می‌رود و اگر کلیدی تکراری بود فقط تازه‌ترین مقدار می‌ماند. خروجی، سگمنتِ مرتبِ جدید است.
  • WAL جدا برای memtable: چون memtable در حافظه است، هر نوشته بلافاصله در یک لاگ روی دیسک هم ثبت می‌شود تا بعد از crash بازسازی شود. بعد از flush، همان بخش از لاگ قابل دور انداختن است.
  • حذف = tombstone: رکورد حذفِ ویژه‌ای اضافه می‌شود که هنگام merge به فرآیند می‌گوید مقادیر قبلیِ همان کلید را دور بریزد.
  • این الگوریتم همان چیزی است که در RocksDB، Cassandra، ScyllaDB و HBase هست (از مقالهٔ Bigtable با اصطلاحات SSTable و memtable) و اصل آن ۱۹۹۶ با نام LSM-tree (Log-Structured Merge-tree) منتشر شد. به همین دلیل به این موتورها LSM storage engines می‌گویند.
  • سگمنت‌ها تغییرناپذیر‌اند: یک‌بار نوشته می‌شوند و بعد فقط خوانده/ادغام/حذف می‌شوند. compaction در thread پس‌زمینه انجام می‌شود و خواندن‌ها هم‌زمان با ورودی‌های merge سرویس می‌گیرند. crash وسط نوشتن هم ساده است: فایل ناتمام دور انداخته می‌شود و لاگ با checksum رکوردهای خراب را کنار می‌گذارد (جزئیات در فصل ۸). این فایل‌ها حتی روی object storage هم عالی‌اند (مثل SlateDB و Delta Lake).
  • In practice, instead of a hash, data is kept sorted by key. Example: the SSTable (Sorted Strings Table) — sorted key-value pairs where each key appears once.
  • Sparse index: instead of all keys, keep only the first key of each multi-kilobyte block. The book’s example: to find handiwork, sorted order tells you it sits between handbag and handsome; jump to the first block’s offset and search just those kilobytes.
  • Each block can be compressed: less space + less I/O bandwidth, for a little CPU.
  • But writes get hard: you can’t simply append (order would break) and rewriting the whole file is far too expensive. The answer is the log-structured approach:
  • Memtable: every write first lands in an in-memory sorted map (red–black tree, skip list or trie) — inserts in any order, efficient lookup, sorted iteration.
  • Flush: when the memtable grows past a few megabytes it is written to disk, sorted, as a separate SSTable (the newest segment); a fresh memtable takes over.
  • Reads: memtable first, then segments newest to oldest; if the key is nowhere, it doesn’t exist.
  • Merge and compaction: like mergesort — several segments are read side by side, the smallest key goes to the output, and for duplicate keys only the newest value survives. The output is a new sorted segment.
  • A separate WAL for the memtable: since the memtable lives in memory, every write is also recorded immediately in an on-disk log so it can be rebuilt after a crash. After a flush, that part of the log can be discarded.
  • Deletes = tombstones: a special delete record is added that tells a merge to throw away that key’s older values.
  • This algorithm is what runs inside RocksDB, Cassandra, ScyllaDB and HBase (SSTable and memtable terms come from the Bigtable paper) and its essence was published in 1996 as the LSM-tree (Log-Structured Merge-tree). Hence these engines are called LSM storage engines.
  • Segments are immutable: written once, then only read, merged or deleted. Compaction runs in a background thread while reads keep being served from the merge inputs. A crash mid-write is simple too: the unfinished file is thrown away and the log’s checksums drop corrupt records (details in Chapter 8). These files are even great on object storage (e.g. SlateDB and Delta Lake).

۲.۱ نمودار ۱ + روایت مدرس2.1 Diagram 1 — LSM write and read paths

نمودار ۱ · Dataflow — مسیر نوشتن و خواندن در موتور LSMDiagram 1 · Dataflow — LSM write and read paths باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۱Teacher narration

مثال فنی برای خواندنِ نمودار: نسخه‌های تازه در LSM

وضعیت اولیه: P7=100 در SSTable قدیمی است و P7=120 در memtable نوشته می‌شود.

اجرای مرحله‌به‌مرحله: نوشتن، WAL و حافظه را به‌روز می‌کند؛ flush فایل مرتب تازه می‌سازد و compaction نسخه‌های لازم را ادغام می‌کند.

نتیجه و دلیل: خواننده باید نسخهٔ قابل‌مشاهدهٔ تازه را انتخاب کند؛ پیدا شدن کلید در یک فایل قدیمی برای توقف کافی نیست.

این نمودار چهار ستون دارد که از چپ به راست عنوان‌شان هست: «Write Requests»، «Reads & Memtable»، «Sorted Segments» و «Background Merge». دو نوار افقی داریم: نوار بالا مسیر نوشتن است و نوار پایین، مسیر خواندن و پشتیبانی.

Read this diagram as four columns — Write Requests, Reads & Memtable, Sorted Segments, Background Merge — with a write lane on top and a read/support lane below. The Write box (put(key, value)) sends update ordered map to the Memtable and append + fsync down to the Write-Ahead Log; the log’s only job is replay after crash back into the memtable.

از جعبهٔ «Write» با زیرنویسِ put(key, value) شروع کنید — بالا سمت چپ. دو فلش از آن بیرون می‌آید: فلش پررنگِ «update ordered map» مستقیم به جعبهٔ «Memtable» (نقشهٔ مرتب در حافظه) می‌رود، و فلش چین‌چینِ «append + fsync» به پایین، به جعبهٔ «Write-Ahead Log» با زیرنویسِ «append-only, checksummed». از همان لاگ، فلش چین‌چینِ دیگری با برچسب «replay after crash» به memtable برمی‌گردد — یعنی تنها دلیلِ وجودِ لاگ، بازسازیِ حافظه بعد از قطعی است.

The write path continues with flush sorted SSTable into SSTable Segments (sorted, immutable files, newest to oldest), then merge segments, keep newest value into Compaction, where tombstones dropped reminds you that deletes are collected here.

مسیر نوشتن ادامه می‌یابد: فلش پررنگِ «flush sorted SSTable» از memtable به جعبهٔ «SSTable Segments» (زیرنویس: «sorted, immutable files»، برچسب: «newest to oldest») و بعد فلشِ «merge segments, keep newest value» به جعبهٔ «Compaction» در ستون آخر — برچسب کنارش «tombstones dropped» یادآوری می‌کند که رکوردهای حذف همین‌جا جمع می‌شوند.

The write path continues with flush sorted SSTable into SSTable Segments, then merge segments, keep newest value into Compaction (tombstones dropped).

حالا نوار پایین: جعبهٔ «Point Read» با زیرنویسِ «get(key), newest first». سه فلش شماره‌گذاری‌شده دارد: «1. check memtable» به بالا به memtable؛ «2. key in this segment?» به راست به جعبهٔ «Bloom Filter» (زیرنویس: «one bitmap per segment»)؛ و از Bloom filter فلشِ «3. seek sparse index + block» به بالا، به سگمنت‌ها. همین سه شماره، تمام قصهٔ جست‌وجوی نقطه‌ای در LSM است: اول حافظه، بعد سگمنت‌های جدید به قدیم، و هر سگمنت را فقط اگر Bloom filter اجازه داد باز می‌کنیم.

Below, Point Read (get(key), newest first) follows three numbered arrows: 1. check memtable, then 2. key in this segment? to the Bloom Filter (one bitmap per segment), then 3. seek sparse index + block up into the segments. That is the whole LSM point-lookup story.

و درسِ ترکیبیِ شکل: مسیر نوشتن همیشه «افزودن» است (به لاگ، به memtable، به سگمنت تازه) و هیچ‌جا بازنویسی درجا نمی‌بینید؛ سنگینیِ کارِ مرتب‌سازی و پاک‌سازی به یک جعبهٔ پس‌زمینه یعنی Compaction منتقل شده است.

The combined lesson: the write path is always “append” (to the log, the memtable, a fresh segment) with no in-place rewrite anywhere; all sorting and cleanup weight moves to one background box, Compaction.

۲.۲ Bloom filter و استراتژی compaction2.2 Bloom filters and compaction strategies

Bloom filter

پاسخِ «نیست» در Bloom filter قطعی است و پاسخِ «شاید هست» بررسیِ واقعی می‌خواهد. این فیلتر مقدارِ رکورد را نگه نمی‌دارد.

مثال فنی: شاید هست، قطعاً نیست

وضعیت اولیه: برای یک SSTable، Bloom filter می‌گوید بیت لازم برای کلید P99 صفر است.

اجرای مرحله‌به‌مرحله: فایل برای P99 خوانده نمی‌شود؛ اگر همهٔ بیت‌ها یک باشند، فایل هنوز باید بررسی شود.

نتیجه و دلیل: پاسخ مثبت می‌تواند کاذب باشد؛ فیلترِ سالم برای مجموعهٔ ساخته‌شده پاسخ منفی کاذب نمی‌دهد و جای بررسی داده را نمی‌گیرد.

  • برای هر سگمنت یک بیت‌آرایه؛ برای هر کلید چند هش حساب می‌شود و بیت‌های متناظر ۱ می‌شوند.
  • هنگام جست‌وجو: اگر حتی یک بیت ۰ بود، کلید قطعاً در آن سگمنت نیست → سگمنت رد می‌شود (بدون I/O).
  • اگر همه ۱ بودند، کلید احتمالاً هست — ولی ممکن است تصادفی (false positive). آن‌وقت باید ایندکس پراکنده را ببینی؛ اگر کلید نبود، فقط کمی کار بیهوده شده.
  • قاعدهٔ تجربی: حدود ۱۰ بیت به‌ازای هر کلید ← احتمال false positive حدود ۱٪؛ هر ۵ بیت اضافه، احتمال را ده‌برابر کم می‌کند.
  • One bit-array per segment; a few hashes per key set the corresponding bits to 1.
  • At lookup: if even one bit is 0, the key is definitely not in that segment → the segment is skipped (no I/O).
  • If all are 1, the key is probably there — but possibly by accident (false positive). Then you must check the sparse index; if the key is absent, only a little work was wasted.
  • Rule of thumb: ~10 bits per key → ~1% false-positive rate; every 5 extra bits cuts the rate tenfold.

دو استراتژی compactionThe two compaction strategies

  • Size-tiered: سگمنت‌های کوچک و تازه با هم و بعد با سگمنت‌های بزرگ‌تر ادغام می‌شوند (مثلاً چهار ۲۵۶MB ← یک ~۸۹۸MB؛ کمتر از ۱۰۲۴ چون حذف و overwrite و TTL جمع می‌شوند). بازده نوشتن بالا، ولی فضای موقت زیاد و خواندن ممکن است چند سگمنت بزرگ را بگردد.
  • Leveled: سگمنت‌ها اندازهٔ ثابت دارند و در «سطح‌های» L0، L1، ... با محدودیت اندازهٔ فزاینده گروه‌بندی می‌شوند؛ وقتی سطحی پر شد، به سطح بعدی ادغام می‌شود. فضای کمتر، ادغام تدریجی‌تر، و خواندن سریع‌تر (سگمنت‌های کمتری برای چک کردن).
  • قاعده: نوشتنِ غالب ← size-tiered؛ خواندنِ غالب ← leveled. خوشبختانه اکثر پیاده‌سازی‌ها هر دو را دارند.
  • Size-tiered: small fresh segments merge together, then with bigger ones (e.g. four 256MB → one ~898MB; less than 1024 because deletes, overwrites and TTLs collapse). High write throughput, but lots of temporary space, and reads may scan several large segments.
  • Leveled: fixed-size segments grouped into “levels” L0, L1, … with growing size caps; when a level fills it merges into the next. Less space, more gradual merges, faster reads (fewer segments to check).
  • Rule: write-heavy → size-tiered; read-heavy → leveled. Luckily most implementations offer both.
دامنهٔ تصور غلطCommon misconception

Bloom filter می‌تواند با قطعیت بگوید یک کلید در فایل نیست، اما پاسخ مثبتش نیاز به بررسی دارد. Compaction فایل‌ها را ادغام می‌کند؛ نوع ادغام بر هزینهٔ خواندن، نوشتن و فضا اثر دارد.

مثال فنی: دو سیاست ادغام

وضعیت اولیه: چهار فایل 100 MB داریم که بعضی کلیدهایشان هم‌پوشانی دارد.

اجرای مرحله‌به‌مرحله: size-tiered می‌تواند فایل‌های هم‌اندازه را ادغام کند؛ leveled فایل‌ها را در سطح‌ها و بازه‌های منظم‌تر نگه می‌دارد.

نتیجه و دلیل: اولی معمولاً فایل و فضای موقت بیشتری می‌خواهد؛ دومی ممکن است بازنویسی بیشتری بسازد. نتیجه به بار و تنظیمات وابسته است.

“LSM always means less space” — no! In size-tiered, until the next big merge both the old and the new version exist; definitive deletion (e.g. for data-protection law) can also take a while because the tombstone must propagate through every level. That same immutability does make cheap snapshots easy, though.

موتورهای جاسازی‌شده (Embedded)Embedded engines

موتور جاسازی‌شده داخل فرایندِ خودِ برنامه اجرا می‌شود. برخلاف یک سرور پایگاه دادهٔ جدا، برنامه مستقیماً از کتابخانهٔ موتور برای ذخیره و خواندن استفاده می‌کند.

مثال فنی: RocksDB داخل فرایند

وضعیت اولیه: یک worker شمارندهٔ هر customer_id را در RocksDB روی دیسک محلی نگه می‌دارد.

اجرای مرحله‌به‌مرحله: فراخوانی کتابخانه بدون RPC شبکه انجام می‌شود؛ برای بازیابیِ ماشینِ ازدست‌رفته، checkpoint یا changelog بیرونی لازم است.

نتیجه و دلیل: embedded بودن سربار شبکه را کم می‌کند؛ به‌خودی‌خود replication یا backup سراسری نمی‌دهد.

  • همهٔ پایگاه داده‌ها سرویس شبکه‌ای نیستند: موتورهای embedded کتابخانه‌اند و در همان process برنامه اجرا می‌شوند (RocksDB، SQLite، LMDB، DuckDB، KùzuDB). رایج در اپ‌های موبایل؛ در بک‌اند وقتی داده روی یک ماشین جا شود و تراکنش‌های هم‌زمان زیاد نباشند (مثلاً یک نمونه به‌ازای هر tenant کوچک و مستقل).
  • Not every database is a network service: embedded engines are libraries running in the application’s own process (RocksDB, SQLite, LMDB, DuckDB, KüzuDB). Common in mobile apps; on the backend when the data fits on one machine and concurrent transactions are few (e.g. one instance per small independent tenant).

۳3B-TreeB-trees

B-tree کلیدها را در ساختاری مرتب و چندسطحی نگه می‌دارد. هر سطح بازهٔ جست‌وجو را کوچک‌تر می‌کند تا بدون پیمایش همهٔ رکوردها به کلید برسیم.

مثال فنی: پیمایشِ B-tree

وضعیت اولیه: ریشه کلید جداکنندهٔ 500 دارد و برگ راست شامل 700,740,780 است.

اجرای مرحله‌به‌مرحله: جست‌وجوی 740 با مقایسه در ریشه به فرزند راست و سپس برگ مربوط می‌رود.

نتیجه و دلیل: هر سطح بازه را محدود می‌کند؛ لازم نیست برای یک کلید همهٔ صفحه‌ها خوانده شوند.

The log-structured approach is popular, but not the only way. The most common structure for reading/writing records by key is the B-tree: introduced in 1970 and called “ubiquitous” less than a decade later. It is still the standard index implementation in almost every relational database.

ساختار: صفحه‌ها، نه سگمنت‌هاAnatomy: pages, not segments

صفحه واحدی از داده است که موتور از دیسک می‌خواند یا می‌نویسد. در B-tree، هر صفحه چند کلید و اشاره‌گر دارد؛ بنابراین یک خواندن می‌تواند اطلاعات زیادی برای انتخاب مسیر بدهد.

مثال فنی: صفحهٔ ثابت، تغییر کوچک

وضعیت اولیه: موتورِ فرضی صفحه‌های 8 KB دارد و رکورد هدف در یک صفحه است.

اجرای مرحله‌به‌مرحله: تغییر چند بایت ممکن است ثبت WAL و dirty شدن همان صفحه را لازم کند؛ flush با واحدهای صفحه انجام می‌شود.

نتیجه و دلیل: اندازهٔ تغییر برنامه لزوماً برابر بایت نوشته‌شده روی دیسک نیست؛ cache و WAL در مسیر دخالت دارند.

  • برخلاف سگمنت‌های با اندازهٔ متغیر و تغییرناپذیرِ LSM، B-tree پایگاه داده را به صفحات با اندازهٔ ثابت (page) می‌شکند که می‌توانند درجا بازنویسی شوند. اندازهٔ سنتی ۴ کیلوبایت؛ PostgreSQL حالا ۸ کیلوبایت و MySQL به‌طور پیش‌فرض ۱۶ کیلوبایت.
  • هر صفحه شماره دارد و صفحه‌ها با شماره به هم ارجاع می‌دهند (مثل pointer، ولی روی دیسک). اگر همه در یک فایل باشند، شماره × اندازهٔ صفحه = offset بایتی.
  • یک صفحه root است و هر جست‌وجو از آن شروع می‌شود. هر صفحهٔ داخلی چند کلید و ارجاع به فرزند دارد و هر فرزند مسئول یک بازهٔ پیوسته از کلیدهاست؛ کلیدهای بین ارجاع‌ها مرز بازه‌ها را مشخص می‌کنند. (گاهی B+ tree می‌نامندش.)
  • تعداد ارجاع‌های هر صفحه = branching factor؛ در عمل چند صدتایی است. به‌همین‌دلیل عمق درخت همیشه ۳ تا ۴ سطح می‌ماند (درخت ۴ سطحی با صفحات ۴KB و عامل انشعاب ۵۰۰ تا ۲۵۰ ترابایت را نگه می‌دارد).
  • برگ‌ها (leaf page) کلیدهای تکی را دارند: یا مقدار را inline نگه می‌دارند یا ارجاع به جایی که مقدار هست.
  • Unlike LSM’s variable-size immutable segments, a B-tree breaks the database into fixed-size pages that can be rewritten in place. The traditional size is 4 KB; PostgreSQL now uses 8 KB and MySQL 16 KB by default.
  • Each page has a number and pages reference each other by number (like pointers, but on disk). If all live in one file, number × page size = byte offset.
  • One page is the root and every lookup starts there. Each internal page holds several keys and child references, each child owning one contiguous key range; the keys between references mark the range boundaries. (Sometimes called a B+ tree.)
  • Each page’s child-reference count is the branching factor; in practice it is in the hundreds. That is why tree depth stays at 3–4 levels (a 4-level tree with 4KB pages and branching factor 500 holds up to 250 TB).
  • Leaf pages hold individual keys: either the value inline or a reference to where the value lives.

رشد: تقسیم صفحه (page split)Growth: page splits

وقتی صفحهٔ B-tree دیگر برای کلید تازه جا نداشته باشد، تقسیم می‌شود. کلیدهای آن میان دو صفحه پخش می‌شوند و راهنمای سطح بالاتر هم به‌روز می‌شود.

مثال فنی: تقسیم برگِ پُر

وضعیت اولیه: برگ ظرفیت سه کلید دارد و شامل 10,20,30 است؛ کلید 25 می‌رسد.

اجرای مرحله‌به‌مرحله: موتور برگ را به دو بخش تقسیم و جداکنندهٔ مناسب را در والد ثبت می‌کند.

نتیجه و دلیل: اگر والد هم پر باشد تقسیم بالا می‌رود؛ خواننده باید پس از پایان، همهٔ کلیدها را از مسیر درست پیدا کند.

  • به‌روزرسانیِ کلید موجود = یافتن برگ و بازنویسی همان صفحه. درج کلید تازه = افزودن به صفحهٔ بازه‌اش؛ اگر جا نبود، صفحه به دو صفحهٔ نیمه‌پر split می‌شود و والد با کلید مرزیِ تازه به‌روز می‌شود.
  • مثال کتاب: درجِ 334 در صفحهٔ ۳۳۳–۳۴۵ (پر) ← دو صفحهٔ ۳۳۳–۳۳۷ و ۳۳۷–۳۴۵ با مرزِ 337؛ اگر والد هم پر بود، split بالا می‌رود تا ریشه، و split ریشه یعنی ریشهٔ تازه بالای آن. حذف پیچیده‌تر است (ممکن است صفحه‌ها merge شوند).
  • نتیجه: درخت همیشه متوازن می‌ماند و عمق O(log n) است.
  • Updating an existing key = find the leaf and rewrite that page. Inserting a fresh key = add it to its range’s page; if there is no room, the page splits into two half-full pages and the parent is updated with the fresh boundary key.
  • The book’s example: inserting 334 into full page 333–345 → two pages 333–337 and 337–345 with boundary 337; if the parent is full too, the split climbs toward the root, and a root split means a fresh root above it. Deletion is trickier (pages may merge).
  • Result: the tree stays balanced forever with O(log n) depth.

۳.۱ نمودار ۲ + روایت مدرس3.1 Diagram 2 — B-tree, page splits and WAL

نمودار ۲ · Architecture — درخت B، تقسیم صفحه و WALDiagram 2 · Architecture — the B-tree, page splits and WAL باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۲Teacher narration

مثال فنی برای خواندنِ نمودار: پیمایشِ B-tree

وضعیت اولیه: ریشه کلید جداکنندهٔ 500 دارد و برگ راست شامل 700,740,780 است.

اجرای مرحله‌به‌مرحله: جست‌وجوی 740 با مقایسه در ریشه به فرزند راست و سپس برگ مربوط می‌رود.

نتیجه و دلیل: هر سطح بازه را محدود می‌کند؛ لازم نیست برای یک کلید همهٔ صفحه‌ها خوانده شوند.

شکل را از چپ شروع کنید: جعبهٔ «Application» با زیرنویسِ «lookup key 251». فلش پررنگِ «lookup key 251» آن را به جعبهٔ «Root Page» می‌رساند که زیرنویسش مرزهای درخت است: «boundaries 200 | 300» و برچسبِ کنارش «start here».

Start left at Application (lookup key 251). The lookup key 251 arrow reaches the Root Page (boundaries 200 | 300, start here), then between 200 and 300 down to the upper internal page and between 250 and 270 into the leaf holding key 251.

از ریشه دو فلش بیرون می‌آید: «between 200 and 300» به «Internal Page» بالایی (بازهٔ ۲۰۰–۳۰۰) و «between 300 and 400» به «Internal Page» پایینی (بازهٔ ۳۰۰–۴۰۰). چون کلید ما ۲۵۱ است، مسیرِ بالایی درست است. از صفحهٔ داخلیِ بالایی هم دو فلش دیگر: «between 250 and 270» به «Leaf Page»‌ای با زیرنویسِ «keys 250–270» و برچسبِ «holds key 251» — مقصد نهاییِ جست‌وجو — و «between 271 and 300» به برگِ همسایه. یک فلش چین‌چینِ افقی با برچسبِ «sibling pointer» این دو برگ را به هم دوخته تا اسکنِ مرتب بدون بازگشت به والد ممکن باشد. از برگ اول هم فلش چین‌چینِ «row pointer» به جعبهٔ «Heap File» (زیرنویس: «rows in no order») می‌رود — یعنی خودِ رکورد این‌جا نیست، فقط ارجاعش.

A dashed sibling pointer stitches the two leaves together for ordered scans without climbing back to the parent, and a row pointer leads from the leaf to the Heap File (rows in no order) — the record itself lives there, not in the index.

نیمهٔ پایینِ شکل داستانِ رشد و ایمنی را می‌گوید. «Leaf Page» سوم با زیرنویسِ «keys 333–345» و برچسبِ «split on overflow» نشان می‌دهد که درجِ ۳۳۴ چطور درخت را می‌رویاند (split با مرز ۳۳۷ و به‌روز شدن والد). و در پایین، جعبهٔ «Write-Ahead Log» با زیرنویسِ «append-only, fsync before apply»: فلشِ پررنگِ «log page change first» از Application به آن می‌آید و فلش چین‌چینِ «replay after crash» از آن به ریشه می‌رود. کل درخت داخل قابِ «B-Tree Index — fixed-size pages (8–16 KiB), depth O(log n)» جمع شده است.

The lower half tells growth and safety: a third leaf (keys 333–345, split on overflow) shows how inserting 334 grows the tree, while the Write-Ahead Log (append-only, fsync before apply) takes log page change first and offers replay after crash back to the root.

درسِ شکل: مسیر خواندن «یک صفحه در هر سطح» است و کوتاه؛ اما هر نوشتن ممکن است چند صفحه را بازنویسی کند (split!) و چون بازنویسی درجا خطرناک است (crash وسط کار = درخت خراب)، اول هر تغییر باید در WAL ثبت و fsync شود.

The lesson: the read path is “one page per level” and short; but each write may rewrite several pages (splits!), and since in-place rewrites are dangerous (mid-work crash = broken tree), every change must hit the WAL with fsync first.

قابلیت اطمینان و واریانت‌هاReliability and variants

برای بازیابی بعد از خرابی، موتور باید بتواند تغییراتِ صفحات را درست بازسازی کند. WAL تغییر را پیش از اعمالِ پایدارِ آن روی صفحات ثبت می‌کند؛ قفل‌های داخلی نیز تغییر هم‌زمانِ ساختار را کنترل می‌کنند.

مثال فنی: لاگ پیش از صفحهٔ داده

وضعیت اولیه: تغییر P7=120 ابتدا در WAL ثبت و طبق تنظیم ماندگاری پایدار می‌شود؛ صفحه هنوز روی دیسک قدیمی است.

اجرای مرحله‌به‌مرحله: برق قطع می‌شود؛ هنگام restart موتور با لاگ، تغییرهای لازم را redo می‌کند.

نتیجه و دلیل: WAL امکان بازیابیِ تغییر تأییدشده را می‌دهد؛ تنظیمی که تأیید را پیش از پایداری می‌دهد تضمین ضعیف‌تری دارد.

  • عملیات پایهٔ B-tree بازنویسی یک صفحه درجا است (برخلاف LSM که فقط append می‌کند). بازنویسی چند صفحه (مثل split) خطرناک است: crash میانهٔ کار می‌تواند صفحهٔ یتیم یا صفحهٔ نیمه‌نوشته (torn page) بسازد.
  • راه‌حل استاندارد: write-ahead log (WAL) — فایلی فقط‌افزودنی که هر تغییر B-tree باید قبل از اعمال روی صفحات، در آن نوشته شود؛ بعد از crash، لاگ برای بازگرداندن درخت به حالت سازگار بازپخش می‌شود. معادلش در فایل‌سیستم‌ها journaling نام دارد.
  • برای کارایی، صفحات در حافظه بافر می‌شوند؛ WAL تضمین می‌کند تا وقتی داده با fsync روی دیسک رفته، بعد از crash قابل بازیابی است.
  • واریانت‌ها: copy-on-write (مثل LMDB — صفحهٔ تغییرکرده در جای تازه نوشته می‌شود و والد جدید ساخته می‌شود؛ برای concurrency control هم مفید است)، کلیدهای مختصر در صفحات داخلی (عامل انشعاب بیشتر، عمق کمتر)، چیدمان ترتیبی برگ‌ها روی دیسک برای اسکن سریع، و pointer خواهر/برادر بین برگ‌ها.
  • A B-tree’s basic operation is rewriting one page in place (unlike LSM, which only appends). Multi-page rewrites (like splits) are dangerous: a mid-work crash can create orphaned or half-written (torn) pages.
  • The standard fix: a write-ahead log (WAL) — an append-only file where every B-tree change must be recorded before touching the pages; after a crash the log is replayed to restore a consistent tree. The filesystem equivalent is called journaling.
  • For performance, pages are buffered in memory; the WAL guarantees that once data reached disk with fsync, it is recoverable after a crash.
  • Variants: copy-on-write (e.g. LMDB — a changed page is written to a fresh location with new parents; also handy for concurrency control), abbreviated keys in internal pages (higher branching factor, shallower depth), sequential leaf layout on disk for fast scans, and sibling pointers between leaves.

۳.۲ چرخهٔ قابلیتِ اطمینانِ B-tree + نمودار ۶3.2 The B-tree reliability cycle + Diagram 6

ثبتِ تغییر، پایدار کردن لاگ و نوشتن صفحه‌ها لزوماً هم‌زمان نیستند. مهم است تأییدِ موفقیت، با تضمین ماندگاریِ انتخاب‌شده سازگار باشد.

مثال فنی: Crash وسطِ split

وضعیت اولیه: دو صفحهٔ تازه ساخته شده‌اند، اما به‌روزرسانی والد هنوز روی دیسک نرفته است.

اجرای مرحله‌به‌مرحله: رکوردهای بازیابیِ موتور مشخص می‌کنند split چگونه تکمیل یا اصلاح شود؛ صرفِ نوشتن جداگانهٔ چند صفحه کافی نیست.

نتیجه و دلیل: ساختار درخت هم باید پس از crash معتبر بماند؛ ماندگاری فقط حفظ مقدار یک رکورد نیست.

In-place page rewrites are the B-tree’s strength and its biggest danger at once: if power dies mid-write of a page, the disk holds neither a healthy old version nor a complete new one — a torn page. The fix is the same strict rule as before: before touching the page itself, write the change record to the write-ahead log. The log is append-only and log writes are sequential; so we fill the log first, then calmly rewrite the page with random writes.

اما «در لاگ نوشتن» به‌تنهایی کافی نیست: تا وقتی fsync برنگشته، رکورد فقط در کشِ سیستم‌عامل است و یک crash ساده آن را می‌بلعد. مرزِ واقعیِ ماندگاری (durability) لحظه‌ای است که fsync با موفقیت برمی‌گردد؛ بعد از آن حتی اگر صفحه هرگز روی دیسک نوشته نشود، ریکاوری می‌تواند با redo — بازپخشِ رو به جلوِ لاگ — همه‌چیز را برگرداند، البته به شرطی که این بازپخش idempotent باشد، چون ممکن است دوبار اجرا شود.

But “writing to the log” alone is not enough: until fsync returns, the record sits only in the OS cache and a plain crash swallows it. The real durability boundary is the moment fsync returns successfully; after that, even if the page never reaches disk, recovery can bring everything back with redo — forward replay of the log — provided the replay is idempotent, since it may run twice.

نمودار ۶ این چرخه را به‌صورت یک ماشینِ وضعیت نشان می‌دهد: مسیرِ عادیِ نوشتن از یک صفحهٔ dirty در حافظه شروع می‌شود، از لاگ و fsync می‌گذرد، و به checkpoint می‌رسد که در آن لاگِ قدیمی دور ریخته می‌شود و restart بعدی سریع می‌ماند. مسیرهای شکست هم در همین شکل‌اند: نوشتنِ همگام‌نشده، صفحهٔ نیمه‌نوشته و نوشتنِ گم‌شده.

Diagram 6 shows this cycle as a state machine: the normal write path starts from a dirty in-memory page, passes the log and fsync, and reaches a checkpoint where the old log is discarded and restarts stay fast. The failure paths — unsynced writes, half-written pages, lost writes — are in the same figure.

نمودار ۶ · Lifecycle — ماشینِ وضعیتِ قابلیتِ اطمینانِ B-treeDiagram 6 · Lifecycle — the B-tree reliability state machine باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۶Teacher narration

مثال فنی برای خواندنِ نمودار: Crash وسطِ split

وضعیت اولیه: دو صفحهٔ تازه ساخته شده‌اند، اما به‌روزرسانی والد هنوز روی دیسک نرفته است.

اجرای مرحله‌به‌مرحله: رکوردهای بازیابیِ موتور مشخص می‌کنند split چگونه تکمیل یا اصلاح شود؛ صرفِ نوشتن جداگانهٔ چند صفحه کافی نیست.

نتیجه و دلیل: ساختار درخت هم باید پس از crash معتبر بماند؛ ماندگاری فقط حفظ مقدار یک رکورد نیست.

این نمودار چهار خط (lane) افقی دارد که از بالا به پایین عنوان‌شان این است: «Write path»، «Buffers and caches»، «Crash recovery» و «Terminal». پنج حالتِ خطِ اول شمارهٔ گام دارند — «01» تا «05» — و مسیرِ عادیِ نوشتن را می‌سازند؛ سه حالتِ دیگر مسیرهای شکست و بازیابی‌اند.

This state machine has four lanes — Write path, Buffers and caches, Crash recovery, Terminal — with five numbered states (01–05) forming the normal write path, plus three failure/recovery states.

مسیرِ اصلی را قدم‌به‌قدم بروید. از «Dirty Page» شروع کنید: زیرنویسش «modified in memory» و برچسبِ کنارش «not on disk yet» — یعنی صفحه در حافظه تغییر کرده ولی هنوز چیزی روی دیسک نیست. فلشِ «append to log» آن را به «WAL Appended» می‌رساند (زیرنویس: «log entry written first»، برچسب: «sequential write»). فلشِ بعدی «fsync» است و به «WAL Synced» می‌رسد — حالتِ لوزی‌شکلِ مسیر با زیرنویسِ «fsync returns» و برچسبِ «now durable» — و این همان مرزِ ماندگاری است. از آنجا فلشِ «later, in background» به «Page Written» می‌رود (زیرنویس: «update in place on disk»، برچسب: «random write») و سرانجام فلشِ «periodically» به «Checkpoint» می‌رسد (زیرنویس: «old log discarded»، برچسب: «restart is fast»).

Walk the main path: Dirty Page (modified in memory, not on disk yet) → append to log → WAL Appended (sequential write) → fsync → the diamond WAL Synced (now durable) — the durability boundary — then later, in background to Page Written (random write) and periodically to Checkpoint (old log discarded, restart is fast).

حالا فلش‌های چین‌چینِ شکست. از «WAL Appended» فلشِ چین‌چینِ «crash before fsync» پایین می‌آید، به خطِ «Buffers and caches» و حالتِ «Unsynced Write» با زیرنویسِ «still in the OS cache» و برچسبِ «power loss loses it». از آنجا فلشِ «nothing to recover» به خطِ «Terminal» و حالتِ «Write Lost» می‌رود (زیرنویس: «never reached the disk»، برچسب: «rolled back»). این تنها مسیرِ بدونِ بازگشتِ شکل است: تا قبل از fsync، یعنی در دنیای کش‌ها، crash به معنای ازدست‌دادنِ همان نوشتن است.

Failure arrows: crash before fsync drops from WAL Appended to Unsynced Write (still in the OS cache, power loss loses it), then nothing to recover to the terminal Write Lost (rolled back) — the figure’s only one-way path: before fsync, a crash means losing that write.

سمتِ دیگرِ مرز، بازیابی است: از «WAL Synced» فلشِ چین‌چینِ «crash after fsync» پایین می‌آید به خطِ «Crash recovery» و حالتِ «Redo from WAL» با زیرنویسِ «roll forward on startup» و برچسبِ «idempotent replay»؛ بعد فلشِ پررنگِ «recovery completes» آن را به همان «Checkpoint» برمی‌گرداند. دقت کنید: مسیرِ بازیابی دقیقاً در همان حالتی تمام می‌شود که مسیرِ عادی — یعنی هرچه با fsync ماندگار شده باشد، بعد از crash هم به مقصد می‌رسد.

Past the boundary lies recovery: crash after fsync falls to Redo from WAL (roll forward on startup, idempotent replay), and recovery completes returns to the same Checkpoint the normal path reaches — whatever fsync made durable gets home after a crash.

آخرین مسیرِ شکست از «Page Written» شروع می‌شود: فلشِ «crash mid-write» به حالتِ «Torn Write» در خطِ «Terminal» می‌رود — زیرنویسِ «checksum mismatch» و برچسبِ «partial page on disk» — یعنی صفحه‌ای نیمه‌قدیمی و نیمه‌جدید که فقط checksum روی صفحه و بلاگِ لاگ می‌تواند آشکارش کند. اما این حالتِ پایانی نیست: فلشِ پررنگِ «rewrite from log» آن را به «Redo from WAL» برمی‌گرداند و صفحه از رویِ لاگِ ماندگار دوباره نوشته می‌شود.

The last failure starts at Page Written: crash mid-write leads to Torn Write (checksum mismatch, partial page on disk) — a half-old, half-new page only page and log checksums can expose. But it is not terminal: rewrite from log returns to Redo from WAL and the page is rewritten from the durable log.

درسِ این ماشینِ وضعیت را در یک قانون جمع کنید: «اول لاگ، بعد صفحه»؛ و مرزِ ماندگاری فقط لحظه‌ای است که fsync با موفقیت برمی‌گردد. نوشتنِ لاگ sequential و ارزان است، بازنویسیِ صفحه random و خطرناک؛ پس هرگز دومی را بدونِ اولی شروع نکنید.

Sum the state machine into one rule: “log first, page second”; durability begins only when fsync returns. Log writes are sequential and cheap, page rewrites random and dangerous — never start the second without the first.

و چند نکته که باید بماند: fsync همیشه راست نمی‌گوید — بعضی سیستم‌عامل‌ها پیش از ماندگاریِ واقعی موفقیت گزارش کرده‌اند و برخی SSDها در تست‌های قطعِ برق دروغ گفته‌اند — پس checksum روی هر صفحه و هر بلاگِ لاگ، فسادِ خاموش را به خطایِ قابل‌تشخیص تبدیل می‌کند. بازپخشِ لاگ باید idempotent باشد چون ممکن است دوبار اجرا شود، و checkpoint هم حجمِ بازپخشِ ری‌استارت را محدود می‌کند و هم اجازه می‌دهد سگمنت‌های قدیمیِ لاگ دور ریخته شوند. و یادتان باشد WAL مخصوصِ B-tree نیست: موتورهای LSM برای بازیابیِ crash معمولاً به آن نیاز ندارند (چون memtable به‌صورت فایلِ مرتب flush می‌شود) و جایگزینِ کلاسیکِ آن، shadow paging با تعویضِ اتمیِ ریشه است.

And a few things to keep: fsync doesn’t always tell the truth — some OSes reported success before real durability and some SSDs lied in power-cut tests — so checksums on every page and log block turn silent corruption into detectable errors. Log replay must be idempotent (it may run twice), checkpoints bound restart replay and let old log segments go, and the WAL isn’t B-tree-only: LSM engines usually don’t need it for crash recovery (the memtable flushes as a sorted file), and its classic alternative is shadow paging with an atomic root swap.

۴4مقایسهٔ B-Tree و LSMComparing B-trees and LSM-trees

قاعدهٔ تجربیRule of thumb

B-tree و LSM هر دو برای خواندن و نوشتن طراحی شده‌اند، اما هزینه‌ها را متفاوت توزیع می‌کنند. انتخاب به الگوی واقعیِ بار و رفتار در زمان ادغام و افزایش داده وابسته است.

مثال فنی: بارِ نوشتن و خواندن

وضعیت اولیه: بار A عمدتاً ورود رویداد است؛ بار B عمدتاً lookup و تغییر رکوردهای کوچک.

اجرای مرحله‌به‌مرحله: LSM نوشتن‌ها را جمع می‌کند و هزینهٔ compaction می‌دهد؛ B-tree صفحه‌ها را به‌روز می‌کند و ساختار خواندنِ مرتب دارد.

نتیجه و دلیل: انتخاب با اندازه‌گیری write/read/space amplification و tail latency روی همان بار انجام می‌شود؛ برندهٔ ثابت نداریم.

LSM fits write-heavy workloads better, B-trees read faster. But benchmarks are extremely sensitive to workload details; for a serious claim you must test with your own workload. And it isn’t either/or — some engines are hybrids (e.g. several B-trees merged LSM-style).

محورB-Tree (بازنویسی درجا)LSM (log-structured)
خواندن نقطه‌اییک صفحه در هر سطح؛ سریع و قابل‌پیش‌بینیممکن است چند سگمنت را بگردد؛ Bloom filter تعداد I/O را کم می‌کند
range queryساده و سریع با ساختار مرتب درختممکن است ولی باید همهٔ سگمنت‌ها را موازی بخوانی و ادغام کنی؛ Bloom filter کمکی نمی‌کند و نسبت به نقطه‌ای گران‌تر است
الگوی نوشتننوشتن‌های کوچک و پراکنده (random writes) به صفحاتِ هرجای دیسکنوشتن‌های بزرگ و ترتیبی (sequential writes) — کل سگمنت در یک نوبت
بازده نوشتنکمتر؛ به‌خصوص روی HDDبیشتر؛ تفاوت روی HDD بزرگ و روی SSD کمتر ولی محسوس
write amplificationهر داده حداقل ۲ بار: WAL + صفحه؛ گاهی کل صفحه برای چند بایتWAL + flush + هر بار compaction؛ معمولاً کمتر (صفحهٔ ثابت ندارد و بلوک‌ها فشرده می‌شوند)
latency نوشتننسبتاً یکنواختممکن است با پر شدن memtable یا عقب افتادن compaction جهش کند؛ موتورهایی مثل RocksDB backpressure می‌گذارند
فضای دیسکfragmentation بعد از حذف انبوه (نیاز به vacuum مثل PostgreSQL)فشرده‌سازی بهتر؛ ولی نسخه‌های قدیمی تا compaction فضا می‌گیرند و size-tiered موقت فضای زیادی می‌خواهد
حذف قطعی دادهمستقیم‌ترtombstone باید در همهٔ سطح‌ها propagate شود؛ ممکن است طول بکشد
snapshotسخت‌تر (صفحات بازنویسی می‌شوند)آسان: فایل‌های تغییرناپذیرِ آن لحظه = snapshot، بدون کپی
عمر SSDwrite amplification بیشتر ← سایش بیشترمعمولاً کمتر می‌نویسد ← عمر بیشتر
AspectB-tree (update in place)LSM (log-structured)
Point readsOne page per level; fast and predictableMay scan several segments; Bloom filters cut I/O
Range queriesSimple and fast with the tree’s sorted structurePossible, but must read all segments in parallel and merge; Bloom filters don’t help, costlier than point reads
Write patternSmall scattered writes (random writes) to pages anywhere on diskLarge sequential writes — a whole segment at once
Write throughputLower, especially on HDDHigher; the gap is big on HDD, smaller but real on SSD
Write amplificationEach datum at least twice: WAL + page; sometimes a whole page for a few bytesWAL + flush + each compaction; usually lower (no fixed pages, compressed blocks)
Write latencyFairly uniformCan spike when the memtable fills or compaction lags; engines like RocksDB apply backpressure
Disk spaceFragmentation after mass deletes (needs vacuum like PostgreSQL)Better compression; but old versions linger until compaction and size-tiered temporarily wants lots of space
Definitive deletionMore directThe tombstone must propagate through all levels; can take a while
SnapshotsHarder (pages get rewritten)Easy: the immutable files of that moment = a snapshot, no copy
SSD lifespanMore write amplification → more wearUsually writes less → longer life
چرا باز هم SSD برای نوشتنِ ترتیبی سریع‌تر است؟Why are sequential writes still faster on SSDs?
در HDD علتش مکانیکی است (حرکت هد چند میلی‌ثانیه طول می‌کشد). در SSD اما: فلش را می‌توان صفحه‌به‌صفحه (معمولاً ۴KB) خواند/نوشت ولی فقط بلوک‌به‌بلوک (معمولاً ۵۱۲KB) پاک کرد. قبل از پاک‌سازی باید صفحاتِ معتبر جابه‌جا شوند (garbage collection). نوشتنِ ترتیبی بلوک‌ها را یکدست نگه می‌دارد (بعداً کل بلوک یک‌جا پاک می‌شود)؛ نوشتنِ تصادفی بلوک‌ها را آلوده می‌کند، GC بیشتر می‌شود و هم پهنای باند می‌خورد و هم SSD را بیشتر می‌ساید.

On HDD the reason is mechanical (head movement takes milliseconds). On SSD: flash can be read/written page by page (usually 4KB) but only erased block by block (usually 512KB). Before erasing, valid pages must be relocated (garbage collection). Sequential writes keep blocks uniform (later the whole block is erased at once); random writes pollute blocks, GC grows, and it costs both bandwidth and extra flash wear.

نکتهٔ بنچ‌مارکBenchmarking note
نوشتن روی یک LSMِ خالی گول‌زننده است: هنوز compactionی در کار نیست و همهٔ پهنای باندِ دیسک مالِ نوشتن است. آزمایش را آن‌قدر طولانی کن که compaction واقعاً وارد بازی شود و write amplification خودش را نشان دهد.

Writing to an empty LSM is deceptive: no compaction is running yet and all disk bandwidth belongs to writes. Run the test long enough for compaction to really join the game and for write amplification to show itself.

۴.۱ ایندکس‌های ثانویه4.1 Secondary indexes

  • تا این‌جا فقط ایندکسِ کلید-مقدار (مثل کلید اصلی) داشتیم. ایندکس ثانویه (با CREATE INDEX) اجازه می‌دهد روی ستون‌های دیگر هم جست‌وجو کنی — مثلاً همهٔ ردیف‌های یک user_id.
  • تفاوت: مقدارهای ایندکس‌شده الزاماً یکتا نیستند. دو راه‌حل: هر مقدار = فهرستی از شناسه‌های رکورد (مثل postings list) یا یکتا کردنِ هر ورودی با چسباندن شناسهٔ ردیف. هم B-tree و هم LSM می‌توانند ایندکس ثانویه بسازند.
  • داده کجا باشد؟ سه الگو:
  • So far we only had key-value indexes (like the primary key). A secondary index (via CREATE INDEX) lets you search other columns too — e.g. all rows of one user_id.
  • The difference: indexed values aren’t necessarily unique. Two fixes: each value = a list of record IDs (like a postings list), or make each entry unique by appending the row ID. Both B-trees and LSM can build secondary indexes.
  • Where should the data live? Three patterns:
الگوتوضیحمثالتریدآف
Clustered indexخودِ رکورد داخل ساختار ایندکس ذخیره می‌شودکلید اصلی در InnoDB؛ یک clustered index در هر جدولِ SQL Serverخواندن سریع؛ جابه‌جایی رکورد گران
Heap fileایندکس فقط ارجاع می‌دهد؛ رکوردها در فایلی بی‌نظمPostgreSQLبه‌روزرسانی درجا (اگر مقدار جدید بزرگ‌تر نباشد)؛ به‌روزرسانیِ بزرگ‌تر = جابه‌جایی + به‌روز کردن همهٔ ایندکس‌ها یا forwarding pointer
Covering indexستون‌هایی از رکورد هم داخل ایندکسindex with included columnsپاسخ بدون مراجعه به heap؛ فضا و نوشتن بیشتر
PatternWhat it meansExampleTrade-off
Clustered indexThe record itself is stored inside the index structurePrimary key in InnoDB; one clustered index per SQL Server tableFast reads; moving a record is expensive
Heap fileThe index only points; records sit in an unordered filePostgreSQLIn-place updates (if the new value isn’t bigger); a bigger update = relocation + updating all indexes or a forwarding pointer
Covering indexRecord columns also stored inside the indexIndex with included columnsAnswers without touching the heap; more space and writes

ایندکس ثانویه داده را با کلیدی غیر از شناسهٔ اصلی پیدا می‌کند. بسته به طراحی موتور، نتیجه می‌تواند خود رکورد، نشانی آن یا کلید اصلی برای یک جست‌وجوی دیگر باشد.

مثال فنی: ایندکس پوشاننده

وضعیت اولیه: query فقط amount سفارش‌های مشتری ۱۷ را می‌خواهد.

اجرای مرحله‌به‌مرحله: ایندکس با کلید customer_id و دادهٔ مبلغ می‌تواند پاسخ را از خود ایندکس بدهد؛ ایندکسِ صرفاً کلید، مراجعه به رکورد اصلی هم می‌خواهد.

نتیجه و دلیل: پوشاندن query خواندن را کم می‌کند، اما مبلغ در ایندکس هم جا می‌گیرد و با تغییر آن باید به‌روز شود.

نمودار مفهومی — ایندکس ثانویه: رکورد کجا زندگی می‌کند؟Concept diagram — secondary indexes: where does the row live?

نمودار مفهومی · خوشه‌ای، heap و پوششیConcept diagram · Clustered, heap and covering باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: ایندکس پوشاننده

وضعیت اولیه: query فقط amount سفارش‌های مشتری ۱۷ را می‌خواهد.

اجرای مرحله‌به‌مرحله: ایندکس با کلید customer_id و دادهٔ مبلغ می‌تواند پاسخ را از خود ایندکس بدهد؛ ایندکسِ صرفاً کلید، مراجعه به رکورد اصلی هم می‌خواهد.

نتیجه و دلیل: پوشاندن query خواندن را کم می‌کند، اما مبلغ در ایندکس هم جا می‌گیرد و با تغییر آن باید به‌روز شود.

سه ستون را با یک پرس‌وجو مقایسه کنید: «ردیف‌های user شماره ۴۲». در «ایندکس خوشه‌ای» خودِ ردیف داخل برگ است (مثل کلید اصلی InnoDB): خواندن با یک جست‌وجو، ولی جابه‌جایی گران. در «heap + ایندکس» ایندکس فقط اشاره‌گر دارد و ردیف‌ها بی‌نظم در heap‌اند (مثل PostgreSQL): به‌روزرسانی ممکن است رکورد را جابه‌جا کند. در «ایندکس پوششی» ستون‌ها داخل ایندکس کپی‌اند و پرس‌وجو بی‌نیاز از heap جواب می‌گیرد — به بهای فضا و نوشتن بیشتر. نوار پایین جمع‌بندی است: چیدمان، سرعت خواندن را در برابر هزینهٔ نوشتن تصمیم می‌گیرد.

Compare the three columns with one query: “rows of user 42”. In the “Clustered index” the row itself lives in the leaf (like InnoDB primary keys): one-lookup reads, but costly moves. In “Heap + index” the index holds only pointers while rows sit unordered in the heap (like PostgreSQL): an update may relocate the record. In the “Covering index” columns are copied inside, answering without touching the heap — for more space and writes. The bottom bar concludes: the layout decides read speed vs write cost.

Multi-column and multidimensional indexes (searching several predicates at once) await in section 8.

۵5نگه‌داشتن همه‌چیز در حافظهKeeping everything in memory

نگهداریِ داده در حافظه خواندن را سریع می‌کند، اما ماندگاری، بازیابی و ظرفیت همچنان مسئله‌اند. حافظهٔ اصلی با ذخیره‌سازیِ پایدار یکسان نیست.

مثال فنی: حافظه و ماندگاری مستقل‌اند

وضعیت اولیه: کل مجموعهٔ فعال 4 GB است و در RAM جا می‌شود.

اجرای مرحله‌به‌مرحله: خواندن از حافظه سریع است، ولی خاموشی RAM را پاک می‌کند؛ WAL پایدار یا replica طبق تضمین موردنیاز باید تغییرها را حفظ کند.

نتیجه و دلیل: in-memory بودن فقط محل اصلی خدمت‌دهی را می‌گوید؛ دربارهٔ دوام داده به‌تنهایی چیزی ثابت نمی‌کند.

  • همهٔ ساختارهای این فصل پاسخی به نارسایی‌های دیسک بودند: دیسک با دو مزیتِ بزرگ تحمل می‌شود — پایداری (با قطع برق از بین نمی‌رود) و هزینهٔ هر گیگابایتِ کمتر از RAM.
  • با ارزان شدن RAM، بسیاری از داده‌ها کوچک‌اند و کامل در حافظه جا می‌شوند → پایگاه‌های داده درون‌حافظه‌ای. بعضی فقط cacheاند (مثل Memcached — از دست رفتن داده با restart قابل قبول است)؛ بعضی دوام می‌خواهند: سخت‌افزار ویژه (RAM با باتری) یا متداول‌تر، نوشتن لاگ تغییرات/اسنپ‌شات دوره‌ای روی دیسک یا تکثیر وضعیت به ماشین‌های دیگر.
  • با وجود نوشتن روی دیسک، باز هم «درون‌حافظه‌ای» حساب می‌شوند چون دیسک فقط لاگِ دوام است و خواندن کلاً از حافظه سرویس می‌شود. مزیت عملیاتی هم دارد: فایل روی دیسک قابل backup و تحلیل با ابزار بیرونی است.
  • نمونه‌ها: VoltDB و SingleStore و Oracle TimesTen (رابطه‌ای، ادعای بهبود بزرگ با حذفِ سربارِ ساختارهای دیسکی)، RAMCloud (key-value با دوام و رویکرد log-structured)، Redis و Couchbase (دوام ضعیف، نوشتن ناهمزمان).
  • نکتهٔ ضدشهود: سرعتِ این پایگاه داده‌ها از «نخواندن از دیسک» نمی‌آید — موتور دیسکی هم با حافظهٔ کافی شاید هرگز به دیسک نزند (cache سیستم‌عامل). علت واقعی، حذفِ سربارِ کدگذاریِ ساختارهای حافظه به قالبِ قابل‌نوشتن روی دیسک است.
  • علاوه بر سرعت، مدل‌های دادهٔ غیرمتعارف را هم آسان می‌کنند: مثلاً Redis رابطِ پایگاه داده‌مانند روی صفِ اولویت‌دار، مجموعه و ... می‌دهد؛ چون همه‌چیز در حافظه است، پیاده‌سازی این ساختارها ساده می‌ماند.
  • Every structure in this chapter answers the shortcomings of disk: disks are tolerated for two big virtues — durability (no power-cut loss) and lower cost per gigabyte than RAM.
  • As RAM got cheaper, much data is small enough to fit entirely in memory → in-memory databases. Some are just caches (e.g. Memcached — losing data on restart is acceptable); some want durability: special hardware (battery-backed RAM) or, more commonly, writing a change log / periodic snapshots to disk or replicating state to other machines.
  • Despite writing to disk they still count as “in-memory” because the disk is only a durability log and reads are served entirely from memory. There is an operational bonus too: the on-disk file can be backed up and analyzed with outside tools.
  • Examples: VoltDB, SingleStore and Oracle TimesTen (relational, claiming big wins by dropping on-disk-structure overhead), RAMCloud (durable key-value with a log-structured approach), Redis and Couchbase (weak durability, async writes).
  • Counter-intuitive point: these databases aren’t fast because they “don’t read from disk” — an on-disk engine with enough memory might never touch disk either (OS cache). The real cause is dropping the overhead of encoding in-memory structures into a disk-writable format.
  • Beyond speed, they also ease unconventional data models: e.g. Redis offers a database-like interface over priority queues, sets and more; with everything in memory these structures stay simple to implement.

۶6ذخیره‌سازی برای تحلیلStorage for analytics

در تحلیل، معمولاً از میان میلیون‌ها ردیف، فقط چند ستون را می‌خوانیم و خلاصه می‌سازیم. بنابراین شیوهٔ چیدن و فشرده‌کردن داده با سامانهٔ ثبت سفارش فرق دارد.

مثال فنی: اسکن چند ستون

وضعیت اولیه: جدول یک میلیارد ردیف و ۵۰ ستون دارد؛ گزارش فقط day, amount را می‌خواهد.

اجرای مرحله‌به‌مرحله: چیدمان ردیفی می‌تواند دادهٔ ستون‌های بی‌نیاز را هم وارد I/O کند؛ چیدمان ستونی ستون‌های لازم را جدا می‌خواند.

نتیجه و دلیل: برای اسکن تحلیلی حجم خواندن کم می‌شود؛ بازسازی یک ردیف کامل ممکن است به چند ستون نیاز داشته باشد.

A data warehouse’s data model is usually relational, because SQL answers analytical queries well: graphical analysis tools generate SQL, visualize results, and let analysts drill down and slice.

  • ظاهر انبارداده و پایگاه داده تراکنشی شبیه است (هر دو SQL دارند) اما درون‌شان بسیار متفاوت است: برای الگوهای کوئریِ کاملاً متفاوت بهینه شده‌اند. بیشتر فروشندگان حالا یا تراکنش را پشتیبانی می‌کنند یا تحلیل را، نه هر دو را.
  • HTAP (hybrid transactional/analytical processing) مثل SQL Server و SAP HANA و SingleStore هر دو را در یک محصول دارد، اما عملاً به دو موتورِ ذخیره‌سازی و کوئریِ جدا با یک رابط SQLِ مشترک تبدیل می‌شوند.
  • A warehouse and a transactional database look alike (both speak SQL) but are very different inside: each is optimized for a completely different query pattern. Most vendors now support either transactions or analytics — not both.
  • HTAP (hybrid transactional/analytical processing) like SQL Server, SAP HANA and SingleStore ships both in one product, but in practice becomes two separate storage and query engines behind one shared SQL interface.

انبارداده‌های ابری و تفکیک لایه‌هاCloud warehouses and layer separation

  • نسل تازه (BigQuery، Redshift، Snowflake) از زیرساخت ابری بهره می‌برند: داده در object storage و محاسبهٔ serverless — یعنی ذخیره‌سازی و محاسبه مستقل از هم مقیاس می‌گیرند و با بقیهٔ سرویس‌های ابری ( ingest لاگ، Dataflow، Kinesis) یکپارچه‌اند.
  • در دنیای متن‌باز هم یک سیستم یکپارچهٔ قدیمی (مثل Hive) به مؤلفه‌های جدا شکسته شده است:
  • The fresh generation (BigQuery, Redshift, Snowflake) exploits cloud infrastructure: data in object storage and serverless compute — storage and compute scale independently and integrate with other cloud services (log ingest, Dataflow, Kinesis).
  • In the open-source world an old monolithic system (like Hive) has likewise split into separate components:
مؤلفهوظیفهنمونه‌ها
Query Engineتجزیهٔ SQL، بهینه‌سازی به طرح اجرا، اجرای موازی و توزیع‌شدهTrino، DataFusion، Presto (یا اجرای وظایف با Spark/Flink)
Storage Formatنحوهٔ تبدیل ردیف‌ها به بایت در فایل (روی object storage یا فایل‌سیستم توزیع‌شده)Parquet، ORC، Lance، Nimble
Table Formatکدام فایل‌ها یک جدول را می‌سازند + اسکیما؛ insert/delete، time travel، GC و حتی تراکنشApache Iceberg، Delta
Data Catalogکدام جدول‌ها در پایگاه داده‌اند؛ ساخت/تغییر نام/حذف جدول — معمولاً سرویسی جدا با RESTPolaris، Unity Catalog، catalog خود Iceberg
ComponentJobExamples
Query engineParse SQL, optimize into an execution plan, run it parallel and distributedTrino, DataFusion, Presto (or run tasks with Spark/Flink)
Storage formatHow rows become bytes in a file (on object storage or a distributed filesystem)Parquet, ORC, Lance, Nimble
Table formatWhich files form a table + schema; inserts/deletes, time travel, GC and even transactionsApache Iceberg, Delta
Data catalogWhich tables are in the database; create/rename/drop tables — usually a separate REST servicePolaris, Unity Catalog, Iceberg’s own catalog

در معماری تحلیلیِ جداشده، داده در مخزن مشترک می‌ماند و پردازنده‌ها برای اجرای گزارش از آن می‌خوانند. لایه‌های کش می‌توانند خواندنِ دوباره را کم کنند.

مثال فنی: compute موقت روی فایل مشترک

وضعیت اولیه: فایل‌های ستونی فروش در object store باقی‌اند و دو تیم query جدا دارند.

اجرای مرحله‌به‌مرحله: هر تیم compute خودش را بالا می‌آورد و فایل مشترک را می‌خواند؛ cache محلی ممکن است خواندن تکراری را کم کند.

نتیجه و دلیل: جدایی storage و compute رقابت CPU را کم می‌کند، اما هزینهٔ اسکن و انتقال داده باقی است.

Splitting the catalog off the query engine also enabled data discovery and governance — systems that only touch the catalog’s metadata.

۶.۱ نمودار ۳ + روایت مدرس6.1 Diagram 3 — the analytical storage stack (data lakehouse)

نمودار ۳ · Architecture — پشتهٔ ذخیره‌سازی تحلیلی (Data Lakehouse)Diagram 3 · Architecture — the analytical storage stack (data lakehouse) باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۳Teacher narration

مثال فنی برای خواندنِ نمودار: compute موقت روی فایل مشترک

وضعیت اولیه: فایل‌های ستونی فروش در object store باقی‌اند و دو تیم query جدا دارند.

اجرای مرحله‌به‌مرحله: هر تیم compute خودش را بالا می‌آورد و فایل مشترک را می‌خواند؛ cache محلی ممکن است خواندن تکراری را کم کند.

نتیجه و دلیل: جدایی storage و compute رقابت CPU را کم می‌کند، اما هزینهٔ اسکن و انتقال داده باقی است.

این شکل یک زنجیرهٔ افقی از چپ به راست است. اول جعبهٔ «Analyst / BI Tool» با زیرنویسِ «SQL over big tables». فلش پررنگِ «SQL query» آن را به «Query Engine» می‌رساند (زیرنویس: «parse, optimize, execute»، برچسب: Trino / Spark).

The figure is a horizontal chain, left to right. First Analyst / BI Tool (SQL over big tables). The bold SQL query arrow reaches the Query Engine (parse, optimize, execute; Trino / Spark).

از بالای موتور کوئری، فلش چین‌چینِ «resolve table metadata» به جعبهٔ بالایی یعنی «Data Catalog» (زیرنویس: «which tables exist») می‌رود، و از کاتالوگ فلش چین‌چینِ «file list + schema» به «Table Format» — جعبه‌ای با زیرنویسِ «which files form a table» و برچسبِ «time travel, GC». فلش پررنگِ دومِ موتور کوئری، «read snapshot / commit»، مستقیم به همین Table Format می‌رود.

From the top of the engine, the dashed resolve table metadata climbs to the Data Catalog (which tables exist), and from the catalog the dashed file list + schema goes to the Table Format (which files form a table; time travel, GC). The engine’s second bold arrow, read snapshot / commit, goes straight to that same Table Format.

در انتهای زنجیره، «Object Storage» با زیرنویسِ «immutable columnar files» و برچسبِ Parquet / ORC نشسته؛ فلشِ «references data files» از Table Format به آن می‌رود و فلش چین‌چینِ برگشتیِ «column blocks» از پایینِ Object Storage به پایینِ موتور کوئری برمی‌گردد. همهٔ این چهار جعبه داخل قابِ «Data Lakehouse — compute and storage scaled independently» جمع شده‌اند و فقط ابزار تحلیل بیرون است.

At the chain’s end sits Object Storage (immutable columnar files; Parquet / ORC); references data files runs from Table Format to it, and the dashed column blocks returns from the bottom of Object Storage to the bottom of the query engine. All four boxes sit inside Data Lakehouse — compute and storage scaled independently; only the analysis tool is outside.

درسِ شکل: هر جعبه یک نقشِ جدا را نگه می‌دارد — موتور کوئری «چطور»، storage format «بایت‌ها چطور»، table format «کدام فایل‌ها = کدام جدول»، و catalog «کدام جدول‌ها وجود دارند». همین تفکیک است که مقیاس‌پذیریِ مستقل، time travel و حاکمیت داده را ممکن کرده است.

The lesson: each box keeps a separate role — the query engine “how”, the storage format “how bytes”, the table format “which files = which table”, the catalog “which tables exist”. That very separation is what made independent scaling, time travel and data governance possible.

۷7ذخیره‌سازی ستونیColumnar storage

ذخیره‌سازی ستونی، مقدارهای هر ستون را کنار هم نگه می‌دارد. این کار برای اسکنِ چند ستون از جدول بزرگ و فشرده‌سازیِ مقدارهای مشابه مناسب است.

مثال فنی: فشرده‌سازیِ ستون مشابه

وضعیت اولیه: ستون country در یک بلوک هزار مقدار تکراری IR دارد.

اجرای مرحله‌به‌مرحله: encoding فرهنگ‌نامه‌ای می‌تواند IR را با یک کد نمایش دهد و run-length طول تکرار را ثبت کند.

نتیجه و دلیل: دادهٔ هم‌نوع و مشابه بهتر فشرده می‌شود؛ این سود با مخلوط شدن همهٔ فیلدهای یک ردیف کمتر قابل‌استفاده است.

Real fact tables in a warehouse are often 100+ columns wide, yet a typical analytical query wants only 4–5 columns (SELECT * is rarely needed in analytics). The book’s example:

SELECT dim_date.weekday, dim_product.category,
       SUM(fact_sales.quantity) AS quantity_sold
FROM fact_sales
JOIN dim_date    ON fact_sales.date_key   = dim_date.date_key
JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
WHERE dim_date.year = 2024
  AND dim_product.category IN ('Fresh fruit', 'Candy')
GROUP BY dim_date.weekday, dim_product.category;
در ذخیره‌سازی ردیف‌محور (رایج در OLTP و پایگاه‌های داده سندی) همهٔ مقادیرِ یک ردیف کنار هم‌اند؛ حتی با ایندکس، موتور باید کل ردیف‌های (۱۰۰+ صفتیِ) منطبق را بارگذاری، تجزیه و فیلتر کند — کند.

In row-oriented storage (common in OLTP and document databases) all values of a row sit together; even with indexes the engine must load, parse and filter whole (100+-attribute) matching rows — slow.

ایده: ستون‌ها را کنار هم بچینThe idea: lay columns side by side

  • ذخیره‌سازی ستونی (columnar): به‌جای یک ردیف، همهٔ مقادیر هر ستون کنار هم ذخیره می‌شوند؛ پس کوئری فقط ستون‌های استفاده‌شده را می‌خواند و تجزیه می‌کند.
  • بازسازی ردیف: چون همهٔ ستون‌ها ردیف‌ها را به یک ترتیب نگه می‌دارند، مؤلفهٔ k امِ هر ستون = ردیفِ k ام.
  • در عمل کلِ ستونِ تریلیون‌ردیفی یک‌جا نیست: جدول به بلوک‌هایی از هزاران/میلیون‌ها ردیف شکسته می‌شود و چون کوئری‌ها معمولاً بازهٔ تاریخی دارند، هر بلوک معمولاً یک بازهٔ زمانی است. کوئری فقط بلوک‌های هم‌پوشانِ بازه و فقط ستون‌های لازم را می‌خواند.
  • این چیدمان تقریباً در همهٔ پایگاه‌های داده تحلیلیِ امروز هست: از Snowflake گرفته تا DuckDB، Pinot و Druid؛ فرمت‌های Parquet، ORC، Lance و Nimble و قالب‌های درون‌حافظه‌ای مثل Arrow و Pandas/NumPy؛ و برخی پایگاه‌های داده سری زمانی (InfluxDB IOx، TimescaleDB). Parquet حتی مدل سندی را هم پشتیبانی می‌کند (تکنیک shredding از Dremel).
  • ستون‌ها را می‌توان فشرده کرد؛ چون ستون‌ها معمولاً تکرارِ زیادی دارند (میلیاردها تراکنش، ۱۰۰ هزار محصولِ متمایز). روش شاخص: bitmap encoding — برای هر مقدارِ متمایز یک bitmap با یک بیت به‌ازای هر ردیف. bitmapهای تنک را run-length encoding می‌کنند و روش‌هایی مثل roaring bitmaps بین دو نمایش، فشرده‌تر را برمی‌گزینند.
  • Columnar storage: instead of one row, all values of each column are stored together; so a query reads and parses only the columns it uses.
  • Row reconstruction: since every column keeps rows in the same order, the k-th element of each column = the k-th row.
  • In practice a trillion-row column never sits in one place: the table is split into blocks of thousands/millions of rows, and since queries usually span a date range, each block is typically one time slice. A query reads only overlapping blocks and only needed columns.
  • This layout is in almost every analytical database today: from Snowflake to DuckDB, Pinot and Druid; formats like Parquet, ORC, Lance and Nimble plus in-memory layouts like Arrow and Pandas/NumPy; and some time-series databases (InfluxDB IOx, TimescaleDB). Parquet even supports the document model (the shredding technique from Dremel).
  • Columns can be compressed; columns usually repeat a lot (billions of transactions, 100k distinct products). The flagship method: bitmap encoding — one bitmap per distinct value with one bit per row. Sparse bitmaps get run-length encoding, and methods like roaring bitmaps pick the denser of the two representations.
دامِ نام‌هاName trap

تفاوتِ ذخیرهٔ سطری و ستونی، جای قرار گرفتن مقدارهاست؛ جدول از دید کاربر هنوز همان جدول است. ستون‌های مشابه اغلب بهتر فشرده می‌شوند.

مثال فنی: فقط مبلغ‌ها را بخوان

وضعیت اولیه: ردیف‌ها شامل id, address, description, amount هستند.

اجرای مرحله‌به‌مرحله: برای SUM(amount) موتور ستونی بلوک مبلغ را می‌خواند و نشانی و توضیح را کنار می‌گذارد.

نتیجه و دلیل: سود از کمتر خواندن می‌آید؛ اگر query همهٔ ستون‌های یک ردیف را بخواهد، همین مزیت الزاماً برقرار نیست.

Don’t confuse column-oriented with wide-column (column-family)! The wide-column model (e.g. Bigtable, Accumulo, HBase) lets each row hold thousands of different columns, but its storage is row-oriented (each row’s values sit together). Only the names are alike.

ترتیب مرتب‌سازی و نوشتنSort order and writes

ترتیب فیزیکی داده کمک می‌کند بخش‌های نامربوط خوانده نشوند. اما درج و به‌روزرسانیِ پراکنده در فایل‌های مرتب هزینه دارد و اغلب با بافر و ادغام مدیریت می‌شود.

مثال فنی: مرتب‌سازی و بلوک تازه

وضعیت اولیه: بلوک‌های فروش بر اساس day مرتب‌اند و هر بلوک حداقل و حداکثر روز را دارد.

اجرای مرحله‌به‌مرحله: فیلترِ یک روز، بلوک‌های خارج از بازه را رد می‌کند؛ دادهٔ تازه ابتدا جدا نوشته و بعد مرتب و ادغام می‌شود.

نتیجه و دلیل: sort order به data skipping کمک می‌کند؛ حفظ این نظم هنگام نوشتن هزینه دارد.

  • ترتیبِ ردیف‌ها مهم نیست (ساده: همان ترتیب درج)، اما می‌توان مرتب‌سازی انتخابی کرد و از آن به‌عنوان ایندکس بهره برد. نکته: ستون‌ها را نمی‌توان مستقل مرتب کرد (ارتباط k ام‌ها می‌شکند)؛ باید کلِ ردیف مرتب شود هرچند ستونی ذخیره می‌شود.
  • کلیدِ مرتب‌سازی اول (مثلاً date_key) برای بازه‌های تاریخی؛ کلید دوم (مثلاً product_sk) برای گروه‌بندیِ درون همان روز. امتیاز جانبی: مرتب‌سازی، تکرارهای طولانی می‌سازد و فشرده‌سازیِ run-length را حتی برای جدولِ میلیاردی چند کیلوبایتی می‌کند (بیشترین اثر روی کلید اول).
  • نوشتن در انبارداده معمولاً ورود دسته‌ای (ETL) است؛ درجِ تک‌ردیفی در وسطِ جدولِ مرتب و فشرده بسیار ناکارآمد است (باید از محل درج به بعد بازنویسی شود). راه‌حل، باز هم رویکرد log-structured: نوشتن‌ها اول به یک فروشگاهِ ردیف‌محورِ مرتبِ درون‌حافظه‌ای می‌روند و بعد دسته‌ای با فایل‌های ستونیِ روی دیسک ادغام و در فایل‌های تازه نوشته می‌شوند؛ چون فایل‌ها تغییرناپذیرند، object storage جای خوبی است.
  • کوئری باید دادهٔ روی دیسک و نوشتن‌های اخیرِ درون حافظه را با هم ببیند؛ موتور اجرا این تفکیک را از کاربر پنهان می‌کند. (Snowflake، Vertica، Pinot، Druid و ...)
  • Row order doesn’t matter (simplest: insertion order), but you may sort selectively and use it as an index. Catch: columns can’t be sorted independently (the k-th-element links would break); the whole row must be sorted even though it is stored column by column.
  • The first sort key (e.g. date_key) serves date ranges; the second (e.g. product_sk) groups within the same day. Side bonus: sorting builds long repetition runs and makes run-length compression shrink even billion-row tables to kilobytes (strongest effect on the first key).
  • Warehouse writes are usually batch ingest (ETL); single-row inserts into the middle of a sorted, compressed table are very inefficient (everything from the insert point on must be rewritten). The fix is the familiar log-structured approach: writes first land in a sorted in-memory row store, then batch-merge with on-disk columnar files into fresh files; since files are immutable, object storage is a good home.
  • A query must see on-disk data and recent in-memory writes together; the execution engine hides that split from the user. (Snowflake, Vertica, Pinot, Druid and more.)

اجرای کوئری: تفسیر، کامپایل و برداریQuery execution: interpretation, compilation, vectorization

پس از خواندن داده، روش اجرای محاسبه هم مهم است: اجرای برداری روی دسته‌ای از مقدارها کار می‌کند و کامپایل، بخشی از منطق پرس‌وجو را به کدِ اجرایی تبدیل می‌کند.

مثال فنی: برداری در برابر کامپایل

وضعیت اولیه: query باید یک میلیون مبلغ را جمع کند.

اجرای مرحله‌به‌مرحله: اجرای برداری دسته‌های مثلاً ۱۰۲۴تایی را به عملگر می‌دهد؛ کامپایل، منطق query را به کد اجرایی تخصصی تبدیل می‌کند.

نتیجه و دلیل: یکی شکل پردازشِ دسته‌ایِ مقدارها و دیگری روش اجرای منطق است؛ می‌توان هر دو را هم‌زمان داشت.

  • کوئریِ تحلیلی به طرحی از operatorها شکسته می‌شود که ممکن است روی چند ماشین موازی شوند. ساده‌ترین اجرا «مفسر» است: در حلقهٔ روی هر ردیف، ساختارِ کوئری چک می‌شود — برای تحلیلِ میلیون‌ردیفی کند است. دو رویکرد کارآمد:
  • Query compilation: موتور از SQL کد تولید می‌کند (حلقهٔ ردیف‌ها، مقایسه‌ها، کپیِ مقادیر منطبق) و آن را با کامپایلری مثل LLVM به کد ماشین تبدیل و اجرا می‌کند — مشابه JIT در JVM.
  • Vectorized processing: تفسیری می‌ماند، اما به‌جای ردیف‌به‌ردیف، روی دسته‌ای از مقادیرِ یک ستون کار می‌کند. مثال: اپراتور تساوی روی ستونِ product_sk و شناسهٔ «موز» یک bitmap برمی‌گرداند؛ همان اپراتور برای فروشگاه bitmap دوم؛ و اپراتور AND بیتی دو bitmap را ترکیب می‌کند (چون مؤلفهٔ k ام هر دو = همان ردیف).
  • هر دو از خصوصیات CPU امروزی بهره می‌برند: دسترسیِ ترتیبی به حافظه (cache miss کمتر)، حلقه‌های داخلیِ تنگ (pipeline شلوغ، پیش‌بینیِ شاخه کمتر)، موازی‌سازی با چند thread و دستورهای SIMD، و کار مستقیم روی دادهٔ فشرده بدون رمزگشایی.
  • An analytical query breaks into a plan of operators that may parallelize across machines. The simplest execution is an “interpreter”: loop over each row, check the query structure — slow for million-row analytics. Two efficient approaches:
  • Query compilation: the engine generates code from the SQL (row loops, comparisons, copying matches) and compiles it with something like LLVM into machine code — similar to JIT in the JVM.
  • Vectorized processing: stays interpreted, but works on batches of one column’s values instead of row by row. Example: an equality operator on product_sk for the “banana” id returns one bitmap; the same operator for the store returns a second; and a bitwise AND operator combines them (since the k-th element of both = the same row).
  • Both exploit modern CPUs: sequential memory access (fewer cache misses), tight inner loops (busy pipelines, fewer branch mispredicts), multi-thread parallelism and SIMD instructions, and working directly on compressed data without decoding.

۷.۱ نمودار ۴ + روایت مدرس7.1 Diagram 4 — from fact table to aggregation on columnar storage

نمودار ۴ · Dataflow — از جدول واقعیه تا تجمیع، روی ذخیره‌سازی ستونیDiagram 4 · Dataflow — from fact table to aggregation on columnar storage باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۴Teacher narration

مثال فنی برای خواندنِ نمودار: فقط مبلغ‌ها را بخوان

وضعیت اولیه: ردیف‌ها شامل id, address, description, amount هستند.

اجرای مرحله‌به‌مرحله: برای SUM(amount) موتور ستونی بلوک مبلغ را می‌خواند و نشانی و توضیح را کنار می‌گذارد.

نتیجه و دلیل: سود از کمتر خواندن می‌آید؛ اگر query همهٔ ستون‌های یک ردیف را بخواهد، همین مزیت الزاماً برقرار نیست.

چهار ستون داریم: «Source Table»، «Column Layout»، «Compression» و «Execution». در نوار بالا، جعبهٔ «Fact Table» با زیرنویسِ «billions of rows, wide table» نشسته است؛ فلش پررنگِ «split into blocks by date range» آن را به «Column Blocks» می‌رساند — جعبه‌ای با زیرنویسِ «one column per block» و برچسبِ «sorted by date_key».

Four columns: Source Table, Column Layout, Compression, Execution. Top lane: Fact Table (billions of rows, wide table); the bold split into blocks by date range leads to Column Blocks (one column per block, sorted by date_key).

نوار پایین، قصدِ کوئری است: جعبهٔ «SQL Query» با زیرنویسِ «3 of 100 columns». فلش پررنگِ «column pruning: load 3 of 100» از آن به بالا، به Column Blocks می‌رود — همان لحظه‌ای که ۹۷ ستونِ دیگر اصلاً خوانده نمی‌شوند. فلشِ «encode low-cardinality columns» از Column Blocks به جعبهٔ «Bitmap + RLE» (زیرنویس: «one bitmap per value»، برچسب: «sparse bits counted») می‌رود.

Bottom lane is the query’s intent: SQL Query (3 of 100 columns). The bold column pruning: load 3 of 100 rises to Column Blocks — the moment 97 other columns are never read. Then encode low-cardinality columns goes to Bitmap + RLE (one bitmap per value).

بعد فلشِ «bitwise AND of filter bitmaps» به «Vectorized Filter» با زیرنویسِ «equality + bitwise AND» و برچسبِ «SIMD batches»؛ از آن فلشِ «matching row indexes» به پایین، به «Aggregation» (زیرنویس: «SUM by weekday, category») می‌رود و در نهایت فلش چین‌چینِ «GROUP BY + SUM» از همان SQL Query به Aggregation، مشخصاتِ تجمیع را می‌رساند.

Next bitwise AND of filter bitmaps reaches Vectorized Filter (equality + bitwise AND, SIMD batches); matching row indexes drops to Aggregation (SUM by weekday, category), and the dashed GROUP BY + SUM from the SQL Query supplies the aggregation spec.

درسِ شکل: دو جریان موازی می‌بینید — جریانِ بالا داده است که لایه‌به‌لایه فشرده و آماده می‌شود، و جریانِ پایین نیتِ کوئری که تعیین می‌کند کدام بلوک‌ها و کدام بیت‌ها وارد بازی شوند. نقطهٔ اوج، Vectorized Filter است: به‌جای گشتنِ ردیف‌به‌ردیف، یک AND بیتی روی دسته‌های بزرگ.

The lesson: two parallel flows — the upper flow is data, compressed and staged layer by layer, and the lower flow is the query’s intent, deciding which blocks and bits join the game. The climax is the Vectorized Filter: one bitwise AND over big batches instead of row-by-row hunting.

نماهای materialized و مکعب داده (data cube)Materialized views and the data cube

نمای ازپیش‌محاسبه‌شده نتیجهٔ یک پرس‌وجو را ذخیره می‌کند. مکعب داده، خلاصه‌ها را در چند بُعد و سطح آماده می‌کند؛ خواندن سریع‌تر می‌شود ولی نگهداریِ خلاصه‌ها هزینه دارد.

مثال فنی: نمای آمادهٔ فروش روزانه

وضعیت اولیه: sales_by_day برای امروز جمع 1000 دارد و سفارش تازه مبلغ 200 دارد.

اجرای مرحله‌به‌مرحله: نگهدارندهٔ نما مقدار را به 1200 می‌رساند؛ اصلاح یا حذف همان سفارش هم باید تغییر معکوس مناسب اعمال کند.

نتیجه و دلیل: خواندن گزارش سریع‌تر می‌شود؛ هزینهٔ نگهداری نما و کنترل شمارش دوباره به مسیر تغییر منتقل می‌شود.

  • View مجازی فقط میان‌برِ نوشتن کوئری است و هنگام خواندن به کوئریِ زیرین باز می‌شود؛ view materialized یک کپیِ واقعی از نتیجهٔ کوئری است که روی دیسک نوشته می‌شود. وقتی دادهٔ زیرین عوض شود باید به‌روز شود (برخی پایگاه داده‌ها خودکار؛ سیستم‌هایی مثل Materialize در همین تخصصی‌اند). هزینه روی نوشتن، بهبود روی خواندنِ تکراری.
  • Materialized aggregate نوعی از آن است: اگر همان COUNT/SUM/AVG ها تکرار می‌شوند، چرا هر بار دادهٔ خام را نسابیم؟ مکعب داده (OLAP cube) شبکه‌ای از تجمیعهاست که بر اساس بُعدها گروه‌بندی شده: مثلاً تاریخ در یک محور و محصول در محور دیگر، و هر خانه = SUM برای آن ترکیب. جمع روی هر سطر/ستون = خلاصه‌ای با یک بُعد کمتر (فروش به‌تفکیک محصول، صرف‌نظر از تاریخ).
  • با ۵ بُعد (تاریخ، محصول، فروشگاه، تبلیغ، مشتری) تصور hypercube سخت می‌شود اما اصل همان است. مزیت: بعضی کوئری‌ها («فروش دیروز هر فروشگاه») عملاً از پیش محاسبه شده‌اند. محدودیت: انعطافِ دادهٔ خام را ندارد — مثلاً نمی‌توانی بپرسی «سهمِ فروشِ اقلام بالای ۱۰۰ دلار چقدر است» اگر قیمت بُعد نباشد. به‌همین‌دلیل انبارداده‌ها دادهٔ خام را نگه می‌دارند و مکعب را فقط تقویت‌کنندهٔ کارایی می‌دانند.
  • A virtual view is just a query-writing shortcut, expanded into the underlying query at read time; a materialized view is a real copy of a query’s result written to disk. When underlying data changes it must be refreshed (some databases do it automatically; systems like Materialize specialize in exactly this). Cost on writes, wins on repeated reads.
  • A materialized aggregate is one flavor: if the same COUNT/SUM/AVG repeats, why grind raw data every time? A data cube (OLAP cube) is a lattice of aggregates grouped by dimensions: e.g. date on one axis, product on the other, each cell = SUM for that combo. Summing a row/column = a summary with one fewer dimension (sales per product regardless of date).
  • With 5 dimensions (date, product, store, campaign, customer) picturing the hypercube gets hard, but the principle holds. Upside: some queries (“yesterday’s sales per store”) are effectively precomputed. Limit: no raw-data flexibility — e.g. you can’t ask “what share of sales came from items over $100” if price isn’t a dimension. So warehouses keep raw data and treat the cube as a pure performance booster.

۸8ایندکس‌های چندبُعدی، متنی و برداریMultidimensional, text and vector indexes

جست‌وجوی عددی، مکانی، متنی و معنایی سؤال‌های متفاوتی هستند. هرکدام به ساختاری نیاز دارند که همان نوع شباهت یا محدوده را سریع پیدا کند.

مثال فنی: سه سؤال، سه ایندکس

وضعیت اولیه: باید سفارش مشتری را بیابیم، سند دارای واژه را پیدا کنیم و محصول مشابه تصویر را پیشنهاد دهیم.

اجرای مرحله‌به‌مرحله: B-tree برای کلید مرتب، inverted index برای واژه و ANN برای بردار مسیرهای متفاوت دارند.

نتیجه و دلیل: «ایندکس داریم» کافی نیست؛ نوع پرسش تعیین می‌کند چه رابطه‌ای بین ورودی و محل داده لازم است.

B-trees and LSM answer range queries over one attribute well (e.g. all names starting with L). But sometimes one attribute isn’t enough.

ایندکس چندستونی و چندبُعدیMulti-column and multidimensional indexes

ایندکس چندستونی به ترتیبِ ستون‌ها حساس است. ایندکس مکانی برای داده‌هایی مثل مختصات ساخته می‌شود تا جست‌وجوی محدوده و همسایگی را آسان کند.

مثال فنی: ترتیبِ ستون‌های ایندکس

وضعیت اولیه: ایندکس (city, created_at) ابتدا شهر و در هر شهر زمان را مرتب می‌کند.

اجرای مرحله‌به‌مرحله: شرط شهر تهران همراه بازهٔ زمان پیوسته است؛ بازهٔ زمان برای همهٔ شهرها ممکن است به چند بخش یا روش دیگری نیاز داشته باشد.

نتیجه و دلیل: یک B-tree چندستونی جای ایندکس هندسی برای همهٔ شرط‌های دوبعدی را نمی‌گیرد.

  • Concatenated index: چند فیلد را به هم می‌چسباند و کلیدِ ترکیبی می‌سازد (مثل دفتر تلفن: (lastname, firstname)). با ترتیبِ مرتب‌سازی، برای «همه با فامیلیِ X» یا «X + نامِ Y» عالی است، اما برای «همه با نامِ Y» بی‌فایده.
  • کوئری چندبُعدی: مثال رستوران‌ها روی نقشه — جست‌وجوی مستطیلِ دیدِ کاربر با طول و عرض جغرافیایی. ایندکسِ چسبانده‌شده نمی‌تواند هر دو بازه را هم‌زمان بدهد (یا بازهٔ عرض در همهٔ طول‌ها، یا بالعکس).
  • راه‌ها: تبدیل مکان به یک عدد با space-filling curve + B-tree معمولی؛ یا ایندکس‌های تخصصی مثل R-tree و Bkd-tree که فضا را طوری تقسیم می‌کنند که نقاطِ نزدیک در یک زیردرخت بیفتند (PostGIS از R-tree با امکانات GiST پست‌گرس استفاده می‌کند)؛ یا شبکه‌های منظم مثلثی/مربعی/شش‌ضلعی.
  • کاربرد فقط جغرافیایی نیست: ایندکس سه‌بُعدی روی (قرمز، سبز، آبی) برای جست‌وجوی بازهٔ رنگ در فروشگاه، یا دو‌بُعدی روی (تاریخ، دما) برای «مشاهداتِ یک سال با دمای ۲۵ تا ۳۰ درجه».
  • Concatenated index: glues several fields into a composite key (like a phone book: (lastname, firstname)). With sort order it is great for “all with family name X” or “X + first name Y”, but useless for “all with first name Y”.
  • Multidimensional query: e.g. restaurants on a map — searching the user’s viewport rectangle with latitude and longitude. A glued index can’t serve both ranges at once (either the latitude range across all longitudes, or vice versa).
  • Options: turn location into one number with a space-filling curve + an ordinary B-tree; or specialized indexes like R-trees and Bkd-trees that split space so nearby points land in one subtree (PostGIS uses R-trees via Postgres GiST); or regular triangular/square/hexagonal grids.
  • Not only geography: a 3-D index over (red, green, blue) for color-range search in a shop, or 2-D over (date, temperature) for “observations of one year at 25–30 degrees”.

جست‌وجوی متن (Full-Text Search)Full-text search

ایندکس معکوس برای هر واژه مشخص می‌کند در کدام سندها آمده است. جست‌وجوی متن می‌تواند علاوه بر حضور واژه، شکل واژه و رتبهٔ نتیجه را هم در نظر بگیرد.

مثال فنی: تقاطع فهرست اسناد

وضعیت اولیه: فهرست سیب برابر [1,3,7] و قرمز برابر [3,5,7] است.

اجرای مرحله‌به‌مرحله: اشتراک فهرست‌ها سندهای 3,7 را می‌دهد؛ برای عبارت دقیق باید موقعیت واژه‌ها هم پشت‌سرهم باشد.

نتیجه و دلیل: داشتن هر دو واژه با داشتن عبارت «سیب قرمز» یکسان نیست؛ ایندکس باید اطلاعات لازم همان سؤال را نگه دارد.

  • جست‌وجوی متن یعنی یافتن اسنادی که کلیدواژه‌ای هرجای متن‌شان هست. در هسته، هر واژه (term) یک بُعد است: سندِ دارایِ term مقدار ۱ و بقیه ۰ — یک کوئریِ چندبُعدی با تعدادِ ابعادِ بسیار زیاد.
  • ساختار رایج: inverted index — نگاشت از term به فهرستِ شناسهٔ اسنادِ حاویِ آن (postings list). اگر شناسه‌ها ترتیبی باشند، postings را می‌توان با bitmap پراکنده هم نمایش داد (بیتِ n ام = سندِ n).
  • «سیب‌های قرمز» = بارگذاریِ bitmap دو واژه و AND بیتی‌شان — حتی با run-length هم بسیار سریع. Lucene (مغز Elasticsearch و Solr) همین‌طور کار می‌کند: نگاشتِ term به postings در فایل‌هایِ مرتبِ SSTable-مانند که با همان رویکرد log-structured در پس‌زمینه ادغام می‌شوند. نوعِ GIN در PostgreSQL هم برای متن و JSON از postings استفاده می‌کند.
  • به‌جای شکستن به واژه‌ها، می‌توان همهٔ زیررشته‌های طول n (n-gram؛ مثلاً trigram های hello = hel, ell, lld) را ایندکس کرد: جست‌وجوی هر زیررشتهٔ حداقل سه‌حرفی و حتی regex — به بهای حجمِ زیاد ایندکس.
  • برای غلط تایپی: Lucene می‌تواند کلماتِ با فاصلهٔ ویرایشیِ مشخص (افزودن/حذف/جایگزینیِ یک حرف) را بیابد؛ مجموعهٔ واژه‌ها را به‌صورت ماشینِ حالتِ متناهی روی کاراکترها (مثل trie) نگه می‌دارد و به Levenshtein automaton تبدیل می‌کند.
  • پردازشِ زبانی (زبان‌های بدون فاصله، مترادف‌ها، صرف‌ها) تخصصِ جداگانه‌ای است و خارج از دامنهٔ کتاب.
  • Text search means finding documents containing a keyword anywhere in their text. At heart, each word (term) is one dimension: a document with the term scores 1, the rest 0 — a multidimensional query with a huge number of dimensions.
  • The common structure: an inverted index — a map from term to the IDs of documents containing it (postings list). With sequential IDs, postings can also be sparse bitmaps (n-th bit = n-th document).
  • “Red apples” = load the two words’ bitmaps and bitwise AND them — very fast even with run-length. Lucene (the heart of Elasticsearch and Solr) works this way: term→postings maps in sorted SSTable-like files merged log-structured in the background. PostgreSQL’s GIN type uses postings for text and JSON too.
  • Instead of splitting into words, you can index every length-n substring (n-grams; e.g. trigrams of hello = hel, ell, lld): search any 3+-letter substring, even regex — at the cost of a huge index.
  • For typos: Lucene can find words at a given edit distance (adding/removing/replacing one letter); it keeps the word set as a finite-state machine over characters (like a trie) and converts it to a Levenshtein automaton.
  • Linguistic processing (spaceless languages, synonyms, inflections) is its own specialty and out of the book’s scope.

جست‌وجوی معنایی و بردارهاSemantic search and vectors

  • جست‌وجوی معنایی (semantic search) فراتر از مترادف و غلط تایپی، مفهومِ سند و نیتِ کاربر را می‌فهمد؛ بخشی مهم از کاربردهای AI مثل RAG (افزودن نتیجهٔ جست‌وجو به خروجی مدل زبانی). مثال: صفحهٔ «لغو اشتراک» باید با جست‌وجوی «چطور حسابم را ببندم» هم پیدا شود.
  • مدلِ embedding، سند را به برداری از اعداد اعشاری (vector embedding) تبدیل می‌کند: نقطه‌ای در فضای چندبُعدی. اسنادِ از نظر معنایی شبیه، بردارهای نزدیک می‌گیرند. مثال سه‌بُعدی کتاب: صفحهٔ کشاورزی [0.38, 0.83, 0.41]، صفحهٔ سبزیجات [0.36, 0.64, 0.67] (نزدیک)، صفحهٔ اسکیمای ستاره‌ای [0.85, 0.10, -0.52] (دور). مدل‌های واقعی بیش از ۱۰۰۰ عدد دارند و معنای تک‌تک اعداد مهم نیست؛ فقط مختصات در فضای انتزاعی‌اند.
  • اندازه‌گیریِ فاصله: cosine similarity (کسینوسِ زاویهٔ دو بردار) یا Euclidean distance (فاصلهٔ خط مستقیم). مدل‌های اولیه (Word2Vec، BERT، GPT) متنی بودند؛ بعد ویدیو/صدا/تصویر آمد و حالا مدل‌های چندحالته (multimodal) یک مدل برای چند نوع داده‌اند.
  • موتور جست‌وجو، کوئری را هم به بردار تبدیل می‌کند و با vector index دنبال نزدیک‌ترین بردارها می‌گردد. R-tree برای ابعادِ زیاد خوب کار نمی‌کند؛ ساختارهای تخصصی:
  • Flat: همه‌چیز ابتدایی؛ هر بار مقایسه با همهٔ بردارها — دقیق ولی کند.
  • IVF (inverted file): فضا به خوشه‌ها (centroid) شکسته می‌شود و کوئری فقط چند خوشه (تعدادِ probes) را می‌گردد؛ سریع‌تر ولی تقریبی (ممکن است سندِ نزدیک در خوشهٔ دیگری افتاده باشد). probes بیشتر = دقت بیشتر، سرعت کمتر.
  • HNSW (Hierarchical Navigable Small World): چند لایه گرافِ مجاورت؛ کوئری از لایهٔ بالایی (گره‌های کم) شروع می‌کند، نزدیک‌ترین را می‌یابد، به همان گره در لایهٔ پایین‌تر می‌رود و در گرافِ متراکم‌تر ادامه می‌دهد تا لایهٔ آخر. مثل IVF تقریبی است.
  • کتابخانه‌های Faiss (چند واریانت از هر دو) و pgvector در PostgreSQL (هر دو) رایج‌اند؛ جزئیات الگوریتم‌ها در مقاله‌هایشان.
  • Semantic search goes beyond synonyms and typos to grasp a document’s concept and the user’s intent; a big part of AI uses like RAG (feeding search results into a language model’s output). Example: the “cancel subscription” page should surface for “how do I close my account” too.
  • An embedding model turns a document into a vector of floats (vector embedding): a point in multi-dimensional space. Semantically close documents get nearby vectors. The book’s 3-D example: the farming page [0.38, 0.83, 0.41], the vegetables page [0.36, 0.64, 0.67] (nearby), the star-schema page [0.85, 0.10, -0.52] (far). Real models have 1000+ numbers and individual values don’t matter; they are just coordinates in an abstract space.
  • Distance measures: cosine similarity (cosine of the two vectors’ angle) or Euclidean distance (straight-line distance). Early models (Word2Vec, BERT, GPT) were textual; then came video/audio/image, and now multimodal models cover several data types in one.
  • The engine embeds the query too and hunts nearest vectors with a vector index. R-trees don’t work well for many dimensions; specialized structures:
  • Flat: the naive everything; compare against all vectors every time — exact but slow.
  • IVF (inverted file): space is split into clusters (centroids) and a query scans only a few clusters (probe count); faster but approximate (a nearby document may have fallen in another cluster). More probes = more accuracy, less speed.
  • HNSW (Hierarchical Navigable Small World): layered proximity graphs; the query starts at the top layer (few nodes), finds the nearest, descends to the same node in the lower layer and continues in the denser graph to the bottom. Approximate like IVF.
  • Faiss (several variants of both) and pgvector in PostgreSQL (both) are common; algorithm details live in their papers.
دو معنای «vector»Two meanings of “vector”

جست‌وجوی برداری، متن یا تصویر را به یک بردار عددی تبدیل می‌کند و همسایه‌های نزدیکش را می‌یابد. نزدیکیِ بردار تخمینی از شباهت است و لزوماً درستیِ معنایی را تضمین نمی‌کند.

مثال فنی: جست‌وجوی تقریبی بردار

وضعیت اولیه: query برداری نزدیک به محصول P7 است، اما فقط چند خوشه یا بخشی از گراف بررسی می‌شود.

اجرای مرحله‌به‌مرحله: IVF خوشه‌های منتخب را می‌کاود؛ HNSW مسیر همسایه‌ها را روی گراف طی می‌کند؛ گسترش جست‌وجو معمولاً کار بیشتری می‌خواهد.

نتیجه و دلیل: ممکن است نزدیک‌ترین همسایهٔ واقعی جا بماند؛ recall باید با جست‌وجوی دقیق روی نمونهٔ مرجع سنجیده شود.

Vectorized processing (section 7) means batch-processing bits with optimized code — “vector” there = a batch of data. In embedding models, “vector” = an array of floats locating a point in multi-dimensional space. Only the names are shared!

۸.۱ نمودار ۵ + روایت مدرس8.1 Diagram 5 — spatial, text and vector indexes

نمودار ۵ · Dataflow — ایندکس‌های فضایی، متنی و برداریDiagram 5 · Dataflow — spatial, text and vector indexes باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۵Teacher narration

مثال فنی برای خواندنِ نمودار: سه سؤال، سه ایندکس

وضعیت اولیه: باید سفارش مشتری را بیابیم، سند دارای واژه را پیدا کنیم و محصول مشابه تصویر را پیشنهاد دهیم.

اجرای مرحله‌به‌مرحله: B-tree برای کلید مرتب، inverted index برای واژه و ANN برای بردار مسیرهای متفاوت دارند.

نتیجه و دلیل: «ایندکس داریم» کافی نیست؛ نوع پرسش تعیین می‌کند چه رابطه‌ای بین ورودی و محل داده لازم است.

این شکل سه ردیف دارد، هر ردیف یک نوع پرس‌وجو، و سه ستون دارد: «Query Shape»، «Index Structure» و «Specialized Engine». هر ردیف یک زنجیرهٔ افقی است.

The figure has three rows — one query type each — and three columns: Query Shape, Index Structure, Specialized Engine. Each row is one horizontal chain.

ردیف بالا برای نقشه است: جعبهٔ «Map Range Query» (زیرنویس: «lat + lon window»، برچسب: «2D range») با فلشِ پررنگِ «partition the space» به «R-Tree» (زیرنویس: «groups nearby points»، برچسب: «bounding boxes»)، و از آن با فلشِ «index by bounding boxes» به «Spatial Search» با زیرنویسِ «box overlap checks».

Top row is for maps: Map Range Query (lat + lon window; 2D range) via the bold partition the space to the R-Tree (groups nearby points; bounding boxes), then index by bounding boxes to Spatial Search (box overlap checks).

ردیف وسط برای متن است: «Keyword Query» با زیرنویسِ «docs with red apples»، فلشِ پررنگِ «map terms to postings» به «Inverted Index» (زیرنویس: «term to postings»)، و فلشِ «bitwise AND of postings» به «Full-Text Search» با برچسبِ «Lucene-style».

Middle row is for text: Keyword Query (docs with red apples), the bold map terms to postings to the Inverted Index (term to postings), and bitwise AND of postings to Full-Text Search (Lucene-style).

ردیف پایین برای معناست: «Semantic Query» با زیرنویسِ «meaning, not words»، فلشِ پررنگِ «embed query as vector» به «Vector Index» (زیرنویس: «HNSW graph / IVF»، برچسب: «1,000+ dims»)، و فلشِ «walk graph / probe clusters» به «Nearest Neighbors» با زیرنویسِ «cosine / euclidean» و برچسبِ «approximate».

Bottom row is for meaning: Semantic Query (meaning, not words), the bold embed query as vector to the Vector Index (HNSW graph / IVF; 1,000+ dims), and walk graph / probe clusters to Nearest Neighbors (cosine / euclidean; approximate).

درسِ شکل: هر سه ردیف یک الگو دارند — «شکلِ پرس‌وجو → ساختارِ ایندکس → موتور تخصصی» — و فقط ماهیتِ «فاصله» عوض می‌شود: فاصلهٔ هندسی در ردیف بالا، هم‌پوشانیِ واژگانی در ردیف وسط، و فاصلهٔ معنایی در ردیف پایین. و فقط ردیفِ پایین صراحتاً برچسبِ «approximate» دارد: IVF و HNSW سرعت را با دقت مبادله می‌کنند.

The lesson: all three rows share one pattern — “query shape → index structure → specialized engine” — only the nature of “distance” changes: geometric distance on top, lexical overlap in the middle, semantic distance at the bottom. And only the bottom row is explicitly tagged approximate: IVF and HNSW trade accuracy for speed.

نقشهٔ فصل — هفت ایده در یک نگاهChapter map — seven ideas at a glance

نقشهٔ فصل ۴ · موتورهای ذخیره‌سازی در یک نگاهChapter 4 map · Storage engines at a glance باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نقشهٔ فصلChapter-map narration

مثال فنی برای خواندنِ نمودار: یک داده، دو مسیر خواندن

وضعیت اولیه: جدول سفارش با id خوانده می‌شود و گزارش فروش فقط ستون amount را جمع می‌زند.

اجرای مرحله‌به‌مرحله: lookup نقطه‌ای از ایندکس استفاده می‌کند؛ اسکن تحلیلی از چیدمان ستونی سود می‌برد.

نتیجه و دلیل: موتور ذخیره‌سازی را باید با نوع خواندن و نوشتن سنجید؛ سریع بودن یک lookup دربارهٔ سرعت اسکن میلیاردی کافی نیست.

گرهٔ میانی «ذخیره‌سازی و بازیابی» است و هفت ایده دورش چیده‌اند: از بالا «تریدآف لاگ و ایندکس» (با یال «افزودن»)، «موتور LSM» («ادغام») و «موتور B-tree» («صفحه‌ها»)؛ در میانه «ایندکس‌های ثانویه» («اشاره») و «فروشگاه‌های درون‌حافظه‌ای» («حافظه»)؛ و در پایین «تحلیل ستونی» («پیمایش») و «جست‌وجوی متنی و برداری» («یافتن»). پیام نقشه: هر گره پاسخی متفاوت به همان دو پرسشِ «نگه‌داشتن» و «پس‌دادن» است — با تریدآف خواندن/نوشتن/فضا.

The hub is “Storage & Retrieval”, ringed by seven ideas: on top the “Log + index trade-off” (“appends”), the “LSM-tree engine” (“merges”) and the “B-tree engine” (“pages”); mid-row “Secondary indexes” (“points”) and “In-memory stores” (“RAM”); below “Columnar analytics” (“scans”) and “Text + vector search” (“finds”). The map’s message: each node answers the same two questions — “keep” and “hand back” — with a different read/write/space trade-off.

۹9بستهٔ مصاحبهInterview pack

۹.۱ همه‌چیز در یک نگاه9.1 Everything at a glance

اصطلاحتعریف یک‌خطیکلیدواژهٔ مصاحبه
Log (append-only)دنباله‌ای فقط‌افزودنی از رکوردها روی دیسکساده‌ترین و سریع‌ترین نوشتن
Indexساختاری مشتق‌شده برای یافتن سریع دادهخواندن سریع، نوشتن کندتر
Hash Indexنگاشت کلید به offset در حافظهبدون range query، حافظه‌محور
SSTableجفت‌کلیدمقدار مرتب، هر کلید یک‌بارایندکس پراکنده، فشرده‌سازی بلوک
Memtableنقشهٔ مرتب درون‌حافظه‌ای موتور LSMflush به‌صورت SSTable
LSM-Treeسگمنت‌های مرتب که در پس‌زمینه ادغام می‌شوندRocksDB، Cassandra، HBase
Compactionادغام سگمنت‌ها و دور ریختن مقادیر کهنهsize-tiered در برابر leveled
Tombstoneرکورد حذفِ ویژه تا merge مقادیر قبلی را دور بیندازدحذفِ قطعیِ کند
Bloom Filterبررسیِ سریع و تقریبیِ وجود کلید در سگمنتfalse positive بی‌ضرر
B-Treeدرختِ صفحاتِ با اندازهٔ ثابت با بازنویسی درجااستاندارد پایگاه‌های داده رابطه‌ای
Branching Factorتعداد ارجاع‌های فرزند در هر صفحهچند صدتا ← عمق ۳–۴ سطح
Page Split / WALتقسیم صفحهٔ پر / لاگ بازنویسیِ پیشاپیشfsync، جلوگیری از torn page
Write Amplificationنسبت بایتِ نوشته‌شده روی دیسک به دادهٔ واقعیپهنای باند، عمر SSD
Sequential vs Random Writesنوشتن‌های بزرگِ ترتیبی در برابر ریزنوشتن‌های پراکندهGC فلش، بازده نوشتن
Secondary / Clustered / Coveringایندکس روی غیرکلید / داده داخل ایندکس / ستون‌های اضافه داخل ایندکسheap file و forwarding pointer
Column-Oriented Storageکنار هم چیدن مقادیر هر ستون به‌جای هر ردیفcolumn pruning، ستون‌ها هم‌ترتیب
Bitmap / RLE / Roaringکدگذاری ستونِ کم‌تنوع و فشرده‌سازی‌اشAND/OR بیتی روی شرط‌ها
JIT / Vectorized Processingکامپایل کوئری به کد ماشین / اجرا روی دسته‌های مقادیرLLVM، SIMD، دادهٔ فشرده
Materialized View / Data Cubeکپیِ نتیجهٔ کوئری روی دیسک / شبکهٔ تجمیعها روی بُعدهاOLAP cube، محدودیت ابعاد
Inverted Index / Postingsنگاشت واژه به فهرست اسناد حاوی‌اشAND بیتی، Lucene، GIN
Vector Embedding / IVF / HNSWبردارِ معنا / خوشه‌بندی / گرافِ لایه‌ایANN تقریبی، cosine / euclidean
TermOne-line definitionInterview keyword
Log (append-only)Append-only sequence of records on diskSimplest, fastest writes
IndexDerived structure for finding data fastFast reads, slower writes
Hash indexKey→offset map in memoryNo range queries, memory-bound
SSTableSorted key-value pairs, each key onceSparse index, block compression
MemtableLSM engine’s in-memory sorted mapFlushes as SSTable
LSM-treeSorted segments merged in the backgroundRocksDB, Cassandra, HBase
CompactionMerging segments, dropping stale valuesSize-tiered vs leveled
TombstoneSpecial delete record so merges drop old valuesSlow definitive deletion
Bloom filterFast approximate key-presence check per segmentHarmless false positives
B-treeFixed-size-page tree with in-place rewritesRelational-database standard
Branching factorChild references per pageA few hundred → 3–4 levels
Page split / WALSplitting full pages / write-ahead logfsync, torn-page prevention
Write amplificationBytes written to disk vs real dataBandwidth, SSD life
Sequential vs random writesBig sequential writes vs scattered micro-writesFlash GC, write throughput
Secondary / clustered / coveringNon-key index / data inside the index / extra columns inside the indexHeap file, forwarding pointers
Column-oriented storageEach column’s values side by side instead of rowsColumn pruning, co-ordered columns
Bitmap / RLE / roaringLow-cardinality column coding and its compressionBitwise AND/OR over predicates
JIT / vectorized processingCompiling queries to machine code / running over value batchesLLVM, SIMD, compressed data
Materialized view / data cubeQuery-result copy on disk / lattice of aggregates over dimensionsOLAP cube, dimension limits
Inverted index / postingsWord→document-ID mapBitwise AND, Lucene, GIN
Vector embedding / IVF / HNSWMeaning vector / clustering / layered graphApproximate ANN, cosine / euclidean

۹.۲ سوالات پرتکرار مصاحبه (برای دیدن جواب کلیک کن)9.2 Frequent interview questions (click for answers)

۱. پایگاه داده در پایین‌ترین سطح چه وظیفه‌ای دارد؟1. What is a database’s job at the lowest level?

موتور باید دادهٔ پذیرفته‌شده را طبقِ تضمینِ ماندگاری نگه دارد و بعداً بتواند آن را پیدا کند. ساختارهای این فصل راه‌های مختلفِ انجامِ همین دو کارند.

مثال فنی: آخرین مقدار در لاگ

وضعیت اولیه: فایل به‌ترتیب P7=100 و P7=120 دارد.

اجرای مرحله‌به‌مرحله: نوشتن فقط به انتهای فایل اضافه می‌کند؛ خواندن بدون ایندکس باید آخرین رکورد P7 را در فایل پیدا کند.

نتیجه و دلیل: append ساده است، ولی خواندن با رشد فایل گران می‌شود؛ ایندکس نشانی آخرین مقدار را نگه می‌دارد.

Two jobs: keep the data you give it and return it when you ask. Everything in this chapter (indexes, LSM, B-trees, columnar) answers those two questions with different trade-offs.

۲. ایندکس چیست و تریدآف اصلی‌اش کدام است؟2. What is an index, and its core trade-off?

ایندکس ساختاری اضافی برای پیدا کردنِ داده است. خواندن را سریع‌تر می‌کند، ولی فضا می‌گیرد و تغییرِ داده باید آن را هم به‌روز کند.

مثال فنی: بهای ایندکسِ اضافه

وضعیت اولیه: روی جدول سفارش، ایندکس customer_id ساخته‌ایم.

اجرای مرحله‌به‌مرحله: خواندن سفارش‌های مشتری ۱۷ دیگر کل جدول را اسکن نمی‌کند؛ هر درج یا تغییر مشتری باید ایندکس را هم اصلاح کند.

نتیجه و دلیل: کاهش هزینهٔ خواندن با فضای بیشتر و کار اضافهٔ نوشتن همراه است؛ ایندکسِ بلااستفاده هزینهٔ رایگان نیست.

An extra structure derived from the primary data (e.g. sorted by key) that speeds up search. The trade-off: every write must update the index too, and it consumes space. That is why we pick indexes manually, based on query patterns.

۳. چرا hash index به‌تنهایی کافی نیست؟3. Why isn’t a hash index enough on its own?

در مدلِ سادهٔ فصل، ایندکسِ هشِ درونِ حافظه جست‌وجوی کلید را سریع می‌کند، اما خواندنِ بازه، ظرفیتِ حافظه، بازیابی و جمع‌آوریِ نسخه‌های قدیمی مسائلِ جداگانه‌اند.

مثال فنی: بلوکِ مناسب به‌جای کل فایل

وضعیت اولیه: SSTable کلیدهای مرتب 100…999 دارد و ایندکس پراکنده شروع هر بلوک را می‌شناسد.

اجرای مرحله‌به‌مرحله: برای کلید 740 بلوکِ شروع‌شونده در 700 پیدا و همان بخش خوانده می‌شود.

نتیجه و دلیل: مرتب بودن هم جست‌وجوی بلوک و هم اسکن بازهٔ 740…760 را ممکن می‌کند؛ هش به‌تنهایی ترتیب بازه را نمی‌دهد.

Four problems: overwritten values’ space is never freed; the hash map must be rebuilt by scanning the whole log on restart; it must fit in memory (hash on disk is slow and complex); and there are no range queries.

۴. SSTable چه مزیتی بر لاگ ساده دارد؟4. What advantage does an SSTable have over a plain log?

SSTable کلیدها را مرتب نگه می‌دارد. این نظم امکانِ ایندکسِ پراکنده، ادغامِ فایل‌ها و خواندنِ بازه‌ای را فراهم می‌کند؛ لازم نیست نشانیِ هر کلید در حافظه باشد.

مثال فنی: بلوکِ مناسب به‌جای کل فایل

وضعیت اولیه: SSTable کلیدهای مرتب 100…999 دارد و ایندکس پراکنده شروع هر بلوک را می‌شناسد.

اجرای مرحله‌به‌مرحله: برای کلید 740 بلوکِ شروع‌شونده در 700 پیدا و همان بخش خوانده می‌شود.

نتیجه و دلیل: مرتب بودن هم جست‌وجوی بلوک و هم اسکن بازهٔ 740…760 را ممکن می‌کند؛ هش به‌تنهایی ترتیب بازه را نمی‌دهد.

Sortedness by key: you can keep a sparse index (only each multi-KB block’s first key), compress blocks, and merge several SSTables mergesort-style with little memory. Lookup: jump to the right block, scan a few kilobytes.

۵. نقش memtable و WAL در موتور LSM چیست؟5. What do the memtable and WAL do in an LSM engine?

Memtable تغییرهای تازه را در حافظهٔ مرتب جمع می‌کند؛ WAL اطلاعاتِ لازم برای بازیابیِ آن‌ها را نگه می‌دارد. پس از پر شدن، داده به SSTable منتقل می‌شود.

مثال فنی: نسخه‌های تازه در LSM

وضعیت اولیه: P7=100 در SSTable قدیمی است و P7=120 در memtable نوشته می‌شود.

اجرای مرحله‌به‌مرحله: نوشتن، WAL و حافظه را به‌روز می‌کند؛ flush فایل مرتب تازه می‌سازد و compaction نسخه‌های لازم را ادغام می‌کند.

نتیجه و دلیل: خواننده باید نسخهٔ قابل‌مشاهدهٔ تازه را انتخاب کند؛ پیدا شدن کلید در یک فایل قدیمی برای توقف کافی نیست.

The memtable is the in-memory sorted map accepting writes; when full it flushes, sorted, as an SSTable. Since the memtable is volatile, every write is simultaneously recorded in an on-disk log (WAL) for post-crash rebuild; after a flush the matching log section is discarded.

۶. جست‌وجوی نقطه‌ای در LSM چطور انجام می‌شود؟6. How does LSM point lookup work?

خواندن وضعیتِ تازه را در حافظه و فایل‌های مرتبط جست‌وجو می‌کند. فیلترها و ایندکس‌ها فایل و بلوکِ لازم را محدود می‌کنند؛ نسخه‌ها و علامتِ حذف تعیین می‌کنند چه نتیجه‌ای معتبر است.

مثال فنی: نسخه‌های تازه در LSM

وضعیت اولیه: P7=100 در SSTable قدیمی است و P7=120 در memtable نوشته می‌شود.

اجرای مرحله‌به‌مرحله: نوشتن، WAL و حافظه را به‌روز می‌کند؛ flush فایل مرتب تازه می‌سازد و compaction نسخه‌های لازم را ادغام می‌کند.

نتیجه و دلیل: خواننده باید نسخهٔ قابل‌مشاهدهٔ تازه را انتخاب کند؛ پیدا شدن کلید در یک فایل قدیمی برای توقف کافی نیست.

In order: memtable, then segments newest to oldest. For each segment the Bloom filter is checked first; a “no” skips the segment, a “yes” sends you via the sparse index to the block. Costliest case: a key updated very long ago, or one that doesn’t exist at all.

۷. Bloom filter چطور کار می‌کند و false positive یعنی چه؟7. How does a Bloom filter work — what is a false positive?

برای کلید، چند موقعیتِ بیت علامت می‌خورد. اگر بیتِ لازم صفر باشد، کلید نیست؛ اگر همه یک باشند، شاید هست، چون کلیدهای دیگر هم همان بیت‌ها را علامت زده‌اند.

مثال فنی: شاید هست، قطعاً نیست

وضعیت اولیه: برای یک SSTable، Bloom filter می‌گوید بیت لازم برای کلید P99 صفر است.

اجرای مرحله‌به‌مرحله: فایل برای P99 خوانده نمی‌شود؛ اگر همهٔ بیت‌ها یک باشند، فایل هنوز باید بررسی شود.

نتیجه و دلیل: پاسخ مثبت می‌تواند کاذب باشد؛ فیلترِ سالم برای مجموعهٔ ساخته‌شده پاسخ منفی کاذب نمی‌دهد و جای بررسی داده را نمی‌گیرد.

Several hashes per key set bits in a bit-array. At lookup, one 0 bit means the key is definitely absent; all 1s means probably present (a false positive = an absent key that looks present). ~10 bits per key → ~1% error; in an LSM engine the error is harmless since it only causes a little wasted work.

۸. دو استراتژی compaction را مقایسه کن.8. Compare the two compaction strategies.

ادغامِ اندازه‌محور فایل‌های مشابه را جمع می‌کند و معمولاً فضای موقت و فایل‌های بیشتری دارد. ادغامِ سطحی بازه‌ها را منظم‌تر نگه می‌دارد، اما می‌تواند داده را بیشتر بازنویسی کند.

مثال فنی: دو سیاست ادغام

وضعیت اولیه: چهار فایل 100 MB داریم که بعضی کلیدهایشان هم‌پوشانی دارد.

اجرای مرحله‌به‌مرحله: size-tiered می‌تواند فایل‌های هم‌اندازه را ادغام کند؛ leveled فایل‌ها را در سطح‌ها و بازه‌های منظم‌تر نگه می‌دارد.

نتیجه و دلیل: اولی معمولاً فایل و فضای موقت بیشتری می‌خواهد؛ دومی ممکن است بازنویسی بیشتری بسازد. نتیجه به بار و تنظیمات وابسته است.

Size-tiered: small segments merge together, then with bigger ones; high write throughput, lots of temporary space, reads may scan several large segments. Leveled: fixed-size segments in levels L0, L1, … with size caps; gradual merges, less space, faster reads. Rule: write-heavy → size-tiered; read-heavy → leveled.

۹. tombstone چیست و چه چالشی دارد؟9. What is a tombstone, and its challenge?

Tombstone علامتِ حذف است تا خواندن و ادغام، نسخهٔ قدیمیِ رکورد را دوباره زنده نکنند. حذفِ خودِ علامت فقط وقتی امن است که نسخه‌های پوشش‌داده‌شده دیگر برنگردند.

مثال فنی: علامت حذفِ هنوز لازم

وضعیت اولیه: فایل قدیمی P7=100 دارد و فایل تازه P7=DELETE.

اجرای مرحله‌به‌مرحله: خواندن علامت حذف را می‌بیند و رکورد را نشان نمی‌دهد؛ compaction فقط وقتی حذف علامت امن است که نسخهٔ قدیمیِ مربوط دیگر برنگردد.

نتیجه و دلیل: پاک کردن زودهنگام tombstone می‌تواند مقدار قدیمی را دوباره زنده کند.

A special delete record telling merges to throw away that key’s older values. Challenge: until the tombstone reaches the oldest segment, old versions still sit on disk — “definitive deletion” (e.g. privacy law) can take a while.

۱۰. ساختار B-tree را در چند جمله توضیح بده.10. Explain the B-tree structure in a few sentences.

B-tree درختی از صفحه‌های دارای کلید و اشاره‌گر است. هر سطح بازه را محدود می‌کند؛ برگ‌ها کلیدهای مرتب را نگه می‌دارند و محلِ رکورد به طراحیِ موتور بستگی دارد.

مثال فنی: پیمایشِ B-tree

وضعیت اولیه: ریشه کلید جداکنندهٔ 500 دارد و برگ راست شامل 700,740,780 است.

اجرای مرحله‌به‌مرحله: جست‌وجوی 740 با مقایسه در ریشه به فرزند راست و سپس برگ مربوط می‌رود.

نتیجه و دلیل: هر سطح بازه را محدود می‌کند؛ لازم نیست برای یک کلید همهٔ صفحه‌ها خوانده شوند.

Fixed-size pages (8–16 KB in common databases) referencing each other by page number. A root, internal pages with boundary keys, leaves with individual keys. A branching factor in the hundreds → 3–4 levels for large data; depth always O(log n).

۱۱. page split و WAL چه می‌کنند؟11. What do page splits and the WAL do?

Page split هنگامِ پر شدنِ صفحه، آن را تقسیم و راهنمای والد را اصلاح می‌کند. WAL تغییرهای لازم را برای بازیابیِ امنِ ساختار و داده ثبت می‌کند.

مثال فنی: Crash وسطِ split

وضعیت اولیه: دو صفحهٔ تازه ساخته شده‌اند، اما به‌روزرسانی والد هنوز روی دیسک نرفته است.

اجرای مرحله‌به‌مرحله: رکوردهای بازیابیِ موتور مشخص می‌کنند split چگونه تکمیل یا اصلاح شود؛ صرفِ نوشتن جداگانهٔ چند صفحه کافی نیست.

نتیجه و دلیل: ساختار درخت هم باید پس از crash معتبر بماند؛ ماندگاری فقط حفظ مقدار یک رکورد نیست.

When a page fills, it splits into two half-full pages and the parent is updated with the fresh boundary key (splits can climb to the root). Since these multi-page rewrites are crash-dangerous (orphaned or torn pages), every change is first written to the WAL with fsync, then applied to the page.

۱۲. مهم‌ترین تفاوت‌های B-tree و LSM چیست؟12. What are the key B-tree vs LSM differences?

B-tree معمولاً با مسیرِ صفحه‌ایِ مرتب می‌خواند؛ LSM تغییرها را در حافظه و فایل‌ها جمع و بعد ادغام می‌کند. هزینهٔ نوشتن، خواندن، فضا و تأخیرِ پس‌زمینه به بار و پیاده‌سازی وابسته است.

مثال فنی: بارِ نوشتن و خواندن

وضعیت اولیه: بار A عمدتاً ورود رویداد است؛ بار B عمدتاً lookup و تغییر رکوردهای کوچک.

اجرای مرحله‌به‌مرحله: LSM نوشتن‌ها را جمع می‌کند و هزینهٔ compaction می‌دهد؛ B-tree صفحه‌ها را به‌روز می‌کند و ساختار خواندنِ مرتب دارد.

نتیجه و دلیل: انتخاب با اندازه‌گیری write/read/space amplification و tail latency روی همان بار انجام می‌شود؛ برندهٔ ثابت نداریم.

Reads: B-trees are fast and predictable (one page per level) with excellent range queries; LSM may scan several segments and ranges cost more. Writes: LSM is sequential with usually lower write amplification. Space: B-trees fragment, LSM compresses better but stale versions linger until compaction. Deletion and snapshots behave differently too.

۱۳. write amplification چیست و چرا مهم است؟13. What is write amplification, and why does it matter?

افزایشِ نوشتن یعنی مقدارِ واقعیِ نوشته‌شده بر رسانه، از دادهٔ تازهٔ برنامه بیشتر باشد. لاگ، صفحه‌ها، ایندکس و ادغام در این هزینه نقش دارند.

مثال فنی: حسابِ write amplification

وضعیت اولیه: برنامه در یک بازه 1 GB دادهٔ تازه می‌نویسد، اما لاگ و compaction مجموعاً 6 GB روی رسانه می‌نویسند.

اجرای مرحله‌به‌مرحله: نسبت نوشتن واقعی به ورودی در این اندازه‌گیری 6× است؛ دامنهٔ اندازه‌گیری، از جمله WAL، باید مشخص باشد.

نتیجه و دلیل: توان و عمر رسانه از بایت واقعی اثر می‌گیرد؛ تنها نرخ دادهٔ ورودی کافی نیست.

The ratio of bytes actually written to disk vs data bytes (excluding indexes). In B-trees at least 2× (WAL + page, sometimes a whole page for a few bytes); in LSM per flush and compaction. In write-heavy workloads it caps write throughput and wears SSDs faster. Benchmarks must run long enough for compaction to join in.

۱۴. چرا SSD هم برای نوشتن ترتیبی سریع‌تر است؟14. Why are sequential writes faster even on SSDs?

فلش واحدهای خواندن و نوشتن و پاک‌کردنِ متفاوت دارد و کنترل‌کننده داده را جابه‌جا می‌کند. نوشتنِ ترتیبی می‌تواند کارِ مدیریتِ رسانه را کمتر کند، ولی اثرش به دستگاه و بار وابسته است.

مثال فنی: چرا نوشتن SSD رایگان نیست؟

وضعیت اولیه: برنامه بلوک‌های کوچک و پراکنده را مرتب بازنویسی می‌کند.

اجرای مرحله‌به‌مرحله: کنترل‌کننده برای پاک‌سازی بلوک‌های بزرگ‌تر ممکن است دادهٔ زنده را جابه‌جا کند؛ نوشتن منظم می‌تواند این فشار را کم کند.

نتیجه و دلیل: اثر به دستگاه، فضای آزاد و بار بستگی دارد؛ SSD داشتن هزینهٔ garbage collection و write amplification را حذف نمی‌کند.

Because flash erases only “block by block” (e.g. 512KB) and valid pages must first be relocated (garbage collection). Sequential writes keep blocks uniform and GC low; random writes pollute blocks, GC eats bandwidth and increases flash wear.

۱۵. clustered index، heap file و covering index چه تفاوتی دارند؟15. How do clustered, heap and covering indexes differ?

در ایندکسِ خوشه‌ای، داده با نظمِ ایندکس نگهداری می‌شود؛ در heap، ایندکس محلِ رکورد را معرفی می‌کند. ایندکسِ پوشاننده فیلدهای لازمِ پرس‌وجو را نیز دارد.

مثال فنی: رکورد پشت ایندکس کجاست؟

وضعیت اولیه: query شمارهٔ سفارش ۴۲ را پیدا می‌کند و مبلغش را می‌خواهد.

اجرای مرحله‌به‌مرحله: در heap، ایندکس می‌تواند محلِ ردیف را بدهد؛ در الگوی clustered، داده با ترتیب کلیدِ خوشه‌ای نگه داشته می‌شود؛ covering index خودِ مبلغ لازم را هم دارد.

نتیجه و دلیل: تعداد مراجعه‌ها و هزینهٔ تغییر رکورد متفاوت است؛ معنای دقیق clustered و سازمان صفحه‌ها را باید در همان موتور بررسی کرد.

Clustered: the record itself lives inside the index structure (e.g. InnoDB primary keys). Heap file: the index only references, records sit in an unordered file (e.g. PostgreSQL). Covering: some record columns also live inside the index so queries answer without touching the heap — faster, but more space and writes.

۱۶. چرا ذخیره‌سازی ستونی برای تحلیل بهتر است؟16. Why is columnar storage better for analytics?

تحلیل اغلب تعدادِ زیادی ردیف ولی چند ستون را می‌خواهد. چیدمانِ ستونی خواندنِ بخشِ لازم و فشرده‌سازیِ مقدارهای مشابه را آسان می‌کند.

مثال فنی: فقط مبلغ‌ها را بخوان

وضعیت اولیه: ردیف‌ها شامل id, address, description, amount هستند.

اجرای مرحله‌به‌مرحله: برای SUM(amount) موتور ستونی بلوک مبلغ را می‌خواند و نشانی و توضیح را کنار می‌گذارد.

نتیجه و دلیل: سود از کمتر خواندن می‌آید؛ اگر query همهٔ ستون‌های یک ردیف را بخواهد، همین مزیت الزاماً برقرار نیست.

Because an analytical query wants only 3–5 of 100+ columns; columnar layout reads just those. Columns repeat heavily and compress with bitmap + RLE; sorting whole rows builds long runs; predicates resolve with cheap bitwise AND/OR and vectorized execution.

۱۷. column-oriented با wide-column چه فرقی دارد؟17. How does column-oriented differ from wide-column?

Column-oriented روشِ چیدنِ فیزیکیِ داده برای خواندنِ ستون‌هاست. Wide-column یک مدلِ داده برای ردیف‌ها و خانواده‌های ستون است؛ شباهتِ نام به‌معنی رفتارِ یکسان نیست.

مثال فنی: ستونی با wide-column فرق دارد

وضعیت اولیه: یک انبار تحلیلی مبلغ همهٔ سفارش‌ها را ستونی نگه می‌دارد؛ سامانه‌ای دیگر کلید customer_id و ردیف‌های دارای ستون‌های متغیر دارد.

اجرای مرحله‌به‌مرحله: اولی چیدمان فیزیکی برای اسکن ستون است؛ دومی مدل wide-column برای دسترسی به ردیف‌ها و خانواده‌های ستون است.

نتیجه و دلیل: اشتراک واژهٔ column به‌معنی یکسان بودن الگوی query یا موتور ذخیره‌سازی نیست.

Wide-column (e.g. Bigtable, HBase) is a data model: each row may hold thousands of different columns, but its storage layout is row-oriented. Column-oriented is a storage layout: each column’s values side by side. Only the names are alike.

۱۸. تفاوت query compilation و vectorized processing؟18. Query compilation vs vectorized processing?

کامپایلِ پرس‌وجو بخشی از منطق را به کدِ اجرایی تبدیل می‌کند. پردازشِ برداری عملیات را روی دسته‌ای از مقدارها انجام می‌دهد؛ این دو روش می‌توانند کنارِ هم استفاده شوند.

مثال فنی: برداری در برابر کامپایل

وضعیت اولیه: query باید یک میلیون مبلغ را جمع کند.

اجرای مرحله‌به‌مرحله: اجرای برداری دسته‌های مثلاً ۱۰۲۴تایی را به عملگر می‌دهد؛ کامپایل، منطق query را به کد اجرایی تخصصی تبدیل می‌کند.

نتیجه و دلیل: یکی شکل پردازشِ دسته‌ایِ مقدارها و دیگری روش اجرای منطق است؛ می‌توان هر دو را هم‌زمان داشت.

Compilation: generate executable code from SQL and compile it (e.g. with LLVM) — like JIT. Vectorized: stays interpreted, but operators work over large batches of values (e.g. one column’s equality → one bitmap, combined with bitwise AND). Both exploit sequential access, tight loops, SIMD and work over compressed data.

۱۹. inverted index چیست و چطور «سیب قرمز» را جواب می‌دهد؟19. What is an inverted index — how does it answer “red apples”?

ایندکسِ معکوس هر واژه را به سندهای دارای آن وصل می‌کند. برای وجودِ هر دو واژه، فهرست‌ها مشترک‌گیری می‌شوند؛ برای عبارتِ دقیق، موقعیتِ واژه‌ها هم لازم است.

مثال فنی: تقاطع فهرست اسناد

وضعیت اولیه: فهرست سیب برابر [1,3,7] و قرمز برابر [3,5,7] است.

اجرای مرحله‌به‌مرحله: اشتراک فهرست‌ها سندهای 3,7 را می‌دهد؛ برای عبارت دقیق باید موقعیت واژه‌ها هم پشت‌سرهم باشد.

نتیجه و دلیل: داشتن هر دو واژه با داشتن عبارت «سیب قرمز» یکسان نیست؛ ایندکس باید اطلاعات لازم همان سؤال را نگه دارد.

A map from each word to the IDs of documents containing it (postings list). “Red apples” means fetching both words’ postings and intersecting (AND-ing) them bitwise — since the n-th bit of both points at the same document. Lucene (the heart of Elasticsearch/Solr) and PostgreSQL’s GIN type work this way.

۲۰. IVF و HNSW چه تفاوتی با هم دارند و «تقریبی» یعنی چه؟20. How do IVF and HNSW differ — what does “approximate” mean?

IVF جست‌وجو را به چند خوشه محدود می‌کند؛ HNSW روی گرافِ همسایگی مسیرِ جست‌وجو می‌سازد. تقریبی بودن یعنی ممکن است نزدیک‌ترین نتیجهٔ واقعی پیدا نشود؛ سرعت و دقت تنظیم می‌شوند.

مثال فنی: جست‌وجوی تقریبی بردار

وضعیت اولیه: query برداری نزدیک به محصول P7 است، اما فقط چند خوشه یا بخشی از گراف بررسی می‌شود.

اجرای مرحله‌به‌مرحله: IVF خوشه‌های منتخب را می‌کاود؛ HNSW مسیر همسایه‌ها را روی گراف طی می‌کند؛ گسترش جست‌وجو معمولاً کار بیشتری می‌خواهد.

نتیجه و دلیل: ممکن است نزدیک‌ترین همسایهٔ واقعی جا بماند؛ recall باید با جست‌وجوی دقیق روی نمونهٔ مرجع سنجیده شود.

Flat scans everything (exact, slow). IVF splits space into clusters and scans only a few (probes). HNSW is a layered proximity graph guiding from the sparse top layer to the dense bottom. Both are approximate: they may miss the nearest document; more probes/layers = more accuracy, less speed.

۹.۳ اشتباهات رایج در جواب‌های مصاحبه9.3 Common interview mistakes

  • یکی دانستنِ «NoSQL با LSM» و «SQL با B-tree» — موتور ذخیره‌سازی از مدل داده جداست؛ موتورهای رابطه‌ای با ساختارهای گوناگون هم می‌آیند.
  • فکر کردن به Bloom filter به‌عنوان جوابِ قطعی؛ ماهیتش تقریبی است (false positive) و فقط «نیست» گفتنش قطعی است.
  • نادیده گرفتن هزینهٔ ایندکس روی نوشتن؛ تعریفِ چند ایندکس بدون فکر به الگوی نوشتن، بازده نوشتن را خفه می‌کند.
  • قضاوت مطلق دربارهٔ «B-tree بهتر است یا LSM»؛ بدون بارکاریِ مشخص و بنچ‌مارکِ طولانی (با compaction واقعی) ادعایی نمی‌توان کرد.
  • گیج شدنِ column-oriented با wide-column، یا vectorized processing با vector embedding.
  • تلقیِ IVF و HNSW به‌عنوان جست‌وجوی دقیق؛ هر دو تقریبی‌اند و recall را با latency مبادله می‌کنند.
  • فراموش کردن اینکه range query از Bloom filter بهره‌ای نمی‌برد و در LSM گران‌تر از نقطه‌ای است.
  • توضیح ندادنِ مسئلهٔ write amplification وقتی از بازده نوشتنِ LSM صحبت می‌شود.
  • Equating “NoSQL with LSM” and “SQL with B-trees” — storage engines are separate from data models; relational engines ship with varied structures too.
  • Treating the Bloom filter as a definitive answer; it is approximate by nature (false positives), and only its “absent” verdict is certain.
  • Ignoring index cost on writes; defining several indexes without thinking about the write pattern chokes write throughput.
  • Absolute verdicts on “B-tree vs LSM”; without a concrete workload and a long benchmark (with real compaction) no claim can be made.
  • Mixing up column-oriented with wide-column, or vectorized processing with vector embeddings.
  • Treating IVF and HNSW as exact search; both are approximate and trade recall against latency.
  • Forgetting that range queries gain nothing from Bloom filters and cost more than point reads in LSM.
  • Not explaining write amplification when discussing LSM write throughput.

۹.۴ چک‌لیست مرور ۵ دقیقه‌ای قبل از مصاحبه9.4 Five-minute pre-interview checklist

  • تریدآفِ اصلیِ ایندکس: خواندن سریع‌تر / نوشتن کندتر / فضای بیشتر.
  • مسیر نوشتن و خواندن در LSM را از حفظ بگو: WAL → memtable → flush → سگمنت‌ها → compaction؛ و جست‌وجو: memtable → جدید به قدیم + Bloom filter.
  • ساختار B-tree و دلیل WAL و page split را در ۳ جمله.
  • سه تفاوتِ محوریِ B-tree و LSM (خواندن/نوشتن/فضا) + مفهوم write amplification.
  • سه الگوی جای دادن رکورد (clustered، heap، covering) و یک جمله دربارهٔ ستونی: column pruning و bitmap.
  • ایندکس‌های پیشرفته در یک جمله هرکدام: R-tree، inverted index، IVF/HNSW — و یادآوریِ «تقریبی» بودنِ برداری‌ها.
  1. The core index trade-off: faster reads / slower writes / more space.
  2. Recite the LSM write and read paths: WAL → memtable → flush → segments → compaction; lookups: memtable → newest-to-oldest + Bloom filter.
  3. The B-tree structure and why WAL and page splits exist, in 3 sentences.
  4. Three core B-tree vs LSM differences (reads/writes/space) + write amplification.
  5. The three record-placement patterns (clustered, heap, covering) and one sentence on columnar: column pruning and bitmaps.
  6. Advanced indexes in one sentence each: R-tree, inverted index, IVF/HNSW — and the “approximate” reminder for vectors.

۱۰10تمرین‌های مصاحبه و آزمایشگاهInterview exercises and hands-on labs

این تمرین‌ها برای مسیر Backend / Distributed Systems در ۱۴ اکتبر ۲۰۲۶ بازبینی شدند. سطح مربوط به نسخهٔ مصاحبه‌ای است؛ زمان آزمایشگاه جداست.Reviewed on 14 October 2026 for Backend / Distributed Systems. Level refers to the interview brief; full-lab time is separate. روش ارزیابی و مسیر مطالعهMethod and study path

تمرین ۱ — موتور کلید-مقدار log-structuredExercise 1 — a log-structured key-value engine

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific45 دقیقه بحث45 min discussion

LSM/WAL برای storage/platform مفید است؛ ساخت موتور کامل و یک میلیون benchmark تمرین چندساعته است، نه سؤال معمول جلسه.A substantial storage lab rather than a normal live interview task.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

برای workload نوشتن‌محور با point read و range read، LSM و B-tree را مقایسه کن. مسیر write، WAL، memtable، SSTable، compaction و tombstone را رسم کن. توضیح بده crash بین flush و ثبت manifest چه می‌کند و چه زمانی write/read/space amplification بالا می‌رود.Compare LSM and B-tree for write-heavy point/range reads. Draw WAL, memtable, SSTable and compaction. Explain crash recovery, manifests, tombstones and write/read/space amplification.

معیار پاسخ خوبWhat a good answer demonstrates

  • durability را از index حافظه جدا می‌کندDurability versus RAM indexing
  • نسخهٔ جدید و tombstone درست انتخاب می‌شوندLatest versions and tombstones
  • benchmark دادهٔ خام، fsync و cache را مشخص می‌کندDefined fsync/cache benchmark conditions

پیگیری مصاحبه:Interview follow-up: حذف tombstone هنگام compaction چه زمانی رکورد قدیمی را زنده می‌کند؟When can dropping a tombstone during compaction resurrect an old value?

آزمایشگاه تکمیلی · زمان تقریبی ۶–۱۲ ساعتExtended hands-on laboratory · estimated 6–12 hours
با همان دو تابعِ bash بخش ۱ شروع کن و قدم‌به‌قدم موتورت را کامل کن: اول hash index در حافظه با بازسازی بعد از restart؛ بعد memtable با flush به فایلِ مرتب (SSTable ساده) و خواندنِ جدیدبه‌قدیم؛ بعد merge دو سگمنت با نگه‌داشتنِ تازه‌ترین مقدار و tombstone برای حذف. در پایان یک آزمایش بنویس: یک میلیون نوشتنِ تصادفی، اندازه‌گیریِ مجموعِ بایت‌های نوشته‌شده روی دیسک تقسیم‌بر دادهٔ خام (write amplification)، و مقایسهٔ زمانِ ۱۰ هزار خواندنِ نقطه‌ای در برابر یک range query.

Start from section 1’s two bash functions and complete your engine step by step: first an in-memory hash index with post-restart rebuild; then a memtable flushing to a sorted file (a simple SSTable) with newest-to-oldest reads; then merging two segments keeping the newest value, with tombstones for deletes. Finish with a test: a million random writes, measuring total disk bytes over raw data (write amplification), and comparing 10k point reads against one range query.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Microsoft · technical interviewing · Atlassian · engineering interview handbook

تمرین ۲ — انباردادهٔ ستونی از صفرExercise 2 — a columnar warehouse from scratch

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific40 دقیقه بحث40 min discussion

column pruning، partition pruning و star schema برای data engineering مناسب‌اند؛ RLE دستی ارزش مصاحبه‌ای کمتری دارد.Strong for data engineering; manual RLE is an optional depth exercise.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

گزارش فروش هر دسته‌بندی به تفکیک روز هفته را روی fact فروش و dimension محصول و تاریخ طراحی کن. SQL، partition key، sort key و ستون‌های خوانده‌شده را مشخص کن. اثر skew، cardinality، bitmap و تغییر دسته‌بندی محصول در تاریخ را تحلیل کن.Design daily category sales analytics with fact and dimension tables. Provide SQL, partition/sort keys and read columns. Explain pruning, skew, bitmap compression and historical category semantics.

معیار پاسخ خوبWhat a good answer demonstrates

  • pruning را با خواندن کمتر ستون تفکیک می‌کندColumn versus block pruning
  • تعریف تاریخی category روشن استHistorical category semantics
  • compression را با cardinality و order توجیه می‌کندCompression under stated cardinality

پیگیری مصاحبه:Interview follow-up: آیا فروش سال گذشته باید با دسته‌بندی امروز گزارش شود یا دسته‌بندی زمان فروش؟Should last year’s sales use today’s category or the category at the time of sale?

آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours
یک جدولِ فروش (تاریخ، محصول، فروشگاه، تعداد، مبلغ) و دو جدولِ بعدِ تاریخ و محصول را طراحی کن. برای پرس‌وجوی «مجموع فروش هر دسته‌بندی به‌تفکیک روزِ هفته در سال گذشته»: کلیدهای مرتب‌سازی منطقی برای فایل‌های ستونی انتخاب کن، کوئری SQL را بنویس، و بگو دقیقاً کدام ستون‌ها و کدام بلوک‌ها خوانده می‌شوند. سپس ستونِ «دسته‌بندی محصول» (با ۵ مقدار متمایز) را دستی bitmap کدگذاری و فشرده‌سازی run-length کن و فیلترِ WHERE category = A AND weekday = 2 را با عملیات بیتی اجرا کن.

Design a sales fact table (date, product, store, quantity, amount) plus date and product dimension tables. For the query “last year’s total sales per category by weekday”: pick sensible sort keys for the columnar files, write the SQL, and say exactly which columns and blocks get read. Then hand-encode the “product category” column (5 distinct values) with bitmaps and run-length compression, and execute WHERE category = A AND weekday = 2 with bitwise ops.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Microsoft · technical interviewing · Atlassian · engineering interview handbook

تمرین ۳ — ایندکس جست‌وجو: از متن تا بردارExercise 3 — a search index: from text to vectors

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific45 دقیقه بحث45 min discussion

الگوی /appl./ دو trigram ثابت app و ppl دارد؛ اشتراک postingها فقط نامزد می‌سازد و regex نتیجه را تأیید می‌کند. recall@k با exact top-k مقایسه می‌شود.The pattern /appl./ requires app and ppl; intersect their postings to find candidates, then verify the regex. Recall@k compares approximate results with exact top-k.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

جست‌وجوی محصول را با inverted index، فیلتر و vector index طراحی کن. برای red AND apples اشتراک postings را توضیح بده. برای regex /appl./ ابتدا candidates با trigram و سپس تأیید regex را انجام بده. در IVF با nprobe=1، recall@k را نسبت به exact top-k و p95 latency بسنج؛ hybrid ranking و جداسازی tenant را هم تحلیل کن.Design lexical and vector product retrieval. Intersect postings for red AND apples; filter /appl./ by trigram then verify the regex. Evaluate IVF using exact top-k recall@k and p95 latency, and discuss hybrid ranking and tenant isolation.

معیار پاسخ خوبWhat a good answer demonstrates

  • candidate filtering را با match قطعی اشتباه نمی‌گیردCandidates versus verified matches
  • ANN ground truth و recall@k درست تعریف شدهExact recall ground truth
  • freshness، ACL و هزینه در retrieval دیده می‌شوندFreshness and authorization

پیگیری مصاحبه:Interview follow-up: اگر regex هیچ trigram ثابت نداشته باشد یا filter بسیار محدود باشد چه مسیر جایگزینی داری؟What fallback handles a regex without any fixed trigram, or a highly selective filter?

آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours

برای ۱۱ سند کوتاه یک inverted index بساز و red AND apples را با اشتراک postingها پاسخ بده. trigram index را با substringهای سه‌حرفی همان متن بساز؛ برای regex بدون حساسیت اضافهٔ زبانی /appl./ از app و ppl نامزد بگیر و سپس خود regex را روی نامزدها اجرا کن. یک نامزد کاذب مثل app ... ppl اضافه کن تا ضرورت تأیید روشن شود. برای IVF، بردارهای ۱۰۰۰بعدی و پنج خوشه صرفاً سناریوی فکری‌اند؛ در آزمایش کوچک می‌توان بردار مصنوعی ساده‌تر با برچسب روشن داشت. exact top-k، نحوهٔ فاصله و tie-breaking را ثابت کن؛ recall@k = |ANN_k ∩ exact_k|/k و p95 زمان پرس‌وجو را برای چند nprobe بسنج. از ۱۱ سند نتیجهٔ کارایی production نگیر؛ محدودیت tenant را پیش از retrieval اعمال کن.

Build an inverted index for 11 short documents and answer red AND apples by intersecting postings. Build trigrams from the same text. For the plain regex /appl./, intersect app and ppl postings, then run the regex on every candidate. Include a false candidate such as app ... ppl to demonstrate verification. Treat 1000-D vectors in five IVF clusters as a thought experiment; label any smaller synthetic demo explicitly. Fix distance and tie-breaking, compute exact top-k, and measure recall@k = |ANN_k ∩ exact_k|/k and p95 query time for several nprobe values. Eleven documents cannot establish production performance. Apply tenant restrictions before retrieval.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Atlassian · engineering interview handbook · Pinecone · production checklist