KDB knowledge

Benchmark report

How fast KDB is against QuestDB and PostgreSQL on the same PC, and what each engine change of 2026-09-23 gave, including where KDB is still slower and what is not measured.

Every number on this page comes from a report or result file in the KDB source folder, named under each section. Nothing is rounded beyond what those files say.

Two kinds of measurement are on this page:

Setup

BenchmarkKindDateAgainstDataRoundsNoise
Time-serieshead-to-head2026-09-12QuestDB 10.0.1, native1,000,000 rows, 527 symbolsp50 of many runs, cache off and onclean machine, just restarted
Time-series re-runhead-to-head2026-09-23QuestDB 10.0.1 in Docker100,000 to 2,000,000 rows1 run per sizeother work on the PC
Floor-sheet loadhead-to-head2026-09-23QuestDB 10.0.1 in Docker814,535 real trades7 rounds, alternating, medians3 s quiet before each load
Floor-sheet querieshead-to-head2026-09-22QuestDB of the 006 app1,530,394 real trades, 30 days9 runs, cache=0quiet machine
News searchhead-to-head2026-09-23PostgreSQL 18.6 (GIN index) in Docker200,000 articles, 378 MB of text21 runs per querya sanitizer build and a fuzz run for the phrase re-run
App workloadhead-to-head2026-09-12 and 2026-09-23PostgreSQL 18.4 native; 18.6 in Docker20,000 users, accounts, posts; 8 clientsmedian of 9; 2 interleaved roundsbusy PC both days
PostgreSQL wirehead-to-head2026-09-12QuestDB 10.0.150,000 rows, 8 clientsmedian of 9busy PC
Indexeshead-to-head2026-09-12QuestDB 10.0.11,000,000 posts1 complete runbusy PC
Memory and diskhead-to-head2026-09-12QuestDB 10.0.11,000,000 minute candlesmedian of 3busy PC
Engine changesKDB-only A/B2026-09-23the build before1,000,000 rows, one thread7-round medians, cache=0agents on the PC

KDB vs QuestDB: time-series, 2026-09-12

Head-to-head. Writes (rows per second, higher is better). The two QuestDB numbers are its results in the cache-off and the cache-on round.

TestQuestDBKDB (cache off)KDB (cache on)Result
Bulk load, 20,000 rows per request (line protocol)865k / 853k1.04M1.36MKDB 1.2-1.6x
SQL INSERT, 100 rows per request6.8k / 8.9k144k166kKDB 19-21x
One row per request (line protocol)10.6k / 11.1k11.8k13.7kKDB 1.1-1.2x
One row per request (SQL)3.3k / 3.2k9.4k11.6kKDB 2.9-3.7x
Replace 10% of the rows (dedup upsert)724k / 698k1.17M1.14MKDB 1.6x
Parallel load, 8 clients, 8 tables2.63M / 3.04M3.52M4.13MKDB 1.3x
Delete all rows of one symbolnot possible3.3 ms4.7 msKDB only

Queries (p50 milliseconds at the client, including HTTP and JSON; lower is better).

TestQuestDBKDB (cache off)KDB (cache on)
count + sum, all rows0.171 / 0.1830.3350.142
1 symbol, 1 day (218 rows)0.290 / 0.3460.2360.176
1 symbol, all days to 1h candles0.299 / 0.2900.3310.170
All symbols, 1 day to daily candles (527)2.88 / 2.992.360.475
Latest row per symbol, LATEST ON (527)0.271 / 0.2400.4160.239 (tie)
GROUP BY symbol, all rows (527)2.78 / 2.581.500.850
ORDER BY volume DESC LIMIT 100.746 / 1.100.3210.122
Parallel queries, 8 clients (queries per second)817 / 7835,1939,744

Source: reports/benchmark-vs-questdb.md, bench/results/2026-09-12-clean-cache-off.json, bench/results/2026-09-12-clean-cache-on.json.

KDB vs QuestDB: time-series re-run, 2026-09-23

