HardТеория6 min

Внутренности баз данных

B-tree, LSM-tree, WAL, buffer pool, query optimizer -- как работают БД изнутри

Зачем знать внутренности

Понимание внутренних механизмов БД позволяет:

  • Проектировать эффективные схемы и запросы
  • Диагностировать проблемы производительности
  • Выбирать правильную БД для задачи
  • Принимать обоснованные решения на System Design интервью

B-tree

B-tree (и его вариант B+ tree) -- основная структура данных для индексов в реляционных БД (PostgreSQL, MySQL).

Структура B+ tree

                    ┌─────────────┐
                    │   [30, 70]  │  Root
                    └──┬───┬───┬──┘
                       │   │   │
          ┌────────────┘   │   └────────────┐
          ▼                ▼                ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │ [10, 20] │    │ [40, 50] │    │ [80, 90] │  Internal
    └─┬──┬──┬──┘    └─┬──┬──┬──┘    └─┬──┬──┬──┘  Nodes
      │  │  │         │  │  │         │  │  │
      ▼  ▼  ▼         ▼  ▼  ▼         ▼  ▼  ▼
    [5,8][12,15][22,28][35,38][45,48][55,65][75,78][85,88][92,99]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        Leaf Nodes (data pointers)
                        Linked list ←→ for range scans

Свойства B+ tree

Свойство Описание
Сбалансированность Все листья на одном уровне
Ветвление Сотни ключей на узел (fanout)
Глубина Обычно 3-4 уровня для миллионов строк
Leaf links Листья связаны для range scan
Lookup O(log_B N) -- 3-4 I/O для миллионов строк
<?php

declare(strict_types=1);

/**
 * B-tree behavior impact on query performance
 */
final class BTreeBehavior
{
    public function __construct(
        private readonly \PDO $db,
    ) {}

    /**
     * B-tree efficient: equality and range on indexed column
     * PostgreSQL does 3-4 page reads to find the row
     */
    public function efficientBTreeQuery(): void
    {
        // Point lookup: O(log N) -- 3-4 I/O
        $this->db->query("SELECT * FROM users WHERE id = 12345");

        // Range scan: find start in O(log N), then follow leaf links
        $this->db->query("SELECT * FROM users WHERE id BETWEEN 100 AND 200");

        // Prefix search uses B-tree
        $this->db->query("SELECT * FROM users WHERE name LIKE 'John%'");
    }

    /**
     * B-tree INefficient: these cannot use standard B-tree index
     */
    public function inefficientBTreeQuery(): void
    {
        // Function on indexed column: can't use index
        $this->db->query("SELECT * FROM users WHERE LOWER(email) = '[email protected]'");
        // Fix: functional index CREATE INDEX idx ON users (LOWER(email))

        // Leading wildcard: can't use B-tree
        $this->db->query("SELECT * FROM users WHERE name LIKE '%john%'");
        // Fix: use GIN index with trigrams or full-text search

        // OR with different columns: usually seq scan
        $this->db->query("SELECT * FROM users WHERE name = 'John' OR email = '[email protected]'");
        // Fix: use UNION or bitmap index scan
    }

    /**
     * Composite index: column order matters!
     */
    public function compositeIndexBehavior(): void
    {
        // Index: (country, city, zip_code)

        // Uses full index (leftmost prefix)
        $this->db->query("SELECT * FROM addresses WHERE country = 'RU' AND city = 'Moscow' AND zip_code = '101000'");

        // Uses first 2 columns of index
        $this->db->query("SELECT * FROM addresses WHERE country = 'RU' AND city = 'Moscow'");

        // Uses first column only
        $this->db->query("SELECT * FROM addresses WHERE country = 'RU'");

        // CANNOT use index (skips leftmost column)
        $this->db->query("SELECT * FROM addresses WHERE city = 'Moscow'");
    }
}

LSM-tree (Log-Structured Merge-tree)

LSM-tree используется в Cassandra, RocksDB, LevelDB, ClickHouse. Оптимизирован для записи.

Как работает LSM-tree

Write path:

1. Write to WAL (disk) ─────────────────────────> Crash safety
2. Write to MemTable (RAM, sorted) ─────────────> Fast writes
3. When MemTable full → flush to SSTable (disk) ─> Sorted file
4. Background compaction merges SSTables ────────> Optimization

Read path:

