如何在Sequelize TypeScript中按多列分组获取组内最新条目
Sequelize TypeScript 按分组维度获取最新业务时间对应条目实现方案
问题背景
现有MySQL表中存在多条核心字段近似、仅createdAt业务时间存在差异的记录,需要基于colA、colB两个字段执行分组操作,返回每个分组下业务时间最新的完整条目。
原参考原生SQL的实现逻辑是通过取分组内最大id定位最新条目,但该逻辑在自增id与业务时间顺序不一致的场景下(比如批量导入历史数据、回溯修改时间戳)会出现结果偏差,需要适配按业务时间字段定位最新条目的需求。
开发者之前尝试的常规分组写法:
MyModel.findAll({ where: { createdAt: { [Op.lt]: today } }, group: ['colA','colB'] })
实际返回结果为每个分组下最早/随机匹配到的条目,和预期获取分组最新数据的目标不符。
问题根源
直接在findAll中配置group参数的写法,需要搭配聚合函数明确指定每个非分组列的取值规则。在MySQL开启ONLY_FULL_GROUP_BY严格模式时,未声明聚合规则的非分组列会直接触发语法报错;未开启严格模式时,数据库会返回分组内随机一行的字段值,完全无法保证返回的是时间最新的条目。
可落地实现方案
方案一:分步查询适配(无数据库版本限制,逻辑易理解)
核心思路是先取出每个分组对应的最大业务时间,再匹配回完整的表记录,和原生SQL逻辑完全对等,仅将聚合目标从自增id替换为业务时间字段。
实现代码:
import { Op } from 'sequelize'; import MyModel from './my-model'; // 第一步:按colA、colB分组,取出每组的最大createdAt值 const groupMaxTimeList = await MyModel.findAll({ attributes: [ 'colA', 'colB', [MyModel.sequelize!.fn('MAX', MyModel.sequelize!.col('createdAt')), 'maxCreatedAt'] ], where: { createdAt: { [Op.lt]: today } // 保留原有业务过滤条件 }, group: ['colA', 'colB'], raw: true // 直接返回原始结构化数据,不生成Model实例 }); // 第二步:构造多维度匹配条件,将分组维度和对应最大时间组合为匹配规则 const matchRuleList = groupMaxTimeList.map(item => ({ colA: item.colA, colB: item.colB, createdAt: item.maxCreatedAt })); // 第三步:查询所有匹配规则对应的完整条目 const latestGroupRecords = await MyModel.findAll({ where: { [Op.or]: matchRuleList } });
注意:如果存在同分组下
createdAt值完全相同的极端场景,可在匹配条件中增加自增id的兜底过滤(比如同时匹配MAX(id)),避免同时间点的多条重复记录被全部返回。
方案二:窗口函数实现(MySQL 8.0+ 版本适用,单次查询完成)
对于支持窗口函数的数据库版本,可通过窗口函数排序直接精准定位每个分区下的最新条目,避免两次查询的性能开销,也不会出现同时间戳重复值导致的结果异常。
实现代码:
import { literal } from 'sequelize'; import MyModel from './my-model'; const latestGroupRecords = await MyModel.findAll({ attributes: { include: [ // 定义窗口排序列:按colA、colB分区,按createdAt倒序排名 [ literal(`ROW_NUMBER() OVER (PARTITION BY colA, colB ORDER BY createdAt DESC)`), 'inner_partition_row_num' ] ] }, where: literal(` inner_partition_row_num = 1 AND createdAt < '${today}' `) });
常见避坑提示
- 不要强行通过调整
group参数的字段顺序,或者在group中加入id、createdAt字段来实现取最新的需求:这种写法会彻底打破原有的分组维度,相当于没有做分组聚合。 - 不要硬搬原IN子查询的
MAX(id)逻辑套在业务时间上:如果存在数据回溯、时间戳修改、批量导入的场景,id和业务时间顺序不一致时,用MAX(id)取最新就会出现结果偏差。 - 方案一在分组数量特别多的场景下,会生成超长的OR匹配条件,查询性能会明显下降,这种场景优先选择窗口函数方案,或直接编写原生SQL实现。
- 注意窗口函数的执行顺序:MySQL中WHERE子句的执行顺序早于SELECT子句的计算列,不能直接在Sequelize的普通where对象中引用窗口函数生成的计算列,需要通过
literal在同一查询层级声明过滤规则,避免触发“未知列”的语法错误。
内容的提问来源于stack exchange,提问作者Yatin Gaikwad
相关产品推荐
相关产品推荐

