Reference

SQL Reference

Commands, syntax and short notes — from SELECT to window functions, indexes and transactions. Open an article to go deeper.

Basics: SELECT and filtering9

SELECT … FROM
Article
SELECT col1, col2 FROM table_name;

Pick columns from a table.

See also:····
WHERE
Article
SELECT * FROM t
WHERE col = 5 AND status = 'active';

Filter rows: =, <>, <, >, AND, OR, IN, BETWEEN, LIKE.

See also:····
ORDER BY
Article
SELECT * FROM t ORDER BY created_at DESC;

Sort the result. ASC ascending (default), DESC descending.

See also:····
LIMIT
Article
SELECT * FROM t ORDER BY id LIMIT 10;

Cap the number of rows returned.

See also:····
BETWEEN
WHERE price BETWEEN 100 AND 500
WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31'

Inclusive range on both ends: x >= low AND x <= high. With timestamps, a right bound at 00:00 cuts off the whole last day.

LIKE
WHERE name LIKE 'Ivan%'   -- % any tail
WHERE code LIKE '_X-%'    -- _ exactly one char

Pattern match: % — any number of characters (including zero), _ — exactly one. Case-sensitive in PostgreSQL (see ILIKE).

IN (value list)
WHERE status IN ('paid', 'shipped', 'done')

Shorter than an OR chain: the value is in the listed set. NOT IN with a NULL in the list silently returns nothing.

OFFSET
SELECT * FROM t ORDER BY id
LIMIT 10 OFFSET 20;   -- page 3 by 10

Skip N rows — pagination together with LIMIT. Without ORDER BY pages are unstable. Large OFFSETs are slow — keyset pagination (WHERE id > last) is faster.

NULLS FIRST / LASTPostgreSQL
SELECT * FROM t ORDER BY score DESC NULLS LAST;

Where NULLs sort. By default PostgreSQL puts NULLs last for ASC and first for DESC — NULLS LAST/FIRST makes it explicit.

Joining tables (JOIN)9

INNER JOIN
Article
SELECT * FROM a JOIN b ON a.id = b.a_id;

Only rows that have a match in both tables.

See also:·····
LEFT JOIN
Article
SELECT * FROM a LEFT JOIN b ON a.id = b.a_id;

All rows from the left table; NULL on the right if no match.

See also:·····
RIGHT JOIN
SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;

All rows from the right table; NULL on the left if no match. The mirror of LEFT JOIN — in practice most people swap the tables and write LEFT.

USING / NATURAL JOIN
SELECT * FROM orders JOIN users USING (user_id);
-- NATURAL JOIN joins by ALL same-named columns — avoid

USING (col) is shorthand for ON a.col = b.col, with one output column. NATURAL JOIN joins on ALL same-named columns — fragile, avoid it.

Aliases
Article
SELECT u.name
FROM users u
JOIN orders o ON o.user_id = u.id;

Short names for tables — required when columns share names.

See also:····
CROSS JOIN
Article
SELECT * FROM sizes CROSS JOIN colors;

Cartesian product — every left row paired with every right row. For generating all combinations.

See also:·····
FULL OUTER JOIN
Article
SELECT * FROM a FULL OUTER JOIN b ON a.id = b.a_id;

All rows from both tables; NULL where there is no match. MySQL has no FULL JOIN — emulate with LEFT ∪ RIGHT.

See also:·····
Self-join
Article
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

A table joined to itself via aliases — for hierarchies like «employee → manager».

See also:·····
Anti-join (LEFT JOIN + IS NULL)
Article
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

Left rows with NO match on the right — «users without orders». An alternative to NOT EXISTS.

See also:·····

Aggregation and grouping31

COUNT
Article
SELECT COUNT(*), COUNT(email) FROM users;

Counts rows in a group. COUNT(*) — all rows, COUNT(col) — non-NULL only, COUNT(DISTINCT col) — unique values.

See also:···
SELECT SUM(amount) FROM orders;

Sum of numeric values in a group. NULLs are ignored. Returns NULL (not 0) for an empty group.

SELECT AVG(price) FROM products;

Arithmetic mean. NULLs are excluded from the divisor. Cast an integer column to numeric or the fractional part is truncated.

SELECT MIN(created_at) FROM orders;

Smallest value in a group. Works with numbers, dates and strings. NULLs are ignored.

SELECT MAX(created_at) FROM orders;

Largest value in a group. Works with numbers, dates and strings. NULLs are ignored.

GROUP BY
Article
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;

Bucket rows. Every non-aggregate column in SELECT must be in GROUP BY.

See also:···
HAVING
Article
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

Filter on aggregates — like WHERE but after GROUP BY.

See also:···
DISTINCT
Article
SELECT DISTINCT country FROM users;

Drop duplicate rows from the result.

See also:····
COUNT(*) FILTERPostgreSQL
Article
COUNT(*) FILTER (WHERE type = 'view') AS views

Conditional aggregate: COUNT/SUM only over rows matching WHERE. Replaces three separate queries with one.

See also:·····
STRING_AGGPostgreSQL
Article
STRING_AGG(name, ', ' ORDER BY created_at)

Concatenate values into a single string with a delimiter. The inner ORDER BY locks down order.

See also:··
ARRAY_AGGPostgreSQL
Article
ARRAY_AGG(amount ORDER BY created_at)

Collect values into an array. Handy when an audit row needs the full history in one cell.

See also:··
GROUPING SETSPostgreSQL
Article
GROUP BY GROUPING SETS ((kind), (user_id), ())

Multiple grouping levels in one query — row by row: by kind, by user_id, and a grand total.

See also:···
ROLLUPPostgreSQL
Article
GROUP BY ROLLUP (DATE_TRUNC('month', ts))

Same grouping level plus a grand-total row (a single NULL row at the end).

See also:···
PERCENTILE_CONTPostgreSQL
Article
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)

Median / quantile. More outlier-resistant than AVG.

See also:···
UNNESTPostgreSQL
Article
SELECT tag FROM articles, UNNEST(tags) tag

Expand an array into rows — one row per array element.

See also:··
DISTINCT ONPostgreSQL
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;

«Latest row per key» in one statement: the first row of each group by ORDER BY. The PostgreSQL idiom replacing ROW_NUMBER() + rn = 1.

COUNT(DISTINCT)
Article
SELECT COUNT(DISTINCT user_id) FROM events;

Counts distinct values. Pricey on big tables — use APPROX_COUNT_DISTINCT / HLL when an estimate is enough.

