Skip to content

Optimize exact composite primary key seeks#1038

Merged
timsehn merged 1 commit into
masterfrom
perf/composite-ac-next
May 22, 2026
Merged

Optimize exact composite primary key seeks#1038
timsehn merged 1 commit into
masterfrom
perf/composite-ac-next

Conversation

@timsehn
Copy link
Copy Markdown
Collaborator

@timsehn timsehn commented May 22, 2026

Summary

  • identify the highest multiplier from PR Avoid duplicate seek for single-row inserts #1036 as composite PK file-backed autocommit writes: oltp_update_index_ac and oltp_delete_insert_ac tied at 4.73x
  • skip the leaf scan for exact WITHOUT ROWID table primary-key seeks when the encoded sort key already matched the tree
  • keep the pending-delete check before returning the tree hit

Local benchmark notes

Focused 100K-row composite autocommit harness, DoltLite-only 21 runs on local Mac:

  • baseline oltp_update_index_ac median 41,706 us, trimmed median 39,928 us
  • branch oltp_update_index_ac median 43,064 us, trimmed median 41,423 us
  • baseline oltp_delete_insert_ac median 58,642 us, trimmed median 55,993 us
  • branch oltp_delete_insert_ac median 37,533 us, trimmed median 36,729 us

Local runs had large scheduler outliers; delete/insert showed the clear win among the tied worst metrics.

Tests

  • make -j8 doltlite doltlite-lib
  • BENCH_ROWS=200 BENCH_RUNS=1 BENCH_MAX_MULTIPLIER=1000 BENCH_AVG_MAX_MULTIPLIER=1000 ./test/sysbench_compare_compositepk.sh
  • make -j8 invariant_test corruption_test c-tests && ./invariant_test && ./corruption_test && test/run_c_tests.sh .

@github-actions
Copy link
Copy Markdown

Sysbench-Style Benchmark: Doltlite vs SQLite

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 20,691 30,510 1.47
oltp_range_select 8,841 12,785 1.45
oltp_sum_range 8,463 12,388 1.46
oltp_order_range 2,325 2,930 1.26
oltp_distinct_range 3,105 3,776 1.22
oltp_index_scan 3,386 5,560 1.64
select_random_points 8,883 15,969 1.80
select_random_ranges 2,614 4,468 1.71
covering_index_scan 3,536 3,725 1.05
groupby_scan 26,987 31,363 1.16
index_join 4,936 8,270 1.68
index_join_scan 2,626 5,101 1.94
types_table_scan 932,801 1,249,759 1.34
table_scan 1,062,115 1,407,625 1.33
oltp_read_only 87,385 116,514 1.33
Average 1.46

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 144,738 199,783 1.38
oltp_insert 12,851 22,469 1.75
oltp_update_index 43,525 90,280 2.07
oltp_update_non_index 28,069 56,891 2.03
oltp_delete_insert 39,138 67,231 1.72
oltp_write_only 18,599 41,316 2.22
types_delete_insert 20,454 38,008 1.86
oltp_read_write 55,627 104,493 1.88
Average 1.86

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 54,999 69,484 1.26
oltp_range_select 13,375 25,720 1.92
oltp_sum_range 12,948 25,512 1.97
oltp_order_range 2,826 4,775 1.69
oltp_distinct_range 3,752 5,700 1.52
oltp_index_scan 7,416 12,430 1.68
select_random_points 17,348 44,354 2.56
select_random_ranges 6,217 10,431 1.68
covering_index_scan 6,570 5,582 0.85
groupby_scan 28,311 41,868 1.48
index_join 7,235 13,299 1.84
index_join_scan 3,385 9,456 2.79
types_table_scan 1,041,107 2,090,184 2.01
table_scan 1,193,227 2,592,415 2.17
oltp_read_only 142,499 206,428 1.45
Average 1.79

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 152,287 213,285 1.40
oltp_insert 20,573 29,196 1.42
oltp_update_index 88,179 166,259 1.89
oltp_update_non_index 57,832 106,998 1.85
oltp_delete_insert 69,042 116,115 1.68
oltp_write_only 44,341 76,127 1.72
types_delete_insert 36,062 63,303 1.76
oltp_read_write 90,044 192,590 2.14
Average 1.73

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 34,414 69,428 2.02
oltp_range_select 11,150 25,827 2.32
oltp_sum_range 10,674 25,613 2.40
oltp_order_range 2,751 4,884 1.78
oltp_distinct_range 3,452 5,659 1.64
oltp_index_scan 5,276 12,069 2.29
select_random_points 15,104 44,558 2.95
select_random_ranges 4,111 10,322 2.51
covering_index_scan 4,634 5,640 1.22
groupby_scan 28,068 41,570 1.48
index_join 6,078 13,427 2.21
index_join_scan 3,097 9,392 3.03
types_table_scan 1,043,069 2,096,970 2.01
table_scan 1,199,759 2,595,291 2.16
oltp_read_only 113,138 204,082 1.80
Average 2.12

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 15,986 55,429 3.47
oltp_insert_ac 16,412 71,354 4.35
oltp_update_index_ac 17,502 78,897 4.51
oltp_update_non_index_ac 16,305 62,398 3.83
oltp_delete_insert_ac 17,800 73,776 4.14
oltp_write_only_ac 17,205 70,131 4.08
types_delete_insert_ac 15,750 67,418 4.28
oltp_read_write_ac 20,411 82,754 4.05
Average 4.09

