microsoft/semantic-link-labs · Archived

dax-optimization

Methodology and an executable rule catalog for diagnosing and optimizing DAX query performance.

First seen Aug 19, 2026

Installation

$ npx skills add microsoft/semantic-link-labs --skill dax-optimization

Summary

  • Methodology and an executable rule catalog for diagnosing and optimizing DAX query performance.
  • Use this when analyzing a slow DAX query, interpreting trace timings / DAX query plans, reducing column cardinality, or extending the performance-analysis rules used by the interactive DAX test widget (sempy_labs.semantic_model.test).

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from microsoft/semantic-link-labs · top by installs.

npx skills add microsoft/semantic-link-labs

Browse all from microsoft/semantic-link-labs

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 573
License LICENSE
Default branch main
Open issues 45
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,310 B
  • docs SUMMARY.md 354 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

DAX Optimization

This skill explains how to determine optimization techniques for a DAX query and its components, and documents the executable rule catalog (daxoptimizationrules.json) that powers the Performance analysis tab of the interactive DAX test widget.

The runtime rule engine lives in src/sempylabs/semanticmodel/daxoptimization.py and loads its rules from src/sempylabs/semanticmodel/daxoptimization_rules.json (the canonical copy that is packaged and executed). The JSON in this skill folder is the same schema and is the human-facing reference; keep the two in sync when adding rules.

When to Use This Skill

  • Diagnosing why a DAX query is slow.
  • Interpreting Formula Engine (FE) vs Storage Engine (SE) timings from a trace.
  • Reading a DAX query plan (logical/physical) for CallbackDataID, spools, scans.
  • Deciding whether high column cardinality is the bottleneck.
  • Adding, editing, or reviewing the performance-analysis rules.

The Inputs

The analysis is computed from up to six artifacts. Each rule declares which artifacts it requires; a rule is skipped if any required artifact is missing.

Input Source What it provides
DAX query The editor text plus the DAX expressions of every measure the query transitively depends on Syntax-level patterns (IFERROR, FILTER over a full table, nested iterators, raw / division). The query usually references a measure only by name, so the syntax rules also scan the DAX of dependent measures (resolved from model metadata) to catch issues that live inside those measures.
Model metadata TOM (connectsemanticmodel) Tables, columns, measures, relationships, data types.
Query dependencies INFO.CALCDEPENDENCY The exact tables/columns the query references.
Trace details Server-side trace QueryEnd, VertiPaqSEQueryEnd, cache matches → total/FE/SE duration, CPU, SE query count, parallelism.
DAX query plan Trace DAXQueryPlan events Logical/physical plan text → CallbackDataID, Spool, scan operators.
Vertipaq Analyzer vertipaq_analyzer(...) Column cardinality, size, encoding, data types. Only used when the cardinalities of the Data columns are not all 1 — otherwise there is nothing meaningful to analyze and Vertipaq rules are skipped.

Optimization Methodology

Work top-down, from the cheapest signal to the most detailed.

1. Establish the engine balance (FE vs SE)

The Storage Engine (VertiPaq) is multi-threaded and fast; the Formula Engine is single-threaded. From the trace:

  • Total Duration = QueryEnd.Duration
  • SE Duration = sum of VertiPaqSEQueryEnd.Duration (excluding Internal subqueries)
  • FE Duration = Total − SE

Then:

  • SE-bound (SE% ≥ 70%): the query spends its time scanning data → attack

data volume and cardinality (rule SE_BOUND).

  • FE-bound (FE% ≥ 70%): the query spends its time in single-threaded logic

→ push work to the SE, remove callbacks, reduce materialization (rule FE_BOUND).