See also:···
BOOL_AND / BOOL_OR
Article
SELECT BOOL_AND(active), BOOL_OR(is_admin) FROM users;

Boolean aggregates: BOOL_AND is true if EVERY row is true; BOOL_OR if at least one is. NULLs are ignored.

See also:·
EVERY
Article
SELECT dept, EVERY(salary > 0) FROM emp GROUP BY dept;

The SQL-standard synonym for BOOL_AND — true when the condition holds for every row in the group.

See also:·
STDDEV
Article
SELECT STDDEV_SAMP(amount), STDDEV_POP(amount) FROM orders;

Standard deviation: _SAMP for a sample (n-1 divisor), _POP for the full population (n divisor). Bare STDDEV equals STDDEV_SAMP.

See also:···
VARIANCE
Article
SELECT VAR_SAMP(amount), VAR_POP(amount) FROM orders;

Variance — the square of the standard deviation. _SAMP for a sample, _POP for a population. Bare VARIANCE equals VAR_SAMP.

See also:···
MODE() WITHIN GROUPPostgreSQL
Article
SELECT MODE() WITHIN GROUP (ORDER BY status) FROM tickets;

The mode — the most frequent value in the group. On ties the first by ORDER BY wins.

See also:···
PERCENTILE_DISC
Article
SELECT PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY amount) FROM orders;

Discrete percentile — returns an actual value from the data, unlike PERCENTILE_CONT which interpolates.

See also:···
BIT_AND / BIT_OR
Article
SELECT BIT_OR(flags), BIT_AND(flags) FROM permissions;

Bitwise aggregates over an integer column: BIT_OR collects every set bit, BIT_AND keeps bits common to all rows. For flag masks.

See also:·
JSON_AGG / JSONB_AGGPostgreSQL
Article
SELECT JSONB_AGG(t ORDER BY t.id) FROM tasks t;

Roll rows up into a JSON array — handy for returning nested data in one query. JSONB_AGG stores it as jsonb (faster, dedups keys).

See also:····
JSONB_OBJECT_AGGPostgreSQL
Article
SELECT JSONB_OBJECT_AGG(key, value) FROM settings;

Fold key-value pairs into a single JSON object. Perfect for turning a settings table into a map.

See also:····
SELECT CORR(price, sales) FROM products;

Pearson correlation coefficient between two columns: -1 to 1. A measure of linear association.

See also:···
REGR_SLOPE / REGR_INTERCEPT
Article
SELECT REGR_SLOPE(y, x), REGR_INTERCEPT(y, x) FROM points;

Slope and intercept of the least-squares line of y on x — a trend in one aggregate, no external stats package.

See also:···
REGR_R2
Article
SELECT REGR_R2(y, x) FROM points;

The R² coefficient of determination for the regression of y on x: 0..1, how well the line fits the data.

See also:···
MAX(...) FILTER (pivot)PostgreSQL
Article
SELECT user_id,
  MAX(amount) FILTER (WHERE kind = 'deposit')  AS deposit,
  MAX(amount) FILTER (WHERE kind = 'withdraw') AS withdraw
FROM tx GROUP BY user_id;

Pivot rows into columns: MAX/SUM with FILTER per category. Replaces a hand-written stack of CASE aggregates.

See also:···
SELECT region, product, SUM(amount)
FROM sales
GROUP BY CUBE (region, product);

Every grouping combination at once: by region, by product, by both, and a grand total. For cross-tab reports.

See also:···

Subqueries6

IN (subquery)
Article
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);

Test membership against a list produced by another query.

See also:···
EXISTS
Article
SELECT * FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

Keep the row if the inner query finds at least one matching row.

See also:···
Scalar subquery
Article
SELECT
  name,
  (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt
FROM users u;

A subquery returning one value — can sit inside SELECT.

See also:···
Correlated subquery
SELECT * FROM orders o
WHERE amount > (
  SELECT AVG(amount) FROM orders i WHERE i.user_id = o.user_id
);

The subquery references the outer row — logically it runs per row. This powers EXISTS patterns and «above the average of its own group».

Subquery in FROM
SELECT dept, MAX(cnt)
FROM (
  SELECT dept, user_id, COUNT(*) AS cnt
  FROM sales GROUP BY dept, user_id
) s
GROUP BY dept;

A derived table: the subquery result is used as a table. The classic «aggregate of an aggregate». The alias after the parenthesis is mandatory.

ANY / ALL
WHERE price > ALL (SELECT price FROM basic_plans)
WHERE id = ANY (ARRAY[1, 2, 3])

Compare against a set: > ALL — greater than every value, > ANY — greater than at least one. = ANY(array) is the PostgreSQL idiom replacing IN for array parameters.

Set operations (UNION/INTERSECT)3

UNION / UNION ALL
Article
SELECT id FROM a
UNION ALL
SELECT id FROM b;

Stack the results of two queries (same column count, compatible types). UNION removes duplicates, UNION ALL keeps them (and is faster).

See also:·
INTERSECT
Article
SELECT user_id FROM purchases
INTERSECT
SELECT user_id FROM refunds;

Rows present in BOTH queries. In MySQL — since 8.0.31.

See also:·
EXCEPT
Article
SELECT user_id FROM users
EXCEPT
SELECT user_id FROM banned;

Rows from the first query that are NOT in the second. In Oracle this is MINUS.

See also:·

Window functions12

ROW_NUMBER
Article
SELECT
  name,
  ROW_NUMBER() OVER (ORDER BY score DESC) AS rn
FROM players;

Unique sequential number per row in the window.

See also:····
RANK / DENSE_RANK
Article
RANK() OVER (PARTITION BY dept ORDER BY salary DESC)

Rank with gaps (RANK) or without (DENSE_RANK) on ties.

See also:··
PARTITION BY
Article
SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at)

Split the window into groups — the aggregate is computed per group.

See also:·
LAG / LEAD
Article
LAG(price, 1) OVER (ORDER BY date)

Value from the previous (LAG) or next (LEAD) row in the window.

See also:··
SUM() OVER (running total)
SELECT created_at, amount,
  SUM(amount) OVER (ORDER BY created_at) AS running_total
FROM payments;

Running total: an aggregate with ORDER BY inside OVER accumulates from the window start to the current row. The single most common window-function interview task.

NTILE
Article
NTILE(4) OVER (ORDER BY score DESC)

Split rows into N equal-sized buckets in order. With uneven counts buckets become 3-3-2-2 (extras go to lower-numbered ones).

