KDB knowledge

Indexes

Make WHERE and ORDER BY ... LIMIT fast with an index, and keep values unique with a UNIQUE index.

An index is a second, sorted list of some columns of a table. Each entry points to its row. It works like the index at the back of a book: KDB finds the rows at once and does not read the whole table.

The examples use a small forum: tables threads and posts (id, thread_id, parent_id, author, body, created_at).

Example: A forum shows the 3 newest posts of thread 1 (sample data: content-forum)

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id);
SELECT id, author, created_at FROM posts
WHERE thread_id = 1
ORDER BY created_at DESC, id DESC
LIMIT 3;

Result:

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id)

index 'posts_thread_time' created on table 'posts' (8 rows)

SELECT id, author, created_at FROM posts WHERE thread_id = 1 ORDER BY created_at DESC, id DESC LIMIT

id (LONG)author (STRING)created_at (TIMESTAMP)
6sita2026-09-01T06:15:00.000Z
5ram2026-09-01T05:45:00.000Z
4gita2026-09-01T05:15:00.000Z

(3 rows)

CREATE INDEX

Syntax:

CREATE INDEX [IF NOT EXISTS] name ON table (column [, column ...])

Makes an index on one or more columns of a table. When the table already has rows, KDB builds the index at once and says how many rows it has.

ArgumentTypeMeaning
namenamethe index name; it must be unique in the whole database (a good habit: table_columns)
tablenamea relational or time-series table
columnnames1 to 16 stored columns. The order matters (see When an index is used)

Example: Indexes for a forum: pages of a thread, replies, and posts of one author (sample data: content-forum)

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id);
CREATE INDEX posts_parent ON posts (parent_id);
CREATE INDEX posts_author ON posts (author);
SELECT id, author, body FROM posts WHERE parent_id = 1 ORDER BY created_at;

Result:

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id)

index 'posts_thread_time' created on table 'posts' (8 rows)

CREATE INDEX posts_parent ON posts (parent_id)

index 'posts_parent' created on table 'posts' (8 rows)

CREATE INDEX posts_author ON posts (author)

index 'posts_author' created on table 'posts' (8 rows)

SELECT id, author, body FROM posts WHERE parent_id = 1 ORDER BY created_at

id (LONG)author (STRING)body (STRING)
2sitaI agree, the profit grew 18%.
3hariPrice is high now. Wait for a dip.

(2 rows)

Example: Trades of one broker in the trade_log table (a time-series table) (sample data: sample)

CREATE INDEX trade_log_buyer ON trade_log (buyer);
SELECT date, transaction_id, symbol, quantity, rate
FROM trade_log
WHERE buyer = 45
ORDER BY transaction_id
LIMIT 3;

Result:

CREATE INDEX trade_log_buyer ON trade_log (buyer)

index 'trade_log_buyer' created on table 'trade_log' (32 rows)

SELECT date, transaction_id, symbol, quantity, rate FROM trade_log WHERE buyer = 45 ORDER BY transac

date (TIMESTAMP)transaction_id (LONG)symbol (SYMBOL)quantity (LONG)rate (DOUBLE)
2026-09-09T00:00:00.000Z100002CITY10515
2026-09-10T00:00:00.000Z100014COAST20399.5
2026-09-10T00:00:00.000Z100018CITY500516

(3 rows)

Example: IF NOT EXISTS: a setup script can run again (sample data: content-forum)

CREATE INDEX IF NOT EXISTS posts_author ON posts (author);
CREATE INDEX IF NOT EXISTS posts_author ON posts (author);

Result:

CREATE INDEX IF NOT EXISTS posts_author ON posts (author)

index 'posts_author' created on table 'posts' (8 rows)

CREATE INDEX IF NOT EXISTS posts_author ON posts (author)

index 'posts_author' already exists

Example: The name is used already (sample data: content-forum; shows an error)

Setup:

CREATE INDEX posts_author ON posts (author);
CREATE INDEX posts_author ON posts (author, created_at);

Result:

Error: index 'posts_author' already exists (on table 'posts'); expected a new index name, or CREATE INDEX IF NOT EXISTS

Example: Not inside a transaction (sample data: content-forum; shows an error)

