10 分钟学会 SQL

SQL 是技术领域最经得住时间考验的技能。过去 50 年的框架换了一批又一批,SQL 还在,下批框架消失后它也会在。它并不难,一下午就能入门。因为你只需告诉数据库想要什么,不用告诉它怎么做。这一页会讲清思维模型、真正让 JOIN 变直观的方法,以及每个人几乎都会踩一次的坑。

🎙️ 发布并录制于: · 更新于 ·

01思维模型:能用语言查询的表格

数据库就是一组有规则的。你可以把它们想成一组表格。每一行代表一个对象,每一列记录它的一项信息。不同的表通过 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.

02SELECT:准确取出需要的数据

大多数查询只需四类信息。要哪些列,从哪张表取,要哪些行,按什么顺序排。第一天就该养成一个习惯:明确写出列名,不要总写 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。它决定了你面对的是一句“糟了”,还是“糟了,四千万行还在往外刷”。

03WHERE 与 NULL 陷阱

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,再查别处。

04JOIN:这样理解就通了

先忘掉维恩图。更实用的理解是: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
经典 JOIN 错误
要“找出没有订单的用户”,有人先写 LEFT JOIN,再加 WHERE orders.amount > 0,然后发现 NULL 行全没了。在 WHERE 中筛选右表,会悄悄把 LEFT JOIN 变回 INNER JOIN。正确做法是筛选缺失的匹配项:WHERE orders.id IS NULL

05GROUP BY:归组与统计

GROUP BY 会把多行收进不同的组。每组只输出一行。再用 COUNTSUMAVGMAX 这样的聚合函数,算出每组的摘要。原始事件表就是这样变成统计报表的。

-- "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
这是 Stack Overflow 上最常被粘贴的 SQL 报错之一。行被归入组以后,SELECT 中的每一列要么是分组键,要么是这一组的汇总结果。一组 50 行可能有 50 个名字,数据库不会替你随便挑一个。解决办法是把该列加入 GROUP BY,或者对它使用聚合函数。

06WHERE 与 HAVING:分组前与分组后

两者都能筛选数据,区别在于时机。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 时,那个别名还没有出现。

07INSERT、UPDATE、DELETE 与事故现场

写入数据只有三个动词。其中 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
能保住工作的固定动作
执行重要的 UPDATE 或 DELETE 之前,先改成 SELECT,保留同一个 WHERE,确认命中的行数,再换回写操作。花 30 秒做这件事,总比事后更新简历轻松。生产环境中还要放进 BEGIN … COMMIT 事务,这样至少还有撤销的机会。

08索引:查询为什么会慢

没有索引时,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" = ✓
为什么不把每列都建索引
每次 INSERT 或 UPDATE 时,相关索引也要更新。读得更快,代价是写得更慢并占用更多磁盘。应当给真正用于筛选和连接的列建索引,例如 user_idemailcreated_at。拿不准时,让 EXPLAIN 给出证据。

09SQL 速查表

下面这些查询最常重复使用。我按用途整理好了。

-- 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,都绕不开命令行。

Tell me what missed

A correction is more useful than a compliment. This goes straight to the person who writes SwiftGrasp.

Was this page useful?
0/1000

Please do not include passwords, private keys, or personal information.