datarekha

SQL cheat sheet

SELECT to window functions — joins, aggregation, CTEs, and the query patterns analysts use daily.

Logical Execution Order

SQL clauses are written in one order but evaluated in another. Knowing this prevents most alias and filter bugs.

-- Written order vs. evaluation order
-- 1. FROM + JOINs          -- build the working rowset
-- 2. WHERE                 -- filter rows (no aggregates yet)
-- 3. GROUP BY              -- bucket rows
-- 4. HAVING                -- filter buckets (aggregates allowed)
-- 5. SELECT                -- compute output columns + aliases
-- 6. DISTINCT              -- remove duplicates
-- 7. ORDER BY              -- sort (aliases allowed here)
-- 8. LIMIT / OFFSET        -- slice result

SELECT / WHERE / ORDER BY / LIMIT

-- Basic shape
SELECT
    col1,
    col2,
    col1 * 2          AS doubled,
    UPPER(col3)       AS upper_col
FROM   schema.table
WHERE  status = 'active'
  AND  created_at >= '2024-01-01'
ORDER BY created_at DESC
LIMIT  100 OFFSET 200;   -- OFFSET skips rows; use for pagination

-- DISTINCT
SELECT DISTINCT department FROM employees;

-- Aliases do NOT exist in WHERE (evaluated before SELECT)
-- Use a subquery or CTE when you need to filter on a computed alias
SELECT * FROM (
    SELECT id, salary * 1.1 AS adj FROM employees
) sub
WHERE adj > 60000;

Operators and Filters

-- Comparison
WHERE age BETWEEN 25 AND 40          -- inclusive on both ends
WHERE country IN ('US', 'CA', 'MX')
WHERE country NOT IN ('XX', 'YY')
WHERE name LIKE 'J%'                 -- % = any sequence, _ = one char
WHERE name ILIKE 'j%'                -- case-insensitive; Postgres only
WHERE email IS NULL
WHERE email IS NOT NULL

-- Pattern tips
WHERE code LIKE 'A_C%'   -- starts with A, any char, then C, then anything

-- Boolean
WHERE is_active = TRUE AND (role = 'admin' OR level >= 5)

-- Regex (Postgres)
WHERE name ~ '^[A-Z]'    -- case-sensitive match
WHERE name ~* '^[a-z]'   -- case-insensitive match

-- ANY / ALL
WHERE salary > ANY (SELECT salary FROM managers)
WHERE salary > ALL (SELECT salary FROM directors)

-- EXISTS (stops at first match — often faster than IN on large sets)
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
)

JOINs

-- INNER JOIN — only matching rows on both sides
SELECT e.name, d.name AS dept
FROM   employees e
JOIN   departments d ON e.dept_id = d.id;

-- LEFT JOIN — all left rows; NULL for unmatched right columns
SELECT e.name, o.order_id
FROM   customers e
LEFT JOIN orders o ON o.customer_id = e.id;

-- Find customers with NO orders (anti-join pattern)
SELECT e.name
FROM   customers e
LEFT JOIN orders o ON o.customer_id = e.id
WHERE  o.order_id IS NULL;

-- RIGHT JOIN — all right rows; NULL for unmatched left columns
-- (usually rewrite as LEFT JOIN with tables swapped — cleaner)
SELECT o.order_id, e.name
FROM   orders o
RIGHT JOIN customers e ON o.customer_id = e.id;

-- FULL OUTER JOIN — all rows from both sides; NULL where no match
SELECT a.id, b.id
FROM   table_a a
FULL JOIN table_b b ON a.key = b.key;

-- CROSS JOIN — every combination (Cartesian product); rows_a * rows_b
SELECT a.size, b.color
FROM   sizes a
CROSS JOIN colors b;

-- SELF JOIN — join a table to itself; requires distinct aliases
SELECT e.name AS employee, m.name AS manager
FROM   employees e
JOIN   employees m ON e.manager_id = m.id;

