Mirajv1.0
EN

37. PostgreSQL: SQL functions

This chapter describes the operators and functions that a MIRAJ instance in PostgreSQL language makes available to queries: strings, pattern matching, numbers, dates and intervals, formatting (to_char, to_date, to_number), conditional expressions, aggregates (including statistical and ordered-set aggregates), window functions, JSON and JSONB (including SQL/JSON paths), conversions, bytes and hashing, system functions, arrays and set-returning functions. Every example was run on a real instance; the results shown are the ones it returned.

The chapter describes the PostgreSQL language as MIRAJ speaks it. Not all PostgreSQL functions are present and a few behave differently: section 37.16 lists them, with the SQLSTATE and the message received.

Table of contents#


37.0 General#

  • Case-insensitive names. upper, UPPER and Upper designate the same function. This chapter writes them in lowercase, like the PostgreSQL documentation. The pg_catalog. prefix is accepted and ignored (pg_catalog.version()).
  • NULL propagation. Unless stated otherwise, a function that receives a NULL argument returns NULL. The exceptions (coalesce, concat, concat_ws, format, quote_nullable, array_to_string...) are flagged in their description; aggregates ignore NULL values (except array_agg, json_agg and jsonb_agg).
  • Positions and lengths are counted in characters, starting at 1. octet_length and bit_length count bytes and bits of the UTF-8 encoding.
  • Byte-by-byte text comparisons. Like PostgreSQL under the "C" collation, texts are compared and sorted byte by byte: 'abc' = 'ABC' and 'é' = 'e' are false, ORDER BY places uppercase letters before lowercase ones and accented letters after z, 'a' and 'A' are two distinct values for DISTINCT, GROUP BY or a UNIQUE key. To compare regardless of case and accents: ILIKE, ~*, lower(x) = lower(y), unaccent or the COLLATE utf8mb4_general_ci collation (see 37.3).
  • Unknown function: error 42883 (FUNCTION … does not exist); wrong number of arguments: 42883 as well, with the PostgreSQL message (function btrim(unknown, unknown, unknown) does not exist).
  • Type of a result: pg_typeof(expr) returns it. The tables in this chapter give the type announced to the client.
  • A computed column without an alias carries the text as written (SELECT upper(nom) FROM client names the column upper(nom), where PostgreSQL writes upper): give an alias (AS …) to columns that are read by name.

Example database#

The examples use the ventes database of the manual. The lines below give the results displayed in this chapter.

CREATE SCHEMA ventes;
SET search_path TO ventes;

CREATE TABLE client (
  id serial PRIMARY KEY, nom varchar(80) NOT NULL, ville varchar(60),
  email varchar(120), cree_le timestamp NOT NULL);
CREATE TABLE article (
  id serial PRIMARY KEY, reference varchar(20) NOT NULL UNIQUE, designation varchar(120) NOT NULL,
  prix numeric(10,2) NOT NULL, stock integer NOT NULL DEFAULT 0, attributs jsonb);
CREATE TABLE commande (
  id serial PRIMARY KEY, client_id integer NOT NULL REFERENCES client (id),
  date_commande date NOT NULL, statut varchar(20) NOT NULL DEFAULT 'en cours', livree_le timestamp);
CREATE TABLE ligne_commande (
  commande_id integer NOT NULL REFERENCES commande (id), article_id integer NOT NULL REFERENCES article (id),
  quantite integer NOT NULL, prix_unitaire numeric(10,2) NOT NULL, PRIMARY KEY (commande_id, article_id));

INSERT INTO client (nom, ville, email, cree_le) VALUES
  ('Dupont', 'Lyon', '[email protected]', '2026-01-15 09:30:00'),
  ('Martin', 'Paris', '[email protected]', '2026-02-03 14:05:00'),
  ('Bernard', 'Lyon', NULL, '2026-03-21 08:00:00'),
  ('Élodie Petit', NULL, '[email protected]', '2026-04-02 17:45:30');
INSERT INTO article (reference, designation, prix, stock, attributs) VALUES
  ('STY-001', 'Stylo bille bleu', 1.20, 500, '{"couleur": "bleu", "tags": ["bureau", "ecriture"], "dim": {"l": 14, "d": 0.8}}'),
  ('CAH-096', 'Cahier 96 pages', 2.50, 120, '{"couleur": "rouge", "tags": ["papier"], "pages": 96}'),
  ('CLA-A4', 'Classeur A4', 4.90, 35, '{"couleur": "noir", "tags": [], "recycle": true}'),
  ('AGR-024', 'Agrafeuse', 12.00, 0, NULL);
INSERT INTO commande (client_id, date_commande, statut, livree_le) VALUES
  (1, '2026-09-28', 'livrée', '2026-10-01 10:15:00'), (1, '2026-10-05', 'en cours', NULL),
  (2, '2026-10-06', 'livrée', '2026-10-08 16:40:00'), (3, '2026-10-09', 'annulée', NULL);
INSERT INTO ligne_commande VALUES
  (1, 1, 100, 1.20), (1, 2, 10, 2.50), (2, 3, 4, 4.90), (3, 1, 50, 1.10), (3, 4, 2, 12.00), (4, 2, 5, 2.50);

The column types are described in chapter 33. Data types, queries in chapter 36. SELECT queries.


37.1 Operators#

Arithmetic#

OperatorRoleExampleResult
+, -, *addition, subtraction, multiplication7 * 214
/division; integer division (truncated toward zero) between two integers7 / 2, -7 / 2, 7 / 2.03, -3, 3.5000000000000000
DIV, div(a, b)integer division (truncated toward zero)7 DIV 2, div(-17, 5)3, -3
%, mod(a, b)remainder, with the sign of the dividend17 % 5, mod(-17, 5)2, -2
^, power(a, b), pow(a, b)power2 ^ 101024
-xnegation-(3 + 2)-5
SELECT 7 / 2, -7 / 2, 7 / 2.0, 1.0 / 3, 7::float8 / 2, 7 % 2, 2 ^ 3;
3 | -3 | 3.5000000000000000 | 0.33333333333333333333 | 3.5 | 1 | 8

As in PostgreSQL, the division of two integers is an integer division: 7 / 2 is 3, of the operands' type. For a decimal quotient, make one operand decimal or floating-point: 7 / 2.0, 7::numeric / 2, 7::float8 / 2.

Result types:

OperationType
integer + - * / % integerinteger; smallint between two smallint, bigint as soon as one operand is a bigint
with a numericnumeric: scale of the operands for + - * (2.50 * 3 is 7.50, 1.10 * 1.1 is 1.210); for /, at least sixteen significant digits, never fewer decimals than an operand (7 / 2.0 is 3.5000000000000000, 1.0 / 3 is 0.33333333333333333333, 2.50 / 2 is 1.25000000000000000000)
with a double precision or a realdouble precision
integer ^ integer, power(2, 10)double precision; exact numeric to 16 digits as soon as one operand is a decimal (2 ^ 0.5 is 1.4142135623730950)

A written integer that fits in 32 bits is an integer, a larger integer a bigint, a number with a decimal point or an exponent (1.5, 1e3) a numeric.

Calculation errors:

StatementResponse
SELECT 1 / 0, 5 % 0, 1.0 / 0.0, div(1, 0)22012: division by zero
SELECT 2147483647 + 122003: integer out of range (convert one operand: 2147483647::bigint + 1 is 2147483648)
SELECT 9223372036854775807 + 122003: BIGINT value is out of range in '9223372036854775807 + 1'
SELECT power(0, -1)2201F: zero raised to a negative power is undefined
SELECT power(-8, 0.5)2201F: a negative number raised to a non-integer power yields a complex result

mod(a, 0) returns the same 22012 error as a % 0.

Comparison#

OperatorRole
=, <>, !=, <, <=, >, >=comparison; NULL if an operand is NULL
a BETWEEN x AND y, a NOT BETWEEN x AND yclosed range
a IN (v1, v2, …), a NOT IN (…), a IN (SELECT …)membership in a list or a subquery
a IS NULL, a IS NOT NULL, a ISNULL, a NOTNULLNULL test
a IS DISTINCT FROM b, a IS NOT DISTINCT FROM bcomparison that treats NULL as a value
b IS TRUE, IS NOT TRUE, IS FALSE, IS NOT FALSE, IS UNKNOWNboolean test
a = ANY (array), a = SOME (array), a <> ALL (array), a > ANY (array)comparison with each element of an array (see 37.14)
a = ANY (SELECT …), a < ALL (SELECT …), EXISTS (SELECT …)comparison with a subquery
(a, b) = (c, d), (a, b) < (c, d), (a, b) IN ((1, 'x'), (2, 'y')), ROW(a, b) = ROW(c, d)row comparison
SELECT 5 BETWEEN 1 AND 10, 5 IN (1, 5), 1 IS DISTINCT FROM NULL, NULL IS NOT DISTINCT FROM NULL,
       1 = ANY (ARRAY[1, 2]), 3 <> ALL (ARRAY[1, 2]), (1, 2) < (1, 3);
t | t | t | t | t | t | t
SELECT x ISNULL, x NOTNULL, 'abc' ~~ 'a%', 'abc' ~~* 'A%', 'abc' !~~ 'b%' FROM (SELECT NULL::int AS x) t;
t | f | t | t | t

~~, ~~*, !~~ and !~~* are the operators of LIKE, ILIKE, NOT LIKE and NOT ILIKE (see 37.3).

A condition placed in the list of a SELECT is a boolean, returned as t or f. Placed in a calculation or passed to a function, it is the integer 1 or 0: (1 = 1) + 1 is 2. pg_typeof(1 = 1) returns boolean, and the condition converted to text is written true or false.

Logic#

AND, OR, NOT, with the standard three-valued logic:

SELECT true AND NULL, false AND NULL, true OR NULL, NOT true;
NULL | f | t | f

&&, || (as OR) and ! are not logical operators in PostgreSQL language: they are rejected (42601) or have another meaning (|| concatenates).

Concatenation#

a || b concatenates two values, converted to text if needed. The result is NULL if either one is NULL.

SELECT 'Stylo' || ' ' || 'bille', 'Total : ' || 12.5 || ' €', 'n=' || NULL, coalesce('n=' || NULL, 'sans');
Stylo bille | Total : 12.5 € | NULL | sans

A concatenated boolean is written true or false ('abc' || true is abctrue); the concat function writes it as t or f. Between two jsonb documents, || merges the documents (see 37.10); between two json values, it is rejected (42883).

Bitwise#

OperatorRoleExampleResult
a & bAND5 & 31
a | bOR5 | 37
a # bexclusive OR5 # 36
~acomplement~5-6
a << n, a >> nshifts1 << 4, -8 >> 116, -4

Integers are signed. bit_count(n) counts the bits set to 1 (bit_count(7) is 3).

Bit strings#

On bit strings (bit(n), bit varying(n), literals B'101' and X'1F', see 33. Data types), the same operators work position by position and return a bit string:

Operator or functionRoleExampleResult
a & b, a | b, a # bAND, OR, exclusive OR of two strings of the same lengthB'101' & B'110'100 (bit)
~acomplement, over the positions of the string~B'101'010
a << n, a >> nshifts, length kept, incoming zerosB'10001' >> 200100
a || bconcatenationB'101' || B'11'10111 (bit varying)
length(a), bit_length(a)number of positionslength(B'10111')5
octet_length(a)number of bytesoctet_length(B'101100111')2
bit_count(a)number of bits set to 1bit_count(B'1101')3
get_bit(a, n)position n, counted from 0 from the leftget_bit(B'101', 1)0
set_bit(a, n, v)string whose position n is v (0 or 1)set_bit(B'101', 1, 1)111
substring(a from d for n)extracted positionssubstring(B'110010111111' from 3 for 2)00
position(s in a)rank of the substringposition(B'010' in B'000001101011')8
overlay(a placing s from d for n)replaced positionsoverlay(B'0101' placing B'11' from 2 for 2)0111

&, | and # require two strings of the same length (22026: cannot AND bit strings of different sizes). Next to a bit string, a written text is read as a bit string (b & '110'); a number is rejected (42883: operator does not exist: bit & integer), as is an arithmetic operator (b + 1). get_bit outside the string: 2202E (bit index 3 out of valid range (0..2)). get_bit and set_bit exist only for a bit string.

Precedence#

From highest to lowest: ::; unary -; ^; *, /, %; +, -; ||; comparisons; NOT; AND; OR. ^ is left-associative (2 ^ 3 ^ 2 is 64). Since + comes before ||, 1 + 2 || 'a' is 3a and 'a' || 1 + 2 is a3.

Since :: comes before the unary minus, -3.7::int reads as -(3.7::int) and is -4, like (-3.7)::int.

Two intervals in a row (d + INTERVAL '1 month' - INTERVAL '1 day') read without parentheses; INTERVAL '1 hour' * 2 multiplies the interval.


37.2 String functions#

Summary:

FunctionRoleType
a || b, concat, concat_wsconcatenationtext
upper, lower, initcapcasetext
length, char_length, character_lengthlength in charactersinteger
octet_length, bit_lengthlength in bytes, in bitsinteger
trim, btrim, ltrim, rtrimremoval of characters (or bytes) at the endstext, bytea
substring, substrsubstringtext
left, rightstart, endtext
position, strposposition of a substringinteger
starts_withprefixboolean
replace, translate, overlayreplacementtext
lpad, rpad, repeat, reversepadding, repetition, reversaltext
split_partnth field of a split stringtext
ascii, chrcode of a character, character of a codeinteger, text
formattemplate with %s, %I, %Ltext
quote_ident, quote_literal, quote_nullablequoting for built SQLtext
normalize, unistrUnicode normal form, Unicode escapestext
parse_identsplitting of a qualified nametext[]
levenshtein, soundex, difference, similarity, unaccentdistance, phonetic code, similarity, accent removalinteger, text, real
md5, sha256, to_hex, encode, decode, convert_to, convert_fromhashing, encodingsee 37.12
regexp_replace, regexp_match and regular expressionspatternssee 37.3
to_char, to_numberformatting and reading of a number or a datesee 37.6
string_aggconcatenation of a groupsee 37.8

lower(1) and upper(1.5) are rejected (42883: function lower(integer) does not exist): these two functions exist only for text. The others convert their argument to text (length(12) is 2).

concat and concat_ws#

concat(value1, value2, ...)
concat_ws(separator, value1, value2, ...)

concat puts its arguments end to end, converted to text; NULL arguments are ignored (concat('a', NULL, 'b') is ab, concat(NULL) an empty string), whereas 'a' || NULL is NULL.

concat_ws inserts separator between the non-NULL arguments; NULL values are ignored. A NULL separator returns NULL.

A boolean is written there as t or f, as in PostgreSQL: concat('a', true) is at, concat_ws(',', true, 1 = 2) is t,f (whereas 'a' || true is atrue). The distinction is kept in the definition of a view, a generated column or a default value.

SELECT concat(nom, ' (', ville, ')') AS a,
       nom || ' (' || coalesce(ville, '?') || ')' AS b,
       concat_ws(' - ', nom, ville, email) AS c
FROM client ORDER BY id;
 a               | b                | c
-----------------+------------------+------------------------------------
 Dupont (Lyon)   | Dupont (Lyon)    | Dupont - Lyon - [email protected]
 Martin (Paris)  | Martin (Paris)   | Martin - Paris - [email protected]
 Bernard (Lyon)  | Bernard (Lyon)   | Bernard - Lyon
 Élodie Petit () | Élodie Petit (?) | Élodie Petit - [email protected]

upper, lower, initcap#

upper(string)    lower(string)    initcap(string)

Uppercase, lowercase (Unicode rules), and first letter of each word in uppercase, the others in lowercase.

SELECT upper('café'), lower('CAFÉ'), initcap('jean-pierre de la fontaine');
CAFÉ | café | Jean-Pierre De La Fontaine

Lengths#

length(string)    char_length(string)    character_length(string)
octet_length(string)    bit_length(string)

length, char_length and character_length count characters; octet_length counts the bytes of the UTF-8 encoding; bit_length eight times more. On a bytea, length and octet_length count bytes.

SELECT length('café'), char_length('café'), octet_length('café'), bit_length('café'), length('abc'::bytea);
4 | 4 | 5 | 40 | 3

trim, btrim, ltrim, rtrim#

trim([BOTH | LEADING | TRAILING] [characters] FROM string)
trim(string)
btrim(string [, characters])    ltrim(string [, characters])    rtrim(string [, characters])

Remove characters at the start (LEADING, ltrim), at the end (TRAILING, rtrim) or on both sides (BOTH, btrim, the default of trim). Without characters, spaces are removed.

