string_agg() 用于字符串聚合,支持 within group(order by...) 排序,但不自动添加末尾分隔符;sql server 2017+ 引入,azure databricks 和 postgresql 也提供类似函数,mysql 需用 group_concat 替代。

用 STRING_AGG() + OVER (ORDER BY ...) 拼出祖先路径
直接在单次查询中生成类似 '/root/parent/child' 的层次路径,关键不是递归,而是利用窗口函数按层级顺序累积拼接。前提是数据已按树的遍历顺序(如前序遍历或按 level + sort_order)排好序。
常见错误是试图对无序的 id 直接 ORDER BY id,结果路径乱序、跳级。必须确保 ORDER BY 子句能反映真实的父子先后关系——通常依赖显式字段如 path_depth、lft/rgt(嵌套集),或业务约定的 sort_weight。
-
STRING_AGG(name, '/') OVER (ORDER BY lft ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)是最稳的写法,lft来自嵌套集模型 - 若只有
parent_id无排序字段,先用WITH RECURSIVE算出lft或path_array,再窗口拼接,不能跳步 - PostgreSQL 15+ 支持
STRING_AGG(... ORDER BY ...)内置排序,但窗口版仍需外层OVER;MySQL 8.0 不支持STRING_AGG,得用GROUP_CONCAT配合变量模拟,稳定性差
用 LAG() 和 CASE 判断层级变化点
当路径需体现“缩进”或“分段标识”(如只显示最近两级:'Marketing → Campaigns'),不能全量拼接,而要识别当前节点相对于上一行的层级变动。
核心逻辑是:比较当前行 level 与上一行 level(用 LAG(level)),决定从哪一级开始截断或重置路径。这比纯递归更轻量,适合实时报表场景。
-
LAG(level) OVER (ORDER BY sort_path)必须和主排序一致,否则LAG取到的是错行的父级 - 层级下降时(如
level=3→level=1),说明已跳出原分支,路径应重置为当前节点自身 - 用
CASE WHEN level > LAG(level) THEN CONCAT(prev_path, '/', name) ELSE name END实现动态拼接,注意prev_path需用COALESCE(LAG(full_path), '')避免 NULL 中断
MySQL 8.0 下替代 RECURSIVE CTE 的窗口函数组合方案
MySQL 8.0 虽支持 WITH RECURSIVE,但深度大时性能陡降,且无法在视图中使用递归 CTE。此时可用窗口函数 + 自连接模拟浅层祖先追溯(≤3 层)。
原理是:对每行,用 LEFT JOIN 关联一次 parent,再用窗口函数把关联结果“拉平”成列。这不是真递归,但覆盖了多数菜单、组织架构的展示需求。
- 先
JOIN t p1 ON t.parent_id = p1.id得到直接父级,再JOIN t p2 ON p1.parent_id = p2.id得到祖父,最多连 3 次 - 用
COALESCE(p2.name, p1.name, t.name)构造根节点名,再用CONCAT_WS('/', p2.name, p1.name, t.name)生成路径 - 若层级不确定,此法失效;必须用递归 CTE 或应用层处理 —— 窗口函数本身不解决任意深度遍历
路径生成后 LIKE 查询变慢?加函数索引或物化路径列
生成的路径字段(如 full_path)若用于 WHERE full_path LIKE '/a/b/%',即使有索引,MySQL/PG 默认也无法高效走索引,因为 LIKE 前缀匹配需 B-tree 支持左前缀,而函数生成列默认不索引。
别指望优化器自动识别路径模式。要么提前固化,要么改写查询逻辑。
- PostgreSQL 可对表达式建索引:
CREATE INDEX idx_path_like ON tree USING btree (full_path varchar_pattern_ops) - MySQL 8.0+ 推荐添加存储列:
ALTER TABLE tree ADD COLUMN path_prefix VARCHAR(512) STORED AS (SUBSTRING_INDEX(full_path, '/', 3)),再对path_prefix建索引 - 更彻底的做法:在写入时维护
path字段(触发器或应用层),避免每次查询都算 —— 窗口函数适合读多写少,不适合高频路径变更场景
WITH RECURSIVE 的地方就用,窗口函数只是补位工具。