Head-to-head, one run per size, QuestDB in Docker, KDB with --memory 4096 and the harness's default result cache (on). Only the writes are shown: the query lines of these runs are cache hits on the KDB side, and every QuestDB request paid Docker's network.

Rows per second, 1,000,000 rowsQuestDBKDB
Bulk load, 20,000 rows per request (line protocol)726,0481,370,726
SQL INSERT, 100 rows per request6,830120,228
Replace 10% of the rows (line protocol upsert)610,207866,090
Parallel load, 8 clients, 8 tables (line protocol)1,598,7234,054,938

At 100,000 and 2,000,000 rows the order is the same (bulk load 1,427,713 against 418,983, and 997,864 against 668,739 rows per second). Read this as "KDB's writes did not get slower since 2026-09-12", not as a new margin.

Source: bench/results/2026-09-23-100k.json, bench/results/2026-09-23-1m.json, bench/results/2026-09-23-2m.json.

Floor sheet: KDB vs QuestDB

Load, head-to-head, 2026-09-23. 814,535 real trades (15 days), 8 columns with three SYMBOL columns, PARTITION BY DAY, 20,000 rows per POST /write, medians of 7 alternating rounds.

QuestDBKDB
before: the floor sheet has no DEDUP KEY, so KDB wrote it row by row686,476 rows/s438,373 rows/sKDB 36% slower
after: keyless tables written as runs, 3 s quiet before each load677,540 rows/s918,067 rows/sKDB 35% faster, 6 of 7 rounds won
after, no pause between loads700,202 rows/s814,553 rows/sKDB 16% faster
the candle table (with a DEDUP KEY), same day726,048 rows/s1,370,726 rows/sKDB 1.9x

Queries, head-to-head, 2026-09-22, in the container on real data (1,530,394 trades over 30 days), cache=0, every answer equal to QuestDB's:

QueryKDB engineKDB wallQuestDBBefore (2026-09-18)
BUY by stock and broker11.9 ms19.2 ms23.9 ms20.0 / 29.0 ms
SELL by stock and broker13.3 ms21.0 ms24.1 ms19.3 / 28.0 ms
BUY + SELL in one (UNION ALL)118.6 ms129.7 ms243.9 ms1,337 ms against QuestDB's 365 ms

The UNION ALL form went from 3.6x slower to 1.9x faster: each part is now grouped in the column engine before the parts are added up.

Found on the way: QuestDB 10.0.1 lost the first 20,000-row request after a DROP TABLE and CREATE TABLE of the same name, in 5 of 6 cycles, while answering 204 to it. Benchmarks now use a fresh table name per round.

Source: reports/keyless-runs.md, bench/results/2026-09-23-1m.json, reports/floorsheet-filter-speed.md.

Head-to-head, 2026-09-23. 200,000 articles (a title of 8 words, a body of 250 words, 378 MB of text), 21 runs per query. PostgreSQL 18.6 gets its best practice: a stored tsvector column with a GIN index, searched with the simple configuration (no stemming, like KDB). KDB gets CREATE FULLTEXT INDEX on title and body. Top-20 lists are ranked differently by design (BM25 against ts_rank_cd), so those are compared on row count only.

QueryRowsKDBPostgreSQL
load 200,000 articles3.9 s (51,514/s)49.2 s (4,068/s)KDB 12.7x
build the full-text index2.18 s9.07 sKDB 4.2x
common word, count182,5713.6 ms250 msKDB 69x
mid word, count40,2550.66 ms82 msKDB 124x
two words AND, count35,0953.6 ms86 msKDB 24x
prefix hydro*, count151,46310 ms284 msKDB 28x
rare word, ids2180.36 ms0.64 msKDB 1.8x
word + symbol + month90.54 ms0.48 msPostgreSQL 1.1x
common word, top 20 by rank207.3 ms408 msKDB 56x
mid word, top 20 by rank2016.9 ms384 msKDB 23x
two words, top 20 by rank205.5 ms364 msKDB 66x
8 clients, ranked search348/s23/sKDB 15x
common word, all 182,571 ids182,571188 ms636 msKDB 3.4x
phrase "hydro power", countabout 2,3769.7 ms (was 2,347 ms)295 msKDB 30x

