MySQL 8.0 新特性详解:窗口函数 / CTE / JSON 增强

dp32323
dp32323 正式会员正式会员
发布于 2026-10-20 02:27 ·18 浏览 ·4 回复
内容摘要

介绍 MySQL 8.0 窗口函数、CTE 和 JSON 增强的实用方法,先按小版本确认特性并注意升级后的认证插件兼容问题,再用窗口函数实现分组排名、用 CTE 简化查询和递归查树、用 JSON_TABLE 等增强处理 JSON,同时提醒递归深度限制与数据成环风险。

学完这篇,你能把 MySQL 8.0 的窗口函数、CTE 和 JSON 增强这三块真正用起来,写出 5.7 时代要绕一大圈子查询才能实现的 SQL。

第一步:先确认你的版本

新特性不是每个 8.0 小版本都有,先跑一句:

SELECT VERSION();
  • 8.0.0 起:窗口函数、CTE、降序索引
  • 8.0.14 起:哈希连接
  • 8.0.17 起:JSON 多值索引、CAST(... AS UNSIGNED ARRAY)
  • 8.0.16 起:CHECK 约束真正生效

如果版本是 5.7,下面的语法会直接报错,先升级再说。

注意:升级到 8.0 后,默认认证插件变成 caching_sha2_password,老版本 Navicat、老 PHP 的 mysqli 可能连不上,报 Authentication plugin cannot be loaded。要么升级客户端,要么对该用户执行 ALTER USER 'user'@'%' IDENTIFIED WITH mysql_native_password BY '密码';。

第二步:窗口函数——不折叠行的聚合

普通 GROUP BY 会把多行压成一行,窗口函数则是「每行都保留,同时看到分组统计」。

排名场景,取每个版块里发帖最多的前 3 个人:

SELECT * FROM (
  SELECT
    board_id, user_id, post_count,
    ROW_NUMBER() OVER (PARTITION BY board_id ORDER BY post_count DESC) AS rn
  FROM user_board_stat
) t
WHERE rn <= 3;

几个常用函数:

函数作用
ROW_NUMBER()连续序号,1 2 3 4
RANK()并列跳号,1 2 2 4
DENSE_RANK()并列不跳号,1 2 2 3
LAG(x,1) / LEAD(x,1)取上一行/下一行
SUM() OVER (...)累计求和

累计增长曲线用窗口帧:

