KDB knowledge

INSERT

Add rows with INSERT, decide what happens with duplicate keys, get new ids back with RETURNING, and write times correctly.

INSERT adds rows to a table. One INSERT can add many rows, and it is all or nothing: when one row is bad, no row of that statement is saved. When KDB says inserted, the rows are already on the disk.

StatementThe key is newThe key is already there
INSERTrow addederror, nothing saved
INSERT OR REPLACErow addedthe old row is replaced by the new one
INSERT OR IGNORErow addedthe new row is skipped

A real example: today's market candles arrive; one of them was already loaded.

Example: Load daily candles; a repeated row is replaced, not doubled

Setup:

CREATE TABLE ochl_day (time TIMESTAMP ROUND 1d, symbol SYMBOL, open DOUBLE, high DOUBLE, low DOUBLE, close DOUBLE, volume DOUBLE)
  TIMESERIES(time) PARTITION BY YEAR DEDUP KEY (symbol, time);
INSERT INTO ochl_day VALUES ('2026-09-13', 'CITY', 505.0, 509.0, 503.1, 508.2, 91000);
INSERT OR REPLACE INTO ochl_day (time, symbol, open, high, low, close, volume) VALUES
  ('2026-09-13', 'CITY', 505.0, 509.0, 503.1, 507.6, 91000),
  ('2026-09-13', 'COAST',  376.0, 379.9, 374.0, 378.5, 120500),
  ('2026-09-13', 'VOLT', 191.0, 194.2, 190.1, 193.7, 64000);
SELECT time, symbol, close FROM ochl_day ORDER BY symbol;

Result:

INSERT OR REPLACE INTO ochl_day (time, symbol, open, high, low, close, volume) VALUES ('2026-09-13',

2 row(s) inserted, 1 replaced

SELECT time, symbol, close FROM ochl_day ORDER BY symbol

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)
2026-09-13T00:00:00.000ZCITY507.6
2026-09-13T00:00:00.000ZCOAST378.5
2026-09-13T00:00:00.000ZVOLT193.7

(3 rows)

INSERT INTO ... VALUES

Syntax:

INSERT INTO table [(column, ...)] VALUES (value, ...) [, (value, ...) ...] [RETURNING ...]

Adds one row for each (...) after VALUES.

PartMeaning
tablea table (not a view)
(column, ...)optional. The columns the values are for, in any order. Columns you leave out get their DEFAULT, or NULL, or the next AUTO id
without a column listgive a value for every stored column, in table order; computed columns are skipped
valuea constant ('CITY', 504.9, NULL, TRUE) or an expression without columns (1 + 1, now(), CAST('7' AS INT))

Example: Register two app users; the id and the time come by themselves

Setup:

CREATE TABLE users (id LONG PRIMARY KEY AUTO, email STRING NOT NULL UNIQUE, name STRING, created TIMESTAMP DEFAULT now());
INSERT INTO users (email, name) VALUES ('ram@example.com', 'Ram Sharma'), ('sita@example.com', 'Sita Rai');
SELECT id, email, name, created IS NOT NULL AS has_created FROM users;

Result:

