Define reusable queries with ES|QL views

A view is a virtual index defined by an ES|QL query. You reference a view by name in the FROM command, just like an ordinary index. The query runs each time the view is referenced, so results always reflect the current state of the data.

A view has two components:

  • Name: unique within the index namespace, used anywhere an index name is accepted in FROM.
  • Definition: a complete ES|QL query that runs each time the view is referenced.

Here's how a view works in practice:

  1. Start with a query

    FROM addresses
    | RENAME city.country.name AS country
    | EVAL country = CASE(country == "United States of America", "United States", country)
    | STATS count=COUNT() BY country
    		
    count:long country:keyword
    1 Japan
    1 Netherlands
    1 United States
  2. Save it as a view

    				PUT /_query/view/country_addresses
    					{
        "query": """
            FROM addresses
            | RENAME city.country.name AS country
            | EVAL country = CASE(country == "United States of America", "United States", country)
            | STATS count=COUNT() BY country
            """
    }
    		
  3. Reference it by name, just like an index

    FROM country_addresses
    		
    count:long country:keyword
    1 Japan
    1 Netherlands
    1 United States

Views are a good fit when you want to:

  • Reuse a named query. Wrap a frequently used ES|QL pipeline as a view and reference it by name, instead of repeating the same query in every request.
  • Abstract common transformations. Centralize renames, type conversions, or derived fields so consumers see a consistent set of columns without needing to know the underlying source structure.
  • Combine pre-processed data sources. Define one view per source, each with its own filters or aggregations, and query them together in a single FROM clause.
  • Simplify queries for downstream tools. Dashboards, alerts, or ad-hoc analysts can query FROM my_view without needing to know the indices or processing commands behind it.

Use the REST API to create, update, delete, and list views:

PUT /_query/view/<name> rejects a query that cannot be parsed.

To check the query without running it, set the validate query parameter to true. Validation is off by default.

With validate=true:

  • If the query uses an index or column that does not exist, the response carries a Warning header.
  • A warning does not stop Elasticsearch from saving the view. The view is saved whether or not validation reports a problem, so you can create a view before the index it reads from.
  • The analysis uses your privileges. An index you cannot read is reported as unknown.
  • Failures unrelated to the query, such as an overloaded cluster, produce no warning.

Some views are reserved. Elasticsearch or its components create them, not users. A reserved view is marked with "reserved": true in the get view response.

You cannot modify or delete a reserved view through the REST API:

  • PUT /_query/view/<name> fails if a reserved view with that name exists. You also cannot set reserved in a request.
  • DELETE /_query/view/<name> fails with cannot delete reserved view [<name>].

You can query and read a reserved view like any other view, subject to the same privileges.

Use views as if they were ordinary indices:

FROM index_pattern
		

Where index_pattern is a comma-separated list of index or view names. Index expressions can include wildcards and date math. Whether a wildcard also matches views depends on the version and deployment type.

By default, a wildcard pattern in FROM does not match views. FROM my-view-1 reads the view by exact name, while FROM my-view-* resolves to indices, data streams, and aliases only. Registered views are excluded.

To include views in wildcard resolution, enable the wildcards_match_views setting:

SET wildcards_match_views = true;
FROM my-view-*
		

You can also send it in the _query request body as "settings": {"wildcards_match_views": true}. A value set in the query overrides the request body, but neither value enters a referenced view. Refer to Settings across view boundaries for the limits and warnings.

As an alternative, set the cluster-wide default with esql.query.settings.wildcards_match_views in elasticsearch.yml or through the cluster settings API. The cluster default applies both outside and inside views.

Wildcard patterns in FROM match registered views automatically.

View operations use the standard Elasticsearch index privileges, applied to the view name.

Operation Privilege (on the view name)
Query (FROM my_view) read, all
Create or update (PUT /_query/view/<name>) create_view, manage_view, manage, all
Read definition (GET /_query/view/<name>) read_view_metadata, manage_view, manage, all
Delete (DELETE /_query/view/<name>) delete_view, manage_view, manage, all

read on a view name permits querying the view but does not grant access to the data behind it. The caller must also have read on every index or alias the view definition resolves to, including through nested views.

Access is enforced at query time. Creating a view requires no privilege over the indices it references.