See also:·····
FIRST_VALUE / LAST_VALUE
Article
LAST_VALUE(score) OVER (
  PARTITION BY team_id ORDER BY score DESC
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)

First / last value in the window. For LAST_VALUE you must widen the frame with ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — the default frame chops the right edge.

See also:··
PERCENT_RANK
Article
PERCENT_RANK() OVER (ORDER BY score DESC, player_id)

Percentile rank in 0..1. A second sort column makes the result deterministic on ties.

See also:··
NTH_VALUE
Article
NTH_VALUE(amount, 2) OVER (
  PARTITION BY customer_id ORDER BY amount DESC
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)

N-th value from the window. Also needs the widened frame.

See also:··
Window frames
Article
AVG(x) OVER (ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)

Rolling windows: «trailing 7 days», «3-day average», etc.

See also:····
CUME_DIST
CUME_DIST() OVER (ORDER BY salary)

Fraction of rows with a value ≤ the current one (0..1]. Sibling of PERCENT_RANK, which counts rows STRICTLY below.

WINDOW clause
SELECT
  ROW_NUMBER() OVER w,
  SUM(amount) OVER w
FROM t
WINDOW w AS (PARTITION BY user_id ORDER BY created_at);

Declare the window once and reuse it across several functions — no copy-pasted PARTITION BY/ORDER BY.

CTEs and recursion (WITH)5

WITH … AS
Article
WITH active AS (
  SELECT * FROM users WHERE status = 'active'
)
SELECT * FROM active WHERE country = 'RU';

A named temporary result — break a big query into steps.

See also:··
Multiple CTEs
Article
WITH a AS (...), b AS (...)
SELECT * FROM a JOIN b ON ...;

Multiple CTEs separated by commas, read top-to-bottom.

See also:··
WITH RECURSIVE
Article
WITH RECURSIVE chain AS (
  SELECT id, manager_id FROM emp WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.manager_id FROM emp e JOIN chain c ON e.manager_id = c.id
)
SELECT * FROM chain;

Walk a hierarchy: anchor query → UNION ALL → recursive step. Perfect for org charts, graphs, chains.

See also:··
LATERAL
Article
FROM customers c
LEFT JOIN LATERAL (
  SELECT * FROM orders WHERE customer_id = c.id
  ORDER BY amount DESC LIMIT 2
) l ON true

Sub-query sees the outer row's columns. Great for «top-N per X». LEFT JOIN LATERAL … ON true keeps outer rows with no match; the comma form silently drops them.

See also:·····
generate_seriesPostgreSQL
Article
generate_series('2024-01-01'::date, '2024-01-15'::date, '1 day')

Generate a calendar / axis. Inclusive on both ends. Standard trick for «fill missing days with zero».

See also:··

Data changes (DML)11

INSERT
Article
INSERT INTO t (col1, col2) VALUES (1, 'a'), (2, 'b');

Add rows to a table.

See also:··
UPDATE
Article
UPDATE t SET col = 'x' WHERE id = 5;

Modify existing rows. Always include WHERE — otherwise all rows update.

See also:··
DELETE
Article
DELETE FROM t WHERE id = 5;

Delete rows. Always include WHERE.

See also:··
TRUNCATE
TRUNCATE TABLE logs;

Instantly empty a whole table. Faster than DELETE (no per-row work), but no WHERE, and it refuses when foreign keys point at the table unless CASCADE.

ON CONFLICT DO NOTHINGPostgreSQL
Article
INSERT INTO t (id) VALUES (1)
ON CONFLICT (id) DO NOTHING;

Idempotent insert — re-running silently skips rows that already exist.

See also:··
ON CONFLICT DO UPDATEPostgreSQL
Article
INSERT INTO t (id, n) VALUES (1, 1)
ON CONFLICT (id) DO UPDATE
  SET n = t.n + EXCLUDED.n;

UPSERT: insert or update. EXCLUDED.col is the value we tried to insert.

See also:··
MERGEPostgreSQL
Article
MERGE INTO t USING src ON t.id = src.id
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...;

Postgres 15+ — UPSERT alternative with MATCHED / NOT MATCHED branches and per-branch conditions.

See also:··
RETURNINGPostgreSQL
Article
INSERT INTO t (name) VALUES ('a')
RETURNING id, name;

Return rows just inserted / updated / deleted in the same statement — no second round-trip.

See also:·····
DELETE … USINGPostgreSQL
Article
DELETE FROM orders o
USING customers c
WHERE o.customer_id = c.id AND c.country = 'US';

JOIN-style DELETE — filter by another table without a subquery.

See also:··
UPDATE … FROMPostgreSQL
Article
UPDATE customers c SET total = s.total
FROM (SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id) s
WHERE c.id = s.customer_id;

Bulk UPDATE driven by an aggregate subquery.

See also:··
CTE + DELETE … RETURNINGPostgreSQL
Article
WITH moved AS (
  DELETE FROM orders WHERE old RETURNING *
)
INSERT INTO archive SELECT * FROM moved;

Atomic archival: move rows in one statement, no race window between DELETE and INSERT.

See also:··

Schema (DDL)15

CREATE TABLE
Article
CREATE TABLE t (
  id INTEGER PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

Create a table with typed columns.

See also:···
ALTER TABLE
Article
ALTER TABLE t ADD COLUMN created_at TIMESTAMP;
ALTER TABLE t DROP COLUMN legacy_code;
ALTER TABLE t RENAME COLUMN name TO full_name;
ALTER TABLE t ALTER COLUMN price TYPE NUMERIC(10,2);

Modify an existing table: add/drop/rename a column, change its type.

See also:···
PRIMARY KEY / UNIQUE / NOT NULL / DEFAULT
CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  status TEXT NOT NULL DEFAULT 'active'
);

The core constraints: PRIMARY KEY — the row’s unique identifier (one per table), UNIQUE — no duplicates, NOT NULL — a value is required, DEFAULT — the fallback value.

DROP TABLE
DROP TABLE IF EXISTS temp_import;
DROP TABLE orders CASCADE;  -- also drops dependent FKs/views

Drop a table and all its data. IF EXISTS — don’t fail when absent; CASCADE — also drop dependent objects. There is no undo.

SERIAL / IDENTITYPostgreSQL
CREATE TABLE t (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);
-- legacy spelling: id BIGSERIAL PRIMARY KEY

Auto-increment id. The modern form is GENERATED AS IDENTITY (SQL standard); SERIAL/BIGSERIAL is the legacy sequence-backed spelling. MySQL uses AUTO_INCREMENT.

