KDB knowledge

ORDER BY, LIMIT, OFFSET

Sort the result, and give back only the first rows or one page of rows.

ORDER BY sorts the rows of the result. LIMIT keeps only the first rows, and OFFSET skips rows before them. They run last, after WHERE, GROUP BY and the select list, so they work on the final result.

Without ORDER BY, the order of the rows is not promised. Always add ORDER BY when the order matters (a "top 10", a page of posts, a report).

Example: The 3 days with the biggest volume, then the next 3 (sample data: sample)

SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3;
SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3 OFFSET 3;

Result:

SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3

time (TIMESTAMP)symbol (SYMBOL)volume (DOUBLE)
2026-09-09T00:00:00.000ZCOAST186330
2026-09-10T00:00:00.000ZCOAST186200
2026-09-10T00:00:00.000ZCITY183040

(3 rows)

SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3 OFFSET 3

time (TIMESTAMP)symbol (SYMBOL)volume (DOUBLE)
2026-09-09T00:00:00.000ZCITY182390
2026-08-31T00:00:00.000ZCITY159240
2026-09-03T00:00:00.000ZVOLT145930

(3 rows)

ORDER BY

Syntax:

ORDER BY sort_key [ASC | DESC] [NULLS FIRST | NULLS LAST], ...

A sort_key can be:

Sort keyExampleMeaning
an output column name or aliasORDER BY change_pctthe value of that output column
a positionORDER BY 2the 2nd output column (counts from 1)
a table columnORDER BY volumealso when it is not in the select list
an expressionORDER BY abs(close - open)the value of the expression for each row

With many keys, the second key sorts rows that are equal in the first key, and so on.

Example: Top gainers of the day, sorted by an alias (sample data: sample)

SELECT symbol, open, close, round((close - open) / open * 100, 2) AS change_pct
FROM ochl_day
WHERE time IN '2026-09-10'
ORDER BY change_pct DESC;

Result:

symbol (SYMBOL)open (DOUBLE)close (DOUBLE)change_pct (DOUBLE)
VOLT185.6191.23.02
CITY504.6504.90.06
COAST380.1376.1-1.05

(3 rows)

Example: Sector first, then symbol inside each sector (sample data: sample)

SELECT sector, symbol, name FROM securities ORDER BY sector, symbol;

Result:

sector (STRING)symbol (SYMBOL)name (STRING)
BankingCITYCity Bank Limited
BankingCOASTCoastal Bank Limited
EnergyVOLTVolt River Hydropower Limited
InvestmentACMEAcme Investment Company
TelecomTELCONorthwind Telecom Limited

(5 rows)

Example: Sort by a column that is not shown, and by an expression (sample data: sample)

SELECT symbol, close FROM ochl_day WHERE time IN '2026-09-10' ORDER BY abs(close - open) DESC;

Result:

symbol (SYMBOL)close (DOUBLE)
VOLT191.2
COAST376.1
CITY504.9

(3 rows)

Example: Newest news first (sample data: content-news)

SELECT published_at, symbol, title FROM news ORDER BY published_at DESC LIMIT 3;

Result:

published_at (TIMESTAMP)symbol (STRING)title (STRING)
2026-09-02T03:15:00.000ZCOASTCoastal Bank merger talks
2026-09-01T02:15:00.000ZNULLMarket index closes at 2,840
2026-08-15T04:15:00.000ZVOLTHydropower company plans IPO

(3 rows)

Example: Sort by position: the 3rd column (type), then the 2nd (name) (sample data: query-app)

SELECT code, name, type FROM accounts WHERE type IN ('income', 'expense') ORDER BY 3, 2;

Result:

code (INT)name (STRING)type (STRING)
5000Broker commissionexpense
5100Office rentexpense
4000Dividend incomeincome
4100Gain on saleincome

(4 rows)

Example: Capital letters come before small letters; lower() fixes it (sample data: query-app)

SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY email;
SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY lower(email);

Result:

SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY email

email (STRING)
GITA.RAI@Gmail.com
anita_m@hotmail.com
sita.k@yahoo.com

(3 rows)

SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY lower(email)

email (STRING)
anita_m@hotmail.com
GITA.RAI@Gmail.com
sita.k@yahoo.com

(3 rows)

Example: An alias inside an expression is not known (sample data: sample; shows an error)

SELECT symbol, close - open AS move FROM ochl_day WHERE time IN '2026-09-10' ORDER BY abs(move);

Result:

Error: line 1, column 91: unknown column 'move' (table 'ochl_day' has: time symbol open high low close volume)

Example: A position must be one of the columns (sample data: sample; shows an error)

SELECT id, name FROM brokers ORDER BY 3;

Result:

Error: line 1, column 39: ORDER BY 3: there are only 2 columns

ASC, DESC

Syntax:

ORDER BY sort_key ASC
ORDER BY sort_key DESC

ASC (ascending) sorts small to big: 1, 2, 3; A, B, C; old times first. It is the default. DESC (descending) sorts big to small: 3, 2, 1; new times first.

Example: Brokers by turnover, biggest first; equal turnover by id, smallest first (sample data: sample)

SELECT buyer, sum(amount) AS turnover
FROM trade_log
WHERE date IN '2026-09-10'
GROUP BY buyer
ORDER BY turnover DESC, buyer ASC;

Result:

buyer (INT)turnover (DOUBLE)
49960370
45695950
58260190
1239420
34205450

(5 rows)

Example: The last 3 minutes of COAST on 2026-09-10 (sample data: sample)

SELECT time, close, volume FROM ochl_minutes
WHERE symbol = 'COAST' AND time IN '2026-09-10'
ORDER BY time DESC
LIMIT 3;

Result:

time (TIMESTAMP)close (DOUBLE)volume (DOUBLE)
2026-09-10T09:14:00.000Z376.11060
2026-09-10T09:13:00.000Z376.21320
2026-09-10T09:12:00.000Z3761150

(3 rows)

COLLATE: dictionary order

Syntax:

sort_key COLLATE "name"
a COLLATE "name" = b      (also <, >, BETWEEN, IN)

COLLATE picks the order for one ORDER BY key or one comparison. Without it, text compares byte by byte.

NameOrderExample
"C", "POSIX", "default"bytes (the same as no COLLATE)Apple, Zebra, apple
"en_US", "en_US.utf8", "und-x-icu", "unicode", other language namesdictionary: letters first, case and accents only break tiesapple, Apple, ěa, Zebra
nocaselike the dictionary, and 'a' = 'A''apple' COLLATE nocase = 'APPLE' is true
naturallike the dictionary, and digits compare as numbersfile2, file10

Example: Byte order, then dictionary order (sample data: query-app)

SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY email;
SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY email COLLATE "en_US";

Result:

SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY email

email (STRING)
GITA.RAI@Gmail.com
anita_m@hotmail.com
sita.k@yahoo.com

(3 rows)

SELECT email FROM app_users WHERE id IN (2, 4, 6) ORDER BY email COLLATE en_US

email (STRING)
anita_m@hotmail.com
GITA.RAI@Gmail.com
sita.k@yahoo.com

(3 rows)

Example: Case-insensitive match and natural order (sample data: sample)

SELECT 'Zebra' < 'apple' AS bytes, 'Zebra' COLLATE "en_US" < 'apple' AS dictionary,
       'apple' COLLATE nocase = 'APPLE' AS same, 'file2' COLLATE natural < 'file10' AS natural;

Result:

bytes (BOOL)dictionary (BOOL)same (BOOL)natural (BOOL)
truefalsetruetrue

(1 row)

Example: An unknown collation (sample data: sample; shows an error)

SELECT 'a' COLLATE klingon = 'a';

Result:

Error: line 1, column 20: collation "klingon" does not exist; KDB knows "C", "en_US" (and other language names), "nocase" and "natural"

NULLS FIRST, NULLS LAST

Syntax:

ORDER BY sort_key [ASC | DESC] NULLS FIRST
ORDER BY sort_key [ASC | DESC] NULLS LAST

Say where rows with a NULL sort key go: before all other rows (NULLS FIRST) or after them (NULLS LAST).

WrittenNULL rows go
ORDER BY x or ORDER BY x ASClast (like NULLS LAST)
ORDER BY x DESCfirst (like NULLS FIRST)
ORDER BY x ... NULLS FIRSTfirst
ORDER BY x ... NULLS LASTlast

Example: Recently active users first, never logged in last (sample data: query-app)

SELECT username, last_login FROM app_users ORDER BY last_login DESC NULLS LAST;

Result:

username (STRING)last_login (TIMESTAMP)
bikash2026-09-11T04:20:00.000Z
gita2026-09-11T00:45:00.000Z
ram2026-09-10T14:30:00.000Z
suman2026-09-10T03:55:00.000Z
sita2026-09-09T02:15:00.000Z
anita2026-08-30T06:15:00.000Z
hari_99NULL
test_userNULL

(8 rows)

Example: Without NULLS LAST, DESC puts NULL first (sample data: query-app)

SELECT username, last_login FROM app_users ORDER BY last_login DESC LIMIT 3;

Result:

username (STRING)last_login (TIMESTAMP)
hari_99NULL
test_userNULL
bikash2026-09-11T04:20:00.000Z

(3 rows)

Example: Securities without a close today at the top (sample data: sample)

SELECT s.symbol, d.close
FROM securities s
LEFT JOIN ochl_day d ON d.symbol = s.symbol AND d.time IN '2026-09-10'
ORDER BY d.close NULLS FIRST;

Result:

symbol (SYMBOL)close (DOUBLE)
ACMENULL
TELCONULL
VOLT191.2
COAST376.1
CITY504.9

(5 rows)

Example: NULLS needs FIRST or LAST (sample data: sample; shows an error)

SELECT id FROM brokers ORDER BY id NULLS;

Result:

Error: line 1, column 41: expected FIRST or LAST after NULLS but found ';'

LIMIT

Syntax:

LIMIT n
LIMIT n OFFSET m
LIMIT ALL
LIMIT lo, hi
LIMIT -n
LIMIT -n, -m
FETCH FIRST n ROWS ONLY

Gives back at most n rows. With ORDER BY, these are the first n rows in that order (a "top n").

ArgumentTypeMeaning
nwhole number, 0 or morethe most rows to give back; LIMIT 0 gives no rows (only the columns)

Example: The 5 biggest trades of the trade log (sample data: sample)

SELECT transaction_id, symbol, quantity, rate, amount FROM trade_log ORDER BY amount DESC LIMIT 5;

Result:

transaction_id (LONG)symbol (SYMBOL)quantity (LONG)rate (DOUBLE)amount (DOUBLE)
100001TELCO1000881881000
100007TELCO1000879879000
100017TELCO500881440500
100031COAST1000402402000
100015COAST1000398398000

(5 rows)

Example: Only the column names: LIMIT 0 (sample data: query-app)

SELECT * FROM app_users LIMIT 0;

Result:

id (INT)username (STRING)email (STRING)full_name (STRING)city (STRING)plan (STRING)created_at (TIMESTAMP)last_login (TIMESTAMP)

(0 rows)

Example: The newest post of the forum (sample data: content-forum)

SELECT id, author, body, created_at FROM posts ORDER BY created_at DESC LIMIT 1;

Result:

id (LONG)author (STRING)body (STRING)created_at (TIMESTAMP)
8gitaWhich ones?2026-09-02T03:25:00.000Z

(1 row)

Example: LIMIT -n: the newest 3 daily candles of CITY, oldest first (QuestDB) (sample data: sample)

SELECT time, symbol, close FROM ochl_day WHERE symbol = 'CITY' LIMIT -3;

Result:

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)
2026-09-08T00:00:00.000ZCITY497.5
2026-09-09T00:00:00.000ZCITY504.6
2026-09-10T00:00:00.000ZCITY504.9

(3 rows)

Example: LIMIT -n, -m: the last 5 rows without the final 2 (sample data: sample)

SELECT id FROM brokers ORDER BY id LIMIT -5, -2;

Result:

id (INT)
1
34
45

(3 rows)

Example: LIMIT lo, hi gives the rows between them (QuestDB) (sample data: sample)

SELECT id FROM brokers ORDER BY id LIMIT 2, 4;
SELECT id FROM brokers ORDER BY id LIMIT 2 OFFSET 2;
SELECT id FROM brokers ORDER BY id FETCH FIRST 2 ROWS ONLY;

Result:

SELECT id FROM brokers ORDER BY id LIMIT 2 OFFSET 2

id (INT)
45
49

(2 rows)

SELECT id FROM brokers ORDER BY id LIMIT 2 OFFSET 2