SELECT '[' || trim('  x  ') || ']', '[' || ltrim('  x  ') || ']', '[' || rtrim('  x  ') || ']';
SELECT trim(BOTH 'x' FROM 'xxaxx'), trim(LEADING 'x' FROM 'xxaxx'), trim(TRAILING 'x' FROM 'xxaxx');
SELECT btrim('yxaxyx', 'xy'), ltrim('yxaxx', 'xy'), rtrim('axyyx', 'xy');
[x] | [x  ] | [  x]
a | axx | xxa
a | axx | a

characters is read as a set of characters, as in PostgreSQL, in all forms: trim(BOTH 'ab' FROM 'baxab') and btrim('baxab', 'ab') are x. trim is also written with a list (trim(BOTH FROM string, characters), trim(LEADING string, characters)) and is kept as btrim / ltrim / rtrim in the definition of a view or a generated column.

These functions exist only for text and for bytea:

  • an argument of another type (number, boolean, date, time, interval, uuid, json, jsonb) is rejected: trim(42) returns 42883 (function btrim(integer) does not exist); convert it (trim(n::text));
  • on a bytea, it is bytes that are removed and the result is a bytea: btrim(bytes, bytes), ltrim, rtrim, trim([BOTH | LEADING | TRAILING] bytes FROM bytes). An untyped literal is read as bytea there ('\x00'). The bytes to remove are mandatory (btrim(bytes): 42883), and a text does not mix with bytes (btrim(bytes, text_column): 42883);
  • more than two arguments: 42883 (function btrim(unknown, unknown, unknown) does not exist).
SELECT btrim('\x0000ff1000'::bytea, '\x00'), trim(LEADING '\x00' FROM '\x0000ff1000'::bytea), rtrim('\x0000ff1000'::bytea, '\x00');
\xff10 | \xff1000 | \x0000ff10

substring and substr#

substring(string FROM start [FOR length])
substring(string FOR length)
substring(string, start [, length])
substr(string, start [, length])
substring(string FROM pattern)

Substring from character start (1 for the first), over length characters or to the end. With a text instead of start, substring(string FROM pattern) reads pattern as a regular expression (see 37.3) and returns the first match, or the text of the first captured group if the pattern has one; NULL if there is no match.

SELECT substring('abcdef' FROM 2 FOR 3), substring('abcdef' FROM 3), substring('abcdef' FOR 2), substr('abcdef', 4);
SELECT substring(email FROM position('@' IN email) + 1) FROM client WHERE id = 1;
bcd | cdef | ab | def
exemple.fr
SELECT substring('abc123def' FROM '[0-9]+'), substring('abc123def' FROM 'c(\d)'), substring('abc' FROM 'z');
123 | 1 | NULL

A negative start counts from the end (substring('abcdef', -2) is ef), and a zero start returns an empty string: PostgreSQL counts these positions outside the string differently.

left and right#

left(string, n)    right(string, n)

The first or last n characters; with a negative n, everything except the last |n| (left) or first |n| (right) characters.

SELECT left('abcdef', 2), right('abcdef', 2), left('abcdef', -2), right('abcdef', -2);
ab | ef | abcd | cdef

position and strpos#

position(substring IN string)    strpos(string, substring)    starts_with(string, prefix)

Position of the first occurrence, 0 if it is absent. starts_with returns a boolean.

SELECT position('cd' IN 'abcdef'), strpos('abcdef', 'cd'), position('z' IN 'abc'), starts_with('abc', 'ab');
3 | 3 | 0 | t

replace, translate, overlay#

replace(string, search, replacement)
translate(string, from, to)
overlay(string PLACING new FROM start [FOR length])
  • replace replaces all occurrences of search.
  • translate replaces each character of from with the character of the same rank in to; a character of from with no counterpart is removed.
  • overlay replaces length characters (the length of new by default) starting at start.
SELECT replace('aXbXc', 'X', '-'), translate('12345', '143', 'ax'),
       overlay('abcdef' PLACING 'ZZ' FROM 2 FOR 3), overlay('abcdef' PLACING 'ZZ' FROM 2);
a-b-c | a2x5 | aZZef | aZZdef

lpad, rpad, repeat, reverse#

lpad(string, length [, fill])    rpad(string, length [, fill])
repeat(string, n)    reverse(string)

lpad and rpad pad on the left or on the right up to length characters (with spaces by default) and truncate a longer string.

SELECT lpad('7', 3, '0'), rpad('7', 3, '*'), lpad('abcdef', 3), '[' || lpad('ab', 5) || ']', repeat('ab', 3), reverse('abc');
SELECT reference, lpad(id::text, 6, '0') AS numero, rpad(designation, 20, '.') || to_char(prix, '990.00') AS ligne
FROM article ORDER BY id;
007 | 7** | abc | [   ab] | ababab | cba

 reference | numero | ligne
-----------+--------+-----------------------------
 STY-001   | 000001 | Stylo bille bleu....   1.20
 CAH-096   | 000002 | Cahier 96 pages.....   2.50
 CLA-A4    | 000003 | Classeur A4.........   4.90
 AGR-024   | 000004 | Agrafeuse...........  12.00

split_part#

split_part(string, separator, n)

The nth field of string split by separator; a negative n counts from the end; empty string if the field does not exist. Zero n: 22023 (field position must not be zero).

SELECT split_part('a,b,c', ',', 2), split_part('a,b,c', ',', 5), split_part('a,b,c', ',', -1);
SELECT reference, split_part(reference, '-', 1) AS famille FROM article WHERE id = 1;
SELECT split_part(email, '@', 2) AS domaine FROM client WHERE id = 1;
b |  | c
STY-001 | STY
exemple.fr

ascii and chr#

ascii(string)    chr(code)

chr returns the character of Unicode code point code (chr(0): 54000, null character not permitted). ascii returns the Unicode code point of the first character (ascii('é') is 233), 0 for an empty string.

SELECT ascii('A'), ascii('é'), ascii('€uro'), chr(66), chr(233), chr(8364);
65 | 233 | 8364 | B | é | €

format#

format(template, value1, value2, ...)

Replaces each marker in template with the next argument:

MarkerWrites
%sthe value as text; nothing for NULL
%Ithe value as an SQL identifier, in double quotes if needed; NULL: 22004
%Lthe value as an SQL literal in single quotes; NULL is written NULL
%%the % character
%2$sthe argument of the given rank
%10s, %-10s, %*sminimum width, right-aligned, left-aligned, or read from the previous argument
SELECT format('%s : %s €', designation, prix) FROM article WHERE id = 1;
SELECT format('%I = %L', 'nom', nom), format('%2$s %1$s', 'a', 'b'), format('[%5s|%-5s]', id, id) FROM client WHERE id = 1;
Stylo bille bleu : 1.20 €
nom = 'Dupont' | b a | [    1|1    ]

Too few arguments: 22023 (too few arguments for format()). Another marker (%d): 22023 (unrecognized format() type specifier).

quote_ident, quote_literal, quote_nullable#

quote_ident(text)    quote_literal(value)    quote_nullable(value)

Quote a value to insert it into a built SQL statement: identifier in double quotes if needed, literal in single quotes (single quotes doubled). quote_literal(NULL) returns NULL, quote_nullable(NULL) returns the text NULL.

SELECT quote_ident('Mon Id'), quote_ident('abc'), quote_literal('it''s'), quote_literal(42), quote_nullable(NULL);
"Mon Id" | abc | 'it''s' | '42' | NULL

normalize and unistr#

normalize(string [, NFC | NFD | NFKC | NFKD])
unistr(string)

normalize returns the requested Unicode normal form (NFC by default): NFD separates letters from their accents, NFKC and NFKD also replace compatibility characters (ligatures...). The form is written as a keyword, without quotes. unistr replaces the Unicode escapes written in the text: \XXXX and \uXXXX (4 hexadecimal digits), \+XXXXXX and \UXXXXXXXX (6 and 8 digits); an invalid escape is rejected (22025: invalid Unicode escape).

SELECT length('é'), length(normalize('é', NFD)), normalize('fi', NFKC), unistr('caf\00e9 \+01F600');
1 | 2 | fi | café 😀

parse_ident#

parse_ident(qualified_name [, strict])

Splits a qualified name into its elements, returned as an array of texts (see 37.14). An unquoted element is lowercased, a quoted element is kept as is. With strict set to false, whatever follows the name is ignored; otherwise, a malformed name is rejected (22023: string is not a valid identifier: "a..b").

SELECT parse_ident('ventes."Mon Client"'), parse_ident('Ventes.Client'), parse_ident('ventes.client()', false);
{ventes,"Mon Client"} | {ventes,client} | {ventes,client}

Approximate comparison: levenshtein, soundex, similarity, unaccent#

Functions of common PostgreSQL extensions (fuzzystrmatch, pg_trgm, unaccent), present without installation (CREATE EXTENSION is accepted and has no effect):

FunctionReturnsType
levenshtein(a, b [, insertion, deletion, substitution])minimum number of edits to turn a into b, with the given costs (1 by default)integer
levenshtein_less_equal(a, b [, costs…], max)the same distance if it does not exceed max; otherwise a number greater than maxinteger
soundex(text)phonetic code (a letter and three digits)text
difference(a, b)number of characters shared by the two soundex codes, from 0 to 4integer
similarity(a, b)trigram similarity, from 0 to 1real
unaccent([dictionary,] text)the text without its accents or ligaturestext
SELECT levenshtein('kitten', 'sitting'), levenshtein('kitten', 'sitting', 1, 1, 2), levenshtein_less_equal('kitten', 'sitting', 2),
       soundex('Robert'), soundex('Rupert'), difference('Robert', 'Rupert'), similarity('word', 'words'), unaccent('Élodie à Noël');
SELECT nom FROM client WHERE levenshtein(lower(nom), 'dupond') <= 1;
SELECT nom FROM client WHERE unaccent(lower(nom)) LIKE 'elo%';
3 | 5 | 3 | R163 | R163 | 4 | 0.5714286 | Elodie a Noel
Dupont
Élodie Petit

37.3 Pattern matching#

LIKE and ILIKE#

string [NOT] LIKE pattern [ESCAPE character]
string [NOT] ILIKE pattern
string ~~ pattern    string ~~* pattern    string !~~ pattern    string !~~* pattern

% stands for any sequence of characters, _ for one character. The backslash \ (or the character given by ESCAPE) removes the special meaning of % and _.

SELECT 'abc' LIKE 'a_c', 'a%b' LIKE 'a\%b', 'a%b' LIKE 'a#%b' ESCAPE '#', 'axb' LIKE 'a\%b';
SELECT nom FROM client WHERE nom ILIKE 'd%';
t | t | t | f
Dupont

LIKE is case- and accent-sensitive, like =: 'ABC' LIKE 'abc' and 'é' LIKE 'e' are false; ILIKE ignores both. The names of the catalog tables (pg_class.relname, information_schema.tables.table_name...) are an exception: like any MIRAJ name, they are compared regardless of case (relname ~ '^(CLIENT)$' finds client). To compare regardless of case and accents, apply the utf8mb4_general_ci collation to the string; COLLATE "C", "POSIX" and ucs_basic compare byte by byte, like the default collation:

SELECT 'abc' LIKE 'A%', 'abc' ILIKE 'A%', 'abc' = 'ABC', 'abc' COLLATE utf8mb4_general_ci = 'ABC', 'é' COLLATE utf8mb4_general_ci = 'e', 'abc' COLLATE "C" = 'ABC';
SELECT nom FROM client WHERE nom COLLATE utf8mb4_general_ci = 'dupont';
f | t | f | t | t | f
Dupont

Regular expressions: ~, ~*, !~, !~*#

OperatorMeaning
string ~ patternthe string contains a match of the pattern
string ~* patternsame, ignoring case and accents
string !~ pattern, string !~* patternno match
SELECT 'a1' ~ '^[a-z]\d$', 'a b' ~ '\s', 'abc' ~ 'a|z', 'aaa' ~ '^a{3}$', 'abc' ~* 'B', 'abc' !~ 'z';
SELECT nom FROM client WHERE nom ~ '^[A-D]' ORDER BY nom;
t | t | t | t | t | t

Bernard
Dupont

The pattern is written in a standard string, where \ is an ordinary character: '\d' is enough. ~ is case-sensitive ('abc' ~ 'B' is false), ~* ignores case. An invalid pattern ('abc' ~ '(') is a 2201B error (invalid regular expression: error: unclosed group), as for the regexp_… functions.

regexp_replace#

regexp_replace(string, pattern, replacement [, start [, n]] [, flags])

Replaces the first match of the pattern; all of them with the g flag; the nth with n (n = 0: all), searching from character start. In replacement, \1 to \9 reuse the captured groups and \& the whole match. Like ~, regexp_replace is case-sensitive, except with the i flag.

SELECT regexp_replace('abc123', '[0-9]+', '#'), regexp_replace('aaa', 'a', 'b', 'g'),
       regexp_replace('Hello', 'L', 'x'), regexp_replace('Hello', 'L', 'x', 'i'),
       regexp_replace('2026-10-11', '(\d+)-(\d+)-(\d+)', '\3/\2/\1'),
       regexp_replace('a1b2c3', '[0-9]', '#', 1, 2),
       regexp_replace('  a   b  ', '\s+', ' ', 'g');
abc# | bbb | Hello | Hexlo | 11/10/2026 | a1b#c3 |  a b 

Invalid pattern: 2201B (invalid regular expression: error: unclosed group).

SELECT reference, regexp_replace(reference, '^([A-Z]+)-0*', '\1:') AS court FROM article ORDER BY id;
 reference | court
-----------+--------
 STY-001   | STY:1
 CAH-096   | CAH:96
 CLA-A4    | CLA:A4
 AGR-024   | AGR:24

Flags#

The regexp_… functions accept a string of flags:

FlagEffect
icase ignored
ccase respected (default)
gall matches: regexp_replace, regexp_matches only (regexp_match rejects it: 22023, regexp_match() does not support the "global" option)
n. does not match the newline, ^ and $ match the starts and ends of lines
xextended syntax: whitespace in the pattern is ignored

Another flag is rejected (22023: invalid regular expression option: "q"), as is a start lower than 1 (22023: invalid value for parameter "start": 0).

regexp_match and regexp_matches#

regexp_match(string, pattern [, flags])
regexp_matches(string, pattern [, flags])

regexp_match returns the first match as an array of texts (see 37.14): one element per captured group, or the whole match if the pattern has no group; NULL if there is none. An element is read by its rank: (regexp_match(…))[2].

regexp_matches is a set-returning function: it returns one row per match (all of them with the g flag, the first one otherwise), no row if there is none. It is used in a FROM or in the SELECT list.

SELECT regexp_match('Commande 2026-10-11', '(\d+)-(\d+)-(\d+)'), regexp_match('abc', 'z'), (regexp_match('ref STY-001', '([A-Z]+)-(\d+)'))[2];
SELECT regexp_matches('ab12cd34', '([a-z]+)(\d+)', 'g');
{2026,10,11} | NULL | 001

{ab,12}
{cd,34}

regexp_split_to_array, regexp_split_to_table, regexp_count#

regexp_split_to_array(string, pattern [, flags])
regexp_split_to_table(string, pattern [, flags])
regexp_count(string, pattern [, start [, flags]])

regexp_split_to_array splits the string at each match of the pattern and returns the array of pieces; regexp_split_to_table returns the same pieces as rows (set-returning function, WITH ORDINALITY numbers the rows). regexp_count counts the matches, starting from character start.

SELECT regexp_split_to_array('a, b,c', ',\s*'), regexp_count('a1b22c333', '\d+'), regexp_count('AbAb', 'a', 1, 'i');
SELECT * FROM regexp_split_to_table('Stylo bille  bleu', '\s+') WITH ORDINALITY AS t(mot, rang);
{a,b,c} | 3 | 2

Stylo | 1
bille | 2
bleu | 3

regexp_substr, regexp_instr, regexp_like#

regexp_substr(string, pattern [, start [, n [, flags [, group]]]])
regexp_instr(string, pattern [, start [, n [, end [, flags [, group]]]]])
regexp_like(string, pattern [, flags])
  • regexp_substr returns the nth match (the first by default) found from character start, or the text of the captured group of rank group; NULL if there is none;
  • regexp_instr returns the position of this match (with end set to 1, the position that follows it), 0 if there is none;
  • regexp_like returns a boolean; like ~, it is case-sensitive, except with the i flag.
