Skip to content

perf: Storage and Executor optimizations for SQLite-parity#20

Merged
poyrazK merged 7 commits intomainfrom
perf/sqlite-speed-parity
Apr 9, 2026
Merged

perf: Storage and Executor optimizations for SQLite-parity#20
poyrazK merged 7 commits intomainfrom
perf/sqlite-speed-parity

Conversation

@poyrazK
Copy link
Copy Markdown
Owner

@poyrazK poyrazK commented Apr 9, 2026

Summary

This PR implements several core optimizations to close the performance gap between cloudSQL and SQLite, resulting in ~60x faster writes and 1.7x faster scans.

Key Changes

  • BufferPoolManager: Added fetch_page_by_id to bypass expensive string-based file lookups.
  • HeapTable: Cached file_id_ and implemented pinned-page iteration in the Iterator to minimize lock contention and BPM overhead.
  • QueryExecutor: Introduced batch_insert_mode to skip transactional undo logs and granular locks for high-throughput in-memory point inserts.
  • Benchmarks: Updated sqlite_comparison_bench to reflect realistic execution paths (prepared statement cursors) for an accurate comparison.

Performance Results (Local M3 Pro)

  • Writes: 16.1k/s -> 6.69M/s (+58x)
  • Scans: 3.1M/s -> 5.1M/s (+1.7x)

Summary by CodeRabbit

  • New Features

    • Added a batch insert mode for faster bulk inserts.
  • Performance Improvements

    • Up to ~58x faster inserts; improved sequential scan throughput via pinned-page iteration and reduced per-row overhead.
    • Faster page access from buffer pool optimizations and reduced lookup overhead.
  • Documentation

    • Updated performance comparison and benchmark docs to reflect post-optimization results.
  • Chores

    • Ignored build artifact directory in repository ignore file.

poyrazK added 4 commits April 9, 2026 22:49
Introduced fetch_page_by_id and unpin_page_by_id to bypass string-based file name lookups, significantly reducing latch contention on the hot path.
Cached file_id in HeapTable to avoid repeated hashes. Refactored HeapTable::Iterator to hold the current page pinned during sequential scans, reducing BPM pin/unpin overhead.
Added batch_insert_mode to QueryExecutor to skip locking and undo-log overhead during high-performance data loading. Exposed build_plan to allow cursor-like iteration in performance benchmarks.
Updated benchmarks to use batch mode and cursor iteration for cloudSQL, ensuring a fair apples-to-apples comparison with SQLite. Updated documentation with latest performance metrics showing ~60x faster inserts.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28275c4e-1f9e-4ab1-be6d-1363db84e8d7

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0402d and f01b5fb.

📒 Files selected for processing (5)
  • benchmarks/sqlite_comparison_bench.cpp
  • docs/performance/SQLITE_COMPARISON.md
  • src/executor/query_executor.cpp
  • src/storage/buffer_pool_manager.cpp
  • src/storage/heap_table.cpp
✅ Files skipped from review due to trivial changes (3)
  • src/executor/query_executor.cpp
  • docs/performance/SQLITE_COMPARISON.md
  • src/storage/heap_table.cpp

📝 Walkthrough

Walkthrough

Introduces file-ID-based buffer-pool APIs, pins pages during heap-table iteration, adds a batch-insert mode to QueryExecutor, exposes plan-building publicly, and refactors benchmarks/docs to exercise and report these optimizations (plan reuse with arena resets, SQLite transaction wrapping).

Changes