BEGIN;
CREATE INDEX posts_author ON posts (author);

Result:

transaction started

Error: CREATE INDEX cannot be used inside a transaction; COMMIT or ROLLBACK first (the transaction was rolled back)

CREATE UNIQUE INDEX

Syntax:

CREATE UNIQUE INDEX [IF NOT EXISTS] name ON table (column [, column ...])

An index that also makes sure no two rows have the same values in its columns. Use it for email addresses, user names, or "one vote per user per post".

ArgumentTypeMeaning
namenamethe index name, unique in the whole database
tablenamethe table
columnnames1 to 16 stored columns; with several columns, the combination must be unique

Example: App users: one account per email address (sample data: content-forum; shows an error)

Setup:

CREATE TABLE users (id LONG PRIMARY KEY, email STRING NOT NULL, name STRING);
CREATE UNIQUE INDEX users_email ON users (email);
INSERT INTO users VALUES (1, 'ram@example.com', 'Ram'), (2, 'sita@example.com', 'Sita');
INSERT INTO users VALUES (3, 'ram@example.com', 'Another Ram');

Result:

Error: duplicate value violates unique index 'users_email' on table 'users': a row with email='ram@example.com' already exists

Example: NULL values never clash, and upper case letters make a different value (sample data: content-forum)

Setup:

CREATE TABLE users (id LONG PRIMARY KEY, email STRING, name STRING);
CREATE UNIQUE INDEX users_email ON users (email);
INSERT INTO users VALUES (1, 'ram@example.com', 'Ram');
INSERT INTO users VALUES (2, NULL, 'Hari'), (3, NULL, 'Gita'), (4, 'RAM@example.com', 'Big Ram');
SELECT * FROM users ORDER BY id;

Result:

INSERT INTO users VALUES (2, NULL, 'Hari'), (3, NULL, 'Gita'), (4, 'RAM@example.com', 'Big Ram')

3 row(s) inserted

SELECT * FROM users ORDER BY id

id (LONG)email (STRING)name (STRING)
1ram@example.comRam
2NULLHari
3NULLGita
4RAM@example.comBig Ram

(4 rows)

Example: Forum votes: one vote per user per post (two columns) (sample data: content-forum; shows an error)

Setup:

CREATE TABLE votes (id LONG PRIMARY KEY, user_id LONG NOT NULL, post_id LONG NOT NULL, value INT NOT NULL);
CREATE UNIQUE INDEX votes_one_per_user ON votes (user_id, post_id);
INSERT INTO votes VALUES (1, 10, 1, 1), (2, 10, 2, 1), (3, 11, 1, -1);
INSERT INTO votes VALUES (4, 10, 1, -1);

Result:

Error: duplicate value violates unique index 'votes_one_per_user' on table 'votes': a row with user_id='10', post_id='1' already exists

Example: The table already has two equal values (sample data: content-forum; shows an error)

CREATE UNIQUE INDEX posts_one_per_author ON posts (author);

Result:

Error: cannot create unique index 'posts_one_per_author': two rows of table 'posts' have author='gita'; each value may be used by one row only

Bitmap index

Syntax:

CREATE INDEX name ON table USING BITMAP (column)
CREATE INDEX name ON table (column) WITH (kind = 'bitmap')

For a column with few different values: a symbol, a side (B / S), a status, a region. For each value the index keeps the set of rows that have it, as a compact bitmap. Two or more such conditions are answered together: region = 5 AND status = 'open' takes the rows in both sets at once (an AND of two bitmaps), not the rows of one index checked one by one.

ArgumentTypeMeaning
namenamethe index name
columnany stored columnone column only
Your queryUses the bitmap indexes
a = 5, a IN (1, 2), a IS NULLyes
a = 5 AND b = 'x' (a bitmap index on each)yes: the rows in both sets
a = 5 AND b <> 'x', a = 5 AND b NOT IN (...)yes: the rows of a = 5, without those of b
(a = 5 OR b = 'x') AND c = 1yes: the rows in either set, then those of c too
a = 5 AND MATCH(body, 'word') (a full-text index)yes: the sets and the matches combined
a = 5 AND d BETWEEN 1 AND 99 (an ordinary index on d)yes, when the range finds few rows: combined too
a = 5 AND ts >= '2026-03-01' AND ts < '2026-04-01'yes: only the partitions of that month
b <> 'x' aloneno
a > 5, a LIKE ...no: use an ordinary index