1. Check MemTable (RAM) ─────> Found? Return
2. Check Bloom filters ──────> Skip SSTables that don't have key
3. Check SSTables (newest first) → Read from disk
┌────────────┐
│  MemTable  │  (RAM, sorted)
│  (Red-Black│
│   Tree)    │
└─────┬──────┘
      │ flush when full
      ▼
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  SSTable L0 │  │  SSTable L0 │  │  SSTable L0 │  Level 0
│  (sorted)   │  │  (sorted)   │  │  (sorted)   │  (may overlap)
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       │                │                │
       └────────────────┼────────────────┘
                        │ compaction
                        ▼
       ┌─────────────────────────────────┐
       │         SSTable Level 1         │  Level 1
       │  (sorted, non-overlapping)      │  (no overlaps)
       └────────────────┬────────────────┘
                        │ compaction
                        ▼
       ┌─────────────────────────────────┐
       │         SSTable Level 2         │  Level 2
       │  (sorted, non-overlapping)      │  (larger)
       └─────────────────────────────────┘

B-tree vs LSM-tree

Критерий B-tree LSM-tree
Write Обновление на месте (in-place) Append-only (sequential)
Read O(log N), предсказуемо Может читать несколько SSTables
Write amplification Средняя Высокая (compaction)
Read amplification Низкая Может быть высокая
Space amplification Низкая Может быть высокая (до compact)
Оптимизирован для Reads Writes
Используется в PostgreSQL, MySQL Cassandra, RocksDB, ClickHouse

WAL (Write-Ahead Log)

WAL обеспечивает durability: сначала записываем изменение в журнал, потом применяем к данным.

Transaction: UPDATE balance = 1000 WHERE id = 5

Step 1: Write to WAL (sequential write, fast)
        ┌─────────────────────────────────────┐
   WAL: │ LSN=100: UPDATE users SET balance=  │
        │ 1000 WHERE id=5 [tx_id=42]          │
        └─────────────────────────────────────┘

Step 2: Update in Buffer Pool (RAM)
        Memory page for users table updated

Step 3: Eventually flush dirty page to disk
        (checkpoint)

Crash Recovery:
  1. Read WAL from last checkpoint
  2. Replay committed transactions
  3. Undo uncommitted transactions
<?php

declare(strict_types=1);

/**
 * Understanding WAL impact on application design
 */
final class WalAwareness
{
    public function __construct(
        private readonly \PDO $db,
    ) {}

    /**
     * synchronous_commit controls WAL durability
     */
    public function configureDurability(): void
    {
        // Maximum durability (default): WAL flushed to disk on commit
        $this->db->exec("SET synchronous_commit = 'on'");

        // Faster commits, but last few ms of data may be lost on crash
        // Good for: logging, analytics, non-critical data
        $this->db->exec("SET synchronous_commit = 'off'");
    }

    /**
     * Batch operations reduce WAL overhead
     */
    public function batchInsert(array $records): void
    {
        // Bad: 1000 individual commits = 1000 WAL flushes
        // Good: 1 transaction = 1 WAL flush
        $this->db->beginTransaction();

        $stmt = $this->db->prepare(
            'INSERT INTO logs (message, level, created_at) VALUES (:msg, :level, NOW())'
        );

        foreach ($records as $record) {
            $stmt->execute(['msg' => $record['message'], 'level' => $record['level']]);
        }

        $this->db->commit(); // Single WAL flush for all inserts
    }
}

Buffer Pool

Buffer Pool -- область памяти, где БД кэширует страницы данных и индексов:

┌─────────────────────────────────────┐
│           Buffer Pool (RAM)         │
│                                     │
│  ┌──────┐ ┌──────┐ ┌──────┐       │
│  │Page 1│ │Page 5│ │Page 9│  ...   │  Hot pages
│  │(clean)│ │(dirty)│ │(clean)│       │  cached
│  └──────┘ └──────┘ └──────┘       │
│                                     │
│  LRU / Clock replacement policy    │
│  Hit ratio target: > 99%           │
└─────────────────────────────────────┘
         ↕ read/write
┌─────────────────────────────────────┐
│              Disk                   │
│  [Page 1][Page 2][Page 3]...       │
└─────────────────────────────────────┘
<?php

declare(strict_types=1);

/**
 * Monitor buffer pool performance
 */
