SQLite EXCLUDE TIES:排除同分记录时,为什么当前这一行仍然参加求和

前天 3阅读

做一张按评分展示的汇总表,要求暂时排除与当前记录同分的其他记录,却保留当前记录本身。EXCLUDE TIES可以表达这个关系,但如果把TIES理解为把整组并列项全删掉,就会算出另一份结果。整组排除对应的是EXCLUDE GROUP。

下面有四行,score分别为十、十、二十、三十,amount分别为二、三、五、七,总额十七。窗口明确覆盖整个分区,所以每一行在排除之前都包含全部四行;比较时唯一变化是EXCLUDE规则,避免累计边界掩盖差异。

保存为demo.mjs,用node demo.mjs执行,使用Node.js内置node:sqlite连接内存数据库。本文在Node.js v24.19.0、SQLite 3.53.3实跑,无磁盘数据库。查询同时返回四种规则,最终按id排列便于逐行核算。

SQLite EXCLUDE TIES:排除同分记录时,为什么当前这一行仍然参加求和

AI模型生成的概念插图:当前同色圆点留在框内,其他同色圆点移到框外,异色圆点仍保留;只表示排除关系,不是查询结果截图。

完整可运行程序

import assert from 'node:assert/strict';
import {DatabaseSync} from 'node:sqlite';

const db = new DatabaseSync(':memory:');
try {
  const rows = db.prepare(`
    WITH sample(id, score, amount) AS (
      VALUES (1, 10, 2), (2, 10, 3), (3, 20, 5), (4, 30, 7)
    )
    SELECT id,
      sum(amount) OVER (base
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE NO OTHERS) AS all_rows,
      sum(amount) OVER (base
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE CURRENT ROW) AS without_current,
      sum(amount) OVER (base
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE GROUP) AS without_group,
      sum(amount) OVER (base
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        EXCLUDE TIES) AS without_ties
    FROM sample
    WINDOW base AS (ORDER BY score)
    ORDER BY id
  `).all().map(row => Object.values(row));
  assert.deepEqual(rows, [
    [1, 17, 15, 12, 14], [2, 17, 14, 12, 15],
    [3, 17, 12, 12, 17], [4, 17, 10, 10, 17]
  ]);
  console.log('id all current group ties');
  for (const row of rows) console.log(row.join(' '));
} finally {
  db.close();
}

本次实际输出

id all current group ties
1 17 15 12 14
2 17 14 12 15
3 17 12 12 17
4 17 10 10 17

先手算第一行,再对照四列

第一行的四个金额结果依次是十七、十五、十二、十四。NO OTHERS不排除任何行;CURRENT ROW只减掉自己amount为二的这一行,得到十五;GROUP连同另一个同分行一起去掉,减掉二与三,得到十二。

TIES排除其他同序值记录,保留已经处在frame里的当前行,所以第一行只减三,得到十四。第二行反过来保留自己的三、排除另一行的二,得到十五。两行的GROUP结果一样,TIES结果不同,正好揭示了当前行身份仍然重要。

ROWS不会让同伴关系失效

本例frame类型是ROWS,但EXCLUDE判断同伴时仍看窗口ORDER BY的值。score为十的两条记录因此互为同伴;不会因为frame按行描述,就把它们当作完全无关的两个位置。这是与单纯理解行范围不同的一层规则。

score为二十、三十的行没有其他同分同伴,TIES列便都等于十七。GROUP则仍会排除当前行自己,因此第三、四行分别是十二与十。单独检查没有并列值的数据,容易把TIES误认为“这个选项没效果”。

窗口排序也定义了谁算同伴

不要为了让页面显示稳定,就随手把唯一id加入窗口内部的ORDER BY。那会使原先同score的两行不再具有完全相同的排序值,改变同伴分组。本例只在查询最外层按id排序,它控制展示,不改变窗口按score建立的关系。

这里让base命名窗口只含ORDER BY,各个OVER再补完整frame与EXCLUDE。SQLite窗口链不允许base已经带着frame后又被这样继承;如果为了少写几行而把全部frame移入base,再在外面只补EXCLUDE,会遇到语法或窗口继承限制。

EXCLUDE处理的是已选frame里的成员,并不会把原本在frame之外的当前行重新加回来。本文使用全分区frame,当前行天然在里面,才方便用“保留自己”解释TIES。换成仅含前几行的frame时,要先列范围,再应用排除。

排查报表时,按“分区有哪些行、窗口排序如何定义同伴、frame先选哪些行、EXCLUDE再去掉谁”的顺序手算。保留一组同分两行和两条独立分数,检查成员集合,比只比对一个总和更容易定位口径错误。

官方资料核验日期:2026年10月2日。上述输出来自本文完整程序的本地执行,全部断言通过,退出状态为零。

参考资料

SQLite官方文档:窗口EXCLUDE子句

SQLite官方文档:Window Chaining的frame限制

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