SQL Cheatsheet

For PostgreSQL

Jump to

PostgresGUI

SELECT

Retrieve data from tables

Select All Columns

Retrieve all columns from a table

SELECT * FROM users;

Select Specific Columns

Retrieve only the columns you need

SELECT name, email, created_at
FROM users;

Select Distinct Values

Remove duplicate rows from results

SELECT DISTINCT country
FROM users;

Column Aliases

Rename columns in output using AS

SELECT
first_name AS "First Name",
last_name AS "Last Name",
email AS "Email Address"
FROM users;

Concatenate Columns

Combine multiple columns into one

SELECT
first_name || ' ' || last_name AS full_name,
email
FROM users;

CASE Expression

Conditional logic in SELECT

SELECT
name,
price,
CASE
WHEN price < 10 THEN 'Budget'
WHEN price < 50 THEN 'Standard'
ELSE 'Premium'
END AS price_tier
FROM products;

Handle NULL Values

Return first non-null value

SELECT
name,
COALESCE(nickname, name) AS display_name,
COALESCE(phone, 'No phone') AS phone
FROM users;

NULLIF Function

Return NULL if two values are equal

SELECT
name,
NULLIF(status, 'inactive') AS active_status
FROM users;

WHERE

Filter rows with conditions

Equal Comparison

Filter rows matching exact value

SELECT * FROM users
WHERE status = 'active';

Comparison Operators

Use >, <, >=, <=, <> for comparisons

SELECT * FROM products
WHERE price >= 100
AND price <= 500;

BETWEEN Range

Filter values within a range (inclusive)

SELECT * FROM orders
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';

IN List

Match any value in a list

SELECT * FROM users
WHERE country IN ('USA', 'Canada', 'UK');

LIKE Pattern Matching

Match patterns with wildcards

-- % matches any sequence
SELECT * FROM users WHERE email LIKE '%@gmail.com';
 
-- _ matches single character
SELECT * FROM products WHERE sku LIKE 'ABC-___';

ILIKE (Case Insensitive)

Pattern matching ignoring case

SELECT * FROM users
WHERE name ILIKE '%john%';

NULL Checks

Check for NULL or NOT NULL

-- Find rows with NULL
SELECT * FROM users WHERE phone IS NULL;
 
-- Find rows without NULL
SELECT * FROM users WHERE phone IS NOT NULL;

AND / OR Logic

Combine multiple conditions

SELECT * FROM users
WHERE (status = 'active' OR status = 'pending')
AND created_at > '2024-01-01';

NOT Operator

Negate conditions

SELECT * FROM users
WHERE NOT (status = 'deleted' OR status = 'banned');

EXISTS Subquery

Check if subquery returns rows

SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);

ANY / SOME Operator

Compare against any subquery value

-- Equal to any value
SELECT * FROM products
WHERE price = ANY(SELECT price FROM featured_products);
 
-- Greater than any (same as > MIN)
SELECT * FROM products
WHERE price > ANY(SELECT price FROM budget_products);

ALL Operator

Compare against all subquery values

-- Greater than all values
SELECT * FROM products
WHERE price > ALL(SELECT price FROM budget_products);
 
-- Not equal to all (exclude all matches)
SELECT * FROM users
WHERE id <> ALL(SELECT user_id FROM banned_users);

ORDER & LIMIT

Sort and paginate results

Sort Ascending

Sort results A-Z, smallest to largest

SELECT * FROM users
ORDER BY created_at ASC;

Sort Descending

Sort results Z-A, largest to smallest

SELECT * FROM products
ORDER BY price DESC;

Multi-Column Sort

Sort by multiple columns

SELECT * FROM users
ORDER BY country ASC, created_at DESC;

NULL Ordering

Control where NULLs appear

SELECT * FROM users
ORDER BY phone NULLS LAST;
 
-- Or put NULLs first
SELECT * FROM users
ORDER BY phone NULLS FIRST;

LIMIT Results

Return only first N rows

SELECT * FROM products
ORDER BY created_at DESC
LIMIT 10;