final class BufferPoolMonitor
{
    public function __construct(
        private readonly \PDO $db,
    ) {}

    /**
     * PostgreSQL: check buffer cache hit ratio
     */
    public function getHitRatio(): array
    {
        return $this->db->query(<<<SQL
            SELECT
                datname,
                blks_hit,
                blks_read,
                CASE WHEN blks_hit + blks_read = 0 THEN 0
                     ELSE round(blks_hit::numeric / (blks_hit + blks_read) * 100, 2)
                END as hit_ratio_pct
            FROM pg_stat_database
            WHERE datname = current_database()
        SQL)->fetch(\PDO::FETCH_ASSOC);
        // Target: > 99% hit ratio
    }

    /**
     * Find tables that don't fit in buffer pool
     */
    public function getTableCacheStats(): array
    {
        return $this->db->query(<<<SQL
            SELECT
                relname as table_name,
                pg_size_pretty(pg_relation_size(relid)) as table_size,
                heap_blks_hit as cache_hits,
                heap_blks_read as disk_reads,
                CASE WHEN heap_blks_hit + heap_blks_read = 0 THEN 0
                     ELSE round(heap_blks_hit::numeric / (heap_blks_hit + heap_blks_read) * 100, 2)
                END as hit_ratio_pct
            FROM pg_statio_user_tables
            ORDER BY heap_blks_read DESC
            LIMIT 20
        SQL)->fetchAll(\PDO::FETCH_ASSOC);
    }
}

Query Optimizer

Query optimizer выбирает оптимальный план выполнения запроса из множества возможных:

Этапы оптимизации

SQL Query
    │
    ▼
┌──────────┐
│  Parser  │  → AST (Abstract Syntax Tree)
└────┬─────┘
     │
     ▼
┌──────────┐
│ Rewriter │  → Simplification, view expansion
└────┬─────┘
     │
     ▼
┌──────────┐
│Optimizer │  → Cost-based optimization
│          │    - Table statistics
│          │    - Index availability
│          │    - Join ordering
│          │    - Access method selection
└────┬─────┘
     │
     ▼
┌──────────┐
│ Executor │  → Execute optimal plan
└──────────┘
<?php

declare(strict_types=1);

/**
 * Working with query optimizer
 */
final class QueryOptimizerInsights
{
    public function __construct(
        private readonly \PDO $db,
    ) {}

    /**
     * EXPLAIN ANALYZE: see actual execution plan
     */
    public function analyzeQuery(string $sql): array
    {
        $result = $this->db->query(
            "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) " . $sql
        )->fetchColumn();

        $plan = json_decode($result, true);

        return [
            'plan' => $plan[0]['Plan'],
            'execution_time_ms' => $plan[0]['Execution Time'],
            'planning_time_ms' => $plan[0]['Planning Time'],
        ];
    }

    /**
     * Statistics: optimizer needs fresh stats
     */
    public function updateStatistics(string $tableName): void
    {
        // Analyze updates table statistics for optimizer
        $this->db->exec("ANALYZE {$tableName}");
    }

    /**
     * Common performance issues and fixes
     */
    public function commonIssues(): array
    {
        return [
            'seq_scan_on_large_table' => [
                'cause' => 'Missing index or outdated statistics',
                'fix' => 'CREATE INDEX or ANALYZE table',
            ],
            'nested_loop_on_large_join' => [
                'cause' => 'Missing index on join column',
                'fix' => 'CREATE INDEX on FK columns',
            ],
            'sort_on_disk' => [
                'cause' => 'work_mem too small for sort',
                'fix' => 'Increase work_mem or add covering index',
            ],
            'bitmap_heap_scan_lossy' => [
                'cause' => 'work_mem too small for bitmap',
                'fix' => 'Increase work_mem',
            ],
        ];
    }
}

Итоги

  • B-tree -- сбалансированное дерево для быстрых reads, используется в PostgreSQL/MySQL
  • LSM-tree -- append-only структура для быстрых writes, используется в Cassandra/RocksDB
  • WAL -- журнал предзаписи, обеспечивает durability через sequential writes
  • Buffer Pool -- кэш страниц в памяти, целевой hit ratio > 99%
  • Query Optimizer -- выбирает план на основе статистики; EXPLAIN ANALYZE -- инструмент диагностики
  • Понимание внутренностей помогает проектировать эффективные схемы и диагностировать проблемы