BI 系统的查询通常不是固定 SQL。用户会选择时间范围、事件、过滤条件、分组字段、标签、分群、虚拟属性和下钻方式。如果每种组合都手写 SQL,代码会很快失控。
更稳的方式是做一层 SQL Planner,把页面配置翻译成中间语义,再由 Builder 生成 SQL。
我的判断: BI 查询的难点不是生成一条 SQL,而是把用户自由配置收敛成可解释、可限流、可复现的查询计划。Planner 是后端对复杂度的主动管理。
先做语义归一化
时间范围、事件名、属性字段、用户主体、虚拟属性、维度表,都要先解析成统一结构。这样后面生成 SQL 时,不需要到处判断字段来自事件表还是用户表。
用 CTE 拆复杂查询
路径分析、漏斗分析、留存分析都适合用 CTE 分段表达。比如先取 base_events,再做会话切分,再展开路径,最后聚合结果。SQL 长一点没关系,关键是每一段可解释。
结果表写入要避免半成品
分群结果、预计算结果、导出结果,不建议直接覆盖正式表。可以先写临时表,写成功后再替换正式结果,避免用户看到计算中的半成品。
给用户配置加上护栏
候选事件数量、过滤条件数量、时间跨度、导出行数都要有限制。BI 系统面对的是自助配置,不加护栏就很容易生成拖垮集群的 SQL。
实际场景:路径分析从页面配置到可执行 SQL
路径分析是 BI 系统里很能体现 SQL Planner 价值的场景。页面上用户看到的是“起始事件、后续事件、时间范围、分析主体、过滤条件、最多展示几步、是否下钻某个节点”,但后端真正要做的是把这些配置转换成稳定 SQL。这个过程如果直接在业务代码里拼字符串,会很快失控。
一个比较清晰的流程是先生成 base_events:过滤项目、时间、分析主体、事件范围和属性条件,只留下后续路径需要的数据。然后按用户或设备排序,使用窗口函数生成前后事件关系。再根据配置展开节点,比如最多 10 步、每步展示 Top N,剩余归到“更多”。最后聚合每个节点的用户数、事件数和转化关系。
下钻查询又是另一层复杂度。用户点击路径图上的某个节点,可能要看命中该节点的用户列表;点击“流失”,要看在当前节点后没有继续到下一节点的人;点击“更多”,要排除已经展示的节点集合。后端不能为每种点击写一套 SQL,而应该把下钻语义归一成 sliceType、当前节点、下一节点、更多桶、过滤条件,再由 Builder 生成最终 SQL。
保存为结果分群也需要 Planner 支撑。路径分析本来是临时计算结果,但运营可能希望把某个节点上的用户保存成一个分群,用于后续分析或投放。这个动作不能只把当前页面结果复制一份,而要保存形成这个人群的 SQL 或计算定义,并确保去掉预览 LIMIT、处理重复用户、记录来源类型和参数。否则后续刷新分群时就无法复现。
StarRocks 适合跑分析型 SQL,但也不是无限资源。Planner 要主动限制候选事件数、过滤条件数、时间跨度和导出规模;复杂查询要用 CTE 拆开;结果写入要避免半成品;慢 SQL 要能通过 traceId 找到生成原因。BI 平台的核心竞争力不是“能拼 SQL”,而是把用户自由配置变成可控、可解释、可优化的查询链路。
实际调优时,不要只看最终耗时。还要看每个 CTE 的数据量、过滤是否前置、是否发生大范围 join、窗口函数排序字段是否合理、临时结果是否可以复用。比如路径分析如果先把全部事件取出来再过滤节点,成本会很高;如果先按项目、时间、候选事件和主体过滤,再进入窗口计算,查询压力会小很多。
结果分群也要考虑生命周期。一次下钻保存出来的人群,可能只是运营临时使用,也可能会被后续看板、标签或投放引用。后端要记录来源分析类型、参数快照、创建人、计算时间和结果数量。这样后续排查“这个分群为什么有这些用户”时,不需要重新猜当时的页面配置。