OFFSET for Pagination

Skip rows for pagination

-- Page 1 (rows 1-10)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 0;
 
-- Page 2 (rows 11-20)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 10;
 
-- Page 3 (rows 21-30)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;

FETCH FIRST (SQL Standard)

Standard SQL alternative to LIMIT

SELECT * FROM products
ORDER BY price DESC
FETCH FIRST 5 ROWS ONLY;

Aggregates

COUNT, SUM, AVG, MIN, MAX

COUNT All Rows

Count total number of rows

SELECT COUNT(*) AS total_users
FROM users;

COUNT Distinct

Count unique values

SELECT COUNT(DISTINCT country) AS countries
FROM users;

SUM Values

Calculate total of numeric column

SELECT SUM(amount) AS total_revenue
FROM orders
WHERE status = 'completed';

AVG Average

Calculate average of numeric column

SELECT AVG(price) AS average_price
FROM products;

MIN and MAX

Find smallest and largest values

SELECT
MIN(price) AS cheapest,
MAX(price) AS most_expensive
FROM products;

Aggregate with FILTER

Apply conditions to aggregates

SELECT
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE status = 'completed') AS completed,
COUNT(*) FILTER (WHERE status = 'pending') AS pending
FROM orders;

GROUP BY

Group rows and aggregate

Basic Grouping

Group rows by column value

SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country;

Group by Multiple Columns

Create groups from column combinations

SELECT
country,
status,
COUNT(*) AS user_count
FROM users
GROUP BY country, status;

HAVING Filter

Filter groups after aggregation

SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

ROLLUP Subtotals

Generate subtotals and grand total

SELECT
country,
city,
COUNT(*) AS user_count
FROM users
GROUP BY ROLLUP(country, city);

CUBE All Combinations

Generate all grouping combinations

SELECT
country,
status,
COUNT(*) AS user_count
FROM users
GROUP BY CUBE(country, status);

JOINs

Combine data from multiple tables

INNER JOIN

Return matching rows from both tables

SELECT
users.name,
orders.id AS order_id,
orders.total
FROM users
INNER JOIN orders ON users.id = orders.user_id;

LEFT JOIN

All rows from left, matching from right

SELECT
users.name,
orders.id AS order_id
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

RIGHT JOIN

Matching from left, all from right

SELECT
users.name,
orders.id AS order_id
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;

FULL OUTER JOIN

All rows from both tables

SELECT
users.name,
orders.id AS order_id
FROM users
FULL OUTER JOIN orders ON users.id = orders.user_id;

CROSS JOIN

Cartesian product of all rows

SELECT
colors.name AS color,
sizes.name AS size
FROM colors
CROSS JOIN sizes;

Self JOIN

Join table to itself

SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Multiple JOINs

Join more than two tables

SELECT
users.name,
orders.id AS order_id,
products.name AS product
FROM users
INNER JOIN orders ON users.id = orders.user_id
INNER JOIN order_items ON orders.id = order_items.order_id
INNER JOIN products ON order_items.product_id = products.id;

JOIN with USING

Shorthand when column names match

SELECT users.name, orders.total
FROM users
INNER JOIN orders USING (user_id);

NATURAL JOIN

Auto-join on matching column names

-- Joins on all columns with same name
SELECT * FROM orders
NATURAL JOIN order_items;
 
-- Be careful: joins on ALL matching names
-- Prefer explicit JOIN conditions

Subqueries

Nested queries within queries

Subquery in WHERE

Use query result as filter condition

SELECT * FROM products
WHERE price > (
SELECT AVG(price) FROM products
);

Subquery with IN

Match against subquery results

SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders
WHERE total > 1000
);

Subquery in FROM

Use subquery as a derived table

SELECT
country,
avg_order
FROM (
SELECT
u.country,
AVG(o.total) AS avg_order
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.country
) AS country_stats
WHERE avg_order > 500;

Scalar Subquery in SELECT

Return single value in column

SELECT
name,
price,
price - (SELECT AVG(price) FROM products) AS diff_from_avg
FROM products;

