关于 omni-query
通过 Omni CLI 在 Omni 语义层上运行查询的技能,面向需要从模型字段获取数据的分析用户。用户指定维度、度量和筛选条件,由 Omni 转换为 SQL。
主要功能
- 01按语义字段构建查询
- 02指定维度、度量和筛选器
- 03支持排序、透视和限制行数
- 04执行表格计算
- 05处理查询结果
适用场景
- 01按主题汇总业务指标
- 02筛选并排序查询结果
- 03计算运行总计或百分比变化
- 04复现 SQL 查询意图
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ exploreomni/ omni-agent-skills --skill omni-query安装后在 Claude Code 中输入/omni-query确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/filter-expressions.mdjob-result-to-presentation.mdtable-calculations.md详细介绍
Omni 查询
通过 Omni CLI 查询 Omni 语义层。Omni 会将字段选择转换为优化后的 SQL——你只需指定所需内容(维度、度量、筛选条件),无需说明如何获取。
提示:如果不知道有哪些可用主题和字段,请先使用
omni-model-explorer。
前提条件
# Verify the Omni CLI is installed — if not, ask the user to install it
# See: https://github.com/exploreomni/cli#readme
command -v omni >/dev/null || echo "ERROR: Omni CLI is not installed."
# Show available profiles and select the appropriate one
omni config show
# If multiple profiles exist, ask the user which to use, then switch:
omni config use <profile-name>
# Confirm the active profile is authenticated and inspect your permissions:
omni whoami whoami
身份验证:配置文件使用 API key 或 OAuth 进行身份验证。如果
whoami(或任何调用)返回 401,请转交处理——让用户运行! omni config login <profile>(OAuth 2.1 浏览器流程;会在浏览器上阻塞约 2 分钟)。不要在无头/CI 会话中自行运行config login(没有浏览器会导致超时);在本地交互式机器上则可以运行。有关配置文件设置(omni config init --auth oauth)以及如何使用--schema查找命令和请求正文的格式,请参阅omni-api-conventions规则。
你还需要一个 模型 ID,并了解有哪些可用的 主题和字段。
发现命令
omni query --help # List query operations
omni query run --help # Show flags for running a query
omni query run --schema # Body schema + example (CLI ≥ 1.5.0 types the `query` object — `fields` and `modelId` are required, `calculations`/`filters`/`sorts`/`pivots` are enumerated; keys outside the typed set are still accepted but not shown)
omni query run --schema --field query # Just the typed query sub-fields
omni ai --help # AI-powered query generation
提示:使用
-o json强制输出结构化内容,以便程序解析;使用-o human输出易读表格。默认值为auto(在 TTY 中输出人类可读格式,通过管道传输时输出 JSON)。
已知问题与安全默认值
- 收到 SQL 时,默认应按主题复现其意图——先阅读 SQL 的作用(粒度、度量、筛选条件、联接),再检查主题是否能够表达它(
omni-model-explorer;omni ai pick-topic/omni ai generate-query --run-query=false)。仅当没有合适的主题,或用户明确要求原样运行 SQL 时,才使用原始userEditedSQL。参见 运行原始 SQL。不要条件反射式地直接透传 SQL(你不是文本转 SQL 生成器),也不要强行套用不匹配的主题。 - 当用户要求计算列、表计算、运行总计、移动平均值、百分比变化、行总计、层级标签、VLOOKUP、SUMIF 或日期差值时,应使用在
query.fields中选定的calculations[]表计算来满足需求。除非用户明确要求其他替代方案,否则不要改用现有模型字段、userEditedSQL、客户端计算或仅用文字说明。 - 确保表计算的答案可审计。 只有最终答案列出计算列名称,并在
query.fields和calculations[]中都展示相同的calc_name,才算完成任务(即使是OMNI_RUNNING_TOTAL这样的简单运算符也一样)。附上精简的查询 JSON 片段——包括query.fields、运算符和操作数完整的实际calculations[]对象、相关的pivots[]/limit,以及验证结果——不要用{ "OMNI_OFFSET_MULTI over": "field" }这样的转述,或“通过OMNI_RUNNING_TOTAL计算”这样的说法代替。较长的 CSV 可能会掩盖查询结构;请展示几行数据和可复用的结构,或说明为简洁起见省略了完整 JSON。 - 如果计算查询成功,但计算列为空,在证明原因之前,应将其视为计算失败。重新检查操作数顺序、
for_calc、日期截断、outside_pivot,以及calc_name是否出现在query.fields中。 - 编写或验证计算时,不要吞掉错误。 保持
swallow_errors: false(默认值),这样错误的计算会明确失败并显示真实错误信息。设为swallow_errors: true时,该列会静默显示#ERROR!,而查询仍返回COMPLETE——很容易被误认为是数据、空白计算结果或引擎错误。如果看到#ERROR!,请使用swallow_errors: false重新运行,以显示原因(通常是query.fields中缺少被引用的字段)。重新运行从文档、仪表板磁贴或omni ai任务中获取的计算查询时,请原样运行——删掉计算引用的字段会人为制造错误,而这并非计算本身的问题。仅在最终定稿的磁贴需要逐单元格容错时使用swallow_errors: true,并先用false验证。参见references/table-calculations.md§5.11 和 §6.5。 - 若有模板,优先使用文档中介绍的 Omni 计算运算符,而不是较底层的原始 SQL / 窗口 AST。例如,当提示要求表计算时,应使用
Omni.OMNI_RUNNING_TOTAL、Omni.OMNI_PERCENT_CHANGE_FROM_PREVIOUS和Omni.OMNI_FX_AVERAGE(Omni.OMNI_OFFSET_MULTI(...))计算移动平均值,而不是手写window_call/LAG。 query run每次都要求在query对象内部包含modelId。 独立请求体应为{"query":{"modelId":"<uuid>", …}};省略后调用会返回 400,错误为query.modelId: Invalid input: expected string, received undefined。这与 v2 仪表板磁贴查询(omni-content-builder)恰恰相反:磁贴查询绝不能携带modelId(服务端会将磁贴绑定到文档的工作簿模型)。不要把磁贴规则误用到独立查询中——磁贴省略modelId;query run必须包含它。 对分支或工作簿 / 草稿模型运行查询时,应将modelId设为该模型的 id(分支模型 id 或草稿的workbookModelId),而不是共享模型;仅在技能明确要求时才在顶层传入branchId(分支自身的模型 id 已能解析分支字段)。- 读取结果行时,应在请求体的顶层设置
resultType:"json"(或"csv")——不要放在query内部。 放错到query内会被静默忽略:你会收到默认的 base64-Arrow 流式封装(行内容不可读),白白多一次往返。参见 处理和验证结果。
围绕主题构建查询
优先选择围绕主题构建每个查询,而不是使用裸基础视图。主题包含受治理的联接、标签和访问权限,而且基于主题构建的查询,受限查询者/查看者无法访问(对你这样的建模者/管理员可用,但对受限角色会静默失败)。将查询的 table 设为主题的基础视图,并传入 join_paths_from_topic_name: <topic>。如果你是受限查询者(拥有 QUERY_TOPICS,但没有 QUERY_FULL_MODEL——请检查 whoami),那么基于主题构建不仅是首选,更是唯一的选项:裸基础视图查询或原始 SQL userEditedSQL 查询需要 QUERY_FULL_MODEL,因此你编写并放入内容的每个查询都必须基于主题构建。
联接映射如何解析已联接视图的字段。 table 始终是主题的基础视图;join_paths_from_topic_name 允许主题的联接映射从该视图访问已联接视图的字段——例如,要在基于 order_items 的主题中选择 users.state,table 仍设为 order_items,联接由主题提供;不要将 table 设为 users。省略 join_paths_from_topic_name(或将 table 指向非基础视图),已联接视图的字段可能无法解析,或可能联接错误。使用 omni models get-topic <modelId> <topic> 确认基础视图和所有可达联接——其中的 base_view_name 和 join_via_map 会显示基础视图,以及通往每个可达视图的联接路径。(这是规范的主题查询形式;omni-content-builder 磁贴和 omni-model-builder 验证查询也采用这种形式。)
决定查询应来自哪里:
- 现有主题可以回答该问题(其基础视图 + 通过联接可达的视图)→ 查询该主题。
- 字段位于通过联接可达的视图中,但主题未公开该字段/缺少相应联接 → 建议扩展主题(添加关系/联接),然后通过
omni-model-builder构建。 - 主题、约束或受众有根本差异 → 建议创建新主题(参见
omni-model-builder中的“新建主题还是扩展主题”标准)。先询问请求者,并在分支上构建。
回退方案——非主题查询路径。 有两种路径不使用任何主题:裸基础视图(table: + 用于联接的全局 relationships 文件)和原始 SQL(userEditedSQL,参见“运行原始 SQL”)。两者有相同的注意事项:不会应用主题范围内的控制项——访问筛选器(行级)和 always_where;在仪表板中,默认情况下 Viewer / Restricted Querier 角色看不到该磁贴(受限受众的处理属于内容权限问题——参见 omni-content-builder)。这两种路径在对象级访问授权方面有所不同:裸视图查询仍会执行这些授权,但原始 SQL 也会绕过它们(这是权限最宽松的路径)。如果主题适用,请优先使用主题;只有在其他方式都无法表达该查询时,才使用非主题路径。
如果结论是“构建或修改主题”,请交由 omni-model-builder 妥善处理。
复合主题。 复合主题(list-topics 中的 is_composite: true)没有基础视图:传入 join_paths_from_topic_name: <composite>,并且不传入 table。字段使用 @_shared_dimensions_.<name>[timeframe]、@_shared_views_.<view>.<field> 和 @<topic>.<view>.<measure> 的形式寻址;以 @<topic>. 开头的筛选键仅应用于该成员主题;@ 不可省略。在复合主题默认的 unrelated_dimension_handling 设置下,属于某个成员主题的维度可以用于筛选,但不能用于选择——如果需要按该维度分组,应将其设为共享维度;否则复合主题应声明 null_fill(其他成员的度量会显示在空值行中)或 repeat(这些度量会在每一行重复)。对复合主题执行 get-topic 可列出可寻址的字段范围。
运行查询
基本查询
omni query run --body '{
"query": {
"modelId": "your-model-id",
"table": "order_items",
"fields": [
"order_items.created_at[month]",
"order_items.total_revenue"
],
"limit": 100,
"join_paths_from_topic_name": "order_items"
}
}'
查询参数
| 参数 | 必填 | 说明 |
|---|---|---|
modelId | 是 — 位于 query 内 | Omni 模型(或分支/工作簿模型)的 UUID。每次独立的 query run 都必须提供;省略会返回 400。对比:v2 仪表板的 图块 查询完全不包含此参数。 |
table | 有条件必填 | 基础视图(FROM)。语义查询必须提供,除非设置了 join_paths_from_topic_name(基础视图来自主题),或使用了 userEditedSQL(忽略此参数)。 |
fields | 是 | view.field_name 引用数组 |
join_paths_from_topic_name | 建议提供 | 用于解析联接的主题 |
limit | 否 | 行数限制(默认值为 1000,最大值为 50000,null 表示不限制)。此处不会应用主题或模型的 default_row_limit:省略 limit 时始终为 1000 |
sorts | 否 | 排序对象数组 |
filters | 否 | 筛选对象 |
pivots | 否 | 用于透视的字段名称数组 |
字段命名
字段使用 view_name.field_name。日期字段支持时间范围括号:
users.created_at[date] — Daily
users.created_at[week] — Weekly
users.created_at[month] — Monthly
users.created_at[quarter] — Quarterly
users.created_at[year] — Yearly
排序
"sorts": [
{ "column_name": "order_items.total_revenue", "sort_descending": true }
]
筛选
query.filters 是一个映射,格式为 fieldName → 类型化筛选对象,与 Omni UI 生成的结构一致(已通过实时仪表板的 filterConfig 和端到端 query run 验证):
"filters": {
"users.state": { "type": "string", "kind": "EQUALS", "values": ["California", "New York"] },
"products.category": { "type": "string", "kind": "CONTAINS", "values": ["Jeans"] },
"order_items.sale_price": { "type": "number", "kind": "GREATER_THAN", "values": [100] },
"order_items.created_at": { "type": "date", "kind": "BEFORE", "values": ["2024-01-01"] },
"order_items.shipped_at": { "type": "date", "kind": "TIME_FOR_INTERVAL_DURATION", "ui_type": "PAST", "left_side": "12 months ago", "right_side": "12 months" },
"order_items.is_shipped": { "type": "boolean", "is_negative": false }
}
type—string/number/date/boolean。布尔筛选使用is_negative(false= 为 true,true= 为 false),而不是kind/values(以及可选的treat_nulls_as_false)。kind(运算符,适用于 string/number/date)— string:EQUALS、CONTAINS、STARTS_WITH、ENDS_WITH、IS_EMPTY、SQL_LIKE;number:EQUALS、GREATER_THAN、LESS_THAN、BETWEEN;date:BEFORE、ON_OR_AFTER、BETWEEN、TIME_FOR_INTERVAL_DURATION/TIME_FOR_UNIT_DURATION(滚动时间窗口)、IS_ON_DAY_OF_WEEK、…(QUERY_OFFSET引用另一个查询)。values— 操作数数组:单个值、列表(多选EQUALS),或BETWEEN使用的[lo, hi]。日期滚动时间窗口使用left_side/right_side+ui_type(例如PAST),而不是values。- 确认筛选确实生效。 筛选对象中的属性与其类型不匹配时(例如,
boolean使用kind/values,但它需要is_negative),该筛选会被静默忽略——查询返回COMPLETE,但筛选并未传入 SQL。可通过cache:"SkipCache"→summary.display_sql(WHERE子句)确认,或检查行数是否实际发生变化。准确的kind/ui_type/布尔枚举值见omni documents v2-create --schema(筛选对象部分);另请参阅 references/filter-expressions.md。
请勿使用裸字符串简写(
"order_items.status": "complete"、"last 90 days"、"not null")。查询 API 会拒绝此格式——截至 2026 年 8 月会返回400 "Unable to parse data stream",较旧版本则会返回500 "Cannot use 'in' operator to search for 'query_id' in <value>"。上述类型化对象是可靠的格式。(已在当前 Omni 上对值、日期、空值字符串、数字和布尔格式进行了复现。)
透视
{
"query": {
"fields": ["order_items.created_at[month]", "order_items.status", "order_items.count"],
"pivots": ["order_items.status"],
"join_paths_from_topic_name": "order_items"
}
}
透视查询不接受 limit: null,除非请求体设置了顶层 resultType;否则请传入明确的数字限制(例如 5000)。
将度量转置为行(transposed_measures)
将一个宽行中的多个度量折叠为长格式,每个度量占一行,从而可以将这些度量作为类别进行图表绘制(例如,漏斗图或“度量作为坐标轴”的条形图)。transposed_measures 是要折叠的度量字段名称数组(与在 fields 中填写的名称相同),不是布尔值——传入 true 会被静默拒绝(z.array(z.string())),导致结果为空。
{
"query": {
"table": "order_items",
"fields": ["order_items.units_sold", "order_items.shipped_items", "order_items.delivered_items"],
"transposed_measures": ["order_items.units_sold", "order_items.shipped_items", "order_items.delivered_items"],
"join_paths_from_topic_name": "order_items"
}
}
结果会在第一个转置度量所在的位置新增三列合成列:
measure_name— 度量的字段名称;在可视化中显示为度量的友好标签(例如“销售数量”)。measure_order— 按列出顺序排列的0, 1, 2, …(即阶段顺序)。measure_value— 该行中此度量的值。
这是通过多个度量构建漏斗图的受支持方式(Omni 的漏斗图需要一个阶段维度和一个度量,而不是 N 个度量):将 measure_name 作为阶段,将 measure_value 作为值绘制图表。请参阅 omni-content-builder → 配置对象:漏斗图。
表格计算
查询后的计算列(累计总计、占比、比率、条件判断)。以 AST 对象的形式编写在 calculations[] 中。查询 API 要求传入解析后的 AST,不接受工作簿前端的 {name, formula} 形式。
最简计算——一个 calculations[] 条目(其 calc_name 也必须出现在 query.fields 中):
{ "calc_name": "calc_pct", "label": "% of Total", "format": "0.0%",
"sql_expression": { "type": "call", "operator": "Omni.OMNI_PERCENT_OF_TOTAL",
"operands": [{ "type": "field", "field_name": "orders.total_revenue", "for_calc": true }] } }
最容易踩的坑:calc_name 也必须出现在 query.fields 中(对于仪表板图块,还必须出现在外层的 queryPresentation.fields 中)。在 calculations[] 中定义了计算,但未将其加入 fields,就会导致计算已执行,却永远不会渲染。
五个快速模板运算符(每个都接受一个带 for_calc: true 的 field 操作数):
Omni.OMNI_PERCENT_OF_TOTAL、Omni.OMNI_PERCENT_OF_PREVIOUS、Omni.OMNI_PERCENT_CHANGE_FROM_PREVIOUS、Omni.OMNI_RUNNING_TOTAL、Omni.OMNI_RANK。
如果你已知道计算的形态,可使用 omni query run,并手动编写或复制 AST。要生成任何非简单内容——表格计算、同期对比、多步骤分析——请优先选择 Agentic 路径(omni ai job-submit):它能编写 generate-query 会悄悄丢弃的计算(例如环比变化百分比)。要从 Agentic 任务中获取可复用的 AST,请从任务的 actions[].generate_query 结果中提取结构化查询 / calculations(不要从 resultSummary 中提取,也不要使用 userEditedSQL / SQL 回退结果),然后使用 swallow_errors:false 重新运行你组装好的查询,并将数值与任务的 csvResult 进行差异比较。任务已经执行过该计算,因此重新运行并不是为了再次验证计算本身,而是为了检查你重组后的查询(字段被丢弃或重命名,就是一种仅凭“查询已运行并返回数据行”无法发现的转换失误;见 table-calculations.md §6)。仅在简单、确定性的单次查询以及不允许执行查询、只需起草查询形态时使用 generate-query。
除非用户明确要求更改模型,否则查询任务均为只读。如果某个字段似乎缺失,请检查主题 / 仪表板查询,并使用正确的模型 / 主题 / 分支,或报告缺失字段的阻塞问题——不要为了让查询生效而创建分支、添加度量或编辑 YAML。(也不要使用客户端数学运算或现有模型字段(如 users.tier_label)来满足计算请求;应构建并验证表格计算,见 Known Issues。)
常见请求 → 运算符(精确 AST 和各方案的注意事项见参考文档——不要自行猜写):
- 占总计的百分比 →
OMNI_PERCENT_OF_TOTAL· 累计总计 →OMNI_RUNNING_TOTAL(按时间升序排序,不要在 Omni 外部反转顺序) · 环比变化百分比 →OMNI_PERCENT_CHANGE_FROM_PREVIOUS(而非omni_period_pivot/LAG) · 最近 N 期平均值 → 对OMNI_OFFSET_MULTI使用OMNI_FX_AVERAGE - 透视表行合计 →
OMNI_FX_SUM+OMNI_PIVOT_OFFSET(outside_pivot:true,数值型limit) · 层级标签 →OMNI_FX_IFS(而非CASE/ 模型字段) · SUMIF →OMNI_FX_SUM_IF· VLOOKUP →OMNI_FX_VLOOKUP(如果字符串查找返回 400 错误No referenced query…,则回退到OMNI_FX_SUM_IF) · 日期差 →OMNI_FX_DATEDIF(操作数使用[date])
各方案对应的精确 JSON AST、完整运算符目录(Omni.* / SqlStdOperatorTable.*)、节点类型、验证规则,以及陌生计算的往返策略,详见 references/table-calculations.md —— SKILL.md 负责工作流约束,参考文档则包含详细结构。
执行时,计算会编译为包裹基础聚合的外层 SELECT;窗口型运算符会在其中生成 ... OVER (...),因此共享数据模型无需窗口函数也能支持这些计算。在透视查询中,模板运算符会自动按透视列分区,以生成各分组的序列;对于横跨透视列的行汇总,请设置 outside_pivot: true,并在 OMNI_PIVOT_OFFSET 外包裹一个聚合器。
运行原始 SQL(userEditedSQL)
给定了 SQL?先通过主题复现它(参见 已知问题)。通过语义层表达 SQL 的意图;只有在没有主题能够表达时(SQL 优先迁移、数据仓库专属 SQL、一次性临时读取),或者用户要求原样运行时,才使用原始
userEditedSQL。如果忠实复现需要一个并不存在但本该存在的字段或主题,应提议对其进行建模(omni-model-builder),而不是默认使用原始 SQL。读取
generate-query的输出:当主题缺少指标所需的度量时,generate-query会返回一个使用${}模板的userEditedSQL(例如,SUM(${view.sale_price}) AS sale_price_sum FROM ${Topic}),并在fields中列出 SQL 输出别名(如sale_price_sum)。这些别名不是模型字段——不要去掉 SQL 并尝试以语义方式运行它们(它们无法解析)。而且,${Topic}标记仅在generate-query自身的执行过程中解析——该模板化查询无法直接通过query run运行,也无法保存为仪表板磁贴(会报错No such view "Order Items")。因此,不要原样重用它;应将其视为主题缺少度量的信号,并添加该度量(omni-model-builder),使该指标成为干净的语义字段。
userEditedSQL 是一种非主题查询路径——与裸视图查询属于同一类(参见“回退方案 — 非主题查询路径”)。它是语义层无法表达 SQL 时的后备方案;条件允许时,优先使用主题或语义字段。
omni query run --body '{
"query": {
"modelId": "<model-id>",
"fields": [],
"userEditedSQL": "select count(*) as cnt from ECOMM.ORDER_ITEMS"
}
}'
- 必须提供
fields(一个数组;可以为空[]);无需提供table。SQL 具有权威性——设置userEditedSQL时,会忽略已填充的fields/table。 - 使用模型的连接运行——请编写数据仓库方言的 SQL,并使用完全限定名称,而非字段引用。
"rewriteSql": false会逐字运行你的 SQL(默认会解析并重新生成 SQL——重新引用标识符,并将投影别名化到view.field命名空间中)。"dbtMode": true允许使用 Jinja/dbt 模板。(两者均采用驼峰命名法;query对象允许额外属性,因此拼错的键或蛇形命名键会被静默丢弃。)- 受权限控制:查询者的角色必须允许执行手写 SQL。否则作业会失败——
error_type: "FORBIDDEN",错误信息为 "queries based on manually written SQL are restricted"——并以HTTP 200 响应返回在作业正文中的错误,而非 4xx。 - 访问行为:原始 SQL 会绕过所有模型控制——对象级访问授权、行级访问筛选器和always_where——并且在仪表板中对 Viewer/Restricted Querier 角色不可见。这是权限最宽松的路径;仅用于特权用户的临时读取,并且从任何重用的查询或仪表板查询中移除它(参见“在仪表板中使用作业结果”)。
- **行数上限:**无界原始查询最多返回 50,000 行(响应会返回 50,001 行——上限加上一行截断标记)。信封中的
limit不会应用于原始 SQL;请在 SQL 中添加LIMIT来限制结果。
请求级选项(位于 query 外部)
这些键位于请求正文的顶层,与 query 并列,而不在其中。query 对象允许包含任意键——如果你把键放错位置,放进 query(例如 resultType、cache),它会被静默丢弃,而不是被拒绝,因此调用会“成功”,但你的选项会被忽略。branchId 是例外:放在 query 内会被拒绝。放错位置的 resultType 的明显迹象是:你请求 JSON/CSV,却收到 base64-Arrow 信封。
| 选项 | 说明 |
|---|---|
resultType | 输出格式:csv、xlsx 或 json。仅限顶层——放在 query 内会被静默忽略,并返回 Arrow。省略时默认返回 base64 Arrow。 |
cache | 缓存策略:Standard(默认)、SkipRequery、SkipCache、SkipCacheAndRebuildExtracts(也会重建查询所读取的 extracts)。 |
userId | 以其他用户身份运行(组织范围的 API 密钥);也对应 --user-id 标志。 |
branchId | 针对模型分支运行(在实时数据上验证模型草稿更改)。必须是同一共享模型的分支。 |
planOnly | 返回执行计划,不运行查询(无需消耗数据仓库成本即可验证/调试)。不能与 resultType 组合使用。 |
formatResults | 导出时输出格式化值(例如 $1,234.56),而非原始值。需要指定 resultType;对 Arrow 无效。 |
timezone | 按请求覆盖时区(IANA ID)。需要连接设置 allowsUserSpecificTimezones 和组织设置 allowsDocumentCanUseTimezoneOverride;任一设置关闭时都会静默无效。 |
workbookUrl | 同时为查询创建临时工作簿,让用户获得“在 Omni 中打开”链接。在 CLI ≥ 1.3.0 中,传入 --workbook,而不是手动设置此项。链接会出现在响应标头中,而非正文中:CLI 会在人类可读输出下打印该链接,或在 JSON 模式下于 stderr 输出 {"workbookUrl": …}。如果(目标)用户没有该模型的工作簿权限,查询仍会成功,链接会被静默省略——没有链接表示没有权限,而不是重试失败。 |
确认是否使用了聚合表
当模型声明了聚合表(materialized_query)时,使用 planOnly: true 和 cache: SkipCache 规划查询,并读取 summary.display_sql。由聚合表提供结果的查询会以 -- Query rewritten to use materialized view "<view>" 开头,下面会注释掉原始 SQL;没有标头则表示使用了事实表。在复合主题上,每个成员查询会分别匹配,且标头会指出使用的第一个表,因此还要查看其他成员对应的 SQL。查询未命中聚合表的原因,见 omni-model-builder 的聚合感知参考文档。
向用户展示结果(CLI ≥ 1.3.0)
在人类可读模式下,query run 会使用模型的标签和数字格式渲染格式化表格,并自行轮询尚未完成的作业。--chart 会将相同结果绘制为终端条形表(--chart-value 选择度量,--chart-rows 限制行数);通过管道输出时仍会绘制,宽度为 80 列,因此适合放入聊天代码块。--chart 会从请求正文中移除 resultType,且不能与 -o json 一起使用。两种视图都不包含作业信封——先通过 JSON 模式运行(见下文)进行验证,再为用户渲染结果。
处理和验证结果
omni query run会流式输出 NDJSON——它不是一个 JSON 对象。 在 JSON 模式下(解析时传入-o json;即使通过管道传输,也会应用config set-format或OMNI_OUTPUT_FORMAT设置的人类可读默认格式,并输出表格),CLI 会打印多个 JSON 对象,每行一个:首先是{"jobs_submitted":{…}}行,然后是一行或多行{"job_id":…,"status":"COMPLETE","summary":{…}}任务行(如果指定了resultType,还会包含结果负载)。直接对整个标准输出调用json.loads()会抛出JSONDecodeError: Extra data。不要编写只解析单个对象的解析器——逐行遍历并选取所需行,或使用jq -s读取全部内容,或读取最后一个非空行。--compact会将每个对象输出为一行整洁的内容。
默认响应:Base64 编码的 Apache Arrow 表。Arrow 结果是二进制数据——无法从原始响应中解析单行数据。行数位于 cache_metadata.num_rows(不是 summary.row_count)。summary 对象包含验证元数据:invalid_calculations、missing_fields、display_sql(编译后的 SQL)、omni_sql_parse_failed。(--schema 不会显示这些内容——它只描述请求正文;响应结构需从实时响应中获取。请参阅 omni-api-conventions 规则。)
如需自行读取行数据,始终在正文的顶层设置 resultType: "json"(或 "csv")——这样标准输出就是干净、可直接解析的 JSON 数组(或 CSV),无需拆解 Arrow/NDJSON 封装。 这是抽查数值唯一可靠的方法;尝试走默认的 Arrow 路径并从中解码行数据,是最常见的时间浪费。
{ "query": { ... }, "resultType": "csv" }
resultType: "xlsx" 也有效,但它会返回一个二进制 .xlsx 文件(基于 zip)——和默认的 Arrow 二进制数据一样,没有电子表格应用或库就无法直接读取。仅在需要向他人交付文件时使用,不要用它来检查结果。供 Agent 端读取时,请使用 csv/json。
结果验证
在信任结果或向用户展示结果之前,都应检查每个查询响应。
检查错误:
- 如果响应包含
error键,则查询失败。常见原因:字段名错误、缺少联接路径、过滤表达式格式错误、权限错误。 - 如果响应包含
remaining_job_ids,则查询仍在运行——请使用omni query wait轮询,然后再检查结果。
检查行数(字段为 cache_metadata.num_rows):
cache_metadata.num_rows == 0——查询没有返回数据。这可能是正常情况(例如,过滤范围内没有数据),但值得提醒用户。常见原因:过滤条件过于严格、日期范围错误、字段与任何行都不匹配。cache_metadata.num_rows等于你设置的limit——结果可能已被截断。如果用户需要完整数据,请使用更高的限制重新运行,或将其设为null表示不限制。
使用 CSV 抽查数据:
当准确性很重要时,请请求 CSV 并检查输出:
omni query run --body '{
"query": { ... },
"resultType": "csv"
}'
请检查:
- 列标题与请求的字段相符
- 数值处于预期范围内(例如,收入不为负数,日期不在未来)
- 聚合结果合理(例如,计数返回的不是总和)
验证过滤行为:
如果查询包含过滤条件,请确认这些条件确实生效:
# Run the same query without filters
omni query run --body '{ "query": { ... (no filters) ... }, "resultType": "csv" }'
# Compare row counts — filtered should be <= unfiltered
如果两个查询返回的行数相同,过滤条件可能没有生效——请检查字段名,并确认过滤对象与字段类型相匹配(boolean 需要 is_negative,而不是 kind/values——不匹配时会被静默忽略)。确认条件出现在 summary.display_sql 中(cache:"SkipCache")。
验证清单
| 检查项 | 方法 | 时机 |
|---|---|---|
| 响应中无错误 | 检查是否存在 error 键 | 每次查询 |
| 计算/字段有效 | summary.invalid_calculations 和 summary.missing_fields 为空 | 每次查询(尤其是涉及计算时) |
| 已返回数据 | cache_metadata.num_rows > 0 | 每次查询 |
| 结果未截断 | cache_metadata.num_rows < limit | 需要确保完整性时 |
| 列正确 | CSV 列标题与请求的字段一致 | 构建仪表板或报告时 |
| 值合理 | 抽查 CSV 输出 | 向用户展示时 |
| 已应用筛选条件 | 比较筛选前后的行数 | 使用筛选条件时 |
| 长时间运行的查询已完成 | 最终响应中没有 remaining_job_ids | 查询大型表时 |
解码 Arrow 结果
import base64, pyarrow as pa
arrow_bytes = base64.b64decode(response["data"])
reader = pa.ipc.open_stream(arrow_bytes)
df = reader.read_all().to_pandas()
长时间运行的查询
如果响应包含 remaining_job_ids,请轮询直至完成:
omni query wait --job-ids job-id-1,job-id-2
JSON 模式不会为你轮询:数据流会在等待窗口结束时终止,页脚中的 remaining_job_ids 是结果未完成的唯一提示。(CLI ≥ 1.3.0 的 Human 模式输出会进行轮询。)
分块流式传输大型结果(CLI ≥ 1.5.0)
如果结果太大,无法放在单个响应中,或者下载过程中连接断开,可以使用 --completion-only true 将结果与完成状态分开,并在之后通过 omni query results 获取结果。此时,query run / query wait 返回的任务行会省略 result;Arrow IPC 字节会以 base64 切片形式返回,你可以将这些切片拼接起来,并在 stream_stats.next_offset 为 null 时进行解析。任务完成后,结果大约一小时内仍可读取。
# 1. Wait for completion without downloading the payload
omni query wait --job-ids "$JID" --completion-only true
# 2. Read the result in slices (resume on drop by passing the previous next_offset)
omni query results --job-id "$JID" --offset 0
在 omni query run 中,将 "completionOnly": true 设为请求正文顶层的值即可达到相同效果。此选项不能与 resultType 同时使用。
从仪表板运行查询
提取并重新运行现有仪表板所用的查询:
# Get all queries from a dashboard
omni documents get-queries <dashboardId>
# Run as a specific user
omni query run --body '{ "query": { ... }, "userId": "user-uuid-here" }'
# Cache policy (valid values: Standard, SkipRequery, SkipCache, SkipCacheAndRebuildExtracts)
omni query run --body '{ "query": { ... }, "cache": "SkipCache" }'
按保存时的状态运行已保存的查询(CLI ≥ 1.5.0)。omni documents run-query <identifier> <queryId> 会在服务端运行文档中保存的一个查询,并返回单个 JSON / CSV / XLSX 文档 — 查询正文会在服务端读取,因此调用方只能为本次运行替换或添加筛选条件。仪表板上的图块(或包含应用的文档中的任意查询)可由任何有权查看它的人运行;隐藏图块或不在仪表板上的标签页则需要工作簿访问权限。<queryId> 是图块的 queryIdentifierMapKey,或来自 omni documents get-queries 的查询 presentation id。每次运行的 filters 以字段名称为键;无法应用的筛选条件会导致运行失败并返回 400,而不会在不应用筛选条件的情况下继续运行。使用组织范围的密钥时,--user-id 会以该成员的身份运行,也适用于 get-queries。如果你希望保留已保存的行为,请优先使用此命令,而不是重建完整的 query run 请求正文。
AI 驱动的查询生成
你可以用自然语言描述需求,让 Omni 的 AI 生成查询,而不必手动构建查询 JSON。
生成查询(同步)
最快的方式 — 同步返回生成的查询 JSON。传入 --run-query false 可仅获取查询结构而不执行(默认会运行查询)。
# Just generate the query JSON (no execution)
omni ai generate-query your-model-id "Show me revenue by month" --run-query false
响应:
{
"query": {
"fields": ["order_items.created_at[month]", "order_items.total_revenue"],
"table": "order_items",
"filters": {},
"sorts": [{"column_name": "order_items.created_at[month]", "sort_descending": false}],
"limit": 500
},
"topic": "order_items",
"error": null
}
# Generate and execute in one call
omni ai generate-query your-model-id "Top 10 customers by lifetime spend"
可选标志:
--branch-id— 针对特定模型分支进行测试--current-topic-name— 将主题选择限制为特定主题
选择主题
查看 AI 会为某个问题选择哪个主题,而不生成完整查询:
omni ai pick-topic your-model-id "How many users signed up last month?"
智能体查询(异步)
体验完整的 Blobby——包括多步分析、工具使用和主题选择,体现 AI 在生产环境中的实际行为。这是异步流程:提交任务、轮询状态,然后获取结果。
# 1. Submit a job
JID=$(omni ai job-submit your-model-id "Analyze revenue trends; identify our fastest growing category" -o json \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["jobId"])')
# 2. Poll with EARLY EXIT on any terminal state — break on success AND on failure
while :; do
S=$(omni ai job-status "$JID" -o json | python3 -c 'import json,sys;print((json.load(sys.stdin).get("state") or "").lower())')
case "$S" in
complete*) break ;; # done → go fetch the result
fail*|cancel*|error*) echo "job $S"; break ;; # terminal failure → STOP, do not keep sleeping
*) sleep 5 ;; # only the non-terminal branch sleeps
esac
done
# 3. Get the result (resultSummary = narrative; actions[] = structured output)
omni ai job-result "$JID" -o json
轮询循环必须在每种终止状态下提前退出——切勿使用固定次数的
for … sleep … done循环,并且只在成功时退出。 如果成功过滤条件有误(status与state混淆,或COMPLETE与COMPLETED混淆),或者任务出错,这种循环就会一直运行到结束。请使用上面的while :; … case … break形式:仅在默认分支中休眠;状态一旦终止(complete* 或 fail/cancel/error),立即退出。容错读取字段(state ?? status,转为小写;startswith("complete")= 完成),这样一个循环就能兼容下文不同任务的拼写差异。
任务状态的结构。 轮询
state——omni ai job-status没有status字段。状态依次为:QUEUED→EXECUTING→DELIVERING→ 终止状态COMPLETE/FAILED/CANCELLED。注意,它是COMPLETE,不是COMPLETED——模型刷新(completed)和models jobs-get-status(COMPLETED)流程中的拼写各不相同,因此跨任务类型复用的轮询循环需要进行容错终止状态检查:读取字段state ?? status,将其转为小写;将startswith("complete")视为已完成,将{failed, cancelled, error}视为失败。回答文本位于resultSummary;结构化输出位于actions[]中(type: "generate_query"→result.query)。
结果包含一个 actions 数组,其中列出了 AI 执行的每个步骤——查找 type: "generate_query" 的操作以提取生成的查询。响应还包含 resultSummary,其中是 AI 对结果的叙述性解读。
在呈现异步任务的答案之前,请检查 actions[] 条目。任务状态可能已变为 COMPLETE,但某个 generate_query 操作的 status 仍为 "pending",或者没有 csvResult;此时,叙述内容可能描述了已生成但尚未执行的查询。每个操作的 status 表示该步骤的结束情况——complete、partial(完成了请求中的部分内容,并说明缺少什么)、skipped 或 failed;它与 result.status(success / error)相互独立,后者仅表示生成的查询是否运行成功。result.status: "success" 的 partial 操作仍然只回答了部分请求。如果必要操作处于待处理、部分完成或失败状态,不要将任务摘要视为最终结果。运行或重新生成该查询,或者通过另一个异步任务继续同一分析,然后只呈现经过验证的结果。
其他任务命令:
omni ai job-cancel <jobId>— 取消正在运行的任务omni ai job-visualization <jobId>— 获取可视化输出omni ai job-feedback-submit <jobId> --body '{"rating":"good"}'— 对已完成的任务记录赞成或反对反馈(CLI ≥ 1.2.2)。rating为必填项(good/bad);comment是可选的自由文本。只有在轮询报告COMPLETE/FAILED后才能提交——其他任何状态都会返回 409。反馈仅追加且无法读回,因此每个任务只提交一次。使用组织范围的密钥时,请传入--user-id,将反馈关联到提交该任务的用户。
在仪表板中使用作业结果
作业结果中的查询对象不能直接用作仪表板的 queryPresentation——需要先进行转换。关键规则:
- 始终移除
userEditedSQL——它会使磁贴成为非主题查询,因此绕过所有模型控制(对象级访问授权、行级访问筛选器和always_where),并且在仪表板中对受限角色不可见。它包含的${Order Items}主题名称标记在作业执行上下文之外也会失效。 - 当
calculations[]非空时,移除userEditedSQL即可——结构化计算会正确呈现。 - 当
calculations[]为空时,Blobby 将计算编写为内联 SQL。解析后的 AST 位于csvResultFields中(位于result层级,而非result["query"]内),可将其重建为正规的calculations[]条目。顶层expr运算符是聚合函数(SUM、COUNT等)的字段无法重建为表计算——应将其作为带筛选条件的度量添加到模型中。
完整的转换算法、判别逻辑、字段引用注入、聚合跳过处理和健全性检查方法,请参阅 references/job-result-to-presentation.md。
如何选择合适的方法
| 方法 | 最适合 |
|---|---|
omni query run | 你明确知道需要哪些字段、筛选器和排序 |
带 calculations[] 的 omni query run | 明确的表计算请求,且你知道或可以复制 AST 结构 |
omni ai generate-query --run-query=false | 起草简单的查询 AST 以供检查或手动编辑;或仅生成结构,不执行查询(无法运行 Agentic 作业时的备用方法) |
omni ai generate-query --run-query=true | 希望获得同步响应的简单维度/度量查询 |
omni ai job-submit | 任何非简单任务——多步骤分析,或生成表计算/可复用的查询 AST。从 actions[].generate_query 中提取结构化查询/calculations,然后用 query run 验证 |
**明确目标结构时,引导提示词:**当表计算是预期或已知正确的输出时,在提示词中说明这一点——追加“……作为表计算”,或指出语义(“累计总计”/“占总量的百分比”/“移动平均值”)——这样 Agentic 作业就会在 actions[].generate_query 中生成真正的计算,而不是回退到 userEditedSQL/SQL。(Agentic 与 generate-query 的分工,以及“从 actions[].generate_query 中提取 AST,再用 query run 验证”的规则,见上表和表计算部分。)
多步骤分析模式
对于复杂分析,可串联查询:
- 宽泛查询——了解数据的整体形态
- 检查结果——识别有趣的细分群体或模式
- 有针对性的后续查询——根据发现添加筛选条件
- 综合归纳——将结果整合成一段叙述
常见查询模式
时间序列:字段 + 日期维度 + 升序排序 + 日期筛选
Top N:字段 + 指标 + 降序排序 + 限制条数
细分聚合:多个维度 + 多个度量 + 按关键指标降序排序
已知错误
- 不带类型的字符串筛选表达式会被拒绝。
"filters": { "field": "complete" | "last 90 days" | "not null" }自 2026 年 8 月起会返回400 "Unable to parse data stream"(较早的构建版本会返回500 "Cannot use 'in' operator to search for 'query_id' in <value>"——即查询引用/field_name_in_query探测)。请使用带类型的筛选器对象(见上文筛选器部分)。已针对值、日期、空值字符串、数字和布尔形式复现此问题。
链接到结果
查询是临时的,不会为查询结果生成永久 URL。要向用户提供可分享的链接:
- 对于现有仪表板:
{OMNI_BASE_URL}/dashboards/{identifier}(identifier来自文档 API 响应) - 对于新的分析:使用查询作为
queryPresentation,通过omni-content-builder创建文档,然后分享{OMNI_BASE_URL}/dashboards/{identifier}
文档参考
相关技能
- omni-model-explorer — 在查询前发现字段和主题
- omni-content-explorer — 查找可提取查询内容的仪表板
- omni-content-builder — 将查询结果转换为仪表板
- omni-ai-eval — 对 AI 查询生成的准确性进行基准测试和验证