分层抽样需用 ROW_NUMBER() OVER (PARTITION BY category ORDER BY RAND()) 实现每类独立随机取样,确保类别平衡;NTILE() 适合全局或分组内等频切分,但不保证严格均等。

用 PARTITION BY 做分层抽样,不是加个 ORDER BY RAND() 就完事
直接在 WHERE 里套 ORDER BY RAND() LIMIT N 会破坏类别分布——尤其当某些类样本极少时,大概率被漏掉。真正要保平衡,得先按类别分组,再在每组内独立随机取样。
核心是:用 ROW_NUMBER() OVER (PARTITION BY category ORDER BY RAND()) 给每类内部打乱序号,再筛出前 K 行。
-
PARTITION BY必须指定真实存在的类别字段(比如label、status),不能是表达式或别名 - MySQL 8.0+、PostgreSQL、SQL Server、BigQuery 都支持;但 SQLite 和旧版 MySQL 不行
-
ORDER BY RAND()在大数据量下性能差,可改用ORDER BY ABS(CHECKSUM(NEWID()))(SQL Server)或ORDER BY RANDOM()(PostgreSQL)替代 - 如果某类总数少于目标样本数,该类会全量入选——这是合理行为,不是 bug
NTILE() 适合等频切分,但不保证每组数量一致
当你需要把数据“大致均分”成 N 份(比如做交叉验证的 fold 划分),NTILE(N) 比手动计数更简洁。但它按总行数除以 N 向上 / 向下取整分配,类别平衡得靠前置 PARTITION BY。
- 单独用
NTILE(5) OVER (ORDER BY id)只是全局切分,完全不管类别 - 要保类别平衡,必须写成
NTILE(5) OVER (PARTITION BY category ORDER BY RAND()) - 结果中每个
category下的NTILE值范围都是1到5,但各组实际行数可能差 1 行(因整除余数) - PostgreSQL 中
NTILE对空值敏感,NULL类别会被归到同一组,需提前WHERE category IS NOT NULL
抽样后还要 join 原表?别重复计算 ROW_NUMBER()
如果抽样只是中间步骤,后续还要关联其他表或加计算字段,反复写窗口函数不仅难读,还可能因执行计划导致多次排序。
- 用 CTE 预先算好带序号的子集:
WITH sampled AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY RANDOM()) AS rn FROM dataset ) SELECT * FROM sampled WHERE rn <= 100; - 别在
WHERE里直接写窗口函数——语法报错:ERROR: window functions are not allowed in WHERE - CTE 不是万能的,BigQuery 中过度嵌套 CTE 可能触发资源限制;大表建议物化为临时表
类别字段有倾斜或空值时,PARTITION BY 会悄悄失效
看起来 SQL 跑通了,但抽出来某几类特别少,大概率是类别字段本身有问题:大量 NULL、空字符串、前后空格、大小写混用,或者存在隐藏控制字符。
- 先检查:
SELECT category, COUNT(*) FROM dataset GROUP BY category ORDER BY COUNT(*) -
PARTITION BY TRIM(UPPER(category))比裸写category更稳妥 - 空值会被归为同一组,若不想让它们参与抽样,显式过滤:
WHERE category IS NOT NULL AND category != '' - 某些数据库(如 Hive)对
PARTITION BY字段类型敏感,string和varchar(32)视为不同分区,需统一 cast
抽样逻辑看似简单,但窗口函数的分区边界、空值处理、数据库方言差异,三者叠加就容易漏掉一类样本——尤其当类别是业务关键维度时,这种遗漏很难被测试数据立刻发现。