KDB knowledge

UPDATE and DELETE

Change rows with UPDATE, remove rows with DELETE, empty tables with TRUNCATE, get the changed rows back with RETURNING, and fix wrong data safely.

UPDATE changes values in rows that are already there. DELETE removes rows. Both take a WHERE condition that says which rows. Like INSERT, one statement is all or nothing, and it is saved as soon as KDB says updated or deleted.

Warning: without WHERE, UPDATE changes every row and DELETE removes every row. Before a big change, run SELECT count(*) with the same WHERE and check the number.

Example: Make every VOLT daily volume of 2026-09 ten percent bigger: count first, then change (sample data: sample)

SELECT count(*) AS rows FROM ochl_day WHERE symbol = 'VOLT' AND time IN '2026-09';
UPDATE ochl_day SET volume = round(volume * 1.1) WHERE symbol = 'VOLT' AND time IN '2026-09';

Result:

SELECT count(*) AS rows FROM ochl_day WHERE symbol = 'VOLT' AND time IN '2026-09'

rows (LONG)
8

(1 row)

UPDATE ochl_day SET volume = round(volume * 1.1) WHERE symbol = 'VOLT' AND time IN '2026-09'

8 row(s) updated

UPDATE

Syntax:

UPDATE table SET column = expression [, column = expression ...] [WHERE condition] [RETURNING ...]

Sets new values in every row where condition is true. The expression can use the old values of the same row: balance = balance - 250.50.

PartMeaning
SET column = expressionthe new value; each column at most once
WHERE conditionwhich rows; without it: all rows. See WHERE
RETURNINGgive the changed rows back; see RETURNING

UPDATE ... FROM

Syntax:

UPDATE table SET column = expression FROM other_table [alias] [, ...] WHERE join_condition
UPDATE table SET column = expression FROM (SELECT ...) alias WHERE join_condition

Takes the new values from another table or from a subquery (PostgreSQL style). FROM can list more than one table, or a JOIN.

Example: Copy corrected prices from another table

Setup:

CREATE TABLE trade_log (id LONG PRIMARY KEY, symbol SYMBOL, price DOUBLE);
CREATE TABLE price_fix (id LONG PRIMARY KEY, price DOUBLE);
INSERT INTO trade_log VALUES (1, 'ACME', 10.5), (2, 'CITY', 20.0), (3, 'VOLT', 30.0);
INSERT INTO price_fix VALUES (1, 11.0), (3, 33.0);
UPDATE trade_log SET price = f.price FROM price_fix f WHERE trade_log.id = f.id RETURNING id, symbol, price;
SELECT * FROM trade_log ORDER BY id;

Result:

UPDATE trade_log SET price = f.price FROM price_fix AS f WHERE trade_log.id = f.id RETURNING id, sym

id (LONG)symbol (SYMBOL)price (DOUBLE)
1ACME11
3VOLT33

(2 rows)

2 row(s) updated

SELECT * FROM trade_log ORDER BY id

id (LONG)symbol (SYMBOL)price (DOUBLE)
1ACME11
2CITY20
3VOLT33

(3 rows)

Example: UPDATE ... FROM a subquery

Setup:

CREATE TABLE trade_log (id LONG PRIMARY KEY, symbol SYMBOL, price DOUBLE);
CREATE TABLE price_fix (id LONG, price DOUBLE);
INSERT INTO trade_log VALUES (1, 'ACME', 10.5), (2, 'CITY', 20.0), (3, 'VOLT', 30.0);
INSERT INTO price_fix VALUES (1, 11.0), (1, 12.0), (3, 33.0);
UPDATE trade_log SET price = s.last_price
FROM (SELECT id, max(price) AS last_price FROM price_fix GROUP BY id) s
WHERE trade_log.id = s.id;
SELECT * FROM trade_log ORDER BY id;

Result:

UPDATE trade_log SET price = s.last_price FROM (SELECT id, max(price) AS last_price FROM price_fix G

2 row(s) updated

SELECT * FROM trade_log ORDER BY id

id (LONG)symbol (SYMBOL)price (DOUBLE)
1ACME12
2CITY20
3VOLT33

(3 rows)

Example: Take 250.50 from an account and give it to another

Setup:

CREATE TABLE accounts (id INT PRIMARY KEY, owner STRING NOT NULL, balance DECIMAL(2) NOT NULL);
INSERT INTO accounts VALUES (1, 'Ram', 1000.00), (2, 'Sita', 0.00);
UPDATE accounts SET balance = balance - 250.50 WHERE id = 1;
UPDATE accounts SET balance = balance + 250.50 WHERE id = 2;
SELECT id, owner, balance FROM accounts ORDER BY id;

Result:

UPDATE accounts SET balance = balance - 250.50 WHERE id = 1

1 row(s) updated

UPDATE accounts SET balance = balance + 250.50 WHERE id = 2

1 row(s) updated

SELECT id, owner, balance FROM accounts ORDER BY id

id (INT)owner (STRING)balance (DECIMAL(2))
1Ram749.50
2Sita250.50

(2 rows)

Example: Edit a forum post and mark it edited

Setup:

CREATE TABLE posts (id LONG PRIMARY KEY AUTO, author STRING NOT NULL, body STRING NOT NULL, edited BOOL NOT NULL DEFAULT FALSE);
INSERT INTO posts (author, body) VALUES ('ram', 'CITY dividend looks god.'), ('sita', 'I agree.');
UPDATE posts SET body = 'CITY dividend looks good.', edited = TRUE WHERE id = 1;
SELECT id, author, body, edited FROM posts ORDER BY id;

Result:

UPDATE posts SET body = 'CITY dividend looks good.', edited = TRUE WHERE id = 1

1 row(s) updated

SELECT id, author, body, edited FROM posts ORDER BY id

id (LONG)author (STRING)body (STRING)edited (BOOL)
1ramCITY dividend looks good.true
2sitaI agree.false

(2 rows)

Example: Round the TELCO live prices (many columns at once) (sample data: sample)

UPDATE live_data SET ltp = round(ltp, 0), point_change = round(point_change, 0) WHERE symbol = 'TELCO';
SELECT timestamp, symbol, ltp, point_change FROM live_data WHERE symbol = 'TELCO';

Result:

UPDATE live_data SET ltp = round(ltp, 0), point_change = round(point_change, 0) WHERE symbol = 'TELC

3 row(s) updated

SELECT timestamp, symbol, ltp, point_change FROM live_data WHERE symbol = 'TELCO'

timestamp (TIMESTAMP)symbol (SYMBOL)ltp (DOUBLE)point_change (DOUBLE)
2026-09-10T05:20:00.000ZTELCO861-21
2026-09-10T07:15:00.000ZTELCO869-12
2026-09-10T09:14:00.000ZTELCO89817

(3 rows)

Example: Moving a candle onto a day that already has one (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-13', 'CITY', 507.6), ('2026-09-14', 'CITY', 511.0);
UPDATE ochl_day SET time = '2026-09-13' WHERE symbol = 'CITY' AND time IN '2026-09-14';

Result:

Error: the update would create a second row with symbol='CITY', time='2026-09-13T00:00:00.000Z' in 'ochl_day'

Example: A subquery in UPDATE

Setup:

CREATE TABLE accounts (id INT PRIMARY KEY, balance DECIMAL(2));
INSERT INTO accounts VALUES (1, 10.00), (2, 20.00);
UPDATE accounts SET balance = (SELECT max(balance) FROM accounts) WHERE id = 1;
SELECT * FROM accounts ORDER BY id;

Result:

UPDATE accounts SET balance = (SELECT max(balance) FROM accounts) WHERE id = 1

1 row(s) updated

SELECT * FROM accounts ORDER BY id

id (INT)balance (DECIMAL(2))
120.00
220.00

(2 rows)

DELETE

Syntax:

DELETE FROM table [WHERE condition] [RETURNING ...]

Removes every row where condition is true. Without WHERE, it removes all rows, but the table stays.

Example: Delete the trades of broker 34 on 2026-09-09 (sample data: sample)

SELECT count(*) AS trades FROM trade_log WHERE date IN '2026-09-09' AND (buyer = 34 OR seller = 34);
DELETE FROM trade_log WHERE date IN '2026-09-09' AND (buyer = 34 OR seller = 34);
SELECT count(*) AS trades FROM trade_log WHERE date IN '2026-09-09' AND (buyer = 34 OR seller = 34);

Result:

SELECT count(*) AS trades FROM trade_log WHERE date IN '2026-09-09' AND (buyer = 34 OR seller = 34)

trades (LONG)
4

(1 row)

DELETE FROM trade_log WHERE date IN '2026-09-09' AND (buyer = 34 OR seller = 34)

4 row(s) deleted

SELECT count(*) AS trades FROM trade_log WHERE date IN '2026-09-09' AND (buyer = 34 OR seller = 34)

trades (LONG)
0

(1 row)

Example: Remove app sessions that have ended

Setup:

CREATE TABLE sessions (token STRING PRIMARY KEY, user_id LONG NOT NULL, expires TIMESTAMP NOT NULL);
INSERT INTO sessions VALUES ('a1', 1, '2026-09-01T00:00:00Z'), ('b2', 2, '2026-09-30T00:00:00Z'), ('c3', 1, '2026-08-15T00:00:00Z');
DELETE FROM sessions WHERE expires < '2026-09-11T00:00:00Z';
SELECT token, user_id, expires FROM sessions;

Result:

DELETE FROM sessions WHERE expires < '2026-09-11T00:00:00Z'

2 row(s) deleted

SELECT token, user_id, expires FROM sessions

token (STRING)user_id (LONG)expires (TIMESTAMP)
b222026-09-30T00:00:00.000Z

(1 row)

Example: Delete bars by a list of (symbol, day) keys

Setup:

CREATE TABLE bars (time TIMESTAMP ROUND 1d TIME ZONE 'Asia/Kathmandu', symbol SYMBOL, close DOUBLE)
  TIMESERIES(time) PARTITION BY MONTH DEDUP KEY (symbol, time);
INSERT INTO bars VALUES ('2026-07-01T00:00:00+05:45', 'CITY', 500), ('2026-08-02T00:00:00+05:45', 'CITY', 505),
  ('2026-09-10T00:00:00+05:45', 'CITY', 512), ('2026-09-10T00:00:00+05:45', 'COAST', 376);
-- Only the July and September partitions are read, and only these keys in them.
DELETE FROM bars WHERE (symbol, time) IN (('CITY', '2026-07-01T00:00:00+05:45'), ('COAST', '2026-09-10T00:00:00+05:45'));
SELECT symbol, to_char(time, 'YYYY-MM-DD', 'Asia/Kathmandu') AS nepal_day, close FROM bars ORDER BY time, symbol;

Result:

DELETE FROM bars WHERE (symbol, time) IN (('CITY', '2026-07-01T00:00:00+05:45'), ('COAST', '2026-09-

2 row(s) deleted

SELECT symbol, to_char(time, 'YYYY-MM-DD', 'Asia/Kathmandu') AS nepal_day, close FROM bars ORDER BY

symbol (SYMBOL)nepal_day (STRING)close (DOUBLE)
CITY2026-08-02505
CITY2026-09-10512

(2 rows)

Example: Remove all rows, keep the table

Setup:

CREATE TABLE import_staging (symbol SYMBOL, close DOUBLE);
INSERT INTO import_staging VALUES ('CITY', 504.9), ('COAST', 376.1);
DELETE FROM import_staging;
SELECT count(*) AS rows FROM import_staging;

Result:

DELETE FROM import_staging

2 row(s) deleted

SELECT count(*) AS rows FROM import_staging

rows (LONG)
0

(1 row)

Example: A broker with trades cannot be deleted (sample data: sample; shows an error)

DELETE FROM brokers WHERE id = 58;

Result:

Error: cannot remove brokers(id) = '58': table 'trade_log' column 'buyer' still refers to it (foreign key 'trade_log_buyer_fkey')

TRUNCATE

Syntax:

TRUNCATE [TABLE] table [, table ...] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT]

Empties tables and keeps them: their columns, indexes, triggers and rights stay. It drops the table's files, as DROP PARTITION does, instead of deleting row by row. So it takes the same short time for ten rows or a hundred million.

PartMeaning
table, ...one or more tables (not views)
RESTART IDENTITYAUTO ids start again at 1
CONTINUE IDENTITYAUTO ids go on from where they were (the default)
CASCADEalso empty the tables whose REFERENCES point at these tables
RESTRICTthe default: refuse when another table points at one of them

Example: Empty a staging table before the next import

Setup:

CREATE TABLE import_staging (time TIMESTAMP, symbol SYMBOL, close DOUBLE) TIMESERIES(time) PARTITION BY DAY;
INSERT INTO import_staging VALUES ('2026-09-10', 'CITY', 504.9), ('2026-09-11', 'COAST', 376.1);
TRUNCATE import_staging;
SELECT count(*) AS rows FROM import_staging;

Result:

TRUNCATE TABLE import_staging

1 table(s) truncated (2 row(s) removed)

SELECT count(*) AS rows FROM import_staging

rows (LONG)
0

(1 row)

Example: Brokers and the trades that point at them, ids from 1 again

Setup:

CREATE TABLE brokers (id INT PRIMARY KEY, name STRING NOT NULL);
CREATE TABLE trades (id LONG PRIMARY KEY AUTO, buyer INT REFERENCES brokers(id), qty LONG);
INSERT INTO brokers VALUES (1, 'Keystone Securities'), (45, 'Imperial Securities');
INSERT INTO trades (buyer, qty) VALUES (1, 10), (45, 20);
TRUNCATE TABLE brokers CASCADE RESTART IDENTITY;
INSERT INTO brokers VALUES (58, 'Northgate Securities');
INSERT INTO trades (buyer, qty) VALUES (58, 5);
SELECT id, buyer, qty FROM trades;

Result:

TRUNCATE TABLE brokers RESTART IDENTITY CASCADE

2 table(s) truncated (4 row(s) removed, AUTO ids start again at 1)

INSERT INTO brokers VALUES (58, 'Northgate Securities')

1 row(s) inserted

INSERT INTO trades (buyer, qty) VALUES (58, 5)

1 row(s) inserted

SELECT id, buyer, qty FROM trades

id (LONG)buyer (INT)qty (LONG)
1585

(1 row)

Example: Another table points at it (shows an error)

Setup:

CREATE TABLE brokers (id INT PRIMARY KEY, name STRING NOT NULL);
CREATE TABLE trades (id LONG PRIMARY KEY AUTO, buyer INT REFERENCES brokers(id));
TRUNCATE brokers;

Result:

Error: cannot truncate 'brokers': table 'trades' has a foreign key to it (trades_buyer_fkey); truncate both in one statement, or add CASCADE

Example: Not inside a transaction (shows an error)

Setup:

CREATE TABLE notes (id INT PRIMARY KEY, body STRING);
BEGIN;
TRUNCATE notes;

Result:

transaction started

Error: TRUNCATE cannot be used inside a transaction or a procedure: KDB cannot undo it. COMMIT or ROLLBACK first, or use DELETE FROM table, which ROLLBACK undoes (the transaction was rolled back)

RETURNING

Syntax:

UPDATE ... [WHERE ...] RETURNING expression [[AS] alias], ... | *
DELETE FROM ... [WHERE ...] RETURNING expression [[AS] alias], ... | *

Gives rows back like a SELECT: for UPDATE the rows after the change, for DELETE the rows as they were before they were removed.

Example: Withdraw money and get the new balance in one statement

Setup:

CREATE TABLE accounts (id INT PRIMARY KEY, owner STRING NOT NULL, balance DECIMAL(2) NOT NULL);
INSERT INTO accounts VALUES (1, 'Ram', 1000.00);
UPDATE accounts SET balance = balance - 250.50 WHERE id = 1 RETURNING id, owner, balance;

Result:

id (INT)owner (STRING)balance (DECIMAL(2))
1Ram749.50

(1 row)

1 row(s) updated

Example: Delete old news and see what was removed

Setup:

CREATE TABLE news (id LONG PRIMARY KEY AUTO, title STRING NOT NULL, published_at TIMESTAMP NOT NULL);
INSERT INTO news (title, published_at) VALUES
  ('Old IPO notice', '2025-01-10T10:00:00+05:45'),
  ('City Bank profit rises 18%', '2026-08-01T11:00:00+05:45');
DELETE FROM news WHERE published_at < '2026-01-01' RETURNING id, title;

Result:

id (LONG)title (STRING)
1Old IPO notice

(1 row)

1 row(s) deleted

Fix wrong rows safely

Syntax:

1. SELECT count(*) ... WHERE condition        -- look, count
2. BACKUP DATABASE  (or kdb backup)           -- a way back
3. DELETE FROM ... WHERE condition            -- the same WHERE
4. INSERT OR REPLACE INTO ... VALUES (...)    -- the correct rows
5. SELECT ... WHERE condition                 -- check

Bad data happens: a wrong file is imported, a price is ten times too big, a day is loaded twice. Use these steps.

Example: A bad import made COAST minute prices ten times too big: find, delete, load again, check (sample data: sample)

Setup:

UPDATE ochl_minutes SET open = open * 10, high = high * 10, low = low * 10, close = close * 10
WHERE symbol = 'COAST' AND time IN '2026-09-10';
SELECT count(*) AS rows, min(low) AS min_low, max(high) AS max_high FROM ochl_minutes WHERE symbol = 'COAST' AND time IN '2026-09-10';
DELETE FROM ochl_minutes WHERE symbol = 'COAST' AND time IN '2026-09-10';
INSERT OR REPLACE INTO ochl_minutes VALUES
  ('2026-09-10T11:00:00+05:45', 'COAST', 380.1, 380.4, 379.8, 380.2, 900),
  ('2026-09-10T11:01:00+05:45', 'COAST', 380.2, 380.5, 380.0, 380.3, 650);
SELECT time, symbol, open, high, low, close, volume FROM ochl_minutes WHERE symbol = 'COAST' AND time IN '2026-09-10';

Result:

SELECT count(*) AS rows, min(low) AS min_low, max(high) AS max_high FROM ochl_minutes WHERE symbol =

rows (LONG)min_low (DOUBLE)max_high (DOUBLE)
23637323820

(1 row)

DELETE FROM ochl_minutes WHERE symbol = 'COAST' AND time IN '2026-09-10'

236 row(s) deleted

INSERT OR REPLACE INTO ochl_minutes VALUES ('2026-09-10T11:00:00+05:45', 'COAST', 380.1, 380.4, 379.

2 row(s) inserted

SELECT time, symbol, open, high, low, close, volume FROM ochl_minutes WHERE symbol = 'COAST' AND tim

time (TIMESTAMP)symbol (SYMBOL)open (DOUBLE)high (DOUBLE)low (DOUBLE)close (DOUBLE)volume (DOUBLE)
2026-09-10T05:15:00.000ZCOAST380.1380.4379.8380.2900
2026-09-10T05:16:00.000ZCOAST380.2380.5380380.3650

(2 rows)

To replace all rows of one symbol (prices adjusted for a bonus share, say), load the new rows into a table with the same columns first, then swap them in one transaction. Readers see the old rows or the new ones, never an empty chart. When the symbol fills its partitions, the DELETE drops them and the INSERT copies the new table's partitions whole: 239,326 minute bars take about 0.1 s.

Example: Re-adjust one symbol's whole history in one transaction

Setup:

CREATE TABLE city_min (time TIMESTAMP, symbol SYMBOL, close DOUBLE) TIMESERIES(time) PARTITION BY YEAR SORT BY (symbol, time) DEDUP KEY (symbol, time);
CREATE TABLE city_adj (time TIMESTAMP, symbol SYMBOL, close DOUBLE) TIMESERIES(time) PARTITION BY YEAR SORT BY (symbol, time) DEDUP KEY (symbol, time);
INSERT INTO city_min VALUES ('2025-12-31T09:00:00Z', 'CITY', 500), ('2026-09-10T09:00:00Z', 'CITY', 520);
INSERT INTO city_adj SELECT time, symbol, round(close * 0.95, 2) FROM city_min;
BEGIN;
DELETE FROM city_min WHERE symbol = 'CITY';
INSERT INTO city_min SELECT time, symbol, close FROM city_adj;
COMMIT;
SELECT time, symbol, close FROM city_min ORDER BY time;

Result:

BEGIN

transaction started

DELETE FROM city_min WHERE symbol = 'CITY'

2 row(s) deleted

INSERT INTO city_min SELECT time, symbol, close FROM city_adj

2 row(s) inserted

COMMIT

transaction committed: 3 change(s) saved

SELECT time, symbol, close FROM city_min ORDER BY time

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)
2025-12-31T09:00:00.000ZCITY475
2026-09-10T09:00:00.000ZCITY494

(2 rows)

Example: Try an UPDATE, look, and roll it back

Setup:

CREATE TABLE accounts (id INT PRIMARY KEY, owner STRING NOT NULL, balance DECIMAL(2) NOT NULL);
INSERT INTO accounts VALUES (1, 'Ram', 1000.00), (2, 'Sita', 500.00);
BEGIN;
UPDATE accounts SET balance = balance * 2;
SELECT id, owner, balance FROM accounts ORDER BY id;
ROLLBACK;
SELECT id, owner, balance FROM accounts ORDER BY id;

Result:

BEGIN

transaction started

UPDATE accounts SET balance = balance * 2

2 row(s) updated

SELECT id, owner, balance FROM accounts ORDER BY id

id (INT)owner (STRING)balance (DECIMAL(2))
1Ram2000.00
2Sita1000.00

(2 rows)

ROLLBACK

transaction rolled back: 1 change(s) undone

SELECT id, owner, balance FROM accounts ORDER BY id

id (INT)owner (STRING)balance (DECIMAL(2))
1Ram1000.00
2Sita500.00

(2 rows)

Example: Nepal times stored without +05:45: move them back 5 h 45 min

Setup:

CREATE TABLE live_data (timestamp TIMESTAMP, symbol SYMBOL, ltp DOUBLE) TIMESERIES(timestamp) DEDUP KEY (timestamp, symbol);
INSERT INTO live_data VALUES ('2026-09-10 11:05:00', 'CITY', 513.0), ('2026-09-10 14:59:00', 'CITY', 527.1);
UPDATE live_data SET timestamp = timestamp - 20700000 WHERE symbol = 'CITY' AND timestamp IN '2026-09-10';
SELECT timestamp, symbol, ltp FROM live_data;

Result:

UPDATE live_data SET timestamp = timestamp - 20700000 WHERE symbol = 'CITY' AND timestamp IN '2026-0

2 row(s) updated

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)