CHECK
Article
ALTER TABLE products
ADD CONSTRAINT price_positive CHECK (price > 0);

Reject invalid values at the DB level, not in code.

See also:···
FK ON DELETE
Article
FOREIGN KEY (post_id) REFERENCES posts(id)
  ON DELETE CASCADE   -- or SET NULL / RESTRICT

What happens to the child row when the parent is deleted: cascade, null out the FK, or block.

See also:···
NOT VALID + VALIDATEPostgreSQL
Article
ALTER TABLE t
  ADD CONSTRAINT fk REFERENCES p(id) NOT VALID;
ALTER TABLE t VALIDATE CONSTRAINT fk;

Add a FK to a big production table without a heavy lock: NOT VALID is instant, VALIDATE doesn't block writers.

See also:···
GENERATED column
Article
total NUMERIC GENERATED ALWAYS AS (price * (1 + tax)) STORED

Column value is computed automatically — the formula lives in one place.

See also:···
Partial UNIQUEPostgreSQL
Article
CREATE UNIQUE INDEX u ON users (email)
WHERE deleted_at IS NULL;

Uniqueness only over live rows — for soft-delete, so users can re-register after deletion.

See also:···
Range partitioningPostgreSQL
Article
CREATE TABLE logs (...) PARTITION BY RANGE (ts);
CREATE TABLE logs_2024 PARTITION OF logs
  FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

Slice a big table by range. Old partitions drop in milliseconds.

See also:···
TRIGGERPostgreSQL
Article
CREATE TRIGGER touch BEFORE UPDATE ON notes
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();

DB-level auto-logic — e.g. set updated_at without touching application code.

See also:···
MATERIALIZED VIEWPostgreSQL
Article
CREATE MATERIALIZED VIEW v AS SELECT ...;
REFRESH MATERIALIZED VIEW v;

Cached result of a heavy query. Refresh on a schedule.

See also:···
CREATE VIEW
CREATE VIEW active_users AS
SELECT * FROM users WHERE status = 'active';

A saved query behind a table name: no data is copied, every SELECT from the view re-runs the query. Contrast with MATERIALIZED VIEW, where the result is cached.

CREATE TEMPORARY TABLE
CREATE TEMPORARY TABLE staging AS
SELECT * FROM imports WHERE batch_id = 42;

A session-scoped table: it vanishes on disconnect and is invisible to other connections. The workhorse of ETL and one-off computations.

Strings and dates9

LOWER / UPPER / LENGTH
Article
LOWER(name), UPPER(code), LENGTH(text)

Lowercase, uppercase, string length.

CONCAT
Article
CONCAT(first_name, ' ', last_name)

Concatenate strings. PostgreSQL also accepts the || operator (in MySQL, || is logical OR by default, not concatenation).

See also:··
EXTRACT
Article
EXTRACT(YEAR FROM created_at), EXTRACT(MONTH FROM created_at)

Pull a part of a date — year, month, day.

See also:···
DATE_TRUNCPostgreSQL
Article
DATE_TRUNC('month', created_at)

Round a timestamp down to a period (day/week/month). The go-to tool for grouping by time.

See also:···
NOW / CURRENT_DATE + INTERVAL
Article
WHERE created_at >= NOW() - INTERVAL '7 days'

Current instant (NOW()) / today (CURRENT_DATE) and interval math — «in the last 7 days».

See also:·
CAST / ::
Article
CAST(price AS INTEGER)   -- or price::int

Convert a value to another type. CAST(x AS type) is standard; x::type is PostgreSQL shorthand.

See also:·
TRIM / SUBSTRING / REPLACE
Article
TRIM(name), SUBSTRING(code FROM 1 FOR 3), REPLACE(phone, '-', '')

Strip whitespace, slice a substring, replace a fragment. Everyday string cleanup.

See also:·
SPLIT_PARTPostgreSQL
Article
SPLIT_PART(email, '@', 2)   -- domain from an e-mail

Split a string on a delimiter and take the N-th part. In MySQL — SUBSTRING_INDEX.

See also:···
ILIKEPostgreSQL
Article
WHERE name ILIKE '%ivan%'

Case-insensitive LIKE (PostgreSQL). In MySQL, plain LIKE is already case-insensitive under the default collation.

See also:···

String functions17

LEFT / RIGHT
Article
LEFT(code, 3), RIGHT(phone, 4)

Take the first N characters (LEFT) or the last N (RIGHT) of a string.

See also:···
CONCAT_WS
CONCAT_WS(', ', city, street, house)

Join strings with a separator, skipping NULLs — an address without «double commas». WS = with separator.

POSITION / STRPOSPostgreSQL
Article
POSITION('@' IN email), STRPOS(email, '@')

Index of the first match (1-based), 0 if not found. STRPOS is the PostgreSQL shorthand.

See also:···
LPAD / RPAD
Article
LPAD(id::text, 6, '0'), RPAD(name, 20, ' ')

Pad a string to a target length on the left (LPAD) or right (RPAD). Classic use: zero-pad an id.

See also:···
INITCAPPostgreSQL
Article
INITCAP('john DOE')   -- John Doe

Uppercase the first letter of each word, lowercase the rest. Absent in MySQL.

See also:···
REPEAT
Article
REPEAT('ab', 3)   -- ababab

Repeat a string N times. Handy for placeholders and simple text bar charts.

See also:···
REVERSE
Article
REVERSE(name)

Reverse a string character by character. Sometimes used to index by suffix.

See also:···
char_length
Article
char_length(name), char_length('açai')   -- 4

String length in CHARACTERS (not bytes) — matters for UTF-8. Synonym of character_length.

See also:···
REGEXP_REPLACEPostgreSQL
Article
REGEXP_REPLACE(phone, '[^0-9]', '', 'g')

Replace by regular expression. The 'g' flag replaces every match; without it only the first.

See also:·
REGEXP_MATCHESPostgreSQL
Article
SELECT (REGEXP_MATCHES(url, '/(\d+)'))[1] AS id;

Return captured regex groups as an array. With the 'g' flag it yields one row per match.

See also:·
REGEXP_SPLIT_TO_ARRAYPostgreSQL
Article
REGEXP_SPLIT_TO_ARRAY('a, b,c', '\s*,\s*')

Split a string by a regex delimiter into an array. There is also …TO_TABLE for rows.

See also:·
TRANSLATEPostgreSQL
Article
TRANSLATE(code, 'abc', 'xyz')   -- a->x, b->y, c->z

Character-by-character mapping between two sets. Extra chars in the first set are deleted. Not REPLACE.