If some underlying indices are unauthorized, the query fails with a 403. If none are accessible, it fails with a 400 Unknown index. Wildcard patterns (FROM view-*) silently exclude unauthorized views.

Nested views are resolved under the calling user's credentials. Access to an outer view does not grant access to any inner view it references.

read on a view name must not carry DLS or FLS. If it does, the query fails with a 403 and the views_with_dls_or_fls error field lists the affected names. This applies at every nesting level.

DLS or FLS on the underlying indices is applied normally.

{
  "indices": [
    {
      "names": ["country_addresses"],
      "privileges": ["read", "create_view", "read_view_metadata", "delete_view"]
    },
    {
      "names": ["addresses"],
      "privileges": ["read"]
    }
  ]
}
		

The following examples show how to use views within the FROM command.

Assume we've defined three views in a similar way to the example above, each counting the number of documents that reference a particular country, but from three different source indices:

  • country_airports - reports counts of documents per country from our airports index
  • country_addresses - reports counts of documents per country from our addresses index
  • country_languages - reports counts of documents per country from our languages index

Now we can query these together with a query like:

FROM country_addresses, country_airports, country_languages
| WHERE country LIKE "United*"
| SORT country ASC, count DESC
		
count:long country:keyword
17 United Kingdom
1 United Kingdom
129 United States
1 United States
1 United States

The same country might appear in multiple views, producing multiple rows. We could combine these with a STATS command, using SUM(count) BY country.

Note

This example requires wildcards_match_views = true. By default, wildcards do not match views. Refer to wildcard patterns and views and review the ES|QL view limitations.

FROM country_*
| STATS count=SUM(count) BY country
| WHERE count > 11
| SORT count DESC, country ASC
		
count:long country:keyword
131 United States
50 India
45 Mexico
41 China
38 Canada
31 Brazil
26 Russia
18 United Kingdom
17 Australia
13 Argentina
13 Germany
12 France
12 Indonesia

Note how we used SUM to combine the counts of the three previously aggregated count columns.

We can define views with complex queries, including commands like LOOKUP JOIN:

				PUT /_query/view/airports_mp_filtered
					{
    "query": """
        FROM airports
        | RENAME abbrev AS code
        | LOOKUP JOIN airports_mp ON abbrev == code
        | WHERE abbrev IS NOT NULL
        | DROP code
       """
}
		

This creates a view called airports_mp_filtered that contains all rows from the airports index that also have a matching abbrev inside the airports_mp index. This is effectively a subset of the airports index.

We could, for example, see how many airports are defined only in airports versus how many are defined in the view, by combining both a view and an index in the same FROM command:

FROM airports_mp_filtered, airports
| STATS duplications=COUNT() BY abbrev
| STATS count=COUNT() BY duplications
| SORT count DESC
		
count:long duplications:long
880 1
7 2
1 3

The METADATA directive is supported both inside and outside a view. Views follow the same rules as subqueries.

Inside the view, the directive generates columns just like other fields. You can use them for filtering and as output columns.

Except for _class and _name, an outer METADATA request returns null when the source output does not contain the requested field. When the source retains a metadata field in its output, the outer query receives its values even if it does not request that field with METADATA.

Warning

METADATA on the outer FROM is not handled consistently.

If the FROM names only a view, the directive is ignored. For example, FROM my_view METADATA _index returns the view contents without an _index column.

If the FROM combines that source with other sources, the requested metadata columns might appear with null values for rows from that source, but this behavior is not consistent.

To retrieve metadata values reliably, request the fields inside the source and retain them in its output.

_class and _name are the only metadata fields that work on a view without the view body declaring them. The outer query answers them for each view. _class is view. _name is the view name. You do not need the directive inside the view. If the view body also declares _class or _name, the outer values replace them.

FROM airports_mp_filtered, airports METADATA _class, _name
| WHERE abbrev == "LUH"
| KEEP abbrev, _class, _name
| SORT _name
		
abbrev:keyword _class:keyword _name:keyword
LUH index airports
LUH view airports_mp_filtered

Views behave like inline subqueries at execution time and when you start combining multiple views, it helps to know how nesting works and where the limits are.

When a query references one or more views, each view's definition query executes independently at query time, in parallel where possible. This is the same execution model used by FROM subqueries and FORK.