Source: reports/news-search.md (with its addendum), bench/postgres/results/2026-09-23-search-200k-r1.json.

App workload: KDB vs PostgreSQL

Head-to-head. Sign-ups, logins, transfers, forum posts, full-text search and a report join, 20,000 rows, 8 clients.

2026-09-12, PostgreSQL 18.4 native, median of 9 runs:

TestKDB ops/sPostgreSQL ops/sKDB wonVerdict
sign-up (user and account, one transaction)3,2039439/9KDB faster
login (lookup by unique login)11,85810,3675/9no clear difference
transfer (BEGIN, 3 statements, COMMIT)2,6968299/9KDB faster
post a thread (5 posts, one transaction)1,3226948/8KDB faster
8 clients transferring1,3434,6610/8PostgreSQL faster
read a thread page (up to 50 posts)5,10511,1110/8PostgreSQL faster
forum full-text search (top 20)1472,5760/8PostgreSQL faster
report join (users, accounts, transfers)19680/8PostgreSQL faster

2026-09-23, PostgreSQL 18.6 in Docker, the build before and after the table-locks merge interleaved in the same minutes (master, merged, master, merged). A sanitizer build and a fuzz test ran on the PC throughout.

TestKDB before the mergeKDB after the mergePostgreSQL, same runs
8 clients transferring689 and 604/s1,821 and 1,981/s861 to 988/s
8 clients transferring, p5011.7 and 13.5 ms4.4 and 4.0 ms8.6 and 8.7 ms
forum full-text search (top 20)2,846 and 2,840/s2,673 and 3,177/s834 to 923/s
read a thread page3,664 and 3,071/s3,166 and 3,847/s875 to 1,310/s
report join18 and 17/s16 and 18/s59 to 63/s

Source: reports/benchmark-postgres.md, bench/results/2026-09-12-relational-medians.txt, reports/table-locks-merge.md, bench/postgres/results/2026-09-23-locks-master-r1.json, -r2.json, 2026-09-23-locks-merged-r1.json, -r2.json, reports/news-search.md.

PostgreSQL wire protocol: KDB vs QuestDB

Head-to-head, 2026-09-12. A Go client (pgx v5) over the PostgreSQL port, 50,000 rows, 8 clients, 9 runs. A winner is named only when the paired sign test gives p below 0.05 (8 or 9 wins out of 9).

TestKDB ops/sQuestDB ops/sKDB wonVerdict
connect (fresh connection)1,4311,5833/9no clear difference
simple query SELECT 114,91712,0005/9no clear difference
prepared query id=$16,3317,8342/9no clear difference
INSERT 1 row12,36813,2934/9no clear difference
INSERT 1,000 rows in one statement104629/9KDB faster
SELECT 1 row6,4176,9032/9no clear difference
SELECT 500 rows3,5113,1935/9no clear difference
SELECT 50,000 rows66793/9no clear difference
8 clients, SELECT 50012,78812,0436/9no clear difference

Eight of nine tests are a tie. A 50,000-row SELECT ran at 66 per second over the PostgreSQL port against 25 over HTTP /exec (KDB against itself, 9 of 9): the PostgreSQL port sends binary, HTTP builds JSON.

Source: reports/benchmark-postgres.md, bench/results/2026-09-12-pgwire-medians.txt.

Indexes, and indexes built at open

Head-to-head, 2026-09-12, 1,000,000 forum posts, one complete run, p50 milliseconds. QuestDB has no full-text index, so it answers with LIKE '%word%'.

