Stored procedures
Save many SQL steps under one name and run them with CALL, as one transaction.
- Category: Stored procedures
- Keywords: procedure, stored procedure, CALL, PROCEDURES, DEBUG, WITH DEBUG, debug, print result, CREATE OR REPLACE PROCEDURE, one transaction, call each other too deeply, READ ONLY, read-only procedure, cannot be READ ONLY, --read-only-calls, KDB_READ_ONLY_CALLS, rounding, 6 decimals, match old system
- See also: The procedure language, Triggers, Transactions, Saved functions, 13. Functions, procedures and triggers
A stored procedure (or just "procedure") is a list of SQL statements with a name. You run it with CALL name(...).
KDB keeps it in the database, like a table. Your app, the web console and kdb sql can all call it.
- A procedure can have variables,
IF, loops, cursors and errors (see the procedure language). - A
CALLis one transaction (a group of changes that are saved together, or not at all). If one step fails, nothing of theCALLis saved.
A real job: after the market closes, a trading app builds the daily candle of each share from the one-minute candles.
Example: End of day: build the daily candles from the minute candles (sample data: sample)
CREATE PROCEDURE end_of_day(day DATE) AS BEGIN
DECLARE minutes LONG DEFAULT 0;
SET minutes = (SELECT count(*) FROM ochl_minutes
WHERE time >= day AND time < dateadd('d', 1, day));
IF minutes = 0 THEN
RAISE concat('no minute candles for ', day);
END IF;
FOR c IN (SELECT symbol, first(open) AS open, max(high) AS high, min(low) AS low,
last(close) AS close, sum(volume) AS volume
FROM ochl_minutes
WHERE time >= day AND time < dateadd('d', 1, day)
GROUP BY symbol) DO
INSERT OR REPLACE INTO ochl_day VALUES (day, c.symbol, c.open, c.high, c.low, c.close, c.volume);
END FOR;
PRINT concat(minutes, ' minute candles read');
SELECT symbol, open, high, low, close, volume FROM ochl_day WHERE time = day ORDER BY symbol;
END;
CALL end_of_day('2026-09-10');
Result:
CREATE PROCEDURE end_of_day(day DATE) AS BEGIN
procedure 'end_of_day' created
CALL end_of_day('2026-09-10')
procedure 'end_of_day' done: 2 row(s) changed
SELECT symbol, open, high, low, close, volume FROM ochl_day WHERE time = day ORDER BY symbol
| symbol (SYMBOL) | open (DOUBLE) | high (DOUBLE) | low (DOUBLE) | close (DOUBLE) | volume (DOUBLE) |
|---|---|---|---|---|---|
| CITY | 504.6 | 507 | 500.7 | 504.9 | 183040 |
| COAST | 380.1 | 382 | 373.2 | 376.1 | 186200 |
| VOLT | 185.6 | 192.1 | 184.2 | 191.2 | 41470 |
(3 rows)
print
| output (STRING) |
|---|
| 476 minute candles read |
(1 row)
CREATE PROCEDURE
Syntax:
CREATE [OR REPLACE] PROCEDURE name(parameter TYPE, ...) [READ ONLY] AS BEGIN
statement;
statement;
...
END
Saves a procedure. OR REPLACE changes one that already exists.
| Part | Meaning |
|---|---|
name | the procedure name. Upper and lower case are the same. |
parameter TYPE | an input value and its type, like day DATE or amt DECIMAL(2). Write () for none. |
READ ONLY | optional: the procedure changes no data. KDB refuses a body that does, and every CALL of it runs like a SELECT. See a CALL that only reads. |
BEGIN ... END | the body. Every statement in it ends with ;. |
What a body can hold: SELECT, INSERT, UPDATE, DELETE, CALL, SHOW ..., EXPLAIN, and the procedure statements DECLARE, SET, IF, WHILE, FOR, OPEN, FETCH, CLOSE, RETURN, RAISE and PRINT (the procedure language).
- Returns:
procedure 'name' created(orreplaced). - Notes:
CREATE,DROP,ALTER,BACKUP,BEGIN,COMMITandROLLBACKcannot be used inside a body. ACALLis already one transaction.- KDB checks the grammar of the body when you create the procedure. Tables and columns are checked only when it runs, so a procedure can use a table that you create later.
foundcannot be a parameter name: every procedure already has a variableFOUND.- A column name wins over a parameter with the same name. Give parameters names that are not column names (
amt, notamount;wanted, notname). CREATE PROCEDUREcannot be used insideBEGIN ... COMMIT.- In
kdb shell, KDB knows thatCREATE PROCEDURE ... BEGIN ... END;is one statement, even with;inside.
Setup used by the examples below:
-- A small bank: accounts, and a ledger with one row for every transfer.
CREATE TABLE accounts (
id INT PRIMARY KEY,
owner STRING NOT NULL,
balance DECIMAL(2) NOT NULL DEFAULT 0
);
CREATE TABLE ledger (
id LONG PRIMARY KEY AUTO,
from_account INT NOT NULL REFERENCES accounts(id),
to_account INT NOT NULL REFERENCES accounts(id),
amount DECIMAL(2) NOT NULL,
created TIMESTAMP DEFAULT now()
);
INSERT INTO accounts VALUES (1, 'Ram', 1000), (2, 'Sita', 0), (3, 'Hari', 500);
Example: Accounting: move money between two accounts (double entry)
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
DECLARE have DECIMAL(2);
IF amt <= 0 THEN
RAISE 'the amount must be more than 0';
END IF;
SET have = (SELECT balance FROM accounts WHERE id = from_id);
IF have IS NULL THEN
RAISE concat('account ', from_id, ' does not exist');
ELSIF have < amt THEN
RAISE concat('not enough money: account ', from_id, ' has ', have, ', the transfer needs ', amt);
END IF;
UPDATE accounts SET balance = balance - amt WHERE id = from_id;
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
INSERT INTO ledger (from_account, to_account, amount) VALUES (from_id, to_id, amt);
PRINT concat('moved ', amt, ' from account ', from_id, ' to account ', to_id);
END;
CALL transfer(1, 2, 250.50);
SELECT id, owner, balance FROM accounts ORDER BY id;
Result:
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
procedure 'transfer' created
CALL transfer(1, 2, 250.50)
procedure 'transfer' done: 3 row(s) changed
print
| output (STRING) |
|---|
| moved 250.50 from account 1 to account 2 |
(1 row)
SELECT id, owner, balance FROM accounts ORDER BY id
| id (INT) | owner (STRING) | balance (DECIMAL(2)) |
|---|---|---|
| 1 | Ram | 749.50 |
| 2 | Sita | 250.50 |
| 3 | Hari | 500.00 |
(3 rows)
Example: Forum: a new post and its topic counter in one step
CREATE TABLE topics (id INT PRIMARY KEY, title STRING NOT NULL, posts LONG NOT NULL DEFAULT 0);
CREATE TABLE posts (id LONG PRIMARY KEY AUTO, topic_id INT NOT NULL REFERENCES topics(id), author STRING, body STRING, created TIMESTAMP DEFAULT now());
INSERT INTO topics (id, title) VALUES (1, 'CITY quarterly results');
CREATE PROCEDURE add_post(topic INT, who STRING, text STRING) AS BEGIN
IF length(trim(text)) = 0 THEN
RAISE 'a post cannot be empty';
END IF;
INSERT INTO posts (topic_id, author, body) VALUES (topic, who, text);
UPDATE topics SET posts = posts + 1 WHERE id = topic;
END;
CALL add_post(1, 'ram', 'Profit is up 12%.');
CALL add_post(1, 'sita', 'Good for the dividend.');
SELECT id, title, posts FROM topics;
Result:
CREATE TABLE topics (
table 'topics' created
CREATE TABLE posts (
table 'posts' created
INSERT INTO topics (id, title) VALUES (1, 'CITY quarterly results')
1 row(s) inserted
CREATE PROCEDURE add_post(topic INT, who STRING, text STRING) AS BEGIN
procedure 'add_post' created
CALL add_post(1, 'ram', 'Profit is up 12%.')
procedure 'add_post' done: 2 row(s) changed
CALL add_post(1, 'sita', 'Good for the dividend.')
procedure 'add_post' done: 2 row(s) changed
SELECT id, title, posts FROM topics
| id (INT) | title (STRING) | posts (LONG) |
|---|---|---|
| 1 | CITY quarterly results | 2 |
(1 row)
Example: Tables are checked when the procedure runs, not when it is created (shows an error)
CREATE PROCEDURE count_archive() AS BEGIN
SELECT count(*) AS rows_in_archive FROM archive;
END;
CREATE TABLE audit (id INT PRIMARY KEY);
CALL count_archive();
Result:
CREATE PROCEDURE count_archive() AS BEGIN
procedure 'count_archive' created
CREATE TABLE audit (
table 'audit' created
Error: procedure 'count_archive', line 2 col 43: table or view 'archive' does not exist
Example: BEGIN cannot be used inside a body (shows an error)
CREATE PROCEDURE p() AS BEGIN
BEGIN;
UPDATE accounts SET balance = 0;
COMMIT;
END;
Result:
Error: line 2, column 3: BEGIN cannot be used inside a procedure or trigger body: the whole CALL is one transaction (use RAISE to undo all of it)
Example: CREATE cannot be used inside a body (shows an error)
CREATE PROCEDURE make_table() AS BEGIN
CREATE TABLE archive (id INT);
END;
Result:
Error: line 2, column 3: CREATE cannot be used inside a procedure or trigger body; expected SELECT, INSERT, UPDATE, DELETE, CALL, DECLARE, SET, IF, WHILE, FOR, OPEN, FETCH, CLOSE, RETURN, RAISE, PRINT or NOTIFY
Example: The name is already used (shows an error)
CREATE PROCEDURE hello() AS BEGIN PRINT 'hello'; END;
CREATE PROCEDURE hello() AS BEGIN PRINT 'namaste'; END;
Result:
procedure 'hello' created
Error: procedure 'hello' already exists; use CREATE OR REPLACE PROCEDURE to change it
CALL
Syntax:
CALL name(argument, ...)
CALL name()
CALL name(argument, ...) WITH DEBUG
Runs a procedure. Each argument can be a value or an expression, like '2026-09-10' or dateadd('d', -1, now()).
- Returns: more than one result:
- the message
procedure 'name' done: N row(s) changed(N counts the rows changed byINSERT,UPDATEandDELETEin the procedure and in the procedures it calls); - the rows of every
SELECTin the procedure, in order; - a result called
printwith one row (columnoutput) for eachPRINT. The web console,kdb sqlandPOST /execshow them like several statements.
- the message
- Notes:
- Each argument is changed to the type of its parameter:
CALL end_of_day('2026-09-10')turns the text into aDATE. A value that does not fit is an error. - The number of arguments must match.
- Inside the body a parameter or variable is a plain value.
WHERE time >= dayskips partitions like a written date, also withdateadd('d', 1, day)around it. See performance tips. - A procedure may
CALLanother procedure, or itself, at most 16 levels deep. CALLis one transaction: see A CALL is one transaction.- When the
CALLfails, theprintlines from before the error are still returned, next to the error. Nothing that the procedure changed is saved. - Add
WITH DEBUGto see every step: CALL ... WITH DEBUG.
- Each argument is changed to the type of its parameter:
Example: Transfer, then look at the books
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
IF (SELECT balance FROM accounts WHERE id = from_id) < amt THEN
RAISE concat('not enough money in account ', from_id);
END IF;
UPDATE accounts SET balance = balance - amt WHERE id = from_id;
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
INSERT INTO ledger (from_account, to_account, amount) VALUES (from_id, to_id, amt);
SELECT id, owner, balance FROM accounts WHERE id IN (from_id, to_id) ORDER BY id;
PRINT concat('ledger rows now: ', (SELECT count(*) FROM ledger));
END;
CALL transfer(3, 2, 120);
Result:
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
procedure 'transfer' created
CALL transfer(3, 2, 120)
procedure 'transfer' done: 3 row(s) changed
SELECT id, owner, balance FROM accounts WHERE id IN (from_id, to_id) ORDER BY id
| id (INT) | owner (STRING) | balance (DECIMAL(2)) |
|---|---|---|
| 2 | Sita | 120.00 |
| 3 | Hari | 380.00 |
(2 rows)
print
| output (STRING) |
|---|
| ledger rows now: 1 |
(1 row)
Example: A text argument becomes a DATE (sample data: sample)
CREATE PROCEDURE trades_of(day DATE) AS BEGIN
SELECT symbol, count(*) AS trades, sum(quantity) AS shares
FROM trade_log WHERE date = day GROUP BY symbol ORDER BY symbol;
END;
CALL trades_of('2026-09-09');
Result:
CREATE PROCEDURE trades_of(day DATE) AS BEGIN
procedure 'trades_of' created
CALL trades_of('2026-09-09')
procedure 'trades_of' done: 0 row(s) changed
SELECT symbol, count(*) AS trades, sum(quantity) AS shares FROM trade_log WHERE date = day GROUP BY
| symbol (SYMBOL) | trades (LONG) | shares (LONG) |
|---|---|---|
| CITY | 4 | 1510 |
| COAST | 2 | 220 |
| TELCO | 2 | 2000 |
(3 rows)
Example: Wrong number of arguments (shows an error)
CREATE PROCEDURE pay(to_id INT, amt DECIMAL(2)) AS BEGIN
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
END;
CALL pay(2);
Result:
procedure 'pay' created
Error: procedure 'pay' takes 2 argument(s) (to_id INT, amt DECIMAL(2)), got 1
Example: An argument that does not fit the type (shows an error)
CREATE PROCEDURE close_account(account INT) AS BEGIN
DELETE FROM accounts WHERE id = account;
END;
CALL close_account('Ram');
Result:
procedure 'close_account' created
Error: argument 1 of procedure 'close_account': variable 'account' (INT): 'Ram' is not a whole number
Example: No such procedure (shows an error)
CALL no_such_procedure();
Result:
Error: procedure 'no_such_procedure' does not exist (SHOW PROCEDURES lists them)
Example: A procedure that calls itself too often (shows an error)
CREATE PROCEDURE countdown(n INT) AS BEGIN
PRINT n;
IF n > 0 THEN
CALL countdown(n - 1);
END IF;
END;
CALL countdown(3);
CALL countdown(20);
Result:
CREATE PROCEDURE countdown(n INT) AS BEGIN
procedure 'countdown' created
CALL countdown(3)
procedure 'countdown' done: 0 row(s) changed
print
| output (STRING) |
|---|
| 3 |
| 2 |
| 1 |
| 0 |
(4 rows)
print
| output (STRING) |
|---|
| 20 |
| 19 |
| 18 |
| 17 |
| 16 |
| 15 |
| 14 |
| 13 |
| 12 |
| 11 |
| 10 |
| 9 |
| 8 |
| 7 |
| 6 |
| 5 |
(16 rows)
Error: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedure 'countdown', line 4: procedures call each other too deeply (more than 16 levels, at procedure 'countdown')
A CALL is one transaction
Syntax:
CALL name(...) -- one transaction by itself
BEGIN; CALL a(...); CALL b(...); COMMIT; -- part of a bigger transaction
All changes of a CALL are saved together when it ends, with one write to the log (the file where KDB writes every change first).
If any statement fails, or RAISE runs, nothing of the CALL is saved, also not the steps before the error.
- Statements inside the procedure see the changes made earlier in the same
CALL. - Inside
BEGIN ... COMMIT, aCALLis part of that bigger transaction. An error in theCALLrolls back (undoes) the whole transaction. See transactions. - Triggers that the procedure fires are part of the same transaction too.
- At most 10 million changed rows in one transaction.
- Before it starts, a
CALLlocks every table its body (and the procedures and triggers it sets off) reads or changes, until it ends. Writers of other tables go on meanwhile; writers of these tables wait. A body withEXECUTEof text made while it runs locks every table. See one writer per table. - A long
CALLlets readers of its tables in between its statements (reader yield, on unless the server turns it off). One long statement inside it still makes them wait until it ends. Readers of other tables do not wait at all (per-table reads). - A
CALLthat changes nothing is not a transaction at all: it locks nothing and runs like aSELECT. See a CALL that only reads.
In this example the first UPDATE takes 100 from Ram. Then the ledger row fails, because account 99 does not exist. The whole CALL is undone, so Ram still has 1000.
Command line: A failed CALL saves nothing (shows an error)
Setup:
CREATE TABLE accounts (id INT PRIMARY KEY, owner STRING NOT NULL, balance DECIMAL(2) NOT NULL DEFAULT 0);
CREATE TABLE ledger (id LONG PRIMARY KEY AUTO, from_account INT NOT NULL REFERENCES accounts(id), to_account INT NOT NULL REFERENCES accounts(id), amount DECIMAL(2) NOT NULL);
INSERT INTO accounts VALUES (1, 'Ram', 1000), (2, 'Sita', 0);
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
UPDATE accounts SET balance = balance - amt WHERE id = from_id;
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
INSERT INTO ledger (from_account, to_account, amount) VALUES (from_id, to_id, amt);
END;
kdb sql market "CALL transfer(1, 99, 100)"
kdb sql market "SELECT id, owner, balance FROM accounts ORDER BY id"
Output:
> kdb sql market "CALL transfer(1, 99, 100)"
error: procedure 'transfer', line 4: column 'to_account' = '99' has no matching row in accounts(id)
exit code 1
> kdb sql market "SELECT id, owner, balance FROM accounts ORDER BY id"
id | owner | balance
---+-------+--------
1 | Ram | 1000.00
2 | Sita | 0.00
(2 rows, 0.3 ms)
Example: Two CALLs in one transaction, then ROLLBACK
CREATE PROCEDURE deposit(account INT, amt DECIMAL(2)) AS BEGIN
UPDATE accounts SET balance = balance + amt WHERE id = account;
END;
BEGIN;
CALL deposit(2, 100);
CALL deposit(3, 100);
SELECT id, balance FROM accounts ORDER BY id;
ROLLBACK;
SELECT id, balance FROM accounts ORDER BY id;
Result:
CREATE PROCEDURE deposit(account INT, amt DECIMAL(2)) AS BEGIN
procedure 'deposit' created
BEGIN
transaction started
CALL deposit(2, 100)
procedure 'deposit' done: 1 row(s) changed
CALL deposit(3, 100)
procedure 'deposit' done: 1 row(s) changed
SELECT id, balance FROM accounts ORDER BY id
| id (INT) | balance (DECIMAL(2)) |
|---|---|
| 1 | 1000.00 |
| 2 | 100.00 |
| 3 | 600.00 |
(3 rows)
ROLLBACK
transaction rolled back: 2 change(s) undone
SELECT id, balance FROM accounts ORDER BY id
| id (INT) | balance (DECIMAL(2)) |
|---|---|
| 1 | 1000.00 |
| 2 | 0.00 |
| 3 | 500.00 |
(3 rows)
A CALL that only reads
Syntax:
CREATE PROCEDURE name(...) READ ONLY AS BEGIN ... END -- promised: it never changes data
CALL name(...) -- runs like a SELECT
A procedure that only reads (a report, a lookup, a chart query) runs like a SELECT:
- it locks no table and starts no transaction;
- it does not wait for writers of the tables it reads, and it does not make readers wait;
- it sees the last committed rows, like a
SELECT. Changes of an open transaction are not seen. - a long one does not hold writers up either: when a writer waits, it lets it in between two of its statements. So each statement sees the rows committed when that statement starts.
KDB works this out by itself before each CALL, from everything the body runs: the procedures it calls, the views and saved functions it uses.
A CALL stays a normal transaction (it locks its tables) when its body, or anything it calls, has one of these:
| In the body | Why |
|---|---|
INSERT, UPDATE, DELETE | it changes data |
SELECT ... FOR UPDATE | it locks rows for a later change |
nextval, setval, set_config, pg_advisory_..., pg_sleep | they change state, or wait on purpose |
EXECUTE of text made while it runs ('...' || name) | KDB cannot tell what it runs |
SHOW ..., information_schema or pg_catalog | they read the whole database |
EXPLAIN CALL | it works through another procedure's steps |
EXPLAIN of anything else only shows a plan, so it does not count as a change.
Write READ ONLY to make it a promise:
CREATE PROCEDURErefuses a body that changes data, with the reason:procedure 'x' cannot be READ ONLY: it changes table 'accounts'.If a procedure it calls is later replaced by one that writes, its
CALLfails before it runs:procedure 'x' is declared READ ONLY, but it changes table .... Nothing is changed.Notes:
- Inside
BEGIN ... COMMIT, a read-onlyCALLis part of the transaction and sees its changes, like any statement there. kdb serve --read-only-calls off(orKDB_READ_ONLY_CALLS=off) makes everyCALLa transaction again, as before.READ ONLYis then only checked when the procedure is created.- A database with a
READ ONLYprocedure cannot be opened by a KDB older than 2026-09-24 (versions).
- Inside
Example: A report procedure that only reads
CREATE PROCEDURE rich_accounts(at_least DECIMAL(2)) READ ONLY AS BEGIN
SELECT id, owner, balance FROM accounts WHERE balance >= at_least ORDER BY id;
PRINT concat((SELECT count(*) FROM ledger), ' ledger rows');
END;
CALL rich_accounts(100);
Result:
CREATE PROCEDURE rich_accounts(at_least DECIMAL(2)) READ ONLY AS BEGIN
procedure 'rich_accounts' created
CALL rich_accounts(100)
procedure 'rich_accounts' done: 0 row(s) changed
SELECT id, owner, balance FROM accounts WHERE balance >= at_least ORDER BY id
| id (INT) | owner (STRING) | balance (DECIMAL(2)) |
|---|---|---|
| 1 | Ram | 1000.00 |
| 3 | Hari | 500.00 |
(2 rows)
print
| output (STRING) |
|---|
| 0 ledger rows |
(1 row)
Example: READ ONLY refuses a body that changes data (shows an error)
CREATE PROCEDURE empty_accounts() READ ONLY AS BEGIN
UPDATE accounts SET balance = 0;
END;
Result:
Error: procedure 'empty_accounts' cannot be READ ONLY: it changes table 'accounts'. Take READ ONLY away to let it change data
Matching another system's rounding
A DOUBLE keeps about 16 digits: 9609.0 / 17 is 565.2352941176... in KDB.
Many programs round a rate before they store it, for example to 6 places: 565.235294.
When a procedure takes over a job from such a program, round the same way, with round(x, 6):
| Where | Why |
|---|---|
| every rate or ratio the procedure stores | the stored numbers then equal the old program's, digit for digit |
| both sides of a comparison in a filter | a difference in the 7th place can flip a > b, so a row appears or disappears |
roundhalves go away from zero (round(0.5)is1), likemath.Roundin Go.- Compare the two systems' tables to the same number of places. A check that allows a tiny difference does not show that one side stores more digits.
- For money that must add up exactly, use a
DECIMALcolumn instead.
Example: Store the buy rate rounded to 6 places
Setup:
CREATE TABLE broker_day (broker INT, amount DOUBLE, quantity LONG);
INSERT INTO broker_day VALUES (1, 9609, 17), (2, 1245.5, 3);
CREATE TABLE broker_rate (broker INT PRIMARY KEY, buy_rate DOUBLE, buy_rate_full DOUBLE);
CREATE PROCEDURE build_rates() AS BEGIN
DELETE FROM broker_rate;
INSERT INTO broker_rate SELECT broker, round(amount / quantity, 6), amount / quantity FROM broker_day;
END;
CALL build_rates();
SELECT broker, buy_rate, buy_rate_full FROM broker_rate ORDER BY broker;
Result:
CALL build_rates()
procedure 'build_rates' done: 2 row(s) changed
SELECT broker, buy_rate, buy_rate_full FROM broker_rate ORDER BY broker
| broker (INT) | buy_rate (DOUBLE) | buy_rate_full (DOUBLE) |
|---|---|---|
| 1 | 565.235294 | 565.235294117647 |
| 2 | 415.166667 | 415.166666666667 |
(2 rows)
Example: Round both sides before a filter compares them
SELECT 9609.0 / 17 > 565.235294 AS full_is_bigger,
round(9609.0 / 17, 6) > 565.235294 AS rounded_is_bigger;
Result:
| full_is_bigger (BOOL) | rounded_is_bigger (BOOL) |
|---|---|
| true | false |
(1 row)
CALL ... WITH DEBUG
Syntax:
CALL name(argument, ...) WITH DEBUG
Runs the procedure and adds a result called debug, with one row for every step it took.
Use it to find out why a procedure does something you did not expect.
| Column | Meaning |
|---|---|
step | 1, 2, 3, ... in the order the steps started |
routine | the procedure the step is in, or trigger name for a trigger. Empty for the CALL itself. |
statement | the statement, or the control line: IF ... THEN, ELSIF ... THEN, ELSE, WHILE ... DO, FOR r IN (...) DO, FOR r: row 2 of 5 |
rows | rows changed (INSERT, UPDATE, DELETE, CALL), rows returned (SELECT), rows read (OPEN, FOR) |
time_us | the time of the step in microseconds (millionths of a second). The first row is the whole CALL. |
changes | variables that got a new value (have=1000.00), the result of a condition (true or false), the PRINT text, the values of a FOR row, or failed: <error>. A statement that read a table also says partitions read: N; so do IF, WHILE and FOR lines whose query read one, and the CALL row counts the whole call. |
- Returns: the usual results of the
CALL, and thedebugresult last. - Notes:
- With
WITH DEBUG, thePRINTlines are in thedebugresult; there is no separateprintresult. - When the
CALLfails, thedebugrows up to the error are still returned, and the last one saysfailed. - Triggers that run during the
CALLadd their steps too. - At most 100,000 steps are kept. Long statement text is cut after 200 characters.
time_usis different every time you run it.partitions readshows whether a time filter skipped partitions: a step that reads all of them where you expected one needs a look. Rows scanned are not counted. To see the plan without running anything, use EXPLAIN CALL.
- With
Example: Every step of a transfer
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
DECLARE have DECIMAL(2) DEFAULT (SELECT balance FROM accounts WHERE id = from_id);
IF have < amt THEN
RAISE concat('not enough money in account ', from_id);
ELSIF amt >= 500 THEN
PRINT 'big transfer: check it by hand';
END IF;
UPDATE accounts SET balance = balance - amt WHERE id = from_id;
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
INSERT INTO ledger (from_account, to_account, amount) VALUES (from_id, to_id, amt);
END;
CALL transfer(1, 2, 600) WITH DEBUG;
Result:
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
procedure 'transfer' created
CALL transfer(1, 2, 600) WITH DEBUG
procedure 'transfer' done: 3 row(s) changed
debug
| step (LONG) | routine (STRING) | statement (STRING) | rows (LONG) | time_us (LONG) | changes (STRING) |
|---|---|---|---|---|---|
| 1 | CALL transfer(1, 2, 600) WITH DEBUG | 3 | 3276 | ||
| 2 | transfer | DECLARE have DECIMAL(2) DEFAULT (SELECT balance FROM accounts WHERE id = from_id) | 0 | 123 | have=1000.00 |
| 3 | transfer | IF have < amt THEN | 0 | 5 | false |
| 4 | transfer | ELSIF amt >= 500 THEN | 0 | 2 | true |
| 5 | transfer | PRINT 'big transfer: check it by hand' | 0 | 1 | big transfer: check it by hand |
| 6 | transfer | UPDATE accounts SET balance = balance - amt WHERE id = from_id | 1 | 61 | |
| 7 | transfer | UPDATE accounts SET balance = balance + amt WHERE id = to_id | 1 | 17 | |
| 8 | transfer | INSERT INTO ledger (from_account, to_account, amount) VALUES (from_id, to_id, amt) | 1 | 94 |
(8 rows)
Example: The debug rows of a CALL that fails (shows an error)
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
DECLARE have DECIMAL(2) DEFAULT (SELECT balance FROM accounts WHERE id = from_id);
IF have < amt THEN
RAISE concat('not enough money in account ', from_id, ': it has ', have);
END IF;
UPDATE accounts SET balance = balance - amt WHERE id = from_id;
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
END;
CALL transfer(2, 1, 50) WITH DEBUG;
Result:
CREATE PROCEDURE transfer(from_id INT, to_id INT, amt DECIMAL(2)) AS BEGIN
procedure 'transfer' created
debug
| step (LONG) | routine (STRING) | statement (STRING) | rows (LONG) | time_us (LONG) | changes (STRING) |
|---|---|---|---|---|---|
| 1 | CALL transfer(2, 1, 50) WITH DEBUG | 0 | 314 | failed | |
| 2 | transfer | DECLARE have DECIMAL(2) DEFAULT (SELECT balance FROM accounts WHERE id = from_id) | 0 | 157 | have=0.00 |
| 3 | transfer | IF have < amt THEN | 0 | 10 | true |
| 4 | transfer | RAISE concat('not enough money in account ', from_id, ': it has ', have) | 0 | 97 | failed: not enough money in account 2: it has 0.00 |
(4 rows)
Error: procedure 'transfer', line 4: not enough money in account 2: it has 0.00
DROP PROCEDURE
Syntax:
DROP PROCEDURE [IF EXISTS] name
Removes a procedure.
- Returns:
procedure 'name' dropped. WithIF EXISTS, a name that does not exist gives the messageprocedure 'name' does not existand no error. - Notes: KDB does not check who uses the procedure. Other procedures and triggers that
CALLit fail when they run, withprocedure 'name' does not exist.DROP PROCEDUREcannot be used insideBEGIN ... COMMIT.
Example
CREATE PROCEDURE hello() AS BEGIN PRINT 'hello'; END;
DROP PROCEDURE hello;
DROP PROCEDURE IF EXISTS hello;
Result:
CREATE PROCEDURE hello() AS BEGIN
procedure 'hello' created
DROP PROCEDURE hello
procedure 'hello' dropped
DROP PROCEDURE IF EXISTS hello
procedure 'hello' does not exist
Example: A trigger that calls a dropped procedure fails (shows an error)
CREATE TABLE posts (id INT PRIMARY KEY, body STRING);
CREATE PROCEDURE recount() AS BEGIN PRINT 'counting'; END;
CREATE TRIGGER count_posts AFTER INSERT ON posts EXECUTE CALL recount();
DROP PROCEDURE recount;
INSERT INTO posts VALUES (1, 'hello');
Result:
CREATE TABLE posts (
table 'posts' created
CREATE PROCEDURE recount() AS BEGIN
procedure 'recount' created
CREATE TRIGGER count_posts AFTER INSERT ON posts EXECUTE CALL recount()
trigger 'count_posts' created on table 'posts'
DROP PROCEDURE recount
procedure 'recount' dropped
Error: trigger 'count_posts', line 1: procedure 'recount' does not exist (SHOW PROCEDURES lists them)
SHOW PROCEDURES
Syntax:
SHOW PROCEDURES
Lists the procedures of the database, in name order.
| Column | Meaning |
|---|---|
name | the procedure name |
arguments | the parameters and their types |
definition | the whole CREATE PROCEDURE statement, written again by KDB (one statement per line) |
- Notes: the line numbers in error messages (
procedure 'transfer', line 4: ...) count the lines of thisdefinition:CREATE PROCEDURE ... AS BEGINis line 1. The text table ofkdb sqlcuts long text after 48 characters; use--format csvor--format jsonto see all of it.
Example
CREATE PROCEDURE deposit(account INT, amt DECIMAL(2)) AS BEGIN
UPDATE accounts SET balance = balance + amt WHERE id = account;
END;
CREATE PROCEDURE hello() AS BEGIN PRINT 'hello'; END;
SHOW PROCEDURES;
Result:
CREATE PROCEDURE deposit(account INT, amt DECIMAL(2)) AS BEGIN
procedure 'deposit' created
CREATE PROCEDURE hello() AS BEGIN
procedure 'hello' created
SHOW PROCEDURES
| name (STRING) | arguments (STRING) | definition (STRING) |
|---|---|---|
| deposit | account INT, amt DECIMAL(2) | CREATE PROCEDURE deposit(account INT, amt DECIMAL(2)) AS BEGIN UPDATE accounts SET balance = balance + amt WHERE id = account; END |
| hello | CREATE PROCEDURE hello() AS BEGIN PRINT 'hello'; END |
(2 rows)