See also:·
BTRIM / LTRIM / RTRIMPostgreSQL
Article
BTRIM(code, '0'), LTRIM(s), RTRIM(s, '/')

Strip given chars from both ends (BTRIM), the left (LTRIM) or the right (RTRIM); spaces by default.

See also:·
FORMATPostgreSQL
Article
FORMAT('Hi %s, id=%L', name, id)

Build a string from a template: %s value, %I identifier, %L safe literal. Key for dynamic SQL.

See also:····
STARTS_WITHPostgreSQL
Article
WHERE STARTS_WITH(path, '/api/')

Whether a string begins with a prefix — clearer than LIKE 'x%'. Available since PostgreSQL 11.

See also:···
ascii / chrPostgreSQL
Article
ascii('A')   -- 65
chr(65)      -- A

Code of the first character (ascii) and the character for a code (chr). In MySQL the inverse is CHAR.

See also:··
to_hexPostgreSQL
Article
to_hex(255)   -- 'ff'

Convert an integer to its hexadecimal string. Handy for colors, bit masks, debugging.

See also:··

Numbers & math16

ROUND
Article
ROUND(3.14159)        -- 3
ROUND(2.5)            -- banker? no: 3

Round to the nearest integer. Halves round away from zero (2.5 → 3).

See also:···
ROUND(x, n)
Article
ROUND(3.14159, 2)     -- 3.14
ROUND(12345.6, -2)    -- 12300

Round to n decimal places; a negative n rounds left of the point. Only works on numeric, not float.

See also:···
CEIL / CEILING
Article
CEIL(4.1)   -- 5
CEIL(-4.1)  -- -4

Round up to the next integer. CEILING is a synonym.

See also:···
FLOOR
Article
FLOOR(4.9)   -- 4
FLOOR(-4.1)  -- -5

Round down to the previous integer. For negatives it goes further from zero.

See also:···
TRUNCPostgreSQL
Article
TRUNC(3.99)     -- 3
TRUNC(3.456, 2) -- 3.45

Drop the fractional part (toward zero), no rounding. In MySQL it is TRUNCATE(x, n).

See also:···
ABS(-7)   -- 7

Absolute value — the magnitude without sign.

See also:···
MOD(10, 3)   -- 1

Remainder of division. Handy for «every N-th row» and parity; the result's sign follows the dividend.

See also:···
POWER
Article
POWER(2, 10)   -- 1024

Raise to a power. POW is a synonym.

See also:··
SQRT(144)   -- 12

Square root. A negative argument raises an error.

See also:··
EXP / LN
Article
EXP(1)    -- 2.7182818...
LN(2.718) -- ~1

Exponential e^x and natural logarithm (base e). LN(0) and LN(negative) error out.

See also:··
LOGPostgreSQL
Article
LOG(100)     -- 2  (base 10)
LOG(2, 8)    -- 3  (base 2)

In PostgreSQL LOG(x) is base 10, LOG(b, x) is an arbitrary base. Caution: in MySQL LOG(x) is the natural log.

See also:··
SIGN(-42)  -- -1
SIGN(0)    -- 0

Sign of a number: -1, 0, or 1. Handy to branch on direction of change.

See also:···
GREATEST / LEAST
Article
GREATEST(a, b, c), LEAST(a, b, c)

Largest / smallest among the arguments within one row (not an aggregate). NULL arguments are ignored.

See also:···
RANDOMPostgreSQL
Article
SELECT * FROM t ORDER BY RANDOM() LIMIT 5;

Random number in [0,1). In MySQL it is RAND(). ORDER BY RANDOM() gives a random sample but is costly on big tables.

See also:
DIV (integer division)PostgreSQL
Article
DIV(7, 2)   -- 3
7 / 2       -- 3 when both are int

Integer division discarding the remainder. In PostgreSQL / already floors when both operands are ints; MySQL uses the DIV operator for this.

See also:···
WIDTH_BUCKETPostgreSQL
Article
WIDTH_BUCKET(score, 0, 100, 10)  -- bucket 1..10

Assign a value to an equal-width histogram bucket between two bounds. For distributions and range bucketing.

See also:····

Date & time functions16

AGEPostgreSQL
Article
AGE(end_ts, start_ts)   -- or AGE(birthday) vs now
AGE('2024-03-01', '2024-01-15')

Difference between two dates as an interval (years/months/days), not seconds. With one argument it counts from today — handy for age.

DATE_PARTPostgreSQL
Article
DATE_PART('hour', created_at), DATE_PART('dow', created_at)

Function form of EXTRACT — pulls a date/time part as a number. The field is a string, so it's easy to pass dynamically.

See also:···
EXTRACT(EPOCH FROM …)
Article
EXTRACT(EPOCH FROM (ended_at - started_at)) AS seconds

Turn an interval or timestamp into seconds (Unix time). The go-to way to measure a duration in seconds — then divide by 60/3600.

See also:···
TO_CHARPostgreSQL
Article
TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI'), TO_CHAR(amount, 'FM999G999D00')

Format a date/number into a string via a pattern (YYYY, MM, DD, HH24...). PostgreSQL patterns differ from MySQL's DATE_FORMAT.

See also:·
TO_DATEPostgreSQL
Article
TO_DATE('2024-03-15', 'YYYY-MM-DD')

Parse a string into a date with an explicit pattern. Safer than a ::date cast when the format is non-standard.

See also:·
TO_TIMESTAMPPostgreSQL
Article
TO_TIMESTAMP('2024-03-15 14:30', 'YYYY-MM-DD HH24:MI')
TO_TIMESTAMP(1710512400)   -- from Unix epoch

Parse a string into a timestamp by pattern, or build a timestamptz from Unix seconds (numeric argument).

See also:·
CURRENT_TIMESTAMP / LOCALTIMESTAMP
Article
SELECT CURRENT_TIMESTAMP, LOCALTIMESTAMP;

Transaction-start time: CURRENT_TIMESTAMP is timezone-aware (timestamptz), LOCALTIMESTAMP is not. Constant within a single transaction.

See also:·
CURRENT_TIME / CURRENT_DATE
Article
SELECT CURRENT_DATE, CURRENT_TIME;

Today's date only (CURRENT_DATE) or time only (CURRENT_TIME) — no parentheses; these are SQL special values, not functions.

See also:·
Date arithmetic (date + int)PostgreSQL
Article
SELECT order_date + 7, due_date - 1, end_dt - start_dt AS days;