-- LATERAL / CROSS APPLY (Postgres LATERAL, SQL Server CROSS APPLY)
-- Right side can reference left-side columns
SELECT c.name, recent.order_date
FROM   customers c
CROSS JOIN LATERAL (
    SELECT order_date FROM orders o
    WHERE o.customer_id = c.id
    ORDER BY order_date DESC
    LIMIT 1
) recent;

GROUP BY, HAVING, and Aggregates

-- Core aggregates
SELECT
    dept_id,
    COUNT(*)                     AS headcount,     -- includes NULLs in count of rows
    COUNT(bonus)                 AS has_bonus,     -- NULLs excluded
    COUNT(DISTINCT job_title)    AS unique_titles,
    SUM(salary)                  AS total_salary,
    AVG(salary)                  AS avg_salary,
    MIN(salary)                  AS min_salary,
    MAX(salary)                  AS max_salary
FROM   employees
GROUP BY dept_id;

-- HAVING filters after aggregation (WHERE runs before)
SELECT dept_id, COUNT(*) AS cnt
FROM   employees
GROUP BY dept_id
HAVING COUNT(*) > 10
ORDER BY cnt DESC;

-- GROUP BY ALL columns not aggregated (or use ordinal)
SELECT year, month, SUM(revenue)
FROM   sales
GROUP BY year, month;       -- explicit (preferred)
-- GROUP BY 1, 2;            -- ordinal shorthand (fragile, avoid in production)

-- ROLLUP — subtotals + grand total (ANSI SQL)
SELECT dept, job, SUM(salary)
FROM   employees
GROUP BY ROLLUP(dept, job);

-- CUBE — all combinations of subtotals
GROUP BY CUBE(dept, job, year);

-- GROUPING SETS — specify exactly which groups you want
GROUP BY GROUPING SETS ((dept), (job), ());

Subqueries

-- Scalar subquery in SELECT (must return exactly one row/col)
SELECT
    name,
    salary,
    (SELECT AVG(salary) FROM employees) AS company_avg
FROM employees;

-- Correlated subquery — references outer query row by row (can be slow)
SELECT e.name, e.salary
FROM   employees e
WHERE  e.salary > (
    SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id
);

-- Derived table (inline view) in FROM
SELECT dept_id, avg_sal
FROM (
    SELECT dept_id, AVG(salary) AS avg_sal
    FROM   employees
    GROUP BY dept_id
) dept_avgs
WHERE avg_sal > 70000;

-- Subquery with IN
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE amount > 1000);

CTEs (Common Table Expressions)

-- Basic CTE — readable alternative to derived tables
WITH high_earners AS (
    SELECT id, name, salary
    FROM   employees
    WHERE  salary > 100000
),
dept_counts AS (
    SELECT dept_id, COUNT(*) AS cnt
    FROM   employees
    GROUP BY dept_id
)
SELECT h.name, h.salary, d.cnt
FROM   high_earners h
JOIN   dept_counts d ON h.dept_id = d.dept_id;

-- Multiple CTEs are separated by commas, one WITH keyword only

