COMPUTE 已在 SQL Server 2012 被移除,替代方案是 ROLLUP——教材留着是因为老代码里能看到。
DISTINCT 作用于整个 SELECT 列表,不是只作用于第一列——这是最常见的误用。
TOP 必须配 ORDER BY,否则「前 3 行」等于「随便 3 行」;
WITH TIES 会把并列的也带出来。
这一节是 8.1 的收尾,讲四个「附加子句」。它们不是必需的,但能省很多事。
教材 8.1c 分了五小节:COMPUTE、DISTINCT、TOP、WITH TIES、UNION。
第一小节要打个折扣——COMPUTE 已经废弃了,学它是为了看懂老代码,不是为了用。
演示里五项全跑了(COMPUTE 只能贴原码,用 UNION ALL 拼合计复刻),
WITH TIES 用 RANK() 窗口函数复刻——SQLite 没有这个语法,但行为能演出来。
一、COMPUTE:已经废弃了
SELECT cat, SUM(price) FROM tb_goods
GROUP BY cat
COMPUTE SUM(SUM(price))
COMPUTE 用来在结果末尾加一行汇总。 但它在 SQL Server 2012 就被移除了——
现在写会报「COMPUTE 子句不再受支持」。
教材还留着这一小节,是因为老系统里能看到。 接手一个十年前的项目,
脚本里冒出 COMPUTE 是正常的——看懂它干什么就行,新代码别用。
替代方案有两个: GROUP BY ROLLUP(...) 自动加合计行,或者用 UNION ALL 手动拼一行。
前者更干净,后者更通用(SQLite 也认)。
ROLLUP 的合计行里,分组列是 NULL。 可以靠这个区分「明细」和「汇总」——
或者用 GROUPING() 函数判断,那个更准。
二、DISTINCT:去重
DISTINCT 去掉重复的行——注意是「行」,不是「列」。
SELECT DISTINCT cat FROM tb_goods -- 两个类别
SELECT DISTINCT cat, gname FROM tb_goods -- 七行,等于没去重
第二行是本节最该记住的坑:DISTINCT 作用于整个 SELECT 列表,不是只作用于第一列。
cat 加 gname 的组合本来就不重复(商品名唯一),所以 DISTINCT 等于没写。
很多人以为 DISTINCT 是「按第一列去重」,写出来发现行数没变,
就去怀疑数据——其实语句本身就没表达出他想要的意思。
COUNT(DISTINCT 列) 是另一个用法:数「有多少个不同的值」。这个写法各家都支持,
而且比「SELECT DISTINCT 再数行数」快。
DISTINCT 要排序去重,数据量大时慢。 而且它常常掩盖问题——
该用 GROUP BY、或者该改表结构的时候,有人拿 DISTINCT 挡一下。
三、TOP:取前 N 行
SELECT TOP 3 gname, price FROM tb_goods ORDER BY price DESC
这是 SQL Server 的写法。 SQLite 和 MySQL 用 LIMIT,
Oracle 用 ROWNUM——三家的写法完全不同,这是 6.1 那张方言表的第一行。
TOP 必须配 ORDER BY。 不写的话「前 3 行」是「随便 3 行」——
因为表本身没有顺序,你说不出「前」是什么意思。
TOP 还能配百分比:TOP 10 PERCENT。 取前 10%——
注意它会向上取整,10 行的 10% 是 1 行,9 行的 10% 也是 1 行。
TOP 在带 UPDATE、DELETE 的时候也能用:DELETE TOP 100 FROM 表。
这是「分批删」的标准写法——一次删一百万行会锁很久,分一万批删就平顺了。
四、WITH TIES:并列的也要
SELECT TOP 3 WITH TIES gname, cat FROM tb_goods ORDER BY cat
WITH TIES 的意思是「第 N 名如果并列,把并列的也带出来」。
所以 TOP 3 WITH TIES 可能返回 3 行,也可能返回 5 行——取决于有没有并列。
这个行为经常被误解成「多了就是 bug」。 它不是 bug,
是 WITH TIES 的定义——「带并列」就是它跟普通 TOP 的唯一区别。
它必须配 ORDER BY,而且必须写在 TOP n 后面。顺序写反了会报语法错误——
TOP 3 WITH TIES,不是 WITH TIES TOP 3。
用窗口函数也能做同样的事: RANK() OVER (ORDER BY 列) <= 3。
两种写法结果一样,但窗口函数更灵活——它能做「每个类别取前三」这种分组排名。
五、UNION:竖着拼两个结果
SELECT gname FROM tb_goods WHERE cat = '电脑'
UNION
SELECT gname FROM tb_goods WHERE price > 1000
UNION 把两个结果「上下接起来」,并且去重。 上面这段:左边 4 行、右边 2 行,
UNION 之后 4 行——笔记本同时满足两个条件,只出现一次。
两个查询的列数必须相同、类型要能对上。 列名以前面那个为准——
后面的列名被忽略,这一点经常让人困惑。
UNION 要排序去重,所以慢。 确定不会有重复时用 UNION ALL——
下一节 8.2 专门讲这个差别,实测差了 3 倍。
UNION 还有一种用法:把「多张结构相同的表」拼成一张。比如按月分表存订单,
查一整年就是 12 个 SELECT 用 UNION ALL 接起来——这是分表方案里最常见的写法。
六、动手 Demo:COMPUTE 已废弃,其余四项照跑
这一段先贴 COMPUTE 原码说明它已废弃,再用 UNION ALL 拼合计复刻;
然后跑 DISTINCT、TOP(用 LIMIT)、WITH TIES(用 RANK())、UNION。
import sqlite3
c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE tb_goods (id INTEGER PRIMARY KEY, gname TEXT,"
u" cat TEXT, price REAL, stock INT)")
c.executemany(u"INSERT INTO tb_goods VALUES (?,?,?,?,?)", [
(1, '笔记本', '电脑', 4999.0, 12), (2, '键盘', '电脑', 199.0, 40),
(3, '鼠标', '电脑', 89.5, 0), (4, '显示器', '电脑', 1299.0, 7),
(5, '打印纸', '办公', 25.0, 200), (6, '订书机', '办公', 18.5, 60),
(7, '文件夹', '办公', 6.0, 150)])
# DISTINCT 作用于整个列表
print(len(c.execute(u"SELECT DISTINCT cat FROM tb_goods").fetchall()))
print(len(c.execute(u"SELECT DISTINCT cat, gname FROM tb_goods").fetchall()))
# TOP 3 -> LIMIT 3
print(c.execute(u"SELECT gname FROM tb_goods ORDER BY price DESC LIMIT 3").fetchall())
# WITH TIES -> RANK()
q = u"""SELECT gname, cat FROM (
SELECT gname, cat, RANK() OVER (ORDER BY cat) AS rk
FROM tb_goods) WHERE rk <= 3"""
print(len(c.execute(q).fetchall()))
实跑输出:
--------------------------------------------------------------------
DISTINCT、TOP、WITH TIES、UNION
--------------------------------------------------------------------
① COMPUTE 子句:已经废弃了
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT cat, SUM(price) FROM tb_goods
| GROUP BY cat
| COMPUTE SUM(SUM(price))
COMPUTE 用来在结果末尾加一行汇总。但它在 SQL Server 2012 就被移除了——
现在写会报「COMPUTE 子句不再受支持」。替代方案是 ROLLUP 或者 UNION ALL 拼一行。
教材还留着这一小节,是因为老系统里能看到。新代码别用。
用 UNION ALL 拼一行合计来复刻同样的效果:
类别 金额和
办公 49.50
电脑 6586.50
合计 6636.00
SQLite 没有 ROLLUP(那是 SQL Server 和 Oracle 的扩展),所以只能手动拼一行。
真实 SQL Server 里一句 GROUP BY ROLLUP(cat) 就够——它会自动加一行合计,
而且合计行的分组列是 NULL,可以靠这个区分明细和汇总。
② DISTINCT:去重
--------------------------------------------------------------------
所有类别:电脑、办公
一共 2 行(原表 7 行)
COUNT(DISTINCT cat) = 2
DISTINCT 作用于「整个 SELECT 列表」,不是只作用于第一列。
SELECT DISTINCT cat, gname 是「cat 和 gname 的组合不重复」——因为 gname 本来就唯一,
所以这条语句会返回全部 7 行,DISTINCT 等于没写。这是最常见的误用。
DISTINCT 要排序去重,数据量大时很慢。而且它常常掩盖问题——
该用 GROUP BY 或者该改表结构的时候,有人拿 DISTINCT 挡一下。
③ TOP:取前 N 行
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT TOP 3 gname, price FROM tb_goods ORDER BY price DESC
这是 SQL Server 的写法。SQLite 和 MySQL 用 LIMIT,Oracle 用 ROWNUM。
LIMIT 3 跑出来:
gname price
笔记本 4999.00
显示器 1299.00
键盘 199.00
TOP 必须配 ORDER BY。不写的话「前 3 行」是「随便 3 行」——
因为表本身没有顺序,你说不出「前」是什么意思。
④ WITH TIES:并列的也要
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT TOP 3 WITH TIES gname, cat FROM tb_goods ORDER BY cat
用窗口函数 RANK() 复刻同样的效果:
gname cat
打印纸 办公
订书机 办公
文件夹 办公
WITH TIES 的意思是「第 N 名如果并列,把并列的也带出来」。
所以 TOP 3 WITH TIES 可能返回 3 行,也可能返回 5 行——取决于有没有并列。
这个行为经常被误解成「多了就是 bug」。它不是 bug,是 WITH TIES 的定义。
⑤ UNION:竖着拼两个结果
--------------------------------------------------------------------
UNION 之后 4 行:显示器、笔记本、键盘、鼠标
左边 4 行 + 右边 2 行 = 6 行,但 UNION 之后只有 4 行
少的那些是重复的——UNION 会去重。笔记本同时满足两个条件,只出现一次。
要去重就得排序,所以要慢一些。确定不会有重复时,用 UNION ALL。
第一段两个数是 2 和 7。 2 是「两个类别」,7 是「七行——DISTINCT 等于没写」,
因为 cat 加 gname 的组合本来就不重复。这就是那个最常见的误用。
第二段取价格最高的三个:笔记本 4999、显示器 1299、键盘 199。SQLite 用的是 LIMIT 3——
SQL Server 里要写 TOP 3。
第三段那个 RANK() 是在复刻 WITH TIES。 本机数据里没有并列,
所以它只返回 3 行——如果有并列,返回的行数会多。
COMPUTE 那段只能贴原码。 但它替代方案演出来了:
用 UNION ALL 拼一行「合计」——思路是「再查一遍总和,然后接到结果后面」。
30 秒实验
- 跑一次
SELECT DISTINCT cat, gname FROM tb_goods,再看SELECT DISTINCT cat。 前者的行数等于原表行数——这就是「作用在整个列表」的意思。 - 把
TOP 3改成TOP 3 WITH TIES,再看返回行数。如果排序列有并列,行数会变多——这不是 bug。 - 想一想:为什么
TOP一定要配ORDER BY? 因为表本身没有顺序。 不指定排序,「前 3 行」就没有定义——数据库只能随便给你 3 行。
常见误区
- 「
COMPUTE还能用」。2012 就移除了,现在写直接报错。 看到老代码里的COMPUTE,就当技术债——能改成ROLLUP就改。 - 「
DISTINCT是按第一列去重」。它作用于整个SELECT列表。 写SELECT DISTINCT cat, gname等于没去重,因为这两个的组合本来就不重复。 - 「
TOP 3一定返回 3 行」。配WITH TIES就可能多—— 并列的都带出来。不配ORDER BY的话,返回哪 3 行也不保证。
自测三问(点开对答案)
1. `COMPUTE` 还能用吗? —— **不能,SQL Server 2012 就移除了。** 替代方案是 `GROUP BY ROLLUP(...)` 或者 `UNION ALL` 拼一行合计。 **教材留着是为了让你看懂老代码。** 2. `SELECT DISTINCT cat, gname` 为什么等于没去重? —— **因为 `DISTINCT` 作用于整个 `SELECT` 列表。** `cat` 加 `gname` 的组合本来就不重复(商品名唯一),所以去重之后还是全部行。 3. `TOP 3` 和 `TOP 3 WITH TIES` 有什么区别? —— **普通 `TOP 3` 只返回 3 行;`WITH TIES` 会把第 3 名并列的也带出来**,所以可能返回 5 行。 **这不是 bug,是它的定义。**下一篇 8.2 专讲 UNION ALL。它不去重,所以快——实测差了 3 倍,而且数据量越大差得越多。
- ☐ (本篇完,下一篇 8.2 使用 UNION ALL 合并)