Loading

Filter rows with ES|QL IN subquery in a WHERE command

An ES|QL query wrapped in parentheses can be used as a subquery on the right-hand side of the IN and NOT IN operators in the WHERE command.

The subquery filters rows from the outer query by comparing a field or expression against the set of values it returns. This lets you filter against the current results of another query without running it separately and copying its values into a literal IN list.

Use IN to keep rows whose values match subquery results and NOT IN to exclude them.

... | WHERE <expression> IN (FROM index_pattern [| processing_commands]) | ...
... | WHERE <expression> NOT IN (FROM index_pattern [| processing_commands]) | ...
		

The subquery starts with a source command followed by zero or more piped processing commands, all enclosed in parentheses. The source command is usually FROM, and ROW and TS are also supported. The subquery must return exactly one column, whose values are compared against <expression>. The column and <expression> must have compatible types.

The outer query is not limited to FROM either: it can also start with ROW or TS and still filter its rows with an IN subquery.

A subquery in a WHERE command is non-correlated: it runs independently and cannot reference columns from the outer query. Because it runs at query time, its results reflect the current state of the data.

Unlike a subquery in a FROM command, which contributes rows to the combined result set, a subquery in a WHERE command returns exactly one column. The outer IN or NOT IN predicate uses the values from that column as its comparison set.

An IN subquery can itself contain another IN subquery, and multiple IN subqueries can be combined with other predicates using AND, OR, and NOT.

The subquery pipeline can include commands such as the following:

Source commands:

Processing commands:

The following examples show how to use IN subqueries within the WHERE command.

Use IN to keep only the rows whose value is contained in the subquery result:

FROM employees
| WHERE emp_no IN (FROM employees | WHERE salary > 70000 | KEEP emp_no)
| SORT emp_no
| KEEP emp_no
		
emp_no:integer
10007
10019
10027
10029
10033
10045
10097
10099

The subquery selects the emp_no of every employee earning more than 70000, and the outer query keeps only those employees.

Use NOT IN to keep only the rows whose value is not contained in the subquery result:

FROM employees
| WHERE emp_no NOT IN (FROM employees | WHERE salary < 10000 | KEEP emp_no)
| SORT emp_no
| KEEP emp_no
| LIMIT 3
		
emp_no:integer
10001
10002
10003

Here the subquery selects every employee earning less than 10000. No employee matches, so the subquery returns an empty result. NOT IN therefore excludes nothing and every employee is kept, so the first three rows by emp_no are 10001 through 10003.

Use STATS inside the subquery to compare against aggregated values:

FROM employees
| WHERE salary IN (FROM employees | STATS m = max(salary) by languages | KEEP m)
| KEEP emp_no, salary, languages
| SORT emp_no
		
emp_no:integer salary:integer languages:integer
10007 74572 4
10019 73717 1
10029 74999 null
10045 74970 3
10094 66817 5
10099 73578 2

The subquery computes the maximum salary per language group, and the outer query keeps only the employees whose salary matches one of those maximums.

Multiple IN subqueries can be combined with other predicates using AND, OR, and NOT:

FROM employees
| WHERE emp_no NOT IN (FROM employees | WHERE languages IN (1, 2) | KEEP emp_no)
    AND emp_no IN (FROM employees | WHERE salary > 70000 | KEEP emp_no)
    AND languages > 0
| SORT emp_no
| KEEP emp_no, languages, salary
		
emp_no:integer languages:integer salary:integer
10007 4 74572
10045 3 74970
10097 3 71165

This query keeps employees who do not speak the two languages, earn more than 70000, and have a known number of languages.

Use a lookup join inside the subquery before building the list of values:

FROM employees
| WHERE emp_no IN (
    FROM employees
    | EVAL language_code = languages
    | LOOKUP JOIN languages_lookup ON language_code
    | WHERE language_name == "German"
    | KEEP emp_no
  )
| SORT emp_no
| KEEP emp_no, first_name
| LIMIT 5
		
emp_no:integer first_name:keyword
10003 Parto
10007 Tzvetan
10010 Duangkaew
10031 null
10036 null

The subquery joins each employee's languages code with the languages_lookup index, keeps only German speakers, and returns their emp_no. The outer query then keeps only those employees.

The subquery can start with a ROW source command to match against an inline list of values:

FROM employees
| WHERE emp_no IN (ROW x = [10001, 10002, 10003] | MV_EXPAND x)
| SORT emp_no
| KEEP emp_no, first_name
		
emp_no:integer first_name:keyword
10001 Georgi
10002 Bezalel
10003 Parto

The ROW command builds a multivalued field, MV_EXPAND turns it into one value per row, and the outer query keeps only the employees whose emp_no is one of those values.

The outer query can also start with ROW, using an IN subquery to keep only the values that exist in another index:

ROW emp_no = [10001, 10002, 99999]
| MV_EXPAND emp_no
| WHERE emp_no IN (FROM employees | KEEP emp_no)
| SORT emp_no
		
emp_no:integer
10001
10002

The ROW command provides a list of candidate emp_no values, and the subquery filters out 99999, which does not match any employee.

The subquery can start with a TS source command to build the list of values from time series data. In this example the outer query reads the downsampled k8s-downsampled index with FROM, while the subquery uses TS over the raw k8s index:

FROM k8s-downsampled
| WHERE cluster IN (TS k8s
                    | STATS max_bytes = max(to_long(network.total_bytes_in)) BY cluster
                    | WHERE max_bytes > 10000
                    | KEEP cluster)