Example: Open orders of one region: two bitmap indexes together (sample data: content-forum)

Setup:

CREATE TABLE orders (id LONG PRIMARY KEY, region INT, status SYMBOL, amount DOUBLE);
INSERT INTO orders SELECT x, x % 7, CASE x % 3 WHEN 0 THEN 'open' WHEN 1 THEN 'paid' ELSE 'sent' END, x * 1.5
FROM generate_series(1, 300) g(x);
CREATE INDEX orders_region ON orders USING BITMAP (region);
CREATE INDEX orders_status ON orders (status) WITH (kind = 'bitmap');
SELECT count(*), sum(amount) FROM orders WHERE region = 5 AND status = 'open';
EXPLAIN SELECT id FROM orders WHERE region = 5 AND status = 'open';
EXPLAIN SELECT id FROM orders WHERE region IN (1, 2) AND status <> 'paid';

Result:

SELECT count(*), sum(amount) FROM orders WHERE region = 5 AND status = 'open'

count(*) (LONG)sum(amount) (DOUBLE)
143118.5

(1 row)

EXPLAIN SELECT id FROM orders WHERE region = 5 AND status = 'open'

plan (STRING)
scan table orders (1 of 1 partitions, about 300 rows)
use bitmap index: orders_region (1 value) AND orders_status (1 value), 14 row(s)
build 3 of 4 stored column(s) per row: id, region, status
filter while scanning: 2 condition(s)
engine: column

(5 rows)

EXPLAIN SELECT id FROM orders WHERE region IN (1, 2) AND status != 'paid'

plan (STRING)
scan table orders (1 of 1 partitions, about 300 rows)
use bitmap index: orders_region (2 values) AND NOT orders_status (1 value), 57 row(s)
build 3 of 4 stored column(s) per row: id, region, status
filter while scanning: 2 condition(s)
engine: column

(5 rows)

Example: An OR of two bitmap indexes, and a text search combined with them (sample data: content-forum)

Setup:

CREATE TABLE notes (id LONG PRIMARY KEY, region INT, status SYMBOL, body STRING);
INSERT INTO notes SELECT x, x % 7, CASE x % 3 WHEN 0 THEN 'open' WHEN 1 THEN 'paid' ELSE 'sent' END,
  CASE WHEN x % 5 = 0 THEN 'oil price up' ELSE 'quiet day' END
FROM generate_series(1, 300) g(x);
CREATE INDEX notes_region ON notes USING BITMAP (region);
CREATE INDEX notes_status ON notes USING BITMAP (status);
CREATE FULLTEXT INDEX notes_body ON notes (body);
SELECT count(*) FROM notes WHERE (region = 5 OR status = 'open') AND MATCH(body, 'oil');
EXPLAIN SELECT id FROM notes WHERE (region = 5 OR status = 'open') AND MATCH(body, 'oil');

Result:

SELECT count(*) FROM notes WHERE (region = 5 OR status = 'open') AND match(body, 'oil')

count(*) (LONG)
26

(1 row)

EXPLAIN SELECT id FROM notes WHERE (region = 5 OR status = 'open') AND match(body, 'oil')

plan (STRING)
scan table notes (1 of 1 partitions, about 300 rows)
use bitmap index: (notes_region (1 value) OR notes_status (1 value)) AND full-text notes_body, 26 row(s)
filter while scanning: 2 condition(s)
engine: row (a condition the column engine does not do)

(4 rows)

DROP INDEX

Syntax:

DROP INDEX [IF EXISTS] name

Removes an index. The rows of the table do not change.

ArgumentTypeMeaning
namenamethe index name (you do not name the table: index names are unique in the database)

Example: Drop an index, then run the same script again (sample data: content-forum)

Setup:

CREATE INDEX posts_author ON posts (author);
DROP INDEX posts_author;
DROP INDEX IF EXISTS posts_author;

Result:

DROP INDEX posts_author

index 'posts_author' dropped

DROP INDEX IF EXISTS posts_author

index 'posts_author' does not exist

Example: There is no such index (sample data: content-forum; shows an error)

DROP INDEX posts_author;

Result:

Error: cannot drop index 'posts_author': it does not exist

Example: A column that an index uses cannot be dropped (sample data: content-forum; shows an error)

Setup:

CREATE INDEX posts_parent ON posts (parent_id);
ALTER TABLE posts DROP COLUMN parent_id;

Result:

Error: cannot drop column 'parent_id' from table 'posts': index 'posts_parent' uses it; DROP INDEX posts_parent first

SHOW INDEXES

Syntax:

SHOW INDEXES
SHOW INDEXES FROM table
SHOW INDEXES ON table

Lists the indexes of the database, or of one table. SHOW INDEX is the same.

ColumnTypeMeaning
nameSTRINGindex name
tableSTRINGtable name
kindSTRINGINDEX, UNIQUE, FULLTEXT, or GIN (a JSON index)
columnsSTRINGthe columns, in index order
builtBOOLtrue when the index is in memory now
rowsLONGentries in the index; NULL when not built
memory_bytesLONGmemory the index uses; NULL when not built
create_sqlSTRINGthe CREATE ... INDEX statement that makes the index again
buildingBOOLtrue while the server's warm-up thread is building it right now (see Built at open)

Example: The index is built by the first query that needs it (sample data: content-forum)

Setup:

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id);
CREATE INDEX posts_author ON posts (author);
SHOW INDEXES FROM posts;
SELECT count(*) AS posts FROM posts WHERE thread_id = 1;
SHOW INDEXES FROM posts;

Result:

SHOW INDEXES FROM posts

name (STRING)table (STRING)kind (STRING)columns (STRING)built (BOOL)rows (LONG)memory_bytes (LONG)create_sql (STRING)building (BOOL)
posts_thread_timepostsINDEXthread_id, created_at, idfalseNULLNULLCREATE INDEX posts_thread_time ON posts (thread_id, created_at, id)false
posts_authorpostsINDEXauthorfalseNULLNULLCREATE INDEX posts_author ON posts (author)false

(2 rows)

SELECT count(*) AS posts FROM posts WHERE thread_id = 1

posts (LONG)
6

(1 row)

SHOW INDEXES FROM posts

name (STRING)table (STRING)kind (STRING)columns (STRING)built (BOOL)rows (LONG)memory_bytes (LONG)create_sql (STRING)building (BOOL)
posts_thread_timepostsINDEXthread_id, created_at, idtrue8400CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id)false
posts_authorpostsINDEXauthorfalseNULLNULLCREATE INDEX posts_author ON posts (author)false

(2 rows)

Here the database was opened again after CREATE INDEX with kdb sql, so no index was in memory at first. The SELECT built posts_thread_time; posts_author is still not built.

Built at open

kdb serve builds every index of a database right after it opens, on a background thread, so the first query finds them ready instead of paying for the build (about 0.2 s per million rows for a plain index, a few seconds for a full-text index over 200,000 articles). Nothing else waits for it:

Example: Every index of the database (sample data: content-forum)

Setup:

CREATE INDEX posts_author ON posts (author);
CREATE TABLE users (id LONG PRIMARY KEY, email STRING NOT NULL);
CREATE UNIQUE INDEX users_email ON users (email);
CREATE FULLTEXT INDEX posts_body ON posts (body);
SHOW INDEXES;

Result:

name (STRING)table (STRING)kind (STRING)columns (STRING)built (BOOL)rows (LONG)memory_bytes (LONG)create_sql (STRING)building (BOOL)
posts_authorpostsINDEXauthorfalseNULLNULLCREATE INDEX posts_author ON posts (author)false
posts_bodypostsFULLTEXTbodyfalseNULLNULLCREATE FULLTEXT INDEX posts_body ON posts (body)false
users_emailusersUNIQUEemailfalseNULLNULLCREATE UNIQUE INDEX users_email ON users (email)false

(3 rows)

When an index is used

Syntax:

EXPLAIN SELECT ...

