droid
The Sort Operator
Querying · Stage 5 of 11

The Sort Operator

ORDER BY: compare rows by several keys, put NULLs in their place, and keep ties in order

Parse ORDER BY with any number of keys, each an expression or an alias with its direction, sort the rows the WHERE keeps with a stable multi-key comparison, and plan a Sort node, except when the scan already delivers the order.

Not run yet · 12 tests
Concepts in this stage
  • A blocking operator: Sort must read all of its input before it hands up the first row
  • Multi-key comparison: the first key decides, and each later key only breaks ties
  • Stability: rows with equal keys keep the order they arrived in, which is key order
  • NULL sorts before every value: first under ASC, last under DESC
  • An alias in ORDER BY means the select-list item; inside a larger key it does not
  • An order the scan already gives needs no Sort
Where this stage sits in the engine
Talking to the user
REPL
Sessions
Printer
Server
Wire protocol
Understanding SQL
Lexer
Parser
Validator
Answering a query
Planner
Executor
Keeping changes safe
TransactionsWrite-ahead log
Undo log
Lock manager
Read views
Storing rows
Row formatSlotted pageB+Tree
Reaching the disk
PagerDatabase file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

Why this stage exists

A table in this engine already comes out in one order: ascending primary key, because the B+Tree keeps it that way. Every other order has to be made. order by dept, salary desc asks for something the storage knows nothing about, and the only way to deliver it is to collect the rows and sort them.

That makes Sort different from every operator so far. A scan, a Filter and a Project each hand up a row as soon as they have one. Sort cannot: the last row the scan reads may be the one that has to come out first. It is a blocking operator, and it holds its whole input in memory. This stage sorts in memory; the next one deals with input that does not fit.

select id from emp order by dept, salary desc
Project (cols=[id])
↑ 7 rows, by dept, then salary descending
reads all first
Sort (key=dept ASC, salary DESC)
hands up nothing until the scan has no row left
↑ 7 rows, in key order
SeqScan (table=emp)
select id from emp order by id
Project (cols=[id])
↑ 7 rows, already in key order
SeqScan (table=emp)
Left: the Sort asks for every row before it hands up the first. Right: ordering by the first column, ascending, is the order the scan already gives, so the plan has no Sort.

Comparing by several keys

ORDER BY takes a list of keys. The first key decides; the second is only looked at when the first is equal, the third when both are, and so on. Each key has its own direction, so order by dept asc, salary desc keeps departments in alphabetical order and puts the highest salary first within each:

iddeptsalarywhy it is here
3eng120eng is first; 120 is its highest salary
1eng100ties with 5 on both keys
5eng100so the tie keeps key order: 1, then 5
7hr10
6hr9
2ops90ties with 4
4ops90

Types compare the way they always have: INTs as numbers, so 9 comes before 10 and 90 before 100, and VARCHARs byte by byte, so Gus comes before ann. A key can be any expression, salary + bonus or -salary or bonus > 1, and a truth value sorts false before true.

Ties, and why the sort must be stable

Rows 1 and 5 above are equal on every key. Something still has to decide which comes first, and the contract says: the order they arrived in. Rows arrive from the scan in primary-key order, so a stable sort, one that never swaps equal elements, gives ties in key order, every time, for free.

An unstable sort (a textbook quicksort or heapsort) gives a valid order too, but not the same one every time, and the tests, like any user comparing two runs, need the same answer. Most languages ship a stable sort next to the unstable one; use it. Do not break ties by adding the key as a hidden last sort key: it gives the same answer here, but it hides the property the next stage depends on, when runs are merged.

Where NULL goes

NULL is not smaller or larger than a value; it is unknown. A sort still has to put it somewhere, and this engine puts it before every value. Under ASC the NULLs come first, and under DESC, which reverses everything, they come last. Two NULLs are equal, so they keep key order like any other tie.

Aliases and keys

A key that is just a name, and the alias of a select-list item, means that item: select salary * 2 as s2 from emp order by s2 sorts by the doubled salary. This holds even when the table has a column of the same name, so select -salary as salary ... order by salary sorts by the negated value. ORDER BY is the one clause that can see aliases, because it runs after the select list is known.

Only a bare name is looked up that way. Inside a larger key, order by s2 + 1, a name is a column of the table as everywhere else, and s2 is not one: 00402.

When no Sort is needed

order by id asks for the order the scan already delivers. The planner sees that the first key is the table's first column, ascending, and plans no Sort at all: the plan is the one without ORDER BY. Any later keys cannot matter, since the first column never has two equal values. order by id desc still needs a Sort, because the scan only reads forwards.

The algorithm in words

1

Compare two rows

Sorting is a comparison function used many times. Getting the function right is most of the stage.

  1. 1.Compute each key's value for both rows. A key that is an alias of a select-list item uses that item's expression.
  2. 2.Take the first key. If exactly one of the two values is NULL, that row is smaller. If both are NULL, they are equal.
  3. 3.Otherwise compare by type: INTs as signed numbers, VARCHARs byte by byte, truth values with false before true.
  4. 4.If the key is DESC, reverse the answer: smaller becomes larger.
  5. 5.If the two values are equal, go on to the next key. If every key is equal, the rows are equal.
  6. 6.Invariant: the comparison never looks at anything but the keys. Ties are left to the sort's stability, not broken by hand.
2

Run the Sort operator

Sort sits between the Filter and the Project. It is the first operator that cannot hand up a row before it has seen them all.

  1. 1.On the first call for a row, ask the child for rows until it has none, computing every key for each and keeping the rows with their keys.
  2. 2.Sort the kept rows with a stable sort, using the comparison above. Rows arrive in ascending key order, so stability makes ties come out in key order.
  3. 3.Hand up the sorted rows one at a time, on this call and on each later one.
  4. 4.An error while computing a key stops the statement before any row is handed up.
  5. 5.Invariant: Sort counts no row; rows_examined is what the scan below it read.