INSERT INTO users (email, name) VALUES ('ram@example.com', 'Ram Sharma'), ('sita@example.com', 'Sita

2 row(s) inserted

SELECT id, email, name, created IS NOT NULL AS has_created FROM users

id (LONG)email (STRING)name (STRING)has_created (BOOL)
1ram@example.comRam Sharmatrue
2sita@example.comSita Raitrue

(2 rows)

Example: A forum post with text in another language (Nepali) and a quote inside

Setup:

CREATE TABLE posts (id LONG PRIMARY KEY AUTO, author STRING NOT NULL, body STRING NOT NULL);
INSERT INTO posts (author, body) VALUES
  ('ram', 'City''s dividend looks good.'),
  ('sita', 'नमस्ते, यो राम्रो छ।');
SELECT id, author, body FROM posts;

Result:

INSERT INTO posts (author, body) VALUES ('ram', 'City''s dividend looks good.'), ('sita', 'नमस्ते, य

2 row(s) inserted

SELECT id, author, body FROM posts

id (LONG)author (STRING)body (STRING)
1ramCity's dividend looks good.
2sitaनमस्ते, यो राम्रो छ।

(2 rows)

Example: Values can be expressions without columns

Setup:

CREATE TABLE ledger (id LONG PRIMARY KEY AUTO, amount DECIMAL(2) NOT NULL, fee DECIMAL(2), created TIMESTAMP);
INSERT INTO ledger (amount, fee, created) VALUES (1000.00, round(1000.00 * 0.004, 2), now());
SELECT id, amount, fee, created > '2026-01-01' AS recent FROM ledger;

Result:

INSERT INTO ledger (amount, fee, created) VALUES (1000.00, round(1000.00 * 0.004, 2), now())

1 row(s) inserted

SELECT id, amount, fee, created > '2026-01-01' AS recent FROM ledger

id (LONG)amount (DECIMAL(2))fee (DECIMAL(2))recent (BOOL)
11000.004.00true

(1 row)

Example: One bad row: no row of the statement is saved (shows an error)

Setup:

CREATE TABLE brokers (id INT PRIMARY KEY, name STRING NOT NULL);
INSERT INTO brokers VALUES (1, 'Keystone Securities'), (2, NULL), (3, 'Vision Securities');

Result:

Error: row 2: column 'name' (STRING): cannot be NULL

Example: The wrong number of values (shows an error)

Setup:

CREATE TABLE brokers (id INT PRIMARY KEY, name STRING NOT NULL);
INSERT INTO brokers VALUES (1, 'Keystone Securities'), (2);

Result:

Error: line 1, column 56: row 2 has 1 value; expected 2 like the first row

Duplicate keys

Syntax:

PRIMARY KEY (column)            relational table
DEDUP KEY (column, ..., time)   time-series table
UNIQUE (column)                 any table

How KDB decides that a new row is "already there":

TableTwo rows are the same when
relational with a PRIMARY KEYthey have the same key value
time-series with a DEDUP KEYall DEDUP KEY columns are the same (after ROUND of the time)
without a keynever: every INSERT adds rows, also exact copies

Example: A candle for the same symbol and day (shows an error)

Setup:

CREATE TABLE ochl_day (time TIMESTAMP ROUND 1d, symbol SYMBOL, close DOUBLE) TIMESERIES(time) DEDUP KEY (symbol, time);
INSERT INTO ochl_day VALUES ('2026-09-10', 'CITY', 504.9);
INSERT INTO ochl_day VALUES ('2026-09-10T14:00:00+05:45', 'CITY', 505.0);

Result:

Error: a row with symbol='CITY', time='2026-09-10T00:00:00.000Z' already exists in 'ochl_day'. Use INSERT OR REPLACE to overwrite it, or INSERT OR IGNORE to skip it

Example: The same key twice in one INSERT OR REPLACE: the last row wins

Setup:

CREATE TABLE live_data (timestamp TIMESTAMP, symbol SYMBOL, ltp DOUBLE) TIMESERIES(timestamp) DEDUP KEY (timestamp, symbol);
INSERT OR REPLACE INTO live_data VALUES
  ('2026-09-10T11:05:00+05:45', 'CITY', 513.0),
  ('2026-09-10T11:05:00+05:45', 'CITY', 513.4);
SELECT timestamp, symbol, ltp FROM live_data;

Result:

INSERT OR REPLACE INTO live_data VALUES ('2026-09-10T11:05:00+05:45', 'CITY', 513.0), ('2026-09-10T1

1 row(s) inserted, 1 replaced

SELECT timestamp, symbol, ltp FROM live_data

timestamp (TIMESTAMP)symbol (SYMBOL)ltp (DOUBLE)
2026-09-10T05:20:00.000ZCITY513.4

(1 row)

INSERT OR REPLACE

Syntax:

INSERT OR REPLACE INTO table [(column, ...)] VALUES (...), ... [RETURNING ...]

Adds rows with new keys, and replaces rows whose key is already there. This is an upsert. It is the best statement for imports: the same file can be loaded again and again without double rows.

Example: The CITY close price was wrong: send the full row again

Setup:

CREATE TABLE ochl_day (time TIMESTAMP ROUND 1d, symbol SYMBOL, open DOUBLE, high DOUBLE, low DOUBLE, close DOUBLE, volume DOUBLE) TIMESERIES(time) DEDUP KEY (symbol, time);
INSERT INTO ochl_day VALUES ('2026-09-13', 'CITY', 505.0, 509.0, 503.1, 508.2, 91000);
INSERT OR REPLACE INTO ochl_day VALUES ('2026-09-13', 'CITY', 505.0, 509.0, 503.1, 507.6, 91000);
SELECT time, symbol, close, volume FROM ochl_day;

Result:

INSERT OR REPLACE INTO ochl_day VALUES ('2026-09-13', 'CITY', 505.0, 509.0, 503.1, 507.6, 91000)

0 row(s) inserted, 1 replaced

SELECT time, symbol, close, volume FROM ochl_day

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)volume (DOUBLE)
2026-09-13T00:00:00.000ZCITY507.691000

(1 row)

Example: Careful: columns you leave out lose their old values

Setup:

CREATE TABLE users (id LONG PRIMARY KEY, email STRING, phone STRING, verified BOOL DEFAULT FALSE);
INSERT INTO users VALUES (1, 'ram@example.com', '9800000000', TRUE);
INSERT OR REPLACE INTO users (id, email) VALUES (1, 'ram.sharma@example.com');
SELECT id, email, phone, verified FROM users;

Result:

INSERT OR REPLACE INTO users (id, email) VALUES (1, 'ram.sharma@example.com')

0 row(s) inserted, 1 replaced

SELECT id, email, phone, verified FROM users

id (LONG)email (STRING)phone (STRING)verified (BOOL)
1ram.sharma@example.comNULLfalse

(1 row)

INSERT OR IGNORE

Syntax:

INSERT OR IGNORE INTO table [(column, ...)] VALUES (...), ... [RETURNING ...]

Adds rows with new keys, and skips rows whose key is already there. The old row stays as it is.

Example: Load a trade log file again: old trades stay, new ones are added

Setup:

CREATE TABLE trade_log (date TIMESTAMP ROUND 1d, transaction_id LONG, symbol SYMBOL, quantity LONG, rate DOUBLE) TIMESERIES(date) PARTITION BY DAY DEDUP KEY (date, transaction_id);
INSERT INTO trade_log VALUES ('2026-09-10', 100009, 'COAST', 50, 400.0);
INSERT OR IGNORE INTO trade_log VALUES
  ('2026-09-10', 100009, 'COAST', 1, 1.0),
  ('2026-09-10', 100010, 'CITY', 50, 517.0);
SELECT transaction_id, symbol, quantity, rate FROM trade_log ORDER BY transaction_id;

Result:

INSERT OR IGNORE INTO trade_log VALUES ('2026-09-10', 100009, 'COAST', 1, 1.0), ('2026-09-10', 10001

1 row(s) inserted, 1 skipped (already there)

SELECT transaction_id, symbol, quantity, rate FROM trade_log ORDER BY transaction_id

transaction_id (LONG)symbol (SYMBOL)quantity (LONG)rate (DOUBLE)
100009COAST50400
100010CITY50517

(2 rows)

RETURNING

Syntax:

INSERT ... VALUES (...) RETURNING expression [[AS] alias], ... | *

Gives the inserted rows back, like a SELECT, with AUTO ids and DEFAULT values filled in. Your app gets the new id in the same statement.

PartMeaning
*all columns, computed columns included
expression [AS alias]columns of the new row, or expressions over them (upper(email))

Example: Register a user and get the new id

Setup:

CREATE TABLE users (id LONG PRIMARY KEY AUTO, email STRING NOT NULL UNIQUE, created TIMESTAMP DEFAULT now());
INSERT INTO users (email) VALUES ('ram@example.com');
INSERT INTO users (email) VALUES ('sita@example.com') RETURNING id, email;

Result:

id (LONG)email (STRING)
2sita@example.com

(1 row)

1 row(s) inserted

Example: Open accounts and see the defaults

Setup:

CREATE TABLE accounts (id INT PRIMARY KEY AUTO, user_id LONG NOT NULL, balance DECIMAL(2) NOT NULL DEFAULT 0, status STRING DEFAULT 'active');
INSERT INTO accounts (user_id, balance) VALUES (1, 1000);
INSERT INTO accounts (user_id) VALUES (2) RETURNING *;

Result:

INSERT INTO accounts (user_id, balance) VALUES (1, 1000)

1 row(s) inserted

INSERT INTO accounts (user_id) VALUES (2) RETURNING *

id (INT)user_id (LONG)balance (DECIMAL(2))status (STRING)
220.00active

(1 row)

1 row(s) inserted

Example: A mistake in RETURNING: nothing is written (shows an error)

Setup:

CREATE TABLE users (id LONG PRIMARY KEY AUTO, email STRING NOT NULL UNIQUE);
INSERT INTO users (email) VALUES ('ram@example.com') RETURNING user_id;
SELECT count(*) FROM users;

Result:

Error: RETURNING: line 1, column 8: unknown column 'user_id' (table 'users' has: id email)

Times and time zones in values

Syntax:

'YYYY-MM-DD[T| ]HH:MM[:SS[.fff]][Z | +HH:MM | +HHMM | -HH:MM]'

KDB keeps every TIMESTAMP in UTC. The text you write is read like this:

You writeKDB reads it as
'2026-09-10T11:00:00+05:45'11:00 Nepal time = 05:15 UTC
'2026-09-10 11:00:00' (no zone)11:00 UTC
'2026-09-10T05:15:00Z'05:15 UTC
'2026-09-10'00:00 UTC
1789017300000milliseconds since 1970-01-01 UTC

Example: Live prices written in Nepal time

Setup:

CREATE TABLE live_data (timestamp TIMESTAMP, symbol SYMBOL, ltp DOUBLE) TIMESERIES(timestamp) DEDUP KEY (timestamp, symbol);
INSERT INTO live_data VALUES
  ('2026-09-10T11:05:00+05:45', 'CITY', 513.0),
  ('2026-09-10T14:59:00+05:45', 'CITY', 527.1);
SELECT timestamp, symbol, ltp FROM live_data;

Result:

INSERT INTO live_data VALUES ('2026-09-10T11:05:00+05:45', 'CITY', 513.0), ('2026-09-10T14:59:00+05:

2 row(s) inserted

SELECT timestamp, symbol, ltp FROM live_data

timestamp (TIMESTAMP)symbol (SYMBOL)ltp (DOUBLE)
2026-09-10T05:20:00.000ZCITY513
2026-09-10T09:14:00.000ZCITY527.1

(2 rows)

Example: Nepal time without +05:45 is stored 5:45 hours too late

Setup:

CREATE TABLE live_data (timestamp TIMESTAMP, symbol SYMBOL, ltp DOUBLE) TIMESERIES(timestamp);
INSERT INTO live_data VALUES ('2026-09-10 11:05:00', 'CITY', 513.0);
SELECT timestamp, symbol, ltp FROM live_data;

Result:

INSERT INTO live_data VALUES ('2026-09-10 11:05:00', 'CITY', 513.0)

1 row(s) inserted

SELECT timestamp, symbol, ltp FROM live_data

timestamp (TIMESTAMP)symbol (SYMBOL)ltp (DOUBLE)
2026-09-10T11:05:00.000ZCITY513

(1 row)

Example: A date in the wrong format (shows an error)

Setup:

CREATE TABLE live_data (timestamp TIMESTAMP, symbol SYMBOL, ltp DOUBLE) TIMESERIES(timestamp);
INSERT INTO live_data VALUES ('10/09/2026 11:05', 'CITY', 513.0);

Result:

Error: column 'timestamp' (TIMESTAMP): '10/09/2026 11:05' is not a time like '2026-09-11 11:00:00' (UTC) or '2026-09-11T11:00:00+05:45'

INSERT ... SELECT, DEFAULT VALUES

Syntax:

INSERT INTO table [(columns)] SELECT ...
INSERT INTO table DEFAULT VALUES

Copies the rows of a query into a table in one statement.

Example: Copy the closes of one symbol into another table

Setup:

CREATE TABLE ochl_day (time TIMESTAMP, symbol SYMBOL, close DOUBLE) TIMESERIES(time);
CREATE TABLE city_close (time TIMESTAMP, close DOUBLE) TIMESERIES(time);
INSERT INTO ochl_day VALUES ('2026-09-09T09:00:00Z', 'CITY', 510), ('2026-09-10T09:00:00Z', 'CITY', 513), ('2026-09-10T09:00:00Z', 'COAST', 900);
INSERT INTO city_close SELECT time, close FROM ochl_day WHERE symbol = 'CITY';
SELECT * FROM city_close ORDER BY time;

Result:

INSERT INTO city_close SELECT time, close FROM ochl_day WHERE symbol = 'CITY'

2 row(s) inserted

SELECT * FROM city_close ORDER BY time

time (TIMESTAMP)close (DOUBLE)
2026-09-09T09:00:00.000Z510
2026-09-10T09:00:00.000Z513

(2 rows)

ON CONFLICT (PostgreSQL upsert)

Syntax:

INSERT INTO table (columns) VALUES (...) | SELECT ...
  ON CONFLICT [(key columns) [WHERE condition] | ON CONSTRAINT name]
  DO NOTHING
  | DO UPDATE SET column = EXCLUDED.column, ... [WHERE condition]
  [RETURNING ..., (xmax = 0) AS inserted]

What to do when a new row has the same key as a row that is there.

Example: Count visits per page: insert the first, update the rest

Setup:

CREATE TABLE page_hits (page STRING PRIMARY KEY, hits LONG NOT NULL);
INSERT INTO page_hits VALUES ('/chart', 1);
INSERT INTO page_hits (page, hits) VALUES ('/chart', 1), ('/news', 1)
ON CONFLICT (page) DO UPDATE SET hits = page_hits.hits + EXCLUDED.hits
RETURNING page, hits, (xmax = 0) AS inserted;

Result:

page (STRING)hits (LONG)inserted (BOOL)
/chart2false
/news1true

(2 rows)

1 row(s) inserted, 1 updated

Example: One open job per key: a partial unique index

Setup:

CREATE TABLE jobs (id INT, dedup_key STRING, status STRING);
CREATE UNIQUE INDEX jobs_open ON jobs (dedup_key) WHERE status <> 'done';
INSERT INTO jobs VALUES (1, 'report', 'queued');
INSERT INTO jobs VALUES (2, 'report', 'queued') ON CONFLICT DO NOTHING;
INSERT INTO jobs VALUES (3, 'report', 'queued') ON CONFLICT (dedup_key) WHERE status <> 'done' DO NOTHING;
SELECT id, status FROM jobs;

Result:

INSERT INTO jobs VALUES (2, 'report', 'queued') ON CONFLICT DO NOTHING

0 row(s) inserted, 1 skipped (already there)

INSERT INTO jobs VALUES (3, 'report', 'queued') ON CONFLICT (dedup_key) WHERE status != 'done' DO NO

0 row(s) inserted, 1 skipped (already there)

SELECT id, status FROM jobs

id (INT)status (STRING)
1queued

(1 row)