<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Fang Xing - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Fang Xing - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/fang-xing</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/fang-xing</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/fang-xing.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sat, 19 Sep 2026 02:46:15 GMT</lastBuildDate>
  <item>
    <title><![CDATA[One ES|QL query instead of two: WHERE IN subquery replaces the copy-paste loop in Elasticsearch]]></title>
    <description><![CDATA[ES|QL's WHERE clause can filter by another Elasticsearch subquery's results instead of a static ID list you copied by hand, with nested subqueries, NOT IN and compound conditions built in.]]></description>
    <content:encoded><![CDATA[<p>The <a href="https://www.elastic.co/docs/reference/query-languages/esql">Elasticsearch Query Language (ES|QL)</a> <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/where"><code>WHERE</code></a> clause can now filter by the results of another query. If you've been running one query to find suspicious users or failing services, copying the IDs, then pasting them into a second query, you can stop. One ES|QL statement does the whole job: the subquery builds the filter list from live data, and it stays current every time you run it. The feature ships as a technical preview in Elasticsearch 9.5 and supports nesting, <code>NOT IN</code> and compound <code>AND</code><code>/</code><code>OR</code> conditions.</p>  The <code>WHERE IN</code> subquery may change or be removed in a future release. Elastic will work to fix any issues, but technical preview features aren’t subject to the support Service Level Agreement (SLA) of official general availability (GA) features.<h2>Static ID lists vs. dynamic filtering with ES|QL's WHERE clause</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt112e1ace022eb1a1/6a75c6d0b966e18d6563ef05/image1.png" alt="Infographic comparing manual ID copying across queries vs. ES|QL WHERE IN subquery dynamic filtering in a single pipeline" /><p></p><p><strong>Static ID list filtering</strong></p><p><strong>Dynamic filtering with WHERE IN subquery</strong></p><p>Run query A to find the IDs you care about</p><p>Outer query asks the main question</p><p>Copy the values by hand</p><p>Subquery builds the filter list from live data</p><p>Paste them into a static <code>WHERE IN</code> list</p><p><code>WHERE</code> field <code>IN</code> (subquery) applies the filter live</p><p>Run query B with the hardcoded list</p><p>Results stay current with the data</p><p>Repeat the whole process when the data changes</p><p>No copied list to maintain</p><p><em>Figure 1. The old copy-paste loop collapses into a single dynamic filter.</em></p><h2>How the WHERE IN subquery replaces static filter lists</h2><p>Traditional <code>IN</code> filtering is still useful when the list is small and static:</p>FROM logs-*
| WHERE status_code IN (401, 403, 429)<p>But many real investigations don’t start with a tidy list. They start with a question: <em>Which users are suspicious</em>, <em>which hosts are noisy</em>, <em>which services are failing</em>, or <em>which accounts crossed a threshold?</em></p><p>That’s where the <code>WHERE IN</code> subquery becomes useful. The list is produced by ES|QL rather than typed by hand.</p><h2>WHERE IN subquery example: filtering logs by suspicious users</h2>FROM logs-*
| WHERE user.name IN (
    FROM auth-logs-*
    | WHERE event.action == "login_failed"
    | STATS failed_attempts = COUNT(*) BY user.name
    | WHERE failed_attempts &gt;= 10
    | KEEP user.name
  )<p>Read it like this: <em>Show me log events for users who appear in the list of users with at least 10 failed login attempts.</em></p><p>The outer query asks the main question, and the subquery builds the dynamic filter list, eliminating the need to copy and paste.</p><p>The subquery can target a different index or index pattern from the outer query.</p><h2>Filtering without subqueries: the manual ID copy workflow</h2><p>Imagine that you want to inspect traffic for the top failing services. First you run:</p>FROM service-logs-*
| WHERE status_code &gt;= 500
| STATS failures = COUNT(*) BY service.name
| SORT failures DESC
| LIMIT 5<p>Then you copy the five service names and paste them into another query:</p>FROM service-logs-*
| WHERE service.name IN ("checkout", "payments", "search", "profile", "orders")<p>That’s fine once, but less fine when the top five change every hour.</p><h2>Dynamic filtering with a WHERE IN subquery</h2>FROM service-logs-*
| WHERE service.name IN (
    FROM service-logs-*
    | WHERE status_code &gt;= 500
      AND @timestamp &gt;= now() - 2 days
    | STATS failures = COUNT(*) BY service.name
    | SORT failures DESC
    | LIMIT 5
    | KEEP service.name
  )
  AND status_code &gt;= 500
  AND @timestamp &gt;= now() - 2 days
| KEEP @timestamp, service.name, status_code, message<p>The subquery finds the top failing services from the last two days, and the outer query returns the log events for those services. One query builds the full picture.</p><h2>Excluding values with NOT IN subqueries in ES|QL</h2><p>Sometimes the interesting question is about what doesn’t belong:</p>FROM access-logs-*
| WHERE user.name NOT IN (
    FROM known-users
    | WHERE user.name IS NOT NULL
    | KEEP user.name
  )<p>That pattern is useful for exclusion checks, gap analysis, and workflows that ask for the things outside an approved or expected set.</p><h2>Nested subquery chains in ES|QL's WHERE clause</h2><p>An <code>IN</code> subquery replaces the literal value list with a query in parentheses. The inner query runs first and returns a single column, and the outer <code>WHERE</code> filters against it. Because that inner query is a full pipeline, it can contain its own <code>IN</code> subquery, which lets you express a chain of lookups that would otherwise require three separate queries and two rounds of copy-paste.</p>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<p>Read it inside out. The innermost query finds priority regions, and the middle query finds customers in those regions, while the outer query sums revenue for those customers. Each layer is a normal ES|QL pipeline, so each one can filter, aggregate, or sort on its own before handing a clean column up to the layer above.</p><h2>Combining WHERE IN subqueries with AND and OR conditions</h2><p>Because an <code>IN</code> subquery is a Boolean condition, it composes with <code>AND</code> and <code>OR</code> like any other predicate. You can require membership in two independent sets or accept membership in either:</p>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, amount<p>The <code>AND</code> combination finds orders placed by VIP customers for products that are being discontinued. Swap <code>AND</code> for <code>OR</code>, and you get orders that match either condition. Each subquery runs its own pipeline, so the two sets are computed independently and then combined by the Boolean operator.</p><h2>Merging multiple indices into one WHERE IN subquery</h2><p>The query inside an <code>IN</code> subquery is a full pipeline, so its <code>FROM</code> command can reference more than one subquery. Each branch runs its own pipeline, and the <code>FROM</code> command merges the rows from all branches into one result set. <code>KEEP host_id</code> selects the single column that the outer filter needs. This is useful when the values you want to filter against live in several indices with different schemas. For more details on how subqueries in the <code>FROM</code> command handle indices with different schemas, see <a href="https://www.elastic.co/search-labs/blog/esql-subquery-from">Three indices walk into a FROM clause: ES|QL subqueries in Elasticsearch</a>.</p>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_id<p>The <code>IN</code> subquery combines matching host IDs from three indices, prod, staging, and edge, into one value list. The outer query then counts alerts for any host in that combined set. Adding a fourth source means adding one more branch, with no change to the outer query.</p><h2>Using an Elasticsearch subquery inside each FROM branch</h2><p>A <code>FROM</code> subquery gives each index its own branch with its own <code>WHERE</code>, and that <code>WHERE</code> can use an <code>IN</code> subquery. This is how you apply the same dynamic filter across several indices that each have their own schema.</p>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<p>Each branch filters its index down to VIP customers before the two branches combine, so the final aggregation runs over a single normalized set of rows.</p><h2>When to use ES|QL WHERE IN subqueries</h2><ul><li><p>Investigations that start by finding risky users, hosts, accounts, or services.</p></li><li><p>Operational dashboards where the interesting entities change over time.</p></li><li><p>Top-N follow-up queries, such as events for the five noisiest services.</p></li><li><p>Set comparison workflows, especially with <code>NOT IN</code>.</p></li><li><p>Queries that would otherwise need glue code just to pass values from one step to the next.</p></li></ul><h2>Requirements and constraints for WHERE IN subqueries</h2><ul><li><p>Return exactly one column from the <code>IN</code> subquery.</p></li><li><p>Use <code>KEEP</code> at the end of the subquery so the comparison field is obvious.</p></li><li><p>Make sure the outer field and the subquery field have compatible types.</p></li><li><p>If the subquery uses <code>SORT</code>, add an explicit <code>LIMIT</code>, as unbounded <code>SORT</code> isn’t supported in ES|QL yet.</p></li><li><p>Use this for membership filtering. If you need columns from both sides, a <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-lookup-join">JOIN`</a> may be the better tool.</p></li></ul><h2>Why ES|QL dynamic filtering replaces manual query workflows</h2><p>The <code>WHERE IN</code> subquery turns a manual workflow into a declarative one. You can let one query build the filter for another query directly inside the <code>WHERE</code> command, instead of asking ES|QL for a list, copying it somewhere else, and hoping it stays fresh. </p><p>Your <code>WHERE</code> clause now has a better way to handle <em>Filter this by whatever that query finds.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dynamic-filtering-esql-where-in-subquery</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dynamic-filtering-esql-where-in-subquery</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Query Languages]]></category>
    <dc:creator><![CDATA[Fang Xing]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8588803197e341c5/6a75c709f124644e306fd73e/good_oone.png" length="0" type="image/png"/>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One query, three data sources: ES|QL subqueries get FROM, TS and ROW]]></title>
    <description><![CDATA[Filter application logs by live metric behavior and combine indexed data with inline test values. Your filter lists pull from time-series data on the fly, so nothing is hard-coded.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/reference/query-languages/esql">Elasticsearch Query Language (ES|QL)</a> subqueries now support three source commands in Elasticsearch 9.5: <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/from"><code>FROM</code></a> for indexed data, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> for time-series metrics, and <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/row"><code>ROW</code></a> for inline literal values. You can use them individually or combine all three in a single query, filtering log data by live metric behavior or mixing real and synthetic rows without any index setup.</p><p>If you've been exploring ES|QL, you might have noticed that a <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-subquery">subquery</a> used to feel like it had exactly one front door: <code>FROM</code>. And it made sense. Most of the time, queries start with a simple directive to go fetch documents from an index. In an earlier <a href="https://www.elastic.co/search-labs/blog/dynamic-filtering-esql-where-in-subquery">post</a>, we taught the <code>WHERE</code> command a new trick: <code>IN</code> subqueries. And before that, <a href="https://www.elastic.co/search-labs/blog/esql-subquery-from">subqueries showed up in the <code>FROM</code> command</a> to combine data sources. In both instances, every subquery started the same way, with <code>FROM</code>.</p><p>But not every useful query starts with a bulk document fetch. Sometimes you need to evaluate time-series semantics. Other times, you just need to whip up a tiny inline row for testing. Sometimes, the absolute best input to a filter is a dynamic query that builds the list for you on the fly, rather than a static, hard-coded list.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4453fe9ed8dad10b/6a75c5db7724f2073a045965/image3.png" alt="Diagram of three ES|QL subquery source commands with log filtering flow for FROM union and WHERE IN placements" /><p></p><p><strong>Source command</strong></p><p><strong>Reads from</strong></p><p><strong>Best for</strong></p><p><strong>Requirements</strong></p><p>FROM</p><p>Indexes, data streams, aliases, views</p><p>Stored document lookups, live filter lists</p><p>None (works with any index)</p><p>TS</p><p>Time-series data streams</p><p>Metric aggregations with counter-reset handling</p><p>TSDS with <code>index.mode: time_series</code></p><p>ROW</p><p>Inline literal values</p><p>Test cases, seed values, synthetic placeholders</p><p>None (no index needed)</p><p>The <code>FROM</code>, <code>TS</code> and <code>ROW</code> source commands are generally available (GA) in Elasticsearch 9.5, while the <a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-in-subquery"><code>WHERE IN</code> subquery</a> remains in technical preview in 9.5.</p><h2>How ES|QL subquery source commands work</h2><p>Think of subqueries as having two specific placements and three different engines.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5542afed68fff9e7/6a75c5f6cd87c322296bcb39/image1.png" alt="ES|QL subquery source commands grid showing FROM, ROW and TS examples in both FROM union and WHERE IN filter placements" /><p></p><h3>Where subqueries go: FROM and WHERE placements</h3><ul><li><p><strong>Inside the </strong><strong><code>FROM</code></strong><strong> command:</strong> The subquery is an independent result source, contributing its rows to the outer query. Fields that exist in one source but not the other are gracefully filled with null values.</p></li><li><p><strong>Inside the </strong><strong><code>WHERE</code></strong><strong> command:</strong> The <code>IN</code> subquery contributes dynamic values to be used as a predicate or filter.</p></li></ul><h3>Three source commands for starting a subquery</h3><h4>Door #1: FROM (the classic door)</h4><p><code>FROM</code> is the familiar workhorse. You use it when the subquery should read stored data from indices, data streams, aliases, or views. One of its best use cases is the "stop-copy-pasting-IDs" pattern. Instead of running one query, manually copying the output values, and pasting them into the filter of another query, the subquery becomes your live filter list.</p>FROM employees
| WHERE emp_no IN (FROM high_value_accounts
                   | KEEP emp_no
                  )
| KEEP emp_no, first_name, last_name<h4>Door #2: ROW (the tiny door)</h4><p><code>ROW</code> is the lightweight option that requires absolutely no index setup. It allows you to build rows completely out of literal, inline values. This makes <code>ROW</code> useful for small seed values, test cases, allow/deny lists, or one-off "what if?" scenarios. In the query below, <code>ROW</code> is the perfect way to staple a synthetic sentinel row or placeholder directly onto real data.</p>FROM
(FROM access_logs
   | WHERE status == 500
| KEEP cluster, status),
  (ROW cluster = "synthetic", status = 0)
| SORT status
| KEEP cluster, status<h4>Door #3: TS (the time-series door)</h4><p>The <code>TS</code> command targets time-series data streams and enables time-series aggregation functions. Why not just use <code>FROM</code> for metrics? <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> is uniquely optimized for time-series data, and it natively handles tricky scenarios, like counter resets on process restarts and uneven metric publish intervals. Using TS as a subquery lets you filter your application logs by what your metrics are saying. For example, imagine asking ES|QL: <em>Show me log events only from clusters whose peak metric throughput crossed 800</em>:</p>FROM access_logs
| WHERE cluster IN (TS k8s_metrics
                    | STATS peak = MAX(bytes_in) BY cluster
                    | WHERE peak &gt; 800
| KEEP cluster
                   )
| SORT cluster, path
| KEEP cluster, status, path<p>In this scenario, the filter is reacting to live metric behavior rather than being hard-coded.</p><h2>Combining FROM, TS and ROW in one query</h2><p>Because subquery placements and source commands are independent, you can freely mix and match them. You can throw all three doors into a single <code>FROM</code> union to generate a cohesive table containing real logs, a live metrics summary, and a synthetic row.</p>FROM
(FROM access_logs
   | KEEP cluster, status),
  (TS k8s_metrics
   | STATS peak = MAX(bytes_in) BY cluster),
  (ROW cluster = "synthetic")
| STATS log_events = COUNT(status), peak = MAX(peak) BY cluster
| SORT cluster
| KEEP cluster, log_events, peak<h2>ES|QL subquery constraints</h2><p>A few constraints to keep in mind before using subqueries:</p><ul><li><p><strong><code>IN</code></strong><strong> subqueries demand one column:</strong> If a subquery feeds an <code>IN</code> operator, it must project exactly one column. Use the <code>KEEP</code> command to make that explicitly clear.</p></li><li><p><strong><code>TS</code></strong><strong> requires a time series data stream (TSDS):</strong> The <code>TS</code> command only works on data stored in a <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">TSDS</a>, which uses <code>index.mode: time_series</code>.</p></li></ul><h2>The takeaway: when to use each source command</h2><p>Subqueries in ES|QL provide a structured way to compose queries by using the results of one query as the input to another. Choosing the appropriate source command (<code>FROM</code>, <code>ROW</code>, or <code>TS</code>) lets you combine data and generate inline values. It also lets you filter dynamically without duplicating query logic. For more details and additional examples, see the ES|QL subqueries documentation.</p><ul><li><p><a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-subquery">Subquery in FROM command</a></p></li><li><p><a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-in-subquery">Subquery in WHERE command</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/esql-subquery-source-commands</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/esql-subquery-source-commands</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Query Languages]]></category>
    <dc:creator><![CDATA[Fang Xing]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71cfd2ab01e3262e/6a75c5beaec90746295fd3b7/image2.png" length="0" type="image/png"/>
    <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>