You can add/subtract whole days to a date (date + 7). Subtracting two dates yields an integer day count; two timestamps yield an interval.

See also:·
make_date / make_timePostgreSQL
Article
make_date(2024, 3, 15), make_time(14, 30, 0)

Build a date or time from separate year/month/day, hour/minute/second numbers — no format-string juggling.

See also:·
make_timestamp / make_intervalPostgreSQL
Article
make_timestamp(2024, 3, 15, 14, 30, 0)
make_interval(days => 10, hours => 2)

Build a timestamp or interval from numeric parts. make_interval takes named arguments (days =>, hours =>).

See also:·
AT TIME ZONEPostgreSQL
Article
ts_utc AT TIME ZONE 'Europe/Moscow'
local_ts AT TIME ZONE 'UTC'

Shift an instant to another time zone. On a timestamptz it yields local wall time in that zone; on a naive timestamp it interprets it as being in that zone.

See also:··
JUSTIFY_INTERVAL / JUSTIFY_HOURSPostgreSQL
Article
JUSTIFY_HOURS(INTERVAL '36 hours')   -- 1 day 12:00:00

Normalize an interval: roll excess hours into days, days into months. Turns «50 hours» into a readable «2 days 02:00:00».

See also:··
DATE_BINPostgreSQL
Article
DATE_BIN('15 minutes', ts, TIMESTAMP '2024-01-01')

Snap a timestamp down to the start of an arbitrary-width bucket (e.g. 15 minutes) from an origin. More flexible than DATE_TRUNC. Postgres 14+.

See also:···
OVERLAPS
Article
(start_a, end_a) OVERLAPS (start_b, end_b)

Test whether two time periods overlap. Handy for finding booking or shift conflicts.

See also:··
Time zone cast (timestamptz)PostgreSQL
Article
now()::timestamptz, '2024-03-15 10:00'::timestamp

timestamptz stores the instant in UTC and applies the zone on display; timestamp is naive wall time. For events you almost always want timestamptz.

See also:··

CASE and NULL5

CASE WHEN
Article
CASE
  WHEN score >= 90 THEN 'A'
  WHEN score >= 70 THEN 'B'
  ELSE 'C'
END

Inline conditional logic — like if/else inside SELECT.

See also:··
CASE (simple form)
CASE status
  WHEN 'paid' THEN 'Paid'
  WHEN 'shipped' THEN 'On the way'
  ELSE '—'
END

The short form for comparing one expression against constants. It cannot catch NULL (WHEN NULL never matches) — use the searched form with IS NULL for that.

COALESCE
Article
COALESCE(nickname, full_name, 'Anonymous')

Return the first non-NULL value in the list.

See also:··
NULLIF
Article
NULLIF(divisor, 0)

Turn a value into NULL when it equals the second argument. Useful to avoid divide-by-zero.

See also:··
NULL & IS DISTINCT FROM
Article
-- = NULL is never true — use IS NULL
WHERE deleted_at IS NULL
-- NULL-safe equality:
WHERE a IS DISTINCT FROM b

Comparing to NULL with = is always «unknown» (not TRUE/FALSE). Use IS NULL to test; use IS DISTINCT FROM for NULL-safe equality.

See also:··

JSON / JSONB19

JSONB ->>PostgreSQL
Article
payload->>'target'

Pull a value from JSONB as text — for string ops and comparisons.

See also:··
JSONB @>PostgreSQL
Article
payload @> '{"plan":"pro"}'

Does the JSONB contain the given fragment. Uses GIN — fast on big tables.

See also:···
GIN + jsonb_path_opsPostgreSQL
Article
CREATE INDEX idx ON events USING GIN (payload jsonb_path_ops)

Optimal index for @> queries on JSONB. Smaller than the default jsonb_ops.

See also:
JSONB -> / ->>PostgreSQL
Article
data->'user'->>'name'   -- -> keeps json, ->> as text

-> pulls a field/element as jsonb (to keep drilling), ->> as text. Key by string, array index by number.

See also:··
#> / #>>PostgreSQL
Article
data #>> '{address,city}'   -- text at a nested path

Read a value at a nested path given as a key array: #> as jsonb, #>> as text. Shorter than chaining ->.

See also:··
JSONB_BUILD_OBJECTPostgreSQL
Article
jsonb_build_object('id', id, 'name', name)

Build a JSON object from alternating key, value pairs. Value types are preserved (numbers stay numbers).

See also:····
JSONB_BUILD_ARRAYPostgreSQL
Article
jsonb_build_array(id, name, created_at)

Build a JSON array from the given arguments of any types.

See also:····
JSONB_AGGPostgreSQL
Article
jsonb_agg(item ORDER BY created_at)

Aggregate: collect a group of rows into a JSON array. The inner ORDER BY locks element order.

See also:····
JSONB_ARRAY_ELEMENTSPostgreSQL
Article
SELECT e FROM t, jsonb_array_elements(t.tags) AS e

Expand a JSON array into rows — one row per element. The _text variant returns text instead of jsonb.

See also:·····
JSONB_ARRAY_LENGTHPostgreSQL
Article
jsonb_array_length(data->'items')

Length of a JSON array. Errors if the value is not an array — guard with jsonb_typeof.

See also:·····
JSONB_SETPostgreSQL
Article
jsonb_set(data, '{address,city}', '"Lima"')

Return a copy of the JSON with the value at a path replaced. create_missing=true (default) adds the key if absent.

See also:·
JSONB_EACHPostgreSQL
Article
SELECT key, value FROM jsonb_each(data)

Expand a JSON object into (key, value) rows — one per key. The _text variant returns value as text.

See also:·····
JSONB_OBJECT_KEYSPostgreSQL
Article
SELECT jsonb_object_keys(data)

Return the top-level key names of a JSON object, one row per key.

See also:·····
? / ?| / ?&PostgreSQL
Article
data ? 'email'        -- has key?
data ?| array['a','b'] -- any of these keys?

Key-existence tests: ? single key, ?| any of these, ?& all of these. Top-level only; GIN-indexable.

See also:·····
JSONB || (merge)PostgreSQL
Article
data || '{"verified":true}'

Merge two JSONB values: right-hand keys overwrite left (shallow, non-recursive). Handy for a partial update.

See also:·
JSONB - / #- (delete)PostgreSQL
Article
data - 'temp'              -- drop a key
data #- '{address,zip}'    -- drop at a path

Delete a key/element: - by top-level key or index, #- at a nested path. Returns a new JSONB.

See also:·
to_jsonbPostgreSQL
Article
to_jsonb(row_var)   -- whole row as a json object