| STATS buckets = COUNT(*) BY cluster
| SORT cluster
		
buckets:long cluster:keyword
9 prod
9 qa

The subquery aggregates the raw time series metrics to find the clusters whose peak ingest exceeds a threshold, and the outer query then reports the downsampled buckets for those clusters. The outer FROM and the TS subquery reference different indices because a single index cannot be read as both a standard source (FROM) and a time series source (TS) in the same query.

The outer query can also start with TS, so both the outer query and the subquery read from time series data:

TS k8s
| WHERE cluster IN (TS k8s
                    | STATS max_bytes = max(to_long(network.total_bytes_in)) BY cluster
                    | WHERE max_bytes > 10000
                    | KEEP cluster)
| STATS max_bytes = max(to_long(network.total_bytes_in)) BY cluster
| SORT cluster
		
max_bytes:long cluster:keyword
10277 prod
10797 qa

The subquery keeps only the clusters whose maximum ingested bytes exceed 10000, and the outer query reports those clusters.

A subquery can union several source commands with the FROM (...) syntax, mixing ROW, FROM, and TS branches. Each branch must produce the same single column:

FROM k8s-downsampled
| WHERE cluster IN (FROM
                       (ROW cluster = "staging"),
                       (FROM employees | WHERE emp_no == 10001 | EVAL cluster = "prod" | KEEP cluster),
                       (TS k8s
                        | STATS max_bytes = max(to_long(network.total_bytes_in)) BY cluster
                        | WHERE max_bytes > 10500
                        | KEEP cluster)
                   )
| STATS buckets = COUNT(*) BY cluster
| SORT cluster
		
buckets:long cluster:keyword
9 prod
9 qa
9 staging

The ROW branch contributes staging, the FROM branch contributes prod, and the TS branch contributes qa. Their union is the list of values the outer query matches against, so all three clusters are kept.

IN and NOT IN subqueries can be nested inside arbitrarily complex boolean conditions built from AND, OR, and NOT:

FROM employees
| WHERE emp_no IN (FROM employees | WHERE salary > 73000 | KEEP emp_no)
     OR (gender == "F" AND (salary > 70000 OR emp_no NOT IN (FROM employees | WHERE languages > 1 | KEEP emp_no)))
| SORT emp_no
| KEEP emp_no, gender, languages, salary
| LIMIT 15
		
emp_no:integer gender:keyword languages:integer salary:integer
10007 F 4 74572
10009 F 1 66174
10019 null 1 73717
10023 F null 47896
10024 F null 64675
10027 F null 73851
10029 M null 74999
10041 F 1 56415
10044 F 1 39728
10045 M 3 74970
10092 F 1 25976
10099 F 2 73578

The outer query keeps an employee when either their emp_no is returned by the first IN subquery (salaries above 73000), or they are female and either earn more than 70000 or their emp_no is not returned by the NOT IN subquery.

An IN subquery can also appear inside a subquery in the FROM command. Here the FROM command unions two employee sources, and the second branch is filtered with an IN subquery that itself nests a NOT IN subquery:

FROM
  (FROM employees | WHERE salary > 70000 | KEEP emp_no, first_name, salary, languages),
  (FROM employees
   | WHERE emp_no IN (
       FROM employees
       | WHERE salary NOT IN (
           FROM employees | SORT salary DESC | LIMIT 3 | KEEP salary
         )
       | KEEP emp_no)
   | WHERE languages == 1
   | KEEP emp_no, first_name, salary, languages)
| SORT emp_no
| KEEP emp_no, first_name, salary, languages
| LIMIT 6
		
emp_no:integer first_name:keyword salary:integer languages:integer
10005 Kyoichi 63528 1
10007 Tzvetan 74572 4
10009 Sumant 66174 1
10013 Eberhardt 48735 1
10019 Lillian 73717 1
10019 Lillian 73717 1

The first FROM branch keeps every employee earning more than 70000. The second branch keeps employees who speak language one and whose emp_no is returned by the IN subquery, which in turn excludes the three highest salaries with a nested NOT IN subquery. The outer query reports the union of both branches.

IN subqueries can be used inside FORK branches, so each branch can apply its own subquery-based filter:

FROM employees
| FORK (WHERE emp_no IN (FROM employees | WHERE salary > 74000 | KEEP emp_no) | KEEP emp_no, salary)
       (WHERE emp_no IN (FROM employees | WHERE salary < 30000 | KEEP emp_no) | KEEP emp_no, salary)
| SORT emp_no, _fork
| KEEP emp_no, salary, _fork
| LIMIT 5
		
emp_no:integer salary:integer _fork:keyword
10007 74572 fork1
10015 25324 fork2
10026 28336 fork2
10029 74999 fork1
10035 25945 fork2

The first FORK branch keeps the high earners returned by its IN subquery, the second branch keeps the low earners returned by its IN subquery, and the _fork column records which branch produced each row.

An IN subquery can only appear in the WHERE command. It is not supported in other commands.

The subquery must produce a single column to compare against the left-hand side expression. A subquery that returns zero or more than one column is rejected.

An IN subquery must sit at the top of the WHERE condition, optionally combined with other predicates using AND, OR, and NOT. It cannot be used as an argument to another expression, such as inside a scalar function or an IS NOT NULL check.

The subquery is executed independently and cannot reference columns from the outer query.