3

Decide whether to sort at all

The planner knows the order the scan delivers. When it is the order asked for, the Sort is pure cost.

  1. 1.Resolve the first key: an alias becomes its item's expression.
  2. 2.If that key is the table's first column, ascending, plan no Sort: the scan delivers that order, and since the first column is unique no later key can matter.
  3. 3.Otherwise put a Sort node above the Filter, or above the scan when there is no WHERE, and below the Project.

Output contract for this stage

These are the exact lines the tests check. The full definition is on the output contract page, sections 39 to 44.

InputOutput
tokenize order by a asc, b desc[KEYWORD_ORDER - order], [KEYWORD_BY - by], [IDENTIFIER - a], [KEYWORD_ASC - asc], [SYMBOL - ,], [IDENTIFIER - b], [KEYWORD_DESC - desc]
ast select id from emp where salary > 50 order by dept, salary + bonus desc;... Where: salary > 50, then Order: [dept ASC, salary + bonus DESC]
explain select id from emp order by dept, salary desc;Project (cols=[id]), then Sort (key=dept ASC, salary DESC), then SeqScan (table=emp)
explain select * from emp order by id;SeqScan (table=emp) alone: no Sort
an unknown column in a key, or an alias inside a larger key[ERROR:00402] <message>
order by with no key, order id, asc desc[ERROR:00302] <message>

Every key prints its direction in ast and in the plan, ASC when none was written. Keys print as expressions do (section 40), and an alias key prints as the alias. The checks run in the order of section 44: columns of the select list, the WHERE, then the keys; then types in the same order; then numbers. rows_examined is the scan's; Sort adds nothing.

What you should see when done

shell
$droid --db staff.db
Welcome to droid!
>create table emp (id int, dept varchar(10), salary int, bonus int);
Create table statement executed successfully
>insert into emp (id, dept, salary, bonus) values (3, 'eng', 120, 0);
Insert statement executed successfully (1 rows)
>insert into emp (id, dept, salary) values (1, 'ops', 90);
Insert statement executed successfully (1 rows)
>insert into emp (id, dept, salary, bonus) values (2, 'eng', 100, 5);
Insert statement executed successfully (1 rows)
>select id, dept, salary from emp order by dept, salary desc;
id | dept | salary
3 | eng | 120
2 | eng | 100
1 | ops | 90
(3 rows)
[EXEC] rows_examined=3
>select id, bonus from emp order by bonus desc;
id | bonus
2 | 5
3 | 0
1 | NULL
(3 rows)
[EXEC] rows_examined=3
>explain select id from emp order by salary;
[PLAN] select id from emp order by salary
[PLAN] └── Project (cols=[id])
[PLAN] └── Sort (key=salary ASC)
[PLAN] └── SeqScan (table=emp)
>explain select id from emp order by id;
[PLAN] select id from emp order by id
[PLAN] └── Project (cols=[id])
[PLAN] └── SeqScan (table=emp)
>.exit
exiting.. good bye!

The NULL bonus went last under DESC. The last plan has no Sort: ordering by the key costs nothing. The messages are yours to word; the tests check only the codes.

Common bugs in this stage

  • An unstable sort. Ties come out in a different order from run to run, or in insertion order. Use a stable sort.
  • Comparing every key as text. 10 sorts before 9. Compare by the value's type.
  • DESC applied to the whole row. Reversing the final list instead of each key's comparison turns dept asc, salary desc into dept desc, salary asc and reverses ties too.
  • NULL treated as 0 or as the empty string. A bonus of NULL lands next to a bonus of 0. NULL is smaller than every value.
  • Aliases resolved inside expressions. order by s2 + 1 must be 00402; only a bare alias means the item.
  • Sorting before filtering. It sorts rows that are thrown away, and can divide by zero on them.
  • A Sort for order by id. The answer is right but the plan is not: the scan already gives that order.
  • Recomputing keys inside the comparison. It works, but a sort compares each row many times; compute the keys once per row.

Language notes

The page above is the same for every language. These are the only language-specific hints.

  • C: qsort is not stable. Write a merge sort over an array of row pointers (it is short, and the next stage needs merging anyway), or sort an array of (keys, arrival index) pairs and use the arrival index, which is not the key, to break ties.
  • C++: std::stable_sort, not std::sort.
  • Rust: slice::sort_by is stable; sort_unstable_by is not. Return an Ordering and chain keys with Ordering::then_with.
  • Zig: std.mem.sort is stable (a block sort); std.mem.sortUnstable is not.

Checklist

One line per thing the tests check. The tags name the test cases.

  • Several keys, each with its own direction
    multi-key-mixed
  • Rows with equal keys keep ascending key order, whatever the direction
    stability
  • INT keys sort as numbers
    int-not-lexicographic
  • VARCHAR keys sort byte by byte
    desc-varchar
  • NULLs come first under ASC and last under DESC
    nulls-position
  • A key can be any expression, a truth value included
    order-by-expression
  • A key that is an alias means the item, even over a column of that name
    order-by-alias
  • A key need not be in the select list, and only the rows the WHERE keeps are sorted
    order-by-unprojected-col
  • Keys are checked like the rest of the statement: 00402, 00403, 00302, and 01501 while sorting
    order-by-errors
  • ORDER BY is a Sort node, below Project and above the Filter
    explain-sort
  • ORDER BY the first column, ascending, needs no Sort
    pk-order-skips-sort
  • order, by, asc and desc are keywords; ast prints an Order line
    tokenize-ast-order