Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions core-spec/expression_language.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,14 @@ Each `ORDER BY` item has three independent parts:
RANK() OVER (ORDER BY revenue DESC NULLS LAST)
```

Without `NULLS FIRST` or `NULLS LAST`, NULL sorts as the largest value: last for `ASC`, first for `DESC`. Implementations MUST produce this order whatever the native default of the engine; see [Window ORDER BY NULL Placement](#window-order-by-null-placement).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note: the MUST can't be met on engines with no NULLS FIRST/LAST syntax, such as MySQL/MariaDB and SQL Server (even if I know there are not the first target candidates 😄 ).

Both treat NULL as smallest, so ASC puts NULL first, the opposite of the Ossie default. Following the MUST there needs a rewrite like ORDER BY expr IS NULL, expr.

Maybe we could make the spec either describe that rewrite or say such items may be reported as unsupported? Otherwise conformance on those dialects is undefined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The new section now covers engines without the clause: a leading CASE WHEN order_expr IS NULL THEN 1 ELSE 0 END key. It is ascending for NULLs last and descending for NULLs first. CASE is the form SQLGlot writes for both MySQL and T-SQL, so one rule covers both.


```sql
-- revenue over three rows: 10, NULL, 5; each comment lists the values numbered 1, 2, 3
ROW_NUMBER() OVER (ORDER BY revenue) -- 5, 10, NULL
ROW_NUMBER() OVER (ORDER BY revenue DESC) -- NULL, 10, 5
```

Frame clause options:

- `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`
Expand Down Expand Up @@ -790,6 +798,24 @@ Sigma's table-calculation functions (`RowNumber`, `Rank`, `RunningSum`, `Running

---

## Window ORDER BY NULL Placement

When an `ORDER BY` item omits the clause, the Ossie placement is `NULLS LAST` for `ASC` and `NULLS FIRST` for `DESC`. An implementation produces any placement, given or default, as follows:

- On an engine with `NULLS FIRST` and `NULLS LAST`, it emits the clause. It MAY omit a default placement that the native default already gives. This holds only if the session cannot change that default, or the implementation sets it. Snowflake (`DEFAULT_NULL_ORDERING`) and DuckDB (`default_null_order`) let a session change it.
- On an engine without them, it adds a leading sort key `CASE WHEN order_expr IS NULL THEN 1 ELSE 0 END`. The key is ascending to put NULLs last and descending to put them first.

Native defaults:

| Engine | `NULLS FIRST`, `NULLS LAST` | Native `ASC` | Native `DESC` |
| :---- | :---- | :---- | :---- |
| Oracle, PostgreSQL, Redshift, Snowflake | yes | NULLS LAST | NULLS FIRST |
| BigQuery, Databricks | yes | NULLS FIRST | NULLS LAST |
| ClickHouse, DuckDB, Trino | yes | NULLS LAST | NULLS LAST |
| MySQL, SQL Server | no | NULLS FIRST | NULLS LAST |

---

## Compliance Levels

### MUST Support (Core)
Expand Down
Loading