SELECT regexp_substr('a1b22c333', '\d+', 1, 3), regexp_substr('STY-001', '([A-Z]+)-(\d+)', 1, 1, '', 2),
       regexp_instr('a1b22c333', '\d+', 1, 2), regexp_instr('a1b22c333', '\d+', 1, 2, 1),
       regexp_like('abc', 'B'), regexp_like('abc', 'B', 'i');
333 | 001 | 4 | 6 | f | t

37.4 Numeric functions#

Summary:

FunctionRoleResult type
abs(x)absolute valuethat of x
sign(x)-1, 0 or 1integer
round(x), round(x, n)rounding to the integer, to n decimalsnumeric (double precision for a float)
trunc(x), trunc(x, n)truncation toward zeronumeric
ceil(x), ceiling(x), floor(x)next integer up, downinteger for a decimal (double precision for a float)
mod(a, b), a % bremainder of the divisioninteger or numeric
div(a, b), a DIV binteger quotientnumeric; integer for DIV
power(a, b), pow(a, b), a ^ bpowerdouble precision or numeric (see 37.1)
sqrt(x), cbrt(x), exp(x), ln(x), log(x), log10(x), log2(x), log(b, x)square and cube roots, exponential, logarithmsdouble precision
pi(), degrees(x), radians(x)π, angle conversionsdouble precision
sin, cos, tan, cot, asin, acos, atan, atan2(y, x)trigonometry, in radiansdouble precision
gcd(a, b), lcm(a, b)GCD, LCMinteger
factorial(n)factorialnumeric
width_bucket(x, low, high, n)rank of the bucket that contains xinteger
scale(x), min_scale(x), trim_scale(x)scale of a numeric, scale without trailing zeros, value without trailing zerosinteger, integer, numeric
greatest(a, b, …), least(a, b, …)largest, smallest valuecommon type
random(), random_normal([mean [, std_dev]])random number in [0, 1), following a normal distributiondouble precision
setseed(x)seeds the session's random generator (x from -1 to 1)

abs, sign#

SELECT abs(-12.50), abs(-3), sign(-12.5), sign(0), sign(7);
12.50 | 3 | -1 | 0 | 1

round, trunc, ceil, floor#

round(x [, n])    trunc(x [, n])    ceil(x)    ceiling(x)    floor(x)

round rounds a numeric to the nearest value, halves moving away from zero; a negative n rounds to the left of the decimal point. A double precision without n is rounded to the nearest even integer, as in PostgreSQL.

SELECT round(2.5), round(-2.5), round(2.567, 2), round(1250, -2), round(2.5::float8), round(3.5::float8);
SELECT trunc(2.567, 2), trunc(-2.567), trunc(1299, -2), ceil(2.1), ceil(-2.1), floor(2.9), floor(-2.1);
3 | -3 | 2.57 | 1300 | 2 | 4
2.56 | -2 | 1200 | 3 | -2 | 2 | -3

Example on the database: total amount, total increased by 20% and average price of the order lines.

SELECT sum(quantite * prix_unitaire) AS total,
       round(sum(quantite * prix_unitaire) * 1.2, 2) AS ttc,
       round(avg(prix_unitaire), 2) AS prix_moyen
FROM ligne_commande;
 total  | ttc    | prix_moyen
--------+--------+------------
 256.10 | 307.32 | 4.03

mod, div#

SELECT mod(17, 5), mod(-17, 5), mod(17.5, 5), 17 % 5, div(17, 5), div(-17, 5), 17 DIV 5;
2 | -2 | 2.5 | 2 | 3 | -3 | 3

power, sqrt, exp, logarithms#

SELECT power(2, 10), power(2.5, 2), 10 ^ -2, sqrt(2), exp(1), ln(exp(1)), log(1000), log(2, 1024), log10(1000), log2(1024);
1024 | 6.2500000000000000 | 0.01 | 1.4142135623730951 | 2.718281828459045 | 1 | 3 | 10 | 3 | 10
SELECT cbrt(27), cbrt(-8), scale(1.230), min_scale(1.230), trim_scale(1.230), pow(2, 3);
3 | -2 | 3 | 2 | 1.23 | 8

log(x) is the decimal logarithm, ln(x) the natural logarithm, log(b, x) the logarithm to base b. Outside their domain, sqrt(-1), ln(0), log(-1), asin(2) and acos(2) return NULL (PostgreSQL raises an error).

Trigonometry#

Angles in radians.

SELECT pi(), degrees(pi()), radians(180), sin(pi() / 2), cos(0), atan2(1, 1);
3.141592653589793 | 180 | 3.141592653589793 | 1 | 1 | 0.7853981633974483

gcd, lcm, factorial, width_bucket#

SELECT gcd(12, 18), lcm(4, 6), factorial(5), factorial(20);
SELECT width_bucket(5.5, 0, 10, 5), width_bucket(-1, 0, 10, 5), width_bucket(10, 0, 10, 5);
6 | 12 | 120 | 2432902008176640000
3 | 0 | 6

width_bucket(x, low, high, n) splits [low, high) into n equal buckets and returns the rank of the one that contains x: 0 below low, n + 1 from high on. factorial(-1): 2201E (factorial of a negative number is undefined).

greatest, least#

greatest(value1, value2, ...)    least(value1, value2, ...)

Largest and smallest of the values (numbers, texts, dates). A NULL argument returns NULL (PostgreSQL ignores NULL values).

SELECT greatest(3, 7, 5), least(3, 7, 5), greatest('abc', 'abd'), least(date '2026-01-01', date '2025-01-01'), greatest(1, NULL);
7 | 3 | abd | 2025-01-01 | NULL

random#

random() returns a random double precision, greater than or equal to 0 and less than 1. For an integer from 0 to 9: floor(random() * 10)::int. random_normal(mean, std_dev) draws a number from a normal distribution (0 and 1 by default). setseed(x) reseeds the session's generator: the draws that follow are then reproducible. gen_random_uuid() is described in 37.13.

SELECT round(random_normal(100, 0)), random_normal() IS NOT NULL;
100 | t

Outside [-1, 1], setseed is rejected (22023: setseed parameter 2 is out of allowed range [-1,1]).


37.5 Dates, times and intervals#

