SQL 是技术领域最经得住时间考验的技能。过去 50 年的框架换了一批又一批,SQL 还在,下批框架消失后它也会在。它并不难,一下午就能入门。因为你只需告诉数据库想要什么,不用告诉它怎么做。这一页会讲清思维模型、真正让 JOIN 变直观的方法,以及每个人几乎都会踩一次的坑。
🎙️ 发布并录制于: · 更新于 ·
数据库就是一组有规则的表。你可以把它们想成一组表格。每一行代表一个对象,每一列记录它的一项信息。不同的表通过 ID 相互关联。SQL 最重要的思想只有一个:你说明想要什么,数据库负责想办法取出来。不需要自己写循环。
-- users -- orders
-- id | name | country -- id | user_id | amount
-- 1 | Ada | UK -- 1 | 1 | 19.99
-- 2 | Lin | SG -- 2 | 1 | 5.00
-- 3 | Sam | UK -- 3 | 2 | 42.50
-- "orders.user_id points at users.id" — that link is the
-- entire secret of relational databases. hold that thought.
大多数查询只需四类信息。要哪些列,从哪张表取,要哪些行,按什么顺序排。第一天就该养成一个习惯:明确写出列名,不要总写 SELECT *。表结构一变,星号查询可能悄悄出问题,还会多读很多根本用不到的数据。
SELECT name, country -- which facts
FROM users -- which table
WHERE country = 'UK' -- which rows
ORDER BY name -- what order
LIMIT 10; -- how many (always LIMIT while exploring!)
SELECT * FROM t LIMIT 10 没问题,星号本来就适合这种临时探索。到了应用代码和报表里,* 就会变成定时炸弹。探索数据时也一定要加 LIMIT。它决定了你面对的是一句“糟了”,还是“糟了,四千万行还在往外刷”。WHERE 用来筛选行。常见比较运算符都能用。模式匹配用 LIKE,列表匹配用 IN。麻烦的是 NULL。它表示值缺失,也会打破普通的判断逻辑。NULL 不等于任何东西,连它自己也不等于。每个 SQL 开发者几乎都会在这里白费一小时。现在你可以省下这一个小时。
WHERE amount > 20
WHERE country IN ('UK', 'SG', 'DE')
WHERE name LIKE 'A%' -- starts with A (% = anything)
-- the trap:
WHERE email = NULL -- ⚠ returns NOTHING. always. silently.
WHERE email IS NULL -- ✓ the only way to ask
WHERE email IS NOT NULL -- ✓ and its opposite
= NULL 不是语法错误。查询会正常执行,然后返回零行。因为在 SQL 逻辑里,NULL = NULL 既不是真,也不是假,而是“未知”。不会有任何报错提醒你。查询莫名其妙返回空结果时,先找有没有 = NULL,再查别处。先忘掉维恩图。更实用的理解是:JOIN 按条件配对两边的行,拼出一张更宽的表。对左边的每一行,到右边寻找满足 ON 条件的行,再把它们并排粘在一起。真正要问的只有一件事:左边某行找不到匹配项时怎么办? INNER 会丢掉它。LEFT 会保留它,并在右侧填上 NULL。
-- "every order, with the customer's name attached"
SELECT orders.id, users.name, orders.amount
FROM orders
JOIN users ON orders.user_id = users.id;
-- "every USER and their orders — including users
-- who never bought anything" → LEFT JOIN
SELECT users.name, orders.amount
FROM users
LEFT JOIN orders ON orders.user_id = users.id;
-- Sam bought nothing → one row: Sam | NULL
WHERE orders.amount > 0,然后发现 NULL 行全没了。在 WHERE 中筛选右表,会悄悄把 LEFT JOIN 变回 INNER JOIN。正确做法是筛选缺失的匹配项:WHERE orders.id IS NULL。GROUP BY 会把多行收进不同的组。每组只输出一行。再用 COUNT、SUM、AVG、MAX 这样的聚合函数,算出每组的摘要。原始事件表就是这样变成统计报表的。
-- "revenue and order count per user"
SELECT user_id,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM orders
GROUP BY user_id;
-- user_id | orders | revenue
-- 1 | 2 | 24.99
-- 2 | 1 | 42.50
ERROR: column "users.name" must appear in the GROUP BY clause or be used in an aggregate function两者都能筛选数据,区别在于时机。WHERE 在分组前筛选单行。HAVING 在聚合后筛选整组。判断方法很简单。条件里有 COUNT() 或 SUM(),就不能放进 WHERE。因为 WHERE 执行时,这些统计值还不存在。
-- "big-spender customers: only count 2026 orders,
-- and only show customers who spent 100+"
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE created_at >= '2026-01-01' -- filter ROWS first
GROUP BY user_id
HAVING SUM(amount) >= 100; -- then filter BUCKETS
这个执行顺序值得记住:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。它也解释了为什么不能在 WHERE 里使用 SELECT 定义的别名。执行到 WHERE 时,那个别名还没有出现。
写入数据只有三个动词。其中 UPDATE 和 DELETE 有一个出了名的危险点:不写 WHERE,就会影响整张表的每一行。不会弹出确认框,也不会问你是否确定。每位数据库管理员都有类似的故事。靠谱的人会把教训变成固定习惯,而不是只留下一段记忆。
INSERT INTO users (name, country) VALUES ('Kai', 'JP');
UPDATE users SET country = 'DE' WHERE id = 2; -- one row ✓
UPDATE users SET country = 'DE'; -- ⚠ EVERY row. gone.
-- the professional habit: wrap risky writes in a transaction
BEGIN;
DELETE FROM orders WHERE created_at < '2020-01-01';
-- check: SELECT COUNT(*) FROM orders; happy?
COMMIT; -- or ROLLBACK; to undo everything
SELECT,保留同一个 WHERE,确认命中的行数,再换回写操作。花 30 秒做这件事,总比事后更新简历轻松。生产环境中还要放进 BEGIN … COMMIT 事务,这样至少还有撤销的机会。没有索引时,WHERE email = '…' 必须读完表里的每一行,这叫全表扫描。索引就像按顺序排好的电话簿。数据库可以直接跳到目标位置。一个查询莫名其妙很慢时,十次里大约有九次都和索引有关。
CREATE INDEX idx_users_email ON users (email);
-- before: WHERE email = '[email protected]' → scans 10,000,000 rows
-- after: same query → ~3 lookups
-- see what the database ACTUALLY does:
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- "Seq Scan" = full read, add an index. "Index Scan" = ✓
user_id、email、created_at。拿不准时,让 EXPLAIN 给出证据。下面这些查询最常重复使用。我按用途整理好了。
-- explore an unknown table
SELECT * FROM t LIMIT 10;
-- top 10 by revenue
SELECT user_id, SUM(amount) AS total
FROM orders GROUP BY user_id
ORDER BY total DESC LIMIT 10;
-- rows in A with no match in B
SELECT a.* FROM a LEFT JOIN b ON b.a_id = a.id
WHERE b.id IS NULL;
-- find duplicates
SELECT email, COUNT(*) FROM users
GROUP BY email HAVING COUNT(*) > 1;
-- safe destructive write
BEGIN; -- ...UPDATE/DELETE with WHERE... COMMIT; -- or ROLLBACK;
-- why is it slow?
EXPLAIN <your query>; -- "Seq Scan" on a big table = missing index
掌握这些,就能真正开始使用 SQL。Postgres、MySQL 和 SQLite 在细节上各有差异,但这一页的内容在它们之间基本通用。接下来可以学 Git,再学命令行。平时使用 SQL 和 Git,都绕不开命令行。