MyBatis 动态 SQL 很适合后台筛选、报表查询和运营平台列表页。但复杂查询一多,XML 里堆满 if、choose、foreach,最后生成什么 SQL,开发自己都不一定看得清。
动态 SQL 的核心不是“能拼出来”,而是“拼出来的 SQL 稳定、可解释、能走索引”。
我的判断: 动态 SQL 写到后期最怕的不是语法复杂,而是组合不可预测。只要开发无法解释最终 SQL 和执行计划,这段查询就已经进入风险区。
参数先在 Java 层归一化
空字符串、空集合、前后空格、默认时间范围,最好在进入 Mapper 前处理掉。不要把所有判空逻辑都塞进 XML,否则查询条件会变得很难测试。
少写万能查询
一个 Mapper 方法支持几十个可选条件,看起来复用,实际上会让执行计划非常不稳定。核心查询可以拆成多个更明确的方法,让数据库更容易命中合适索引。
foreach 要控制规模
IN 条件很好用,但列表过大时会拖慢 SQL。批量查询可以考虑分片、临时表、批处理任务,或者把大集合写入中间表后 join。
上线前看 EXPLAIN
动态 SQL 最后还是 SQL。对核心列表、报表、导出接口,要把典型参数生成的 SQL 打出来,用 EXPLAIN 看索引、扫描行数和排序方式。
实际场景:BI 筛选条件越灵活,越不能把 SQL 写散
BI 平台里的动态 SQL 和普通后台列表完全不是一个量级。用户会选择事件、时间范围、分析主体、属性过滤、标签过滤、分群过滤、维度表属性、虚拟属性、分组方式、排序方式,还可能继续下钻到用户列表。如果每个接口都在 Mapper XML 里堆 if,很快就会变成“能跑但没人敢改”的 SQL。
更稳的做法是把动态 SQL 分成三层。第一层是请求归一化,把空值、默认时间、属性类型、枚举值都整理成服务端内部结构。第二层是语义解析,判断字段来自事件表、用户表、维度表还是虚拟属性,并解析出 join 关系、过滤表达式和展示字段。第三层才是 SQL 生成,把这些结构翻译成 CTE、where、group by、order by。
比如路径分析不是简单查列表,它要先取基础事件,再按用户或设备切会话,再按时间排序生成行为序列,最后展开节点并统计后续事件。这里如果直接用 XML 拼,LAG、LEAD、窗口函数、节点过滤、更多桶、下钻条件都会混在一起。用 Builder 拆成多个 CTE,SQL 虽然长,但每一段都有名字,排查时也能单独打印和验证。
动态 SQL 最怕用户输入失控。比如 IN 列表太长、过滤条件太多、时间范围太大、分组字段不走索引、排序字段来自表达式,都会让查询变慢。后端应该主动给护栏:最多多少个事件、最多多少个过滤条件、最大时间跨度、最大导出行数。你项目里的路径分析就有类似的限制思路,这比上线后靠 DBA 救火靠谱得多。
还有一点是 SQL 可观测。复杂查询上线后,一定要能拿到最终 SQL、参数、耗时、扫描量和 traceId。用户说“这个分析慢”时,开发应该能定位是过滤条件太复杂、虚拟属性展开太重、维度表 join 太多,还是 StarRocks 本身资源紧张。MyBatis 动态 SQL 的目标不是少写代码,而是让最终 SQL 可预测、可解释、可治理。
我还会给核心 SQL 留一组典型用例。比如“单事件 + 一个属性过滤”“多事件 + 用户分群过滤”“虚拟属性 + 维度表属性”“下钻用户列表”“导出全量结果”。这些用例不一定都写成完整自动化测试,但至少要能打印最终 SQL 并人工验证执行计划。动态 SQL 的风险在组合爆炸,靠肉眼看 XML 很难覆盖。
对于需要长期维护的查询,最好把字段来源和过滤能力沉淀成配置或枚举,而不是让前端随便传字段名。字段白名单不仅能降低 SQL 注入风险,也能避免用户选到不支持索引、不支持聚合、或者跨主体不合法的属性。复杂查询越自由,后端越要把自由度收在可解释的范围内。