The date, time, timestamp (without time zone) and timestamptz (an instant, displayed in the session's time zone) types are described in chapter 33. Data types; SET TIME ZONE in chapter 38. Transactions and session. The examples in this section that display a timestamptz were run after:

SET TIME ZONE 'Europe/Paris';

Current date and time#

ExpressionReturnsType
now(), current_timestamp, transaction_timestamp()date and time of the start of the transaction (of the statement, outside a block)timestamptz
statement_timestamp(), clock_timestamp()date and time of the start of the statement; of the clock at each calltimestamptz
current_timestamp(n)same, with n decimals of a secondtimestamptz
localtimestamp, localtimestamp(n)current date and time of the session, without time zonetimestamp
current_datetoday's datedate
current_time, current_time(n)current time, without offsettime
localtime, localtime(n)current time of the sessiontime
timeofday()date and time of the clock, as texttext
SELECT now(), localtimestamp, current_date, current_time;
2026-10-11 21:36:38+02 | 2026-10-11 21:36:38 | 2026-10-11 | 21:36:38

Literals#

SELECT date '2026-10-11', time '14:30:00', timestamp '2026-10-11 14:30:00',
       timestamptz '2026-10-11 14:30:00+02', '2026-10-11'::date, '2026-10-11T12:30:00Z'::timestamptz;
2026-10-11 | 14:30:00 | 2026-10-11 14:30:00 | 2026-10-11 14:30:00+02 | 2026-10-11 | 2026-10-11 14:30:00+02

Dates are written in ISO format (YYYY-MM-DD). The words 'today', 'tomorrow', 'yesterday', 'now' and 'epoch' are read as in PostgreSQL. A text that is not a valid date is rejected ('2026-02-30'::date, '11/10/2026'::date: 22008, date/time field value out of range: "2026-02-30"): to read another format, use to_date (37.6).

SELECT 'today'::date = current_date, 'tomorrow'::date - 'yesterday'::date, 'epoch'::timestamp;
t | 2 | 1970-01-01 00:00:00

Arithmetic#

OperationResultExampleValue
date + integer, date - integerdatedate '2026-10-11' + 72026-10-18
date - datenumber of days (integer)date '2026-12-25' - date '2026-10-11'75
date + timetimestampdate '2026-10-11' + time '14:30'2026-10-11 14:30:00
date ± intervaltimestampdate '2026-01-31' + INTERVAL '1 month'2026-02-28 00:00:00
timestamp ± intervaltimestamptimestamp '2026-10-11 14:30:00' - INTERVAL '1 week'2026-10-04 14:30:00
timestamptz ± intervaltimestamptznow() + INTERVAL '2 hours'
time ± intervaltime, wrapped around 24 hourstime '23:30' + INTERVAL '2 hours'01:30:00
time - timeintervaltime '16:00' - time '14:30'01:30:00
timestamp - timestampintervaltimestamp '2026-10-13 16:30:00' - timestamp '2026-10-11 14:30:00'2 days 02:00:00

A month added to an end of month gives the end of the next month (2026-01-31 + 1 month = 2026-02-28). A number added to a timestamp, or a decimal number added to a date, is rejected (42883: operator does not exist: date + numeric): add an interval.

SELECT id, date_commande, livree_le,
       livree_le - date_commande AS delai,
       livree_le::date - date_commande AS jours
FROM commande WHERE livree_le IS NOT NULL ORDER BY id;
 id | date_commande | livree_le           | delai           | jours
----+---------------+---------------------+-----------------+-------
  1 | 2026-09-28    | 2026-10-01 10:15:00 | 3 days 10:15:00 | 3
  3 | 2026-10-06    | 2026-10-08 16:40:00 | 2 days 16:40:00 | 2

timestamptz and daylight saving changes. An interval in days, weeks, months or years keeps the session's civil time; an interval in hours, minutes or seconds counts elapsed time. The difference of two timestamptz values is the real duration between the two instants. Around the switch to winter time on October 25, 2026:

SELECT timestamptz '2026-10-25 00:30:00+02' + INTERVAL '1 day' AS un_jour,
       timestamptz '2026-10-25 00:30:00+02' + INTERVAL '24 hours' AS vingt_quatre_heures,
       timestamptz '2026-10-26 00:30:00+01' - timestamptz '2026-10-25 00:30:00+02' AS ecart;
 un_jour                | vingt_quatre_heures    | ecart
------------------------+------------------------+----------------
 2026-10-26 00:30:00+01 | 2026-10-25 23:30:00+01 | 1 day 01:00:00

Intervals#

An interval is written INTERVAL 'quantity unit …' or '…'::interval (also col::interval, $1::interval). As in PostgreSQL, it keeps its months, its days and its duration apart, is written 1 mon 2 days 03:00:00 and is announced as interval (OID 1186) to the client; a column can be of type interval (the written text is checked there, 22007, and rewritten: '25 hours' becomes 25:00:00).

NotationAccepted
unitsmicrosecond(s), millisecond(s), second(s) / sec, minute(s) / min, hour(s) / h, day(s) / d, week(s), month(s) / mon, year(s)
several units'2 hours 30 minutes', '1 day 02:00:00', '1 year 2 months', '2 weeks 3 days'
time'01:30', '1:30:15.5'
decimal or negative quantity'1.5 hours', '-1 day', '1 day ago'
standard formINTERVAL '3' DAY (after a date: d + INTERVAL '3' DAY)
ISO 8601'P1D', 'P1Y2M3DT4H5M6S' (M: months before the T, minutes after), 'PT1.5S', 'P2W', 'P0001-02-03T04:05:06'
SELECT timestamp '2026-10-11 14:30:00' + INTERVAL '90 minutes',
       timestamp '2026-10-11 14:30:00' + INTERVAL '1 day 2 hours',
       timestamp '2026-10-11 14:30:00' + INTERVAL '1 year 2 months',
       timestamp '2026-10-11 14:30:00' + INTERVAL '500 milliseconds',
       timestamp '2026-10-11 14:30:00' + '2 hours 30 minutes'::interval;
2026-10-11 16:00:00 | 2026-10-12 16:30:00 | 2027-12-11 14:30:00 | 2026-10-11 14:30:00.5 | 2026-10-11 17:00:00

Written alone in a SELECT list, an interval returns its text, PostgreSQL style:

SELECT INTERVAL '45 days', INTERVAL '2 hours 30 minutes', INTERVAL '1 year 2 months', INTERVAL '1 week',
       INTERVAL '1 day 12 hours', INTERVAL '1 month 1 day', INTERVAL '1.5 days';
45 days | 02:30:00 | 1 year 2 mons | 7 days | 1 day 12:00:00 | 1 mon 1 day | 1 day 12:00:00
OperationResultExampleValue
interval ± intervalfield by fieldINTERVAL '1 mon' - INTERVAL '1 day'1 mon -1 days
interval * n, n * interval, interval / n, -intervaleach field, the fraction of a month moving to 30-day days, that of a day to hoursINTERVAL '1 mon' * 1.51 mon 15 days
=, <>, <, >…by duration (30-day month, 24-hour day)INTERVAL '1 day' = INTERVAL '24 hours't
ORDER BY, BETWEEN, IN, greatest, leastby duration (greatest and least ignore NULL values)INTERVAL '1 day' IN ('24:00:00')t
DISTINCT, GROUP BY (including ROLLUP, CUBE, GROUPING SETS), count(DISTINCT …), joinby duration: '1 day' and '24:00:00' are the same value, the value returned is that of one of the rows
UNION, INTERSECT, EXCEPT (including WITH RECURSIVE … UNION)by duration; UNION keeps the first notation encounteredSELECT INTERVAL '1 day' UNION SELECT INTERVAL '24:00:00'1 day
primary key, UNIQUE, unique indexby duration: '24:00:00' is rejected (23505) if '1 day' exists; a foreign key to the column follows the same equality
min, maxby durationmax(duree)1 mon
sum, avgfield by field; avg divides like interval / n (sum(DISTINCT …), avg(DISTINCT …): 0A000)avg(duree)10 days 17:00:00

A text next to an interval is read as an interval (WHERE duree > '1 day'); interval / 0: 22012; interval + 1: 42883. justify_*, age and timestamp - timestamp return an interval.

In a UNION, INTERSECT or EXCEPT, in the recursive term of a WITH RECURSIVE as in a VALUES list, a text written without a type next to an interval is also read as an interval (SELECT INTERVAL '1 day' UNION SELECT '24:00:00': one row 1 day, of type interval); if unreadable, it is rejected (22007). See 5.8.

extract and date_part#

extract(field FROM source)
date_part('field', source)

Return a field of a date, a timestamp, a timestamptz or a time. extract returns a numeric, date_part a double precision. The field can be written in quotes in extract (extract('month' FROM d)).

FieldValueFor 2026-10-11 14:30:45.5 (a Sunday)
year, month, dayyear, month, day2026, 10, 11
hour, minutehour, minute14, 30
secondseconds with their fraction45.500000
milliseconds, microsecondsseconds in milliseconds, in microseconds45500.000, 45500000
dowday of the week, 0 = Sunday0
isodowISO day of the week, 1 = Monday, 7 = Sunday7
doyday of the year284
weekISO week41
isoyearyear of the ISO week2026
quarterquarter4
decade, century, millenniumdecade, century, millennium202, 21, 3
julianJulian day2461325
epochseconds since January 1, 1970 at 00:00 UTCsee below
SELECT extract(year FROM date '2026-10-11'), extract(second FROM timestamp '2026-10-11 14:30:45.5'),
       extract(dow FROM date '2026-10-11'), extract(week FROM date '2027-01-01'), extract(isoyear FROM date '2027-01-01'),
       date_part('hour', timestamp '2026-10-11 14:30:45.5'), extract(hour FROM time '14:30:45');
2026 | 45.500000 | 0 | 53 | 2026 | 14 | 14

epoch counts the exact instant of a timestamptz; a date or a timestamp without time zone is read as UTC, as in PostgreSQL. The result is an integer for a date, with six decimals otherwise:

SELECT extract(epoch FROM timestamptz '2026-10-11 14:30:00+02') AS instant,
       extract(epoch FROM timestamp '2026-10-11 14:30:00') AS sans_fuseau,
       extract(epoch FROM date '2026-10-11') AS jour;
 instant           | sans_fuseau       | jour
-------------------+-------------------+------------
 1791721800.000000 | 1791729000.000000 | 1791676800

From an interval, extract reads its fields like PostgreSQL: hour is not wrapped to the day (extract(hour FROM INTERVAL '26 hours') is 26), epoch counts the month as 30 days and the year as 365.25 (extract(epoch FROM a - b): the duration in seconds). For a duration between two instants, the two epoch values can also be subtracted:

SELECT id, (extract(epoch FROM livree_le) - extract(epoch FROM date_commande)) / 3600 AS heures
FROM commande WHERE livree_le IS NOT NULL ORDER BY id;
 id | heures
----+---------------------
 1  | 82.2500000000000000
 3  | 64.6666666666666667

extract(timezone FROM …): 0A000 (EXTRACT(timezone FROM …) is not supported yet in the PostgreSQL language).

date_trunc#

date_trunc('field', source)

Brings source back to the start of the unit: microseconds, milliseconds, second, minute, hour, day, week (Monday), month, quarter, year, decade, century, millennium. The result is a timestamp for a date or a timestamp, a timestamptz for a timestamptz (truncated in the session's time zone). Unknown unit: 22023 (unit "fortnight" not recognized for type timestamp without time zone).

SELECT date_trunc('month', timestamp '2026-10-11 14:30:45'), date_trunc('week', timestamp '2026-10-11 14:30:45'),
       date_trunc('hour', timestamp '2026-10-11 14:30:45'), date_trunc('quarter', date '2026-08-15')::date;
2026-10-01 00:00:00 | 2026-10-05 00:00:00 | 2026-10-11 14:00:00 | 2026-07-01

Number of orders per month:

SELECT date_trunc('month', date_commande)::date AS mois, count(*) AS commandes
FROM commande GROUP BY 1 ORDER BY 1;
 mois       | commandes
------------+-----------
 2026-09-01 | 1
 2026-10-01 | 3

Last day of the month of a date:

SELECT ((date_trunc('month', date '2026-02-10') + INTERVAL '1 month') - INTERVAL '1 day')::date;   -- 2026-02-28

make_date, make_time, make_timestamp, make_timestamptz, make_interval#

make_date(year, month, day)
make_time(hour, minute, second)
make_timestamp(year, month, day, hour, minute, second)
make_timestamptz(year, month, day, hour, minute, second [, zone])
make_interval([years [, months [, weeks [, days [, hours [, mins [, secs]]]]]]])

Build a date, a time, a timestamp, a timestamptz (time read in zone, the session's by default) or an interval; the second can have a fraction. The arguments can also be written in named notation (make_date(year => 2026, month => 3, day => 1), make_interval(days => 3, hours => 2)); omitted make_interval arguments are 0. A value outside the calendar is a 22008 error (date field value out of range: 2026-02-30, time field value out of range: 25:00:00).

SELECT make_date(2026, 2, 28), make_time(10, 30, 15.5), make_timestamp(2026, 2, 28, 10, 30, 0);
SELECT make_timestamptz(2026, 10, 11, 14, 30, 0), make_timestamptz(2026, 10, 11, 14, 30, 0, 'UTC');
SELECT make_interval(days => 3, hours => 2), make_interval(1, 2), make_interval(weeks => 2, secs => 1.5);
2026-02-28 | 10:30:15.5 | 2026-02-28 10:30:00
2026-10-11 14:30:00+02 | 2026-10-11 16:30:00+02
3 days 02:00:00 | 1 year 2 mons | 14 days 00:00:01.5

date_bin#

date_bin(stride, source, origin)

Brings source (a timestamp or a timestamptz) back to the start of the slice of length stride that contains it, the slices being counted from origin. The stride contains neither months nor years (22023: timestamps cannot be binned into intervals containing months or years) and it is positive (22023: stride must be greater than zero).

SELECT date_bin('15 minutes', timestamp '2026-10-11 14:38:20', timestamp '2026-01-01'),
       date_bin('1 hour', timestamp '2026-10-11 14:38:20', timestamp '2026-01-01 00:30');
2026-10-11 14:30:00 | 2026-10-11 14:30:00

date_add, date_subtract, timezone#

date_add(instant, interval [, zone])
date_subtract(instant, interval [, zone])
timezone(zone, value)

date_add and date_subtract add or remove an interval to or from a timestamptz; days and months are counted in civil time of zone (the session's by default), hours in elapsed time. timezone(zone, value) is the function form of value AT TIME ZONE zone (see below).

SELECT date_add(timestamptz '2026-10-24 12:00+02', interval '1 day'), date_add(timestamptz '2026-10-24 12:00+02', interval '24 hours'),
       date_subtract(timestamptz '2026-10-25 12:00+01', interval '1 day', 'UTC');
SELECT timezone('America/New_York', timestamptz '2026-10-11 14:30:00+02'), timezone('UTC', timestamp '2026-10-11 14:30:00');
2026-10-25 12:00:00+01 | 2026-10-25 11:00:00+01 | 2026-10-24 13:00:00+02
2026-10-11 08:30:00 | 2026-10-11 16:30:00+02

isfinite(date | timestamp | interval) returns t: a date, a time or an interval is always finite ('infinity'::timestamp is rejected, 22007).

age, justify_days, justify_hours, justify_interval#

age(end, start)    age(date_or_timestamp)
justify_days(interval)    justify_hours(interval)    justify_interval(interval)

age(end, start) returns the gap in years, months and days, as an interval; age(t) counts from today's date. justify_days groups days into 30-day months, justify_hours hours into 24-hour days, justify_interval does both.

SELECT age(date '2026-10-11', date '1990-05-20'), age(timestamp '2026-10-11 10:00', timestamp '2026-10-11 08:30'),
       age(timestamp '2024-02-29', timestamp '2026-10-11');
SELECT justify_days(INTERVAL '45 days'), justify_hours(INTERVAL '50 hours'), justify_interval(INTERVAL '400 days');
36 years 4 mons 22 days | 01:30:00 | -2 years -7 mons -11 days
1 mon 15 days | 2 days 02:00:00 | 1 year 1 mon 10 days

The number of years is read with extract:

SELECT extract(year FROM age(date '2026-10-11', date '1990-05-20')), pg_typeof(age(date '2026-10-11', date '1990-05-20'));
36 | interval

AT TIME ZONE#

value AT TIME ZONE 'zone'
  • a timestamptz becomes the timestamp of the civil time in zone;
  • a timestamp (without time zone) is read as a time in zone and becomes a timestamptz.

zone is a time zone name ('Europe/Paris', 'UTC', 'America/New_York') or an offset; a written offset ('+02') is read west of Greenwich (POSIX notation), as in PostgreSQL. Unknown time zone: 22023 (time zone "Mars/Olympus" not recognized).

SELECT timestamptz '2026-10-11 14:30:00+02' AT TIME ZONE 'America/New_York' AS a_new_york,
       timestamptz '2026-07-14 12:00:00+02' AT TIME ZONE 'UTC' AS en_utc,
       timestamp '2026-10-11 14:30:00' AT TIME ZONE 'UTC' AS instant;
 a_new_york          | en_utc              | instant
---------------------+---------------------+------------------------
 2026-10-11 08:30:00 | 2026-07-14 10:00:00 | 2026-10-11 16:30:00+02

to_timestamp (seconds)#

to_timestamp(seconds) returns the timestamptz located that many seconds after January 1, 1970 at 00:00 UTC; it is the inverse of extract(epoch FROM …).

SELECT to_timestamp(1760000000), to_timestamp(0) AT TIME ZONE 'UTC';
2025-10-09 10:53:20+02 | 1970-01-01 00:00:00

37.6 Formatting: to_char, to_date, to_timestamp#

to_char of a date or time#

to_char(date_or_time, template)

Writes a date, a timestamp, a timestamptz (in the session time zone) or a time according to template.

PatternWritesFor 2026-10-11 14:05:09.123456
YYYY, YYY, YY, Yyear, its last 3, 2 or 1 digits2026, 026, 26, 6
IYYYISO week year2026
MMmonth, 01 to 1210
Month, MONTH, monthEnglish month name, padded to 9 charactersOctober
Mon, MON, monabbreviated nameOct
DDday of the month, 01 to 3111
DDDday of the year284
Day, DAY, dayEnglish day name, padded to 9 charactersSunday
Dy, DY, dyabbreviated nameSun
Dday of the week, 1 = Sunday1
IDISO day of the week, 1 = Monday7
IW, WWISO week, week of the year41, 41
Qquarter4
CCcentury21
JJulian day2461325
HH24hour, 00 to 2314
HH12, HHhour, 01 to 1202
MI, SSminutes, seconds05, 09
SSSSseconds since midnight50709
MS, USmilliseconds, microseconds123, 123456
AM, PMhalf-day indicatorPM
TH, th (after a number)English ordinal suffix11TH, 11th
FM (before a pattern)no leading zeros or padding spacesFMDD: 11, FMMonth: October
"text"the text as is

Any other character is copied.

SELECT to_char(timestamp '2026-10-11 14:05:09', 'YYYY-MM-DD HH24:MI:SS'),
       to_char(timestamp '2026-10-11 14:05:09', 'DD/MM/YYYY HH12:MI AM'),
       to_char(timestamp '2026-10-11 14:05:09', 'FMDay, FMDD FMMonth YYYY'),
       to_char(date '2026-03-01', 'FMDD/FMMM/YYYY'),
       to_char(time '14:05:09', 'HH24hMI');
2026-10-11 14:05:09 | 11/10/2026 02:05 PM | Sunday, 11 October 2026 | 1/3/2026 | 14h05
SELECT nom, to_char(cree_le, 'DD/MM/YYYY à HH24"h"MI') AS cree, to_char(cree_le, 'YYYY"-T"Q') AS trimestre
FROM client ORDER BY id;
 nom          | cree               | trimestre
--------------+--------------------+-----------
 Dupont       | 15/01/2026 à 09h30 | 2026-T1
 Martin       | 03/02/2026 à 14h05 | 2026-T1
 Bernard      | 21/03/2026 à 08h00 | 2026-T1
 Élodie Petit | 02/04/2026 à 17h45 | 2026-T2

Day and month names are in English. More patterns:

PatternMeaning
FF1 to FF6fraction of a second, 1 to 6 digits (FF3: milliseconds)
RM, rmmonth in Roman numerals (XI), padded to 4 characters without FM
TZ, tzabbreviation of the session time zone for a timestamptz (CEST, UTC; the offset, +05:30, for a zone without a name or the machine's zone); empty for a timestamp
OFtime zone offset from UTC (+02, +05:30); +00 for a timestamp
TZH, TZMhours and minutes of the offset (+02, 00)
Wweek of the month, from 1 to 5 (the first starts on the 1st)
TM (before Day, Month...)name translated into the language of lc_time; it is C: English names
SET TIME ZONE 'Europe/Paris';
SELECT to_char(timestamptz '2026-07-04 05:06:07+00', 'HH24:MI TZ OF'),
       to_char(timestamp '2026-03-04 05:06:07.123456', 'SS.FF3'), to_char(date '2026-11-04', 'FMRM');
SELECT to_char(timestamptz '2026-07-04 05:06:07+00', 'HH24:MI TZH:TZM'), to_char(date '2026-10-11', 'W'), to_char(date '2026-10-11', 'TMDay TMMonth');
07:06 CEST +02 | 07.123 | XI
07:06 +02:00 | 2 | Sunday October

to_date and to_timestamp do not read FF1...FF6, RM, TZ or OF.

to_char of an interval#

to_char(interval, template)

Same patterns as for a date: YYYY, MM (years and months taken from the months), DD (days), DDD (days, months counted as 30), HH24 (hours not reduced to the day: 50 hours gives 50), HH12 / HH, MI, SS, MS, US, SSSS (seconds of the duration), CC, Q, WW, with FM, TH and text between quotes. A negative field keeps its sign (-05). Month and day names, AM / PM, D, ID, IW, IYYY and J have no meaning for an interval: 22007 (invalid format specification for an interval value).

SELECT to_char(interval '15h 2m 12s', 'HH24:MI:SS'), to_char(interval '50 hours 5.25 seconds', 'HH24 HH12 MI SS MS'),
       to_char(interval '1 year 2 months 3 days', 'YYYY-MM-DD'), to_char(interval 'P2DT3H', 'FMDD "jours" FMHH24 "h"');
15:02:12 | 50 02 00 05 250 | 0001-02-03 | 2 jours 3 h

to_char of a number#

to_char(number, template)
PatternMeaning
9a digit; a non-significant zero is replaced by a space
0a digit; leading zeros are written
., Ddecimal separator (a point)
,, Gthousands separator (a comma)
Ssign + or -, written in place
MItrailing - sign for a negative number
Lcurrency symbol from lc_monetary; it is C: a space
PRnegative number between angle brackets (<485>), positive between two spaces
RN, rnRoman numerals (uppercase, lowercase) of the rounded integer, from 1 to 3999, on 15 right-aligned positions; outside these bounds: 15 #
EEEEscientific notation, with as many decimals as the template (9.99EEEE)
Vnumber multiplied by 10 to the power of the number of digits that follow V (99V9)
FMno leading spaces or trailing zeros

Without FM, the result starts with the place of the sign: a space for a positive number. A zero number is written 0 in a template without decimals (to_char(0, '99'): ' 0'); before decimals, the zero of the integer part is not written (to_char(0.5, '99.99'): ' .50'), like PostgreSQL.

SELECT '[' || to_char(1234.5, '9999.99') || ']', '[' || to_char(1234.5, 'FM9999.00') || ']',
       '[' || to_char(12, '9999') || ']', '[' || to_char(12, 'FM9999') || ']',
       to_char(7, 'FM000'), to_char(-7, '000'), to_char(1234567.891, 'FM9,999,999.99'),
       to_char(12.5, 'FMS999.9'), to_char(0.126, 'FM0.00');
[ 1234.50] | [1234.50] | [   12] | [12] | 007 | -007 | 1,234,567.89 | +12.5 | 0.13
SELECT reference, to_char(prix, 'FM9990.00') AS prix, to_char(prix * stock, 'FM999G990D00') AS valeur_stock,
       to_char(stock, 'FM000000') AS stock
FROM article ORDER BY id;
 reference | prix  | valeur_stock | stock
-----------+-------+--------------+--------
 STY-001   | 1.20  | 600.00       | 000500
 CAH-096   | 2.50  | 300.00       | 000120
 CLA-A4    | 4.90  | 171.50       | 000035
 AGR-024   | 12.00 | 0.00         | 000000

The number is rounded to the number of decimals of the template. A number too large for the template is written as # (to_char(12345, '999') returns ###).

SELECT '[' || to_char(-485, '999PR') || ']', to_char(485, 'FMRN'), to_char(1994, 'FMrn'),
       '[' || to_char(0.0004859, '9.99EEEE') || ']', '[' || to_char(-1234.5, '9.99EEEE') || ']',
       '[' || to_char(12.45, '99V9') || ']';
[<485>] | CDLXXXV | mcmxciv | [ 4.86e-04] | [-1.23e+03] | [ 125]

The SG pattern is not recognized, and neither is to_char with a single argument.

to_number#

to_number(text, template)

Reads a number written according to a template made of to_char patterns (9, 0, . / D, , / G, S, MI, L); thousands separators and the currency symbol are skipped. The result is a numeric.

SELECT to_number('12,454.8-', '99G999D9S'), to_number('$1,234.50', 'L9,999.99'), to_number('-42', '999'), pg_typeof(to_number('1', '9'));
-12454.8 | 1234.50 | -42 | numeric

A text without a readable digit is rejected (22P02); the EEEE pattern is not allowed for input (0A000: "EEEE" not supported for input, like PostgreSQL). For a text in ordinary decimal format, the ::numeric conversion is enough (see 37.11).

to_date and to_timestamp#

to_date(text, template)
to_timestamp(text, template)

Read a text according to a template written with the to_char patterns. to_date returns a date, to_timestamp a timestamptz (the time read is that of the session time zone).

SELECT to_date('11/10/2026', 'DD/MM/YYYY'), to_date('20261011', 'YYYYMMDD'), to_date('11 oct 2026', 'DD Mon YYYY'),
       to_date('11 October 2026', 'DD Month YYYY'), to_date('11.10.26', 'DD.MM.YY'), to_date('2026-284', 'YYYY-DDD');
SELECT to_timestamp('2026-10-11 14:30:45', 'YYYY-MM-DD HH24:MI:SS'),
       to_timestamp('11/10/2026 02:30 PM', 'DD/MM/YYYY HH12:MI AM');
2026-10-11 | 2026-10-11 | 2026-10-11 | 2026-10-11 | 2026-10-11 | 2026-10-11
2026-10-11 14:30:45+02 | 2026-10-11 14:30:00+02

A missing field takes its starting value (to_date('10/2026', 'MM/YYYY') returns 2026-10-01). Errors:

StatementResponse
SELECT to_date('31/02/2026', 'DD/MM/YYYY')22008: date/time field value out of range: "31/02/2026"
SELECT to_date('abc', 'DD/MM/YYYY')22007: invalid value "ab" for "DD"
SELECT to_timestamp('2026-10-11 25:00', 'YYYY-MM-DD HH24:MI')22008: date/time field value out of range: "2026-10-11 25:00"

37.7 Conditional expressions#

CASE#

CASE WHEN condition THEN result [WHEN ...] [ELSE result] END
CASE expression WHEN value THEN result [WHEN ...] [ELSE result] END

Returns the result of the first true branch, that of ELSE otherwise, NULL without ELSE.

SELECT reference, stock,
       CASE WHEN stock = 0 THEN 'rupture' WHEN stock < 50 THEN 'faible' ELSE 'ok' END AS etat
FROM article ORDER BY id;
 reference | stock | etat
-----------+-------+---------
 STY-001   | 500   | ok
 CAH-096   | 120   | ok
 CLA-A4    | 35    | faible
 AGR-024   | 0     | rupture

The CASE expression WHEN value form compares with =, so byte by byte for a text: CASE ville WHEN 'lyon' THEN 1 ELSE 0 END is 0 for Lyon.

The result type is the common type of the branches: integer and numeric give a numeric. Branches of different families (CASE WHEN ... THEN 1 ELSE 'a' END) are returned as text, where PostgreSQL rejects the query.

coalesce#

coalesce(value1, value2, ...)

The first value that is not NULL; NULL if all of them are.

SELECT nom, coalesce(ville, 'inconnue') AS ville FROM client ORDER BY id;
 nom          | ville
--------------+----------
 Dupont       | Lyon
 Martin       | Paris
 Bernard      | Lyon
 Élodie Petit | inconnue

nullif#

nullif(a, b)

NULL if a = b, a otherwise. Used to avoid a division by zero:

SELECT reference, prix / nullif(stock, 0) AS prix_par_unite_en_stock FROM article ORDER BY id;
 reference | prix_par_unite_en_stock
-----------+-------------------------
 STY-001   | 0.0024000000000000
 CAH-096   | 0.0208333333333333
 CLA-A4    | 0.1400000000000000
 AGR-024   | NULL

Since the comparison is case-sensitive, nullif('a', 'A') returns a.

greatest and least#

See 37.4. A NULL argument returns NULL.

Booleans#

TRUE and FALSE are returned as t and f. A boolean column and the ::boolean conversion of a text read t, true, yes, on, 1, their opposites and their prefixes; any other text is rejected (22P02: invalid input syntax for type boolean: "maybe"). An integer also converts (0: false); a numeric, a float, a date or a time do not convert (2.5::boolean: 42846, cannot cast type numeric to boolean), as in PostgreSQL.

SELECT 1::boolean, 0::boolean, 'yes'::boolean, 'off'::boolean, true::int, false::int;
t | f | t | f | 1 | 0

A boolean converted to text is written true or false: true::text, CAST(actif AS varchar), 'x' || true (xtrue). The ::boolean conversion of a computed text (column, value extracted from a document) returns a boolean, and likewise rejects a text that is not one (22P02).

SELECT true::text, false::varchar, 'actif : ' || true, ('{"ok": "yes"}'::jsonb ->> 'ok')::boolean;
true | false | actif : true | t

37.8 Aggregate functions#

An aggregate function computes a value for a group of rows (GROUP BY, see 36. SELECT queries) or for all rows. NULL values are ignored (except by array_agg, json_agg and jsonb_agg); on no rows, count and regr_count return 0 and the other aggregates NULL.

FunctionPurposeResult type
count(*), count(expr), count(DISTINCT expr)number of rows, of non-NULL values, of distinct valuesbigint
sum(expr)sumbigint for integers, numeric for numeric, double precision for floats
avg(expr)averagenumeric with at least 16 decimals; double precision for floats
min(expr), max(expr)smallest, largest valuethat of expr
any_value(expr)one of the non-NULL values of the groupthat of expr
string_agg(expr, separator)concatenationtext
array_agg(expr)array of the valuesarray (integer[], text[]...)
json_agg, jsonb_agg, json_object_agg, jsonb_object_aggJSON array or objectjson or jsonb
bool_and(cond), bool_or(cond), every(cond)all true, at least one trueboolean
stddev, stddev_samp, stddev_pop, variance, var_samp, var_popstandard deviation, variancenumeric or double precision
corr, covar_pop, covar_samp, regr_*correlation, covariance, linear regressiondouble precision (regr_count: bigint)
percentile_cont, percentile_disc, modequantiles, most frequent value (WITHIN GROUP)double precision, that of the value
rank, dense_rank, percent_rank, cume_dist with WITHIN GROUPrank a value would have in the groupbigint, double precision
bit_and(expr), bit_or(expr), bit_xor(expr)bitwise AND, OR, XOR (of a bit(n) column: the number, not the bit string)bigint

count, sum, avg, min, max#

SELECT count(*), count(ville), count(DISTINCT ville) FROM client;
SELECT sum(stock), avg(stock), min(prix), max(prix), sum(prix), avg(prix) FROM article;
4 | 3 | 2
655 | 163.7500000000000000 | 1.20 | 12.00 | 20.60 | 5.1500000000000000

avg of integers or decimals is an exact numeric with 16 decimals (or at the scale of the argument if it is larger); round for display: round(avg(prix), 2). sum of a bigint column is a numeric. sum(DISTINCT expr) and avg(DISTINCT expr) are accepted.

SELECT article_id, sum(quantite) AS quantite, round(avg(prix_unitaire), 2) AS prix_moyen, count(*) AS lignes
FROM ligne_commande GROUP BY article_id ORDER BY article_id;
 article_id | quantite | prix_moyen | lignes
------------+----------+------------+--------
 1          | 150      | 1.15       | 2
 2          | 15       | 2.50       | 2
 3          | 4        | 4.90       | 1
 4          | 2        | 12.00      | 1

min and max also apply to texts, dates and booleans.

FILTER#

aggregate(...) FILTER (WHERE condition)

Passes to the aggregate only the rows that satisfy the condition.

SELECT count(*) FILTER (WHERE statut = 'livrée') AS livrees,
       count(*) FILTER (WHERE statut <> 'livrée') AS autres
FROM commande;
SELECT count(*) FILTER (WHERE stock > 0), sum(stock) FILTER (WHERE prix < 3), avg(prix) FILTER (WHERE stock > 0) FROM article;
2 | 2
3 | 620 | 2.8666666666666667

A discarded row is not seen by the aggregate: it does not appear in json_agg (no null), and its NULL key does not make json_object_agg fail. A retained row keeps its value, NULL included. On a group with no retained row, count returns 0 and the other aggregates NULL. The result type is that of the aggregate without FILTER (sum(bigint): numeric).

SELECT json_agg(nom) FILTER (WHERE ville = 'Lyon') FROM client;
SELECT json_object_agg(reference, stock) FILTER (WHERE stock > 100) FROM article;
["Dupont", "Bernard"]
{"STY-001": 500, "CAH-096": 120}

FILTER only goes with an aggregate: after another function, 42809 (FILTER specified, but upper is not an aggregate function); after a window function that is not an aggregate (row_number, lag...), 0A000. The condition contains no aggregate (42803). The argument of the aggregate is computed for all rows, discarded ones included: a computation error on a discarded row (sum(10 / n) FILTER (WHERE n <> 0): 22012) is raised, where PostgreSQL does not compute the argument.

In a view or a routine body, the filtered aggregate is kept in the form aggregate(PG_AGG_FILTER(condition, argument)), which both languages read back (count(*): count(CASE WHEN condition THEN 1 END)).

string_agg#

string_agg(expr, separator [ORDER BY ...])
string_agg(DISTINCT expr, separator [ORDER BY ...])

Concatenates the non-NULL values of the group, separated by separator, in the requested order. Both arguments are required; expr is converted to text; separator must be a constant.

SELECT string_agg(nom, ', ' ORDER BY nom) FROM client;
SELECT string_agg(DISTINCT ville, '/') FROM client;
SELECT ville, string_agg(nom, ' + ' ORDER BY nom) AS clients FROM client GROUP BY ville ORDER BY ville;
Bernard, Dupont, Martin, Élodie Petit
Lyon/Paris

 ville | clients
-------+------------------
 Lyon  | Bernard + Dupont
 Paris | Martin
 NULL  | Élodie Petit
SELECT string_agg(reference || '=' || stock, '; ' ORDER BY stock DESC) FROM article;
STY-001=500; CAH-096=120; CLA-A4=35; AGR-024=0

On no rows, string_agg returns NULL. The sort follows byte order: Élodie comes after Martin.

array_agg#

array_agg(expr [ORDER BY ...])
array_agg(DISTINCT expr [ORDER BY ...])

Returns the array of the values of the group (see 37.14), NULL included, in the requested order; with DISTINCT, each value once, sorted (NULL last). On no rows, NULL. The result type is the array of the type of expr (integer[], text[]...).

SELECT array_agg(nom ORDER BY nom) AS noms, array_agg(DISTINCT ville) AS villes, array_agg(id) FILTER (WHERE ville = 'Lyon') AS lyonnais FROM client;
SELECT c.nom, array_agg(o.id ORDER BY o.id) AS commandes FROM client c JOIN commande o ON o.client_id = c.id GROUP BY c.nom ORDER BY c.nom;
{Bernard,Dupont,Martin,"Élodie Petit"} | {Lyon,Paris,NULL} | {1,3}

 nom     | commandes
---------+-----------
 Bernard | {4}
 Dupont  | {1,2}
 Martin  | {3}

JSON aggregates#

json_agg(expr [ORDER BY ...])    jsonb_agg(expr [ORDER BY ...])
json_object_agg(key, value)    jsonb_object_agg(key, value)

json_agg and jsonb_agg return the JSON array of the values of the group (NULL included, written null); json_object_agg and jsonb_object_agg an object. A NULL key is rejected (22032: JSON documents may not contain NULL member names.).

SELECT json_agg(nom ORDER BY nom) FROM client;
SELECT json_object_agg(reference, stock) FROM article;
SELECT c.nom, json_agg(jsonb_build_object('commande', o.id, 'date', o.date_commande) ORDER BY o.id) AS commandes
FROM client c JOIN commande o ON o.client_id = c.id GROUP BY c.nom ORDER BY c.nom;
["Bernard", "Dupont", "Martin", "Élodie Petit"]
{"STY-001": 500, "CAH-096": 120, "CLA-A4": 35, "AGR-024": 0}

 nom     | commandes
---------+--------------------------------------------------------------------------------
 Bernard | [{"date": "2026-10-09", "commande": 4}]
 Dupont  | [{"date": "2026-09-28", "commande": 1}, {"date": "2026-10-05", "commande": 2}]
 Martin  | [{"date": "2026-10-06", "commande": 3}]

bool_and, bool_or, every#

bool_and(condition) (or every) is true if the condition is true for all rows, bool_or(condition) if it is true for at least one. The result is a boolean (t / f), whether the argument is a boolean column or a condition; it is NULL with no rows. In a condition (HAVING bool_or(...)), it is used as is.

SELECT bool_and(stock > 0), bool_or(stock > 0), every(prix > 1) FROM article;
f | t | t

Standard deviation and variance#

FunctionComputes
stddev(expr), stddev_samp(expr)corrected (sample) standard deviation
stddev_pop(expr)population standard deviation
variance(expr), var_samp(expr)corrected variance
var_pop(expr)population variance

On integers or decimals, the result is an exact numeric with at least 16 decimals; on floats, a double precision.

SELECT stddev(quantite), variance(quantite), stddev_pop(quantite), var_pop(quantite), stddev(quantite::float8)
FROM ligne_commande;
39.4246116023988244 | 1554.3000000000000000 | 35.9895818258562154 | 1295.2500000000000000 | 39.42461160239883

Correlation and regression#

Two-argument aggregates, y (dependent variable) then x, computed on the rows where both are not NULL. The result is a double precision, except regr_count (bigint).

FunctionComputes
corr(y, x)correlation coefficient
covar_pop(y, x), covar_samp(y, x)population covariance, sample covariance
regr_slope(y, x), regr_intercept(y, x)slope and intercept of the least-squares line
regr_r2(y, x)square of the correlation coefficient
regr_count(y, x)number of retained rows
regr_avgx(y, x), regr_avgy(y, x)averages of x and y
regr_sxx(y, x), regr_syy(y, x), regr_sxy(y, x)sums of squares and of products of deviations
SELECT corr(y, x), covar_pop(y, x), covar_samp(y, x), regr_slope(y, x), regr_intercept(y, x), regr_r2(y, x), regr_count(y, x)
FROM (VALUES (1, 3), (2, 5), (3, 7), (4, NULL)) v(x, y);
SELECT regr_avgx(y, x), regr_avgy(y, x), regr_sxx(y, x), regr_syy(y, x), regr_sxy(y, x)
FROM (VALUES (1, 3), (2, 5), (3, 7), (4, NULL)) v(x, y);
1 | 1.3333333333333333 | 2 | 2 | 1 | 1 | 3
2 | 5 | 2 | 8 | 4

Ordered-set aggregates: WITHIN GROUP#

percentile_cont(fraction) WITHIN GROUP (ORDER BY expr)
percentile_disc(fraction) WITHIN GROUP (ORDER BY expr)
mode() WITHIN GROUP (ORDER BY expr)
rank(value) WITHIN GROUP (ORDER BY expr)        -- likewise dense_rank, percent_rank, cume_dist
  • percentile_cont returns the quantile fraction (from 0 to 1) of the sorted values, interpolated between the two neighboring values (double precision); percentile_cont(0.5) is the median;
  • percentile_disc returns the first value of the group whose rank reaches the fraction, without interpolation (type of expr);
  • mode() returns the most frequent value;
  • rank, dense_rank, percent_rank and cume_dist with an argument return the rank value would have if it were added to the group (NULLS FIRST / NULLS LAST accepted in the ORDER BY).

A fraction outside [0, 1] is rejected (22023: percentile value 1.5 is not between 0 and 1). FILTER can be added as with the other aggregates.

Fractions can be given as an array: percentile_cont and percentile_disc then return an array of as many quantiles (double precision[] for percentile_cont, array of the value type for percentile_disc); a NULL fraction gives a NULL element, a NULL array returns NULL.

SELECT percentile_cont(ARRAY[0.25, 0.5]) WITHIN GROUP (ORDER BY v),
       percentile_disc(ARRAY[0.5, 1]) WITHIN GROUP (ORDER BY v)
FROM (VALUES (10), (20), (30), (40)) AS w(v);
-- {17.5,25} | {20,40}
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY quantite) AS mediane, percentile_disc(0.5) WITHIN GROUP (ORDER BY quantite) AS mediane_disc,
       percentile_cont(0.9) WITHIN GROUP (ORDER BY quantite) AS p90, mode() WITHIN GROUP (ORDER BY article_id) AS article_frequent
FROM ligne_commande;
SELECT rank(10) WITHIN GROUP (ORDER BY quantite), dense_rank(10) WITHIN GROUP (ORDER BY quantite),
       percent_rank(10) WITHIN GROUP (ORDER BY quantite), cume_dist(10) WITHIN GROUP (ORDER BY quantite)
FROM ligne_commande;
 mediane | mediane_disc | p90 | article_frequent
---------+--------------+-----+------------------
 7.5     | 5            | 75  | 1

4 | 4 | 0.5 | 0.7142857142857143

Multiple groupings#

GROUP BY ROLLUP (...), GROUP BY CUBE (...), GROUP BY GROUPING SETS (...) and the grouping(column) function (1 on a total row) are described in chapter 36. SELECT queries.

SELECT grouping(ville) AS total, ville, count(*) FROM client GROUP BY ROLLUP (ville);
 total | ville | count(*)
-------+-------+----------
 0     | NULL  | 1
 0     | Lyon  | 2
 0     | Paris | 1
 1     | NULL  | 4

37.9 Window functions#

A window function computes, for each row, a value over a set of neighboring rows without grouping them. The OVER clause, frames and the WINDOW clause are described in chapter 36. SELECT queries.

function(...) OVER ([PARTITION BY ...] [ORDER BY ...] [frame])
function(...) OVER window_name
FunctionPurposeType
row_number()number of the row in its partitionbigint
rank()rank, with gaps after tiesbigint
dense_rank()rank without gapsbigint
percent_rank()relative rank, from 0 to 1double precision
cume_dist()share of rows with a lower or equal rankdouble precision
ntile(n)group number, the partition being split into n groupsinteger
lag(expr [, offset [, default]])value of a preceding rowthat of expr
lead(expr [, offset [, default]])value of a following rowthat of expr
first_value(expr), last_value(expr)first, last value of the framethat of expr
nth_value(expr, n)nth value of the framethat of expr
count, sum, avg, min, max, stddev, variance..., bool_and, bool_or, everyaggregate over the framethat of the aggregate

Ranks#

SELECT id, designation, prix,
       row_number() OVER (ORDER BY prix DESC) AS n,
       rank() OVER (ORDER BY prix DESC) AS rang,
       ntile(2) OVER (ORDER BY prix DESC) AS moitie
FROM article ORDER BY id;
 id | designation      | prix  | n | rang | moitie
----+------------------+-------+---+------+--------
 1  | Stylo bille bleu | 1.20  | 4 | 4    | 2
 2  | Cahier 96 pages  | 2.50  | 3 | 3    | 2
 3  | Classeur A4      | 4.90  | 2 | 2    | 1
 4  | Agrafeuse        | 12.00 | 1 | 1    | 1
SELECT id, percent_rank() OVER w, cume_dist() OVER w, dense_rank() OVER w
FROM article WINDOW w AS (ORDER BY prix) ORDER BY id;
1 | 0                  | 0.25 | 1
2 | 0.3333333333333333 | 0.5  | 2
3 | 0.6666666666666666 | 0.75 | 3
4 | 1                  | 1    | 4

A window function cannot be cited in WHERE: to keep the two most expensive items, use a derived table, or the QUALIFY clause (a MIRAJ extension):

SELECT * FROM (SELECT id, prix, rank() OVER (ORDER BY prix DESC) AS r FROM article) x WHERE r <= 2 ORDER BY r;
SELECT id, prix FROM article QUALIFY row_number() OVER (ORDER BY prix DESC) <= 2 ORDER BY id;
4 | 12.00 | 1
3 | 4.90  | 2

3 | 4.90
4 | 12.00

lag and lead#

SELECT id, prix,
       lag(prix) OVER (ORDER BY id) AS precedent,
       lead(prix) OVER (ORDER BY id) AS suivant,
       lag(prix, 2, 0) OVER (ORDER BY id) AS avant_dernier
FROM article ORDER BY id;
 id | prix  | precedent | suivant | avant_dernier
----+-------+-----------+---------+---------------
 1  | 1.20  | NULL      | 2.50    | 0.00
 2  | 2.50  | 1.20      | 4.90    | 0.00
 3  | 4.90  | 2.50      | 12.00   | 1.20
 4  | 12.00 | 4.90      | NULL    | 2.50

Number of days between an order and the previous one:

SELECT id, date_commande, date_commande - lag(date_commande) OVER (ORDER BY date_commande) AS jours
FROM commande ORDER BY id;
 id | date_commande | jours
----+---------------+-------
 1  | 2026-09-28    | NULL
 2  | 2026-10-05    | 7
 3  | 2026-10-06    | 1
 4  | 2026-10-09    | 3

first_value, last_value, nth_value#

These functions read the row's frame. With ORDER BY and no explicit frame, the frame stops at the current row: last_value then returns the value of the row itself. For the last value of the partition, write the whole frame.

SELECT id, prix,
       first_value(prix) OVER (ORDER BY id) AS premier,
       last_value(prix) OVER (ORDER BY id) AS dernier_du_cadre,
       last_value(prix) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS dernier,
       nth_value(prix, 2) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS deuxieme
FROM article ORDER BY id;
 id | prix  | premier | dernier_du_cadre | dernier | deuxieme
----+-------+---------+------------------+---------+----------
 1  | 1.20  | 1.20    | 1.20             | 12.00   | 2.50
 2  | 2.50  | 1.20    | 2.50             | 12.00   | 2.50
 3  | 4.90  | 1.20    | 4.90             | 12.00   | 2.50
 4  | 12.00 | 1.20    | 12.00            | 12.00   | 2.50

Window aggregates#

SELECT commande_id, article_id, quantite,
       sum(quantite) OVER (PARTITION BY commande_id) AS total_commande,
       count(*) OVER (PARTITION BY commande_id) AS lignes
FROM ligne_commande ORDER BY 1, 2;
 commande_id | article_id | quantite | total_commande | lignes
-------------+------------+----------+----------------+--------
 1           | 1          | 100      | 110            | 2
 1           | 2          | 10       | 110            | 2
 2           | 3          | 4        | 4              | 1
 3           | 1          | 50       | 52             | 2
 3           | 4          | 2        | 52             | 2
 4           | 2          | 5        | 5              | 1

Running total, sliding average over two rows and share of the total:

SELECT id, prix,
       sum(prix) OVER (ORDER BY id) AS cumul,
       avg(prix) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS moyenne_glissante,
       round(100 * prix / sum(prix) OVER (), 1) AS part
FROM article ORDER BY id;
 id | prix  | cumul | moyenne_glissante  | part
----+-------+-------+--------------------+------
 1  | 1.20  | 1.20  | 1.2000000000000000 | 5.8
 2  | 2.50  | 3.70  | 1.8500000000000000 | 12.1
 3  | 4.90  | 8.60  | 3.7000000000000000 | 23.8
 4  | 12.00 | 20.60 | 8.4500000000000000 | 58.3

FILTER is also used with OVER (count(*) FILTER (WHERE stock > 0) OVER (ORDER BY id)), as are ordered-set aggregates:

SELECT id, prix, percentile_cont(0.5) WITHIN GROUP (ORDER BY prix) OVER () AS mediane FROM article ORDER BY id;
 id | prix  | mediane
----+-------+---------
 1  | 1.20  | 3.7
 2  | 2.50  | 3.7
 3  | 4.90  | 3.7
 4  | 12.00 | 3.7

Supported frames: ROWS with UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW, n FOLLOWING, UNBOUNDED FOLLOWING; RANGE without offset (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). RANGE with an offset, GROUPS, EXCLUDE, as well as string_agg and json_agg with OVER, are not supported (see 37.16).

SELECT DISTINCT ON (keys) ... keeps the first row of each group of keys in the ORDER BY order (see 36. SELECT queries): the last order of each customer.

SELECT DISTINCT ON (client_id) client_id, id, date_commande FROM commande ORDER BY client_id, date_commande DESC;
 client_id | id | date_commande
-----------+----+---------------
 1         | 2  | 2026-10-05
 2         | 3  | 2026-10-06
 3         | 4  | 2026-10-09

37.10 JSON and JSONB#

The json and jsonb types are described in chapter 33. Data types. A jsonb column has the storage and functions of a json column, keeps its declared type and stores its documents normalized (keys sorted by length then in binary order, duplicates removed). The examples use the column article.attributs (jsonb).

Operators#

OperatorPurposeExampleResult
doc -> 'key'member of an object, as JSON'{"a": 1}'::jsonb -> 'a'1
doc -> nelement of an array (0 for the first, negative from the end), as JSON'[1, 2, 3]'::jsonb -> -13
doc ->> 'key', doc ->> nlikewise, as text'{"s": "txt"}'::jsonb ->> 's'txt
doc #> '{a,0}'element at the path written as an array, as JSON'{"b": {"c": [10, 20, 30]}}'::jsonb #> '{b,c,2}'30
doc #>> '{a,0}'likewise, as text'{"b": {"c": [10, 20, 30]}}'::jsonb #>> '{b,c,0}'10
a @> ba contains b'{"a": 1, "b": 2}'::jsonb @> '{"a": 1}'t
a <@ ba is contained in b'[1, 2]'::jsonb <@ '[1, 2, 3]'t
doc ? 'key'the object has this member, or the array contains this string'{"a": 1}'::jsonb ? 'a't
doc ?| ARRAY[...]at least one of the keys exists'{"a": 1}'::jsonb ?| ARRAY['x', 'a']t
doc ?& ARRAY[...]all the keys exist'{"a": 1}'::jsonb ?& ARRAY['a', 'b']f
a || bmerge of two objects, concatenation of two arrays'{"a": 1}'::jsonb || '{"b": 2}'::jsonb{"a": 1, "b": 2}
doc - 'key'object without this member'{"a": 1, "b": 2}'::jsonb - 'a'{"b": 2}
doc - narray without this element'[1, 2, 3]'::jsonb - 0[2, 3]
doc #- '{a,b}'document without the element at the path'{"a": {"b": 1, "c": 2}}'::jsonb #- '{a,b}'{"a": {"c": 2}}

Operators can be chained: doc -> 'b' -> 'c' -> 1, doc -> 'a' ->> 'b'.

SELECT reference,
       attributs -> 'couleur' AS json,
       attributs ->> 'couleur' AS texte,
       attributs -> 'tags' ->> 0 AS premier_tag,
       attributs #>> '{dim,l}' AS longueur
FROM article ORDER BY id;
 reference | json    | texte | premier_tag | longueur
-----------+---------+-------+-------------+----------
 STY-001   | "bleu"  | bleu  | bureau      | 14
 CAH-096   | "rouge" | rouge | papier      | NULL
 CLA-A4    | "noir"  | noir  | NULL        | NULL
 AGR-024   | NULL    | NULL  | NULL        | NULL
  • A missing member or element returns NULL. -> on a null member returns the JSON null; ->> returns NULL.
  • -> and #> return jsonb on a jsonb column or value, json on json; ->> and #>> return text.
  • ->> always returns a text: convert to compare numbers, (attributs ->> 'pages')::int > 50.

Searching in a JSON column:

SELECT reference FROM article WHERE attributs ->> 'couleur' = 'bleu';              -- STY-001
SELECT reference FROM article WHERE attributs @> '{"couleur": "noir"}';            -- CLA-A4
SELECT reference FROM article WHERE attributs -> 'tags' @> '["papier"]';           -- CAH-096
SELECT reference FROM article WHERE (attributs ->> 'pages')::int > 50;             -- CAH-096
SELECT reference FROM article WHERE attributs -> 'pages' IS NOT NULL;              -- CAH-096
SELECT attributs ->> 'couleur' AS couleur, count(*) FROM article GROUP BY attributs ->> 'couleur' ORDER BY 1;
 couleur | count(*)
---------+----------
 bleu    | 1
 noir    | 1
 rouge   | 1
 NULL    | 1

Key existence. ?, ?| and ?& test the first-level keys of an object, or the strings of an array; a NULL document returns NULL.

SELECT reference, attributs ? 'pages' AS a_pages, attributs ?| ARRAY['pages', 'recycle'] AS l_un,
       attributs ?& ARRAY['couleur', 'tags'] AS les_deux, attributs -> 'tags' ? 'papier' AS tag_papier
FROM article ORDER BY id;
 reference | a_pages | l_un | les_deux | tag_papier
-----------+---------+------+----------+------------
 STY-001   | f       | f    | t        | f
 CAH-096   | t       | t    | t        | t
 CLA-A4    | f       | t    | t        | f
 AGR-024   | NULL    | NULL | NULL     | NULL

The same tests are written as functions: jsonb_exists(doc, key), jsonb_exists_any(doc, array), jsonb_exists_all(doc, array). The other operators also have a function form: jsonb_concat(a, b) (||), jsonb_delete(doc, key) (-), jsonb_delete_path(doc, path) (#-), jsonb_contains(a, b) (@>), jsonb_contained(a, b) (<@).

|| merges two documents. Next to a jsonb, an object or array written as an untyped string is read as jsonb (attributs || '{"promo": true}'); between two texts, or between a json and an untyped string, || remains text concatenation.

UPDATE article SET attributs = attributs || '{"promo": true}'::jsonb WHERE id = 3 RETURNING attributs;
UPDATE article SET attributs = attributs - 'promo' WHERE id = 3 RETURNING attributs;
{"tags": [], "promo": true, "couleur": "noir", "recycle": true}
{"tags": [], "couleur": "noir", "recycle": true}

Removing an index from an object ('{"a": 1}'::jsonb - 0): 22023 (cannot delete from object using integer index).

Construction#

FunctionReturns
jsonb_build_object(key1, value1, ...), json_build_object(...)an object; odd number of arguments: 42883
jsonb_build_array(value1, ...), json_build_array(...)an array
to_jsonb(value), to_json(value)the value as JSON: number, quoted string, true / false, null
json_object(text_array), json_object(keys, values)an object with text values, from {key1, value1, ...} or two arrays; odd number of elements: 22023 (array must have even number of elements)
SELECT jsonb_build_object('a', 1, 'b', 'x', 'c', true, 'd', NULL, 'e', 1.5), jsonb_build_array(1, 'x', true, NULL),
       jsonb_build_object('n', jsonb_build_object('x', 1), 't', jsonb_build_array(1, 2));
SELECT to_jsonb('txt'::text), to_json(42), to_jsonb(true), to_jsonb(date '2026-10-11'), to_jsonb(NULL::text);
SELECT jsonb_build_object('id', id, 'nom', nom, 'ville', ville) FROM client WHERE id IN (1, 4) ORDER BY id;
SELECT json_object('{a, 1, b, 2}'), json_object('{a, b}', '{x, y}');
{"a": 1, "b": "x", "c": true, "d": null, "e": 1.5} | [1, "x", true, null] | {"n": {"x": 1}, "t": [1, 2]}
"txt" | 42 | true | "2026-10-11" | null
{"id": 1, "nom": "Dupont", "ville": "Lyon"}
{"id": 4, "nom": "Élodie Petit", "ville": null}
{"a": "1", "b": "2"} | {"a": "x", "b": "y"}

to_jsonb does not apply to an entire row (to_jsonb(c): 42703): build the object with jsonb_build_object.

Modification#

jsonb_set(document, path, value [, create])

Replaces the element at path (written as an array, '{a,0}') with value, a JSON document. If the last element of the path does not exist, it is added, unless create is false; a path with a missing intermediate element leaves the document unchanged.

SELECT jsonb_set('{"a": 1}'::jsonb, '{a}', '5'), jsonb_set('{"a": {"b": 1}}'::jsonb, '{a,b}', '"x"'),
       jsonb_set('{"a": 1}'::jsonb, '{b}', '2'), jsonb_set('{"a": 1}'::jsonb, '{b}', '2', false),
       jsonb_set('{"a": [1, 2]}'::jsonb, '{a,0}', '9');
SELECT jsonb_set(attributs, '{stock}', to_jsonb(stock)) FROM article WHERE id = 1;
{"a": 5} | {"a": {"b": "x"}} | {"a": 1, "b": 2} | {"a": 1} | {"a": [9, 2]}
{"dim": {"d": 0.8, "l": 14}, "tags": ["bureau", "ecriture"], "stock": 500, "couleur": "bleu"}

In an UPDATE: UPDATE article SET attributs = jsonb_set(attributs, '{couleur}', '"vert"') WHERE id = 3.

jsonb_insert(document, path, value [, after])
jsonb_strip_nulls(document)    json_strip_nulls(document)

jsonb_insert inserts value at path: in an array, before the element of that index (after it with after set to true); in an object, as a new member; an existing member is rejected (22023: cannot replace existing key). jsonb_strip_nulls and json_strip_nulls remove, at all levels, object members whose value is null (the nulls of an array stay).

SELECT jsonb_insert('{"a": [1, 2]}', '{a, 1}', '9'), jsonb_insert('{"a": [1, 2]}', '{a, 1}', '9', true), jsonb_insert('{"a": 1}', '{b}', '2');
SELECT jsonb_strip_nulls('{"a": 1, "b": null, "c": {"d": null}}'), json_strip_nulls('{"a": null, "b": 2}');
{"a": [1, 9, 2]} | {"a": [1, 2, 9]} | {"a": 1, "b": 2}
{"a": 1, "c": {}} | {"b":2}

Inspection#

FunctionReturns
jsonb_typeof(doc), json_typeof(doc)object, array, string, number, boolean, null
jsonb_array_length(doc), json_array_length(doc)number of elements of an array; any other document: 22023 (cannot get array length of a non-array)
jsonb_extract_path(doc, key1, key2, ...), jsonb_extract_path_text(...)element at the path given key by key (an index for an array), as JSON or text: like #> and #>>
jsonb_pretty(doc)the document laid out on several lines, indented by four spaces
SELECT jsonb_typeof('{"a": 1}'::jsonb), jsonb_typeof('[1]'::jsonb), jsonb_typeof('1'::jsonb),
       jsonb_typeof('"a"'::jsonb), jsonb_typeof('true'::jsonb), jsonb_typeof('null'::jsonb);
SELECT reference, jsonb_array_length(attributs -> 'tags') AS tags, jsonb_typeof(attributs -> 'dim') AS dim
FROM article ORDER BY id;
object | array | number | string | boolean | null

 reference | tags | dim
-----------+------+--------
 STY-001   | 2    | object
 CAH-096   | 1    | NULL
 CLA-A4    | 0    | NULL
 AGR-024   | NULL | NULL
SELECT jsonb_extract_path(attributs, 'dim', 'l'), jsonb_extract_path_text(attributs, 'tags', '1') FROM article WHERE id = 1;
SELECT jsonb_pretty(attributs) FROM article WHERE id = 2;
14 | ecriture

{
    "tags": [
        "papier"
    ],
    "pages": 96,
    "couleur": "rouge"
}

The aggregates json_agg, jsonb_agg, json_object_agg and jsonb_object_agg are described in 37.8.

SQL/JSON paths#

jsonb_path_exists(doc, path [, variables [, silent]])
jsonb_path_match(doc, path [, variables [, silent]])
jsonb_path_query_array(doc, path [, variables [, silent]])
jsonb_path_query_first(doc, path [, variables [, silent]])

Evaluate an SQL/JSON path ($.a.b, $.tags[*], $.a[last], $.dim.*, filters ? (@ > 2) with ==, !=, <, >, &&, ||, !), in lax mode (default: missing member ignored) or strict (written at the start of the path: missing member is an error 2203A, JSON object does not contain key "b"). variables is a JSON object whose members are cited as $name in the path; silent set to true turns path errors into NULL.

  • jsonb_path_exists: does the path find at least one element?
  • jsonb_path_match: result of a path that is a predicate ($.pages > 50); a path that does not return a single boolean is rejected (22038: single boolean result is expected);
  • jsonb_path_query_array: all the elements found, as a JSON array;
  • jsonb_path_query_first: the first element found, NULL if there is none.
SELECT reference, jsonb_path_exists(attributs, '$.tags[*] ? (@ == "papier")') AS papier, jsonb_path_match(attributs, '$.pages > 50') AS gros,
       jsonb_path_query_array(attributs, '$.dim.*') AS dims, jsonb_path_query_first(attributs, '$.tags[last]') AS dernier_tag
FROM article ORDER BY id;
SELECT jsonb_path_query_array('{"a": [1, 2, 3, 4]}', '$.a[*] ? (@ > $min && @ < $max)', '{"min": 1, "max": 4}');
SELECT jsonb_path_exists('{"a": 1}', 'strict $.b', '{}', true), jsonb_path_exists('{"a": 1}', 'lax $.b');
 reference | papier | gros | dims      | dernier_tag
-----------+--------+------+-----------+-------------
 STY-001   | f      | f    | [0.8, 14] | "ecriture"
 CAH-096   | t      | t    | []        | "papier"
 CLA-A4    | f      | f    | []        | NULL
 AGR-024   | NULL   | NULL | NULL      | NULL

[2, 3]
NULL | f

JSON set-returning functions#

FunctionReturns one row perColumns
jsonb_each(doc), json_each(doc)object memberkey (text), value (JSON)
jsonb_each_text(doc), json_each_text(doc)object memberkey, value (text)
jsonb_array_elements(doc), json_array_elements(doc)array elementvalue (JSON)
jsonb_array_elements_text(doc), json_array_elements_text(doc)array elementvalue (text)
jsonb_object_keys(doc), json_object_keys(doc)object keyone text column

They are used in a FROM, lateral to the tables that precede them (FROM article a, jsonb_array_elements_text(a.attributs -> 'tags') AS t(tag)), with WITH ORDINALITY (one more column of numbers) and column aliases AS e(cle, valeur). Written in the SELECT list with a constant document, they return one row per value (see 37.14); with an argument that cites a column, place them in the FROM (otherwise 0A000).

SELECT * FROM jsonb_each_text('{"a": 1, "b": "x", "c": null}');
SELECT * FROM jsonb_array_elements('[1, "x", {"k": true}]') WITH ORDINALITY AS e(valeur, rang);
SELECT a.reference, t.tag FROM article a, jsonb_array_elements_text(a.attributs -> 'tags') AS t(tag) ORDER BY a.id, t.tag;
SELECT a.reference, k.cle FROM article a, jsonb_object_keys(a.attributs) AS k(cle) WHERE a.id = 2 ORDER BY 2;
a | 1
b | x
c | NULL

1 | 1
"x" | 2
{"k": true} | 3

 reference | tag
-----------+----------
 STY-001   | bureau
 STY-001   | ecriture
 CAH-096   | papier

 reference | cle
-----------+---------
 CAH-096   | couleur
 CAH-096   | pages
 CAH-096   | tags

MIRAJ language JSON functions#

The MIRAJ JSON functions remain available, with their paths written '$.a.b[0]':

FunctionUse
json_keys(doc)keys of an object, as a JSON array
json_remove(doc, '$.a.b')document without the element at the path
json_pretty(doc)layout
json_table(doc, path COLUMNS (...))typed rows and columns taken from a document, in the FROM
json_extract(doc, '$.a[0]'), json_value(doc, '$.a'), json_contains, json_searchreading and searching by path
json_valid(text), json_depth(doc), json_length(doc)validity, depth, size
SELECT json_keys(attributs), json_length(attributs), json_remove(attributs, '$.dim'), json_value(attributs, '$.couleur')
FROM article WHERE id = 1;
SELECT a.reference, t.tag
FROM article a, json_table(a.attributs, '$.tags[*]' COLUMNS (tag text PATH '$')) AS t ORDER BY a.id, t.tag;
["dim", "tags", "couleur"] | 3 | {"tags": ["bureau", "ecriture"], "couleur": "bleu"} | bleu

 reference | tag
-----------+----------
 STY-001   | bureau
 STY-001   | ecriture
 CAH-096   | papier

Their full description is in chapter 10. SQL functions (calls are written with strings between apostrophes).

Differences from PostgreSQL#

  • A jsonb document is normalized, as in PostgreSQL: in each object, a repeated key keeps only its last value and keys are sorted by length then in binary order; spacing is canonical and a number written with an exponent is expanded. '{"b": 1, "a": 2, "a": 3}'::jsonb is displayed as {"a": 3, "b": 1}, and '{"a": 1, "b": 2}'::jsonb = '{"b": 2, "a": 1}'::jsonb is true. A json keeps the text as written. Two jsonb values are compared by value, in the order specific to the type ('1.0'::jsonb = '1'::jsonb is true, so is '2'::jsonb < '10'::jsonb; see 33.8), including for ORDER BY, DISTINCT, GROUP BY, UNION and a join key. Leftovers: a document stored in a jsonb column by an earlier version, or written in the MIRAJ language, keeps its text until its next write in the PostgreSQL language (UPDATE t SET doc = doc::jsonb normalizes it); a primary key, a UNIQUE constraint and a search through an index on a jsonb operate on the text of the document.
  • An unreadable document is rejected (22P02), in a json or jsonb column as in the conversion of a text: '{"a": 1'::jsonb.
  • The results of ||, -, #-, jsonb_set, jsonb_build_object, jsonb_build_array, to_jsonb and of the jsonb_agg / jsonb_object_agg aggregates are jsonb (type announced to the client and pg_typeof), normalized; those of json_build_object, json_build_array, to_json, json_agg and json_object_agg are json, in the written order. ||, -, #- and jsonb_set exist only for a jsonb, as in PostgreSQL: on a json (column, ::json, result of a json_* function), they are rejected (42883: operator does not exist: json || json); convert with ::jsonb. CASE ... THEN doc END and coalesce keep the type of their values (jsonb, json, uuid, interval, enumeration); max(doc) and min(doc) of a jsonb, absent from PostgreSQL, follow the type's order and return a jsonb.

37.11 Conversions#

The types and their literals are described in chapter 33. Data types. This section summarizes the conversions useful in an expression.

expression::type
CAST(expression AS type)
type 'text'
SELECT '42'::int + 1, CAST('42' AS integer), 42::text || '!', '3.14'::numeric(5,1), '3.14'::float8,
       int4 '5', boolean 't', '  42  '::int;
43 | 42 | 42! | 3.1 | 3.14 | 5 | t | 42
ConversionRuleExampleResult
decimal number → integerrounded to the nearest3.7::int, 2.5::int, (-3.7)::int4, 3, -4
→ numeric(p,s)rounded to s decimals123.456::numeric(5,2)123.46
→ numeric without precisionvalue kept, without trailing zeros'1.50'::numeric1.5
→ textwritten form of the number, of the date1.50::text, date '2026-10-11'::text1.50, 2026-10-11
→ varchar(n)truncated to n characters42::varchar(1)4
timestamp → date, timedate part, time part'2026-10-11 14:30:45'::timestamp::date2026-10-11
date → timestampat midnightdate '2026-10-11'::timestamp2026-10-11 00:00:00
→ timestamp(n)n decimals of second'2026-10-11 14:30:45.123456'::timestamp(0)2026-10-11 14:30:45
text → timestamptzoffset (+02, Z) or time zone name read, instant displayed in the session time zone'2026-10-11T12:30:00Z'::timestamptz2026-10-11 14:30:00+02
text → byteabytes of the text, or hexadecimal \x...'\x414243'::bytea\x414243
bytea → textUTF-8 text of the bytes'\x414243'::bytea::textABC
hexadecimal, octal, binary text → integerprefixes 0x, 0o, 0b, separator _'0x1F'::int, '0b101'::int, '1_000'::int31, 5, 1000

An unreadable or out-of-range value is an error, not a zero, a numeric prefix or a NULL:

StatementResponse
SELECT 'abc'::int22P02: invalid input syntax for type integer: "abc"
SELECT '1.5'::int, SELECT ''::int22P02
SELECT 'x'::numeric22P02: invalid input syntax for type numeric: "x"
SELECT 99999::smallint22003: smallint out of range
SELECT 3000000000::int22003: integer out of range
SELECT 'maybe'::boolean22P02: invalid input syntax for type boolean: "maybe"
SELECT 2.5::boolean42846: cannot cast type numeric to boolean
SELECT '{"a": 1}'::json || '{"b": 2}'::json42883: operator does not exist: json || json
SELECT 'abc'::interval22P02: invalid input syntax for type interval: "abc"
SELECT 12345.678::numeric(5,2)22003: numeric field overflow: A field with precision 5, scale 2 must round to an absolute value less than 10^3.
SELECT '2026-02-30'::date22008: date/time field value out of range: "2026-02-30"
SELECT 'pas-un-uuid'::uuid22P02: invalid input syntax for type uuid: "pas-un-uuid"
SELECT '{"a": 1'::jsonb22P02: invalid input syntax for type jsonb: "{"a": 1"

pg_input_is_valid(text, type) tells whether a text is readable in a type without raising the error (see 37.13).

pg_typeof(expression) returns the type name of an expression:

SELECT pg_typeof(1), pg_typeof(3000000000), pg_typeof(1.5), pg_typeof(1.5::float8), pg_typeof('a'::text),
       pg_typeof(true), pg_typeof(now()), pg_typeof(current_date);
integer | bigint | numeric | double precision | text | boolean | timestamp with time zone | date

Conversions to regclass, regtype... and format_type are used to read the catalog: see 41. Catalog and tools.


37.12 Bytes, hashing and encoding#

FunctionPurposeType
md5(text_or_bytes)MD5 digest, 32 hexadecimal digitstext
sha224(bytes), sha256(bytes), sha384(bytes), sha512(bytes)SHA-2 digestsbytea
digest(data, algorithm)md5, sha1, sha224, sha256, sha384 or sha512 digestbytea
to_hex(integer)hexadecimal formtext
encode(bytes, format)bytes → text; formats hex, base64, escapetext
decode(text, format)text → bytesbytea
convert_to(text, encoding)text → bytes in the given encodingbytea
convert_from(bytes, encoding)bytes of an encoding → texttext
get_byte(bytes, n), set_byte(bytes, n, value)byte at index n (from 0), bytes with that byte replacedinteger, bytea
length(bytes), octet_length(bytes)number of bytesinteger
crypt(password, salt), gen_salt(type)password hash, fresh salttext
gen_random_bytes(n)n random bytes (1 to 1024)bytea
SELECT md5('abc'), to_hex(255), to_hex(-1);
SELECT encode('abc'::bytea, 'hex'), encode('abc'::bytea, 'base64'), decode('616263', 'hex'), decode('YWJj', 'base64'),
       length(decode('ff00', 'hex'));
SELECT encode(nom::bytea, 'base64'), decode('w4lsb2RpZSBQZXRpdA==', 'base64')::text FROM client WHERE id = 4;
900150983cd24fb0d6963f7d28e17f72 | ff | ffffffff
616263 | YWJj | \x616263 | \x616263 | 2
w4lsb2RpZSBQZXRpdA== | Élodie Petit

A bytea is returned to the client in hexadecimal (\x...). The escape format writes non-printable bytes as \ooo (octal) and reads them the same way (decode('a\000b', 'escape') is \x610062). Unknown format: 22023 (unrecognized encoding: "bin"); invalid hexadecimal text: 22023 (invalid hexadecimal digit).

Digests#

sha224 to sha512 return the digest as bytes: encode(..., 'hex') writes it in hexadecimal. digest (from the pgcrypto extension) accepts a text or bytes; an unknown algorithm is rejected (22023: Cannot use "sha3": No such hash algorithm). The MIRAJ functions sha1(text) and sha2(text, bits) directly return the hexadecimal.

SELECT sha256('abc'), encode(sha224('abc'), 'hex'), length(sha384('abc')), length(sha512('abc'));
SELECT encode(digest('abc', 'sha256'), 'hex'), digest('abc', 'md5') = decode(md5('abc'), 'hex'), sha2('a', 256);
\xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad | 23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7 | 48 | 64
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad | t | ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb

Encodings and bytes#

convert_to and convert_from know the encodings UTF8, LATIN1, WIN1252 and SQL_ASCII (any other name: 22023, invalid source encoding name "XYZ"). A character with no equivalent in the target encoding is rejected (22P05: character with byte sequence 0xe2 0x82 0xac in encoding "UTF8" has no equivalent in encoding "LATIN1"), and so is an invalid byte sequence (22021: invalid byte sequence for encoding "UTF8": 0xff). get_byte and set_byte outside the bytes: 2202E (index 5 out of valid range, 0..0).

SELECT convert_to('café', 'LATIN1'), convert_to('café', 'UTF8'), convert_from('\x636166e9'::bytea, 'LATIN1'), convert_from('\x80'::bytea, 'WIN1252');
SELECT get_byte('\x1234ff'::bytea, 2), set_byte('\x1234ff'::bytea, 0, 0);
\x636166e9 | \x636166c3a9 | café | €
255 | \x0034ff

Passwords and random bytes#

crypt(password, salt) hashes a password according to the salt: a fresh salt from gen_salt('md5') ($1$...) or gen_salt('des') (2 characters), or an existing hash, which makes it possible to verify a password: crypt(entered, hash) = hash. gen_salt('bf') and gen_salt('xdes') are rejected (0A000), and so is any other type (22023: gen_salt: Unknown salt algorithm). gen_random_bytes(n) outside 1 to 1024: 22023 (Length not in range).

SELECT crypt('secret', '$1$abcdefgh'), crypt('secret', '$1$abcdefgh$cHJi5PXp/ki/ktXzqlk6I1') = '$1$abcdefgh$cHJi5PXp/ki/ktXzqlk6I1',
       length(gen_salt('des')), gen_salt('md5') LIKE '$1$%', length(gen_random_bytes(16));
$1$abcdefgh$cHJi5PXp/ki/ktXzqlk6I1 | t | 2 | t | 16

37.13 System, session, sequences and series#

Session and server#

FunctionReturns
version()PostgreSQL 16.4 (MIRAJ 1.0.0) on aarch64-macos, 64-bit (the server platform)
current_database(), current_catalogmiraj: the only PostgreSQL database of the instance
current_schema, current_schema()the current MIRAJ database (ventes)
current_schemas(true){pg_catalog,ventes}
current_user, session_user, user, current_rolethe connected account, without its host (root)
pg_backend_pid()connection number
pg_cancel_backend(pid), pg_terminate_backend(pid [, timeout])interrupts the running query of session pid, closes that session: t; f with a warning if pid is unknown; 42501 without the right on it (see 38.10)
txid_current(), pg_current_xact_id()transaction number: positive, the same until it ends (specific to MIRAJ)
txid_current_if_assigned(), pg_current_xact_id_if_assigned()the same number inside a BEGIN ... COMMIT block, NULL outside a block
txid_current_snapshot(), pg_current_snapshot()snapshot xmin:xmax: (both bounds equal the transaction number, the list of running transactions is empty)
pg_is_in_recovery()f
pg_postmaster_start_time(), pg_conf_load_time()server start time (timestamptz)
inet_client_addr(), inet_server_addr()client address; server address for a loopback connection, NULL otherwise
pg_my_temp_schema()0
current_setting(name [, missing_ok])value of a parameter; unknown parameter: 42704, or NULL with missing_ok set to true
set_config(name, value, local)sets the parameter and returns value: like SET if local is false, like SET LOCAL (until the end of the transaction) if true; applied at the end of the statement (see 38.7)
pg_reload_conf()t: parameters apply as soon as they are written, there is nothing to reload; 42501 without the SUPER privilege
pg_notify(channel, payload)same effect as NOTIFY channel, 'payload'; returns an empty value
pg_typeof(expr)type name of the expression
pg_column_size(value)bytes occupied by the stored value (length header included for a text)
pg_input_is_valid(text, type)is the text a valid value of the type, without raising the error? Integer types, numeric, floats, boolean, uuid, json, jsonb, bytea, interval, texts; unknown type: 42704; date, time and others: 0A000
pg_sleep(seconds)waits; returns 0; if canceled, fails with 57014
SELECT current_database(), current_schema, current_user, current_setting('server_version'), current_setting('TimeZone'),
       current_setting('inconnu.param', true);
miraj | ventes | root | 16.4 | Europe/Paris | NULL
SELECT pg_input_is_valid('42', 'integer'), pg_input_is_valid('4x', 'integer'), pg_input_is_valid('{"a":', 'jsonb'),
       pg_input_is_valid('1 day', 'interval'), pg_input_is_valid('maybe', 'boolean');
SELECT pg_column_size('abc'::text), pg_column_size(true), inet_client_addr(), pg_postmaster_start_time() <= now();
t | f | f | t | f
4 | 1 | 127.0.0.1 | t

The functions that describe objects (pg_get_indexdef, pg_get_constraintdef, obj_description, has_table_privilege, pg_table_is_visible, to_regclass...) and sizes (pg_relation_size, pg_total_relation_size, pg_database_size, pg_size_pretty) are described in chapter 41. Catalog and tools.

Identifiers#

gen_random_uuid() (or uuid_generate_v4(), from the uuid-ossp extension) returns a random UUID (version 4), of type uuid, written in 36 characters:

SELECT gen_random_uuid();   -- fe6bc144-804b-49de-9ad2-555c3f027f68

Sequences#

FunctionPurpose
nextval('sequence')advances the sequence and returns the new value (bigint)
currval('sequence')last value returned by nextval in the session; 55000 if the session has not yet called it on this sequence
setval('sequence', n [, is_called])sets the value: the next nextval returns the following value, or n itself if is_called is false

Creating a sequence is described in chapter 34. DDL language.

CREATE SEQUENCE seq_facture START 100 INCREMENT 10;
SELECT nextval('seq_facture'), nextval('seq_facture');      -- 100 | 110
SELECT currval('seq_facture');                              -- 110
SELECT setval('seq_facture', 500), nextval('seq_facture');  -- 500 | 510
SELECT setval('seq_facture', 1000, false), nextval('seq_facture');   -- 1000 | 1000
DROP SEQUENCE seq_facture;

Unknown sequence: 42P01. A serial column has no named sequence (pg_get_serial_sequence('article', 'id') returns NULL, and currval('article_id_seq') fails): the assigned value is read with INSERT ... RETURNING id (see 35. DML language). lastval() returns the last value served to the session, all sequences combined, by nextval or by a serial / IDENTITY column; as long as the session has received none, it returns 55000 (lastval is not yet defined in this session).

generate_series#

generate_series(start, stop [, step])

Set-returning function: returns the values from start to stop, in steps of step (1 by default, negative for a countdown), one per row; no rows if stop is already passed. The bounds are integers, numeric or dates and instants (date, timestamp, timestamptz), the step then being an interval (for a timestamptz, a step in days keeps the session's civil time). It is used in a FROM (with WITH ORDINALITY and a column alias AS s(n)) or in the SELECT list (see 37.14).

SELECT n, n * n AS carre FROM generate_series(1, 3) AS s(n);
SELECT * FROM generate_series(10, 1, -3) WITH ORDINALITY AS s(n, rang);
SELECT * FROM generate_series(0.5, 1.5, 0.5);
SELECT * FROM generate_series(timestamp '2026-10-11 08:00', timestamp '2026-10-11 10:00', interval '1 hour');
SELECT generate_series(1, 3) AS n, 'x' AS lettre;
1 | 1
2 | 4
3 | 9

10 | 1
7 | 2
4 | 3
1 | 4

0.5
1.0
1.5

2026-10-11 08:00:00
2026-10-11 09:00:00
2026-10-11 10:00:00

1 | x
2 | x
3 | x

A series of days serves as a calendar: number of orders per day, days without orders included.

SELECT j.jour::date AS jour, count(o.id) AS commandes
FROM generate_series(date '2026-10-04', date '2026-10-07', interval '1 day') AS j(jour)
LEFT JOIN commande o ON o.date_commande = j.jour
GROUP BY j.jour ORDER BY 1;
 jour       | commandes
------------+-----------
 2026-10-04 | 0
 2026-10-05 | 1
 2026-10-06 | 1
 2026-10-07 | 0

A zero step: 22023 (step size cannot equal zero).

Advisory locks#

FunctionPurpose
pg_advisory_lock(key)takes the lock, waiting if it is held by another session
pg_try_advisory_lock(key)takes the lock without waiting; t if taken, f otherwise
pg_advisory_unlock(key)releases the lock; f, with a warning, if the session did not hold it
pg_advisory_xact_lock(key)takes the lock until the end of the transaction, waiting if it is held
pg_try_advisory_xact_lock(key)likewise without waiting; t if taken, f otherwise

The key is a bigint or two integer values (pg_try_advisory_lock(1, 2)). Session locks are cumulative (taken twice, they are released twice) and all released at the end of the session; transaction locks are released at COMMIT or ROLLBACK, without pg_advisory_unlock.

SELECT pg_try_advisory_lock(42), pg_try_advisory_lock(42), pg_advisory_unlock(42), pg_advisory_unlock(42), pg_advisory_unlock(42);
BEGIN;
SELECT pg_try_advisory_xact_lock(8);
COMMIT;
t | t | t | t | f
t

37.14 Arrays and set-returning functions#

Arrays#

An array is a list of values of the same type, written ARRAY[v1, v2, ...] or as text '{v1,v2,...}'::type[]. It is returned to the client in PostgreSQL notation ({1,2,3}; empty elements, or containing a comma, a brace, a quote or a blank, between quotes; NULL written as is) and announced under the array type (integer[], text[]...). The element type is the common type of the written values (ARRAY[1, 2.5] is a numeric[]); incompatible values are rejected (ARRAY[1, 'a']: 22P02). Arrays are expression values: a table column cannot be of an array type (0A000).

SELECT ARRAY[1, 2, 3], ARRAY['a', 'b c', NULL], ARRAY[1, 2.5], '{1,2,3}'::int[], ARRAY[]::text[], pg_typeof(ARRAY[1, 2]);
SELECT ARRAY[true, false], ARRAY['', 'a,b', 'q"x'], ARRAY[date '2026-10-11'];
{1,2,3} | {a,"b c",NULL} | {1,2.5} | {1,2,3} | {} | integer[]
{t,f} | {"","a,b","q\"x"} | {2026-10-11}
ExpressionReturnsExampleResult
array[n]element at index n (from 1); NULL outside the array(ARRAY[10, 20, 30])[2]20
array_length(array, 1), cardinality(array)number of elements (array_length: NULL for another dimension or an empty array)cardinality(ARRAY[1, 2])2
array_lower(array, 1), array_upper(array, 1)first and last indexarray_upper(ARRAY[1, 2, 3], 1)3
array_position(array, value)index of the first occurrence; NULL if absentarray_position(ARRAY['a', 'b'], 'b')2
array_to_string(array, separator [, null_text])elements joined by separator; NULLs are skipped, or replaced by null_textarray_to_string(ARRAY[1, NULL, 3], ',', '*')1,*,3
=, <>, <, >...element-by-element comparisonARRAY[1, 2] < ARRAY[1, 3]t
value = ANY (array), value > ALL (array)comparison with each element2 = ANY (ARRAY[1, 2])t
SELECT (ARRAY[10, 20, 30])[2], ('{a,b,c}'::text[])[3], (ARRAY[1])[5], (current_schemas(true))[1];
SELECT array_length(ARRAY[1, 2, 3], 1), array_length(ARRAY[1, 2], 2), cardinality(ARRAY[1, 2]), array_upper(ARRAY[1, 2, 3], 1),
       array_position(ARRAY['a', 'b'], 'b'), array_position(ARRAY['a'], 'z');
SELECT array_to_string(ARRAY[1, NULL, 3], ','), array_to_string(ARRAY[1, NULL, 3], ',', '*'), ARRAY[1, 2] = '{1,2}'::int[];
SELECT nom FROM client WHERE ville = ANY (ARRAY['Paris', 'Marseille']);
20 | c | NULL | pg_catalog
3 | NULL | 2 | 3 | 2 | NULL
1,3 | 1,*,3 | t
Martin

Functions that return an array: array_agg, regexp_match, regexp_split_to_array, parse_ident, current_schemas. The array columns of the catalog (pg_proc.proargnames, pg_constraint.conkey...) are read with the same functions.

unnest#

unnest(array)

Set-returning function: one row per element of the array, in order. In a FROM, WITH ORDINALITY adds the index; the array is written out (ARRAY[...], '{...}'::type[]) or passed as a parameter (unnest($1::int[])).

SELECT * FROM unnest(ARRAY['rouge', 'vert']) WITH ORDINALITY AS c(couleur, rang);
SELECT max(x), count(*) FROM unnest(ARRAY[3, 1, 2]) AS x;
rouge | 1
vert | 2

3 | 3

Set-returning functions in the SELECT list#

generate_series, unnest, regexp_matches, regexp_split_to_table, json[b]_array_elements[_text] and json[b]_object_keys can also be written in the SELECT list: the query then returns one row per value, each row of the source being repeated. Several functions in the same list advance together: the longest sets the number of rows, the others are padded with NULL.

SELECT unnest(ARRAY[1, 2]) AS n, unnest(ARRAY['a', 'b', 'c']) AS l;
SELECT unnest(ARRAY[id, id * 10]) AS v FROM client WHERE id <= 2;
1 | a
2 | b
NULL | c

1
10
2
20

For the JSON and pattern functions, an argument that cites a column requires the FROM form (0A000 in the SELECT list: jsonb_array_elements_text citing a column in the select list is not supported yet in the PostgreSQL language).


37.15 MIRAJ language functions#

An instance in the PostgreSQL language keeps the MIRAJ language functions, described in chapter 10. SQL functions. They are called with PostgreSQL lexical conventions (strings between apostrophes, identifiers between double quotes). When a PostgreSQL function has the same name, it is the one that is called: length counts characters, format reads a %s template, log(x) is decimal, trunc, to_char, version()... follow this chapter.

A few are useful where a PostgreSQL function is missing:

MIRAJ functionUseExampleResult
last_day(date)last day of the monthlast_day(date '2026-02-10')2026-02-28
datediff(a, b), timestampdiff(unit, a, b)difference in days, in a unittimestampdiff(day, date '2026-10-01', date '2026-10-11')10
dayname(d), monthname(d)name of the day, of the monthdayname(date '2026-10-11')Sunday
sha1(t), sha2(t, bits), crc32(t)digestssha1('a')86f7e437faa5a7fce15d1ddcb9eaeaea377667b8
substring_index(t, sep, n)start of a text up to the nth separatorsubstring_index('a.b.c', '.', 2)a.b
find_in_set(t, list)index in a comma-separated listfind_in_set('b', 'a,b')2
group_concat(expr ORDER BY ... SEPARATOR '...')concatenation of a groupsee string_agg
json_keys, json_remove, json_pretty, json_table...JSONsee 37.10
inet_aton(t), inet_ntoa(n)IPv4 addressesinet_aton('10.0.0.1')167772161

The vector functions (vec_distance_euclidean, l2_distance, cosine_distance), with the pgvector operators <-> (Euclidean distance), <=> (cosine distance), <#> (negative inner product) and <+> (Manhattan distance), the spatial functions (st_distance, st_astext...) and full-text search (MATCH (...) AGAINST (...)) are also available: see chapters 10, 11 and 30.

SELECT '[1,2]'::vector(2) <-> '[1,3]'::vector(2), '[1,2]'::vector(2) <=> '[1,3]'::vector(2), '[1,2]'::vector(2) <#> '[1,3]'::vector(2),
       '[1,2]'::vector(2) <+> '[1,3]'::vector(2), l2_distance('[1,2]'::vector(2), '[1,3]'::vector(2)), cosine_distance('[1,2]'::vector(2), '[1,3]'::vector(2));
1 | 0.010050506338833531 | -7 | 1 | 1 | 0.010050506338833531

To compare while treating NULL as a value, write a IS NOT DISTINCT FROM b.

A view, a generated column or a constraint that cites a PostgreSQL-specific function keeps the language of its creation (see 30.10).


37.16 Unsupported features and differences#

Missing functions and operators#

Unless stated otherwise, the response is 42883 (FUNCTION ... does not exist).

FamilyMissingInstead
stringsstring_to_array, string_to_table, to_ascii, pg_client_encodingregexp_split_to_array, regexp_split_to_table, unaccent
stringsstring SIMILAR TO pattern, substring(string SIMILAR ...): 42601~, LIKE
stringsstring ^@ prefix: 42601starts_with(string, prefix)
stringsUnicode literal U&'\0041': 42703unistr('\0041'), chr(65)
numberssinh, cosh, tanh, asinh, acosh, atanh, sind, cosd, tand..., erf, random(a, b), num_nulls, num_nonnullsfloor(random() * n)
numbersoperators @ (absolute value), |/, ||/, !: 42601abs, sqrt, cbrt
comparisonBETWEEN SYMMETRIC: 42601; x LIKE ANY (ARRAY[...])BETWEEN least(a, b) AND greatest(a, b); OR
dates(a, b) OVERLAPS (c, d): 42601a < d AND c < b
datesdate_trunc(field, t, zone)date_trunc(field, t AT TIME ZONE zone)
datesextract(timezone FROM ...): 0A000
aggregatesxmlagg, range_aggstring_agg, json_agg
aggregatescount(DISTINCT a, b): 42601count(DISTINCT a || '/' || b)
windowsRANGE with an offset: 0A000 (This version of Miraj doesn't yet support 'RANGE with an offset in a window frame'); GROUPS, EXCLUDE: 42601ROWS
windowsstring_agg(...) OVER, json_agg(...) OVER: 0A000
JSON@?, @@: 42601jsonb_path_exists, jsonb_path_match
JSONjsonb_path_query, jsonb_set_lax, jsonb_object, jsonb_to_record, jsonb_populate_record, json_extract_path, row_to_json, array_to_jsonjsonb_path_query_array, jsonb_extract_path, json_object, json_table, jsonb_build_object
arrayscolumn of an array type (int[]): 0A000; ARRAY(SELECT ...), || between arrays: 0A000array_agg
arraysarray_append, array_cat, array_remove, array_replace, array_prepend, array_dims, array_ndims, array_fill, trim_array, generate_subscripts
systempg_get_serial_sequence returns NULLINSERT ... RETURNING, lastval()
systempg_advisory_lock_shared, pg_advisory_unlock_all, inet_server_port, inet_client_port, pg_get_keywordspg_advisory_lock

Behavior differences#

These forms are accepted but do not return the PostgreSQL result.

FormMIRAJPostgreSQL
'a' = 'a 'true (trailing spaces ignored)false
greatest(1, NULL), least(...)NULLthe NULL is ignored
sqrt(-1), ln(0), asin(2)NULLerror
ceil(1.2), floor(prix) of a decimalintegernumeric
1 + true2error
2.5::float8::int32 (rounded to even)
brut || '{"k": 1}' (json column, untyped string)text concatenationerror (merge for a jsonb, here too)
name of a computed columnthe written text (upper(nom))the function name (upper) or ?column?

The condensed list of what the grammar accepts or rejects is in chapter 30.