Results from all sources (indices, views, subqueries) are unioned into a single result set. Duplicate rows are preserved. Columns that exist in one source but not another are filled with null.

A view definition can reference another view. This is called a nested view. ES|QL allows nesting to a depth of 10.

When multiple views are referenced within the same index pattern, each view executes independently (in parallel if possible), similar to subqueries and FORK.

Query compaction flattens nested view branches into one merge only when the result has at most 20 branches by default (esql.query.max_branch_count). If flattening would exceed that cap, ES|QL keeps the nested plan instead of failing the query. The same setting also caps the total number of leaf branches in the query.

A query and the views and subqueries it references are limited to 500 commands in total (esql.query.max_command_count). The limit applies both to the query as written and to the expanded plan after every view and subquery body is inlined, so deeply nested or heavily repeated view references fail instead of consuming unbounded memory.

Views, subqueries, and FORK share a maximum branch count of 8. For example, a single index pattern could reference four views and four subqueries, but adding one more would exceed the limit and the query will fail.

Nested branching is supported. A view definition can contain FROM subqueries, nested views, and FORK, and a query can combine that view with other branched sources. The complete expanded query remains subject to the branch and command limits described above. The restriction on two user-written FORK commands along the same uninterrupted pipeline still applies.

Branching and nesting are allowed in combination only when there is no more than one branch point:

  • A view can contain subqueries, but that view cannot be used together with other views, and the subqueries can reference only nested views that contain no further branching.
  • A subquery can contain views, but those views must not introduce any additional branch points through subqueries or FORK.

When a view definition contains branches, such as subqueries or references to other views, query compaction tries to flatten the inner branches into the outer branch set.

Compaction produces a single-level plan when the flattened width is within esql.query.max_branch_count (20 by default). If flattening would exceed the limit, ES|QL keeps the nested plan.

Compaction produces the single branch point these versions require when the flattened plan has at most 8 branches. Otherwise, the query fails.

The following example shows how compaction works. Two views are each defined as a pair of subqueries:

				PUT /_query/view/view_x
					{
    "query": """
        FROM (
            FROM app-events-* | KEEP msg, level
        ), (
            FROM auth-events-* | KEEP msg, level
        )
       """
}
		
				PUT /_query/view/view_y
					{
    "query": """
        FROM (
            FROM nginx-events-* | KEEP msg, level
        ), (
            FROM apache-events-* | KEEP msg, level
        )
       """
}
		

A query references both views alongside a regular index:

FROM other-events, view_x, view_y
| STATS count(msg) BY level
		

Without compaction, this would create two levels of branching. Three outer branches exist, and two of them branch again inside their view definitions:

Mermaid diagram

Compaction flattens the inner view branches into the outer branch set, producing a single-level plan with five branches:

Mermaid diagram

Compaction doesn't apply if the view definition contains processing commands after its subqueries. Those commands must run on the combined branch output, so the branch level can't be collapsed.

If compaction isn't possible, or flattening would exceed the branch cap, ES|QL keeps the nested plan and executes it subject to the overall branch and command limits.

If compaction isn't possible, the query fails because these versions allow only one branch point in the plan.

ES|QL views have the following limitations:

A role entry that grants read on a view name must not carry DLS or FLS restrictions. If it does, the query fails with a 403. DLS/FLS on the underlying indices is applied normally. Refer to view privileges.

Views may be nested up to depth 10.

View definitions can contain branched query plans created by FROM subqueries, nested views, and FORK. These branches can be nested, subject to the branch and command limits described in How views execute.

Branching and nesting are allowed in combination only when there is no more than one branch point. A view can contain subqueries only when the resulting plan can be flattened through query compaction.

Views can reference remote indices in cross-cluster search, but you can't query a view that is defined on a remote cluster.

Remote views are invisible to source resolution. An exact reference such as FROM cluster:view returns an Unknown index error.

A query fails if its source expression matches a remote view.

Views are available in serverless and cross-project search, but with some limitations:

  • You can define views in both origin and linked projects.
  • CPS resolves index expressions against indices in every project that the expression targets, but it resolves views only in the origin project. Views in linked projects are invisible to the query, including through qualified expressions such as FROM linked-project:view.
  • CPS uses the same resolution process for index expressions in the top-level query and inside view definitions.
  • An unqualified index expression can match a view in the origin project and indices in linked projects. CPS returns results from both.

