EasyDebug.NET

SQL Cheat Sheet

From joins and window functions to common pitfalls, each with a usable example

104 snippets

SELECT col1, col2 FROM tQuery shape

SELECT id, name, created_at FROM users

Select named columns; avoid SELECT * so new columns do not break callers.

SELECT * FROM t WHERE id = 1Query shape

SELECT * FROM users WHERE id = 1

Select every column; fine for ad-hoc checks only.

SELECT DISTINCT col FROM tQuery shape

SELECT DISTINCT city FROM users

Distinct rows; note it applies to the whole selected row.

SELECT col AS alias FROM tQuery shape

SELECT user_name AS name FROM users

Alias a column or expression.

SELECT ... ORDER BY col DESCQuery shape

SELECT id FROM orders ORDER BY created_at DESC

Sort descending; ASC is the default.

SELECT ... ORDER BY a ASC, b DESCQuery shape

SELECT id, score FROM exam ORDER BY score DESC, id ASC

Multi-column sort; always add a unique tiebreaker when paginating.

SELECT ... LIMIT 10 OFFSET 20Query shape

SELECT id FROM orders ORDER BY id LIMIT 10 OFFSET 20

Pagination: rows 21 through 30.

SELECT TOP 10 ... FROM tQuery shape

SELECT TOP 10 id FROM orders ORDER BY id DESC

SQL Server syntax for taking the first N rows.

SELECT ... FETCH FIRST 10 ROWS ONLYQuery shape

SELECT id FROM orders ORDER BY id FETCH FIRST 10 ROWS ONLY

Standard SQL for the first N rows; Oracle and newer PostgreSQL.

SELECT a FROM t1 UNION SELECT b FROM t2Query shape

SELECT email FROM users UNION SELECT email FROM leads

Union with deduplication; column counts and types must match.

SELECT a FROM t1 UNION ALL SELECT b FROM t2Query shape

SELECT id FROM a UNION ALL SELECT id FROM b

Union without deduplication; much faster than UNION.

WITH cte AS (SELECT ...) SELECT * FROM cteQuery shape

WITH paid AS (SELECT * FROM orders WHERE status = 1) SELECT COUNT(*) FROM paid

Common table expression; breaks a complex query into readable steps.

WITH RECURSIVE cte AS (... UNION ALL ...) SELECT * FROM cteQuery shape

WITH RECURSIVE tree AS (SELECT id, parent_id FROM nodes WHERE parent_id IS NULL UNION ALL SELECT n.id, n.parent_id FROM nodes n JOIN tree t ON n.parent_id = t.id) SELECT * FROM tree

Recursive CTE for tree and graph structures.

SELECT CASE WHEN cond THEN a ELSE b END FROM tQuery shape

SELECT CASE WHEN score >= 60 THEN 1 ELSE 0 END AS passed FROM exam

Conditional expression inside the result set.

SELECT CAST(col AS INT) FROM tQuery shape

SELECT CAST(price AS DECIMAL(10,2)) FROM orders

Explicit cast, safer than relying on implicit conversion.

SELECT COALESCE(a, b, 0) FROM tQuery shape

SELECT COALESCE(nickname, user_name) FROM users

First non-null value; the usual way to handle nulls.

SELECT CONCAT(a, b) FROM tQuery shape

SELECT CONCAT(first_name, surname) FROM users

String concatenation; PostgreSQL and MySQL also accept the || operator.

SELECT ROW_NUMBER() OVER (...) FROM tQuery shape

SELECT ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users

Add a row number; see the window group.

WHERE col = valueFiltering

WHERE status = 1

Equality filter.

WHERE col <> valueFiltering

WHERE status <> 0

Not-equal filter; != also works.

WHERE col IN (1, 2, 3)Filtering

WHERE status IN (1, 2, 5)

Match any of several values; clearer than chained ORs.

WHERE col NOT IN (SELECT ...)Filtering

WHERE id NOT IN (SELECT user_id FROM bans)

Exclusion by subquery; a NULL in the subquery empties the whole result.

WHERE col BETWEEN 10 AND 20Filtering

WHERE created_at BETWEEN 2 AND 8

Inclusive on both ends; with dates it drops the tail of the end day.

WHERE name LIKE 'ab%'Filtering

WHERE email LIKE 'admin%'

Prefix match, which can use an index.

WHERE name LIKE '%ab%'Filtering

WHERE remark LIKE '%退货%'

Contains match; the leading wildcard prevents index use.

WHERE col IS NULLFiltering

WHERE deleted_at IS NULL

Test for null with IS NULL; = NULL never matches.

