Functions & operators
Where the functions come from
Everything DuckDB documents under duckdb.org/docs/sql/functions is available: strftime, regexp_matches, list_aggregate, quantile_cont, string_agg, row_number() OVER (…), generate_series, and so on. Where a Postgres name differs from DuckDB’s, DuckDB usually accepts the Postgres alias too (substring, position, to_char via strftime, coalesce, nullif, greatest/least).
The shims below are the ones the engine adds or guarantees on top of that library because drivers and ORMs generate them.
| Postgres form | Status | Notes |
|---|---|---|
gen_random_uuid(), uuid() | Supported | Random UUID v4 as uuid. uuid()::text for the string. |
now(), current_timestamp, current_date | Supported | UTC. |
date_trunc('day', ts), EXTRACT(field FROM ts), date_part | Supported | Evaluated in UTC, so results are identical from any client. |
ts AT TIME ZONE 'zone', timezone('zone', ts) | Supported | AT TIME ZONE lowers to timezone(). |
SELECT a, b FROM t GROUP BY a where a is the key | Supported | Postgres allows a functionally dependent projection; the engine completes the GROUP BY with the bare columns so the result is identical. |
col = ANY($1) with an array parameter | Supported | The filter-by-list idiom; no string-built IN (…) needed. |
$1 … $n placeholders | Supported | Typed from the OID the driver declares, or inferred from the query when undeclared. |
::type casts, CAST(x AS type) | Supported | Postgres type names accepted (::bigint, ::numeric(10,2), ::jsonb, ::uuid, ::vector). |
nextval(), currval(), lastval() | Refused42P01 | There are no sequences (Scalar Function … does not exist); use RETURNING id. |
clock_timestamp() | Supported since 0.1.44 | Lowered to now() before execution; works inside expressions. |
pg_advisory_lock(), pg_sleep(), txid_current() | Refused42P01 | Scalar Function … does not exist. Use now() for the time. |
version(), current_database(), current_schema(), current_setting(), set_config(), pg_is_in_recovery() | Supported | Answered locally when called as the only expression in the SELECT; see Catalog. |
Arithmetic
| Expression | Status | Result |
|---|---|---|
NUMERIC / NUMERIC, NUMERIC / integer | Enforced since 0.1.43 | Exact. The quotient carries at least 16 fractional digits; CAST(… AS NUMERIC(p,s)) or assignment to a typed column rounds half away from zero, as Postgres does. |
x / 0 | Enforced22012 since 0.1.43 | division by zero. Never a silent NULL. |
integer / integer | Supported since 0.1.44 | Integer division, truncating toward zero (7 / 2 = 3, -7 / 2 = -3), as in Postgres. / 0 is 22012. Cast a side to NUMERIC or DOUBLE for a fractional result. |
integer / 0 | Partial | Returns NULL. Postgres raises 22012. Only the decimal path raises. |
DOUBLE / DOUBLE | Supported | IEEE double, as in Postgres. |
+ - * on NUMERIC | Enforced | Exact, with the Postgres result scale. Overflow past 38 digits is 22003. |
ROUND(x, n), TRUNC, CEIL, FLOOR, ABS, SIGN, MOD, POWER, SQRT | Supported | ROUND on NUMERIC rounds half away from zero. |
SUM, AVG, MIN, MAX on NUMERIC | Supported | Exact; AVG is an exact division. |
SELECT CAST(2.01 / 2 AS NUMERIC(10,2)); -- 1.01 (Postgres: 1.01)
SELECT ROUND(100.00 * 0.0325 / 12, 2); -- 0.27 monthly interest, exact
SELECT 7 / 2; -- 3.5 (Postgres: 3) ← integer division differs
SELECT 1 / 0; -- ERROR: division by zero (22012)
SELECT CAST(7 AS NUMERIC) / 2; -- 3.5 exact NUMERIC
SELECT amount / 0 FROM ledger;
-- ERROR: division by zero (SQLSTATE 22012)Strings
DuckDB’s string library, including ||, concat, length, lower/upper, trim, split_part, regexp_replace, regexp_matches, regexp_extract, like/ilike, similar to, starts_with, left/right, repeat, format. LIKE is case-sensitive, ILIKE is not, as in Postgres. String comparison is byte-wise; there are no collations.
JSON
A JSON/JSONB column accepts the Postgres operators and DuckDB’s json_* functions:
SELECT payload->'sku' FROM events; -- JSON
SELECT payload->>'sku' FROM events; -- text
SELECT (payload->>'amount')::NUMERIC(12,2) FROM events; -- typed
SELECT json_extract(payload, '$.items[0].sku') FROM events;
SELECT json_extract_string(payload, '$.sku') FROM events;
SELECT json_keys(payload) FROM events;
SELECT json_array_length(payload->'tags') FROM events;
SELECT unnest(json_extract(payload, '$.tags[*]')) AS tag FROM events;
SELECT payload::json->'tags'->>0 FROM events; -- Postgres-style cast round-tripPaths use JSONPath ($.a.b[0], $.tags[*]). json_extract returns JSON, json_extract_string returns text. Nested STRUCT and ARRAY columns answer the same operators, because they are carried as JSON on the read path. @>, ? and the other Postgres containment operators are not available; express containment with json_extract comparisons.
For fields you filter or aggregate on constantly, promote them to typed columns and keep JSON for the long tail.
Vector operators
The pgvector distance operators work on VECTOR(n) columns and literals:
SELECT id FROM docs ORDER BY emb <-> '[0.12, -0.04, …]' LIMIT 10; -- L2
SELECT id FROM docs ORDER BY emb <=> '[0.12, -0.04, …]'::vector LIMIT 5; -- cosine
SELECT a <#> b FROM pairs; -- negative inner product| Operator | Distance |
|---|---|
<-> | Euclidean (L2) |
<#> | Negative inner product |
<=> | Cosine distance |
Exactly these three. A '[…]'::vector cast is accepted and dropped. Scores are distances: lower is closer. An ORDER BY <distance> LIMIT k uses an HNSW or IVF index when one exists (Schema changes → Indexes) and an exact scan otherwise. Session knobs: SET hnsw.ef_search = N, SET ivfflat.probes = N, SET dodil.vector_exact_scan = on (force exact), SET dodil.vector_consistency = strong | eventual. The full vector story, including index choice and recall, is on Vector.
Graph functions
Tables bound into a graph with CREATE GRAPH are traversable from SQL through the graph_* functions (k-hop, neighbours, shortest path, PageRank, connected components, BFS) and from Cypher over Bolt. They are documented on Graph → SQL functions.
Reserved words
DuckDB’s reserved words apply. The ones that collide with common column names: at, end, order, table, to, user. A bare at TIMESTAMP in CREATE TABLE, or WHERE at > …, is 42601. Double-quote the identifier everywhere ("at"), or choose a name such as occurred_at.