作者:来自 Elastic Fang Xing
ES|QL 的WHERE子句现在可以根据另一个 Elasticsearch subquery 的结果进行过滤,而不再需要手动复制静态 ID 列表;同时还支持嵌套 subqueries、NOT IN以及复合条件。
Elasticsearch Query Language(ES|QL) 的 WHERE 子句现在可以根据另一个查询的结果进行过滤。
如果你之前需要先运行一个查询来查找可疑用户或出现故障的服务,然后复制这些 ID,再将它们粘贴到第二个查询中,那么现在不再需要这样做了。
一个 ES|QL 语句就可以完成整个过程:subquery 从实时数据中动态构建过滤列表,并且每次运行查询时都会保持最新。
该功能在 Elasticsearch 9.5 中以 technical preview 形式提供,并支持:
嵌套 subqueries
NOT IN复合
AND/OR条件
注意:
WHERE INsubquery 未来可能会发生变化,也可能在未来版本中被移除。Elastic 会努力修复相关问题,但 technical preview 功能不受正式 GA 功能所适用的支持 Service Level Agreement(SLA)约束。
静态 ID 列表与使用 ES|QLWHERE子句进行动态过滤的对比
| 静态 ID 列表过滤 | 使用WHERE INsubquery 进行动态过滤 |
|---|---|
| 运行查询 A,找出你关注的 ID | 外层查询直接提出主要问题 |
| 手动复制这些值 | Subquery 根据实时数据构建过滤列表 |
将这些值粘贴到静态WHERE IN列表中 | WHERE字段IN(subquery)实时应用过滤 |
| 使用硬编码列表运行查询 B | 结果会随数据保持最新 |
| 数据发生变化后,重复整个流程 | 无需维护复制出来的列表 |
WHERE INsubquery 如何取代静态过滤列表
当列表较小且内容固定时,传统的IN过滤仍然非常有用:
FROM logs-* | WHERE status_code IN (401, 403, 429)但许多实际的调查并不是从一个整齐的列表开始的,而是从一个问题开始的:
- 哪些用户可疑?
- 哪些主机产生了大量噪声?
- 哪些服务出现故障?
- 哪些账户超过了某个阈值?
这正是WHERE INsubquery 发挥作用的地方。这个列表由 ES|QL 动态生成,而不再需要手动输入。
WHERE INsubquery 示例:根据可疑用户过滤日志
FROM logs-* | WHERE user.name IN ( FROM auth-logs-* | WHERE event.action == "login_failed" | STATS failed_attempts = COUNT(*) BY user.name | WHERE failed_attempts >= 10 | KEEP user.name )可以这样理解:
显示那些出现在 “至少有 10 次登录失败的用户列表” 中的用户所产生的日志事件。
外层查询负责提出主要问题,而 subquery 负责动态构建过滤列表,从而省去了复制和粘贴的步骤。
Subquery 可以针对与外层查询不同的索引或索引模式。
不使用 subqueries 进行过滤:手动复制 ID 的工作流
假设你想检查故障最多的服务所产生的流量。首先运行:
FROM service-logs-* | WHERE status_code >= 500 | STATS failures = COUNT(*) BY service.name | SORT failures DESC | LIMIT 5然后,你需要复制这 5 个服务名称,再将它们粘贴到另一个查询中:
FROM service-logs-* | WHERE service.name IN ("checkout", "payments", "search", "profile", "orders")一次这样做当然没问题,但如果排名前五的服务每小时都会变化,就不太方便了。
使用WHERE INsubquery 进行动态过滤
FROM service-logs-* | WHERE service.name IN ( FROM service-logs-* | WHERE status_code >= 500 AND @timestamp >= now() - 2 days | STATS failures = COUNT(*) BY service.name | SORT failures DESC | LIMIT 5 | KEEP service.name ) AND status_code >= 500 AND @timestamp >= now() - 2 days | KEEP @timestamp, service.name, status_code, messageSubquery 会从过去两天的数据中找出故障最多的服务,而外层查询则返回这些服务对应的日志事件。一个查询就能呈现完整情况。
在 ES|QL 中使用NOT INsubqueries 排除值
有时,我们真正关心的问题恰恰是哪些内容不属于某个范围:
FROM access-logs-* | WHERE user.name NOT IN ( FROM known-users | WHERE user.name IS NOT NULL | KEEP user.name )这种模式适用于排除检查、差距分析,以及那些需要查找不属于已批准或预期集合的数据的工作流。
ES|QLWHERE子句中的嵌套 subquery 链
INsubquery 会将字面量值列表替换为括号中的一个查询。内部查询首先执行,并返回单个列,然后外层WHERE根据该列进行过滤。
由于这个内部查询本身就是一个完整的 pipeline,因此它可以包含自己的INsubquery。这样,你就可以表达一连串的查找操作,而不再需要运行三个独立的查询,也不需要进行两轮复制和粘贴。
FROM orders | WHERE customer_id IN ( FROM customers | WHERE region_id IN ( FROM regions | WHERE tier == "priority" | KEEP region_id ) | KEEP customer_id ) | STATS revenue = SUM(amount) BY customer_id从内到外阅读。最内层查询查找优先级区域,中间层查询查找这些区域中的客户,而最外层查询则汇总这些客户的收入。每一层都是一个普通的 ES|QL 管道,因此每一层都可以先自行进行过滤、聚合或排序,然后再将整理好的列传递给上一层。
将 WHERE IN 子查询与 AND 和 OR 条件结合使用
由于IN子查询是一个布尔条件,因此它可以像其他谓词一样与AND和OR组合使用。你可以要求同时属于两个独立的集合,也可以接受属于其中任意一个集合:
FROM orders | WHERE customer_id IN (FROM vip_customers | KEEP customer_id) AND product_id IN (FROM discontinued_products | KEEP product_id) | KEEP order_id, customer_id, product_id, amountAND组合会查找由 VIP 客户下单且产品正在停产的订单。将AND替换为OR,就可以得到满足任一条件的订单。每个子查询都会运行自己的管道,因此两个集合会独立计算,然后再通过布尔运算符进行组合。
将多个索引合并到一个 WHERE IN 子查询中
IN子查询中的查询是一个完整的管道,因此其中的FROM命令可以引用多个子查询。每个分支都会运行自己的管道,而FROM命令会将所有分支中的行合并到一个结果集中。KEEP host_id会选择外层过滤器所需要的唯一列。当你要进行过滤的值分布在多个具有不同架构的索引中时,这种方式非常有用。有关FROM命令中的子查询如何处理具有不同架构的索引的更多详细信息,请参阅 三个索引进入一个 FROM 子句:Elasticsearch 中的 ES|QL 子查询。
FROM alerts | WHERE host_id IN ( FROM (FROM prod_hosts | WHERE region == "us-east"), (FROM staging_hosts | WHERE region == "us-east"), (FROM edge_hosts | WHERE region == "us-east") | KEEP host_id ) | STATS alert_count = COUNT(*) BY host_idIN子查询会将三个索引prod、staging和edge中匹配的主机 ID 合并到一个值列表中。外层查询随后会统计这个合并集合中任意主机的告警数量。添加第四个数据源意味着再添加一个分支,而无需修改外层查询。
在每个 FROM 分支中使用 Elasticsearch 子查询
FROM子查询会为每个索引提供一个独立的分支,每个分支都有自己的WHERE,而这个WHERE可以使用IN子查询。这就是如何在多个具有各自架构的索引之间应用相同的动态过滤器。
FROM (FROM orders | WHERE customer_id IN (FROM vip_customers | KEEP customer_id)), (FROM refunds | WHERE customer_id IN (FROM vip_customers | KEEP customer_id)) | STATS total_events = COUNT(*) BY customer_id每个分支都会先将其索引过滤为 VIP 客户,然后再合并两个分支,因此最终的聚合会针对一个统一的行集合执行。
何时使用 ES|QL WHERE IN 子查询
从查找高风险用户、主机、账户或服务开始的调查。
有趣的实体会随时间变化的运维仪表板。
Top-N 后续查询,例如查询噪声最大的五个服务产生的事件。
集合比较工作流,尤其是使用
NOT IN时。原本需要编写粘合代码,只是为了将一个步骤中的值传递到下一步骤的查询。
WHERE IN 子查询的要求和限制
IN子查询必须恰好返回一列。在子查询末尾使用
KEEP,这样可以明确比较字段。确保外层字段和子查询字段具有兼容的类型。
如果子查询使用
SORT,请添加显式的LIMIT,因为 ES|QL 目前不支持无界SORT。将其用于成员资格过滤。如果你需要两侧的列,那么 JOIN 可能是更合适的工具。
为什么 ES|QL 动态过滤可以替代手动查询工作流
WHERE IN子查询将手动工作流转变为声明式工作流。你可以直接在WHERE命令中让一个查询为另一个查询构建过滤器,而不必先让 ES|QL 返回一个列表,再将其复制到其他地方,并希望它始终保持最新。
现在,你的WHERE子句有了一种更好的方式来处理根据该查询找到的内容进行过滤。
常见问题
子查询有哪些限制?
子查询必须恰好返回一列。在子查询末尾使用KEEP,以明确指定比较字段。外层查询和子查询中的字段类型必须兼容。如果子查询包含SORT,请添加显式的LIMIT。
什么时候应该使用 WHERE IN 子查询,而不是 JOIN?
将WHERE IN子查询用于成员资格过滤,也就是说,过滤外层查询中与另一个查询返回的一组值匹配(或不匹配)的行。当你需要在同一个结果行中获取关系两侧的列时,请使用JOIN。
原文:ES|QL WHERE clause: dynamic filtering with IN subqueries | Elasticsearch Labs