KDB picks an index by itself. There is no hint to force one. Put EXPLAIN before a query: the line use index <name> says which index is used. No such line: every row is read.

For an index on the columns (a, b, c):

Your queryUses the index
a = 5, a IN (1, 2, 3), a IS NULLyes
a = 5 AND b = 7yes, both columns
a > 5, a BETWEEN ..., a LIKE 'abc%'yes: a range of a
a = 5 AND b > 100yes: a exactly, then a range of b
a = 5 ORDER BY b DESC LIMIT 20yes, and it stops after 20 rows
a = 5 AND (b, c) < (x, y) ORDER BY b DESC, c DESC LIMIT 20yes: keyset pagination (below)
b = 7 aloneno: b is not the first column
a = 5 OR a = 7yes: the rows of each side, once (or write a IN (5, 7))
a = 5 OR x = 7, with another index that starts with xyes: each side uses its own index
a = 5 OR b = 7no: b = 7 has no index of its own, so every row is read
lower(a) = 'x', a + 1 = 6no: a function or math around the column
a != 5, a NOT IN (...), a LIKE '%abc', NOT LIKEno

Example: A forum page: the index gives the rows in order and stops after 3 (sample data: content-forum)

Setup:

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id);
EXPLAIN SELECT id FROM posts WHERE thread_id = 1 ORDER BY created_at DESC, id DESC LIMIT 3;

Result:

plan (STRING)
scan table posts (1 of 1 partitions, about 8 rows)
use index posts_thread_time (thread_id = value, rows in ORDER BY order, stops after 3 row(s))
build 3 of 6 stored column(s) per row: id, thread_id, created_at
filter while scanning: 1 condition(s)
sort by 2 key(s)
limit 3
engine: column

(7 rows)

Example: Next page (keyset pagination): posts older than post 4 (sample data: content-forum)

Setup:

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id);
SELECT id, author, created_at FROM posts
WHERE thread_id = 1 AND (created_at, id) < ('2026-09-01T11:00:00+05:45', 4)
ORDER BY created_at DESC, id DESC
LIMIT 3;

Result:

id (LONG)author (STRING)created_at (TIMESTAMP)
3hari2026-09-01T04:35:00.000Z
2sita2026-09-01T04:20:00.000Z
1ram2026-09-01T04:15:00.000Z

(3 rows)

Example: IN and LIKE 'text%' use the index; a function around the column does not (sample data: content-forum)

Setup:

CREATE INDEX posts_author ON posts (author);
EXPLAIN SELECT id FROM posts WHERE author IN ('ram', 'sita');
EXPLAIN SELECT id FROM posts WHERE author LIKE 'ra%';
EXPLAIN SELECT id FROM posts WHERE lower(author) = 'ram';

Result:

EXPLAIN SELECT id FROM posts WHERE author IN ('ram', 'sita')

plan (STRING)
scan table posts (1 of 1 partitions, about 8 rows)
use index posts_author (author in 2 values)
build 2 of 6 stored column(s) per row: id, author
filter while scanning: 1 condition(s)
engine: column

(5 rows)

EXPLAIN SELECT id FROM posts WHERE author LIKE 'ra%'

plan (STRING)
scan table posts (1 of 1 partitions, about 8 rows)
use index posts_author (author in a range)
build 2 of 6 stored column(s) per row: id, author
filter while scanning: 1 condition(s)
engine: column

(5 rows)

EXPLAIN SELECT id FROM posts WHERE lower(author) = 'ram'

plan (STRING)
scan table posts (1 of 1 partitions, about 8 rows)
build 2 of 6 stored column(s) per row: id, author
filter while scanning: 1 condition(s)
engine: row (a condition the column engine does not do)

(4 rows)

Example: Not the first column of the index: the table is read (sample data: content-forum)

Setup:

CREATE INDEX posts_thread_time ON posts (thread_id, created_at, id);
EXPLAIN SELECT id FROM posts WHERE created_at > '2026-09-01';

Result:

plan (STRING)
scan table posts (1 of 1 partitions, about 8 rows)
build 2 of 6 stored column(s) per row: id, created_at
filter while scanning: 1 condition(s)
engine: column

(4 rows)