Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
79ab7fe
dax testing
m-kovalsky May 26, 2026
d91eb43
enhancements
m-kovalsky May 28, 2026
4d4e07c
model view
m-kovalsky May 28, 2026
27ff733
chart
m-kovalsky May 28, 2026
f8edf9b
add dax parser
m-kovalsky May 28, 2026
c1382f1
color coding dax
m-kovalsky May 29, 2026
16764e8
calc groups
m-kovalsky May 31, 2026
297ecbc
desc
m-kovalsky May 31, 2026
1a3e55d
roles
m-kovalsky May 31, 2026
54c27f0
generate, model picker
m-kovalsky May 31, 2026
e3affca
fix
m-kovalsky May 31, 2026
283ad8a
model picker
m-kovalsky May 31, 2026
fba0f66
query builder
m-kovalsky May 31, 2026
2561f0f
dax parser IN statement, IN, NOT IN for query builder
m-kovalsky May 31, 2026
afc8bda
no same objects in query builder pane
m-kovalsky May 31, 2026
5bd670e
helper text
m-kovalsky May 31, 2026
9847c53
colors
m-kovalsky May 31, 2026
c643497
change
m-kovalsky Jun 1, 2026
44684f4
ui components
m-kovalsky Jun 1, 2026
1656015
define measures; fix loading issue
m-kovalsky Jun 1, 2026
8367e92
dax formatter button
m-kovalsky Jun 1, 2026
72d25d4
undo, redo buttons
m-kovalsky Jun 1, 2026
4d46d78
size
m-kovalsky Jun 1, 2026
d3855ee
optimize
m-kovalsky Jun 2, 2026
763110b
1 trace
m-kovalsky Jun 2, 2026
80dc788
query history
m-kovalsky Jun 2, 2026
706f94c
query history, download button
m-kovalsky Jun 2, 2026
f251568
charts
m-kovalsky Jun 2, 2026
7fae6e2
first query
m-kovalsky Jun 2, 2026
0ad3da7
remove generate button
m-kovalsky Jun 2, 2026
b8c985c
cut, copy, paste, impersonation output
m-kovalsky Jun 2, 2026
a502f96
fix loading new model
m-kovalsky Jun 2, 2026
1688860
dax query plan
m-kovalsky Jun 4, 2026
fe1f2c7
dark mode parameter
m-kovalsky Jun 4, 2026
55fca85
dax query plan
m-kovalsky Jun 4, 2026
32ec8d5
query plan working
m-kovalsky Jun 4, 2026
84b85f0
query dep columns
m-kovalsky Jun 7, 2026
d88f404
fix dax query plan
m-kovalsky Jun 7, 2026
1fb8453
vertipaq analyzer
m-kovalsky Jun 7, 2026
2905c82
hide warning
m-kovalsky Jun 7, 2026
15df73a
analysis button
m-kovalsky Jun 7, 2026
2a38421
dax parse comments
m-kovalsky Jun 7, 2026
44a1ad9
nl 2 dax
m-kovalsky Jun 7, 2026
23227cc
dax comments color coded
m-kovalsky Jun 7, 2026
e69d1e3
pop out dax
m-kovalsky Jun 7, 2026
ddf0100
popout color icon
m-kovalsky Jun 7, 2026
ca93f64
consistent button size
m-kovalsky Jun 7, 2026
24a72e9
query plan one block
m-kovalsky Jun 7, 2026
0b3660d
properly filter trace results using RequestID
m-kovalsky Jun 7, 2026
b063084
black
m-kovalsky Jun 9, 2026
08a3f98
sizing
m-kovalsky Jun 9, 2026
ad074d4
added execution metrics
m-kovalsky Jun 9, 2026
d9ca7a1
preload models
m-kovalsky Jun 9, 2026
af61964
filter rule
m-kovalsky Jun 9, 2026
3f3d535
full screen baby
m-kovalsky Jun 9, 2026
0583818
download query result
m-kovalsky Jun 9, 2026
53fd0bf
capture report queries
m-kovalsky Jun 9, 2026
82faa10
return trace
m-kovalsky Jun 9, 2026
0bb27f8
application context
m-kovalsky Jun 10, 2026
86b8255
full screen mode for all apps
m-kovalsky Jun 10, 2026
f4bed18
delta analyzer picker
m-kovalsky Jun 10, 2026
4566efe
fix delta
m-kovalsky Jun 14, 2026
7d6385a
hammer
m-kovalsky Aug 4, 2026
9d31a6c
UI fixes
m-kovalsky Aug 4, 2026
308147f
ui upgrades
m-kovalsky Aug 4, 2026
a56c706
faster
m-kovalsky Aug 4, 2026
000e736
ui
m-kovalsky Aug 4, 2026
a967c13
cycle reports
m-kovalsky Aug 4, 2026
84adc28
move trace history, capture report queries
m-kovalsky Aug 5, 2026
b55cd23
callbackdataid
m-kovalsky Aug 5, 2026
ccc6dc2
clean
m-kovalsky Aug 5, 2026
441ef2d
workspace monitoring
m-kovalsky Aug 5, 2026
3b1912c
fix workspace monitoring, dax formatting
m-kovalsky Aug 5, 2026
1d75c8f
define measure, workspace monitoring output cleanup
m-kovalsky Aug 5, 2026
dcb6281
ui upgrades
m-kovalsky Aug 5, 2026
925a607
model view upgrades
m-kovalsky Aug 5, 2026
067add6
model view child object indentation
m-kovalsky Aug 5, 2026
329e661
dependencies
m-kovalsky Aug 5, 2026
2084c70
color dependencies
m-kovalsky Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions .claude/skills/dax-optimization/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
name: dax-optimization
description: 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).
---