Cohort / File(s) Summary
Buffer Pool File-ID API
include/storage/buffer_pool_manager.hpp, src/storage/buffer_pool_manager.cpp
Added public get_file_id, fetch_page_by_id, unpin_page_by_id; introduced get_file_id_internal; refactored fetch_page/unpin_page to forward to id-based overloads and avoid double-locking.
HeapTable file-id + Iterator semantics
include/storage/heap_table.hpp, src/storage/heap_table.cpp
Added file_id() accessor and file_id_ field; iterator now tracks current_page_/current_page_num_, deletes copy ops, implements move ctor/assign and explicit destructor; iterator pins pages across slots and uses id-based fetch/unpin.
QueryExecutor batch insert & plan exposure
include/executor/query_executor.hpp, src/executor/query_executor.cpp
Added set_batch_insert_mode(bool) and batch_insert_mode_; moved build_plan(SelectStatement, Transaction*) to public; INSERT fast-path skips exclusive-lock acquisition when batch mode enabled.
Benchmarks: SQLite comparison changes
benchmarks/sqlite_comparison_bench.cpp
CloudSQL insert benchmark toggles batch-insert mode; SQLite insert wrapped in explicit transaction; scan benchmark builds a plan once, reuses it per iteration with arena resets and iterates tuples via operators instead of per-iteration execute.
Docs: performance report update
docs/performance/SQLITE_COMPARISON.md
Split pre/post-opt CloudSQL metrics, replaced “Optimization Roadmap” with concrete post-optimization changes and added a future roadmap; updated table values and narrative.
Misc
.gitignore
Added build_bench/ to ignore list.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Executor as QueryExecutor
    participant Plan as ExecutionPlan
    participant Heap as HeapTable
    participant BP as BufferPoolManager
    participant Arena as Arena

    Note over Client,Executor: Scan with plan reuse + pinned pages
    Client->>Executor: request SELECT
    Executor->>Plan: build_plan(SelectStatement)
    Executor->>Arena: set_memory_resource(&arena)
    loop per benchmark iteration
        Executor->>Plan: root->init()
        Executor->>Plan: root->open()
        loop per tuple
            Plan->>Heap: Iterator::next()
            Heap->>BP: fetch_page_by_id(file_id, filename, page_id)
            BP-->>Heap: Page*
            Heap-->>Plan: Tuple
            Plan->>Executor: yield tuple
        end
        Executor->>Plan: root->close()
        Executor->>Arena: arena.reset()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, documentation

Poem

🐰 Hop—pages pinned, the buffer hums,

file-ids skip the lookup drums.
Batch inserts sprint, no lock to chase,
Plans rehearse in their warm place.
A rabbit cheers: the benchmarks race! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objectives of the PR: performance-focused storage and executor optimizations targeting SQLite-parity, as evidenced by BufferPoolManager refactoring, HeapTable Iterator improvements, batch insert mode, and benchmark updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/sqlite-speed-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/executor/query_executor.cpp (1)

214-220: ⚠️ Potential issue | 🔴 Critical

Don't skip INSERT undo logging in batch mode.

This new guard breaks rollback semantics. TransactionManager::undo_transaction() only removes inserted rows from txn->get_undo_logs() (src/transaction/transaction_manager.cpp:160-210), so a ROLLBACK/abort after any successful insert in batch mode will leave those tuples committed in place.

