SQLite ntile 分桶:分数相同也会跨组,多出的行又先分给谁

前天 3阅读

把任务按分数排成三批时,很容易把 ntile(3) 理解成“相同分数一定放在一组”。实际上,它的目标是按窗口顺序让各桶的行数尽量均匀。相同分值碰上桶边界,可以被拆开;总行数不能整除时,靠前的桶会多分到一行。

下面用八条虚构数据,刻意把三个20分放在第一、第二桶交界处。程序在 Node.js v24.19.0 和内置 SQLite 3.53.3 上实跑,需要支持 node:sqlite 的版本。保存为 demo.mjs,运行 node --no-warnings demo.mjs。只使用内存数据库,不写入现有业务表;同样可以把其中SQL交给具备窗口函数的SQLite工具运行。

SQLite ntile 分桶:分数相同也会跨组,多出的行又先分给谁

AI模型生成概念示意:八枚圆片分入三个容器,数量为三、三、二;图中颜色仅辅助区分图形,不代表示例记录的分数、编号或精确分配顺序。

完整实验

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
try {
  db.exec(`
    CREATE TABLE sample (id INTEGER PRIMARY KEY, score INTEGER NOT NULL);
    INSERT INTO sample VALUES
      (1, 10), (2, 20), (3, 20), (4, 20),
      (5, 30), (6, 40), (7, 50), (8, 60);
  `);
  const rows = db.prepare(`
    SELECT id, score, ntile(3) OVER (ORDER BY score, id) AS bucket
    FROM sample ORDER BY score, id
  `).all();
  for (const row of rows) console.log('row', JSON.stringify(row));
  const sizes = db.prepare(`
    WITH assigned AS (
      SELECT ntile(3) OVER (ORDER BY score, id) AS bucket FROM sample
    )
    SELECT bucket, count(*) AS size FROM assigned
    GROUP BY bucket ORDER BY bucket
  `).all();
  console.log('sizes', JSON.stringify(sizes));
  const sparse = db.prepare(`
    SELECT id, ntile(5) OVER (ORDER BY id) AS bucket
    FROM sample WHERE id <= 2 ORDER BY id
  `).all();
  console.log('more-buckets', JSON.stringify(sparse));
  try {
    db.prepare('SELECT ntile(0) OVER (ORDER BY id) FROM sample').all();
  } catch (error) {
    console.log('invalid-count', error.message);
  }
} finally {
  db.close();
}

本地实际输出

row {"id":1,"score":10,"bucket":1}
row {"id":2,"score":20,"bucket":1}
row {"id":3,"score":20,"bucket":1}
row {"id":4,"score":20,"bucket":2}
row {"id":5,"score":30,"bucket":2}
row {"id":6,"score":40,"bucket":2}
row {"id":7,"score":50,"bucket":3}
row {"id":8,"score":60,"bucket":3}
sizes [{"bucket":1,"size":3},{"bucket":2,"size":3},{"bucket":3,"size":2}]
more-buckets [{"id":1,"bucket":1},{"id":2,"bucket":2}]
invalid-count argument of ntile must be a positive integer

八行分三桶,先给每桶两行,还剩两行,于是前两桶各补一行,得到 3、3、2。sizes 的实际输出就是这个结果。每行的 bucket 从1开始,反映它在这次完整查询中的位置分配,而不是表中永久保存的属性。

id为2、3、4的三条记录都为20分,其中前两条进入桶1,第三条进入桶2。查询写了 ORDER BY score, id,使用唯一的 id 把相同分数之间的顺序也固定下来。只按 score 排序时,相同分数的内部次序没有被这段SQL完整规定,不适合要求重复执行后每个编号都落到相同桶的场景。

窗口里的排序决定分桶;查询最外面的 ORDER BY 决定结果展示。两个位置虽然写了相同字段,却各管一件事。若仅改变最外层排序,看到的行顺序会变,窗口函数已经算出的桶号不会因此重算。

边界与使用约定

more-buckets 只保留两行,却指定五桶,返回的桶号只有1、2。查询不会额外造出三个没有记录的行。如果报表必须显示全部五个桶,即使数量为零也要出现,应另备桶编号表再关联结果。不能因为传了5,就断言这次查询一定返回5个非空组。

invalid-count 捕获了 ntile(0) 的错误,提示参数必须是正整数。业务入口应明确校验桶数为正整数,不依赖隐式数值转换处理小数或文字。动态桶数可以使用参数绑定,但绑定成功仍不等于分桶数量合理。

需要“同分必须同组”时,应先定义分数阈值或对分数等级制定分组规则,并接受各组行数不一定均匀。若目的是把处理工作尽量平均分批,ntile更合适,但它不会考虑每条任务的耗时差异。新增、删除或筛选记录还会改变分桶结果,因此稳定归属需求应把分配另行存储,而不是每次临时重算。

参考资料

官方资料核验日期:2026-10-02。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。