上一章已经把“小满商店”装进数据库。顾客在 customers,商品在 products,订单头在 orders,订单中的每件商品在 order_items。数据已经有了,接下来我们要回答一个更实际的问题:怎样把它们准确地拿出来?
这就是 SELECT 的工作。
第一次看 SELECT,你可能觉得它只是“从表里抄几列”。再往后又会遇到 WHERE、ORDER BY、JOIN、GROUP BY 和子查询,语句一下变长。其实这些部分一直在回答几类很具体的问题:数据从哪里来,哪些行留下,结果显示哪些列,同类数据怎样汇总,最后按什么顺序交给我们。
这一章先把查询的整幅地图摊开。我们会从一张表的简单读取走到两张表的连接,再看一眼聚合和子查询。这里的重点不是抢先学完后面章节,而是建立两个稳定习惯:按照固定的语法顺序写查询,按照逻辑处理顺序读查询。
本章会反复用到下面五张表:
写查询前,先问“结果的一行要代表什么”,再从上表找数据源。这个动作看似慢,实际上能提前避开连接重复、错误计数和漏行。

本章以 MySQL 8.4 的常用写法为主。示例中的 SELECT、FROM、WHERE、DISTINCT、ORDER BY、JOIN、GROUP BY 和 LIMIT ... OFFSET ... 也有助于理解其他关系型数据库。切换数据库时,分页、标识符引用和少数函数可能有细节差异。
SELECT 产生的结果仍然是一张表。它有列、有行,还能继续交给另一条查询使用。数据库中的原表是已经保存的数据,查询结果则是根据当前语句临时计算出来的。
先看最小的一条查询:
SELECT product_name
FROM products;结果只有一列:
这条语句可以拆成两句话:
FROM products:数据从商品表来。SELECT product_name:结果只显示商品名称。语法里先写 SELECT,理解时却可以先问 FROM。这是读查询的第一把钥匙:书写顺序从“我要什么”开始,逻辑处理通常先从“数据在哪里”开始。
运营想查看商品编号、名称、价格、库存和状态:
SELECT product_id,
product_name,
unit_price,
stock,
status
FROM products
ORDER BY product_id;SELECT 后列的排列顺序,就是结果列的排列顺序。把 product_id、product_name 放在前面,读者先知道“是谁”,再看价格和库存。
把列顺序调换,原表不会变化,结果表头会跟着变:
SELECT status,
stock,
product_name
FROM products
WHERE product_id = 7;WHERE 先筛出商品 7,SELECT 再决定只展示状态、库存和名称。原表中其他列仍然存在,只是没有出现在这次结果中。
* 表示当前数据源可见的全部普通列:
SELECT *
FROM customers
WHERE customer_id = 1;练习时,SELECT * 能快速确认小表有什么。长期维护的报表、接口或多表查询更适合明确写列名:
SELECT customer_id,
customer_name,
city,
registered_at
FROM customers
WHERE customer_id = 1;明确列名有几个直接好处:结果只传输真正需要的数据;读 SQL 的人不必猜星号会展开成什么;以后表里新增字段,现有结果结构不会悄悄改变;多表连接时不会把两边同名的编号和状态全部混在一起。
前面的第一条查询没有写 ORDER BY,表格按当前看到的顺序列出来,只是为了阅读方便。数据库并不保证以后仍按商品编号返回。
如果顺序是需求的一部分,就明确写出:
SELECT product_id,
product_name
FROM products
ORDER BY product_id;即使连续运行两次看到相同顺序,也不要把现象当成保证。执行计划、索引、数据量或并发变化后,数据库可能用另一种顺序交出行。
MySQL 能直接计算表达式,不强制写 FROM:
SELECT 2 * 39.90 AS 两只马克杯原价;这个例子说明,SELECT 列表里不只可以放原始列,还可以放常量、算术表达式和函数调用。没有 FROM 时表达式计算一次;有 FROM 时,它通常会对数据源留下的每一行计算一次。
业务问题很少只需要把原始列原封不动搬出来。商品列表可能显示库存金额,订单明细需要显示折后小计,报表还希望把技术字段换成读得懂的标题。SELECT 列表正是制作这些结果列的地方。

先给三件商品计算九折展示价:
SELECT product_id,
product_name,
unit_price,
ROUND(unit_price * 0.90, 2) AS sale_price
FROM products
WHERE product_id IN (1, 2, 3)
ORDER BY product_id;sale_price 没有写回 products,只存在于这次结果中。下次查询 unit_price,仍然得到原价。
ROUND(unit_price * 0.90, 2) 可以从里向外读:先用价格乘 0.90,再把结果四舍五入到两位小数。金额在表中使用定点数,展示时再按业务需要处理小数位。
小满商店的 discount_rate 表示减免比例。0.1124 的意思是减免原价的 11.24%,不是“支付 11.24%”。因此折后明细金额公式是:
数量 × 成交单价 × (1 - 折扣率)查看订单 101 的两条明细:
SELECT order_item_id,
order_id,
product_id,
quantity,
unit_price,
discount_rate,
ROUND(
quantity * unit_price * (1 - discount_rate),
2
) AS line_amount
FROM order_items
WHERE order_id = 101
ORDER BY order_item_id;第一条明细没有减免,所以是 39.90。第二条明细计算为 89 × (1 - 0.1124),四舍五入后是 79.00。
order_items.unit_price 是下单时的历史成交单价,不能用商品表的当前价格替换。以后商品改价,旧订单仍要按下单当时的价格解释。
如果不写别名,数据库可能把整段表达式当成列标题:
SELECT quantity * unit_price * (1 - discount_rate)
FROM order_items
WHERE order_item_id = 2;加上别名并按金额显示:
SELECT ROUND(
quantity * unit_price * (1 - discount_rate),
2
) AS line_amount
FROM order_items
WHERE order_item_id = 2;AS line_amount 明确告诉读者这是结果列名。AS 在很多位置可以省略,但初学阶段保留会更清楚。
对中文阅读者,也可以使用中文别名:
SELECT product_id AS 商品编号,
product_name AS 商品名称,
unit_price AS 当前单价,
stock AS 库存量
FROM products
WHERE product_id = 5;别名只改变当前查询的表头,不会重命名表中的字段。临时报表使用中文别名很直观;程序接口和可复用数据模型常用稳定的英文别名。选择哪一种取决于结果由谁继续使用。
计算每件商品按当前单价计的库存金额,只保留超过 4000 元的商品:
SELECT product_id,
product_name,
ROUND(unit_price * stock, 2) AS stock_value
FROM products
WHERE unit_price * stock > 4000
ORDER BY stock_value DESC,
product_id;WHERE 中重复写了 unit_price * stock,没有直接写 stock_value;ORDER BY 却能使用别名。原因是逻辑上先过滤行,后形成 SELECT 输出列,再排序。处理 WHERE 时,输出别名还没有形成。
读 SELECT 列表时,对每一项问两句:它是原始列、常量、表达式还是函数结果?它在结果中的列名是什么?这两问能快速还原结果表的形状。
顾客表有 10 位客户,其中苏小满和陆野都在杭州。直接查询城市,每位客户贡献一行:
SELECT customer_id,
customer_name,
city
FROM customers
ORDER BY customer_id;如果问题是“客户来自哪些城市”,就只选城市并去重:
SELECT DISTINCT city
FROM customers
ORDER BY city;10 位客户得到 9 个不同城市,杭州只保留一次。具体中文排序受字符集和排序规则影响;重要的是结果包含九个唯一城市。

下面不是“只对城市去重”:
SELECT DISTINCT city,
customer_name
FROM customers
ORDER BY city, customer_id;DISTINCT 检查的是 (city, customer_name) 整行组合。苏小满与陆野姓名不同,即使同在杭州,两行组合也不同,因此结果仍有 10 行。
订单状态也有重复。先查看状态清单:
SELECT DISTINCT status
FROM orders
ORDER BY status;如果选择 status, customer_id,去重单位会变成“状态与客户编号的组合”。写 DISTINCT 前,先用一句话说清结果一行代表什么。
一个订单可以包含多条明细。把 orders 连接到 order_items,订单 101 出现两行是正常的,因为结果粒度从“一行一张订单”变成了“一行一条明细”。
如果连接结果莫名其妙暴涨,第一反应不该是加 DISTINCT。先检查连接条件是否缺失,确认关系是否本来就是一对多,再决定是否需要去重或分组。DISTINCT 能删掉相同结果行,却不能修复连接逻辑。
DISTINCT 去掉的是结果中所有选择列完全相同的行,不是只处理紧挨着它的某一列。粒度说不清,去重就容易变成碰运气。
商品表有 12 行,订单表有 13 行,但业务问题通常只关心其中一部分。例如“商品 7 怎么了”“哪些订单已经完成”“2026 年完成了哪些订单”。WHERE 检查每一行的条件,只把结果为真的行交给后续步骤。
本章只建立基础过滤感觉。下一章会专门拆开比较、范围、集合、模式匹配、逻辑组合和 NULL 的三值逻辑。
主键通常能精确定位一行:
SELECT product_id,
product_name,
unit_price,
stock,
status
FROM products
WHERE product_id = 7;数字 7 不加引号,因为 product_id 是数值字段。文本字面量用单引号:
SELECT product_id,
product_name,
unit_price,
stock
FROM products
WHERE status = '在售'
ORDER BY product_id;双引号在不同 SQL 模式和数据库产品中可能承担不同角色。把文本稳定地写成单引号,更容易迁移。
查金额大于 180 元的订单:
SELECT order_id,
customer_id,
status,
total_amount
FROM orders
WHERE total_amount > 180
ORDER BY total_amount DESC,
order_id;> 不包含 180 本身。若问题是“至少 180 元”,应写 >= 180。自然语言中的“超过”“不少于”“最多”“早于”“不晚于”对应不同边界,写 SQL 前先把它翻译成明确符号。
查询 2026 年完成的订单:
SELECT order_id,
customer_id,
order_date,
total_amount
FROM orders
WHERE status = '已完成'
AND order_date >= '2026-01-01'
AND order_date < '2027-01-01'
ORDER BY order_date DESC,
order_id DESC;下界包含 2026 年第一刻,上界排除 2027 年第一刻。这样不用猜年底最后一秒应该写到多少位小数,也不会漏掉带微秒的时间。
这里已经用了 AND 把三个条件同时满足的行留下。括号、OR、IN、BETWEEN 和 LIKE 会在下一章详细解释;本章先把它们看成“哪些行能进入结果”的规则。
只展示已完成订单的编号、客户和金额,不显示状态:
SELECT order_id,
customer_id,
total_amount
FROM orders
WHERE status = '已完成'
ORDER BY order_id
LIMIT 5;WHERE 决定哪些行留下,SELECT 决定留下的行显示哪些列。分工不同,所以过滤列不必出现在最终结果里。
白露和陆野没有手机号。查找缺少手机号的客户要用 IS NULL:
SELECT customer_id,
customer_name,
phone
FROM customers
WHERE phone IS NULL
ORDER BY customer_id;不要写 phone = NULL。NULL 表示未知或缺失,不是一个能和普通值直接比较的字符串。为什么 phone <> '13800001001' 也会漏掉白露和陆野,下一章会用三值逻辑解释。
表有主键,不代表查询自动按主键返回;索引刚好提供某种顺序,也不代表以后都会如此。只有写出 ORDER BY,顺序才成为查询含义的一部分。
按商品价格从低到高:
SELECT product_id,
product_name,
unit_price
FROM products
ORDER BY unit_price ASC,
product_id ASC;ASC 表示升序,也是默认方向。按价格从高到低则使用 DESC:
SELECT product_id,
product_name,
unit_price
FROM products
ORDER BY unit_price DESC,
product_id ASC;按订单状态排序,同一状态内金额从高到低:
SELECT order_id,
status,
total_amount
FROM orders
ORDER BY status ASC,
total_amount DESC,
order_id ASC;数据库先比较 status。只有状态相同,才比较 total_amount;金额也相同才比较 order_id。最后加上稳定键,能避免同值行在不同执行中互换。
下面只列出“已完成”这个状态内部的排序片段,便于核对第二个键:
不同中文排序规则可能让各状态组的先后位置不同,但组内金额降序与最后的订单编号规则仍然明确。
只显示商品名称和价格,但用编号保证稳定顺序:
SELECT product_name,
unit_price
FROM products
ORDER BY product_id;排序键参与结果生成,不一定成为结果列。
输出别名可以用于后续排序:
SELECT order_item_id,
order_id,
ROUND(
quantity * unit_price * (1 - discount_rate),
2
) AS line_amount
FROM order_items
WHERE order_id = 113
ORDER BY line_amount DESC,
order_item_id;line_amount 在 SELECT 阶段形成,排序在逻辑上更晚,因此 ORDER BY 能引用它。
排序不是单纯美化。当问题包含“最高”“最新”“前几名”“第几页”时,ORDER BY 直接参与业务定义。缺少它,所谓“前五条”只是数据库碰巧先交出的五条。
结果有成千上万行时,我们通常不会一次全部展示。MySQL 用 LIMIT 控制最多返回多少行,用 OFFSET 控制跳过前面多少行。

查看价格最高的五件商品:
SELECT product_id,
product_name,
unit_price
FROM products
ORDER BY unit_price DESC,
product_id ASC
LIMIT 5;数据库先把全部商品按价格排好,再留下前五行。若没有排序,LIMIT 5 只能得到任意五件,不能回答“最贵的五件”。
商品按库存从高到低,每页四行。第一页:
SELECT product_id,
product_name,
stock
FROM products
ORDER BY stock DESC,
product_id ASC
LIMIT 4 OFFSET 0;第二页跳过前四行:
SELECT product_id,
product_name,
stock
FROM products
ORDER BY stock DESC,
product_id ASC
LIMIT 4 OFFSET 4;OFFSET 4 表示跳过四行,从第五行开始,不是“从第四行开始”。如果页码从 1 开始:
OFFSET = (页码 - 1) × 每页行数MySQL 还支持 LIMIT 4, 4,前一个数字是偏移、后一个是行数。LIMIT 4 OFFSET 4 把角色写得更明白,不容易记反。
只写 ORDER BY stock DESC,库存相同的商品之间没有确定位置。请求第一页时 A 在前、B 在后,数据或执行计划变化后可能互换,翻页就可能重复或遗漏。
追加 product_id ASC,完整规则变成“先按库存降序,库存相同按商品编号升序”。只要查询期间数据没变化,每行都有稳定位置。
第三页继续跳过八行,最多取四行:
SELECT product_id,
product_name,
stock
FROM products
ORDER BY stock DESC,
product_id ASC
LIMIT 4 OFFSET 8;若只剩两行,结果自然只有两行,不会因为 LIMIT 4 而补空或报错。
OFFSET 很大时还会有性能问题,因为数据库往往仍要识别和跳过前面的许多行。后面学习索引和查询优化时,会接触根据上一页最后一个排序键继续查询的方法。本章先守住两条规则:分页前一定排序,排序条件尽量唯一确定行的位置。
orders 保存 customer_id,却不重复保存客户姓名;order_items 保存 product_id,却不重复保存商品名称。这是关系型数据库的正常设计:每类事实放在合适的表中,通过键建立关系。
当问题要同时展示“订单编号、客户姓名和订单金额”时,单独一张表不够。JOIN 根据关联条件,把来自不同表的行组成结果。

SELECT o.order_id,
c.customer_name,
o.order_date,
o.status,
o.total_amount
FROM orders AS o
INNER JOIN customers AS c
ON c.customer_id = o.customer_id
ORDER BY o.order_id
LIMIT 6三个角色分别是:
orders AS o:左侧数据源。customers AS c:连接进来的右侧数据源。ON c.customer_id = o.customer_id:两边什么样的行算匹配。o 和 c 是表别名。o.status 明确来自订单,c.customer_name 明确来自客户。多表查询里即使列名暂时不冲突,也建议限定来源。
商品与分类也通过 category_id 匹配:
SELECT p.product_id,
p.product_name,
c.category_name,
p.unit_price
FROM products AS p
INNER JOIN categories AS c
ON c.category_id = p.category_id
ORDER BY p.product_id;结果一行代表“一件商品以及它匹配到的分类”。连接键不一定要显示,我们选择更适合阅读的分类名称。
订单 101 有两条明细。连接后订单信息重复出现两次:
SELECT o.order_id,
o.order_date,
i.order_item_id,
i.product_id,
i.quantity
FROM orders AS o
INNER JOIN order_items AS i
ON i.order_id = o.order_id
WHERE o.order_id = 101
订单没有被错误复制。一个订单对应两条明细,结果粒度已经从“一行一张订单”变成“一行一条订单明细”。
再连接商品表,就能把商品编号补成名称:
SELECT o.order_id,
p.product_name,
i.quantity,
i.unit_price,
i.discount_rate,
ROUND(
i.quantity * i.unit_price * (1 - i.discount_rate),
2
) AS line_amount
读多表连接时不要一口气念完整条语句。先看 orders JOIN order_items 产生订单明细,再看 JOIN products 为每条明细补商品信息。每增加一次连接,都问“靠哪个键匹配”“结果一行现在代表什么”。
陆野从未下单。用内连接查询客户与订单时,他不会出现。若问题是“列出所有客户及订单数”,需要从客户表出发保留左侧:
SELECT c.customer_id,
c.customer_name,
COUNT(o.order_id) AS order_count
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
GROUP BY c.customer_id,
c.customer_name
ORDER BY陆野在右侧找不到订单,LEFT JOIN 仍保留客户行,并用 NULL 补齐右表字段。COUNT(o.order_id) 忽略补出的 NULL,所以得到 0。
只连接已完成订单,但仍保留所有客户:
SELECT c.customer_id,
c.customer_name,
o.order_id,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = '已完成'
ORDER BY c.customer_id
若把 o.status = '已完成' 放进 WHERE,右侧补出的 NULL 行不会满足条件,无已完成订单的客户会消失,效果接近内连接。
本章先记住:ON 说明两边怎样匹配,WHERE 对连接结果继续筛选。外连接细节会在后面的专章展开。
10 位客户与 13 张订单做无条件组合,会得到 130 行:
SELECT COUNT(*) AS row_count
FROM customers AS c
CROSS JOIN orders AS o;CROSS JOIN 明确表达“我要所有组合”,本身不是语法错误。问题在于本来想按 customer_id 匹配,却遗漏了 ON。连接结果突然膨胀时,优先检查连接键和关系类型,不要先用 DISTINCT 消肿。
业务还会问“有多少订单”“总金额是多少”“平均是多少”。聚合函数接收一组行,返回统计值。本章只认识全景,后面会专门讲分组、HAVING、条件聚合和空值处理。

SELECT COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS total_amount,
ROUND(AVG(total_amount), 2) AS average_amount,
MIN(total_amount) AS min_amount,
MAX(total_amount) AS max_amount
FROM orders;13 行订单被压成一行:COUNT 数行,SUM 求和,AVG 求平均,MIN 和 MAX 找边界。
这份总金额包含待支付、取消和退款订单,只适合演示聚合函数,不等同于经营口径的有效销售额。小满商店默认把 已支付、已发货、已完成 视作有效销售状态。
SELECT COUNT(*) AS valid_order_count,
ROUND(SUM(total_amount), 2) AS valid_sales
FROM orders
WHERE status IN ('已支付', '已发货', '已完成');WHERE 先从 13 行中留下 10 张有效订单,聚合函数再计算。取消、退款和待支付订单不会进入有效销售统计。
按状态分别统计:
SELECT status,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS total_amount
FROM orders
GROUP BY status
ORDER BY status;GROUP BY status 把状态相同的订单放进同一组。每组输出一行,COUNT 和 SUM 在组内计算。
普通结果列通常应是分组字段,其他列放进聚合函数。下面写法逻辑不完整:
-- 一个状态组有多张订单,无法确定该展示哪一个 order_id
SELECT status,
order_id,
SUM(total_amount)
FROM orders
GROUP BY status;在 MySQL 常见的严格分组模式下,这会被拒绝。即使某种配置允许执行,随意挑出的订单编号也没有可靠含义。
统计每位客户的有效订单数和有效消费:
SELECT c.customer_id,
c.customer_name,
COUNT(o.order_id) AS order_count,
ROUND(SUM(o.total_amount), 2) AS total_amount
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c
这里一行代表一位有有效订单的客户。WHERE 先排除无效状态,GROUP BY 再按客户压缩行。
聚合查询先确定“谁是一组”。没有 GROUP BY 时,全部过滤结果是一组;按 customer_id 分组时,每位客户是一组。统计函数只在当前组中工作。
“找出金额高于平均值的订单”中,平均值本身也要查询。先用内部查询算平均金额,再让外层逐行比较。
SELECT order_id,
customer_id,
total_amount
FROM orders
WHERE total_amount > (
SELECT AVG(total_amount)
FROM orders
)
ORDER BY total_amount DESC,
order_id;内部 AVG 返回一行一列,平均订单金额约为 143.52。外层把每张订单金额与未四舍五入的平均值比较。
展示时可以 ROUND,判断时保留原精度。平均值的准确结果是所有金额之和除以 13,不应先四舍五入再参与边界判断。
找出下过有效订单的客户:
SELECT customer_id,
customer_name,
city
FROM customers
WHERE customer_id IN (
SELECT customer_id
FROM orders
WHERE status IN ('已支付', '已发货', '已完成')
)
ORDER BY customer_id;内部查询返回客户编号集合,IN 检查外层编号是否属于集合。内部即使同一客户编号重复出现,也不会让外层客户重复,因为外层仍逐行检查 customers。
先汇总有效消费,再连接客户姓名:
SELECT c.customer_id,
c.customer_name,
s.order_count,
s.total_amount
FROM customers AS c
INNER JOIN (
SELECT customer_id,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS total_amount
FROM orders
括号里的查询产生客户编号、订单数和金额三列结果,AS s 给它取表别名。外层像连接普通表一样使用 s.total_amount。
逻辑上这样理解最清楚,不代表数据库一定把完整结果写入临时文件。优化器可以在保持语义的前提下改写具体执行方式。
放在 = 右侧的子查询应该返回一个值。如果它返回多个客户编号,数据库不知道和哪一个比较。集合成员关系应使用 IN,一张临时结果表则放在 FROM 中并取别名。
第一次写子查询,先把括号内部分单独运行,确认它返回一值、一列多值还是多列结果,再放回外层。这样比一次调试整条长查询更容易定位问题。
到这里,一条查询的主要部件已经出现。它们在语法中有固定书写位置:
SELECT [DISTINCT] 结果列或表达式
FROM 数据源
[JOIN 其他数据源 ON 连接条件]
[WHERE 行过滤条件]
[GROUP BY 分组列]
[HAVING 分组过滤条件]
[ORDER BY 排序表达式]
[LIMIT 行数 OFFSET 偏移量];方括号表示这一部分按需要出现,不是实际 SQL 要输入的字符。长语句通常让不同子句各占一行,同一列表中的项目对齐。
适合初学者理解的逻辑顺序是:
这是一套理解语义的逻辑模型,不是数据库物理执行过程的逐帧录像。优化器可能利用索引、交换内连接顺序、提前应用过滤,只要最终结果符合 SQL 语义。我们用逻辑顺序判断名字何时可用、过滤作用于行还是组,以及排序和截取谁先发生。
再看库存金额查询:
SELECT product_name,
unit_price * stock AS stock_value
FROM products
WHERE unit_price * stock > 5000
ORDER BY stock_value DESC;逻辑上 WHERE 早于 SELECT。处理 WHERE 时,stock_value 这个输出别名还没形成,因此过滤条件写原表达式。ORDER BY 在 SELECT 之后,能够引用别名。
找有效订单数至少为 2 的客户。WHERE 处理的是分组前订单行,不能直接检查分组后的 COUNT:
SELECT customer_id,
COUNT(*) AS order_count
FROM orders
WHERE status IN ('已支付', '已发货', '已完成')
GROUP BY customer_id
HAVING COUNT(*) >= 2
ORDER BY customer_id;WHERE 先去掉无效订单,GROUP BY 按客户分组,HAVING 再根据每组算出的订单数筛组。后面聚合章节会展开 HAVING,这里先记住它与 WHERE 检查的对象不同。
查询最近三张订单:
SELECT order_id,
order_date,
total_amount
FROM orders
ORDER BY order_date DESC,
order_id DESC
LIMIT 3;“最近”先由下单时间降序定义,LIMIT 3 再截前三行。没有排序,“前三张”只表示系统当时最先返回的三张,不能回答最近问题。
SELECT c.customer_id,
c.customer_name,
COUNT(o.order_id) AS order_count,
ROUND(SUM(o.total_amount), 2) AS total_amount
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c
按逻辑顺序拆开:
从客户表和订单表读取数据,用相同的 customer_id 匹配。当前结果一行代表一张订单,并带有客户信息。
WHERE 只留下已支付、已发货、已完成订单。待支付、取消和退款不进入销售统计。
GROUP BY 把同一客户的有效订单放进一组,结果粒度即将变成一行一位客户。
SELECT 输出客户信息、订单数和累计金额。
结果如下:
长查询不要从第一行硬念到底。先从 FROM 和 JOIN 找数据与粒度,再依次看 WHERE、GROUP BY、SELECT、ORDER BY 和 LIMIT。长查询仍然只是几个固定问题的组合。
第一次独立写 SQL,最难的常常不是关键字,而是不知道从哪开始。把问题拆成六个答案,骨架就会自己出现。
SELECT product_id,
product_name,
stock,
unit_price
FROM products
WHERE status = '在售'
ORDER BY stock DESC,
product_id ASC
LIMIT 5;若漏掉 WHERE,缺货或下架商品也会参与排名;若漏掉 ORDER BY,LIMIT 5 只能任取五件。每个遗漏都能对应到拆解表中的一问。
这次一张表不够,因为订单没有重复保存姓名:
SELECT o.order_id,
c.customer_name,
o.order_date,
o.status,
o.total_amount
FROM orders AS o
INNER JOIN customers AS c
ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC,
结果一行代表订单,所以从 orders 开始,再连接客户姓名。先确定粒度,会让连接路径和排序键更容易判断。
> 和 >=、日期上界、分页偏移是否符合问题?SQL 能运行只说明语法可以被解析,不代表它答对了业务问题。这四问分别能发现重复统计、漏行、边界错误和分页漂移。
不稳定写法:
SELECT order_id, order_date
FROM orders
LIMIT 3;如果问题是最早三张订单,就明确排序:
SELECT order_id, order_date
FROM orders
ORDER BY order_date ASC,
order_id ASC
LIMIT 3;SELECT *
FROM orders AS o
INNER JOIN customers AS c
ON c.customer_id = o.customer_id;它会带出两边的 customer_id,还可能把电话、邮箱等与任务无关的信息带入结果。明确列出订单编号、客户姓名、时间和金额,结构更稳定,也减少敏感数据被无意读取。
订单 101 连接明细得到两行,是正确的一对多展开。若要订单编号清单,可以选择 DISTINCT o.order_id;若要订单金额汇总,应聚合明细。先确定结果粒度,再选择去重或分组。
想保留全部客户,却在 WHERE 写右表状态:
SELECT c.customer_name,
o.order_id
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = '已完成';右表补出的 NULL 行会被过滤。若要“全部客户以及他们的已完成订单”,把状态条件放在 ON:
SELECT c.customer_name,
o.order_id
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = '已完成'
ORDER BY c.customer_id,
o.order_id;从客户左连接订单时,没有订单的陆野仍产生一行结果。COUNT(*) 会把这行计为 1,COUNT(o.order_id) 只数非空订单编号,才得到 0。计数前要问自己究竟在数结果行,还是在数某个业务对象。
orders.total_amount 是最终订单金额,可能包含没有单独建列保存的订单级优惠或运费。order_items 的折后金额适合解释每条商品明细,但不能无条件断言两者之和始终等于订单总额。选择哪个金额字段,要看问题问的是订单最终金额还是商品明细金额。
逻辑顺序用于理解名称和结果变化,优化器可以改变物理计划。不要根据 SQL 文本位置断言数据库一定先扫描哪张表、逐行执行哪段子查询。需要讨论真实执行方式时,要看执行计划,那是后面的主题。
先自己写,再展开答案。核对时不只看语句是否相似,还要检查一行代表什么、顺序是否稳定。
列出商品 1、2、3 的编号、名称、原价和八五折展示价,按编号升序。
商品按库存降序、编号升序,每页四行,写出第二页。
列出所有客户和订单数,没有订单也要显示 0。
找出金额等于当前最大订单金额的订单,不要把 227.00 写死。
SELECT c.city,
COUNT(*) AS order_count
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = '已完成'
GROUP BY c.city
ORDER BY order_count DESC,
c这一章从最小的 SELECT ... FROM ... 出发,把查询的主要部件走了一遍:
FROM 与 JOIN 决定数据源以及表之间怎样匹配。WHERE 在分组前过滤明细行。GROUP BY 改变粒度,让聚合函数按组计算。SELECT 决定结果列,也能计算表达式并设置别名。DISTINCT 删除重复的结果行组合。ORDER BY 给结果建立明确顺序。LIMIT 与 OFFSET 从有序结果中截取一段。书写时按语法组织:
SELECT → FROM / JOIN → WHERE → GROUP BY → HAVING
→ ORDER BY → LIMIT / OFFSET理解时按逻辑追踪:
FROM / JOIN → WHERE → GROUP BY → HAVING
→ SELECT → DISTINCT → ORDER BY → LIMIT / OFFSET当结果不对,沿第二条链检查:数据源是否正确,连接是否改变粒度,过滤是否漏行,分组是否过细,投影是否需要去重,排序是否稳定,截取是否发生在最后。
下一章会停在 WHERE 上,专门研究条件怎样得到真、假和未知。你会看到一条很反直觉的查询:
SELECT customer_id,
customer_name,
phone
FROM customers
WHERE phone <> '13800001001';它不会把手机号为 NULL 的白露和陆野算作“号码不同”。原因不是数据库漏数据,而是 NULL 参与比较后得到的不是 TRUE。等我们把逻辑运算、范围、集合、模式匹配和 NULL 放到一起,你就能解释为什么有些行看起来应该留下,却会从结果中悄悄消失。
ORDER BY 按累计金额降序,金额相同时按客户编号升序稳定排列。
LIMIT 3 只交出排好序后的前三位客户。
陆野的 order_count 是 0。关键是从 customers 出发使用 LEFT JOIN,并数右表非空主键,不是 COUNT(*)。