Correlated Subquery

Subquery referencing outer query

SELECT * FROM products p
WHERE price > (
SELECT AVG(price)
FROM products
WHERE category_id = p.category_id
);

LATERAL Join

Subquery can reference preceding tables

SELECT
u.name,
recent.order_date,
recent.total
FROM users u
CROSS JOIN LATERAL (
SELECT order_date, total
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
LIMIT 3
) AS recent;

NOT EXISTS

Check subquery returns no rows

-- Users with no orders
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);

CTEs

Common Table Expressions (WITH)

Basic CTE

Define a named subquery

WITH active_users AS (
SELECT * FROM users
WHERE status = 'active'
)
SELECT * FROM active_users
WHERE created_at > '2024-01-01';

Multiple CTEs

Chain multiple CTEs together

WITH
active_users AS (
SELECT * FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT
u.name,
COALESCE(o.order_count, 0) AS orders
FROM active_users u
LEFT JOIN user_orders o ON u.id = o.user_id;

Recursive CTE

Query hierarchical or tree data

WITH RECURSIVE org_chart AS (
-- Base case: top-level (no manager)
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
 
UNION ALL
 
-- Recursive case
SELECT e.id, e.name, e.manager_id, oc.level + 1
FROM employees e
INNER JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY level, name;

Generate Number Series

Create sequence with recursive CTE

WITH RECURSIVE numbers AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT * FROM numbers;

Window Functions

ROW_NUMBER, RANK, LAG, LEAD

ROW_NUMBER

Assign sequential numbers to rows

SELECT
name,
price,
ROW_NUMBER() OVER (ORDER BY price DESC) AS rank
FROM products;

RANK vs DENSE_RANK

Handle ties differently

SELECT
name,
price,
RANK() OVER (ORDER BY price DESC) AS rank,
DENSE_RANK() OVER (ORDER BY price DESC) AS dense_rank
FROM products;

PARTITION BY

Window function per group

SELECT
category,
name,
price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) AS category_rank
FROM products;

LAG and LEAD

Access previous/next row values

SELECT
order_date,
total,
LAG(total) OVER (ORDER BY order_date) AS prev_total,
LEAD(total) OVER (ORDER BY order_date) AS next_total
FROM orders;

Running Total

Cumulative sum with SUM window

SELECT
order_date,
total,
SUM(total) OVER (ORDER BY order_date) AS running_total
FROM orders;

Moving Average

Average over sliding window

SELECT
order_date,
total,
AVG(total) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7day
FROM orders;

FIRST_VALUE / LAST_VALUE

Get first or last value in window

SELECT
name,
price,
FIRST_VALUE(name) OVER (ORDER BY price DESC) AS most_expensive,
LAST_VALUE(name) OVER (
ORDER BY price DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS cheapest
FROM products;

NTILE Buckets

Divide rows into N equal groups

SELECT
name,
price,
NTILE(4) OVER (ORDER BY price) AS quartile
FROM products;

String Functions

Text manipulation functions

CONCAT / ||

Join strings together

SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
first_name || ' ' || last_name AS full_name_alt
FROM users;

UPPER / LOWER

Change case

SELECT
UPPER(name) AS uppercase,
LOWER(email) AS lowercase,
INITCAP(name) AS titlecase
FROM users;

SUBSTRING

Extract part of string

SELECT
SUBSTRING(phone FROM 1 FOR 3) AS area_code,
SUBSTRING(email FROM '@(.+)$') AS domain
FROM users;

TRIM / LTRIM / RTRIM

Remove whitespace or characters

SELECT
TRIM(' hello ') AS trimmed,
LTRIM(' hello ') AS left_trimmed,
RTRIM(' hello ') AS right_trimmed,
TRIM(BOTH '-' FROM '--hello--') AS custom_trim
FROM users;

REPLACE

Replace substring with another

SELECT
REPLACE(phone, '-', '') AS phone_digits,
REPLACE(name, 'Mr. ', '') AS name_no_title
FROM users;

SPLIT_PART

Split string and get part

SELECT
email,
SPLIT_PART(email, '@', 1) AS username,
SPLIT_PART(email, '@', 2) AS domain
FROM users;

LENGTH / CHAR_LENGTH

Get string length

SELECT
name,
LENGTH(name) AS name_length,
CHAR_LENGTH(name) AS char_count
FROM users;

POSITION / STRPOS

Find substring position

SELECT
email,
POSITION('@' IN email) AS at_position,
STRPOS(email, '@') AS at_position_alt
FROM users;

Date & Time

Date and time functions

Current Date/Time

Get current timestamp

SELECT
CURRENT_DATE AS today,
CURRENT_TIME AS now_time,
CURRENT_TIMESTAMP AS now_full,
NOW() AS now_func;

EXTRACT Parts

Get specific date/time parts

SELECT
created_at,
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(MONTH FROM created_at) AS month,
EXTRACT(DAY FROM created_at) AS day,
EXTRACT(HOUR FROM created_at) AS hour
FROM orders;

DATE_TRUNC

Truncate to specified precision

SELECT
created_at,
DATE_TRUNC('month', created_at) AS month_start,
DATE_TRUNC('year', created_at) AS year_start,
DATE_TRUNC('hour', created_at) AS hour_start
FROM orders;

Date Arithmetic

Add or subtract intervals

SELECT
NOW() AS now,
NOW() + INTERVAL '1 day' AS tomorrow,
NOW() - INTERVAL '1 week' AS last_week,
NOW() + INTERVAL '3 months' AS three_months
FROM orders;

AGE Function

Calculate difference between dates

SELECT
name,
birth_date,
AGE(birth_date) AS age,
AGE(NOW(), hire_date) AS tenure
FROM employees;

TO_CHAR Formatting

Format date as string

SELECT
created_at,
TO_CHAR(created_at, 'YYYY-MM-DD') AS iso_date,
TO_CHAR(created_at, 'Mon DD, YYYY') AS pretty_date,
TO_CHAR(created_at, 'HH24:MI:SS') AS time_24h
FROM orders;

Generate Date Series

Create sequence of dates

SELECT date::date
FROM generate_series(
'2024-01-01'::date,
'2024-12-31'::date,
'1 month'::interval
) AS date;

JSON

JSON data manipulation

Access JSON Fields

Use -> and ->> operators

SELECT
data->'name' AS name_json, -- returns JSON
data->>'name' AS name_text, -- returns text
data->'address'->>'city' AS city -- nested access
FROM users;

JSON Path

Use #> and #>> for path access

SELECT
data #> '{address,city}' AS city_json,
data #>> '{address,city}' AS city_text,
data #>> '{tags,0}' AS first_tag
FROM users;

JSONB Containment

Check if JSON contains value

-- Contains this JSON?
SELECT * FROM products
WHERE data @> '{"status": "active"}';
 
-- Is contained by?
SELECT * FROM products
WHERE '{"status": "active", "type": "book"}' @> data;

Build JSON Objects

Create JSON from columns

SELECT
json_build_object(
'id', id,
'name', name,
'email', email
) AS user_json
FROM users;

JSON Aggregation

Aggregate rows into JSON array

SELECT
category,
json_agg(name) AS product_names,
json_agg(json_build_object('name', name, 'price', price)) AS products
FROM products
GROUP BY category;

JSONB_SET Update

Update nested JSON value

UPDATE users
SET data = jsonb_set(
data,
'{address,city}',
'"New York"'
)
WHERE id = 1;

INSERT / UPDATE / DELETE

Modify data in tables

Basic INSERT

Insert a single row

INSERT INTO users (name, email, status)
VALUES ('John Doe', 'john@example.com', 'active');

Insert Multiple Rows

Insert several rows at once

INSERT INTO users (name, email)
VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Carol', 'carol@example.com');

INSERT with RETURNING

Get inserted data back

INSERT INTO users (name, email)
VALUES ('John', 'john@example.com')
RETURNING id, name, created_at;

INSERT from SELECT

Insert rows from a query

INSERT INTO users_archive (name, email)
SELECT name, email
FROM users
WHERE status = 'inactive';

UPSERT (ON CONFLICT)

Insert or update if exists

INSERT INTO users (id, name, email)
VALUES (1, 'John', 'john@example.com')
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name,
email = EXCLUDED.email;

INSERT or Ignore

Skip if already exists

INSERT INTO users (email, name)
VALUES ('john@example.com', 'John')
ON CONFLICT (email) DO NOTHING;

Basic UPDATE

Update rows matching condition

UPDATE users
SET status = 'inactive',
updated_at = NOW()
WHERE last_login < '2024-01-01';

UPDATE with RETURNING

Get updated rows back

UPDATE products
SET price = price * 1.10
WHERE category = 'electronics'
RETURNING id, name, price;

UPDATE with FROM

Update using another table

UPDATE orders
SET status = 'shipped'
FROM shipments
WHERE orders.id = shipments.order_id
AND shipments.shipped_at IS NOT NULL;

Basic DELETE

Delete rows matching condition

DELETE FROM users
WHERE status = 'deleted'
AND updated_at < NOW() - INTERVAL '30 days';

DELETE with RETURNING

Get deleted rows back

DELETE FROM sessions
WHERE expires_at < NOW()
RETURNING id, user_id;

TRUNCATE Table

Delete all rows quickly

-- Fast delete all rows (resets sequences)
TRUNCATE TABLE logs;
 
-- Truncate multiple tables
TRUNCATE TABLE orders, order_items
RESTART IDENTITY CASCADE;

Tables & DDL

CREATE, ALTER, DROP structures

CREATE TABLE

Create a new table

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
);

CREATE IF NOT EXISTS

Create table only if missing

CREATE TABLE IF NOT EXISTS logs (
id SERIAL PRIMARY KEY,
message TEXT,
level VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE AS

Create table from query

CREATE TABLE active_users AS
SELECT id, name, email
FROM users
WHERE status = 'active';

Add Column

Add a new column to table

ALTER TABLE users
ADD COLUMN phone VARCHAR(20);
 
-- With default value
ALTER TABLE users
ADD COLUMN is_verified BOOLEAN DEFAULT false;

Drop Column

Remove a column from table

ALTER TABLE users
DROP COLUMN phone;
 
-- Drop if exists
ALTER TABLE users
DROP COLUMN IF EXISTS temp_field;

Rename Column / Table

Rename columns or tables

-- Rename column
ALTER TABLE users
RENAME COLUMN name TO full_name;
 
-- Rename table
ALTER TABLE users
RENAME TO customers;

Change Column Type

Modify column data type

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(10,2);
 
-- With conversion
ALTER TABLE users
ALTER COLUMN age TYPE INTEGER
USING age::INTEGER;

Set / Drop Default

Change column default value

-- Set default
ALTER TABLE users
ALTER COLUMN status SET DEFAULT 'pending';
 
-- Remove default
ALTER TABLE users
ALTER COLUMN status DROP DEFAULT;

DROP TABLE

Delete a table

DROP TABLE users;
 
-- Drop if exists
DROP TABLE IF EXISTS temp_users;
 
-- Drop with dependent objects
DROP TABLE orders CASCADE;

CREATE INDEX

Create index for faster queries

-- Basic index
CREATE INDEX idx_users_email ON users(email);
 
-- Unique index
CREATE UNIQUE INDEX idx_users_username ON users(username);
 
-- Multi-column index
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

DROP INDEX

Remove an index

DROP INDEX idx_users_email;
 
DROP INDEX IF EXISTS idx_temp;

CREATE VIEW

Create a virtual table

CREATE VIEW active_orders AS
SELECT o.id, u.name, o.total, o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'active';
 
-- Then query like a table
SELECT * FROM active_orders;

Constraints

PRIMARY KEY, FOREIGN KEY, UNIQUE

PRIMARY KEY

Unique identifier for rows

-- Inline definition
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
);
 
-- Named constraint
CREATE TABLE products (
id INTEGER,
sku VARCHAR(50),
CONSTRAINT pk_products PRIMARY KEY (id)
);

FOREIGN KEY

Reference another table

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10,2)
);
 