100000 rows, median of 5 invocations per test, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (6x individual, 5x average)

All tests within ceilings.

@github-actions
Copy link
Copy Markdown

Sysbench-Style Benchmark (composite PK): Doltlite vs SQLite

Companion to the classic Sysbench-Style Benchmark. Every workload here
runs against tables with a 2-column INTEGER PRIMARY KEY(a, b) WITHOUT ROWID.

Individual ratios gated at 6×; section averages gated at 5×.

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 32,800 44,579 1.36
oltp_range_select 20,402 25,335 1.24
oltp_sum_range 18,314 24,341 1.33
oltp_order_range 3,753 4,382 1.17
oltp_distinct_range 4,864 5,499 1.13
oltp_index_scan 4,663 7,149 1.53
select_random_points 27,304 36,811 1.35
select_random_ranges 7,545 9,163 1.21
covering_index_scan 4,288 4,774 1.11
groupby_scan 39,844 45,535 1.14
index_join 7,919 11,970 1.51
index_join_scan 4,092 6,838 1.67
types_table_scan 1,119,319 1,523,487 1.36
table_scan 1,289,081 1,737,048 1.35
oltp_read_only 152,002 187,413 1.23
Average 1.31

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 244,153 311,334 1.28
oltp_insert 19,152 33,278 1.74
oltp_update_index 66,910 124,144 1.86
oltp_update_non_index 51,499 85,220 1.65
oltp_delete_insert 49,834 98,616 1.98
oltp_write_only 27,159 57,921 2.13
types_delete_insert 32,577 58,030 1.78
oltp_read_write 100,051 165,534 1.65
Average 1.76

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 125,768 127,376 1.01
oltp_range_select 30,918 54,172 1.75
oltp_sum_range 29,573 52,459 1.77
oltp_order_range 4,843 8,164 1.69
oltp_distinct_range 5,829 9,200 1.58
oltp_index_scan 14,352 21,285 1.48
select_random_points 44,611 95,124 2.13
select_random_ranges 16,763 21,252 1.27
covering_index_scan 12,518 10,971 0.88
groupby_scan 43,031 66,243 1.54
index_join 13,414 27,712 2.07
index_join_scan 5,396 17,812 3.30
types_table_scan 1,329,734 3,417,485 2.57
table_scan 1,559,274 4,179,249 2.68
oltp_read_only 292,869 374,663 1.28
Average 1.80

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 252,864 337,498 1.33
oltp_insert 25,562 49,654 1.94
oltp_update_index 157,397 278,135 1.77
oltp_update_non_index 104,136 181,466 1.74
oltp_delete_insert 104,077 196,140 1.88
oltp_write_only 63,380 128,434 2.03
types_delete_insert 60,712 105,974 1.75
oltp_read_write 163,142 340,851 2.09
Average 1.82

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 69,625 125,202 1.80
oltp_range_select 25,445 53,576 2.11
oltp_sum_range 24,286 51,927 2.14
oltp_order_range 4,280 8,107 1.89
oltp_distinct_range 5,320 9,136 1.72
oltp_index_scan 8,750 20,886 2.39
select_random_points 38,544 94,456 2.45
select_random_ranges 11,090 21,030 1.90
covering_index_scan 7,127 10,970 1.54
groupby_scan 42,292 66,155 1.56
index_join 10,740 27,177 2.53
index_join_scan 4,839 17,895 3.70
types_table_scan 1,307,790 3,366,683 2.57
table_scan 1,543,046 4,184,490 2.71
oltp_read_only 214,183 374,734 1.75
Average 2.18

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 17,569 57,630 3.28
oltp_insert_ac 18,345 79,002 4.31
oltp_update_index_ac 21,589 90,300 4.18
oltp_update_non_index_ac 16,248 71,232 4.38
oltp_delete_insert_ac 19,499 85,501 4.38
oltp_write_only_ac 20,561 87,106 4.24
types_delete_insert_ac 17,136 79,026 4.61
oltp_read_write_ac 27,954 101,282 3.62
Average 4.13

