SELECT col1, col2 FROM table从一张表里把指定的列查出来,所有行都返回。最基本的拉数据语句。
⚠ 常见坑: 调试时 SELECT * 没问题,但写到生产代码里就给自己挖坑 —— 表加一列下游就多一个字段,反序列化全乱套。
SELECT id, name FROM users;
SELECT * FROM orders LIMIT 10;
SQL 速查表,100+ 条覆盖 SELECT、JOIN、窗口函数、索引,含 MySQL/PostgreSQL/SQLite 方言差异。
SELECT col1, col2 FROM table从一张表里把指定的列查出来,所有行都返回。最基本的拉数据语句。
⚠ 常见坑: 调试时 SELECT * 没问题,但写到生产代码里就给自己挖坑 —— 表加一列下游就多一个字段,反序列化全乱套。
SELECT id, name FROM users;
SELECT * FROM orders LIMIT 10;
WHERE col = value按条件过滤行,可以用 AND / OR / NOT 拼。一行一行地判断。
⚠ 常见坑: WHERE col = NULL 永远是假,NULL 不等于 NULL。要判空用 IS NULL。
SELECT * FROM users WHERE status = 'active';
SELECT * FROM orders WHERE amount > 100 AND created_at > NOW() - INTERVAL 7 DAY;
SELECT * FROM users WHERE deleted_at IS NULL;
ORDER BY col [ASC|DESC]按一列或多列排序,默认升序。PostgreSQL NULL 默认排最后,MySQL 默认排最前。
⚠ 常见坑: 不写 ORDER BY,返回顺序是未定义的 —— 上次什么顺序这次未必。做分页一定要显式 ORDER BY。
SELECT * FROM users ORDER BY created_at DESC;
SELECT * FROM products ORDER BY price ASC, name ASC;
SELECT * FROM users ORDER BY created_at DESC NULLS LAST; -- PostgreSQL
LIMIT n [OFFSET m]从偏移 m 后取 n 行,分页常用。MySQL / PostgreSQL / SQLite 都支持这个语法,SQL Server 用 TOP / OFFSET FETCH。
⚠ 常见坑: OFFSET 越大越慢 —— 第 1000 页要先扫前 1000 条。深分页用游标法:WHERE id > last_id ORDER BY id LIMIT 20。
SELECT * FROM users ORDER BY id LIMIT 20;
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100;
SELECT * FROM users WHERE id > 12345 ORDER BY id LIMIT 20; -- keyset
DISTINCT col去重,作用在 SELECT 列表里的整行上,不是单独某一列。
⚠ 常见坑: SELECT DISTINCT a, b 是对 (a, b) 组合去重,不是只对 a 去重。新手经常理解错。
SELECT DISTINCT country FROM users;
SELECT DISTINCT user_id, product_id FROM orders;
SELECT COUNT(DISTINCT user_id) FROM logins;
GROUP BY col把同一组(指定列相同的行)合成一行,给聚合函数用。
⚠ 常见坑: SELECT 里没用聚合函数的列必须出现在 GROUP BY 里(PostgreSQL 和新版 MySQL only_full_group_by 强制)。老 MySQL 默默给你一个随机行,旧代码注意。
SELECT country, COUNT(*) FROM users GROUP BY country;
SELECT user_id, SUM(amount) FROM orders GROUP BY user_id;
SELECT DATE(created_at) d, COUNT(*) FROM logs GROUP BY DATE(created_at);
IN (...) / NOT IN (...)匹配固定一组值,等价于一串 OR。
⚠ 常见坑: NOT IN (子查询) 只要子查询里出现一个 NULL,整个表达式变成 UNKNOWN,外层返回空。三值逻辑的坑,改用 NOT EXISTS。
SELECT * FROM users WHERE status IN ('active', 'pending');SELECT * FROM orders WHERE id NOT IN (1, 2, 3);
SELECT * FROM users WHERE id IN (SELECT user_id FROM admins);
BETWEEN a AND b区间过滤,两端都闭。数字、日期、字符串都能用。
⚠ 常见坑: 日期 BETWEEN '2026-01-01' AND '2026-01-31' 在 2026-01-31 00:00 就截止了,1 月 31 号大半天都不算。改用 >= 开始 AND < 下一段开始。
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01';
LIKE 'pattern%'模糊匹配,% 任意多个字符,_ 一个字符。MySQL 默认不区分大小写,PostgreSQL 区分(用 ILIKE 忽略)。
⚠ 常见坑: 通配符在开头 (%foo) 走不了普通 B 树索引,全表扫。结尾通配 (foo%) 能走索引。
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM products WHERE name ILIKE 'iphone%'; -- PostgreSQL
SELECT * FROM users WHERE phone LIKE '138_____1234'; -- 11-digit pattern
CASE WHEN ... THEN ... ELSE ... END行内条件表达式,第一个命中 WHEN 返回对应 THEN,都不中走 ELSE。SELECT / WHERE / ORDER BY 都能用。
⚠ 常见坑: CASE 短路求值但返回类型会被统一 —— 混了 INT 和 TEXT 全变字符串。要显式 CAST。
SELECT name, CASE WHEN age < 18 THEN 'minor' WHEN age < 60 THEN 'adult' ELSE 'senior' END AS age_group FROM users;
SELECT SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS paid_total FROM orders;
COALESCE(a, b, c, ...)返回第一个非 NULL 的参数,给字段补默认值最干净的写法。
SELECT COALESCE(nickname, name, 'anonymous') AS display_name FROM users;
SELECT COALESCE(updated_at, created_at) AS last_seen FROM posts;
NULLIF(a, b)a 和 b 相等返回 NULL,否则返回 a。常用来把哨兵值(0、空串)转成 NULL。
SELECT total / NULLIF(count, 0) AS avg FROM stats; -- avoid divide-by-zero
SELECT NULLIF(name, '') AS name FROM users;
UNION / UNION ALL把两个结果集竖着拼起来。UNION 会去重,UNION ALL 保留重复且快得多。
⚠ 常见坑: 两边列数必须一样、类型要兼容。ORDER BY 只能放最后一个 SELECT,作用在整体上。
SELECT id FROM active_users UNION SELECT id FROM trial_users;
SELECT 'a' AS source, * FROM table_a UNION ALL SELECT 'b' AS source, * FROM table_b;
INTERSECT / EXCEPT集合运算。INTERSECT 取两边都有的,EXCEPT(Oracle 叫 MINUS)取第一个有第二个没有的。
⚠ 常见坑: MySQL 8.0.31 才加 INTERSECT / EXCEPT,老版本得用 LEFT JOIN ... WHERE IS NULL 或 NOT EXISTS 模拟。
SELECT id FROM customers INTERSECT SELECT id FROM newsletter_subscribers;
SELECT id FROM all_users EXCEPT SELECT id FROM banned;
EXISTS vs IN — semantics with NULLWHERE col IN (子查询带 NULL) 没事;WHERE col NOT IN (子查询带 NULL) 永远返空。EXISTS / NOT EXISTS 对 NULL 安全。
-- TRAP: SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM bans); -- empty if any bans.user_id IS NULL -- SAFE: SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM bans b WHERE b.user_id = u.id);
AS alias for columns and tables给列或子查询起个好读的名字。AS 多数方言可省,但写上也合法。带空格或保留字的别名用双引号(MySQL 用反引号)。
SELECT u.id AS user_id, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id;
SELECT * FROM (SELECT user_id, SUM(amount) total FROM orders GROUP BY user_id) AS spend WHERE total > 1000;
COUNT(*) / COUNT(col)COUNT(*) 统所有行包括 NULL,COUNT(col) 只数 col 非 NULL 的行,COUNT(DISTINCT col) 数去重后的非空值。
⚠ 常见坑: COUNT(1) 和 COUNT(*) 在所有现代优化器里完全一样,"COUNT(1) 更快" 是 90 年代老掉牙的谣言。哪个看着顺写哪个。
SELECT COUNT(*) FROM users;
SELECT COUNT(email) FROM users; -- non-null only
SELECT COUNT(DISTINCT country) FROM users;
SUM(col) / AVG(col)数值聚合。整列全 NULL 时 SUM 返回 NULL(不是 0),AVG 跳过 NULL(非空值之和 / 非空值数量)。
⚠ 常见坑: 想让 SUM 在没数据时返回 0 而不是 NULL?外面套一层 COALESCE:COALESCE(SUM(x), 0)。
SELECT SUM(amount) FROM orders WHERE user_id = 42;
SELECT AVG(price) FROM products;
SELECT COALESCE(SUM(amount), 0) FROM orders WHERE user_id = 999;
MIN(col) / MAX(col)列的最小值 / 最大值。数字、日期、字符串都行(字符串按字典序)。
SELECT MIN(created_at), MAX(created_at) FROM orders;
SELECT user_id, MAX(score) AS best FROM games GROUP BY user_id;
HAVING <agg condition>在 GROUP BY 之后过滤分组(WHERE 是分组前过滤行)。条件里出现聚合函数时必须用 HAVING。
⚠ 常见坑: 别用 HAVING 写非聚合条件 —— 那是 WHERE 的活,跑得更早(要分组的行更少)。HAVING 写非聚合能跑但不走索引。
SELECT country, COUNT(*) c FROM users GROUP BY country HAVING COUNT(*) > 100;
SELECT user_id FROM orders GROUP BY user_id HAVING SUM(amount) > 10000;
GROUP_CONCAT / STRING_AGG把组里某列拼成一个逗号分隔字符串。MySQL 用 GROUP_CONCAT,PostgreSQL 用 STRING_AGG,SQLite 用 GROUP_CONCAT。
⚠ 常见坑: MySQL GROUP_CONCAT 默认 1024 字节就被截掉,且不报错。拼大字段先调 group_concat_max_len。
SELECT user_id, GROUP_CONCAT(product_id ORDER BY created_at SEPARATOR ',') FROM orders GROUP BY user_id; -- MySQL
SELECT user_id, STRING_AGG(product_id::text, ',' ORDER BY created_at) FROM orders GROUP BY user_id; -- PostgreSQL
ARRAY_AGG(col) / JSON_AGG(col)PostgreSQL 把组打包成数组 / JSON 数组的聚合,搭 json_build_object 做嵌套汇总。
SELECT user_id, ARRAY_AGG(product_id ORDER BY created_at) FROM orders GROUP BY user_id;
SELECT user_id, JSON_AGG(json_build_object('id', id, 'amount', amount)) FROM orders GROUP BY user_id;FILTER (WHERE ...)标准 SQL 和 PostgreSQL 的条件聚合写法,比 SUM(CASE WHEN ...) 干净。
⚠ 常见坑: MySQL 不支持 FILTER,求兼容写 SUM(CASE WHEN ... THEN 1 ELSE 0 END)。
SELECT COUNT(*) FILTER (WHERE status = 'paid') AS paid_count, COUNT(*) FILTER (WHERE status = 'refunded') AS refunded_count FROM orders; -- PostgreSQL
GROUPING SETS / ROLLUP / CUBE一条语句出多层汇总。ROLLUP(a, b) 出 (a, b)、(a)、() 三档总;CUBE 出所有子集;GROUPING SETS 是显式写法。
⚠ 常见坑: 汇总行里 grouping 列出现的 NULL 是"这一层是汇总",不是真实 NULL。用 GROUPING(col) 函数区分。
SELECT country, city, COUNT(*) FROM users GROUP BY ROLLUP(country, city);
SELECT a, b, SUM(x) FROM t GROUP BY GROUPING SETS ((a), (b), (a, b), ());
SELECT COUNT(*) — estimating row count fast大表上 SELECT COUNT(*) 很慢,要扫整索引或堆。要估算从系统表读:MySQL information_schema.tables.table_rows,PostgreSQL pg_class.reltuples。
SELECT table_rows FROM information_schema.tables WHERE table_name = 'orders'; -- MySQL estimate
SELECT reltuples::bigint FROM pg_class WHERE relname = 'orders'; -- PostgreSQL estimate
INNER JOIN ... ON ...只保留左右两边都匹配上的行。JOIN 的默认行为,左边没匹配的行会被丢掉。
⚠ 常见坑: ON 写错或漏了条件就退化成 CROSS JOIN,行数爆炸。一定要把 join 条件挂在有索引的列上。
SELECT u.name, o.amount FROM users u INNER JOIN orders o ON o.user_id = u.id;
SELECT u.id FROM users u JOIN orders o ON o.user_id = u.id AND o.status = 'paid';
LEFT JOIN ... ON ...左表所有行都留下,右边没匹配的位置填 NULL。常用于"用户和他可选的订单"这种场景。
⚠ 常见坑: 在 WHERE 里过滤右表的列会把 LEFT JOIN 退化成 INNER JOIN —— 右边为 NULL 的行被丢掉。过滤条件挪进 ON 里。
SELECT u.id, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id;
SELECT u.* FROM users u LEFT JOIN bans b ON b.user_id = u.id WHERE b.user_id IS NULL; -- anti-join
RIGHT JOIN ... ON ...右表所有行都留下,等价于左右换位置写 LEFT JOIN,实际很少有人写。
⚠ 常见坑: 团队约定一般不用 RIGHT JOIN —— 换成 LEFT JOIN 让"保留的那张表"永远在左边,从上读到下更直观。
SELECT u.id, b.reason FROM bans b RIGHT JOIN users u ON u.id = b.user_id;
FULL OUTER JOIN ... ON ...左右两边的行都保留,对不上的另一边填 NULL。对两本账核对差异时常用。
⚠ 常见坑: MySQL 完全不支持 FULL OUTER JOIN,需要的话用 LEFT JOIN UNION RIGHT JOIN 模拟。
SELECT a.id, b.id FROM table_a a FULL OUTER JOIN table_b b ON a.id = b.id WHERE a.id IS NULL OR b.id IS NULL; -- find mismatches
CROSS JOIN笛卡尔积,左边每行配右边每行。生成日期序列、日历、组合数据时主动用。
⚠ 常见坑: 逗号 join (FROM a, b WHERE ...) 一旦忘了 WHERE 就变 CROSS JOIN。写明 CROSS JOIN 让意图一眼看出来。
SELECT d.day, p.name FROM generate_series('2026-01-01'::date, '2026-01-31', '1 day') d(day) CROSS JOIN products p; -- PostgreSQL daily report skeletonSELF JOIN (table aliased twice)一张表起两个别名 join 自己,用于父子行、领导下属、两两比较。
SELECT e.name AS employee, m.name AS manager FROM employees e LEFT JOIN employees m ON m.id = e.manager_id;
SELECT a.id, b.id FROM events a JOIN events b ON a.user_id = b.user_id AND b.created_at > a.created_at AND b.created_at < a.created_at + INTERVAL 1 HOUR;
Multi-table JOIN chain三张表以上一连串 join 拼数据。优化器自己选 join 顺序,但代码可读性还是按你写的顺序。
⚠ 常见坑: 长链 join 容易"展开"—— 一行接 5 行子表再接 3 行孙表,一行变 15 行,COUNT 立刻翻车。
SELECT u.name, o.id, oi.product_id, p.name FROM users u JOIN orders o ON o.user_id = u.id JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id;
USING (col)两边列名相同时的 JOIN 简写。结果集里 join 列只出现一次,不会重复。
SELECT * FROM users JOIN orders USING (user_id);
SELECT * FROM a NATURAL JOIN b; -- joins on ALL columns with matching names (rarely safe)
LATERAL JOINJOIN 右侧能引用左侧的列,类似把相关子查询写在 JOIN 位置。用来取"每组前 N 行"特别干净。
SELECT u.id, recent.* FROM users u LEFT JOIN LATERAL (SELECT * FROM orders o WHERE o.user_id = u.id ORDER BY created_at DESC LIMIT 3) recent ON true; -- PostgreSQL
Anti-join — find rows with no match两种写法等效:LEFT JOIN ... WHERE right.id IS NULL 和 NOT EXISTS (子查询)。优化器对两者差不多,哪个读着顺写哪个。
SELECT u.* FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE o.user_id IS NULL;
SELECT u.* FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
Scalar subquery in SELECTSELECT 里的子查询返回单行单列。少用 —— 外层每行都会跑一次。
⚠ 常见坑: 子查询返回多行就会运行时报错。加 LIMIT 1 + ORDER BY 保证确定性。
SELECT u.id, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count FROM users u;
SELECT u.id, (SELECT MAX(amount) FROM orders WHERE user_id = u.id) AS biggest FROM users u;
WHERE col IN (subquery)用子查询返回的一组值过滤外层行,优化器通常会改写成半连接 (semi-join)。
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
WHERE EXISTS (subquery)子查询返回至少一行就为真,短路执行,命中第一行就停。
⚠ 常见坑: EXISTS 里写 SELECT 1 FROM ... 是惯用,SELECT 列表会被忽略,只看存不存在。别再写 SELECT *。
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM bans b WHERE b.user_id = u.id);
Correlated subquery子查询引用了外层的列 —— 外层每行都会执行一次。强大但容易写出 O(N²) 慢查询。
⚠ 常见坑: WHERE 里的相关子查询通常能改成 JOIN + GROUP BY,速度差几个数量级。两种写法都 EXPLAIN 一下对比。
SELECT u.id FROM users u WHERE u.last_login < (SELECT MAX(o.created_at) FROM orders o WHERE o.user_id = u.id);
WITH cte AS (...) SELECT ... (CTE)CTE,给子查询起名字后面用,多层变换写起来好读。
⚠ 常见坑: 老 PostgreSQL (< 12) CTE 总是物化的,是优化栅栏。新版 PostgreSQL 默认内联,MySQL 8 也是。要老行为加 MATERIALIZED。
WITH heavy_users AS (SELECT user_id FROM orders GROUP BY user_id HAVING SUM(amount) > 10000) SELECT u.* FROM users u JOIN heavy_users h ON h.user_id = u.id;
Recursive CTE (WITH RECURSIVE)能引用自身的 CTE,用于树形 / 图遍历、生成序列、层级汇总。
⚠ 常见坑: 忘记 UNION ALL 锚定行或递归终止条件就无限递归。多数引擎大约 1000 层就报错停住。
WITH RECURSIVE org AS (SELECT id, name, manager_id, 0 AS depth FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, o.depth + 1 FROM employees e JOIN org o ON e.manager_id = o.id) SELECT * FROM org ORDER BY depth, id;
WITH RECURSIVE nums(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM nums WHERE n < 100) SELECT * FROM nums; -- generate 1..100
ANY / ALL operatorscol > ANY (子查询) 只要大于子查询任一值就为真;col > ALL (...) 要大于所有。等价于和 MAX/MIN 比较。
SELECT * FROM products WHERE price > ANY (SELECT price FROM products WHERE category = 'book');
SELECT * FROM products WHERE price >= ALL (SELECT price FROM products); -- most expensive
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)在每个分区内给行编号 1..N。"每组前 N 行" 经典套路,外层包一下过滤 rn <= N。
⚠ 常见坑: 同分按 ORDER BY 后面的列分高下;想给同分留空位用 RANK,不留空位用 DENSE_RANK。ROW_NUMBER 永远不重复。
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders) t WHERE rn <= 3; -- top 3 most recent orders per user
RANK() / DENSE_RANK()RANK 同分后面留空 (1, 2, 2, 4),DENSE_RANK 不留 (1, 2, 2, 3)。做排行榜常用。
SELECT user_id, score, RANK() OVER (ORDER BY score DESC) AS rnk FROM scores;
SELECT category, name, price, DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) FROM products;
LAG(col) / LEAD(col)在分区里取上一行 (LAG) 或下一行 (LEAD)。算时序差值、上一单金额、漏斗下一步特别趁手。
SELECT user_id, created_at, amount, LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_amount FROM orders;
SELECT day, sales, sales - LAG(sales) OVER (ORDER BY day) AS day_over_day_change FROM daily_stats;
SUM(col) OVER (PARTITION BY ... ORDER BY ...) (running total)窗口内的累积聚合。PARTITION BY 按组分,ORDER BY 让它累加到当前行为止。
⚠ 常见坑: 不写 frame,有 ORDER BY 时默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,同分的行会被合到一起。想"严格 N 行" 用 ROWS 不用 RANGE。
SELECT day, sales, SUM(sales) OVER (ORDER BY day) AS cumulative FROM daily_stats;
SELECT user_id, created_at, amount, SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS lifetime_spend FROM orders;
AVG(col) OVER (... ROWS BETWEEN n PRECEDING AND CURRENT ROW) (moving window)滑动窗口聚合,覆盖前 n 行。算移动平均、平滑时序常用。
SELECT day, price, AVG(price) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7d FROM prices;
NTILE(n) OVER (ORDER BY ...)按 ORDER BY 把行平均分到 n 个桶,用于分位、四分位、十分位划分。
SELECT user_id, total_spend, NTILE(4) OVER (ORDER BY total_spend DESC) AS quartile FROM user_spend;
FIRST_VALUE / LAST_VALUE取窗口里第一个 / 最后一个值。LAST_VALUE 必须显式写 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,否则只到当前行(结果就是当前行自己)。
SELECT user_id, created_at, amount, FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS first_order_amount FROM orders;
SELECT *, LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM orders;
PERCENT_RANK() / CUME_DIST()统计窗口函数。PERCENT_RANK = (rank - 1) / (n - 1),CUME_DIST = 到当前行(含)的行数 / 总行数。
SELECT user_id, score, PERCENT_RANK() OVER (ORDER BY score) AS pr FROM scores;
WINDOW clause — name a reusable window用 WINDOW 子句给窗口起名字,多个窗口函数复用。比把同样的 OVER (...) 抄三遍干净。
SELECT id, amount, RANK() OVER w AS rnk, AVG(amount) OVER w AS avg_in_window, SUM(amount) OVER w AS sum_in_window FROM orders WINDOW w AS (PARTITION BY user_id ORDER BY created_at);
INSERT INTO ... VALUES (...)插入新行。永远显式写目标列名 —— 按位置插入的代码,有人调整列顺序当天就崩。
⚠ 常见坑: 循环里一行一行 INSERT 是"导数据要四小时"第一名。批量塞进一个 INSERT 多行 VALUES,或用 COPY(PostgreSQL)/ LOAD DATA(MySQL)。
INSERT INTO users (id, name, email) VALUES (1, 'alice', 'a@example.com');
INSERT INTO users (name, email) VALUES ('a', 'a@x.com'), ('b', 'b@x.com'), ('c', 'c@x.com'); -- batchINSERT ... SELECT ...把一个查询的结果灌进另一张表。做 ETL、物化快照、表回填常用。
INSERT INTO users_archive (id, name, email, created_at) SELECT id, name, email, created_at FROM users WHERE created_at < NOW() - INTERVAL 1 YEAR;
UPDATE table SET col = ... WHERE ...改满足条件的行。SQL 允许不写 WHERE,但脑子里必须永远有这一行。
⚠ 常见坑: 先把同样的 WHERE 用 SELECT * 跑一遍,看影响多少行,确认后再换成 UPDATE。不带 WHERE 的 UPDATE 整表全改。
UPDATE users SET status = 'active' WHERE id = 42;
UPDATE orders SET amount = amount * 1.1 WHERE created_at < '2026-01-01';
UPDATE ... FROM other_table (JOIN update)用另一张表的值更新这张表。PostgreSQL 写 UPDATE ... FROM,MySQL 写 UPDATE t1 JOIN t2 SET ...。
UPDATE orders o SET amount = c.fixed_amount FROM corrections c WHERE o.id = c.order_id; -- PostgreSQL
UPDATE orders o JOIN corrections c ON o.id = c.order_id SET o.amount = c.fixed_amount; -- MySQL
DELETE FROM table WHERE ...删行。和 UPDATE 一样的纪律 —— 先用 SELECT 预览。
⚠ 常见坑: DELETE 每行都写 WAL / binlog —— 一条语句删 1 亿行能把日志撑爆、锁满、库挂掉。按主键范围分批 + LIMIT。
DELETE FROM logs WHERE created_at < NOW() - INTERVAL 30 DAY LIMIT 10000; -- batch in a loop
DELETE FROM users WHERE id = 42;
UPSERT — INSERT ... ON CONFLICT / ON DUPLICATE KEY没冲突就插入,命中唯一键就更新。PostgreSQL 用 ON CONFLICT (col) DO UPDATE,MySQL 用 ON DUPLICATE KEY UPDATE,SQLite 两种都能用。
⚠ 常见坑: MySQL ON DUPLICATE KEY 即使走更新分支也会消耗 auto_increment ID —— upsert 频繁的话主键序列会出大量空洞。
INSERT INTO users (id, name, email) VALUES (1, 'alice', 'a@x.com') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email; -- PostgreSQL
INSERT INTO users (id, name) VALUES (1, 'alice') ON DUPLICATE KEY UPDATE name = VALUES(name); -- MySQL
INSERT INTO counters (key, n) VALUES ('a', 1) ON CONFLICT (key) DO UPDATE SET n = counters.n + 1; -- atomic incrementTRUNCATE TABLE快速清表,DDL 级别操作,不写每行 WAL。多数引擎会重置自增。
⚠ 常见坑: MySQL 里 TRUNCATE 不能回滚(隐式提交)。不会触发行级触发器。PostgreSQL 外键不会自动级联,要加 CASCADE。
TRUNCATE TABLE logs;
TRUNCATE TABLE orders RESTART IDENTITY CASCADE; -- PostgreSQL
RETURNING * (PostgreSQL / SQLite)把刚 INSERT / UPDATE / DELETE 的行一次性返回,省一次 SELECT 往返。
⚠ 常见坑: MySQL 8.x 仍不支持 RETURNING,得用 LAST_INSERT_ID() + SELECT 凑。
INSERT INTO users (name) VALUES ('alice') RETURNING id, created_at;UPDATE orders SET status = 'paid' WHERE id = 42 RETURNING *;
DELETE FROM logs WHERE id < 100 RETURNING id;
MERGE INTO ... (standard UPSERT)标准 UPSERT 语法。PostgreSQL 15+ / SQL Server / Oracle 都支持。比 ON CONFLICT 更强,命中时还能 DELETE。
⚠ 常见坑: 高并发下 MERGE 有微妙的竞态(查和插不是原子的)。纯 upsert 优先 PostgreSQL 的 INSERT ... ON CONFLICT。
MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
DELETE ... USING / JOIN (delete by other table)按另一张表的匹配删行。PostgreSQL: DELETE ... USING;MySQL: DELETE t1 FROM t1 JOIN t2 ON ...。
DELETE FROM orders USING users WHERE orders.user_id = users.id AND users.deleted_at IS NOT NULL; -- PostgreSQL
DELETE o FROM orders o JOIN users u ON u.id = o.user_id WHERE u.deleted_at IS NOT NULL; -- MySQL
CREATE TABLE ... (...)建表,定义列、类型、约束。数据质量的第一道关卡 —— 在第一条 INSERT 之前就定好。
⚠ 常见坑: 建表时就把 NULL 与否、主键定好 —— 后面加 NOT NULL 要回填默认值、可能全表重写。
CREATE TABLE users ( id BIGINT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW() );
PRIMARY KEY (col)把一列或几列标成行的唯一标识。隐式 UNIQUE + NOT NULL,每表只能有一个主键。
CREATE TABLE orders (id BIGINT PRIMARY KEY, user_id BIGINT, amount NUMERIC);
CREATE TABLE order_items (order_id BIGINT, product_id BIGINT, PRIMARY KEY (order_id, product_id));
FOREIGN KEY (col) REFERENCES other(col)把列约束成另一表存在的值。ON DELETE CASCADE / SET NULL / RESTRICT 控制父行被删时的行为。
⚠ 常见坑: FK 检查需要父列上有索引(PK 自带,但引用非主键的 UNIQUE 列时要检查)。大表上 CASCADE 删除能把库压垮。
CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
UNIQUE constraint禁止某列或几列组合出现重复值。多数引擎里 NULL 允许多次出现(NULL 之间不算相等)。
CREATE TABLE users (email TEXT UNIQUE);
ALTER TABLE orders ADD CONSTRAINT uq_user_day UNIQUE (user_id, DATE(created_at));
CHECK (predicate)在 INSERT / UPDATE 时检查布尔条件,不过就报错。适合枚举、有界数字、格式约束。
⚠ 常见坑: MySQL 8.0.16 之前 CHECK 是写了不查的 —— 老代码 DDL 里看着有约束实际从不生效。用 SHOW CREATE TABLE 验证一下。
CREATE TABLE products (price NUMERIC CHECK (price >= 0));
ALTER TABLE users ADD CONSTRAINT chk_age CHECK (age BETWEEN 0 AND 150);
ALTER TABLE ... ADD COLUMN / DROP COLUMN加 / 删列。带默认值的 ADD COLUMN 在新版 PostgreSQL (≥ 11) 和 MySQL (≥ 8.0) 很快,老版本要全表重写。
⚠ 常见坑: 大表上 DROP COLUMN 是长 DDL,会拿 ACCESS EXCLUSIVE 锁 —— 安排到维护窗口。在线变更工具 (pt-online-schema-change、gh-ost) 能绕开。
ALTER TABLE users ADD COLUMN last_login TIMESTAMP;
ALTER TABLE users DROP COLUMN deprecated_field;
ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'; -- fast on new engines
ALTER TABLE ... ALTER COLUMN TYPE改列类型,每行的值都得能成功转换。
⚠ 常见坑: 变宽(INT → BIGINT、VARCHAR(50) → TEXT)通常便宜。变窄或换不兼容类型可能失败,PostgreSQL 需要 USING 子句。
ALTER TABLE orders ALTER COLUMN amount TYPE NUMERIC(18, 4); -- PostgreSQL
ALTER TABLE users MODIFY COLUMN id BIGINT; -- MySQL
ALTER TABLE t ALTER COLUMN flag TYPE BOOLEAN USING (flag::int <> 0); -- PostgreSQL
CREATE INDEX idx ON table (col)在列上建 B 树索引,加速这列的 WHERE / JOIN / ORDER BY。代价是写入时每条 INSERT / UPDATE 都要维护索引。
⚠ 常见坑: PostgreSQL 生产库建索引一定用 CREATE INDEX CONCURRENTLY —— 普通 CREATE INDEX 长时间持写锁。MySQL InnoDB 在线 DDL 多数情况能自动避免。
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX CONCURRENTLY idx_orders_created ON orders (created_at); -- PostgreSQL production
CREATE UNIQUE INDEX idx_users_email_unique ON users (email);
DROP TABLE [IF EXISTS]删表,数据一起没。加 IF EXISTS 表不存在时不报错,幂等迁移里好用。
⚠ 常见坑: DROP 没有撤销,没有回收站。永远先 SELECT COUNT(*) 确认一下,再确认你连的是哪个库。
DROP TABLE IF EXISTS old_logs;
DROP TABLE orders CASCADE; -- PostgreSQL
CREATE VIEW v AS SELECT ...把一个 SELECT 起名字当虚拟表用,查视图每次都会重跑底层 SELECT。
⚠ 常见坑: 视图不缓存。重型聚合用物化视图(PostgreSQL MATERIALIZED VIEW)+ REFRESH MATERIALIZED VIEW。
CREATE VIEW active_users AS SELECT * FROM users WHERE deleted_at IS NULL AND status = 'active';
CREATE MATERIALIZED VIEW daily_sales AS SELECT day, SUM(amount) FROM orders GROUP BY day; -- PostgreSQL
CREATE INDEX ... CONCURRENTLY (PostgreSQL)在不阻塞写入的情况下建索引。比普通 CREATE INDEX 慢,且不能放在事务块里。
⚠ 常见坑: CREATE INDEX CONCURRENTLY 中途失败会留下 INVALID 索引 —— \d 看得到,优化器不用。DROP 掉重来。
CREATE INDEX CONCURRENTLY idx_orders_created ON orders (created_at);
BEGIN / START TRANSACTION开事务,后续写入在 COMMIT 之前对其他会话不可见(read-committed 及以上)。
⚠ 常见坑: 事务开着不关会一直持锁、撑大 undo / WAL。连接池把 BEGIN 包在外面再抛异常,连接就永远 idle in transaction —— PostgreSQL 设 idle_in_transaction_session_timeout。
BEGIN;
START TRANSACTION;
START TRANSACTION READ ONLY;
COMMIT / ROLLBACKCOMMIT 把暂存写入落盘并对其他会话可见,ROLLBACK 全部撤销。每个 BEGIN 必须以其中之一收尾。
COMMIT;
ROLLBACK;
SAVEPOINT name; ROLLBACK TO SAVEPOINT name在事务里打一个回滚点,可以只撤销这一段。"试一批,错了只撤这一批,主事务继续" 这种场景用。
BEGIN; INSERT INTO users (id, name) VALUES (1, 'a'); SAVEPOINT sp1; INSERT INTO orders (user_id) VALUES (1); ROLLBACK TO SAVEPOINT sp1; COMMIT;
SET TRANSACTION ISOLATION LEVEL ...选事务隔离级别:READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE。级别越高异常越少,但吞吐降、死锁多。
⚠ 常见坑: 默认不同:PostgreSQL 是 READ COMMITTED,MySQL InnoDB 是 REPEATABLE READ,Oracle 是 READ COMMITTED。同样 SQL 不同库行为不一样。
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- MySQL
SELECT ... FOR UPDATE对返回的行加行级写锁,持有到事务结束。"读-改-写"防竞态的正解。
⚠ 常见坑: 没有覆盖索引的 FOR UPDATE 可能锁住远超预期的行(MySQL InnoDB REPEATABLE READ 下还会锁间隙)。EXPLAIN 看清楚,注意间隙锁。
BEGIN; SELECT * FROM accounts WHERE id = 42 FOR UPDATE; UPDATE accounts SET balance = balance - 100 WHERE id = 42; COMMIT;
SELECT * FROM jobs WHERE status = 'queued' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; -- worker queue
Deadlock — detection and avoidance两个事务各持对方要的锁时,引擎会挑一个事务杀掉("牺牲者")报死锁。避免方法:所有事务按相同顺序加锁。
⚠ 常见坑: 应用要做死锁重试 —— 这是正常结果不是 bug。打日志 + 退避重试。
-- MySQL: SHOW ENGINE INNODB STATUS \G -- latest deadlock -- PostgreSQL: log line 'deadlock detected', error SQLSTATE 40P01
EXPLAIN / EXPLAIN ANALYZE看优化器选的执行计划。ANALYZE 会真跑一遍,报真实行数和耗时。
⚠ 常见坑: EXPLAIN ANALYZE 用在 UPDATE / DELETE 上会真的改数据 —— 只想看的话外面包事务最后 ROLLBACK。
EXPLAIN SELECT * FROM users WHERE email = 'a@x.com';
EXPLAIN ANALYZE SELECT * FROM orders WHERE created_at > NOW() - INTERVAL 1 DAY; -- PostgreSQL
EXPLAIN FORMAT=JSON SELECT * FROM users; -- MySQL
Composite index (a, b, c)联合索引,只有匹配最左前缀的查询才走得到 —— (a)、(a, b)、(a, b, c) 都能用;(b)、(c)、(b, c) 用不到。
⚠ 常见坑: 列顺序很关键。最常做过滤、选择性最高的列放前面;ORDER BY 的列放最后让索引顺便给你排好。
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at);
-- Uses index: SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC; -- Does NOT use this index (skips leftmost): SELECT * FROM orders WHERE created_at > '2026-01-01';
Covering index (INCLUDE / extra cols)索引里包含查询需要的所有列,引擎从索引就能答完,不用再回表。
CREATE INDEX idx_orders_cover ON orders (user_id) INCLUDE (amount, status); -- PostgreSQL ≥ 11
CREATE INDEX idx_orders_cover ON orders (user_id, amount, status); -- MySQL covering (all in B-tree)
Index NOT used — common causes索引列被函数包了 (WHERE LOWER(email) = ...)、隐式类型转换 (WHERE int_col = '42')、通配符在开头 (LIKE '%foo')、OR 跨多个不同索引列。EXPLAIN 都能看出来。
⚠ 常见坑: 修法:给表达式建函数索引、参数类型对齐、把 %foo 改成 foo% 或建 trigram / 全文索引、OR 改写成 UNION ALL。
CREATE INDEX idx_users_email_lower ON users (LOWER(email)); -- functional index
SELECT * FROM users WHERE id = 42; -- not '42', integer literal
Partial index — CREATE INDEX ... WHERE只给满足条件的行建索引,省空间省写代价。多数行不需要被过滤时特别值得(比如只索引未删除的行)。
CREATE INDEX idx_active_users_email ON users (email) WHERE deleted_at IS NULL; -- PostgreSQL
ANALYZE / OPTIMIZE TABLE — update stats刷新优化器统计,让它选对计划。大批量导入或数据分布大变之后跑一次。
ANALYZE users; -- PostgreSQL
ANALYZE TABLE users; -- MySQL
VACUUM ANALYZE users; -- PostgreSQL: reclaim space AND refresh stats
pg_stat_statements — slowest queriesPostgreSQL 的扩展,记录归一化后的查询统计:总耗时、均耗时、调用次数、行数。生产环境定位真正慢查询最快的工具。
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; SELECT query, calls, mean_exec_time, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
Auto-increment ID — across dialectsMySQL: BIGINT AUTO_INCREMENT PRIMARY KEY;PostgreSQL: BIGSERIAL 或 GENERATED ALWAYS AS IDENTITY;SQLite: INTEGER PRIMARY KEY AUTOINCREMENT。
CREATE TABLE t (id BIGINT AUTO_INCREMENT PRIMARY KEY); -- MySQL
CREATE TABLE t (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY); -- PostgreSQL standard
CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT); -- SQLite
LIMIT / pagination syntaxMySQL / PostgreSQL / SQLite: LIMIT n OFFSET m;SQL Server: OFFSET m ROWS FETCH NEXT n ROWS ONLY;Oracle 12 以前要套子查询用 ROWNUM。
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100;
SELECT * FROM users ORDER BY id OFFSET 100 ROWS FETCH NEXT 20 ROWS ONLY; -- SQL Server / standard
String concat — || vs CONCAT vs +PostgreSQL / SQLite / 标准:a || b(带 NULL 整体变 NULL);MySQL: CONCAT(a, b)(CONCAT 遇 NULL 整体 NULL,CONCAT_WS 把 NULL 当空串);SQL Server: +。
⚠ 常见坑: MySQL 默认 a || b 是逻辑 OR,不是字符串拼接。要兼容写法设 sql_mode=PIPES_AS_CONCAT。
SELECT 'hello' || ' ' || 'world'; -- PostgreSQL / SQLite
SELECT CONCAT('hello', ' ', 'world'); -- MySQL / portableSELECT CONCAT_WS(' ', 'hello', NULL, 'world'); -- MySQL: 'hello world'Current timestamp — NOW vs CURRENT_TIMESTAMPNOW() 在 MySQL 和 PostgreSQL 都行;CURRENT_TIMESTAMP 是标准、最通用;SQLite 用 CURRENT_TIMESTAMP 或 datetime('now')。
SELECT NOW();
SELECT CURRENT_TIMESTAMP;
SELECT datetime('now'); -- SQLiteDate arithmetic — INTERVAL syntaxMySQL: NOW() - INTERVAL 7 DAY;PostgreSQL / 标准: NOW() - INTERVAL '7 days';SQLite: datetime('now', '-7 days')。
SELECT NOW() - INTERVAL 7 DAY; -- MySQL
SELECT NOW() - INTERVAL '7 days'; -- PostgreSQL
SELECT datetime('now', '-7 days'); -- SQLiteBoolean type — across dialectsPostgreSQL 原生 BOOLEAN;MySQL 是 TINYINT(1),BOOLEAN 是别名;SQLite 直接存 0 / 1。
CREATE TABLE t (active BOOLEAN NOT NULL DEFAULT TRUE); -- PostgreSQL
CREATE TABLE t (active TINYINT(1) NOT NULL DEFAULT 1); -- MySQL
JSON type — JSONB vs JSONPostgreSQL 优先 JSONB(二进制,可索引);MySQL 用 JSON(自带校验,二进制存储);SQLite 用 TEXT + json1 扩展函数。
⚠ 常见坑: PostgreSQL JSON 保留空白和键序,JSONB 不保留。除非应用确实在乎原文回环,否则一律选 JSONB。
SELECT data->>'name' FROM events WHERE data->>'event' = 'signup'; -- PostgreSQL
SELECT JSON_EXTRACT(data, '$.name') FROM events; -- MySQL
SELECT json_extract(data, '$.name') FROM events; -- SQLite json1
String quoting — single vs double标准 SQL:单引号字符串,双引号标识符。MySQL 默认双引号也是字符串 —— 想兼容设 sql_mode=ANSI_QUOTES。
SELECT 'hello' AS greeting, "user_id" FROM users; -- standard / PostgreSQL
SELECT `user_id` FROM users; -- MySQL backtick quotes identifiers always
Case sensitivity — identifiers and valuesPostgreSQL 把没加引号的标识符折成小写 ("Users" → users)。MySQL Linux/Mac 上小写,Windows 上保留大小写(lower_case_table_names 控制)。字符串比较 MySQL utf8mb4_general_ci 不区分大小写,PostgreSQL 区分。
SELECT * FROM "Users"; -- PostgreSQL: case-sensitive when quoted
SELECT * FROM users WHERE LOWER(email) = LOWER('A@x.com'); -- portable case-insensitive compareNULL is not equal to NULLNULL 参与的比较返回 UNKNOWN,不是 TRUE 也不是 FALSE。WHERE col = NULL、WHERE col != NULL、甚至 WHERE NULL = NULL 都是 UNKNOWN(WHERE 当假)。
⚠ 常见坑: 改用 IS NULL / IS NOT NULL / IS DISTINCT FROM(PostgreSQL 标准)/ <=>(MySQL NULL 安全等号)。
SELECT * FROM users WHERE deleted_at IS NULL;
SELECT * FROM users WHERE last_login IS DISTINCT FROM previous_login; -- PostgreSQL: NULL-safe inequality
SELECT * FROM users WHERE col <=> NULL; -- MySQL NULL-safe equals
GROUP BY missing non-aggregated columnsSELECT 里没在聚合函数里的列都必须出现在 GROUP BY 里。PostgreSQL 和新 MySQL 强制,老 MySQL 默默给你随机一行。
⚠ 常见坑: MySQL 配置打开 only_full_group_by,把潜在 bug 在报表出错之前暴露出来。
-- WRONG (old MySQL silently allowed): SELECT country, name FROM users GROUP BY country; -- RIGHT: SELECT country, MAX(name) FROM users GROUP BY country; -- OR: SELECT country, name FROM users GROUP BY country, name;
SQL injection — never string-concat user input把用户输入直接字符串拼到 SQL 里能让攻击者 ' OR 1=1; DROP TABLE users; -- 拿下整库。永远用参数化 / 预编译语句。
⚠ 常见坑: ORM 和驱动 (psycopg、pg、mysql2、sqlx、prisma) 自带 ? 或 $1 占位符 —— 没有任何理由再去拼字符串。
-- WRONG (vulnerable):
-- query('SELECT * FROM users WHERE id = ' + userInput);
-- RIGHT:
-- query('SELECT * FROM users WHERE id = ?', [userInput]); // mysql2 / sqlite
-- query('SELECT * FROM users WHERE id = $1', [userInput]); // pg / postgresLEFT JOIN that secretly turns into INNER JOIN在 WHERE 里过滤右表的列会把 LEFT JOIN 退化成 INNER JOIN —— 右边为 NULL 的行被丢掉,反而漏数据。
⚠ 常见坑: 把右表的过滤条件挪到 ON 里,或者 WHERE 里显式允许 NULL:WHERE r.col = X OR r.col IS NULL。
-- WRONG (drops users with no orders): SELECT u.* FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE o.status = 'paid'; -- RIGHT: SELECT u.* FROM users u LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid';
DELETE / UPDATE without WHEREDELETE / UPDATE 忘写 WHERE 整表改光。经典周五晚上事故,打开 safe-updates 模式的头号理由。
⚠ 常见坑: MySQL: SET SQL_SAFE_UPDATES = 1;PostgreSQL: 写触发器或装 pg_safeupdate。养成习惯:先用同一个 WHERE 跑 SELECT。
-- DISASTER: DELETE FROM users; -- deletes everything -- ALWAYS: SELECT * FROM users WHERE id = 42; -- confirm one row DELETE FROM users WHERE id = 42;
COUNT vs SUM(CASE WHEN ...)COUNT(col) 只数 col 非 NULL 的行。条件计数用 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 或 COUNT(*) FILTER (WHERE ...)。
SELECT SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_count, COUNT(*) AS total FROM orders;
SELECT COUNT(*) FILTER (WHERE status = 'paid') FROM orders; -- PostgreSQL
Floating-point money math货币用 FLOAT / DOUBLE 会出现 0.1 + 0.2 = 0.30000000000000004。钱永远存 NUMERIC / DECIMAL,明确精度。
CREATE TABLE orders (amount NUMERIC(18, 4) NOT NULL); -- safe for money
-- WRONG: CREATE TABLE orders (amount DOUBLE);
Implicit type cast kills index usageMySQL 用字符串字面量和 INT 列比较时会把字符串转成数字,但反过来 —— 用 INT 列和长字符串比较 —— 会转列,索引就走不了。字符集不一致也是同样的坑。
-- Index used (literal cast): SELECT * FROM users WHERE id = '42'; -- Index SKIPPED (column cast): SELECT * FROM users WHERE phone_number_int = '13800001234'; -- if column is INT and literal is wider
Cartesian explosion from forgotten JOIN condition逗号列多表又没写 WHERE join 条件(或 ON 写错),行数会爆炸。1k × 1k 一下变 100 万。
-- DISASTER (no join condition): SELECT * FROM users u, orders o; -- RIGHT: SELECT * FROM users u JOIN orders o ON o.user_id = u.id;
可搜索的 SQL 速查表,覆盖后端、数据分析、DBA 日常真在敲的 100+ 条语句,不是凑数的 SELECT * FROM users 入门列表。十一 大分类:基础(SELECT、WHERE、GROUP BY、ORDER BY、LIMIT、 DISTINCT、IN、BETWEEN、LIKE、CASE、COALESCE、UNION、 INTERSECT),聚合(COUNT、SUM、AVG、MIN、MAX、HAVING、 GROUP_CONCAT / STRING_AGG、ARRAY_AGG、FILTER、ROLLUP / CUBE), JOIN(INNER、LEFT、RIGHT、FULL OUTER、CROSS、SELF、多表连 查、USING、LATERAL、反连接),子查询 / CTE(标量、IN、 EXISTS、相关子查询、WITH、递归、ANY / ALL),窗口函数 (ROW_NUMBER、RANK、DENSE_RANK、LAG、LEAD、PARTITION BY 累 加、ROWS BETWEEN 滑动窗口、NTILE、FIRST_VALUE、PERCENT_RANK、 WINDOW 子句),写操作(INSERT、INSERT ... SELECT、UPDATE、 UPDATE ... JOIN、DELETE、UPSERT 用 ON CONFLICT / ON DUPLICATE KEY、TRUNCATE、RETURNING、MERGE),DDL(CREATE TABLE、主 键、外键 + 级联、UNIQUE、CHECK、ALTER TABLE、CREATE INDEX、 DROP、VIEW + PostgreSQL 物化视图),事务(BEGIN / COMMIT / ROLLBACK、SAVEPOINT、四种隔离级别、SELECT FOR UPDATE + SKIP LOCKED 做工作队列、死锁重试),索引与 EXPLAIN(联合索引最 左前缀、覆盖索引 / INCLUDE、部分索引、索引为什么没走、 ANALYZE、pg_stat_statements),方言差异(MySQL vs PostgreSQL vs SQLite 在自增主键、LIMIT OFFSET vs OFFSET FETCH、字符串 拼接 || / CONCAT / +、NOW 与 CURRENT_TIMESTAMP、INTERVAL 写 法、布尔、JSON 与 JSONB、大小写敏感),以及 7 个真烧钱的坑 (NULL 不等于 NULL、GROUP BY 漏列、SQL 注入、LEFT JOIN 被 WHERE 写法降级成 INNER JOIN、DELETE / UPDATE 忘 WHERE、 COUNT(col) 与 SUM(CASE WHEN) 区别、浮点存钱、隐式类型转换 让索引失效)。每条都附一行真实"常见坑"、1-3 条可直接复制 的例子,并在 SQL 写法分叉的地方标方言 tag。搜索框跨语句 / 说明 / 坑 / 例子四个字段一起过滤,分类胶囊缩范围,方言筛 选器一键只看 MySQL / PostgreSQL / SQLite 条目,每条一键 复制。完全在浏览器里跑,不连数据库,不上传,不追踪 —— 堡垒机后面都能用。配合 SQL Formatter 美化,搭配 Docker / kubectl / Nginx / Regex 速查覆盖整条技术栈。
把内容粘贴或拖入工具面板。
点击按钮,在浏览器内本地处理,文件不上传。
一键复制结果或下载到本地。
适合穿插在写代码、查问题、做 Review、上线前的小任务里。
这些入口会把当前任务接到更完整的工具链里。
做你这行的人, 还会一起用这些。