-- With ON DELETE action
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES posts(id)
ON DELETE CASCADE,
content TEXT
);

UNIQUE Constraint

Prevent duplicate values

-- Single column
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE
);
 
-- Multiple columns
CREATE TABLE memberships (
user_id INTEGER,
group_id INTEGER,
UNIQUE (user_id, group_id)
);

NOT NULL

Require a value

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL
);

CHECK Constraint

Validate column values

CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2) CHECK (price > 0),
quantity INTEGER CHECK (quantity >= 0)
);
 
-- Named check
CREATE TABLE users (
age INTEGER,
CONSTRAINT valid_age CHECK (age >= 0 AND age < 150)
);

DEFAULT Value

Set automatic default

CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
status VARCHAR(20) DEFAULT 'draft',
views INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);

Add Constraint to Existing Table

Add constraints with ALTER

-- Add foreign key
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id);
 
-- Add unique constraint
ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email);
 
-- Add check constraint
ALTER TABLE products
ADD CONSTRAINT positive_price CHECK (price > 0);

Drop Constraint

Remove a constraint

ALTER TABLE orders
DROP CONSTRAINT fk_orders_user;
 
ALTER TABLE users
DROP CONSTRAINT IF EXISTS unique_email;

Set Operations

UNION, INTERSECT, EXCEPT

