SQL dialects

This handbook runs every query in DuckDB. The AdventureWorks exercises run on SQL Server, whose dialect is called T-SQL. Much of the SQL you will find online was written for Postgres or SQLite. The engines agree on nearly everything in the handbook, and this page lists where they do not, plus one shared behavior that is easy to get wrong.

Each table shows what one statement returned on each engine on 2026-10-18, with DuckDB 1.5.6, PostgreSQL 18.3, SQL Server on Azure SQL Database (version 12.0) and SQLite 3.53.1. In the tables, error means the engine refused the statement and NULL means it ran and returned NULL.

The first entries change the answer without raising an error, so nothing warns you. The later entries stop the statement with an error, which you see as soon as you run it.

Integer division#

DuckDB returns 2.5 for 5 / 2. Postgres, SQL Server and SQLite drop the remainder when both sides are integers, so they return 2, and a rate such as 3 / 4 comes out as 0. Multiply one side by 1.0 before dividing, and all four return the same answer.

DuckDB Postgres SQL Server SQLite
5 / 2 2.5 2 2 2
3 / 4 0.75 0 0 0
5 * 1.0 / 2 2.5 2.5 2.5 2.5

Averaging a column of integers#

AVG over the integers 1 and 2 returns 1.5 in DuckDB, Postgres and SQLite. SQL Server averages integers as integers and returns 1. Write AVG(x * 1.0) when the column holds integers.

DuckDB Postgres SQL Server SQLite
AVG(x) 1.5 1.5 1 1.5
AVG(x * 1.0) 1.5 1.5 1.5 1.5

See: §13. Aggregate functions

Rounding a value that ends in 5#

On a DECIMAL column, ROUND(2.5, 0) returns 3 on all four engines, which is what Excel does. On a DOUBLE, Postgres usually rounds a value ending in exactly 5 to the nearest even number, so ROUND(x) returned 2 there and 3 on the other three. Postgres also refuses ROUND(x, 0) on a DOUBLE. Cast to DECIMAL before rounding money, and the four engines agree.

DuckDB Postgres SQL Server SQLite
ROUND(2.5, 0) on a DECIMAL 3 3 3 3.0
ROUND(x) on a DOUBLE holding 2.5 3.0 2 3.0 3.0
ROUND(x, 0) on the same DOUBLE 3.0 error 3.0 3.0

See: ROUND in the Python phrasebook, where Python and polars round to the nearest even number

Dividing by zero#

1 / 0 stops the query with an error in Postgres and SQL Server, returns NULL in SQLite, and returns inf, which is infinity, in DuckDB. NULLIF(denominator, 0) turns a zero denominator into NULL before the division, and the division then returns NULL on all four.

DuckDB Postgres SQL Server SQLite
1 / 0 inf error error NULL
1 / NULLIF(0, 0) NULL NULL NULL NULL

Comparing text that differs only in case#

The course's AdventureWorks database uses the collation SQL_Latin1_General_CP1_CI_AS, where CI stands for case-insensitive, so 'abc' = 'ABC' is true there. DuckDB and Postgres treat the two strings as different. SQLite treats them as different for = and as equal for LIKE. A text filter can therefore match more rows on SQL Server than the same filter matches in DuckDB. The same collation merges values that differ only in case when SQL Server counts distinct values or forms groups, so 'abc', 'ABC' and 'Abc' count as one value there and as three on the other engines.

DuckDB Postgres SQL Server SQLite
'abc' = 'ABC' false false true false
'Abc' LIKE 'a%' false false true true
COUNT(DISTINCT k) over 'abc', 'ABC' and 'Abc' 3 3 1 3
groups from GROUP BY k over the same values 3 3 1 3

See: §7. WHERE · §14. GROUP BY

Comparing text with a number#

'01' = 1 is true in DuckDB, Postgres and SQL Server, which convert the text to a number before comparing. SQLite compares a text value and a number as two different types and returns false.

DuckDB Postgres SQL Server SQLite
'01' = 1 true true true false

See: §7. WHERE

Spaces at the end of text#

SQL Server ignores spaces at the end of a string in two places. Its LEN('Chai ') returns 4, where LENGTH('Chai ') returns 5 on the other three engines. And 'Chai' = 'Chai ' is true on SQL Server and false on the rest. Northwind stores one customer id as 'Val2 ', so WHERE "CustomerID" = 'Val2' finds nothing in DuckDB, and the same comparison is true on SQL Server.

DuckDB Postgres SQL Server SQLite
length of 'Chai ' 5 5 4 5
'Chai' = 'Chai ' false false true false

See: §7. WHERE

Where NULL goes in a sort#

In an ascending sort, NULL comes last in DuckDB and Postgres and first in SQL Server and SQLite, so the first rows of the same sort can differ between engines. In a descending sort, only Postgres puts NULL first. NULLS LAST and NULLS FIRST fix the position in DuckDB, Postgres and SQLite. SQL Server rejects both keywords. Sort on CASE WHEN x IS NULL THEN 1 ELSE 0 END first and on x second, which puts NULL last on all four, ascending or descending.

DuckDB Postgres SQL Server SQLite
first value of ORDER BY x over 2, NULL and 1 1 1 NULL NULL
the same, with NULLS LAST 1 1 error 1
the same, sorted on the CASE first 1 1 1 1
first value of ORDER BY x DESC over the same values 2 NULL 2 2
the same, sorted on the CASE first 2 2 2 2

See: §8. ORDER BY and LIMIT

NULL inside NOT IN#

