هذا المقال للمستوى المحترف. بيفترض إنك شغلت PostgreSQL و RocksDB أو LevelDB في إنتاج فعلي، وعارف يعني إيه WAL، compaction، و write amplification.
B-tree vs LSM-tree: ليه PostgreSQL و RocksDB بيدّوا أرقام كتابة مختلفة على نفس SSD
لو شغّلت PostgreSQL 16 و RocksDB 8.10 على نفس Samsung 990 Pro NVMe، PostgreSQL بيكتب 78MB/s sustained، RocksDB بيكتب 340MB/s. الفرق 4.3x مش في تحسين الكود — الفرق في data structure مختلفة بالكلية. ركز في الفرق ده قبل ما تختار engine لأي workload write-heavy.
المشكلة باختصار
اختيار database engine بدون فهم الـ write path الداخلي بيخلّيك تختار B-tree لـ workload write-heavy، فتدفع 4x SSD، أو تختار LSM-tree لـ workload read-heavy فترفع P99 من 2ms لـ 38ms. القرار ده بيتم مرة واحدة في بداية المشروع، بس الثمن بتدفعه طول عمر النظام.
تشبيه يقرّب الفكرة قبل التعريف العلمي
تخيّل موظفين بيستلموا أوراق طوال اليوم، كل واحد بطريقته:
- موظف B-tree: كل ورقة تيجي، يفتح الملف الصح، يلاقي مكانها بالأبجدية، يدخّلها مكانها بالظبط. لو الملف اتلخبط من كتر التعديلات، يعيد ترتيبه قبل ما يقفل الدرج. سريع لما تيجي تدوّر على ورقة، بطيء وقت الإدخال علشان في ترتيب لحظي مع كل ورقة.
- موظف LSM-tree: كل ورقة تيجي، يكتبها في دفتر يومي بالترتيب اللي وصلت بيه. آخر اليوم، يقعد ساعة يدمج كل الدفاتر القديمة في ملف واحد منظّم. سريع جداً ساعة الإدخال، بس بطيء أحياناً لما يدوّر على ورقة قديمة في كذا دفتر مفتوح.
ده بالظبط الفرق في الـ write path. B-tree بيكتب in-place فبيعمل random I/O. LSM بيكتب append-only فبيعمل sequential I/O. على SSD حديث، الـ sequential write أسرع 4-5x من الـ random write نفسه، ودي مصدر الفرق في الأرقام اللي ابتدينا بيها المقال.
التعريف العلمي الدقيق
B-tree: شجرة متوازنة، كل عقدة فيها مجموعة keys مرتّبة و pointers لـ children. الكتابة بتمشي من الـ root للـ leaf الصح، تعدّل الصفحة في مكانها على disk، بعدين الـ Write-Ahead Log بيُكتب وبيتحط fsync. لو الصفحة امتلت، بتنقسم وبيتمّ rebalancing. المرجع الأصلي: ورقة Bayer & McCreight (1972) "Organization and Maintenance of Large Ordered Indexes". التطبيق العملي في PostgreSQL هو B+tree (variant بيحط الـ data في الـ leaves بس).
LSM-tree: هيكل متعدد الطبقات. الكتابة بتدخل في memtable داخل RAM (typically skip-list)، لما يتلخّم memtable بحجم محدد، بيتفلش لـ SSTable على disk. الـ SSTables بتتجمّع في levels (L0, L1, L2…)، وعملية compaction خلفية بتدمج SSTables من level لـ level أعلى. الورقة الأصلية: O'Neil et al. (1996) "The Log-Structured Merge-Tree".
الأرقام المقاسة على نفس الـ workload
Benchmark على سيرفر Hetzner AX52 (Ryzen 7 7700، 64GB RAM، Samsung 990 Pro 2TB)، بيانات 100M record، حجم 256 byte لكل واحد، 32 thread: