Parse LIMIT and OFFSET, plan a Limit node at the top of every plan that has one, and make it stop asking for rows once it has handed up its last, so that nothing below it reads a row it does not need.
Since volcano-executor, every operator has worked the same way: asked for a row, it asks the operator below it, does its bit, and hands one row up. So far that was a matter of style; every query read the whole table anyway. LIMIT is where the style pays. select * from t limit 5 needs five rows, and an operator that simply stops asking after the fifth means the scan below it reads five rows out of a million and is never asked for a sixth.
That is the whole stage: a small operator whose most important behaviour is what it does not do. rows_examined, the counter from the index-scan stage, makes it visible.
limit 5 over a thousand rows examines 5. Add a WHERE that keeps every tenth row, and limit 3 examines 30: the Filter reads rows until it has passed three, and stops being asked. OFFSET is not free: limit 10 offset 100 reads the hundred rows it skips, so it examines 110.
A Sort changes everything. order by name limit 5 must find the five smallest names, and the last row the scan reads might be one of them, so the Sort reads all thousand before it can hand up the first. The Limit still stops after five, but the work below it is already done:
| Query over ids 1 to 1000 | rows_examined | Why |
|---|---|---|
limit 5 | 5 | the scan stops when asked for no more |
limit 10 offset 100 | 110 | skipped rows are still read |
where id % 10 = 0 limit 3 | 30 | the Filter reads until three rows pass |
order by name limit 5 | 1000 | a Sort reads everything before its first row |
order by id limit 5 | 5 | no Sort: the scan is already in key order |
limit 0 | 0 | nothing is ever asked for |
The last useful observation: order by id limit 5 examines only 5, because the planner left the Sort out (the previous stages) and the scan delivers key order by itself. Choosing a plan that can stop early is what the Indexes and the Optimizer part is about.
In SQL, rows without ORDER BY come back in no promised order, and "LIMIT without ORDER BY returns an arbitrary subset" is the standard warning. In this engine the order is promised: a scan reads the B+Tree from its first leaf to its last, so rows come in ascending key order, and the contract says so. limit 2 is always the two smallest keys the WHERE keeps. Real databases make no such promise, because a parallel scan, a hash join or a different index can change the order; write ORDER BY when you need one.
An expression is evaluated only for rows that are asked for. In a table where only row 500 has b = 0, select id, 10 / b from t limit 3 evaluates the division three times and succeeds, while limit 600 reaches row 500 and fails with 01501. Put a Sort below the Limit, order by 10 / b limit 1, and the Sort evaluates its key for every row, so it fails too. And OFFSET is not a way around it: the rows a Limit skips are still asked for, from the Project below it, so limit 1 offset 500 projects row 500 and fails on it, whereas limit 2 offset 400 never reaches it.
At the other end, limit 0 asks for nothing at all, so even a Sort below it never starts: order by name limit 0 examines no row and prints sort_runs=0.
Limit keeps two numbers: how many rows it still has to skip, and how many it may still hand up.
Limit applies to the finished result, so it goes above everything.
These are the exact lines the tests check. The full definition is on the output contract page, section 46.
| Input | Output |
|---|---|
select id from t limit 5; over 1000 rows | ids 1 to 5, (5 rows), [EXEC] rows_examined=5 |
select id from t order by name limit 5; | the five smallest names' ids, [EXEC] rows_examined=1000 sort_runs=1 |
explain select id from t where id > 5 order by name limit 5 offset 2; | Limit (count=5, offset=2), Project (cols=[id]), Sort (key=name ASC), IndexScan (table=t, range: id > 5) |
explain select * from t limit 3; | Limit (count=3, offset=0) over SeqScan (table=t) |
ast select id from t order by name limit 5 offset 2; | ... Order: [name ASC], then Limit: count=5, offset=2 |
limit -1, limit abc, limit;, limit 4294967296 | [ERROR:01505] <message>, from the parser, before any other check |
limit 1 + 1, offset 5 without LIMIT, LIMIT on DELETE | [ERROR:00302] <message> |
An OFFSET past the last row and limit 0 print the header and (0 rows). The Limit node is always at the top of the plan, above the Project.
$droid --db big.dbWelcome to droid!>select id, name from t limit 3;id | name1 | n9192 | n8383 | n757(3 rows)[EXEC] rows_examined=3>select id from t where id % 100 = 0 limit 2 offset 1;id200300(2 rows)[EXEC] rows_examined=300>select id from t order by name limit 2;id1000679(2 rows)[EXEC] rows_examined=1000 sort_runs=1>explain select id from t order by name limit 2;[PLAN] select id from t order by name limit 2[PLAN] └── Limit (count=2, offset=0)[PLAN] └── Project (cols=[id])[PLAN] └── Sort (key=name ASC)[PLAN] └── SeqScan (table=t)>.exitexiting.. good bye!
This is the test table: a thousand rows whose names are n and three digits, in an order that has nothing to do with the key. The first query read three rows. The second skipped one matching row and kept two, reading up to row 300. The third read every row, because of the Sort, and only then kept two.
rows_examined=1000.limit 5 examines 6.limit 0 must examine nothing.limit 10 offset 100 hands up 10 rows, not 0 or 90.limit 1 offset 500 stops failing (see the next bullet but one).limit 1 + 1 is a syntax error.limit 0 examine every row.limit 3 fail on row 500.The page above is the same for every language. These are the only language-specific hints.
uint64_t and compare with 4294967295 before narrowing.Iterator, skip(offset).take(count) is exactly this operator, and take stops calling next on its source.u32 and check the parsed value with std.math.cast before storing it.