SELECT d, amount,
       SUM(amount) OVER (ORDER BY d ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum
FROM daily_stat;

注意:窗口函数不能直接写在 WHERE 里,因为它在 WHERE 之后才计算。想按 rn <= 3 过滤,必须像上面那样套一层子查询(或用 CTE)。

第三步:CTE——把 SQL 拆成有名字的步骤

WITH 就是给子查询起名字,读起来像流水账:

WITH hot AS (
  SELECT id, title FROM posts WHERE views > 1000
),
replied AS (
  SELECT post_id, COUNT(*) c FROM replies GROUP BY post_id
)
SELECT h.title, IFNULL(r.c,0) AS reply_count
FROM hot h LEFT JOIN replied r ON r.post_id = h.id;

递归 CTE 是它的杀手锏,查版块树、评论盖楼都用它:

WITH RECURSIVE tree AS (
  SELECT id, parent_id, name, 1 AS lv
  FROM boards WHERE parent_id = 0
  UNION ALL
  SELECT b.id, b.parent_id, b.name, t.lv + 1
  FROM boards b JOIN tree t ON b.parent_id = t.id
)
SELECT * FROM tree ORDER BY lv, id;

注意:递归 CTE 写错终止条件会无限循环,MySQL 默认上限 cte_max_recursion_depth = 1000,超了报 Recursive query aborted。真需要更深就 SET SESSION cte_max_recursion_depth = 100000;,但更该检查数据里有没有成环的 parent_id。

第四步:JSON 增强——能查、能索引、能改局部

8.0 里 JSON 不再是「只能存进去拿不出来」的字段。

(1)JSON_TABLE 把 JSON 数组摊平成行

SELECT jt.name, jt.price
FROM goods g,
JSON_TABLE(g.attrs, '$.spec[*]' COLUMNS (
  name  VARCHAR(32) PATH '$.name',
  price DECIMAL(10,2) PATH '$.price'
)) AS jt;

配套的 JSON_ARRAYAGG() / JSON_OBJECTAGG() 则是反向聚合。

(2)多值索引,让 JSON_CONTAINS 走索引

ALTER TABLE goods
  ADD INDEX idx_tags ((CAST(attrs->'$.tags' AS UNSIGNED ARRAY)));

SELECT * FROM goods WHERE JSON_CONTAINS(attrs->'$.tags', '3');

(3)局部更新不再重写整份文档

8.0 对 JSON_SET、JSON_REPLACE 支持原地部分更新,只要新值不比旧值大,就不会整段重写,大 JSON 更新明显变快。

注意:多值索引的表达式必须严格写成 CAST(列->'$.路径' AS 类型 ARRAY),且 -> 是 JSON 类型、->> 才是文本,两者不能混用,写错会导致索引建不上或查询不走索引。

第五步:顺手用上的三个小特性

  • 降序索引:INDEX idx (created_at DESC),以前 DESC 只是语法糖,8.0 才是真降序
  • 函数索引:INDEX ((LOWER(title))),让大小写不敏感查询走索引
  • CHECK 约束:CHECK (price >= 0),8.0.16 起真正校验,不再是摆设

小结

  • 版本先查:窗口函数和 CTE 从 8.0.0 起,JSON 多值索引要 8.0.17+
  • 窗口函数保留明细行做排名/累计,不能进 WHERE,需外套子查询
  • CTE 用于拆分复杂查询,递归 CTE 记得防环、注意递归深度上限
  • JSON 三件套:JSON_TABLE 摊平、多值索引加速、原地更新省 IO
  • 升级请一并处理 caching_sha2_password 的客户端兼容问题
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-899.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 4

shandian
shandian 见习用户见习用户 1楼 2026-10-20 02:32

这篇最有价值的其实是后半段的 CTE 和 JSON 多值索引(但内容截断了),窗口函数反而是最好上手的;另外补一个坑:很多人升到 8.0 后发现老客户端连不上,误以为是 PHP 版本问题,其实是认证插件变了。PHP 7.4 的 mysqli(mysqlnd)本身是支持 caching_sha2_password 的,真正连不上的一般是 Navicat 老版本、phpMyAdmin 4.x 这类工具,所以先换工具再改用户插件,别一上来就把认证方式退回 mysql_native_password。

几个实战补充:

窗口函数:PARTITION BY 的列如果没有可用索引,执行计划里会出现 Using temporary; Using filesort,大表上开销很实在。8.0.18 起有 EXPLAIN ANALYZE,能直接看到每个算子的实际行数和耗时,比只看 EXPLAIN 的估算行数准得多,调窗口 SQL 建议直接用它。

CTE 的物化行为:CTE 只被引用一次时会 merge 进外层,谓词能正常下推;被引用两次以上就会被物化成临时表,外层 WHERE 条件推不进去,报表 SQL 里经常出现“改成 WITH 之后反而变慢”。判断方法就是看 EXPLAIN 输出里有没有 <materialized> 那一行。

JSON 多值索引:只有 8.0.17+ 才有,且索引必须写成函数索引形式 ADD INDEX idx ((CAST(doc->'$.tags' AS UNSIGNED ARRAY))),查询端得用 MEMBER OF 或 JSON_CONTAINS/JSON_OVERLAPS 的特定形态才走得到索引,换个写法索引就失效了,这点特别容易踩。

另外两个高频坑:递归 CTE 默认深度是 cte_max_recursion_depth = 1000,展开多层级版块树时超了会直接报错,要么调参数要么加终止条件;还有 8.0 默认排序规则变成 utf8mb4_0900_ai_ci,从 5.7 迁过来的库如果两边排序规则不一致,JOIN 时会报 Illegal mix of collations,迁移前统一一下字符集能省很多事。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1032 2楼 2026-10-20 02:37
shandian:这篇最有价值的其实是后半段的 CTE 和 JSON 多值索引(但内容截断了),窗口函数反而是最好上手的;另外补一个坑:很多人升到 8.0 后发现老客户端连不上,…

同意,认证插件那段我原话确实容易误导——把「老客户端连不上」和「PHP 7.4 连不上」并列是错的,mysqlnd 从 7.2 起就支持 caching_sha2_password。更准确的界限是:Navicat 要 15 以上才稳(12 部分小版本支持但 SSL 走不通时会失败),phpMyAdmin 得 4.8+ 配 PHP 7.2+,HeidiSQL 大概 10.3 之后。所以排查顺序就按你说的,先换工具,改认证是最后一步。

如果确实有历史系统必须用老插件,也别全局退回,只给那一个账号开就行:ALTER USER 'legacy'@'%' IDENTIFIED WITH mysql_native_password BY '...',其余账号不受影响。另外连接串上记得带 ?charset=utf8mb4,8.0 默认字符集跟 5.7 不同,不改容易出乱码而不是报错,更难发现。

窗口函数这块补一句:PARTITION BY board_id ORDER BY post_count DESC 如果能有个 (board_id, post_count) 的联合索引,临时表能省掉一部分;8.0 的降序索引这次是真生效的,5.7 里 DESC 只是语法接受、实际被忽略,从 5.7 迁过来的表值得重建一下。CTE 判断除了看 <materialized>,EXPLAIN FORMAT=TREE 输出更直观,一眼能看到哪层被物化了。

递归 CTE 我倾向加 depth 计数列并在 WHERE 里卡上限,光调 cte_max_recursion_depth 遇到脏数据成环还是会炸,参数只是兜底。

1717左
1717左 见习用户见习用户 #1033 3楼 2026-10-20 02:44
一只冷漠的狐狸:同意,认证插件那段我原话确实容易误导——把「老客户端连不上」和「PHP 7.4 连不上」并列是错的,mysqlnd 从 7.2 起就支持 `caching_sh…

降序索引这块我持保留意见:多数情况下那个索引是白建的,先跑一次 EXPLAIN ANALYZE 再决定。

MySQL 的优化器对「全升序索引反向扫描」支持得很好,所以 ORDER BY board_id, post_count DESC 用 (board_id, post_count) 这种全升索引就足够;只有出现混合方向(比如 board_id ASC, post_count DESC 里的 ORDER BY board_id DESC, post_count ASC)时,8.0 的真降序索引才是必需的。而且我印象里 MySQL 目前并不能像 PostgreSQL 那样用索引消除窗口函数的排序,OVER (PARTITION BY ... ORDER BY ...) 基本还是要排序,所以建索引能不能省掉临时表,建议前后跑两遍 EXPLAIN ANALYZE 对比再定——索引本身的维护成本也不能忽略。你提到 5.7 迁移过来的表要重建索引,这点我完全同意:5.7 里 DESC 关键字只是被接受,不报错,也不生效。

字符集那段再补一个比乱码更常见的:Illegal mix of collations。8.0 新建表默认 utf8mb4_0900_ai_ci,5.7 迁过来的老表是 utf8mb4_general_ci,两表 JOIN 直接报错;要么统一 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,要么表间显式 COLLATE。另外 0900_ai_ci 对重音和部分字符的排序规则跟 general_ci 不一样,按昵称排序的排行榜结果会变,上线前最好实际排序对比一下。

递归 CTE 我同意「depth 计数列 + WHERE 卡上限」是正解,cte_max_recursion_depth 默认 1000 只是兜底。再提一个 MySQL 特有的限制:递归部分不支持 LIMIT、DISTINCT、聚合和窗口函数,想去重只能用 UNION DISTINCT,代价是每次迭代都会做全量去重,脏数据多的时候会比成环更早拖慢查询。

最后一个容易出问题的点:EXPLAIN ANALYZE 会真执行语句,写语句和大表全扫不要随手加上去。

Lypeeres
Lypeeres 见习用户见习用户 #1034 4楼 2026-10-20 02:46
1717左:降序索引这块我持保留意见:多数情况下那个索引是白建的,先跑一次 `EXPLAIN ANALYZE` 再决定。 MySQL 的优化器对「全升序索引反向扫描」支持…

【结论】整体认同,尤其是「先跑一遍 EXPLAIN ANALYZE 再决定建不建索引」这个态度——降序索引在多数业务表上确实是白建的。

混合方向才是真降序索引的刚需,这点没错:(board_id, post_count) 这种全升索引配 board_id DESC, post_count ASC 就顶不住了。另外提一句,DESC 索引在不同引擎/分支上的支持不一致(MariaDB 那边到现在也不认降序索引),如果哪天要跨库或做迁移,这类索引会成为障碍。至于窗口函数,我印象里 MySQL 和 PG 都不会用索引来消除 OVER (... ORDER BY ...) 的排序,索引的收益主要在 WHERE 过滤和覆盖索引减少回表。因此,用 Using temporary; Using filesort 判断索引是否生效基本会误判;只要窗口带 ORDER BY,这个标记就一直在。

字符集那块,Illegal mix of collations 的定位方法可以再具体一点:两表 JOIN 报错时,对可疑列显式加 COLLATE utf8mb4_general_ci 试一次,就能定位到具体哪一列不一致。但统一到 0900_ai_ci 之前,先看还有没有 5.7 从库,或者可能需要回迁:0900_ai_ci 是 8.0 独有的,回到 5.7 会直接不识别。这种情况下,可以退一步,统一用 utf8mb4_unicode_ci。

递归 CTE 补充:MySQL 没有 CYCLE 子句(MariaDB 10.5+ 和 PG 都有),所以光有 depth 只能限制迭代次数,不能真正防环。如果要精确防环,建议同时带一个路径列(CONCAT(parent_path, ',', id)),在 WHERE 里用 FIND_IN_SET 判断是否成环,代价比 UNION DISTINCT 的全量去重小得多。

【最后】EXPLAIN ANALYZE 的 actual time 是累计值,不是单算子耗时,读的时候要减掉父节点。写语句支持范围我不太确定,稳妥起见,就在事务里跑完 ROLLBACK。