WHERE col IS NOT NULLFiltering

WHERE email IS NOT NULL

Not-null test.

WHERE a = 1 AND (b = 2 OR c = 3)Filtering

WHERE status = 1 AND (type = 2 OR type = 3)

Parentheses decide precedence; AND binds tighter than OR.

WHERE EXISTS (SELECT 1 FROM b WHERE b.a_id = a.id)Filtering

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

Existence test; usually faster than IN and immune to NULL.

WHERE created_at >= '2026-01-01'Filtering

WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'

Use a half-open range; never wrap the column in a function.

SELECT ... FROM a INNER JOIN b ON a.id = b.a_idJoins

SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id

Inner join, keeping only matched rows on both sides.

SELECT ... FROM a LEFT JOIN b ON a.id = b.a_idJoins

SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id

Left join keeps all left rows, filling NULL for missing right rows.

SELECT ... FROM a FULL OUTER JOIN b ON ...Joins

SELECT * FROM a FULL OUTER JOIN b ON a.id = b.a_id

Full outer join keeps unmatched rows from both sides.

SELECT ... FROM a CROSS JOIN bJoins

SELECT s.size, c.color FROM sizes s CROSS JOIN colors c

Cartesian product, useful for building combination matrices.

SELECT ... FROM a LEFT JOIN b ON ... WHERE b.id IS NULLJoins

SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL

Find rows in the left table with no match on the right (anti-join).

SELECT ... FROM a JOIN b USING (id)Joins

SELECT * FROM users JOIN profiles USING (user_id)

Join on same-named columns; the column appears once in the result.

SELECT ... FROM a x JOIN a y ON x.pid = y.idJoins

SELECT e.name, m.name AS manager FROM emp e JOIN emp m ON e.mgr_id = m.id

Self join for hierarchical relationships.

LEFT JOIN b ON ... AND b.status = 1Joins

SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 1

Extra conditions on a left join belong in ON; in WHERE they turn it into an inner join.

SELECT col, COUNT(*) FROM t GROUP BY colAggregation

SELECT city, COUNT(*) FROM users GROUP BY city

Group rows and count.

SELECT COUNT(*) FROM tAggregation

SELECT COUNT(*) FROM orders

Count rows, including rows with nulls.

SELECT COUNT(col) FROM tAggregation

SELECT COUNT(email) FROM users

Count non-null values; may differ from COUNT(*).

SELECT COUNT(DISTINCT col) FROM tAggregation

SELECT COUNT(DISTINCT user_id) FROM orders

Count distinct values.

SELECT SUM(col), AVG(col), MIN(col), MAX(col) FROM tAggregation

SELECT SUM(amount), AVG(amount) FROM orders

Sum, average, min, max; AVG skips nulls and shrinks the denominator.

SELECT col FROM t GROUP BY col HAVING COUNT(*) > 1Aggregation

SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1

Filter after grouping; the usual way to find duplicates.

SELECT GROUP_CONCAT(col) FROM t GROUP BY gAggregation

SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM users GROUP BY city

MySQL aggregation into a string; PostgreSQL uses STRING_AGG.

SELECT COUNT(*) FILTER (WHERE cond) FROM tAggregation

SELECT COUNT(*) FILTER (WHERE status = 1) AS paid FROM orders

Conditional aggregation in one scan; MySQL uses SUM(condition).

SELECT a, b, COUNT(*) FROM t GROUP BY ROLLUP (a, b)Aggregation

SELECT city, status, COUNT(*) FROM orders GROUP BY ROLLUP (city, status)

Adds subtotal and grand total rows.

SELECT a, b FROM t GROUP BY a, bAggregation

SELECT city, status FROM orders GROUP BY city, status

Multi-column grouping; non-aggregated select columns must all be grouped.

ROW_NUMBER() OVER (PARTITION BY g ORDER BY t DESC)Window functions

ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn

Row number within a partition; the core of the latest-per-group pattern.

RANK() OVER (ORDER BY score DESC)Window functions

RANK() OVER (ORDER BY score DESC) AS r

Rank with gaps for ties (1,2,2,4).

DENSE_RANK() OVER (ORDER BY score DESC)Window functions

DENSE_RANK() OVER (ORDER BY score DESC) AS r

Rank without gaps (1,2,2,3).

LAG(col, 1) OVER (ORDER BY t)Window functions

LAG(amount, 1, 0) OVER (ORDER BY created_at) AS prev_amount

Previous row value, for deltas and period-over-period.

LEAD(col, 1) OVER (ORDER BY t)Window functions