QueryRowsKDB no indexKDB with indexQuestDBAgainst QuestDB
lookup author = x635.410.350.57KDB 1.6x
range scan status + 1 month10,7081.7410.760.26QuestDB 42x
keyset page 1201.81281.9312.74QuestDB 22x
OFFSET page 10,00020373.87347.359.65QuestDB 36x
search a rare word, top 20 by rank2015,0910.4927.35 (no rank)KDB 56x
phrase search28,1017,6033,721not possibleKDB only

The full-text lines of this table are from before the search fixes of 2026-09-23; see news search for today's numbers.

Indexes built at open, KDB-only A/B, 2026-09-23. Indexes live in memory and were built by the first query that needed them after a restart. Now kdb serve builds them on a background thread right after the open. First request after a restart, 200,000 posts, 4 indexes, 9 rounds, cache off:

First request after a restartBuilt by the queryBuilt at openWarm won
lookup author = x89.7 ms1.5 ms9/9
range scan status + 1 month77.8 ms6.7 ms9/9
common word, count774.4 ms3.1 ms9/9
all indexes ready after the open685 ms

The open itself did not get slower (274 against 291 ms, ranges overlap). One line got slower by direction only: keyset page 1, 23.0 against 26.4 ms, inside the round-to-round spread.

Source: reports/benchmark-indexes-and-footprint.md, bench/results/2026-09-12-indexes-1m-bench3-full.json, reports/warm-at-open.md, bench/results/2026-09-23-warm-restart-r1.json to -r9.json.

Memory and disk

Head-to-head, 2026-09-12, 1,000,000 minute candles, median of 3 runs. QuestDB's memory is the whole Java server, so only its growth is close to fair.

QuestDBKDB --memory 2048KDB --memory 256
load (line protocol, timed until readable)672,442 rows/s842,872 rows/s855,944 rows/s
disk after a checkpoint230.9 MB120.3 MB164.7 MB
memory at start, empty database633 MB (whole JVM)9.95 MB11.1 MB
working set after the load657 MB112 MB139 MB
peak working set over the run1,507 MB (whole JVM)229 MB231 MB

Source: reports/benchmark-indexes-and-footprint.md, bench/results/2026-09-12-indexes-and-footprint-tables.txt.

Engine changes of 2026-09-23

KDB-only A/B. 1,000,000 rows, one thread, ?cache=0, 7-round medians, before and after on the same build, unless the line says otherwise.

ChangeQuery or loadBeforeAfter
a trigger on one table no longer slows loads into the others20,000-row load into a table without a trigger34 ms8 ms
the same trigger made DEFERRED (on the real floor sheet)20,000-row load103 ms9 ms
LIKE / ILIKE / NOT LIKE in the column enginesym LIKE 'S6%'242 ms1.2 ms
NOT LIKE243 ms3.3 ms
ILIKE230 ms2.2 ms
col = a OR col = b as an IN listsum(qty) WHERE sym = 'S1' OR sym = 'S2'151 ms0.30 ms
qty = 7 OR qty = 9115 ms12 ms
branch-free filters in the column enginerandom LONG x > L4.68 ms0.67 ms
random DOUBLE y > L7.03 ms0.62 ms
b BETWEEN ...8.77 ms0.84 ms
a > X AND b < Y, on the thread pool1.17 ms0.49 ms
count, sum, min, max from saved stats4,000,000 rows in 200 day files, after a restart99.7 ms, 200 partition loads0.28 ms, 0 loads
the row path builds only the columns it reads (21-column table)count(*) WHERE a * 1 > X460 ms210 ms
ORDER BY ... LIMIT 10554 ms268 ms
GROUP BY549 ms242 ms
SELECT * (must build every column)465 ms436 ms
manifest keys of dropped tables removed300 partitions, one table dropped: manifest size1,128 KB573 KB
checkpoint24.6 ms14.7 ms
zone maps (814,535 floor-sheet trades, 15 day files, HTTP round trip)contract > p99: 14 of 15 files skipped0.93 ms0.56 ms
a value above every file: 15 of 15 skipped1.66 ms0.59 ms
rate > p99: nothing skipped1.99 ms2.15 ms (noise)
stop flag read in the scan of unsaved rows (200,000 rows)a 300 ms timeout308.8 s, never stopped0.94 s, stopped
keyless tables written as runsfloor-sheet load, rows/s438,373918,067
table-level write locks + group commit8 clients transferring604/s1,981/s