100000 rows, median of 5 invocations per test, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (6x individual, 5x average)

All tests within ceilings.

@github-actions
Copy link
Copy Markdown

Sysbench-Style Benchmark (BLOB PK): Doltlite vs SQLite

Companion to the classic Sysbench-Style Benchmark. Every workload here
runs against tables with a 16-byte big-endian BLOB PRIMARY KEY.

Individual ratios gated at 6×; section averages gated at 5×.

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 30,193 41,612 1.38
oltp_range_select 13,536 18,827 1.39
oltp_sum_range 11,932 18,346 1.54
oltp_order_range 3,128 3,798 1.21
oltp_distinct_range 4,214 4,791 1.14
oltp_index_scan 4,484 7,178 1.60
select_random_points 17,409 25,857 1.49
select_random_ranges 4,190 6,341 1.51
covering_index_scan 4,509 5,157 1.14
groupby_scan 34,817 41,240 1.18
index_join 6,786 11,993 1.77
index_join_scan 4,217 7,611 1.80
types_table_scan 1,122,917 1,612,808 1.44
table_scan 1,303,373 1,821,802 1.40
oltp_read_only 120,633 158,380 1.31
Average 1.42

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 243,514 315,596 1.30
oltp_insert 20,573 38,542 1.87
oltp_update_index 73,255 142,627 1.95
oltp_update_non_index 49,978 90,494 1.81
oltp_delete_insert 49,974 107,058 2.14
oltp_write_only 29,087 65,084 2.24
types_delete_insert 33,170 57,909 1.75
oltp_read_write 81,663 153,167 1.88
Average 1.87

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 123,164 129,168 1.05
oltp_range_select 24,285 53,407 2.20
oltp_sum_range 23,411 52,593 2.25
oltp_order_range 4,094 8,378 2.05
oltp_distinct_range 5,152 9,318 1.81
oltp_index_scan 14,789 22,713 1.54
select_random_points 35,345 89,125 2.52
select_random_ranges 13,314 19,555 1.47
covering_index_scan 13,395 14,516 1.08
groupby_scan 38,259 67,714 1.77
index_join 14,296 36,387 2.55
index_join_scan 7,110 26,068 3.67
types_table_scan 1,340,482 3,974,863 2.97
table_scan 1,594,566 4,832,627 3.03
oltp_read_only 261,787 379,941 1.45
Average 2.09

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 254,245 344,739 1.36
oltp_insert 35,572 60,185 1.69
oltp_update_index 180,609 329,522 1.82
oltp_update_non_index 110,119 199,171 1.81
oltp_delete_insert 119,089 227,668 1.91
oltp_write_only 73,408 143,135 1.95
types_delete_insert 63,954 119,637 1.87
oltp_read_write 156,260 360,550 2.31
Average 1.84

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 68,445 130,153 1.90
oltp_range_select 18,856 53,179 2.82
oltp_sum_range 18,128 52,910 2.92
oltp_order_range 3,678 8,376 2.28
oltp_distinct_range 4,693 9,294 1.98
oltp_index_scan 9,334 22,645 2.43
select_random_points 30,448 89,339 2.93
select_random_ranges 7,922 19,370 2.45
covering_index_scan 8,135 14,124 1.74
groupby_scan 37,395 67,573 1.81
index_join 12,036 35,766 2.97
index_join_scan 6,887 26,264 3.81
types_table_scan 1,364,763 4,049,834 2.97
table_scan 1,622,169 4,871,949 3.00
oltp_read_only 184,402 379,506 2.06
Average 2.54

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 15,855 56,952 3.59
oltp_insert_ac 18,152 89,988 4.96
oltp_update_index_ac 20,986 102,344 4.88
oltp_update_non_index_ac 16,315 78,349 4.80
oltp_delete_insert_ac 17,578 88,459 5.03
oltp_write_only_ac 18,299 88,840 4.85
types_delete_insert_ac 15,424 76,175 4.94
oltp_read_write_ac 24,934 100,463 4.03
Average 4.64

100000 rows, median of 5 invocations per test, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (6x individual, 5x average)

All tests within ceilings.

@github-actions
Copy link
Copy Markdown

Sysbench-Style Benchmark (TEXT PK): Doltlite vs SQLite

Companion to the classic Sysbench-Style Benchmark. Every workload here
runs against tables with a 32-char hex TEXT PRIMARY KEY (UUID-shaped).