Turn any SQL value/row/array into jsonb. A whole table row becomes a JSON object column → value.

See also:·····
JSONB_TYPEOFPostgreSQL
Article
jsonb_typeof(data->'price')   -- 'number','string',...

The JSON value's type as text: object, array, string, number, boolean, null. Useful to guard jsonb_array_length etc.

See also:·····
JSONB_PRETTYPostgreSQL
Article
jsonb_pretty(data)

Pretty-print JSONB with indentation for readable output/debugging.

See also:·····

Performance and indexes9

CREATE INDEX
CREATE INDEX idx_orders_user ON orders (user_id);

A plain B-tree index: speeds up equality and range lookups on the column. The cost — disk space and slightly slower writes.

Partial indexPostgreSQL
Article
CREATE INDEX i ON orders (id) WHERE status = 'pending';

Index only over the «hot» subset of rows — smaller and faster to scan.

See also:····
Composite index
Article
CREATE INDEX i ON orders (customer_id, created_at DESC);

Covers filter + sort in one read. Column order matters.

See also:····
Sargable WHERE
Article
-- bad : WHERE EXTRACT(YEAR FROM ts) = 2024
-- good: WHERE ts >= '2024-01-01' AND ts < '2025-01-01'

Don't wrap the column in a function — the index won't fire. Rewrite as a range.

See also:····
NOT EXISTS vs NOT IN
Article
-- NOT IN collapses to 0 rows on any NULL
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

NOT IN silently breaks on any NULL in the subquery. NOT EXISTS is NULL-safe.

See also:···
CONCURRENTLYPostgreSQL
Article
CREATE INDEX CONCURRENTLY i ON events (user_id, kind);

Build an index on a hot table without a heavy lock. Forbidden inside a transaction.

See also:····
EXPLAIN
Article
EXPLAIN SELECT * FROM orders WHERE user_id = 5;

Show the query plan without running it: which scans (Seq Scan / Index Scan), join order, estimated rows.

See also:····
EXPLAIN (ANALYZE, BUFFERS)
Article
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 5;

Actually run the query and show REAL time and row counts vs the estimate. A big estimate↔actual gap signals stale stats or a missing index.

See also:····
VACUUM / ANALYZEPostgreSQL
VACUUM (ANALYZE) orders;
ANALYZE orders;  -- stats only

VACUUM reclaims dead row versions left by UPDATE/DELETE; ANALYZE refreshes planner statistics. Bad plans after bulk changes are fixed exactly here.

Transactions7

BEGIN / COMMIT / ROLLBACK
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
COMMIT;   -- or ROLLBACK; to undo everything

A transaction: everything between BEGIN and COMMIT applies as a whole, or (after ROLLBACK) not at all. Nothing outside sees the intermediate state.

SAVEPOINT
BEGIN;
SAVEPOINT before_bonus;
UPDATE accounts SET balance = balance + 50 WHERE id = 7;
ROLLBACK TO before_bonus;  -- undo just this part
COMMIT;

A rollback point inside a transaction: ROLLBACK TO undoes only the steps after the SAVEPOINT, not the whole transaction.

Isolation levelsPostgreSQL
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- READ COMMITTED (default) | REPEATABLE READ | SERIALIZABLE

What a transaction sees of concurrent changes. READ COMMITTED (default) — each statement sees fresh commits; REPEATABLE READ — one snapshot for the whole transaction; SERIALIZABLE — as if transactions ran one by one (be ready to retry serialization errors).

SELECT … FOR UPDATE
Article
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) FOR UPDATE;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
COMMIT;

Lock rows until the end of the transaction. Standard for money transfers.

See also:··
Conditional UPDATE
Article
UPDATE accounts SET balance = balance - 200
WHERE id = 1 AND balance >= 200;

Check-and-update in one atomic statement. If 0 rows updated — surface «insufficient funds».

See also:··
FOR UPDATE SKIP LOCKED
Article
SELECT id FROM jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED LIMIT 1;

Worker queue: each worker grabs its own job, skipping rows locked by others.

See also:··
Atomic counter
Article
UPDATE counters SET n = n + 1 WHERE id = 1;

A single UPDATE increments the counter race-safely. SELECT-then-UPDATE loses increments.

See also:··

Access control (GRANT/REVOKE)4

GRANT
Article
GRANT SELECT, INSERT ON orders TO analyst;

Give privileges on an object to a role/user. List the actions needed (SELECT, INSERT, UPDATE, DELETE).

See also:··
REVOKE
Article
REVOKE INSERT ON orders FROM analyst;

Take back privileges previously granted.

See also:··
CREATE ROLE
Article
CREATE ROLE analyst LOGIN PASSWORD 'secret';

Create a role (user/group). Privileges are granted to the role; users are members of it.

See also:··
Read-only role
Article
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO readonly;

The standard read-only pattern: schema access + SELECT on all tables + a rule for future tables via ALTER DEFAULT PRIVILEGES.

See also:··

BigQuery (GoogleSQL)38

COUNTIF
SELECT city, COUNTIF(is_paid) AS paid
FROM events
GROUP BY city;

Counts rows where the condition is true. Replaces SUM(CASE WHEN … THEN 1 ELSE 0 END).

IF
SELECT IF(revenue > 2000, 'big', 'normal') AS size
FROM events;

A ternary choice as one function: condition, value-if-true, value-if-false.

IFNULL / COALESCE
SELECT IFNULL(revenue, 0) AS revenue FROM events;

Substitutes a fallback for NULL. IFNULL takes two arguments, COALESCE any number.

SAFE_DIVIDE
SELECT SAFE_DIVIDE(COUNTIF(is_paid), COUNT(*)) AS cr
FROM events;

Division that returns NULL on a zero denominator instead of failing.

SAFE_CAST
SELECT SAFE_CAST(session_id AS INT64) AS code FROM events;

A cast that yields NULL on an unconvertible value instead of killing the query.

SAFE. prefix
SELECT SAFE.PARSE_DATE("%Y-%m-%d", raw_date) AS d FROM events;

The SAFE. prefix on most scalar functions turns a runtime error into NULL.

LOGICAL_OR / LOGICAL_AND
SELECT platform, LOGICAL_OR(is_paid) AS had_purchase
FROM events GROUP BY platform;

Boolean aggregates: was any value TRUE, and were all values TRUE.

QUALIFY
SELECT city, event_id, revenue
FROM events
QUALIFY ROW_NUMBER() OVER (PARTITION BY city ORDER BY revenue DESC) = 1;

