有一次面试,我问一个自称做过两年业务开发的候选人:一条 SELECT 查询,你通常先写哪部分?他不假思索地说:先写 SELECT,再写 FROM,然后根据报错修修补补。这个答案很诚实,因为很多人都是这么过来的。真正让我想把 SELECT 讲透的,不是这句回答,而是他后来反问的一句:SELECT 不就是从表里拿数据吗,还有什么可学的?
SELECT 虽然只有六个字母,但它背后是 SQL 语言最核心的一种能力:把无序的数据变成你想要的信息集合。用它查一条用户记录是 SELECT,用它跑一张上亿行的报表也是 SELECT。有人在 SELECT 上经常写出慢查询,有人靠 SELECT 在面试里聊出深度。所以我把这些年写查询积累的东西整理出来,不打算照着官方文档念语法,而是把 SELECT 当成一把钥匙,从执行顺序、组合逻辑、窗口函数、慢查询排查,一直聊到周边那些容易翻车的“同名兄弟”。不管你是刚接触数据库,还是写过几年 SQL 但总觉得差点意思,这篇内容应该都能给你一点能直接上手的收获。
1. 别被SELECT骗了:它的真正执行顺序和你写的不一样
很多教程讲 SELECT 都会先给语法结构,select、from、where、group by、having、order by。这没问题,但它只是“书写顺序”。数据库真正执行时,并不是从左到右读你这句话的。
1.1 一条普通查询在数据库内部是怎么“按顺序”跑的
先看一条最简单的工资查询:
sql复制SELECT
emp_id,
salary * 12 AS annual_salary
FROM employee
WHERE department_id = 3
ORDER BY annual_salary DESC;
很多初学者在这里会疑惑:明明 SELECT 在最前面,为什么不能在 WHERE 里用别名 annual_salary?
看执行顺序就明白了。逻辑上的执行顺序大致是:
- 先到 FROM,确定要处理哪张表。
- 然后走 WHERE,过滤行。
- 接着做 GROUP BY 和 HAVING,做分组和分组后的过滤。
- 到了 SELECT,才开始计算结果列,这时候 annual_salary 这个别名才产生。
- 最后才执行 ORDER BY,所以 ORDER BY 里能用 SELECT 里起的别名。
也就是说,WHERE 发生在 SELECT 之前,它根本不认识 annual_salary 这个别名。这不是数据库故意和你作对,而是 SQL 的语义顺序天然如此。
你需要特别小心的是,很多数据库会在内部做执行计划优化,实际的物理执行顺序可能看起来不同,但逻辑语义仍然要遵守上面的顺序。我们写 SQL 时,把这条逻辑顺序放在脑子里,很多看着莫名其妙的报错都能想通。
1.2 WHERE和HAVING的分工,是我见过最多的理解偏差
还有一个高频翻车点:以为 HAVING 是 WHERE 的替代品,或者认为两者差不多。它们其实处理的是不同阶段的过滤。
看一个统计订单状态的例子:
sql复制SELECT
status,
COUNT(*) AS order_cnt
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY status
HAVING COUNT(*) > 100;
WHERE 是在分组之前过滤每一行订单,比如只保留今年创建的订单;HAVING 是在分组之后过滤组,比如只要订单数超过 100 的状态。
所以如果你需要一个“分组结果”层面的过滤条件,比如 COUNT(*) > 100、SUM(amount) > 10000,你只能用 HAVING。反过来,如果只是想找一个订单金额大于 100 的订单行,把它放在 WHERE 里效率更好,因为它提前减少数据的规模,而不是把大量数据分组后再过滤。
有些人为了让 SQL 长得“统一”,硬把 WHERE 能做的事搬进 HAVING。不是不能运行,但会多跑很多无用的行,性能差很多。这个习惯能改还是尽早改。
1.3 SELECT子句的本质:从一堆列里“投影”你要的形状
如果把一张表理解成关系代数里的一个关系,SELECT 做的不是一个一个“掏数据”,而是对整个集合做一次“投影”:从列空间中挑出你需要的列,并允许你在列的基础上计算新表达式。
SELECT 后面能放的东西,远不止字段名。它可以放:
- 普通列:
SELECT user_id - 固定常量:
SELECT user_id, 'active' AS status - 函数计算:
SELECT user_id, UPPER(email) - 算术表达式:
SELECT price * quantity - 标量子查询:
SELECT user_id, (SELECT MAX(amount) FROM orders WHERE orders.user_id = u.id) - 聚合函数:
SELECT COUNT(*) FROM users
这种表达能力,决定了 SELECT 不只是一个“读数据”的关键字,而是一个构造结果集的工具。你用 SELECT DISTINCT 可以去掉重复行,用 SELECT TOP/LIMIT 可以限制返回条数,用 SELECT 里的 CASE WHEN 可以把编码值转换成可读文案。
理解“投影”这个层面,你会意识到写 SELECT 的目标不是把整张表原样搬出来,而是想清楚最终需要什么形状的结果。这比背十条语法规则重要得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实战里最常用的组合:过滤、连接、临时结果
单独讲 SELECT 语法很容易枯燥,但放回真实业务里就不一样了。数据十有八九不是一张表摆在那里让你随意取,而是要经过条件过滤、多表连接、拆解成临时结果之后再组合。
2.1 WHERE条件不只看结果对不对,还要看它能不能“走近道”
很多人在 WHERE 上的认知停留在“能查出来就行”,但同一条 SQL,数据量大了以后,性能可能差几十倍甚至几百倍。
举个例子:
sql复制SELECT *
FROM orders
WHERE customer_id = 10086
AND status = 'paid'
ORDER BY created_at DESC;
这条语句如果分别在 customer_id 和 status 上有索引,数据库会选择更高效的方式去定位数据。如果两张索引都能用,MySQL 通常会用最挑剔的条件:哪个列能过滤掉更多行,就先用哪个,然后回表再过滤另一个条件。
但换成下面这种写法,情况就变了:
sql复制SELECT *
FRO