Individual ratios gated at 6×; section averages gated at 5×.

In-Memory

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 31,382 47,550 1.52
oltp_range_select 13,800 21,543 1.56
oltp_sum_range 12,688 20,696 1.63
oltp_order_range 3,030 4,027 1.33
oltp_distinct_range 4,122 5,183 1.26
oltp_index_scan 4,964 8,398 1.69
select_random_points 19,185 29,717 1.55
select_random_ranges 4,076 6,637 1.63
covering_index_scan 4,645 5,509 1.19
groupby_scan 33,696 39,798 1.18
index_join 7,086 12,652 1.79
index_join_scan 4,492 8,363 1.86
types_table_scan 1,217,924 1,654,377 1.36
table_scan 1,411,759 1,811,323 1.28
oltp_read_only 133,214 174,755 1.31
Average 1.48

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 234,305 346,644 1.48
oltp_insert 23,845 40,300 1.69
oltp_update_index 79,318 162,602 2.05
oltp_update_non_index 53,611 101,607 1.90
oltp_delete_insert 53,484 121,951 2.28
oltp_write_only 32,638 69,462 2.13
types_delete_insert 34,729 64,204 1.85
oltp_read_write 98,915 174,743 1.77
Average 1.89

File-Backed

Reads

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 108,493 127,901 1.18
oltp_range_select 22,846 54,182 2.37
oltp_sum_range 22,203 53,462 2.41
oltp_order_range 4,031 8,304 2.06
oltp_distinct_range 5,145 9,371 1.82
oltp_index_scan 13,445 22,344 1.66
select_random_points 35,147 88,220 2.51
select_random_ranges 11,841 18,192 1.54
covering_index_scan 12,767 13,570 1.06
groupby_scan 35,381 65,546 1.85
index_join 14,563 35,218 2.42
index_join_scan 7,845 24,667 3.14
types_table_scan 1,299,965 3,885,877 2.99
table_scan 1,558,486 4,593,299 2.95
oltp_read_only 242,810 379,726 1.56
Average 2.10

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert 246,524 369,184 1.50
oltp_insert 47,904 73,443 1.53
oltp_update_index 192,876 321,613 1.67
oltp_update_non_index 134,218 194,823 1.45
oltp_delete_insert 130,265 222,252 1.71
oltp_write_only 88,672 136,761 1.54
types_delete_insert 67,559 120,137 1.78
oltp_read_write 181,244 352,089 1.94
Average 1.64

File-Backed (autocommit)

Each statement runs as its own transaction — exposes per-commit
fixed costs that the wrapped-in-BEGIN/COMMIT tests amortize away.
SQLite uses WAL mode with synchronous=FULL in this section so
the comparison uses SQLite's durable WAL autocommit path.

Reads

Reads have no commit cost; these are the same SQL files as the
File-Backed Reads section, included here for symmetry and to
catch any per-statement overhead doltlite pays on the read path.

Test SQLite (us) Doltlite (us) Multiplier
oltp_point_select 61,570 124,697 2.03
oltp_range_select 17,943 53,025 2.96
oltp_sum_range 17,418 53,895 3.09
oltp_order_range 3,636 8,486 2.33
oltp_distinct_range 4,693 9,359 1.99
oltp_index_scan 8,963 22,406 2.50
select_random_points 30,131 90,049 2.99
select_random_ranges 7,360 18,108 2.46
covering_index_scan 8,669 13,568 1.57
groupby_scan 35,738 65,684 1.84
index_join 12,392 36,124 2.92
index_join_scan 7,807 25,867 3.31
types_table_scan 1,297,347 3,908,025 3.01
table_scan 1,530,578 4,611,645 3.01
oltp_read_only 178,922 379,181 2.12
Average 2.54

Writes

Test SQLite (us) Doltlite (us) Multiplier
oltp_bulk_insert_ac 23,059 80,161 3.48
oltp_insert_ac 28,173 98,436 3.49
oltp_update_index_ac 33,385 125,213 3.75
oltp_update_non_index_ac 24,548 99,485 4.05
oltp_delete_insert_ac 27,791 110,389 3.97
oltp_write_only_ac 26,363 108,235 4.11
types_delete_insert_ac 22,266 94,038 4.22
oltp_read_write_ac 32,869 119,059 3.62
Average 3.84

100000 rows, median of 5 invocations per test, workload-only timing via host monotonic clock when available.

Performance Ceiling Check (6x individual, 5x average)

All tests within ceilings.

@timsehn timsehn merged commit 558c46f into master May 22, 2026
12 checks 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