Displaying/processing benchmark results
The LumoSQL project runs benchmarks on different versions of SQLite optionally combined with third party storage backends. The results of these
benchmarks are kept in a SQLite database, by default benchmarks.sqlite.benchmark-filter is a TCL script with reporting and update features,
including the ability to extend the schema. An example would be a field to identify who ran the benchmarks. Benchmarks can be exported and imported.
Assuming benchmarks.sqlite exists, call the tool with:
tclsh tool/benchmark-filter.tcl OPTIONS
Here is a simple example, with the default limit being the first 20 lines:
tclsh tool/benchmark-filter.tcl -list -fields ID:8,TARGET,DURATION
ID TARGET DURATION
B70E9DE8 3.53.2 12.292
416B6EAB 3.43.0 11.440
6CA8B349 3.43.0+lmdb-0.9.35 5.829
D414B582 3.45.0 12.429
89819885 3.45.0+lmdb-0.9.35 5.613
280983F9 3.50.0 10.691
C1801DA5 3.50.0+lmdb-0.9.35 4.882
632A43FD 3.53.2+lmdb-0.9.35 5.996
FCDD0A85 3.53.2+lmdbv1-1.0 5.780
Run IDs can be abbreviated to any unambiguous prefix from the hash. To display selected tests from one run:
tclsh tool/benchmark-filter.tcl -summary -tests 2,total B70E9DE8
Benchmark: sqlite 3.53.2
Target: 3.53.2
(3.53.2 2026-06-03 19:12:13 d6e03d8c777cfa2d35e3b60d8ec3e0187f3e9f99d8e2ee9cac695fd6fcdfalt1 (64-bit))
Ran at: 2026-06-21 07:30:38
Duration: 12.292
Disk time: read: 0.577; write: 3.235
TIME TEST NAME
9.257 2 1000 INSERTs
------------
12.292 (total benchmark run time)
Multiple IDs produce side-by-side results:
tclsh tool/benchmark-filter.tcl -quick -tests 2,total B70E9DE8 632A43FD FCDD0A85
3.53.2 3.53.2 3.53.2 (sqlite version)
lmdb lmdbv1 (backend name)
0.9.35 1.0 (backend version)
--------------- TIME -------------- TEST NAME
9.257 2.578 2.475 2 1000 INSERTs
------------------------------------
12.292 5.996 5.780 (total benchmark run time)
The same runs can be selected by their properties:
tclsh tool/benchmark-filter.tcl -quick -tests 2,total -version 3.53.2
To compare all LMDB runs:
tclsh tool/benchmark-filter.tcl -quick -tests 2,total -backend lmdb
3.43.0 3.45.0 3.50.0 3.53.2 (sqlite version)
lmdb lmdb lmdb lmdb (backend name)
0.9.35 0.9.35 0.9.35 0.9.35 (backend version)
--------------------- TIME -------------------- TEST NAME
2.440 2.746 1.600 2.578 2 1000 INSERTs
------------------------------------------------
5.829 5.613 4.882 5.996 (total benchmark run time)
For many runs, put tests in columns and select only those of interest:
tclsh tool/benchmark-filter.tcl -quick -column benchmark \
-tests 2,8,total -backend lmdb
Column 2: 1000 INSERTs
Column 8: 5000 SELECTs with an index
Column T: Total run duration
2 8 T Target
2.440 0.074 5.829 3.43.0+lmdb-0.9.35
2.746 0.094 5.613 3.45.0+lmdb-0.9.35
1.600 0.099 4.882 3.50.0+lmdb-0.9.35
2.578 0.106 5.996 3.53.2+lmdb-0.9.35
Full set of options
The tool accepts loads of options:
environment
-V,--version- print the benchmark-filter version and exit-database,-dbPATH_TO_DATABASE- the database to read, default is the last database updated bymake benchmark, normallybenchmarks.sqlite-sqlitePATH_TO_SQLITE- the sqlite3 executable; by default the tool tries to find it either in the LumoSQL build directory or installed on the system-limitN- limit the output to the most recentNruns which match other criteria; the default is 20-importFILE[FILE]...- instead of using runs in the database, read eachFILE(which must have been created using the-exportoption) into a temporary database, then process the data as normal; if it is desired to import the runs into a permanent database, see the-copyoption below; multiple files can be specified, for example-import FILE1 FILE2or-import downloads/data.*; the file names must not start with a-which would be interpreted as the next option; such files can be specified using./-NAME
selecting runs
If more than one selection option is provided, the tool will select runs which match all the criteria; however if the same option is repeated, it selects any which match: so for example -version N -version X -backend B selects all runs with backend B which also used saqlite version N or X.
RUN_ID- specifying a run ID (which appears as a long hexadecimal string) means that only that run will be processed; if this option is repeated, it select all the runs listed; the option can be abbreviated to fewer digits, and the program will look up the full ID-optionNAME-VALUE- select runs which used the named option and value in the target-missingNAME- select runs which do not have optionNAMErecorded in the database as a target option-datasizeN- select runs which used thedatasizeoption with valueN; this is an abbreviation foroptiondatasize-N; like thedatasizeoption,Ncould also be two numbers separated by comma-targetT- select runs which used the specified target (same syntax as each element of theTARGETSmake option)-versionV- select runs which used the specified version of sqlite3; this differ from-targetas the-versionoption can select any backend, while-targetselects on the full specification of version of sqlite3, backend, options-backendB- select runs which used the specified backend (any version)-backendB-V- select runs which used versionVof backendB-no-backend- select runs which used an unmodified sqlite (any version, unless-versionis also secified). The-backendand-no-backendoptions can be combined, and they include anything which matches, so-backendlmdb-no-backendmeans "select anything with an unmodified sqlite OR the LMDB backend".-failed- select runs which have failed tests-interrupted- select runs in which some tests were interrupted by a signal-completed- select runs in which all tests completed successfully and the run itself recorded an end time-crashed- select runs which have a start time but not an end time: this usually means that the runs have crashed; however a currently running benchmark will also be selected becauase it does not have an end time yet-empty- selects runs with no tests; usually combined with-delete(see below) to clean up the database-invalid- select runs which are invalid for some reason, for example they have test data but not information about the run itself, or the sums don't add up; usually combined with-deleteor-add(see below) to clean up the data-cpu-comment,-cpuPATTERN- select runs whose "cpu comment" matches the pattern, for example-cpu-comment%amd%would select all benchmarks running on an AMD processor, assuming the cpu comment was set appropriately (or left at the default, and the tool could detect the CPU type); if this option is repeated, select runs which match any of the patterns-disk-comment,-diskPATTERN- select runs whose "disk comment" matches the pattern, for example-disk-comment%nvme%would select runs which stored the databases on an NVME SSD, assuming the disk comment was set appropriately (or left at the default, and the benchmark could detect the device); if this option is repeated, select runs which match any of the patterns-since,-afterEPOCH- select runs started at or after the given time, expressed as a Unix epoch in seconds (for example the output ofdate +%s); useful for scoping output to runs produced after a recorded cutoff
output format
More than one output format option can appear in the command line, and they all apply, unless specified otherwise
-average- aggregate the selected runs by their target (or by-group-by, see below) and show, for each group, the average per-test real time across all matching repeats together with the per-group total. Honours every selection filter, so for example-average -backend lmdbaverages only the LMDB-backed runs;-average -group-by backend-versioncollapses across SQLite versions to show how each LMDB version performs on its own.-stats- per-target summary table with one row per group: count of runs, average / minimum / maximum total real-time (sum of per-testreal-timeacross the run), and average disk read / write time. Like-averageit respects every selection filter and the-group-byoption.-list- list the selected runs, one per line, with no information about the single tests; this is the default if there are no selection options-fieldsFIELD[,FIELD]...- change the fields printed by-list, default isRUN_ID,TARGET,DATE,TIME,DURATION; see below for the possible values-summary- display a summary of each test in each selected run; this only works if the selected runs have the same tests; cannot be combined with-details; this is the default if there are some selection options-quick- similar to summary, but omits the initial test description and just shows the columns of timings: the column headers show the sqlite/backend combination-count- only shows the number of results matching filters-details- display full details for each test in each selected run including all the information in the database; cannot be combined with-summary-formatFORMAT- choose the output format used by-stats,-averageand-compare. Recognised values:text(the default) - fixed-width human-readable columns, with a header row and a separator line of dashes.tsv- tab-separated values: one record per line, fields separated by a single TAB character, no padding, header row included. Suitable for feeding intoawk -F'\t', spreadsheets, plotting tools, or anything else that expects machine-readable columnar data. The other output modes (-list,-summary,-quick,-details) are unaffected by this option.
-group-byKEY- change the grouping key used by-statsand-average. The default istarget, which produces one row per distinct build target (e.g.3.36.0+lmdb-0.9.33). Other values:- any
run_datakey, e.g.backend,backend-version,sqlite-version,cpu-comment,option-datasize,option-rowsum, ... (seedoc/lumo-benchmark-schema.mdfor the full list); - the synthetic key
backend-versioncollapses targets across SQLite versions, so3.36.0+lmdb-0.9.33and3.53.0+lmdb-0.9.33end up in the same group (lmdb-0.9.33), and unmodified-SQLite runs end up in asqlite-onlygroup. This is the right grouping for "how does each LMDB version perform on its own?" reports.
- any
-compare- switch the tool into "compare two groups" mode. After-compare, subsequent arguments are tagged into one of two named groups, A and B, by preceding each filter with-Aor-B. The selected runs are then aggregated per benchmark, and the output shows the average real time of each test in group A, the average in group B, and the speed-up ratio A/B (a value greater than 1 means group B is faster). Because-compareconsumes all remaining argv tokens, any options that affect the outer output (notably-format) must appear before-compareon the command line. For example:
shows, for each individual benchmark in the run, how vanilla SQLite compares to LMDB-backed SQLite. Eachbenchmark-filter.tcl -db DB \ -compare \ -A -no-backend \ -B -backend lmdb-A/-Bmay be followed by any of the existing selection options (-target,-backend,-version,-option,-datasize,-cpu-comment, ...).-compareis exclusive with-stats,-average,-list,-summaryand the other output modes;-format tsvapplies to its output as it does for-stats/-average.-columnWHAT- what to show in columns, whereWHATis one of:test,benchmarkortarget(testandbenchmarkare considered equivalent for this option); applies only to-summaryand-quick-testsLIST(or equivalently-benchmarksLIST) - in the-summaryand-quickoutput formats, selects which tests are included, by default without this option they are all shown; the LIST is a comma-separate list of test/benchmark numbers, as shown in the normal output, or anything starting with the letter "t" to include the total run duration; this option is normally used to make the output narrower when using-columntest(or the equivalent-columnbenchmark)-ignore-numbers- replace all numbers in test names with "#"; this allows the-summaryand-quickoutput to compare tests which differ only by numbers, for example because they have different data sizes.-normalise- replaces timings in list, summary and details outputs with a normalised number calculated as 1000 multiplied by the test time and divided by the total run time for all tests; this could be used to compare results obtained from similar systems but which differ in the base speed by eliminating the variation due to the speed-exportFILE- write the selected runs toFILEin a text format, useful for example to send the run information by email-copyDATABASE- copies all information about the selected runs toDATABASE; if the database already exists, it must have the same schema and must not already contain the selected runs (the database will be created if it does not exist)
If no output format options are specified, the default is -list if there
are no specific run selection criteria, -summary if there are any criteria.
The aggregating modes (-stats, -average, -compare) and the explicit
output modes (-list, -summary, -quick, -details, -count,
-export, -copy) override this default.
When -stats, -average or -compare is in effect and the user has not
explicitly given -limit, the row limit defaults to 0 (no limit), since
truncating an aggregate over the first 20 runs would normally produce a
useless result. -list and -quick default to 20.
list columns
The following entries are valid values for the -fields option, selecting which
columns are displayed:
RUN_IDorID: the run identifier, a long hexadecimal string which identifies the run uniquelyRUN_ID:abbrevorID:abbrev: same asRUN_ID, but abbreviated toabbrevhexadecimal digits (minimum 8)TARGET: the encoded target, for example3.36.0or3.36.0+lmdb-0.9.29; using this with the build tool allows to repeat the benchmark with exactly the same optionsTITLE: a human-readable version ofTARGET, for example "sqlite 3.36.0 with lmdb 0.9.29"SQLITE_NAME: the output ofsqlite -versionDATEandTIME: a representation of the date or time the run startedEND_DATEandEND_TIME: a representation of the date or time the run completed, or-for runs which did not completeDONE: "YES" or "NO", depending on whether the run completed or notOK,INTRorFAIL: the count of tests which succeeded, were interrupted or failed with some error, respectivelyCPU_TYPEorARCH: the CPU architecture used to run the tests, for examplex86_64,armors390xOS_TYPEorOS: the operating system used to run the tests, for exampleLinuxorNetBSDCPU_COMMENTorCPU: a user-provided comment intended to describe the system used for the benchmark; if not provided, it shows as "-"; note that the benchmark system will try to detect the CPU if the user did not provide a commentDISK_COMMENTorDISK: a user-provided comment intended to describe the storage medium used for the databases during the benchmark, or "-" if not provided; note that the benchmark system will try to detect the storage device if the user did not provide a ciomment; this doesn't always succeed, so the column can still show as "-"DISK_TIME(orDISK_WRITE_TIME): the time taken to write a fixed amount of data (256 KB) to disk, which may help determining how fast the storage medium was; older benchmarks don't have this information and it will show as "-".SQLITEorSQLITE_VERSION: the version of sqlite used in the benchmark; backends which used their own bundled sqlite show this as an empty fieldBACKEND_NAME(orBE_NAME: the name of the backend used (an empty field if this was unmodified sqlite)BACKEND_VERSION(orBE_VERSION: the version of the backend used (an empty field if this was unmodified sqlite)N_TESTS: the number of tests in the run, currently 18 for benchmarks; however future versions of LumoSQL may define more tests
Worked example: comparing two groups
Given a database containing both vanilla SQLite and SQLite+LMDB runs, produce a per-test comparison with a speed-up ratio:
tclsh tool/benchmark-filter.tcl -db benchmarks.sqlite \
-compare -A -no-backend -B -backend lmdb 2>/dev/null | head -n 5
TEST_NUM TEST A_AVG_S B_AVG_S SPEEDUP
-------- --------------------------------------------------------------- ------- ------- -------
1 Creating database and tables 0.128 0.099 1.29x
2 1000 INSERTs 8.448 2.341 3.61x
3 100 UPDATEs without an index, upgrading a read-only transaction 0.144 0.114 1.27x
The same query as TSV, suitable for eg piping into column -ts$'\t':
tclsh tool/benchmark-filter.tcl -db benchmarks.sqlite -format tsv \
-compare -A -no-backend -B -backend lmdb
The grouping key for -stats and -average can also be changed; for example, to see the per-LMDB-version trend collapsed across SQLite versions:
tclsh tool/benchmark-filter.tcl -db benchmarks.sqlite \
-stats -group-by backend-version
extra actions
-addNAME=VALUE- adds some run information, for example to find older benchmark results which did not include the default value fordatasizeand to update them to have it, one could specify: "-missing datasize -add option-datasize=1"-delete- delete the selected runs from the database; it is recommended to run the tool with-listinstead of-deletefirst, and/or make a copy of the database before running-delete-delete-fromDB- delete the selected runs fromDB, as opposed to the database used to extract the data from; for example, if runs were added using-copyDB, then running the same filter with-delete-fromDBwill delete the runs again
Merging/combining databases
benchmark-filter.tcl does not merge two .sqlite files, but the same effect can be achieved with export/import:
tclsh tool/benchmark-filter.tcl -db db1.sqlite -export dump1.txt
tclsh tool/benchmark-filter.tcl -db db2.sqlite -export dump2.txt
tclsh tool/benchmark-filter.tcl -import dump1.txt dump2.txt -export combined.txt
checking test results
When running tests (as opposed to benchmarks) the build tool will store the results in a different database and the useful data could be different; one can get a summary of test results with:
tool/benchmark-filter.tcl -database tests.sqlite -list -fields TARGET,DONE,OK,INTR,FAIL
or have complete information about targets with failed tests:
tool/benchmark-filter.tcl -database tests.sqlite -details -failed
Failed steps: diagnosing and cleaning
A failed benchmark step shows up in benchmark-filter output as ERR1
(or similar) in the status column. The runner persists the sqlite3
shell's stderr text and exit code for such steps under the error-text
and error-exit-code keys on the relevant test_data row. These are
populated only on failure.
Run the following two queries against any benchmark database before treating its totals as performance evidence.
List every failed step with the SQL error and the target it failed under:
SELECT t.run_id,
(SELECT value FROM run_data
WHERE run_id = t.run_id AND key = 'target') AS target,
t.test_number,
(SELECT value FROM test_data t2
WHERE t2.run_id = t.run_id
AND t2.test_number = t.test_number
AND t2.key = 'test-name') AS test_name,
(SELECT value FROM test_data t2
WHERE t2.run_id = t.run_id
AND t2.test_number = t.test_number
AND t2.key = 'status') AS status,
t.value AS error_text
FROM test_data t
WHERE t.key = 'error-text';
List any step whose status is not OK. This is broader than the query
above: it also catches runs killed by signal (SIGHUP, etc.) which
have no error-text because the shell never got to print one:
SELECT t.run_id,
(SELECT value FROM run_data
WHERE run_id = t.run_id AND key = 'target') AS target,
t.test_number,
(SELECT value FROM test_data t2
WHERE t2.run_id = t.run_id
AND t2.test_number = t.test_number
AND t2.key = 'test-name') AS test_name,
t.value AS status
FROM test_data t
WHERE t.key = 'status' AND t.value != 'OK';
A run with a missing step in the middle of speedtest1 has a total that no longer means what it appears to mean. Either fix the underlying cause and re-run, or drop the affected runs entirely:
BEGIN;
CREATE TEMP TABLE bad_runs AS
SELECT DISTINCT run_id FROM test_data
WHERE key='status' AND value != 'OK';
DELETE FROM test_data WHERE run_id IN (SELECT run_id FROM bad_runs);
DELETE FROM run_data WHERE run_id IN (SELECT run_id FROM bad_runs);
DROP TABLE bad_runs;
COMMIT;
VACUUM;
tool/benchmark-filter.tcl -delete is the filter-driven equivalent
when the selection criteria are expressible as filter options.