A Query DSL filter on the request applies to the output of a view, after the commands in the view definition. If Elasticsearch can't translate a filter clause to ES|QL, it omits that clause for the view and returns a warning. Check the response warnings because omitting a clause can produce more results than the complete filter would.

For example, create a view that counts sales by region:

				PUT /_query/view/sales_by_region
					{
  "query": "FROM sales | STATS total = COUNT(*) BY region"
}
		

The following request requires both should clauses to match:

				POST /_query
					{
  "query": "FROM sales_by_region | KEEP region, total | LIMIT 1000",
  "filter": {
    "bool": {
      "should": [
        { "term": { "region": "eu" } },
        { "term": { "total": 3 } }
      ],
      "minimum_should_match": 2
    }
  }
}
		

ES|QL can't translate a minimum_should_match value greater than 1. It skips the should group for the view, which can return more rows than the complete filter would, and returns this warning:

The request filter could not be fully applied to view(s); the following Query DSL constructs are not supported and were skipped: [bool[minimum_should_match=2]] on view [sales_by_region]. Use a WHERE clause to filter rows from views instead
		

A Query DSL filter on the request applies to the source indices in the view definition, not to the view output.

A view definition can read from index aliases and data streams. An alias cannot point to a view.

An Elasticsearch transform is not an ES|QL source. To query transform results from a view, use the transform's destination index as the source in the view definition.

A view definition can read from a dataset, which ES|QL Data Federation uses to query external data.

A wildcard in the view definition matches datasets only when the view definition sets wildcards_match_datasets to true.

Only the following settings are supported in a view definition:

Setting Supported values
unmapped_fields DEFAULT, NULLIFY, and LOAD
wildcards_match_views true and false
wildcards_match_datasets true and false

LOAD_ALL is not supported in a view definition.

All other settings, including time_zone, are rejected in a view definition.

A view definition can't contain a SET directive.

The three settings supported in view definitions are scoped at every view boundary. Values from the calling query, request body, or an enclosing view don't enter the view. The cluster setting is used to set the default throughout the query, both inside and outside views. A value in a view definition overrides that default for its body, but doesn't enter a nested view. If neither is configured, the built-in default applies.

Other query settings are rejected in a view definition, but aren't scoped to individual views. Their effective value for the calling query can apply while evaluating a view. For example, time_zone can apply inside a view.

At a view boundary:

  • unmapped_fields: Fields loaded by an outer query are null for rows from a view unless the view produces those fields itself. If an outer setting would have loaded fields that the view doesn't produce, the response warning identifies the view and the fields that are null.
  • For each supported wildcard setting, exact names work regardless of the setting. A wildcard matches the corresponding target type only when the view definition sets the setting to true. If a true value from outside the view stops at the boundary and the wildcard would otherwise have matched targets, the response returns a warning that identifies the view and the missed matches.

The effective query settings apply throughout the query, including inside view bodies.

Query parameters are not allowed in a stored view definition. A calling query can still use parameters in commands after FROM view_name, but those parameters are not substituted into the view body.

A view that starts with TS must also contain a STATS command after it. A view without STATS is rejected when you create or update it. This ensures the view returns plain tabular results. A PROMQL view must also use an aggregation function.

Query a time series view with FROM view_name. A view name cannot be used as a source in TS or PROMQL. An exact view name in one of those source commands is rejected, and a wildcard source expression skips matching views.

When you create or update a view, Elasticsearch validates its name, size, query syntax, and the settings that are allowed in a view definition.

For a time series view, Elasticsearch also validates that TS is followed by STATS.

Elasticsearch doesn't resolve or execute the query, and it doesn't check whether referenced indices, fields, datasets, or views exist, or whether nested views form a cycle. Creation doesn't return warnings for those problems. Elasticsearch reports them when you query the view.

Deleting a resource doesn't check whether a view depends on it. Deleting an index, data stream, alias, dataset, or nested view can therefore cause existing views to fail when queried.

For a detailed comparison of views, subqueries, and FORK, refer to Combine and reuse ES|QL queries.

  • ES|QL subqueries: nest queries inside other queries, in FROM or with IN / NOT IN.
  • FROM command: full reference for index expressions, where view names are used.
  • Query multiple indices: how index patterns, wildcards, and date math combine sources in a single FROM.