You need to enable JavaScript to run this app. 如何在Sequelize TypeScript中按多列分组获取组内最新条目
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.04 00:54:00