🪟 第8章 SQL数据查询基础
📄 一句话概括

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 秒实验

  1. 跑一次 SELECT DISTINCT cat, gname FROM tb_goods,再看 SELECT DISTINCT cat。 前者的行数等于原表行数——这就是「作用在整个列表」的意思。
  2. 把 TOP 3 改成 TOP 3 WITH TIES,再看返回行数。如果排序列有并列,行数会变多——这不是 bug。
  3. 想一想:为什么 TOP 一定要配 ORDER BY? 因为表本身没有顺序。 不指定排序,「前 3 行」就没有定义——数据库只能随便给你 3 行。

常见误区

  1. 「COMPUTE 还能用」。2012 就移除了,现在写直接报错。 看到老代码里的 COMPUTE,就当技术债——能改成 ROLLUP 就改。
  2. 「DISTINCT 是按第一列去重」。它作用于整个 SELECT 列表。 写 SELECT DISTINCT cat, gname 等于没去重,因为这两个的组合本来就不重复。
  3. 「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 倍,而且数据量越大差得越多。

相关笔记