UNION

Combine results, remove duplicates

SELECT name, email FROM customers
UNION
SELECT name, email FROM suppliers;

UNION ALL

Combine results, keep duplicates

SELECT product_id, 'sale' AS type FROM sales
UNION ALL
SELECT product_id, 'return' AS type FROM returns;

INTERSECT

Rows in both queries

SELECT user_id FROM premium_users
INTERSECT
SELECT user_id FROM active_users;

EXCEPT

Rows in first but not second

SELECT user_id FROM all_users
EXCEPT
SELECT user_id FROM banned_users;

ORDER with Set Operations

Sort combined results

(SELECT name, 'customer' AS type FROM customers)
UNION
(SELECT name, 'supplier' AS type FROM suppliers)
ORDER BY name;

Type Casting

Convert between data types

:: Operator

PostgreSQL cast shorthand

SELECT
'123'::INTEGER AS num,
'2024-01-15'::DATE AS date,
123::TEXT AS str,
'3.14'::NUMERIC AS decimal;

CAST Function

Standard SQL casting

SELECT
CAST('123' AS INTEGER) AS num,
CAST('2024-01-15' AS DATE) AS date,
CAST(price AS INTEGER) AS rounded
FROM products;

Numeric Conversions

Convert between number types

SELECT
1234.567::INTEGER AS truncated, -- 1234
1234.567::NUMERIC(10,1) AS rounded, -- 1234.6
'99.99'::DECIMAL(5,2) AS decimal;