# DAX Optimization

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

The runtime rule engine lives in
`src/sempy_labs/semantic_model/_dax_optimization.py` and loads its rules from
`src/sempy_labs/semantic_model/_dax_optimization_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 (`connect_semantic_model`) | 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: `CALLBACK_DATA_ID`, `USES_IFERROR`, `DIVISION_WITHOUT_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 (`MANY_SE_QUERIES`).
- **A single slow scan** (`≥ 50 ms`) points at a specific large/high-cardinality
table — inspect its xmSQL in the plan (`SLOW_SE_SCAN`).
- **Low parallelism** (`SE CPU / SE Duration < 1.2x` over a meaningful SE
duration) means scans are effectively single-threaded (`LOW_SE_PARALLELISM`).

### 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 (`HIGH_CARDINALITY_COLUMN`).
- **High-cardinality floating point** columns are especially expensive — convert
to fixed decimal/integer or round (`FLOAT_HIGH_CARDINALITY_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)` (`FILTER_FULL_TABLE`). 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")`
(`FILTER_COLUMN_USE_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 (`MANY_REFERENCED_COLUMNS`).

### 7. Diagnostics

If the trace or plan wasn't captured, the engine emits an informational finding
(`NO_TRACE_CAPTURED`, `NO_QUERY_PLAN_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:

```jsonc
{
"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`, `not_contains`,
`regex`, `in`, `not_in`. Unknown operators / type errors evaluate to `false`,
so a malformed rule can never crash the analysis.

### Available metrics (scalar context)

`has_query`, `query_length`, `iterator_count`, `nested_iterator`,
`uses_iferror`, `uses_divide_function`, `uses_division_operator`,
`filter_full_table_count`, `filter_column_predicate_count`, `cold_cache`,
`has_trace`, `total_duration_ms`,
`se_duration_ms`, `fe_duration_ms`, `cpu_time_ms`, `se_pct`, `fe_pct`,
`se_query_count`, `se_internal_count`, `se_cache_match_count`, `se_cpu_ms`,
`se_parallelism`, `has_query_plan`, `callback_dataid_count`,
`encode_callback_count`, `spool_count`, `referenced_column_count`,
`referenced_table_count`, `has_dependencies`, `vertipaq_available`,
`vertipaq_skipped_trivial`, `max_data_column_cardinality`,
`high_cardinality_data_column_count`. Display helpers: `se_pct_display`,
`fe_pct_display`, `se_parallelism_display`.

### Available collections (`for_each` context)

| Collection | Item fields |
|------------|-------------|
| `high_cardinality_columns` | `table`, `column`, `cardinality`, `cardinality_display`, `data_type`, `is_floating_point`, `data_size`, `encoding` |
| `slow_se_queries` | `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
`build_context()` in `_dax_optimization.py`.
3. Keep `severity` honest: reserve `high` for things that clearly dominate
runtime (e.g. `CallbackDataID`, `IFERROR`).
4. Provide an actionable `recommendation` and at least one authoritative
`reference` (SQLBI or Microsoft Learn).
5. Validate: `python -c "import json,sys; json.load(open('src/sempy_labs/semantic_model/_dax_optimization_rules.json'))"`.

---

## References

- SQLBI — Understanding DAX query plans: https://www.sqlbi.com/articles/understanding-dax-query-plans/
- SQLBI — Optimizing high-cardinality columns in VertiPaq: https://www.sqlbi.com/articles/optimizing-high-cardinality-columns-in-vertipaq/
- SQLBI — Error handling in DAX measures: https://www.sqlbi.com/articles/error-handling-in-dax-measures/
- Microsoft Learn — DIVIDE function: https://learn.microsoft.com/dax/divide-function-dax
- Microsoft Learn — Data reduction techniques for import modeling: https://learn.microsoft.com/power-bi/guidance/import-modeling-data-reduction
Loading