All four engines agree on this one. When the subquery returns a NULL, WHERE id NOT IN (subquery) keeps no rows at all. NOT EXISTS keeps the rows you meant. In the table, the outer query holds 3 and 4 and the subquery returns 1 and NULL.

DuckDB Postgres SQL Server SQLite
rows kept by NOT IN 0 0 0 0
rows kept by NOT EXISTS 2 2 2 2

See: §21. Subqueries

Taking the first rows#

DuckDB, Postgres and SQLite end a query with LIMIT n. SQL Server rejects LIMIT and writes TOP n directly after SELECT.

DuckDB Postgres SQL Server SQLite
SELECT 1 AS x ORDER BY x LIMIT 1 1 1 error 1
SELECT TOP 1 1 AS x error error 1 error

See: §8. ORDER BY and LIMIT

Joining strings#

DuckDB, Postgres and SQLite join text with two vertical bars, as in first_name || last_name. SQL Server joins it with +. With either operator, a single NULL makes the whole result NULL. CONCAT() works on all four and treats a NULL as empty text.

DuckDB Postgres SQL Server SQLite
'a' joined to NULL with the operator NULL NULL NULL NULL
CONCAT('a', NULL) a a a a

Median#

MEDIAN() exists in DuckDB and in the SQLite build tested here. Postgres and SQL Server have no such function, and neither did an older SQLite, version 3.45.1. The standard form is PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x), which DuckDB and Postgres accept as written and SQLite rejects. SQL Server rejects it as written and accepts SELECT DISTINCT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x) OVER () FROM t.

DuckDB Postgres SQL Server SQLite
MEDIAN(x) over 1, 2 and 10 2 error error 2
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x) 2 2 error error
the same, with DISTINCT and OVER () error error 2 error

See: §13. Aggregate functions

Using a column alias in WHERE#

DuckDB and SQLite accept a name defined in SELECT inside WHERE. Postgres and SQL Server reject it, because they evaluate WHERE before SELECT has named anything. Repeating the expression in WHERE works on all four.

DuckDB Postgres SQL Server SQLite
SELECT x * 2 AS dbl ... WHERE dbl > 2 4 error error 4
SELECT x * 2 AS dbl ... WHERE x * 2 > 2 4 4 4 4

See: §16. The order the clauses actually run in

True and false#

SELECT TRUE returns true in DuckDB and Postgres, and 1 in SQLite, which stores true and false as 1 and 0. SQL Server has no TRUE keyword. A SQL Server column that holds true or false has the type bit, and you compare it with 1 or 0.

DuckDB Postgres SQL Server SQLite
SELECT TRUE true true error 1
a true value compared with true, or with 1 in SQL Server true true true true

Writing a date#

The handbook writes a date as DATE '2016-07-04', and turns text into a date with ::DATE. DuckDB and Postgres accept both forms. SQL Server and SQLite reject both. CAST(text AS DATE) runs on all four engines, but SQLite returns the number 2016 without an error, so on SQLite write date('2016-07-04 10:30:00').

DuckDB Postgres SQL Server SQLite
DATE '2016-07-04' 2016-07-04 2016-07-04 error error
'2016-07-04 10:30:00'::DATE 2016-07-04 2016-07-04 error error
CAST('2016-07-04 10:30:00' AS DATE) 2016-07-04 2016-07-04 2016-07-04 2016
the same, with date(...) on SQLite 2016-07-04 2016-07-04 2016-07-04 2016-07-04

See: §12. Dates: truncating, differencing, grouping by month

Days between dates#

The handbook writes date_diff('day', a, b), which only DuckDB accepts. DuckDB and Postgres also subtract one date from another directly. SQL Server writes DATEDIFF(DAY, a, b), and SQLite subtracts two julianday() values, which gives a decimal number.

DuckDB Postgres SQL Server SQLite
date_diff('day', a, b) from 2016-07-04 to 2016-07-16 12 error error error
the engine's own form 12 12 12 12.0

See: §12. Dates: truncating, differencing, grouping by month

First day of the month#

DuckDB and Postgres write date_trunc('month', d), SQL Server writes DATETRUNC(month, d), and SQLite writes date(d, 'start of month'). For 2016-07-16 all four land on 2016-07-01, and they return it as four different types. Cast the result with CAST(... AS DATE) in DuckDB and Postgres to get a date.

DuckDB Postgres SQL Server SQLite
type returned TIMESTAMP timestamp with time zone date text
value after the cast in DuckDB and Postgres 2016-07-01 2016-07-01 2016-07-01 2016-07-01

See: §12. Dates: truncating, differencing, grouping by month

Names with spaces#

A table or column name that contains a space, such as "Order Details", goes in double quotes, and all four engines accept that. SQL Server and SQLite also accept square brackets, as in [Order Details]. DuckDB and Postgres reject the brackets.

DuckDB Postgres SQL Server SQLite
SELECT 1 AS "Order Details" 1 1 1 1
SELECT 1 AS [Order Details] error error 1 1

See: What a database is

Shortcuts only DuckDB accepts#

ANTI JOIN, SEMI JOIN and GROUP BY ALL run in DuckDB and fail in the other three. When a query has to run on another engine, write the anti join as NOT EXISTS or as a LEFT JOIN followed by WHERE ... IS NULL, and list the grouping columns in GROUP BY.

DuckDB Postgres SQL Server SQLite
ANTI JOIN runs error error error
SEMI JOIN runs error error error
GROUP BY ALL runs error error error
LEFT JOIN ... WHERE ... IS NULL runs runs runs runs

See: §18. LEFT JOIN, and what it preserves · §14. GROUP BY

Last updated: Sunday, 10/18/2026