TABLESAMPLE 是基于数据页或块的近似抽样,非逐行随机,结果不精确且易偏斜;适用于快速数据探查,不适用于统计推断。

TABLESAMPLE 是近似抽样,不是随机选行
很多人以为 TABLESAMPLE 会像 ORDER BY RAND() 那样逐行打乱再取前 N 条,实际不是。PostgreSQL 和 SQL Server 的 TABLESAMPLE 是基于数据页(page)或块(block)的采样,它先随机挑若干存储块,再把块里所有行都拉出来——这意味着:样本可能严重偏斜(比如某块全是 NULL 值)、行数不精确(指定 10% 可能返回 7% 或 12%)、且无法保证均匀分布。
适用场景有限:只适合快速看数据“长什么样”,比如检查字段是否为空、类型是否异常、有无乱码;不适合做统计推断或质量评估。
- PostgreSQL 中必须配合
SYSTEM或BERNOULLI子句,BERNOULLI是按行概率抽(更准但慢),SYSTEM是按块抽(快但不均) - MySQL 不支持
TABLESAMPLE,别在 MySQL 里试,会报错ERROR 1064 - SQL Server 默认是
SYSTEM模式,且对分区表行为更复杂,抽样结果可能集中在某个分区
RAND() 排序抽样在大表上会触发全表扫描和临时排序
写 SELECT * FROM big_table ORDER BY RAND() LIMIT 1000 看似简单,但对千万级以上表极其危险:数据库要为每一行计算一次 RAND(),生成临时列,再对整张表排序——内存爆掉、磁盘排序、执行十几分钟都是常态。
这不是语法问题,是算法复杂度问题:O(n log n) 时间 + O(n) 空间,n 是总行数。
- MySQL 8.0+ 对
RAND()有部分优化,但没改根本逻辑,仍不推荐用于 >100 万行的表 - PostgreSQL 的
ORDER BY RANDOM()同样全表扫描,且RANDOM()不是确定性函数,无法用索引加速 - 如果只要“任意几行”,其实不需要真排序——跳过前 K 行再取,比排序快得多
更靠谱的大表抽样:OFFSET + RANDOM() 控制起始点
利用主键或自增 ID 的连续性(或近似连续),可以避开排序。假设表有自增 id 字段,且最小值是 1、最大值是 10000000:
SELECT * FROM big_table WHERE id >= FLOOR(RAND() * (10000000 - 1000)) + 1 LIMIT 1000;
这比 ORDER BY RAND() 快两个数量级,因为走的是主键范围扫描。但要注意:
- ID 必须基本连续,如果删得太多(空洞率 >30%),抽到的行数会明显少于预期
- 需要先查出
MIN(id)和MAX(id),这两条是轻量查询,可缓存 - 若用时间字段(如
created_at)代替 ID,需确保该字段有索引,且分布相对均匀 - 不要用
OFFSET FLOOR(RAND() * N)——MySQL 在大 offset 下性能极差,会扫掉前面所有行
真正生产可用的方案:分桶抽样 + LIMIT
当表没有合适连续字段,又不能接受 TABLESAMPLE 的偏差时,可以用分桶法:把表逻辑切分成固定数量桶,每个桶内随机取少量行。例如分 100 桶,每桶取 10 行,共 1000 行:
SELECT * FROM big_table WHERE ABS(HASH(id)) % 100 = FLOOR(RAND() * 100) LIMIT 1000;
这里 HASH(id) 是示意(PostgreSQL 用 hashtext(),MySQL 用 CRC32()),关键是让哈希值模一个固定数后均匀分布。实际中建议先测试哈希分布是否平坦——有些哈希函数对小整数输出集中,导致某些桶永远抽不到。
- 比
ORDER BY RAND()快,因为不排序,只过滤 + 限流 - 比
TABLESAMPLE更可控,行数接近目标值 - 缺点是仍需全表扫描(除非加了合适的 WHERE 条件提前剪枝)
- 如果表有分区,优先在每个分区里独立抽样,避免跨分区倾斜
抽样这件事,本质是在精度、速度、实现成本之间做取舍。越想“真正随机”,代价越高;日常数据探查,用带偏移的 ID 范围查询已经够用——别一上来就迷信 RAND(),也别高估 TABLESAMPLE 的适用边界。

















