Materialized Views
Materialized views provide precomputed, automatically refreshed query results stored as Elasticsearch indices. They are ideal for:
- Denormalizing joins — flatten data from multiple indices into a single queryable index
- Precomputing aggregations — store GROUP BY results for fast dashboard queries
- Enriching data — combine lookup data with transactional data
- Computed columns — add scripted fields to the materialized result
Unlike regular views, materialized views persist their results and refresh automatically at a configurable interval.
Architecture
Under the hood, a materialized view translates into a pipeline of Elasticsearch primitives:
| SQL Concept | Elasticsearch Primitive |
|---|---|
| Source tables | Source indices |
| JOIN | Enrich policies + ingest pipelines |
| Computed columns | Script processors in ingest pipelines |
| Continuous refresh | Transforms (latest mode) with configurable frequency |
| Aggregations (GROUP BY) | Transforms (pivot mode) |
| Auto-refresh watcher | Watcher (re-executes enrich policies on source data changes) |
Deployment Sequence
When a materialized view is created, the engine deploys artifacts in this order:
- Alter source schemas — add changelog tracking fields (
_updated_at) - Create intermediate indices — changelog, enriched, and final view index
- Preload changelogs — copy existing data into changelog indices
- Create enrich policies — define lookup enrichment from source indices
- Create watcher — schedule automatic re-execution of enrich policies
- Execute enrich policies — build initial enrich indices
- Create ingest pipelines — enrichment + computed field processors
- Create transforms — changelog, enrichment, computed fields, aggregation
- Start transforms — sequentially, with checkpoint waits between groups
- Save metadata — persist MV definition for SHOW/DESCRIBE/DROP
Rollback is automatic on deployment failure.
CREATE MATERIALIZED VIEW
CREATE [OR REPLACE] MATERIALIZED VIEW [IF NOT EXISTS] view_name[REFRESH EVERY interval time_unit][WITH (option = value [, ...])]AS select_statement| Component | Required | Description |
|---|---|---|
view_name | Yes | Unique name for the materialized view |
OR REPLACE | No | Replace existing view (drops and recreates) |
IF NOT EXISTS | No | Skip creation if view already exists |
REFRESH EVERY | No | Automatic refresh interval |
WITH (...) | No | Additional options (delay, user_latency) |
AS select | Yes | The SELECT query defining the view |
Refresh Interval
REFRESH EVERY 10 SECONDSREFRESH EVERY 5 MINUTESREFRESH EVERY 1 HOUROptions
| Option | Type | Description |
|---|---|---|
delay | Interval | Delay before processing new data (allows late arrivals) |
user_latency | Interval | Maximum acceptable query latency for users |
Simple View (no JOIN)
CREATE MATERIALIZED VIEW active_orders_mvREFRESH EVERY 30 SECONDSASSELECT id, amount, status, created_atFROM ordersWHERE status = 'active';View with JOIN
CREATE OR REPLACE MATERIALIZED VIEW orders_with_customers_mvREFRESH EVERY 8 SECONDSWITH (delay = '2s', user_latency = '1s')ASSELECT o.id, o.amount, c.name AS customer_name, c.email, c.department.zip_code AS customer_zip, UPPER(c.name) AS customer_name_upper, COALESCE( NULLIF(o.createdAt, DATE_PARSE('2025-09-11', '%Y-%m-%d') - INTERVAL 2 DAY), CURRENT_DATE ) AS effective_dateFROM orders AS oJOIN customers AS c ON o.customer_id = c.idWHERE o.status = 'completed';This creates changelog transforms, enrich policies, ingest pipelines with enrichment and script processors, and the final materialized view index.
View with Aggregations
CREATE OR REPLACE MATERIALIZED VIEW orders_by_city_mvASSELECT c.city, c.country, COUNT(*) AS order_count, SUM(o.amount) AS total_amount, AVG(o.amount) AS avg_amount, MAX(o.amount) AS max_amountFROM orders oJOIN customers c ON o.customer_id = c.idWHERE o.status = 'completed'GROUP BY c.city, c.countryHAVING SUM(o.amount) > 10000ORDER BY total_amount DESCLIMIT 100;DROP MATERIALIZED VIEW
DROP MATERIALIZED VIEW [IF EXISTS] view_name;Drops the view and all associated artifacts: transforms, intermediate indices, ingest pipelines, and enrich policies.
REFRESH MATERIALIZED VIEW
REFRESH MATERIALIZED VIEW [IF EXISTS] view_name [WITH SCHEDULE NOW];Forces an immediate refresh by refreshing changelog indices and re-executing enrich policies.
Inspect Commands
-- View the schemaDESCRIBE MATERIALIZED VIEW orders_with_customers_mv;
-- View metadataSHOW MATERIALIZED VIEW orders_with_customers_mv;
-- View the normalized SQLSHOW CREATE MATERIALIZED VIEW orders_with_customers_mv;
-- Check transform statusSHOW MATERIALIZED VIEW STATUS orders_with_customers_mv;
-- List all materialized viewsSHOW MATERIALIZED VIEWS;Complete Example
1. Create source tables
CREATE TABLE IF NOT EXISTS orders ( id INT NOT NULL, customer_id INT NOT NULL, amount DOUBLE, status KEYWORD DEFAULT 'pending', items ARRAY<STRUCT> FIELDS ( product_id INT, quantity INT, price DOUBLE ), createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS customers ( id INT NOT NULL, name VARCHAR, email KEYWORD, department STRUCT FIELDS ( name VARCHAR, zip_code KEYWORD ), PRIMARY KEY (id));2. Load data
COPY INTO orders FROM '/data/orders.json' WITH (format = 'json');COPY INTO customers FROM '/data/customers.json' WITH (format = 'json');3. Create the materialized view
CREATE OR REPLACE MATERIALIZED VIEW orders_with_customers_mvREFRESH EVERY 8 SECONDSWITH (delay = '2s', user_latency = '1s')ASSELECT o.id, o.amount, c.name AS customer_name, c.email, c.department.zip_code AS customer_zip, UPPER(c.name) AS customer_name_upperFROM orders AS oJOIN customers AS c ON o.customer_id = c.idWHERE o.status = 'completed';4. Query the materialized view
SELECT * FROM orders_with_customers_mvWHERE customer_name = 'Alice'ORDER BY amount DESCLIMIT 10;5. Force a refresh
REFRESH MATERIALIZED VIEW orders_with_customers_mv WITH SCHEDULE NOW;6. Drop the view
DROP MATERIALIZED VIEW IF EXISTS orders_with_customers_mv;Version Compatibility
| Feature | ES6 | ES7 | ES8 | ES9 |
|---|---|---|---|---|
| Materialized Views | No | Yes* | Yes | Yes |
WITH SCHEDULE NOW | No | No | Yes | Yes |
* Requires Elasticsearch 7.5+ (transforms and enrich policies)
Limitations
| Limitation | Details |
|---|---|
| UNNEST JOIN | Not supported in materialized views |
| Quota limits | Community edition: max 3 views. Pro: limited. Enterprise: unlimited |
| Watcher dependency | Automatic enrich policy re-execution relies on Elasticsearch Watchers (requires Platinum/Enterprise license) |
| Eventual consistency | Data is eventually consistent based on refresh frequency and delay |
| Join cardinality | JOINs use enrich policies which match on a single field |
Watcher Dependency and Elasticsearch Licensing
Materialized views with JOINs rely on enrich policies to denormalize data. When lookup table data changes, the corresponding enrich policy must be re-executed. The engine creates an Elasticsearch Watcher to automate this, but Watchers require an Elasticsearch Platinum or Enterprise license.
Workaround for clusters without Watcher support:
Use an external scheduled job (cron, Kubernetes CronJob, Airflow) to periodically re-execute enrich policies:
EXECUTE ENRICH POLICY orders_with_customers_mv_customers_enrich_policy;-- Or trigger a full refreshREFRESH MATERIALIZED VIEW orders_with_customers_mv;