Date/Time Conversions

Convert date and time types

SELECT
NOW()::DATE AS today,
NOW()::TIME AS current_time,
'2024-01-15 10:30:00'::TIMESTAMP AS ts,
EXTRACT(EPOCH FROM NOW())::INTEGER AS unix;

Boolean Conversions

Convert to/from boolean

SELECT
'true'::BOOLEAN,
'yes'::BOOLEAN,
'1'::BOOLEAN,
1::BOOLEAN,
true::INTEGER; -- 1

JSON Conversions

Convert to/from JSON

SELECT
'{"name": "John"}'::JSON,
'{"name": "John"}'::JSONB,
row_to_json(users.*) FROM users;

Numeric Functions

ROUND, ABS, CEIL, FLOOR

ROUND

Round to decimal places

SELECT
ROUND(3.14159) AS whole, -- 3
ROUND(3.14159, 2) AS two_dec, -- 3.14
ROUND(1234.5, -2) AS hundreds; -- 1200

CEIL / FLOOR

Round up or down

SELECT
CEIL(4.2) AS ceiling, -- 5
FLOOR(4.8) AS floor, -- 4
CEIL(-4.2) AS neg_ceil, -- -4
FLOOR(-4.8) AS neg_fl; -- -5

TRUNC

Truncate toward zero