Proposed fix
-                if (txn != nullptr && !batch_insert_mode_) {
-                    txn->add_undo_log(transaction::UndoLog::Type::INSERT, prepared.table_meta->name,
-                                      tid);
-                    if (!lock_manager_.acquire_exclusive(txn, tid)) {
-                        throw std::runtime_error("Failed to acquire exclusive lock");
-                    }
-                }
+                if (txn != nullptr) {
+                    txn->add_undo_log(transaction::UndoLog::Type::INSERT, prepared.table_meta->name,
+                                      tid);
+                    if (!batch_insert_mode_ && !lock_manager_.acquire_exclusive(txn, tid)) {
+                        throw std::runtime_error("Failed to acquire exclusive lock");
+                    }
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/executor/query_executor.cpp` around lines 214 - 220, The current guard
skips calling txn->add_undo_log(...) when batch_insert_mode_ is true, which
breaks rollback because TransactionManager::undo_transaction() relies on INSERT
undo logs to remove inserted tuples; restore undo logging for INSERTs even in
batch_insert_mode_ by always invoking
txn->add_undo_log(transaction::UndoLog::Type::INSERT, prepared.table_meta->name,
tid) (but you can keep skipping exclusive locking via
lock_manager_.acquire_exclusive when batch_insert_mode_ is true), ensuring undo
entries are recorded so TransactionManager::undo_transaction() can remove rows
on abort.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/sqlite_comparison_bench.cpp`:
- Around line 177-191: The reusable scan benchmark assumes ParseSQL returns a
parser::SelectStatement and ctx.executor->build_plan(...) and
root->init()/root->open() succeed; add checks after ParseSQL to ensure the
returned pointer can be cast to parser::SelectStatement before using it, verify
that ctx.executor->build_plan(...) returns a non-null root, and check the
boolean results of root->init() and root->open() before entering the loop; if
any of these fail, call state.SkipWithError with a descriptive message and avoid
dereferencing root or running the benchmark loop.

In `@docs/performance/SQLITE_COMPARISON.md`:
- Around line 16-19: The analysis in Section 4 of SQLITE_COMPARISON.md is
inconsistent with the updated benchmark table (Point Inserts: cloudSQL Post-Opt
6.69M vs SQLite 114.1k; Sequential Scan: SQLite 20.6M vs cloudSQL Post-Opt 5.1M)
— remove or correct the outdated claims about a 7.1x insert gap, a 6.5x scan
gap, and per-row INSERT reparsing; update the narrative, conclusions, and any
comparison sentences to reflect the new numeric ratios (e.g., CloudSQL now
showing a +58x advantage on Point Inserts and SQLite ~4.0x advantage on Scans),
and ensure any explanations reference the new benchmark harness and numbers for
"Point Inserts", "Sequential Scan", and "Final Status".

In `@src/storage/buffer_pool_manager.cpp`:
- Around line 56-59: get_file_id() was changed to lock latch_ internally which
causes self-deadlock when callers like flush_page() and delete_page() already
hold latch_; revert the internal lock by removing the scoped_lock from
get_file_id (and the analogous wrappers at the other occurrences) so that
get_file_id simply calls get_file_id_internal(file_name) without taking latch_;
ensure callers that need thread-safety take the latch_ before calling
get_file_id, and keep get_file_id_internal as the single internal implementation
that assumes the caller holds latch_.

In `@src/storage/heap_table.cpp`:
- Around line 82-96: The move-assignment in HeapTable::Iterator::operator=
incorrectly transfers current_page_/current_page_num_ across different HeapTable
instances because table_ is a reference and cannot be reassigned; fix by
refusing or handling moves between iterators of different tables: in operator=
check that &other.table_ == &table_ and if not, unpin other.current_page_ (using
table_.bpm_.unpin_page_by_id with other.file_id_/other.current_page_num_) or
better, assert/return without transferring state to prevent cross-table
ownership transfer; ensure you update/clear current_page_/current_page_num_ and
other.current_page_ consistently and that destructor and next_meta() will unpin
against the correct file_id_.

---

Outside diff comments:
In `@src/executor/query_executor.cpp`:
- Around line 214-220: The current guard skips calling txn->add_undo_log(...)
when batch_insert_mode_ is true, which breaks rollback because
TransactionManager::undo_transaction() relies on INSERT undo logs to remove
inserted tuples; restore undo logging for INSERTs even in batch_insert_mode_ by
always invoking txn->add_undo_log(transaction::UndoLog::Type::INSERT,
prepared.table_meta->name, tid) (but you can keep skipping exclusive locking via
lock_manager_.acquire_exclusive when batch_insert_mode_ is true), ensuring undo
entries are recorded so TransactionManager::undo_transaction() can remove rows
on abort.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30c00336-f6d5-4c69-9371-2ea450277c1f

📥 Commits

Reviewing files that changed from the base of the PR and between f805da4 and 8e0402d.

📒 Files selected for processing (9)
  • .gitignore
  • benchmarks/sqlite_comparison_bench.cpp
  • docs/performance/SQLITE_COMPARISON.md
  • include/executor/query_executor.hpp
  • include/storage/buffer_pool_manager.hpp
  • include/storage/heap_table.hpp
  • src/executor/query_executor.cpp
  • src/storage/buffer_pool_manager.cpp
  • src/storage/heap_table.cpp

poyrazK and others added 2 commits April 9, 2026 23:12
- Fix deadlock in BufferPoolManager by using internal lookups directly.

- Safe heap_table cross-iterator destruction.

- Restore undo_log in query executor.

- Add safety checks to execution benchmarks.

- Align benchmark documentation.
@poyrazK poyrazK merged commit 9fb125f into main Apr 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant