🦏 第5章 高级查询
📄 一句话概括

rand() 给一个 0 到 1 之间的小数,每次调用都不一样。 rand(种子) 给固定的一串,同一个种子永远同一串。 order by rand() limit n 是「随机抽 n 条」——Demo 里连跑两次,抽到的三件完全不同。 代价是全表排序:每行算一个随机数再整体排,索引一点用不上。 大表要换成「随机 id」或「随机 offset」。

📝 本章和前面几章的关系

上一篇按条件找,这一篇不要条件,要「随便来几条」。 用途很实在:抽奖、随机推荐、随机出题、抽样核对数据。 但它也是被误用最多的函数之一——小表随便写,大表直接拖垮库。

一、两个用法:无种子与有种子

rand() 每次给不同的数,Demo 里给一个四位小数。 所以它不能用来做「可复现」的事——结果没法写进测试用例。 rand(7) 用 7 当种子,结果固定。 同一句里写两次 rand(7),两个数完全一样, 因为每次都从同一个种子重新开始。 规矩:造测试数据、做抽样脚本,一律用带种子的 rand(固定值); 只有「就是要每次不同」的场景才用无种子的。

二、随机抽几条:order by rand() limit n

写法:SELECT ... FROM 表 ORDER BY RAND() LIMIT 3。 Demo 里连跑两次,抽到的三件完全不同。 原理:给每一行算一个随机数, 按这个随机数排序,再取前三条。 所以它是「全表 + 排序」,不是「随便挑三条就走」。 本沙箱的 rand() 是垫片(用 Python 的 random 实现),数值序列跟 MySQL 对不上, 但「每次都不同」这个行为一致。 所以 Demo 里的具体数字不必和 MySQL 的一致。

三、代价:它是「全表排序」

order by rand() 要给每一行算一次随机数、再整体排序, 复杂度是「全表 + 排序」。 索引帮不上忙,临时表也用得上。 十万行以内还能忍,上百万行就是灾难——每次请求都要排一遍全表,并发一上来直接把库拖死。 三种替代:一、先随机一个 id 再取(WHERE id >= 随机值 ORDER BY id LIMIT 3); 二、先 count(*) 拿总数,随机一个 offset 配 LIMIT offset, n; 三、真要「不重复地抽」,把抽过的记下来。

四、一个容易忽略的点:抽到的不是「均匀样本」

order by rand() limit 3 每一行被抽中的概率是相同的,这一点没问题。 但「抽三次、每次三条」不等于「随机分三组」。 做数据抽样核对时要当心:随机抽 100 行看质量,抽到的永远是「随机分布」的 100 行, 按时间、按地区成片出错的那类问题,很可能一条都抽不到。 抽样要分层:按类别各抽几条,比「全表随机抽」更能发现问题。

五、动手 Demo:给种子与不给种子,抽到的完全不是一回事

Demo 四段:先看无种子的 rand(),再给种子固定下来,然后连抽两次三条商品。 注意:抽奖那两句的输出只是其中一次的样本,你自己跑, 抽到的三件一定不是这三件——这正是这一篇的重点。

import _q46 as Q
c = Q.con()

# 无种子:每次都变
c.execute("SELECT CAST(ROUND(RAND(), 4) AS TEXT) AS r")
# 有种子:结果固定
c.execute("SELECT CAST(ROUND(RAND(7), 4) AS TEXT) AS a, "
          "CAST(ROUND(RAND(7), 4) AS TEXT) AS b")
# 随机抽三条:两次结果不一样
c.execute("SELECT name FROM goods ORDER BY RAND() LIMIT 3")
c.execute("SELECT name FROM goods ORDER BY RAND() LIMIT 3")

实跑输出:

--------------------------------------------------------------------
第二十一课:rand(),以及它对大表做了什么
--------------------------------------------------------------------

  rand() 给一个 0 到 1 之间的小数
  SQL> SELECT CAST(ROUND(RAND(), 4) AS TEXT) AS r
  r
  -------
  0.8053
  (1 行)

  **每跑一次都不一样。** **这正是「随机」的定义,也是它不能用来做「可复现结果」的原因。**

  给个种子:结果就固定了
  SQL> SELECT CAST(ROUND(RAND(7), 4) AS TEXT) AS a, CAST(ROUND(RAND(7), 4) AS TEXT) AS b
  a          b
  ------------------
  0.3238     0.3238
  (1 行)

  **`RAND(7)` 用 7 当种子,两次都算出同一个数。** **换种子就换一串数,同一个种子永远同一串。**
  **所以测试里要随机数据,别用 `RAND()`,用 `RAND(固定种子)`**——否则今天过、明天挂,查都没法查。

  随机取几条:order by rand() limit n
  SQL> SELECT name FROM goods ORDER BY RAND() LIMIT 3
  name
  -------
  运动鞋
  护腕
  瑜伽垫
  (3 行)

  SQL> SELECT name FROM goods ORDER BY RAND() LIMIT 3
  name
  ---------
  篮球
  乒乓球拍
  跳绳
  (3 行)

  **两次抽到的三件不一样。** **这段输出只是其中一次的样本**,你再跑一次,三件肯定又变了。
  **它的原理**:给每一行算一个随机数,**按这个随机数排序,再取前三条。** **所以是全表排序**,
  不是「随便挑三条就走」。

  代价:十万行才看得出问题
  **`order by rand()` 要给每一行都算一次随机数、再整体排序**,
  **复杂度是「全表 + 排序」,索引一点用不上。**
  **小表随便用,几万行以上就得换思路**:
  **一、先随机一个 id 区间再取**:`where id >= 随机值 order by id limit 3`;
  **二、程序里先 `count(*)` 拿到总数,随机一个偏移量配 `limit offset, n`;**
  **三、真要「每次都不重复地抽」,把抽过的记下来,别靠 `rand()` 硬碰。**

  **顺带一句**:`rand()` 是 MySQL 的函数,标准 SQL 里没有。
  **本沙箱的 `rand()` 是垫片**(用 Python 的 random 实现的),
  **数值序列跟 MySQL 对不上,但「每次都不同」这个行为是一致的。**

第一段无种子。 给一个四位小数,每次都不一样。 第二段给种子。 rand(7) 两次都是 0.3238——同一种子同一串,这才叫可复现。 第三段抽两次。 六件商品,抽出来的三条两次都不同。 这不是「随机数变了」,是「排序结果变了」。 第四段没有 SQL, 全在讲代价与三种替代写法——order by rand() 的开销才是这一篇最该带走的东西。

30 秒实验

  1. 把 RAND(7) 换成 RAND(8),两个数还是 0.3238 吗?
  2. ORDER BY RAND() LIMIT 3 改成 LIMIT 6,第二次还会换吗?为什么?
  3. 连续跑三次同一句 ORDER BY RAND() LIMIT 3,有没有可能两次抽到一样的?

常见误区

  1. 在测试里用无种子的 rand()。 今天过、明天挂, 而且查不出原因——因为「昨天那个随机数」再也复现不了。
  2. 在百万行的表上写 order by rand()。 每次请求都排一遍全表,几个人同时点就把库拖死。
  3. 以为随机抽样能查出所有问题。 它按行均匀抽,成片出错(按时间、按地区、 按批次)的那类问题经常一条都抽不到。
自测三问(点开对答案) 1. `rand()` 与 `rand(7)` 有什么区别? —— `rand()` 每次给不同的数;`rand(7)` 用 7 当种子,同一个种子永远给同一串数。 2. `order by rand() limit 3` 是怎么实现的? —— 给每一行算一个随机数,按它整体排序,再取前三条。所以是全表加排序。 3. 大表要随机取几条,怎么写更稳? —— 先随机一个 id 区间再取,或者先 `count(*)` 拿到总数、随机一个 offset 配 `limit`。

like 和 rand() 都只在一条语句里折腾。 下一篇把一条查询的结果喂给另一条——子查询。

相关笔记