LEAD(amount) OVER (ORDER BY created_at) AS next_amount

Next row value.

SUM(x) OVER (ORDER BY t)Window functions

SUM(amount) OVER (ORDER BY created_at) AS running_total

Running total; the default frame spans from the start to the current row.

AVG(x) OVER (PARTITION BY g)Window functions

AVG(amount) OVER (PARTITION BY user_id) AS user_avg

Average within a partition without collapsing rows.

NTILE(4) OVER (ORDER BY x)Window functions

NTILE(4) OVER (ORDER BY amount DESC) AS quartile

Split into N buckets, for quantiles and stratified sampling.

FIRST_VALUE(x) OVER (PARTITION BY g ORDER BY t)Window functions

FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS first_amount

Value from the first row in the partition.

LAST_VALUE(x) OVER (... ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)Window functions

LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_amount

Last row in the partition; without the full frame you get the current row.

SELECT * FROM (SELECT ..., ROW_NUMBER() OVER (...) rn FROM t) x WHERE rn = 1Window functions

SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn FROM orders) x WHERE rn = 1

The standard latest-per-group query; it needs an outer subquery.

WHERE rn = 1 -- 窗口函数不能直接写在 WHERE 里Window functions

SELECT * FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY g ORDER BY t) rn FROM t) x WHERE rn <= 3

Window functions are not evaluated yet in WHERE; materialize them in a subquery first.

INSERT INTO t (a, b) VALUES (1, 2), (3, 4)Modifying data

INSERT INTO users (name, email) VALUES ('Ada', 'ada@example.com')

Insert one or many rows; batching values is faster.

INSERT INTO t (a, b) SELECT a, b FROM srcModifying data

INSERT INTO archive SELECT * FROM orders WHERE created_at < 2025

Bulk insert from a query.

INSERT ... ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.colModifying data

INSERT INTO s (id, v) VALUES (1, 10) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v

PostgreSQL upsert: update on conflict, insert otherwise.

INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col)Modifying data

INSERT INTO s (id, v) VALUES (1, 10) ON DUPLICATE KEY UPDATE v = VALUES(v)

The MySQL upsert form.

UPDATE t SET col = value WHERE condModifying data

UPDATE users SET status = 1 WHERE id = 10

Update matching rows; omitting WHERE updates everything.

UPDATE t SET a = b.a FROM b WHERE t.id = b.idModifying data

UPDATE orders o SET price = p.price FROM products p WHERE o.product_id = p.id

PostgreSQL join update; MySQL uses JOIN, SQL Server uses FROM.

DELETE FROM t WHERE condModifying data

DELETE FROM sessions WHERE expires_at < NOW()

Delete matching rows.

TRUNCATE TABLE tModifying data

TRUNCATE TABLE staging_rows

Empty a table faster than DELETE and reset identity; usually not rollback-able.

MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE ...Modifying data

MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.v = s.v WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.v)

Standard SQL merge; supported by Oracle and SQL Server.

REPLACE INTO t (id, v) VALUES (1, 2)Modifying data

REPLACE INTO s (id, v) VALUES (1, 10)

MySQL only: delete then insert; auto-increment changes and triggers fire differently.

CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(50) NOT NULL)Schema changes

CREATE TABLE users (id BIGINT PRIMARY KEY, name VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT NOW())

Create a table; primary key and NOT NULL are the basics.

CREATE TABLE t (..., CONSTRAINT fk FOREIGN KEY (a_id) REFERENCES a(id))Schema changes

CREATE TABLE orders (id BIGINT PRIMARY KEY, user_id BIGINT, CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id))

Foreign key for referential integrity at a write-cost.

CREATE UNIQUE INDEX uk ON t (col)Schema changes

CREATE UNIQUE INDEX uk_email ON users (email)

Unique constraint; also blocks duplicate concurrent writes.

ALTER TABLE t ADD COLUMN c TYPESchema changes

ALTER TABLE users ADD COLUMN phone VARCHAR(20)

Add a column; on large tables a NOT NULL default may lock.

ALTER TABLE t DROP COLUMN cSchema changes

ALTER TABLE users DROP COLUMN phone

Drop a column.

ALTER TABLE t ALTER COLUMN c TYPE VARCHAR(100)Schema changes

ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(30)

Change a column type; MySQL spells it MODIFY COLUMN.

ALTER TABLE t RENAME TO t2Schema changes

ALTER TABLE users RENAME TO app_users

Rename a table.

DROP TABLE IF EXISTS tSchema changes

DROP TABLE IF EXISTS tmp_import

Drop a table; IF EXISTS keeps scripts idempotent.