SELECT
TRUNC(4.9) AS truncated, -- 4
TRUNC(-4.9) AS neg_trunc, -- -4
TRUNC(123.456, 1) AS one_dec; -- 123.4

ABS

Absolute value

SELECT
ABS(-5) AS positive, -- 5
ABS(5) AS still_pos, -- 5
ABS(price - 100) AS diff
FROM products;

POWER / SQRT

Exponents and square root

SELECT
POWER(2, 3) AS eight, -- 8
POWER(10, 2) AS hundred, -- 100
SQRT(16) AS four, -- 4
SQRT(2) AS root2; -- 1.414...

MOD / %

Remainder after division

SELECT
MOD(10, 3) AS remainder, -- 1
17 % 5 AS mod_operator, -- 2
MOD(id, 2) AS is_odd
FROM users;

RANDOM

Generate random numbers

SELECT
RANDOM() AS zero_to_one,
FLOOR(RANDOM() * 100) AS zero_to_99,
FLOOR(RANDOM() * (max - min + 1) + min) AS range;
 
-- Random row
SELECT * FROM users
ORDER BY RANDOM()
LIMIT 1;

SIGN / DIV

Sign and integer division

SELECT
SIGN(-5) AS negative, -- -1
SIGN(0) AS zero, -- 0
SIGN(5) AS positive, -- 1
DIV(17, 5) AS int_div; -- 3

Arrays

PostgreSQL array operations

Array Literals

Create array values

SELECT
ARRAY[1, 2, 3] AS int_array,
ARRAY['a', 'b', 'c'] AS text_array,
'{1,2,3}'::INTEGER[] AS cast_array;

Access Array Elements

Get elements by index (1-based)

SELECT
(ARRAY['a','b','c'])[1] AS first, -- 'a'
(ARRAY['a','b','c'])[2:3] AS slice, -- {'b','c'}
tags[1] AS first_tag
FROM posts;

ANY / ALL with Arrays

Compare against array values

-- Match any value in array
SELECT * FROM users
WHERE status = ANY(ARRAY['active', 'pending']);
 
-- Match all values
SELECT * FROM products
WHERE price > ALL(ARRAY[10, 20, 30]);

Array Contains

Check array containment

-- Contains all elements
SELECT * FROM posts
WHERE tags @> ARRAY['sql', 'database'];
 
-- Is contained by
SELECT * FROM posts
WHERE tags <@ ARRAY['sql', 'database', 'postgresql'];
 
-- Overlaps (has common elements)
SELECT * FROM posts
WHERE tags && ARRAY['sql', 'nosql'];

ARRAY_AGG

Aggregate values into array

SELECT
category,
ARRAY_AGG(name) AS product_names,
ARRAY_AGG(DISTINCT status) AS statuses
FROM products
GROUP BY category;

UNNEST

Expand array to rows

SELECT UNNEST(ARRAY[1, 2, 3]) AS num;
 
-- Expand column array
SELECT id, UNNEST(tags) AS tag
FROM posts;

Array Functions

Common array operations

SELECT
ARRAY_LENGTH(ARRAY[1,2,3], 1) AS len, -- 3
ARRAY_CAT(ARRAY[1,2], ARRAY[3,4]) AS concat, -- {1,2,3,4}
ARRAY_APPEND(ARRAY[1,2], 3) AS append, -- {1,2,3}
ARRAY_REMOVE(ARRAY[1,2,3], 2) AS removed; -- {1,3}

Array Search

Find elements in arrays

SELECT
ARRAY_POSITION(ARRAY['a','b','c'], 'b') AS pos, -- 2
'b' = ANY(ARRAY['a','b','c']) AS exists, -- true
CARDINALITY(ARRAY[1,2,3]) AS size; -- 3