Filters on a window function result in place — no wrapping subquery or CTE. Runs after SELECT and WINDOW.

SELECT * EXCEPT
SELECT * EXCEPT(items, session) FROM events;

Every column except the listed ones — without spelling out the rest.

SELECT * REPLACE
SELECT * REPLACE(IFNULL(revenue, 0) AS revenue) FROM events;

Swaps one column value while keeping its name and position among the rest.

ARRAY
SELECT ["food", "bed"] AS items;
-- column type: ARRAY<STRING>

A repeated field: one cell holds a list of same-typed values.

UNNEST (GoogleSQL)
SELECT event_id, item
FROM events, UNNEST(items) AS item;

Expands an array into rows — one row per element. An event with an empty array drops out.

UNNEST … WITH OFFSET
SELECT item, pos
FROM events, UNNEST(items) AS item WITH OFFSET AS pos;

The same expansion plus the element index inside the array, starting at zero.

ARRAY_LENGTH
SELECT ARRAY_LENGTH(items) AS basket_size FROM events;

The number of elements in an array, with no expansion.

ARRAY_AGG (GoogleSQL)
SELECT user_id,
       ARRAY_AGG(event_name ORDER BY event_ts LIMIT 2) AS first_two
FROM events GROUP BY user_id;

Collects a group into an array. Accepts DISTINCT, ORDER BY and LIMIT inside the call.

ARRAY_TO_STRING
SELECT ARRAY_TO_STRING(items, ", ") AS basket FROM events;

Joins an array into a single string with the given separator.

IN UNNEST
SELECT event_id FROM events WHERE "food" IN UNNEST(items);

Tests membership in an array — no join, no flattening the whole table.

OFFSET / ORDINAL
SELECT APPROX_QUANTILES(revenue, 2)[OFFSET(1)] AS median FROM events;

Indexing into an array: OFFSET counts from zero, ORDINAL from one.

STRUCT
SELECT STRUCT(city AS city, revenue AS amount) AS order_info
FROM events;

A nested record: several named fields inside one column.

Struct field access
SELECT session.source, session.minutes
FROM events
WHERE session.minutes > 5;

Record fields are addressed with a dot — in SELECT, WHERE and GROUP BY alike.

ARRAY_AGG(STRUCT(…))
SELECT user_id,
       ARRAY_AGG(STRUCT(event_name AS name, event_ts AS ts) ORDER BY event_ts) AS events
FROM events GROUP BY user_id;

An array of records: a whole group history in one column, structure intact.

Wildcard table
SELECT user_id, minutes
FROM `sessions_d*`;

Reads every table matching the prefix as one. Replaces a chain of UNION ALL.

_TABLE_SUFFIX
SELECT _TABLE_SUFFIX AS day, COUNT(*) AS sessions
FROM `sessions_d*`
WHERE _TABLE_SUFFIX BETWEEN 'd20260314' AND 'd20260316'
GROUP BY day;

A pseudo-column holding the part of the table name the wildcard matched — used to group and to prune shards.

PARTITION BY / CLUSTER BY
CREATE TABLE sessions (d DATE, user_id INT64)
PARTITION BY d
CLUSTER BY user_id;

Partitioning splits a table by date, clustering orders data inside it — together they cut the bytes scanned.

DATE_TRUNC (GoogleSQL)
SELECT DATE_TRUNC(DATE(event_ts), MONTH) AS month FROM events;

Truncates a date. The arguments are the other way round: date first, unit second — and unquoted.

TIMESTAMP_DIFF / DATE_DIFF
SELECT TIMESTAMP_DIFF(MAX(event_ts), MIN(event_ts), MINUTE) AS minutes
FROM events;

The gap between two instants in a chosen unit. Subtracting timestamps directly is not allowed.

FORMAT_TIMESTAMP
SELECT FORMAT_TIMESTAMP("%A", event_ts) AS day_name FROM events;

Formats an instant into a string by pattern. The counterpart of PostgreSQL TO_CHAR.

GENERATE_DATE_ARRAY
SELECT day
FROM UNNEST(GENERATE_DATE_ARRAY(DATE "2026-03-14", DATE "2026-03-16")) AS day;

A gapless date series — a calendar to join data onto. The counterpart of generate_series.

EXTRACT (GoogleSQL)
SELECT EXTRACT(HOUR FROM event_ts) AS hour FROM events;

Pulls a part out of an instant: HOUR, DAYOFWEEK, WEEK, MONTH, YEAR and more.

APPROX_COUNT_DISTINCT
SELECT platform, APPROX_COUNT_DISTINCT(user_id) AS users
FROM events GROUP BY platform;

An approximate distinct count. Cheaper than exact COUNT(DISTINCT) at scale, at the cost of small error.

APPROX_QUANTILES
SELECT APPROX_QUANTILES(revenue, 2)[OFFSET(1)] AS median FROM events;

Approximate quantiles: splits the sample into N buckets and returns the boundaries as an array.

ANY_VALUE
SELECT user_id, ANY_VALUE(city) AS city, COUNT(*) AS events
FROM events GROUP BY user_id;

Takes an arbitrary value from the group — for a column that is constant within it and needs no GROUP BY entry.

PIVOT
SELECT * FROM (SELECT city, platform FROM events)
PIVOT(COUNT(*) FOR platform IN ('ios', 'android', 'web'));

Turns column values into columns of their own. The value list is spelled out explicitly.

UNPIVOT
SELECT * FROM wide_table
UNPIVOT(value FOR metric IN (dau, wau, mau));

The inverse: several columns collapse into name/value pairs.

GROUP BY ROLLUP
SELECT city, platform, COUNT(*) AS cnt
FROM events
GROUP BY ROLLUP(city, platform);

Adds subtotals and a grand total: in the extra rows the grouped columns come back as NULL.

STRING_AGG (GoogleSQL)
SELECT platform, STRING_AGG(DISTINCT city, ", " ORDER BY city) AS cities
FROM events GROUP BY platform;

Joins a group into one string. Accepts DISTINCT and ORDER BY inside the call.

Bytes scanned
-- SELECT * reads every column;
-- naming columns reads only those.
SELECT city, revenue FROM events;

BigQuery bills bytes scanned, not rows: with columnar storage SELECT * is the most expensive way to ask.

Partition pruning
SELECT COUNT(*) FROM sessions
WHERE d BETWEEN DATE "2026-03-14" AND DATE "2026-03-16";

A predicate on the partitioning column drops whole partitions before reading — the main saving there is.