CREATE VIEW v AS SELECT ...Schema changes

CREATE VIEW v_paid AS SELECT * FROM orders WHERE status = 1

View: stores the query, not the data.

CREATE TABLE t AS SELECT ...Schema changes

CREATE TABLE users_backup AS SELECT * FROM users

Create a table from a query; constraints and indexes are not copied.

CREATE INDEX idx ON t (col)Indexes and plans

CREATE INDEX idx_created ON orders (created_at)

Plain index: speeds reads, slows writes.

CREATE INDEX idx ON t (a, b)Indexes and plans

CREATE INDEX idx_user_time ON orders (user_id, created_at)

Composite index follows the leftmost prefix rule; a query on b alone cannot use it.

DROP INDEX idxIndexes and plans

DROP INDEX idx_created

Drop an index; SQL Server spells it DROP INDEX table.index.

EXPLAIN SELECT ...Indexes and plans

EXPLAIN SELECT * FROM orders WHERE user_id = 1

Show the plan; watch type and key in MySQL, Seq Scan in PostgreSQL.

EXPLAIN ANALYZE SELECT ...Indexes and plans

EXPLAIN ANALYZE SELECT COUNT(*) FROM orders

Actually runs the query with real timings and row counts.

CREATE INDEX idx ON t (a) INCLUDE (b)Indexes and plans

CREATE INDEX idx_user ON orders (user_id) INCLUDE (amount)

Covering index: carry the selected column to avoid table lookups.

BEGINTransactions and locks

BEGIN

Start a transaction (START TRANSACTION also works).

COMMITTransactions and locks

COMMIT

Commit the transaction.

ROLLBACKTransactions and locks

ROLLBACK

Roll back the transaction.

SAVEPOINT sp1Transactions and locks

SAVEPOINT sp1

Set a savepoint for partial rollback.

ROLLBACK TO SAVEPOINT sp1Transactions and locks

ROLLBACK TO SAVEPOINT sp1

Roll back to a savepoint, keeping earlier work.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READTransactions and locks

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

Set isolation: read uncommitted, read committed, repeatable read, serializable.

SELECT ... FOR UPDATETransactions and locks

SELECT * FROM accounts WHERE id = 1 FOR UPDATE

Take a write lock against lost updates; must be inside a transaction.

SELECT ... FOR UPDATE SKIP LOCKEDTransactions and locks

SELECT * FROM jobs WHERE status = 0 ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED

Skip locked rows; the standard queue-worker pattern.

col = NULL -- 永远不成立Common pitfalls

SELECT * FROM t WHERE deleted_at = NULL

NULL means unknown; comparisons with it are unknown, so use IS NULL.

NOT IN (子查询含 NULL) -- 结果为空Common pitfalls

SELECT * FROM a WHERE id NOT IN (SELECT a_id FROM b)

A single NULL in the subquery empties NOT IN; use NOT EXISTS.

WHERE id = '123' -- 数字列用字符串比较Common pitfalls

SELECT * FROM users WHERE phone = 13800000000

Implicit conversion disables the index; match parameter and column types.

WHERE DATE(created_at) = '2026-01-01'Common pitfalls

WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'

A function on the column disables the index; rewrite as a range.

WHERE a = 1 OR b = 2Common pitfalls

SELECT * FROM t WHERE a = 1 UNION SELECT * FROM t WHERE b = 2

OR across columns prevents single-column index use; split and UNION.

LIMIT 20 OFFSET 100000Common pitfalls

SELECT * FROM t WHERE id > 100000 ORDER BY id LIMIT 20

Deep offsets scan and discard everything before; use keyset pagination.

DELETE FROM t -- 没有 WHERECommon pitfalls

DELETE FROM t WHERE created_at < 2024

A missing WHERE empties the table; verify with a matching SELECT first.

SELECT ... FROM t ORDER BY id -- 排序不稳定Common pitfalls

SELECT id, name FROM t ORDER BY score DESC, id ASC LIMIT 20

Ties make ordering non-deterministic and pagination unstable; add a unique tiebreaker.

一个事务里改几十万行Common pitfalls

-- 改成每批 1000 行循环提交 DELETE FROM logs WHERE created_at < 2024 LIMIT 1000;

Huge transactions hold locks and bloat rollback segments; batch them.

字符串比较受排序规则影响Common pitfalls

SELECT * FROM t WHERE name = 'Ada' -- 大小写敏感取决于 collation

MySQL collations are often case-insensitive; another database may not be.

Something broken or missing?

Send feedback
Author's Blog
Share an idea