Older engine A/Bs: window frames corr(...) ROWS 199 PRECEDING 9,536 to 431 ms (2026-09-12); count(*), sum(volume) after replaced rows 164 to 44 µs (2026-09-12); the UNION ALL buy-and-sell query 1,750 to 29.2 ms on synthetic data (2026-09-22).

Source: TASKS.md "Done 2026-09-23" and "Done 2026-09-23 evening", reports/zone-maps.md, reports/text-fuzz.md, reports/keyless-runs.md, reports/table-locks-merge.md, reports/window-frame-fast-path.md, reports/column-engine-speed.md, reports/floorsheet-filter-speed.md.

Where the time of one request goes

KDB-only, 2026-09-23 evening, the container on 9900 through Docker Desktop and a native build on Windows loopback, same minutes:

PathRound trip
the engine alone (server time of SELECT 1)8-10 µs
/exec through Docker Desktop448-800 µs
/health (does nothing) through Docker Desktop465-685 µs
/exec on Windows loopback48-103 µs
a bare Node server on Windows loopback49-80 µs

So the socket path is under 2% of a request through Docker and under 15% on loopback. The rest is Docker Desktop's network and the /exec path itself.

Source: TASKS.md, "Re-judged 2026-09-23 evening, measured".

Where KDB is still slower, or not measured

WhereMeasuredSource
joins with GROUP BYreport join 16-18/s against PostgreSQL's 59-63/s (2026-09-23)reports/table-locks-merge.md
SELECT 1 throughputflat at about 3,000 requests/s with 8 clients (p50 469 to 2,071 µs), while /health does about 19,000/s: the ceiling is in the /exec pathTASKS.md, HFT item 1
opening a big data folder124 to 233 s for 11,104 partitions (9.5 GB): one file open of 10 to 16 ms per partitionTASKS.md, item E
more slow statements than HTTP workersa SELECT 1 waited 18.6 s behind 12 slow scans on the 8 workersreports/text-fuzz.md
regexp_like26 to 345 µs per row: 5 to 69 s over 200,000 rowsreports/text-fuzz.md
text + cheap filterword + symbol + month 0.54 ms against PostgreSQL's 0.48 ms; the new plan is not measuredreports/news-search.md
zone maps on pricesrate > p99 skips no file: every day holds the top pricesreports/zone-maps.md
index choicean index used where a scan is faster: keyset page 1 1.81 ms without, 281.93 ms with (2026-09-12)reports/benchmark-indexes-and-footprint.md
deep pagesOFFSET page 10,000: 347 ms against QuestDB's 9.65 ms (2026-09-12)reports/benchmark-indexes-and-footprint.md
ORDER BY over many groups377-580 ms on 270,000+ groups against 44-54 ms without the ORDER BYreports/floorsheet-filter-speed.md
the outer step of a UNION ALL groupingabout 90 ms of the 118.6 ms floor-sheet query runs on the row pathreports/floorsheet-filter-speed.md
commit with a flush4.8 ms per commit with --sync-commits always: the SSD's own flushreports/performance-and-cache.md
not measureda quiet re-run of the news search after the phrase and planner changes; the contention test on the merged table-locks build; any benchmark on Linux; the numbers on 9900 since the last image rebuildTASKS.md, reports/news-search.md, reports/table-locks-merge.md

What changed since the list of 2026-09-12: many writers at once (then 1,343 against PostgreSQL's 4,661 transfers/s) and full-text search (then 147 against 2,576 searches/s, and a common-word count 11.8x slower than QuestDB's scan) are now KDB wins. They are not on this list any more.

Source: the files named in the table.