Metabase 原生 SQL 可选变量(Optional Variables)完全指南:用[[ ]]让查询子句智能显隐
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
导读
本文聚焦 Metabase 原生(Native)SQL 编辑器中的可选变量(optional variables)机制:通过将包含变量的子句用双重方括号[[ .. ]]包裹,即可让该子句在变量未赋值时自动从查询中消失、在赋值时自动还原,从而用同一份 SQL 模板同时满足"带筛选"与"不带筛选"两种查询场景。读完本文,你将掌握可选变量的完整语法、多可选子句的编排规则、MongoDB 中的等价写法,以及用注释语法注入复杂默认值的高级技巧,并了解其背后的解析与替换实现原理。
什么是可选变量
在 SQL 参数(SQL parameters) 的基础上,Metabase 允许你把查询中的某个子句(clause)整体标记为"可选"。典型场景是:创建一个包含变量的可选WHERE子句,当用户没有给该变量提供值(无论是通过筛选器控件还是 URL 参数),查询依然可以正常运行,效果等同于该WHERE子句根本不存在。
语法非常简单:把包含{% raw %}{{variable}}{% endraw %}的整个子句用[[ .. ]]包裹起来即可。
- 当有人在筛选器控件中给变量输入了值,Metabase 会把
[[ ]]内的子句放回模板中,并正常完成变量替换; - 当变量没有值,Metabase 会忽略整个
[[ ]]子句,就像它从未出现在 SQL 中一样。
以下示例中,如果没有给cat传值,查询只统计products表中的全部行;如果cat有值(比如"Widget"),则只统计 category 为 Widget 的产品:
{% raw %} SELECT count(*) FROM products [[WHERE category = {{cat}}]] {% endraw %}从源码层面看,这正是可选参数(optional param)的核心语义。在 native.clj 的实现注释中明确写道:
{% raw %}{{x}}{% endraw %}(必需参数)会被替换为:x的值;- 而
[[AND {{x}}]]这类可选参数,如果:x未指定,[[...]]内的整个子句会被替换为空字符串;如果指定了,则{% raw %}{{x}}{% endraw %}照常替换,子句中其余部分(如AND ...)原样保留。
实际的替换逻辑位于 substitute.clj 的substitute-optional:它先尝试替换[[ ]]内的所有子参数,只要其中有任何一个参数缺失(opt-missing非空),就整体丢弃该可选子句;只有内部所有参数都齐备时,才把替换后的子句拼回 SQL。
前提:不带可选子句时 SQL 也必须合法
使用可选变量的硬性前提是:当[[ ]]内的子句被移除后,剩下的 SQL 仍然是一段合法、可执行的查询。这一点务必先想清楚,否则变量为空时查询会直接报错。
一个典型的错误写法是把WHERE关键词放在[[ ]]之外:
-- 这样写会报错: {% raw %} SELECT count(*) FROM products WHERE [[category = {{cat}}]] {% endraw %}原因在于:当cat没有值时,Metabase 会按"子句不存在"来执行,实际运行的 SQL 变成:
SELECT count(*) FROM products WHERE以WHERE结尾的查询显然不是合法 SQL。正确的做法是把整个WHERE子句(连同关键词)一起放进[[ ]]:
{% raw %} SELECT count(*) FROM products [[WHERE category = {{cat}}]] {% endraw %}这样当cat没有值时,Metabase 实际执行的是:
{% raw %} SELECT count(*) FROM products {% endraw %}这仍然是一段完整合法的查询。对应到实现上,解析器会把[[与]]之间的内容整体识别为一个 optional token(:optional-begin/:optional-end),见 parse_test.cljc 中的用例"SELECT * FROM toucanneries WHERE TRUE [[AND num_toucans > {{num_toucans}}]]",而替换阶段对缺失参数的可选子句直接产出空字符串,参见 substitute_test.clj 的 "optional substitution -- param not present" 用例。
多个可选子句:至少需要一个真实的 WHERE
如果要在一条查询中使用多个可选子句,Metabase 要求你至少写一个普通(非可选)的WHERE子句,随后每个可选子句都要以AND开头:
{% raw %} SELECT count(*) FROM products WHERE TRUE [[AND id = {{id}}]] [[AND {{category}}]] {% endraw %}这里有几个要点:
WHERE TRUE是常用套路:固定的WHERE TRUE保证查询始终合法,同时让后面的多个[[AND ...]]可以自由地追加或移除,互不干扰。从 parse_test.cljc 的 "Multiple optional clauses" 用例可以看出,多个可选子句会被依次解析为多个独立的 optional token,Metabase 会逐一独立判定是否替换。- 字段筛选变量(field filter)不带列名:最后一个
[[AND {{category}}]]使用的是字段筛选变量(field filter),注意AND后面没有写具体列名。使用字段筛选变量时,必须在查询中省略列名,而需要在右侧的变量配置侧栏中把该变量映射到具体字段。对于可选子句中的字段筛选变量,源码中还有一处特殊处理:在 substitute.clj 的substitute-field-param中,处于可选子句内且没有取值的字段筛选变量会被整体忽略并最终整体移除(注释明确写着 "no-value field filters inside optional clauses are ignored, and eventually emitted entirely")。
可选变量在 MongoDB 中的写法
如果你的数据库是 MongoDB,同样可以用[[ ]]实现可选子句。单个可选条件:
{% raw %} [ [[{ $match: {category: {{cat}}} },]] { $count: "Total" } ] {% endraw %}多个可选筛选条件:
{% raw %} [ [[{ $match: {{cat}} },]] [[{ $match: { price: { "$gt": {{minprice}} } } },]] { $count: "Total" } ] {% endraw %}注意这里的$match也遵循同样的"整子句可选"原则:每个可选 stage(包括结尾的逗号)都被完整包在[[ ]]内,这样当对应变量缺失时,Metabase 可以干净地移除整个 stage 而不破坏 JSON 数组的结构。MongoDB 驱动同样复用了 Metabase 统一的参数解析与替换框架(解析与替换逻辑位于 native.clj 所描述的metabase.query-processor.parameters.*与各驱动自己的parameters.*命名空间中),保证行为与 SQL 驱动一致。
在查询中直接设置复杂默认值
可选变量还可以用来在查询内部为参数定义默认值,方法是在可选参数结束括号的紧后方写入注释语法,把默认值放在注释之后:
WHERE column = [[ {% raw %}{{ your_parameter }}{% endraw %} --]] your_default_value其工作原理是:当你给your_parameter传值时,[[ ]]内的内容(包括注释开头--)会"激活"并拼回 SQL,注释会把后面的your_default_value注释掉;当你不传值时,整个[[ ]]连同注释一起消失,SQL 中只剩下默认值your_default_value。
这个技巧尤其适合默认值是比较复杂的表达式(例如函数)的场景。下面的 PostgreSQL 示例把 Date 筛选器的默认值设为当前日期:
{% raw %} SELECT * FROM orders WHERE DATE(created_at) = [[ {{dateOfCreation}} --]] CURRENT_DATE {% endraw %}- 给
dateOfCreation传值时:WHERE子句正常执行,--把默认的CURRENT_DATE注释掉; - 不传值时:
[[ ]]整段消失,查询实际执行DATE(created_at) = CURRENT_DATE,实现"默认查今天"。
需要特别提醒:示例中的--是 SQL 的行注释语法,不同数据库的注释语法不同,实际使用时请替换为对应数据库支持的注释写法(例如某些数据库用#或/* ... */)。从解析器测试可以看到这类写法是被明确支持的——parse_test.cljc 中的用例"/* [[AND num_toucans > {{num_toucans}} --]] */"展示了注释包围在可选子句内外的解析结果。
解析与替换的边界规则(避坑提示)
基于仓库中的解析器测试 parse_test.cljc,以下几个边界行为值得注意:
- 方括号必须成对闭合:像
select * from foo [[where bar = {{baz}}(缺少]])、[[where bar = {{baz]]({{与]]交叠)这类不完整写法会被判定为非法输入(见该文件 L130-L135 的 invalid 用例)。 - 引号内的
[[与]]不受影响:解析器能正确区分字符串字面量,select ']]' from t [[where x = {{foo}}]]中']]'是普通字符串,不影响后续可选子句的识别(L122-L123)。 - 嵌套可选子句是支持的:当多个参数出现在同一个可选块中时,替换逻辑会以"全部齐备才保留"为准则;而嵌套的可选子句则各自独立判定,见 substitute_test.clj 的 "nested optionals" 用例。
小结与延伸阅读
可选变量把"静态 SQL 模板"升级为"可交互的动态查询",是 Metabase 原生查询中复用度最高的能力之一。回顾核心规则:
- 用
[[ 整个子句 ]]包裹变量所在的完整子句(含WHERE等关键词); - 保证去掉可选子句后 SQL 依然合法;
- 多个可选子句时,先写一个真实
WHERE,后续每个可选子句以AND开头; - MongoDB 中同样适用,注意把整个 stage(含逗号)包进
[[ ]]; - 用
[[ {{param}} --]] 默认值注释技巧实现复杂默认值,并按数据库替换注释符号。
想深入了解参数体系的更多玩法,可以继续阅读同目录下的相关文档:SQL 参数、字段筛选变量、筛选器控件 与 原生 SQL 编辑入门;进阶用户可进一步研读源码 native.clj 与 substitute.clj,并结合 parse_test.cljc 与 substitute_test.clj 中的测试用例验证各种边界行为。
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考