id (INT)
45
49

(2 rows)

SELECT id FROM brokers ORDER BY id LIMIT 2

id (INT)
1
34

(2 rows)

OFFSET

Syntax:

LIMIT n OFFSET m

Skips the first m rows, then gives back at most n rows.

ArgumentTypeMeaning
mwhole number, 0 or morehow many rows to skip

Example: Rows 4 to 6 by volume (sample data: sample)

SELECT time, symbol, volume FROM ochl_day ORDER BY volume DESC LIMIT 3 OFFSET 3;

Result:

time (TIMESTAMP)symbol (SYMBOL)volume (DOUBLE)
2026-09-09T00:00:00.000ZCITY182390
2026-08-31T00:00:00.000ZCITY159240
2026-09-03T00:00:00.000ZVOLT145930

(3 rows)

Example: Past the end: no rows (sample data: sample)

SELECT id, name FROM brokers ORDER BY id LIMIT 5 OFFSET 10;

Result:

id (INT)name (STRING)

(0 rows)

Example: OFFSET by itself: skip rows, keep the rest (sample data: sample)

SELECT id, name FROM brokers ORDER BY id OFFSET 2;

Result:

id (INT)name (STRING)
45Imperial Securities
49Online Securities
58Northgate Securities

(3 rows)

Paging

Syntax:

-- page p (1, 2, 3, ...) with k rows per page:
ORDER BY key, unique_column LIMIT k OFFSET (p - 1) * k

-- keyset paging: the page after the last row shown
WHERE (key, unique_column) < (last_key, last_unique) ORDER BY key DESC, unique_column DESC LIMIT k

An app shows long lists one page at a time: 20 forum posts, then the next 20.

There are two ways:

WayHowGoodNot good
LIMIT + OFFSETpage 3 = LIMIT 20 OFFSET 40simple; can jump to any page numberslow for deep pages; rows move between pages when new rows arrive
keyset paging"the 20 rows after the last row I showed" with a row comparisonfast on every page; no rows are skipped or shown twiceonly "next page" and "previous page"

Example: Forum posts, 3 per page: page 1 (sample data: content-forum)

SELECT id, author, created_at FROM posts ORDER BY created_at DESC, id DESC LIMIT 3 OFFSET 0;

Result:

id (LONG)author (STRING)created_at (TIMESTAMP)
8gita2026-09-02T03:25:00.000Z
7hari2026-09-02T03:15:00.000Z
6sita2026-09-01T06:15:00.000Z

(3 rows)

Example: Page 2 (sample data: content-forum)

SELECT id, author, created_at FROM posts ORDER BY created_at DESC, id DESC LIMIT 3 OFFSET 3;

Result:

id (LONG)author (STRING)created_at (TIMESTAMP)
5ram2026-09-01T05:45:00.000Z
4gita2026-09-01T05:15:00.000Z
3hari2026-09-01T04:35:00.000Z

(3 rows)

Example: Page 2 with keyset paging: after post 6, the last row of page 1 (sample data: content-forum)

SELECT id, author, created_at FROM posts
WHERE (created_at, id) < ('2026-09-01T12:00:00+05:45', 6)
ORDER BY created_at DESC, id DESC
LIMIT 3;

Result:

id (LONG)author (STRING)created_at (TIMESTAMP)
5ram2026-09-01T05:45:00.000Z
4gita2026-09-01T05:15:00.000Z
3hari2026-09-01T04:35:00.000Z

(3 rows)

Example: A ledger page for the accountant: 5 lines per page, page 2 (sample data: query-app)

SELECT line_id, entry_date, account, debit, credit, memo
FROM ledger
ORDER BY entry_date, line_id
LIMIT 5 OFFSET 5;

Result:

line_id (INT)entry_date (TIMESTAMP)account (INT)debit (DECIMAL(2))credit (DECIMAL(2))memo (STRING)
62026-09-03T00:00:00.000Z1100NULL257500.00Pay the broker
72026-09-03T00:00:00.000Z50001030.00NULLCommission on the CITY buy
82026-09-03T00:00:00.000Z1100NULL1030.00Commission on the CITY buy
92026-09-04T00:00:00.000Z100030000.00NULLTake cash from the bank
102026-09-04T00:00:00.000Z1100NULL30000.00Take cash from the bank

(5 rows)