Copyright (c) 2026 MindMesh Academy. All rights reserved. This content is proprietary and may not be reproduced or distributed without permission.

4.3.4. Data Warehouse Query Optimization

💡 First Principle: Warehouse query performance depends on statistics accuracy, caching, and query structure. The query optimizer makes execution plan decisions based on statistics—outdated statistics lead to suboptimal plans, like navigating with a map from last year. Without proper optimization, queries that should take seconds take minutes as data grows.

Scenario: A reporting query joins three large tables and suddenly takes 10x longer after a bulk data load. The statistics haven't been updated since the load, so the optimizer underestimates row counts and chooses a nested loop join instead of a hash join.

Statistics Management

Statistics help the query optimizer estimate result sizes and choose efficient execution plans:

ActionCommandWhen to Use
Update all statisticsUPDATE STATISTICS dbo.TableNameAfter bulk loads
Create specific statsCREATE STATISTICS stat_name ON dbo.Table(Column)Frequently filtered columns
Auto-create statisticsEnabled by defaultGood for most scenarios
View statistics healthDBCC SHOW_STATISTICS('dbo.Table', 'stat_name')Diagnosing slow queries
When statistics go stale:
  • After loading >20% new data into a table
  • After significant DELETE operations
  • After schema changes (new columns)

Automatic vs. manual: At query time the engine auto-creates missing single-column statistics and refreshes stale ones. That work is synchronous, so the triggering query pays for it; proactive statistics refresh (on by default) tries to do those refreshes ahead of time after data changes, but only for auto-created statistics. None of this updates statistics continuously as rows are inserted, so run UPDATE STATISTICS right after large loads. Only single-column statistics can be created manually; multi-column statistics aren't supported.

Why stale statistics hurt so much here: The warehouse engine is distributed, so row-count estimates decide the join strategy and data movement. For example, it may broadcast a small dimension to every node, or it may shuffle (redistribute) both tables for a hash join. A wrong estimate on a 3,650-row date dimension can add a large shuffle that disappears once statistics are current.

Result Set Caching

Fabric warehouses automatically cache query results:

  • How it works: Identical queries return cached results without re-executing
  • Cache duration: Invalidated when any referenced table changes; unused cache is evicted automatically (after 24 hours without a hit)
  • Benefit: Repeated dashboard queries return instantly
  • Limitation: Only benefits identical queries—parameter changes miss the cache. Only pure SELECT queries qualify: not CTAS or DML, and not queries using GETDATE(), row-level security or dynamic data masking. The query must also read at least one table of 100,000+ rows.
  • Configuration: On by default; turn off with ALTER DATABASE <name> SET RESULT_SET_CACHING OFF, or per query with OPTION (USE HINT ('DISABLE_RESULT_SET_CACHE'))

Query Performance Best Practices

PracticeWhyExample
Filter early (WHERE clause)Reduce data scannedWHERE date > '2024-01-01' before joining
**Avoid SELECT ***Reads unnecessary columnsSpecify only needed columns
Use appropriate JOINsWrong join = wrong results or poor performanceINNER vs LEFT based on requirements
Minimize DISTINCTForces expensive sortingRedesign query or fix upstream duplicates
Materialize a CTE used several timesThe optimizer inlines a CTE, so every reference re-runs its queryLoad a CTE referenced 2+ times (e.g. a large aggregation) into a #temp table once, then join to it
Use CTEs for readabilityOptimizer flattens them anywayNo performance cost, easier to debug

No index or partition tuning in the warehouse: Fabric Warehouse tables have no user-defined indexes to add, drop or rebuild. Partitioned tables and computed columns aren't supported either. Your levers are statistics, query shape, caching and, for large tables filtered on mid-to-high-cardinality columns (IDs, dates), data clustering (preview), set with WITH (CLUSTER BY (col)) when the table is created. Clustering helps little on a low-cardinality column (a handful of values, such as a tier or region). If a workload needs partition pruning on such a column, store the table as a lakehouse Delta table partitioned on it and query it through the SQL analytics endpoint.

Workload isolation (custom SQL pools, preview): By default the warehouse isolates SELECT work from ingestion (non-SELECT) work. To stop ad-hoc queries delaying critical reports, a workspace admin creates custom SQL pools (up to 8 per workspace). The admin classifies requests into them by the connection's application name and caps each pool's share of resources. Query timeouts only cancel work, and statistics don't prioritize it.

Materialized Views (Preview)

  • Status: Not supported in Fabric Data Warehouse. Materialized views are on the unsupported T-SQL list; they are a Synapse dedicated SQL pool feature.
  • Warehouse alternative: A pre-aggregated summary table rebuilt on a schedule with CTAS or INSERT...SELECT, plus result set caching for repeated identical queries
  • Where materialized views do exist in Fabric: KQL materialized views in an Eventhouse (see 4.3.5) and materialized lake views in a lakehouse

⚠️ Exam Trap: Result set caching is on by default—you don't need to enable it or add a query hint. When a question describes a slow first run and fast identical re-runs, or asks what makes repeated identical queries fast, the answer is result set caching. When a query is slow on every run, look at statistics and query shape instead.

⚠️ Common Pitfall: Running UPDATE STATISTICS on every table daily regardless of data changes. Only update statistics on tables that received significant data modifications.

See how it connects
Alvin Varughese
Written byAlvin Varughese
Founder•20 professional certifications