2. Look for CallbackDataID (the #1 red flag)

CallbackDataID in the physical plan means the SE had to call back into the FE mid-scan. It disables VertiPaq optimizations and is usually caused by:

  • IF / IFERROR / ISERROR / error handling inside an iterator,
  • division by / (wrap in DIVIDE),
  • rounding / date arithmetic / conditional logic evaluated row-by-row.

Rules: CALLBACKDATAID, USESIFERROR, DIVISIONWITHOUT_DIVIDE.

3. Count and size the Storage Engine queries

  • Many SE queries (≥ 10) usually means fusion failed — simplify filter

context and use variables to compute base values once (MANYSEQUERIES).

  • A single slow scan (≥ 50 ms) points at a specific large/high-cardinality

table — inspect its xmSQL in the plan (SLOWSESCAN).

  • Low parallelism (SE CPU / SE Duration < 1.2x over a meaningful SE

duration) means scans are effectively single-threaded (LOWSEPARALLELISM).

4. Inspect the query plan for materialization

Large/many spools materialize intermediate results in the FE and cost memory and time (LARGE_SPOOL). Reduce them with variables and earlier filtering.

5. Reduce cardinality (Vertipaq)

Cardinality drives dictionary size, scan cost, and DISTINCTCOUNT/join cost. Focus on columns the query actually references:

  • High-cardinality columns (≥ 1,000,000 unique values): split datetime

into date+time, bucket/round numerics, drop unused keys (HIGHCARDINALITYCOLUMN).

  • High-cardinality floating point columns are especially expensive — convert

to fixed decimal/integer or round (FLOATHIGHCARDINALITY_COLUMN).

6. Simplify the DAX itself

  • Nested iterators multiply row evaluations (NESTED_ITERATORS).
  • Many iterators (≥ 5) raise the chance of row-by-row work (MANY_ITERATORS).
  • FILTER over a whole table to evaluate a measure (e.g. `FILTER(Sales,

[Total Qty] > 100)) tests the measure on every row of the table — iterate the smallest grouping instead, e.g. FILTER(VALUES(Sales[OrderId]), [Total Qty] > 100) (FILTERFULLTABLE`). This rule fires only when the FILTER predicate references a measure.

  • FILTER wrapping a column predicate (e.g. `FILTER(Customer,

Customer[Category] = "A")) materializes the whole table for a condition over its columns — rewrite as KEEPFILTERS(Customer[Category] = "A") (FILTERCOLUMNUSE_KEEPFILTERS`). This rule fires when the FILTER predicate references columns (and no measure). Measure vs. column is resolved from model metadata when available, otherwise inferred from whether the bracket reference is table-qualified.

  • Many referenced columns (≥ 15) widen datacaches — project only what's

needed (MANYREFERENCEDCOLUMNS).

7. Diagnostics

If the trace or plan wasn't captured, the engine emits an informational finding (NOTRACECAPTURED, NOQUERYPLAN_CAPTURED) telling the user to run the query first so the full analysis can be produced.


The Rules JSON Schema

Each entry in rules is one rule:

{
  "id": "CALLBACK_DATA_ID",          // stable identifier
  "title": "…",                      // short headline
  "category": "Query plan",          // grouping label
  "severity": "high|medium|low|info",
  "requires": ["query_plan"],        // artifacts that must be present
  "kind": "scalar|for_each",         // evaluation mode
  "condition": { … },                // scalar rules: evaluated against metrics
  "collection": "high_cardinality_columns", // for_each rules: list to iterate
  "where": { … },                    // for_each rules: per-item filter
  "max_findings": 8,                 // for_each rules: cap on emitted findings
  "message": "… {placeholder} …",    // templated; {tokens} filled from context
  "recommendation": "…",
  "references": ["https://…"]
}

Conditions

A condition is a tree of:

  • Leaf — {"metric": "se_pct", "op": ">=", "value": 0.7} for scalar rules,

or {"field": "cardinality", "op": ">=", "value": 1000000} inside a for_each where.

  • Composite — {"all": [ … ]}, {"any": [ … ]}, {"not": { … }}.

Operators: >, >=, <, <=, ==, !=, contains, notcontains, regex, in, notin. Unknown operators / type errors evaluate to false, so a malformed rule can never crash the analysis.

Available metrics (scalar context)

hasquery, querylength, iteratorcount, nestediterator, usesiferror, usesdividefunction, usesdivisionoperator, filterfulltablecount, filtercolumnpredicatecount, coldcache, hastrace, totaldurationms, sedurationms, fedurationms, cputimems, sepct, fepct, sequerycount, seinternalcount, secachematchcount, secpums, separallelism, hasqueryplan, callbackdataidcount, encodecallbackcount, spoolcount, referencedcolumncount, referencedtablecount, hasdependencies, vertipaqavailable, vertipaqskippedtrivial, maxdatacolumncardinality, highcardinalitydatacolumncount. Display helpers: sepctdisplay, fepctdisplay, separallelism_display.

Available collections (for_each context)

Collection Item fields
highcardinalitycolumns table, column, cardinality, cardinalitydisplay, datatype, isfloatingpoint, data_size, encoding
slowsequeries subclass, duration, cpu

message placeholders for for_each rules can reference any item field as well as any scalar metric.


Adding or Editing a Rule

  1. Add the rule object to both JSON copies (package + this skill folder).
  2. If the rule needs a new metric or collection, add it in

buildcontext() in dax_optimization.py.

  1. Keep severity honest: reserve high for things that clearly dominate

runtime (e.g. CallbackDataID, IFERROR).

  1. Provide an actionable recommendation and at least one authoritative

reference (SQLBI or Microsoft Learn).

  1. Validate: python -c "import json,sys; json.load(open('src/sempylabs/semanticmodel/daxoptimization_rules.json'))".

References