ORDER BY, LIMIT, OFFSET
Sort the result, and give back only the first rows or one page of rows.
- Category: Querying
- Keywords: sort, order, sorting, ORDER BY, ASC, DESC, ascending, descending, NULLS FIRST, NULLS LAST, NULLS, FIRST, LAST, LIMIT, OFFSET, top N, first rows, paging, pagination, page, next page, LIMIT a, b, order by, limit, offset, asc, desc, paging
- See also: SELECT, WHERE (row-comparison), GROUP BY and HAVING, UNION and subqueries (union-all), Window and series functions, EXPLAIN
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.000Z | COAST | 186330 |
| 2026-09-10T00:00:00.000Z | COAST | 186200 |
| 2026-09-10T00:00:00.000Z | CITY | 183040 |
(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.000Z | CITY | 182390 |
| 2026-08-31T00:00:00.000Z | CITY | 159240 |
| 2026-09-03T00:00:00.000Z | VOLT | 145930 |
(3 rows)
ORDER BY
Syntax:
ORDER BY sort_key [ASC | DESC] [NULLS FIRST | NULLS LAST], ...
A sort_key can be:
| Sort key | Example | Meaning |
|---|---|---|
| an output column name or alias | ORDER BY change_pct | the value of that output column |
| a position | ORDER BY 2 | the 2nd output column (counts from 1) |
| a table column | ORDER BY volume | also when it is not in the select list |
| an expression | ORDER 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.
- Returns: the same rows, sorted.
- Notes: text is sorted by its bytes:
'A'comes before'B', and every capital letter comes before every small letter ('Z'before'a'). This is PostgreSQL'sCOLLATE "C". For dictionary order writeORDER BY name COLLATE "en_US"(COLLATE), orORDER BY lower(name). - An alias can be used alone (
ORDER BY move), but not inside an expression (ORDER BY abs(move)is an error in a query withoutGROUP BY): write the expression again. - After
GROUP BYor with aggregates,ORDER BYcan only use the output columns (names, aliases, positions) and expressions of them, likeORDER BY q / n. - After
UNION ALL,ORDER BYcan only use output column names or positions (see UNION ALL). - Rows that are equal in every key keep the order they came in. For stable pages, end with a unique column (like
id). ORDER BY ... LIMIT non a big table only keeps the bestnrows while it reads, so it is fast.ORDER BY time DESC LIMIT non a time-series table even stops early: it reads the newest rows first (see LIMIT).
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) |
|---|---|---|---|
| VOLT | 185.6 | 191.2 | 3.02 |
| CITY | 504.6 | 504.9 | 0.06 |
| COAST | 380.1 | 376.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) |
|---|---|---|
| Banking | CITY | City Bank Limited |
| Banking | COAST | Coastal Bank Limited |
| Energy | VOLT | Volt River Hydropower Limited |
| Investment | ACME | Acme Investment Company |
| Telecom | TELCO | Northwind 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) |
|---|---|
| VOLT | 191.2 |
| COAST | 376.1 |
| CITY | 504.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.000Z | COAST | Coastal Bank merger talks |
| 2026-09-01T02:15:00.000Z | NULL | Market index closes at 2,840 |
| 2026-08-15T04:15:00.000Z | VOLT | Hydropower 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) |
|---|---|---|
| 5000 | Broker commission | expense |
| 5100 | Office rent | expense |
| 4000 | Dividend income | income |
| 4100 | Gain on sale | income |
(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.
- Notes:
ASCorDESCbelongs to one sort key.ORDER BY a DESC, bsortsabig first, andbsmall first. DESCalso changes whereNULLgoes by default (see NULLS FIRST, NULLS LAST).
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) |
|---|---|
| 49 | 960370 |
| 45 | 695950 |
| 58 | 260190 |
| 1 | 239420 |
| 34 | 205450 |
(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.000Z | 376.1 | 1060 |
| 2026-09-10T09:13:00.000Z | 376.2 | 1320 |
| 2026-09-10T09:12:00.000Z | 376 | 1150 |
(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.
| Name | Order | Example |
|---|---|---|
"C", "POSIX", "default" | bytes (the same as no COLLATE) | Apple, Zebra, apple |
"en_US", "en_US.utf8", "und-x-icu", "unicode", other language names | dictionary: letters first, case and accents only break ties | apple, Apple, ěa, Zebra |
nocase | like the dictionary, and 'a' = 'A' | 'apple' COLLATE nocase = 'APPLE' is true |
natural | like the dictionary, and digits compare as numbers | file2, file10 |
- Returns: a comparison gives
trueorfalse;ORDER BYsorts.SELECT name COLLATE "en_US"givesnameunchanged. - Notes: every language name uses the same dictionary order (a letter with an accent sorts right after the plain letter). An unknown name is an error.
- In
ORDER BY, write the column or expression withCOLLATE, not an alias or a position. COLLATEafter a column type inCREATE TABLEis still read and ignored: write it in the query.- Sorting with
COLLATEworks out a sort key for every row, so it is slower than a plainORDER BY.
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) |
|---|---|---|---|
| true | false | true | true |
(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).
| Written | NULL rows go |
|---|---|
ORDER BY x or ORDER BY x ASC | last (like NULLS LAST) |
ORDER BY x DESC | first (like NULLS FIRST) |
ORDER BY x ... NULLS FIRST | first |
ORDER BY x ... NULLS LAST | last |
- Notes: the defaults are the same as in PostgreSQL:
NULLcounts as bigger than every value. - A common case: "last login, newest first" should show users who never logged in at the end:
ORDER BY last_login DESC NULLS LAST.
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) |
|---|---|
| bikash | 2026-09-11T04:20:00.000Z |
| gita | 2026-09-11T00:45:00.000Z |
| ram | 2026-09-10T14:30:00.000Z |
| suman | 2026-09-10T03:55:00.000Z |
| sita | 2026-09-09T02:15:00.000Z |
| anita | 2026-08-30T06:15:00.000Z |
| hari_99 | NULL |
| test_user | NULL |
(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_99 | NULL |
| test_user | NULL |
| bikash | 2026-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) |
|---|---|
| ACME | NULL |
| TELCO | NULL |
| VOLT | 191.2 |
| COAST | 376.1 |
| CITY | 504.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").
| Argument | Type | Meaning |
|---|---|---|
n | whole number, 0 or more | the most rows to give back; LIMIT 0 gives no rows (only the columns) |
- Returns: the first
nrows, or fewer when there are fewer. - Notes:
LIMITcomes afterORDER BY, at the very end of the query. WithUNION ALL, it works on all the rows together. nmust be a plain number: not an expression, not negative.LIMIT ALLmeans no limit. The QuestDB formLIMIT lo, higives rowslo + 1tohi, soLIMIT 10, 20is the same asLIMIT 10 OFFSET 10. The standardFETCH FIRST n ROWS ONLYworks too.LIMIT -n(QuestDB) gives the lastnrows, still in their own order. On a time-series table these are the newest rows by time, and KDB reads only the newest partitions to find them.LIMIT -n, -mgives the lastnrows without the finalm:LIMIT -5, -2is the 3 rows before the last 2. It cannot be used withOFFSETorFOR UPDATE.- Server programs (
kdb sql, HTTP/exec) also cut very big results; see limits.
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) |
|---|---|---|---|---|
| 100001 | TELCO | 1000 | 881 | 881000 |
| 100007 | TELCO | 1000 | 879 | 879000 |
| 100017 | TELCO | 500 | 881 | 440500 |
| 100031 | COAST | 1000 | 402 | 402000 |
| 100015 | COAST | 1000 | 398 | 398000 |
(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) |
|---|---|---|---|
| 8 | gita | Which 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.000Z | CITY | 497.5 |
| 2026-09-09T00:00:00.000Z | CITY | 504.6 |
| 2026-09-10T00:00:00.000Z | CITY | 504.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.
| Argument | Type | Meaning |
|---|---|---|
m | whole number, 0 or more | how many rows to skip |
- Returns: rows
m + 1tom + n. Whenmis past the end, no rows. - Notes:
OFFSETmay be written afterLIMIT, or by itself (then there is no limit), or beforeLIMITas PostgreSQL allows.OFFSET m ROWSis accepted too. - KDB still makes and sorts the skipped rows, so a big
OFFSETis slow. See paging.
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.000Z | CITY | 182390 |
| 2026-08-31T00:00:00.000Z | CITY | 159240 |
| 2026-09-03T00:00:00.000Z | VOLT | 145930 |
(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) |
|---|---|
| 45 | Imperial Securities |
| 49 | Online Securities |
| 58 | Northgate 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:
| Way | How | Good | Not good |
|---|---|---|---|
LIMIT + OFFSET | page 3 = LIMIT 20 OFFSET 40 | simple; can jump to any page number | slow 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 comparison | fast on every page; no rows are skipped or shown twice | only "next page" and "previous page" |
- Notes: the page order must be unique: add a unique column like
idas the last sort key. Otherwise rows with equal keys can move from one page to another. - For the offset, KDB needs a whole number: compute
(p - 1) * kin your program.
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) |
|---|---|---|
| 8 | gita | 2026-09-02T03:25:00.000Z |
| 7 | hari | 2026-09-02T03:15:00.000Z |
| 6 | sita | 2026-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) |
|---|---|---|
| 5 | ram | 2026-09-01T05:45:00.000Z |
| 4 | gita | 2026-09-01T05:15:00.000Z |
| 3 | hari | 2026-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) |
|---|---|---|
| 5 | ram | 2026-09-01T05:45:00.000Z |
| 4 | gita | 2026-09-01T05:15:00.000Z |
| 3 | hari | 2026-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) |
|---|---|---|---|---|---|
| 6 | 2026-09-03T00:00:00.000Z | 1100 | NULL | 257500.00 | Pay the broker |
| 7 | 2026-09-03T00:00:00.000Z | 5000 | 1030.00 | NULL | Commission on the CITY buy |
| 8 | 2026-09-03T00:00:00.000Z | 1100 | NULL | 1030.00 | Commission on the CITY buy |
| 9 | 2026-09-04T00:00:00.000Z | 1000 | 30000.00 | NULL | Take cash from the bank |
| 10 | 2026-09-04T00:00:00.000Z | 1100 | NULL | 30000.00 | Take cash from the bank |
(5 rows)