-- Recursive CTE — traverses hierarchies / generates series
-- Postgres, SQL Server, SQLite, MySQL 8+
WITH RECURSIVE org_chart AS (
    -- Anchor: top-level (no manager)
    SELECT id, name, manager_id, 1 AS depth
    FROM   employees
    WHERE  manager_id IS NULL

    UNION ALL

    -- Recursive member: join to previous level
    SELECT e.id, e.name, e.manager_id, oc.depth + 1
    FROM   employees e
    JOIN   org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY depth, name;

-- Generate a date series (Postgres)
WITH RECURSIVE dates AS (
    SELECT '2024-01-01'::date AS d
    UNION ALL
    SELECT d + 1 FROM dates WHERE d < '2024-01-31'
)
SELECT d FROM dates;

Window Functions

Window functions operate across rows related to the current row without collapsing the rowset (unlike GROUP BY).

-- Syntax template
function_name(expr) OVER (
    PARTITION BY col      -- optional: reset per group
    ORDER BY   col        -- required for ranking/running calcs
    ROWS/RANGE BETWEEN ... AND ...  -- optional frame
)

Ranking

SELECT
    name,
    dept_id,
    salary,
    ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
      -- unique sequential integer; no ties
    RANK()       OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk,
      -- ties get same rank; next rank skips (1,1,3)
    DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drnk,
      -- ties get same rank; no gaps (1,1,2)
    NTILE(4)     OVER (ORDER BY salary DESC)                      AS quartile
FROM employees;

Offset Functions

SELECT
    order_date,
    revenue,
    LAG(revenue, 1, 0)  OVER (ORDER BY order_date) AS prev_rev,
      -- value from N rows before; 3rd arg = default when no row exists
    LEAD(revenue, 1, 0) OVER (ORDER BY order_date) AS next_rev,
    FIRST_VALUE(revenue) OVER (ORDER BY order_date
                               ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS first_rev,
    LAST_VALUE(revenue)  OVER (ORDER BY order_date
                               ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS last_rev
FROM daily_sales;

Running Totals and Moving Averages

SELECT
    order_date,
    revenue,
    SUM(revenue) OVER (ORDER BY order_date
                        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
    AVG(revenue) OVER (ORDER BY order_date
                        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)          AS rolling_7d_avg,
    SUM(revenue) OVER (PARTITION BY EXTRACT(YEAR FROM order_date)
                        ORDER BY order_date
                        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)  AS ytd_total
FROM daily_sales;

Frame Clause Reference

-- ROWS  — physical row count (ignores ties)
-- RANGE — logical value range (groups ties together)

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW   -- cumulative
ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING           -- centered window
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING   -- suffix total
RANGE BETWEEN INTERVAL '7' DAY PRECEDING            -- date-aware (Postgres)
          AND CURRENT ROW

Distribution Functions

SELECT
    score,
    PERCENT_RANK() OVER (ORDER BY score) AS pct_rank,   -- 0..1
    CUME_DIST()    OVER (ORDER BY score) AS cum_dist     -- 0..1 inclusive
FROM results;

CASE Expressions

-- Searched CASE (most flexible)
SELECT
    name,
    salary,
    CASE
        WHEN salary >= 120000 THEN 'Senior'
        WHEN salary >= 80000  THEN 'Mid'
        ELSE                       'Junior'
    END AS band
FROM employees;

-- Simple CASE (equality only)
SELECT
    status,
    CASE status
        WHEN 'A' THEN 'Active'
        WHEN 'I' THEN 'Inactive'
        ELSE          'Unknown'
    END AS status_label
FROM accounts;

-- CASE inside aggregate (conditional count / pivot)
SELECT
    dept_id,
    COUNT(CASE WHEN gender = 'F' THEN 1 END) AS female_count,
    COUNT(CASE WHEN gender = 'M' THEN 1 END) AS male_count,
    AVG(CASE WHEN job_level >= 3 THEN salary END) AS senior_avg_sal
FROM employees
GROUP BY dept_id;

NULL Handling

-- NULLs propagate: NULL + 5 = NULL, NULL = NULL is UNKNOWN (not TRUE)
-- Always use IS NULL / IS NOT NULL, not = NULL

-- COALESCE — first non-NULL argument
SELECT COALESCE(nickname, first_name, 'Anonymous') AS display_name
FROM users;

-- NULLIF — returns NULL when two values are equal (avoids division by zero)
SELECT revenue / NULLIF(units_sold, 0) AS avg_price
FROM sales;

-- NULLIF in aggregates: treat sentinel values as NULL
SELECT AVG(NULLIF(score, -1)) FROM results;

-- NULL in aggregates: SUM/AVG/MIN/MAX ignore NULLs; COUNT(*) counts all rows
SELECT COUNT(*), COUNT(email) FROM users;  -- differ when email has NULLs

-- Postgres: use IS DISTINCT FROM to compare NULLs safely
WHERE a IS DISTINCT FROM b       -- true even when both are NULL
WHERE a IS NOT DISTINCT FROM b   -- true when both NULL or both equal

Set Operations

-- UNION — combine results, remove duplicates (sorts internally — slower)
SELECT id FROM table_a
UNION
SELECT id FROM table_b;

-- UNION ALL — combine results, keep duplicates (faster; prefer when dupes OK)
SELECT 'revenue' AS type, amount FROM sales
UNION ALL
SELECT 'refund',          amount FROM refunds;

-- INTERSECT — rows present in BOTH results
SELECT customer_id FROM orders_2023
INTERSECT
SELECT customer_id FROM orders_2024;

-- EXCEPT (MINUS in Oracle) — rows in first NOT in second
SELECT email FROM all_users
EXCEPT
SELECT email FROM unsubscribed;

-- Rules: same number of columns, compatible types; column names from first query

INSERT / UPDATE / DELETE / UPSERT

-- INSERT — single row
INSERT INTO products (name, price, category)
VALUES ('Widget', 9.99, 'tools');

-- INSERT — multiple rows
INSERT INTO products (name, price, category) VALUES
    ('Gadget', 19.99, 'electronics'),
    ('Doohickey', 4.99, 'tools');

-- INSERT FROM SELECT
INSERT INTO archive_orders
SELECT * FROM orders WHERE created_at < '2023-01-01';

-- UPDATE
UPDATE employees
SET    salary     = salary * 1.10,
       updated_at = CURRENT_TIMESTAMP
WHERE  dept_id = 3 AND performance_rating = 'A';

-- UPDATE with JOIN (Postgres)
UPDATE employees e
SET    salary = e.salary * 1.05
FROM   departments d
WHERE  e.dept_id = d.id
  AND  d.name = 'Engineering';

-- DELETE
DELETE FROM sessions WHERE expires_at < NOW();

-- TRUNCATE — removes all rows, faster than DELETE, cannot be filtered
TRUNCATE TABLE log_events;           -- Postgres: add RESTART IDENTITY to reset sequences

-- UPSERT — Postgres (ON CONFLICT)
INSERT INTO user_stats (user_id, logins)
VALUES (42, 1)
ON CONFLICT (user_id)
DO UPDATE SET logins = user_stats.logins + 1,
              last_seen = NOW();

-- UPSERT — MySQL (ON DUPLICATE KEY)
INSERT INTO user_stats (user_id, logins)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE logins = logins + 1;

-- MERGE (ANSI SQL; Postgres does not support MERGE pre-v15; use on SQL Server / Oracle)
MERGE INTO target t
USING source s ON t.id = s.id
WHEN MATCHED     THEN UPDATE SET t.val = s.val
WHEN NOT MATCHED THEN INSERT (id, val) VALUES (s.id, s.val);

DDL — CREATE TABLE and Indexes

-- CREATE TABLE with common constraints
CREATE TABLE orders (
    id          SERIAL PRIMARY KEY,          -- Postgres auto-increment; MySQL uses AUTO_INCREMENT
    customer_id INT          NOT NULL,
    status      VARCHAR(20)  NOT NULL DEFAULT 'pending',
    amount      NUMERIC(12,2) CHECK (amount >= 0),
    note        TEXT,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),   -- with time zone (Postgres)
    FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE
);

-- ALTER TABLE
ALTER TABLE orders ADD COLUMN shipped_at TIMESTAMPTZ;
ALTER TABLE orders DROP COLUMN note;
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'new';  -- Postgres
ALTER TABLE orders RENAME COLUMN status TO order_status;

-- DROP
DROP TABLE IF EXISTS temp_staging;

-- INDEXES
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_created  ON orders (created_at DESC);

-- Composite index — order matters; leftmost prefix is used
CREATE INDEX idx_orders_cust_date ON orders (customer_id, created_at);

-- Unique index
CREATE UNIQUE INDEX idx_users_email ON users (email);

-- Partial index (Postgres) — index only a subset of rows
CREATE INDEX idx_active_orders ON orders (customer_id) WHERE status = 'active';

-- Full-text index (MySQL)
CREATE FULLTEXT INDEX idx_ft_body ON articles (body);

Date and Time Functions

-- Current date/time
SELECT CURRENT_DATE;              -- ANSI; returns date only
SELECT CURRENT_TIMESTAMP;         -- ANSI; date + time + tz
SELECT NOW();                     -- Postgres + MySQL alias for CURRENT_TIMESTAMP
SELECT GETDATE();                 -- SQL Server

-- Extract parts
SELECT EXTRACT(YEAR  FROM created_at) AS yr,     -- ANSI
       EXTRACT(MONTH FROM created_at) AS mo,
       EXTRACT(DOW   FROM created_at) AS day_of_week;  -- Postgres: 0=Sun

SELECT DATE_PART('hour', created_at);             -- Postgres equivalent
SELECT YEAR(created_at), MONTH(created_at);       -- MySQL shorthand

-- Truncate to period (Postgres)
SELECT DATE_TRUNC('month', created_at);   -- first instant of the month
SELECT DATE_TRUNC('week',  created_at);

-- MySQL equivalent
SELECT DATE_FORMAT(created_at, '%Y-%m-01');

-- Arithmetic
SELECT created_at + INTERVAL '7 days'   FROM orders;   -- Postgres
SELECT created_at + INTERVAL 7 DAY      FROM orders;   -- MySQL
SELECT DATEADD(day, 7, created_at)      FROM orders;   -- SQL Server

-- Difference between dates
SELECT AGE(end_date, start_date)                      -- Postgres; returns interval
SELECT DATEDIFF(end_date, start_date)                 -- MySQL; days
SELECT DATEDIFF(day, start_date, end_date)            -- SQL Server

-- Format (Postgres)
SELECT TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI:SS');

-- Format (MySQL)
SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s');

String Functions

-- Concatenation
SELECT first_name || ' ' || last_name AS full_name;          -- ANSI / Postgres
SELECT CONCAT(first_name, ' ', last_name);                   -- MySQL + Postgres
SELECT CONCAT_WS(', ', last_name, first_name);               -- with separator

-- Case
SELECT UPPER(name), LOWER(name), INITCAP(name);  -- INITCAP: Postgres / Oracle

-- Length
SELECT LENGTH(name);            -- character length (Postgres / MySQL)
SELECT CHAR_LENGTH(name);       -- synonym; safer for multibyte

-- Trim and pad
SELECT TRIM('  hello  ');       -- both sides
SELECT LTRIM(name), RTRIM(name);
SELECT LPAD(code, 5, '0');      -- left-pad to width 5 with '0'
SELECT RPAD(code, 5, '-');

-- Substring
SELECT SUBSTRING(name, 1, 3);      -- ANSI; 1-based indexing
SELECT SUBSTR(name, 1, 3);         -- alias (MySQL / Oracle)
SELECT LEFT(name, 3);              -- MySQL + SQL Server
SELECT RIGHT(name, 3);

-- Position
SELECT POSITION('at' IN 'data');   -- ANSI; returns 2
SELECT STRPOS('data', 'at');       -- Postgres

-- Replace
SELECT REPLACE(phone, '-', '');

-- Split (Postgres)
SELECT SPLIT_PART('a,b,c', ',', 2);   -- returns 'b'

-- Regex replace / extract (Postgres)
SELECT REGEXP_REPLACE(name, '[^a-z]', '', 'gi');
SELECT REGEXP_MATCHES(text_col, '(\d+)', 'g');   -- returns array of matches

-- Type casting
SELECT '42'::INT;                              -- Postgres cast syntax
SELECT CAST('42' AS INT);                      -- ANSI
SELECT CONVERT(INT, '42');                     -- SQL Server

Common Interview Patterns

Second Highest Value

-- Method 1: OFFSET (simple, Postgres/MySQL)
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

-- Method 2: subquery (portable)
SELECT MAX(salary)
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

-- Method 3: dense_rank (handles NULLs, ties cleanly)
SELECT salary FROM (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS dr
    FROM employees
) ranked
WHERE dr = 2
LIMIT 1;

Deduplicate with ROW_NUMBER

-- Keep the most-recently-inserted duplicate per email
WITH deduped AS (
    SELECT *,
           ROW_NUMBER() OVER (
               PARTITION BY email
               ORDER BY created_at DESC
           ) AS rn
    FROM users
)
DELETE FROM users
WHERE id IN (
    SELECT id FROM deduped WHERE rn > 1
);

-- Or: SELECT only the deduplicated rows (no delete)
SELECT * FROM deduped WHERE rn = 1;

Top-N per Group

-- Top 3 earners per department
SELECT dept_id, name, salary
FROM (
    SELECT
        dept_id, name, salary,
        ROW_NUMBER() OVER (
            PARTITION BY dept_id
            ORDER BY salary DESC
        ) AS rn
    FROM employees
) ranked
WHERE rn <= 3;

Running Percentage of Total

SELECT
    category,
    revenue,
    ROUND(100.0 * revenue / SUM(revenue) OVER (), 2) AS pct_of_total
FROM category_revenue
ORDER BY revenue DESC;

Gap and Island Detection

-- Find start/end of consecutive date sequences per user
SELECT
    user_id,
    MIN(activity_date) AS island_start,
    MAX(activity_date) AS island_end
FROM (
    SELECT
        user_id,
        activity_date,
        activity_date - ROW_NUMBER() OVER (
            PARTITION BY user_id ORDER BY activity_date
        ) * INTERVAL '1 day' AS grp   -- Postgres
    FROM user_activity
) grouped
GROUP BY user_id, grp
ORDER BY user_id, island_start;

Pivot Without PIVOT Syntax

-- Quarter revenue pivoted into columns using CASE
SELECT
    product_id,
    SUM(CASE WHEN quarter = 'Q1' THEN revenue END) AS q1,
    SUM(CASE WHEN quarter = 'Q2' THEN revenue END) AS q2,
    SUM(CASE WHEN quarter = 'Q3' THEN revenue END) AS q3,
    SUM(CASE WHEN quarter = 'Q4' THEN revenue END) AS q4
FROM quarterly_revenue
GROUP BY product_id;

Median (ANSI percentile)

-- Postgres
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary
FROM employees;

-- MySQL 8+ and SQL Server also support PERCENTILE_CONT

Transactions and Isolation

-- Explicit transaction block
BEGIN;
    UPDATE accounts SET balance = balance - 500 WHERE id = 1;
    UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;

-- Roll back on error
BEGIN;
    -- ... statements ...
ROLLBACK;   -- undo everything since BEGIN

-- Savepoints — partial rollback within a transaction
BEGIN;
    INSERT INTO audit_log VALUES (1, NOW());
    SAVEPOINT sp1;
    UPDATE risky_table SET val = 0;
    ROLLBACK TO SAVEPOINT sp1;   -- undoes UPDATE, keeps INSERT
COMMIT;

-- Isolation levels (ANSI) — set before or at transaction start
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;    -- default in Postgres / SQL Server
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;   -- MySQL default; prevents non-repeatable reads
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;      -- strictest; prevents all anomalies

Quick Reference — Operator and Function Summary

-- Arithmetic         +  -  *  /  %  ^(Postgres power)
-- Comparison         =  <>  !=  <  >  <=  >=
-- Logical            AND  OR  NOT
-- String concat      ||  (ANSI)    CONCAT() (MySQL/Postgres)
-- Membership         IN  NOT IN  BETWEEN  LIKE  ILIKE(Postgres)
-- Existence          EXISTS  NOT EXISTS
-- NULL checks        IS NULL  IS NOT NULL  COALESCE  NULLIF

-- Aggregate          COUNT  SUM  AVG  MIN  MAX
--                    STRING_AGG(col, ',') -- Postgres; GROUP_CONCAT in MySQL
--                    ARRAY_AGG(col)       -- Postgres only

-- Casting            CAST(x AS type)   x::type (Postgres)

-- Conditional        CASE WHEN ... THEN ... ELSE ... END
--                    IIF(cond, a, b)   -- SQL Server
--                    IF(cond, a, b)    -- MySQL

-- Set ops            UNION ALL  UNION  INTERSECT  EXCEPT (MINUS in Oracle)

-- Window             ROW_NUMBER  RANK  DENSE_RANK  NTILE
--                    LAG  LEAD  FIRST_VALUE  LAST_VALUE  NTH_VALUE
--                    SUM  AVG  COUNT  MIN  MAX  (all work as window fns too)
Go deeper The full SQL course →

Explore further

Skip to content