ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高效查询每组最新一条记录:SQL优化方案与实战对比

高效查询每组最新一条记录:SQL优化方案与实战对比 1. 问题场景1.1 这个需求为什么值得单独写一篇查询同一用户最新的一条交易记录看起来是个再普通不过的业务查询。但是在我带团队做支付系统重构、面试候选人、以及帮几个社区朋友排查线上慢查询时这个需求反复出现而且几乎每次都会有人写出执行计划非常糟糕的方案。它的难度不在于写出一条能跑的SQL而在于如何在数据量上来之后仍然保持可接受的性能以及如何写出语义足够准确的语句。先说一个我真实遇到的案例。某个做电商积分系统的朋友订单表积压了大约3700万行而他们的后台需要展示每个用户最近的一笔积分变动。初版SQL是网上最常见的写法——用GROUP BY user_id配合MAX(create_time)去子查询里关联。在百万级数据时尚且能跑但到千万级之后直接变成十几秒的超时查询。后来整个模块被拖垮不仅后台卡甚至影响了主库的写入。这就是典型的结构化误判语法没错业务逻辑也没错但执行路径完全走错了。类似的坑几乎每个团队都会踩一遍。这篇文章我就把这个需求从语义分析、方案选型、性能验证到分布式扩展完整拆一遍尽量让不同技术栈、不同数据规模的人都能找到对自己有用的那一段。1.2 典型的业务场景画像这个查询在不同领域里有不同的叫法但本质完全一致都是从一对多关系中取出每个分组的最新/最大/最符合条件的记录。举几个我实际接触过的例子金融交易系统查询每个账户的最后一笔入账用于余额校准或风控预判。电商订单系统展示每个用户的最近一单用于复购分析或营销触达。日志/埋点系统提取每个设备的最新一条上报信息用于在线状态判断。CRM系统取每个客户最近一次跟进记录用于销售漏斗分析。这些场景的共同特点是数据持续增长、分组数量庞大、每组记录数不均衡。最差的用户可能只有1条记录最活跃的用户可能有几万条这种分布特性会直接影响索引策略和SQL写法的选择。很多人在小数据集上写成什么样都能跑但一旦分布变得倾斜某些方案的缺点就会几何级放大。2. 方案选型的底层逻辑2.1 先别急着写SQL把语义界定清楚在动手写SQL之前我建议先在业务层面回答三个问题否则后面容易返工。第一最新的定义到底是基于哪个时间字段交易系统里经常存在多个时间字段比如create_time创建时间、update_time更新时间、pay_time支付时间、callback_time回调时间。如果业务方说最新一笔交易通常指的是下单创建时间但处理退款、售后单时又可能需要支付时间。混用字段会导致同一查询在不同场景下结果不一致。这个我踩过坑曾有一个报表任务用update_time取最新更新,结果因为一条订单反复被修改导致报表里的最新订单和交易详情页展示的不一致排查了整整两天。第二每个用户在业务上是否允许有同一时间戳的多条记录如果不允许那时间字段可以当作唯一键的一部分使用如果允许你必须在时间相同的情况下再定义排序规则比如按主键倒序、按交易金额排序否则查出来的结果不稳定。MySQL在ONLY_FULL_GROUP_BY等模式下会对非聚合列报错但在某些数据库或宽松模式下同样的SQL执行多次可能返回不同行这也是一个需要注意的坑。第三查询是需要返回完整记录还是只需要某个特定字段比如用户ID和最新金额完整记录意味着你必须找到整个行而不仅是聚合结果这会直接影响分组后取最大值的关联这一经典问题。我把这三个问题的回答写进需求模板里让每个接到类似需求的人都先填清楚。有了明确的语义后面SQL怎么写都不容易出现方向性错误。2.2 五种主流方案的横向对比针对取每个分组内最新一条完整记录这个需求业界有几种常见解法我基于不同数据库、数据量和查询模式做了大量实测。先把方案列出来做个对比后面每个方案单独展开。方案适用数据库核心思路数据量优势区间主要风险窗口函数ROW_NUMBER()MySQL 8.0、PostgreSQL、SQL Server、Oracle按用户分区按时间倒序编号取编号为1的行千万级表现优秀需数据库版本支持数据全量排序有内存压力相关子查询全版本通用逐用户找最新时间点再关联原表取全行百万级以内可用数据量大时半连接效率低JOIN GROUP BY全版本通用先按用户取MAX(create_time)再JOIN回原表百万级以内可用需要额外的唯一索引支撑自连接非等值条件全版本通用用LEFT JOIN让每个用户和其他记录比较取没有更晚记录的行小数据量教学常用千万级直接产生笛卡尔积爆炸反范式冗余字段任意数据库在用户表维护latest_transaction_id亿级依然可行需在写入链路中增加状态维护2.3 为什么窗口函数常常是我的首选如果数据库版本允许我在绝大多数场景下首选ROW_NUMBER()窗口函数。原因在于它的执行逻辑非常贴合人类对问题的理解按用户分区在分区内按时间倒序排列给每行一个序号然后只保留序号为1的行。SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM transaction t ) tmp WHERE rn 1;有朋友问过为什么ROW_NUMBER()在千万级数据上比子查询GROUP BY的方案快那么多关键差异在于执行计划。窗口函数只需要对排序后的数据扫描一边而子查询JOIN方案往往需要多次B树索引查找每一条用户记录都可能触发一次回表。举个例子如果有50万个用户每个用户平均20条流水那么JOIN GROUP BY方案在最坏情况下需要50万次索引点查而窗口函数可能是全表扫描一次大排序后者在机械硬盘或者普通云盘上反而更可控。当然窗口函数不是银弹。它的弱点在于所有数据都要进入排序集如果你有上亿条流水那么内存排序区的压力会非常大稍有不慎会把sort_buffer_size或临时表空间打爆。正因如此在极大规模场景下我反而会更倾向于做二级索引或者走反范式方案。为印证这一点我专门用MySQL 8.0配置为16核32GInnoDB默认设置对一张模拟表做了测试。表结构为id BIGINT PRIMARY KEY AUTO_INCREMENT、user_id BIGINT、amount DECIMAL(10,2)、create_time DATETIME共插入约1200万行数据覆盖约60万用户。窗口函数方案的平均耗时约1.8秒关联子查询方案约9.4秒JOIN MAX方案约11.2秒。从执行计划看窗口函数走的是全表扫描 filesort而后两者均通过索引反复回表。排序其实没有想象中可怕可怕的往往是随机IO。2.4 老旧数据库下最稳妥的JOINGROUP BY如果公司还在用MySQL 5.7或者更早版本那ROW_NUMBER()肯定用不了。在5.7上我最常用的方案是JOIN GROUP BYSELECT t.* FROM transaction t INNER JOIN ( SELECT user_id, MAX(create_time) AS max_time FROM transaction GROUP BY user_id ) tmp ON t.user_id tmp.user_id AND t.create_time tmp.max_time;这个方案的核心思想是先在子查询里算出每个用户的最大时间然后用这个结果集和原表做等值关联。它有几个前置要求第一transaction表必须有(user_id, create_time)的联合索引第二如果create_time允许重复需要谨慎处理可能需要加上id取最大来保证唯一性第三子查询的结果集会占用临时表空间用户量极大时需要注意。实测中我遇到过一种奇怪的现象同样的SQL在某个环境上跑得飞快换到另一套环境却奇慢无比。后来查了执行计划才发现慢是因为MySQL 5.7的优化器有时候会选择t表作为驱动表导致对子查询里的每一条记录都做全表扫描。解决办法是给子查询加一个SQL_BIG_RESULT提示或者改写成STRAIGHT_JOIN。这类优化器抽风问题在实际运维中经常出现比SQL本身的技术难度更磨人。从时间戳唯一性角度我会在表设计阶段就建议业务方给(user_id, create_time)加唯一约束。如果历史原因已经造成数据重复那就一定得在子查询里把MAX(id)也带上再通过(user_id, create_time, id)三个字段去做关联确保每个用户只取那一条确定的行。3. 分库分表场景下的偏移量陷阱3.1 单库方案在分库分表后失效前面讨论的都是单库场景。但在交易、支付这类高并发业务中分库分表几乎是必经之路。假设我们的交易流水表按user_id哈希分成16个库、每个库再分成32张表那之前所有依赖全局排序的SQL都会失效因为MAX(create_time)或ROW_NUMBER()都只能在单张物理表内部计算你没法跨512张物理表做一个全局的最近一笔。这种时候我会把方案分成三类来思考。第一如果业务允许最终一致性和若干秒延迟最好的方案是下游汇总。用数据同步工具例如Canal把分表数据实时或准实时汇总到一张独立的宽表或StarRocks/Doris这类分析引擎中再加一层窗口函数计算。同步链路会增加一个中间件但换来的是查询逻辑极其简单这在绝大多数报表场景中是划算的。第二如果必须实时读取分表数据通常需要引入一个每用户最新记录索引表在写入交易流水的同时维护这张索引表。它的表结构可以简化为user_id、latest_txn_id、latest_create_time和必要的冗余字段每次交易发生时通过事务或分布式事务更新。查询时先点查索引表拿到最新记录ID再按ID反查流水主表。这个方案本质上就是前面提到的反范式冗余字段只不过放到了分库分表之上牺牲一定的写入成本来换取极致的读取性能。第三还有一种折中方案就是利用分表键天然就是user_id这个特性在每个分片上各取各的最新记录然后交给应用层去合并。查询层并发地向16个库的32张分表发起取最新记录的请求然后在服务内存里做归并排序挑出每个用户的最终最新记录。这个方案不需要额外建表但应用层逻辑会复杂一些通常我会把它封装在数据访问层对外暴露一个统一接口。3.2 我用过的分布式实时方案实例这里分享一个真实做过的方案。当时是给一个第三方聚合支付平台做商户交易实时看板数据落在MySQL分库分表里每天新增流水约800万条要求看板展示每个商户user_id维度其实就是商户号的最新一笔交易允许秒级延迟。我采用的方案是本地索引表消息队列异步更新。具体做法是每个分片库内部维护一张merchant_latest_txn表这张表以merchant_id为主键字段包括txn_id、txn_time、amount等。当交易流水在主表写入成功后应用层在同一本地事务里更新这张索引表。因为分库键一致所以同一商户的所有流水都落在同一分片本地事务就能保证强一致不需要分布式事务。-- 在同一个本地事务内执行 INSERT INTO txn_detail (id, merchant_id, amount, create_time) VALUES (?, ?, ?, ?); UPDATE merchant_latest_txn SET txn_id ?, txn_time ?, amount ? WHERE merchant_id ? AND txn_time ?;这个txn_time ?条件是防止并发的回调通知把旧的流水错当成新流水覆盖过来。虽然我在业务上已经要求回调按时间递增但加这个条件相当于兜底实测下来确实拦截了几次乱序问题。待查询时只查索引表因为已经是最新数据连JOIN回主表都可以省掉性能提升非常明显。同时每条流水写入后还会发送一条消息到MQ由一个独立消费者把最新流水按merchant_id再汇总到ClickHouse供更复杂的时间范围分析用。链路虽长但每一层的职责都比较单一排障也容易。4. 实操过程与核心环节实现4.1 造一份可信的测试数据集要验证同一用户最新交易记录的查询策略建议先造一份能模拟真实分布的数据。我一般用存储过程批量插入重点模拟两种情况一种是一千个用户每个用户的交易数随机在1到500之间另一种是极端倾斜场景少数用户的交易数过万大多数用户只有个位数交易。下面是MySQL 5.7风格的一个造数过程其他数据库思路类似。先把数据量做到约500万行用来对比不同方案的耗时。CREATE TABLE merchant_txn ( id BIGINT NOT NULL AUTO_INCREMENT, merchant_id BIGINT NOT NULL, amount DECIMAL(12, 2) NOT NULL, txn_status TINYINT NOT NULL DEFAULT 1, create_time DATETIME(3) NOT NULL, PRIMARY KEY (id), KEY idx_merchant_time (merchant_id, create_time) ) ENGINE InnoDB; DROP PROCEDURE IF EXISTS generate_txn; DELIMITER $$ CREATE PROCEDURE generate_txn(IN total_rows INT) BEGIN DECLARE i INT DEFAULT 1; DECLARE v_merchant_id BIGINT; DECLARE v_amount DECIMAL(12, 2); DECLARE v_create_time DATETIME(3); SET session.autocommit 0; WHILE i total_rows DO SET v_merchant_id FLOOR(1 RAND() * 5000); SET v_amount ROUND(10 RAND() * 9990, 2); SET v_create_time TIMESTAMP(2023-01-01) INTERVAL FLOOR(RAND() * 5000000) SECOND; INSERT INTO merchant_txn (merchant_id, amount, create_time) VALUES (v_merchant_id, v_amount, v_create_time); IF i % 5000 0 THEN COMMIT; END IF; SET i i 1; END WHILE; COMMIT; END$$ DELIMITER ; CALL generate_txn(5000000);这里的核心是(merchant_id, create_time)复合索引。等值条件下用merchant_id做前缀匹配排序时再依赖create_time的有序性。很多人不加这个索引就指望SQL快起来这基本不现实。4.2 三种SQL在千万级数据下的实测对比我分别准备了三类查询在同一台MySQL 8.0实例上各跑5次取平均值。以下是具有代表性的结果。方案平均耗时执行计划关键点排序内存使用ROW_NUMBER()窗口函数约1.95秒全表扫描filesort约180MB相关子查询约9.8秒对每个商户子查询走二级索引低INNER JOIN MAX约3.2秒子查询全表扫描分组外查询索引点查低窗口函数在这个数据量上依然是最快的而且SQL写法最简洁。相关子查询那么慢本质在于每处理一行外层数据就要执行一次内层索引查找500万行就意味着接近500万次随机IO这个开销在机械盘或网络存储上会被进一步放大。JOINMAX方案介于两者之间适合MySQL 5.7用户。值得注意的是窗口函数的排序内存占用了180MB。这说明ROW_NUMBER()并非完全没有代价它用较大的内存/临时空间换取了更优的IO模式。生产环境务必确保sort_buffer_size和tmp_table_size配置合理否则内存溢出的风险会比较高。4.3 当每组最新记录的定义还包含状态条件时业务上常见的变体是最新一笔交易并不是看全部流水而是看成功的交易。比如要求取每个用户最新一笔已支付订单而不是最新一笔包括退款、失败、待支付等全部状态的流水。这个条件看起来只是加一个WHERE txn_status 1但位置放错了会导致结果错误。正确的写法是把状态过滤放进子查询或窗口函数的分区内SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (PARTITION BY merchant_id ORDER BY create_time DESC) AS rn FROM merchant_txn t WHERE txn_status 1 ) tmp WHERE rn 1;假如把txn_status 1放到外层WHERE rn 1之后SQL会先在所有状态里找到一个最新记录然后再判断这个记录是否为已支付状态。当某个用户的最新一笔流水恰好是失败流水时结果里就少了他真正的最新成功交易这是很常见的隐性bug。我在给团队做代码评审时至少见过5次类似问题每次报出来的数据都对不上。如果业务上存在大量非成功状态的流水为了减少排序数据量也可以在内层直接过滤后再分区排序这样不仅语义正确性能也会更好。5. 索引、执行计划与数据库优化细节5.1 复合索引的有效性取决于列顺序上面反复提到(merchant_id, create_time)联合索引。为什么是这个顺序最核心的原则是等值条件列放在前面排序或范围条件列放在后面并且索引顺序要匹配SQL中的排序方向。如果是(create_time, merchant_id)那么WHERE merchant_id 123根本无法有效利用索引前缀只能在创建时间范围内过滤出大量记录后再归并分组性能会差出一个数量级。查询每个用户最新一条的本质是先按用户分组、再在组内排序。索引(merchant_id, create_time)本质上已经把数据在物理存储层面按这两个维度排好了。理论上各个用户的记录是挨着的时间又是递增的取最大值只需要跳到每组最后一条。但InnoDB的索引叶子节点在每个分组内并不保存最大值指针所以实际执行时还是得扫描组内所有条目这也是为什么即使有索引GROUP BY merchant_id子查询仍然要消耗一定时间的原因。只有当merchant_id的选择性很高、每组记录数很少时索引才能发挥出接近点查的效果。另一个不起眼但重要的细节是如果只需要merchant_id、amount、create_time这些字段可以把这几个字段都放到二级索引中构成覆盖索引。这样执行计划里会显示Using index不再需要回表。千万不要在查询里直接写SELECT t.*让数据库回表拉每一行的全部列这会白白多出大量磁盘IO。5.2 查看执行计划的经验之谈很多新手喜欢问怎么判断SQL健不健康我一般先看三条是否使用了预期索引是否存在filesort/using temporary扫描行数是否远大于结果行数。用EXPLAIN即可看到这些信息。以JOINGROUP BY方案为例EXPLAIN SELECT t.* FROM merchant_txn t INNER JOIN ( SELECT merchant_id, MAX(create_time) AS max_time FROM merchant_txn GROUP BY merchant_id ) tmp ON t.merchant_id tmp.merchant_id AND t.create_time tmp.max_time;通常看到的执行计划是第一行是DERIVED类型子查询第二行是SIMPLE类型主表访问如果key列出现idx_merchant_time说明关联列走索引了如果出现Using temporary; Using filesort说明子查询里的分组排序在内存或磁盘上完成数据量一大就要格外小心。如果rows列的估算值比真实数据大很多说明统计信息过期要先ANALYZE TABLE刷新。MySQL 8.0里还可以用EXPLAIN ANALYZE得到实际执行时间和每步耗时这对定位瓶颈帮助极大。有一次我定位一个慢查询表面看瓶颈在ORDER BY但EXPLAIN ANALYZE显示真正的耗时在Nested Loop反复访问索引。换了个索引后时间从9秒降到0.2秒这种收益只有实测后才看得到。5.3 用覆盖索引减少回表压力针对窗口函数方案如果只需要返回少量字段可以这样建索引ALTER TABLE merchant_txn ADD INDEX idx_merchant_time_cover (merchant_id, create_time, amount, txn_status);查询语句改为只选取这些字段后执行计划会变为Using index状态意味着整个过程都在索引层面完成不回表。磁盘IO大幅减少。不过覆盖索引也会占用更多空间而且写入时更新索引的成本更高所以不要为了覆盖而覆盖量力而行。我的经验是单表字段很多、回表代价很高时才值得做。6. 常见问题与排查技巧实录6.1 为什么我加了索引还是慢这是市面上出现频率最高的问题。通常情况下索引被跳过的原因有几种一是SQL写法不够sargable比如对create_time加了函数转换导致索引失效二是统计信息不准确优化器选错了执行计划三是行数太大优化器判断全表扫描更快。举个例子写WHERE DATE(create_time) 2024-01-01与写WHERE create_time 2024-01-01 AND create_time 2024-01-02的区别就是前者无法使用索引后者可以使用。同样的道理也适用于在JOIN条件里对字段做计算比如ON t.user_id 0 tmp.user_id这就会让user_id上的索引失效。早期我帮一个朋友排查过类似问题他的查询在测试环境1秒内返回上了生产却要8秒。后来发现测试环境数据量小全表扫描成本低优化器选择全表扫描生产环境数据量大了统计信息却还是旧的优化器依然走了全表扫描。解决办法就是刷新统计信息并把optimizer_switch里某些启发式选项调成off生产环境才恢复正常。6.2 在大数据量下如何避免排序内存被打爆窗口函数在排序阶段会占用大量内存如果SQL直接对全表做PARTITION BY user_id ORDER BY create_time DESC在几亿行的表上尤其危险。我在处理一个超大数据量日志表时曾因为一次临时排序直接导致实例的临时表空间写满后续查询全部阻塞。排查之后发现根本原因不是SQL不对而是设计表结构时缺少一个时间裁剪维度。日志类数据通常只关心最近90天那我就会在查询里先加一个时间过滤条件把排序范围缩小。如果是纯交易类数据可以考虑按create_time做RANGE分区例如按月份分区查询最新交易时优先访问最近分区。MySQL的分区裁剪partition pruning会把不必要的分区直接排除掉排序数据量瞬间减少好几个量级。6.3 事务隔离级别和锁对查询的影响如果业务需要在查询时保证一致性快照MySQL InnoDB在REPEATABLE READ隔离级别下会使用MVCC机制。普通查询是快照读不加锁但如果你把查询放到事务中并且后续还要更新最新记录就要小心间隙锁和锁等待问题。特别是某些ORM框架默认自动开启事务一条看似简单的SELECT可能会因为和写事务交错执行而阻塞。我这里有一个亲身教训。有次一个查询最新交易的接口莫名出现延迟尖刺排查发现是因为底层ORM在同一个事务里先查了商户信息又查了最新交易两个查询之间存在间隙另一个批量更新任务正在修改同一批商户的流水导致锁等待叠加。最后改成只读事务并把两个查询拆开才解决。6.4 一套可以直接套用的排查清单日常排查时我建议按顺序走一遍这个流程步骤操作目标1EXPLAIN查看执行计划确认是否命中预期索引2检查SQL是否为sargable写法防止函数操作导致索引失效3确认统计信息是否最新避免优化器误判4检查sort_buffer_size,tmp_table_size排除排序/临时表瓶颈5观察锁等待与事务隔离级别排除并发干扰6用线上真实数据量做压测避免测试环境和生产环境差距这套清单帮团队解决过几十个慢查询问题包括很多看起来玄学的问题。大多数时候慢得离谱并不是数据库卡而是执行计划走到了错误的路径上。7. 再扩展一步取最近N条、分页与漏斗分析7.1 取每个用户最近N笔交易从最新一条扩展到最近N条并不难只需把ROW_NUMBER()换成RANK()或者把过滤条件从rn 1改成rn N。如果需求是最近3笔成功交易就在窗口函数内先过滤状态再排序这样不会有语义误差。但要注意RANK()在处理并列时间戳时会出现多个相同序号如果想保证固定返回N条还是用ROW_NUMBER()更合适。SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (PARTITION BY merchant_id ORDER BY create_time DESC) AS rn FROM merchant_txn t WHERE txn_status 1 ) tmp WHERE rn 3;这类查询也常用于单个用户的最近交易列表页面性能上和取一条基本没有差别因为每组只多取几行。7.2 用最新流水做漏斗分析的思路当每个用户的最新一笔交易被用于漏斗分析时需求通常会变成统计每个最新交易的金额分布。最直接的做法是把上面SQL的结果作为子查询再按金额区间聚合。SELECT CASE WHEN amount 100 THEN 0-100 WHEN amount 1000 THEN 100-1000 ELSE 1000 END AS amount_bucket, COUNT(*) AS user_cnt FROM ( SELECT merchant_id, amount, ROW_NUMBER() OVER (PARTITION BY merchant_id ORDER BY create_time DESC) AS rn FROM merchant_txn WHERE txn_status 1 ) tmp WHERE rn 1 GROUP BY amount_bucket;这种写法在BI报表里非常普遍。如果数据量极大建议将每个用户最新一笔的结果物化成一张中间表再让报表直接查询中间表避免每次跑报表都调度一次重量级计算。7.3 和缓存、实时数仓的组合在生产实践中我不建议所有查询都直接打到交易主库上。即使SQL优化到极致高并发下依然会对数据库产生压力。我常用的组合是用ETL任务将最新交易定期写入Rediskey为merchant_idvalue为交易对象JSON设置合理的过期时间。查询接口优先走缓存缓存未命中再回源数据库并用SELECT ... FOR UPDATE或分布式锁防止并发回源时出现缓存击穿。这种架构做出来之后线上接口RT从平均80ms降到了5ms以下数据库QPS下降超过90%。说到底一条查询的优化做到极致技术上确实有天花板但架构上的拆分能带来质的飞跃。每当有人问我这个SQL还能怎么优化我会建议他先想想这条SQL是不是必须要在这个数据库上执行。我个人在实际操作中最深的体会有两点第一不要迷信某一种“最佳方案”窗口函数虽好但老版本数据库、极端数据分布、高并发场景都可能有更合适的替代第二任何方案都要回到底层执行计划的验证上数据量不同、硬件不同、统计信息不同结果都可能天差地别。最后再分享一个小技巧。如果你需要在MySQL 5.6/5.7时代模拟窗口函数的效果可以用GROUP_CONCAT配合SUBSTRING_INDEX取最新一条的某个字段虽然不支持取整行但在只取最新金额、最新状态等单字段场景下非常轻量SELECT merchant_id, SUBSTRING_INDEX(GROUP_CONCAT(amount ORDER BY create_time DESC), ,, 1) AS latest_amount, SUBSTRING_INDEX(GROUP_CONCAT(txn_status ORDER BY create_time DESC), ,, 1) AS latest_status FROM merchant_txn GROUP BY merchant_id;这个技巧在数据量可控、仅需少量字段时可以写出一个非常简洁且容易维护的查询。不过要小心group_concat_max_len的限制字段值太长或记录太多会截断。这个坑我踩过一次后来统一改用窗口函数或JOIN方案了但偶尔应付临时分析还是没有问题的。
返回列表