B-tree کلیدها را در ساختاری مرتب و چندسطحی نگه میدارد. هر سطح بازهٔ جستوجو را کوچکتر میکند تا بدون پیمایش همهٔ رکوردها به کلید برسیم.
مثال فنی: پیمایشِ B-tree
وضعیت اولیه: ریشه کلید جداکنندهٔ 500 دارد و برگ راست شامل 700,740,780 است.
اجرای مرحلهبهمرحله: جستوجوی 740 با مقایسه در ریشه به فرزند راست و سپس برگ مربوط میرود.
نتیجه و دلیل: هر سطح بازه را محدود میکند؛ لازم نیست برای یک کلید همهٔ صفحهها خوانده شوند.
صفحه واحدی از داده است که موتور از دیسک میخواند یا مینویسد. در B-tree، هر صفحه چند کلید و اشارهگر دارد؛ بنابراین یک خواندن میتواند اطلاعات زیادی برای انتخاب مسیر بدهد.
مثال فنی: صفحهٔ ثابت، تغییر کوچک
وضعیت اولیه: موتورِ فرضی صفحههای 8 KB دارد و رکورد هدف در یک صفحه است.
اجرای مرحلهبهمرحله: تغییر چند بایت ممکن است ثبت WAL و dirty شدن همان صفحه را لازم کند؛ flush با واحدهای صفحه انجام میشود.
نتیجه و دلیل: اندازهٔ تغییر برنامه لزوماً برابر بایت نوشتهشده روی دیسک نیست؛ cache و WAL در مسیر دخالت دارند.
وقتی صفحهٔ B-tree دیگر برای کلید تازه جا نداشته باشد، تقسیم میشود. کلیدهای آن میان دو صفحه پخش میشوند و راهنمای سطح بالاتر هم بهروز میشود.
مثال فنی: تقسیم برگِ پُر
وضعیت اولیه: برگ ظرفیت سه کلید دارد و شامل 10,20,30 است؛ کلید 25 میرسد.
اجرای مرحلهبهمرحله: موتور برگ را به دو بخش تقسیم و جداکنندهٔ مناسب را در والد ثبت میکند.
نتیجه و دلیل: اگر والد هم پر باشد تقسیم بالا میرود؛ خواننده باید پس از پایان، همهٔ کلیدها را از مسیر درست پیدا کند.
روایت مدرس از نمودار ۲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.
برای بازیابی بعد از خرابی، موتور باید بتواند تغییراتِ صفحات را درست بازسازی کند. WAL تغییر را پیش از اعمالِ پایدارِ آن روی صفحات ثبت میکند؛ قفلهای داخلی نیز تغییر همزمانِ ساختار را کنترل میکنند.
مثال فنی: لاگ پیش از صفحهٔ داده
وضعیت اولیه: تغییر P7=120 ابتدا در WAL ثبت و طبق تنظیم ماندگاری پایدار میشود؛ صفحه هنوز روی دیسک قدیمی است.
اجرای مرحلهبهمرحله: برق قطع میشود؛ هنگام restart موتور با لاگ، تغییرهای لازم را redo میکند.
نتیجه و دلیل: WAL امکان بازیابیِ تغییر تأییدشده را میدهد؛ تنظیمی که تأیید را پیش از پایداری میدهد تضمین ضعیفتری دارد.
ثبتِ تغییر، پایدار کردن لاگ و نوشتن صفحهها لزوماً همزمان نیستند. مهم است تأییدِ موفقیت، با تضمین ماندگاریِ انتخابشده سازگار باشد.
مثال فنی: Crash وسطِ split
وضعیت اولیه: دو صفحهٔ تازه ساخته شدهاند، اما بهروزرسانی والد هنوز روی دیسک نرفته است.
اجرای مرحلهبهمرحله: رکوردهای بازیابیِ موتور مشخص میکنند split چگونه تکمیل یا اصلاح شود؛ صرفِ نوشتن جداگانهٔ چند صفحه کافی نیست.
نتیجه و دلیل: ساختار درخت هم باید پس از crash معتبر بماند؛ ماندگاری فقط حفظ مقدار یک رکورد نیست.
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.
روایت مدرس از نمودار ۶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.