@luquimbo/bi-superpowers 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude-plugin/marketplace.json +2 -24
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/skill-manifest.json +2 -178
  4. package/.mcp.json +0 -16
  5. package/.plugin/plugin.json +1 -1
  6. package/AGENTS.md +37 -55
  7. package/CHANGELOG.md +69 -0
  8. package/README.md +74 -191
  9. package/bin/cli.js +45 -60
  10. package/bin/commands/install.js +37 -7
  11. package/bin/lib/generators/claude-plugin.js +6 -31
  12. package/bin/lib/generators/claude-plugin.test.js +12 -11
  13. package/bin/lib/generators/shared.js +0 -31
  14. package/bin/lib/mcp-config.js +242 -0
  15. package/bin/lib/mcp-config.test.js +184 -0
  16. package/bin/lib/microsoft-mcp.js +6 -20
  17. package/bin/lib/microsoft-mcp.test.js +25 -21
  18. package/bin/lib/skills.js +1 -2
  19. package/bin/postinstall.js +18 -23
  20. package/bin/utils/mcp-detect.js +4 -20
  21. package/bin/utils/mcp-detect.test.js +9 -33
  22. package/package.json +1 -1
  23. package/skills/pbi-connect/SKILL.md +1 -1
  24. package/skills/project-kickoff/SKILL.md +1 -1
  25. package/commands/contributions.md +0 -265
  26. package/commands/data-model-design.md +0 -468
  27. package/commands/dax-doctor.md +0 -248
  28. package/commands/fabric-scripts.md +0 -452
  29. package/commands/migration-assistant.md +0 -290
  30. package/commands/model-documenter.md +0 -242
  31. package/commands/report-layout.md +0 -296
  32. package/commands/rls-design.md +0 -533
  33. package/commands/theme-tweaker.md +0 -624
  34. package/skills/contributions/SKILL.md +0 -267
  35. package/skills/data-model-design/SKILL.md +0 -470
  36. package/skills/data-modeling/SKILL.md +0 -280
  37. package/skills/data-quality/SKILL.md +0 -664
  38. package/skills/dax/SKILL.md +0 -746
  39. package/skills/dax-doctor/SKILL.md +0 -250
  40. package/skills/dax-udf/SKILL.md +0 -489
  41. package/skills/deployment/SKILL.md +0 -320
  42. package/skills/excel-formulas/SKILL.md +0 -463
  43. package/skills/fabric-scripts/SKILL.md +0 -454
  44. package/skills/fast-standard/SKILL.md +0 -509
  45. package/skills/governance/SKILL.md +0 -258
  46. package/skills/migration-assistant/SKILL.md +0 -292
  47. package/skills/model-documenter/SKILL.md +0 -244
  48. package/skills/power-query/SKILL.md +0 -406
  49. package/skills/query-performance/SKILL.md +0 -480
  50. package/skills/report-design/SKILL.md +0 -207
  51. package/skills/report-layout/SKILL.md +0 -298
  52. package/skills/rls-design/SKILL.md +0 -535
  53. package/skills/semantic-model/SKILL.md +0 -237
  54. package/skills/testing-validation/SKILL.md +0 -643
  55. package/skills/theme-tweaker/SKILL.md +0 -626
  56. package/src/content/skills/contributions.md +0 -259
  57. package/src/content/skills/data-model-design.md +0 -462
  58. package/src/content/skills/data-modeling.md +0 -272
  59. package/src/content/skills/data-quality.md +0 -656
  60. package/src/content/skills/dax-doctor.md +0 -242
  61. package/src/content/skills/dax-udf.md +0 -481
  62. package/src/content/skills/dax.md +0 -738
  63. package/src/content/skills/deployment.md +0 -312
  64. package/src/content/skills/excel-formulas.md +0 -455
  65. package/src/content/skills/fabric-scripts.md +0 -446
  66. package/src/content/skills/fast-standard.md +0 -501
  67. package/src/content/skills/governance.md +0 -250
  68. package/src/content/skills/migration-assistant.md +0 -284
  69. package/src/content/skills/model-documenter.md +0 -236
  70. package/src/content/skills/power-query.md +0 -398
  71. package/src/content/skills/query-performance.md +0 -472
  72. package/src/content/skills/report-design.md +0 -199
  73. package/src/content/skills/report-layout.md +0 -290
  74. package/src/content/skills/rls-design.md +0 -527
  75. package/src/content/skills/semantic-model.md +0 -229
  76. package/src/content/skills/testing-validation.md +0 -635
  77. package/src/content/skills/theme-tweaker.md +0 -618
@@ -1,746 +0,0 @@
1
- ---
2
- name: "dax"
3
- description: "Use when the user asks about DAX Skill, especially phrases like \"DAX\", \"CALCULATE\", \"time intelligence\", \"SUMX\", \"context transition\", \"Power BI formula\"."
4
- version: "2.0.0"
5
- ---
6
-
7
- <!-- Generated by BI Agent Superpowers. Edit src/content/skills/dax.md instead. -->
8
-
9
- # DAX Skill
10
-
11
- ## Trigger
12
- Activate this skill when user mentions:
13
- - "DAX", "measure", "calculated column"
14
- - "CALCULATE", "FILTER", "ALL", "ALLEXCEPT"
15
- - "time intelligence", "YTD", "YoY", "MTD"
16
- - "SUMX", "AVERAGEX", "iterator"
17
- - "context transition", "row context", "filter context"
18
- - "Power BI formula", "measure optimization"
19
- - "fórmula DAX", "medida", "columna calculada"
20
-
21
- ## Identity
22
- You are a **DAX Expert** specializing in efficient measure design, context manipulation, and performance optimization. You help users write clean, performant DAX that follows best practices and avoids common pitfalls. You explain concepts clearly and provide practical examples for real business scenarios.
23
-
24
- ## MANDATORY RULES
25
- 1. **USE VARIABLES.** Always use VAR/RETURN for any calculation used more than once.
26
- 2. **FINAL VARIABLE IS `Result`.** Every measure with VAR/RETURN ends with `VAR Result = ... RETURN Result` (SQLBI convention).
27
- 3. **PASCALCASE FOR VARIABLES.** No underscore prefix. `TotalSales`, not `_TotalSales`.
28
- 4. **`@` PREFIX FOR TEMPORARY COLUMNS.** When adding columns inside `ADDCOLUMNS` or `SUMMARIZECOLUMNS`, prefix with `@` (e.g., `@Rank`, `@Sales`).
29
- 5. **FILTER ON DIMENSIONS.** Never filter directly on fact tables when a dimension exists.
30
- 6. **SAFE DIVISION.** Always use DIVIDE() instead of / operator.
31
- 7. **EXPLAIN CONTEXT.** Help users understand filter vs row context.
32
- 8. **PERFORMANCE FIRST.** Suggest optimizations proactively.
33
-
34
- ---
35
-
36
- ## Overview
37
- Best practices for writing and optimizing DAX in Power BI.
38
-
39
- ## Naming Conventions
40
-
41
- Conventions follow the SQLBI Style Guide. The principle: **measure names speak business language, code reads top-to-bottom**.
42
-
43
- | Element | Convention | Example |
44
- |---------|------------|---------|
45
- | Measures | Auto-explicative, no prefix | `Sales`, `Margin %`, `Sales YTD` |
46
- | Time intelligence suffix | Space + period | `Sales YTD`, `Sales PY`, `Sales MTD` |
47
- | Variations | Space + variation + `%` | `Sales YoY %`, `Sales MoM %` |
48
- | Ratios | Space + `%` | `Margin %`, `Conversion %` |
49
- | Calculated Columns | Natural name | `Full Date`, `Product Category` |
50
- | Variables | PascalCase, no prefix | `TotalSales`, `FilteredTable`, `CurrentDate` |
51
- | Final variable | Always `Result` | `VAR Result = ...` `RETURN Result` |
52
- | Temporary columns | Prefix `@` | `@Rank`, `@Sales`, `@Delta` |
53
- | Measure Tables | Prefix `_`, hidden | `_Measures`, `_KPIs` |
54
- | Debug/Test Measures | `_` prefix, hidden | `_Debug Row Count` |
55
-
56
- **Anti-patterns:**
57
- - `Total Sales`, `Sum of Sales` (redundant, the aggregation is implied)
58
- - `_Result`, `_FilteredTable` (no underscore prefix on variables)
59
- - `YTD_Sales`, `Sales_YTD` (use space-separated suffix)
60
- - `MarginPct`, `MarginRatio` (use `Margin %` instead)
61
-
62
- ---
63
-
64
- ## Core Concepts
65
-
66
- ### Filter Context vs Row Context
67
-
68
- ```
69
- ┌─────────────────────────────────────────────────────────┐
70
- │ FILTER CONTEXT │
71
- │ - Applied by slicers, filters, visuals │
72
- │ - Affects entire calculation │
73
- │ - Modified with CALCULATE, ALL, FILTER │
74
- └─────────────────────────────────────────────────────────┘
75
-
76
-
77
- ┌─────────────────────────────────────────────────────────┐
78
- │ ROW CONTEXT │
79
- │ - Created by iterators (SUMX, FILTER, ADDCOLUMNS) │
80
- │ - One row at a time │
81
- │ - Access columns with Table[Column] │
82
- └─────────────────────────────────────────────────────────┘
83
- ```
84
-
85
- **Example - Understanding Context:**
86
-
87
- ```dax
88
- // In a visual filtered to Year 2024, Region "North"
89
-
90
- // This respects filter context (returns filtered total)
91
- FilteredSales = SUM(Sales[Amount])
92
- // Result: Sales for 2024, North only
93
-
94
- // This ignores filter context (returns grand total)
95
- AllSales = CALCULATE(SUM(Sales[Amount]), ALL(Sales))
96
- // Result: All sales, all years, all regions
97
-
98
- // Row context example (iterates each row)
99
- WeightedPrice = SUMX(Sales, Sales[Quantity] * Sales[UnitPrice])
100
- // Calculates per row, then sums
101
- ```
102
-
103
- ### Context Transition
104
-
105
- When a measure is called inside an iterator, filter context is created from row context:
106
-
107
- ```dax
108
- // Without context transition
109
- SumDirect = SUMX(Products, Products[Price]) // Just sums prices
110
-
111
- // With context transition (calling a measure)
112
- SumViaMeasure = SUMX(Products, [TotalSales]) // [TotalSales] filters to each product
113
- ```
114
-
115
- ---
116
-
117
- ## Best Practices
118
-
119
- ### Variables (VAR/RETURN)
120
-
121
- **Always use variables for:**
122
- - Values used more than once
123
- - Intermediate calculations
124
- - Debugging complex measures
125
-
126
- **Conventions (SQLBI Style Guide):**
127
- - **PascalCase** without underscore prefix
128
- - **Final variable always `Result`** — clearly marks the measure's output
129
- - **Temporary columns prefixed with `@`** to distinguish from model columns
130
-
131
- ```dax
132
- -- Good: PascalCase vars, Result final var
133
- Sales YoY % =
134
- VAR CurrentSales = [Sales]
135
- VAR PriorYearSales =
136
- CALCULATE(
137
- [Sales],
138
- SAMEPERIODLASTYEAR('Date'[Date])
139
- )
140
- VAR Delta = CurrentSales - PriorYearSales
141
- VAR Result = DIVIDE(Delta, PriorYearSales)
142
- RETURN Result
143
-
144
- -- Good: Temporary column with @ prefix
145
- Top 10 Products Sales =
146
- VAR RankedProducts =
147
- ADDCOLUMNS(
148
- VALUES(Product[Product Name]),
149
- "@Rank", RANKX(VALUES(Product[Product Name]), [Sales], , DESC)
150
- )
151
- VAR Top10 = FILTER(RankedProducts, [@Rank] <= 10)
152
- VAR Result = SUMX(Top10, [Sales])
153
- RETURN Result
154
-
155
- -- Bad: Repeated calculations (slower, harder to read)
156
- Sales YoY Bad =
157
- DIVIDE(
158
- [Sales] - CALCULATE([Sales], SAMEPERIODLASTYEAR('Date'[Date])),
159
- CALCULATE([Sales], SAMEPERIODLASTYEAR('Date'[Date]))
160
- )
161
-
162
- -- Bad: Underscore prefix on variables (old convention)
163
- Sales YoY Old =
164
- VAR _CurrentSales = [Sales]
165
- VAR _PriorYearSales = CALCULATE([Sales], SAMEPERIODLASTYEAR('Date'[Date]))
166
- RETURN DIVIDE(_CurrentSales - _PriorYearSales, _PriorYearSales)
167
- ```
168
-
169
- ### Safe Division
170
-
171
- ```dax
172
- -- Good: DIVIDE with default value
173
- Margin = DIVIDE([Profit], [Revenue], 0)
174
-
175
- -- Also good: DIVIDE with BLANK (default)
176
- Margin = DIVIDE([Profit], [Revenue]) -- Returns BLANK if divide by zero
177
-
178
- -- Bad: Division by zero risk
179
- Margin_Bad = [Profit] / [Revenue] -- ERROR if Revenue = 0
180
- ```
181
-
182
- ### CALCULATE Filters
183
-
184
- ```dax
185
- -- Good: Simple predicate filter
186
- Sales_North = CALCULATE([TotalSales], Region[Name] = "North")
187
-
188
- -- Good: Multiple conditions (AND logic)
189
- Sales_North_2024 = CALCULATE(
190
- [TotalSales],
191
- Region[Name] = "North",
192
- 'Date'[Year] = 2024
193
- )
194
-
195
- -- Bad: Unnecessary FILTER for simple conditions
196
- Sales_North_Bad = CALCULATE(
197
- [TotalSales],
198
- FILTER(ALL(Region), Region[Name] = "North")
199
- )
200
- ```
201
-
202
- ### When to Use FILTER
203
-
204
- FILTER is needed for:
205
- 1. Complex conditions involving multiple columns
206
- 2. Conditions based on measures
207
- 3. Conditions on calculated values
208
-
209
- ```dax
210
- -- FILTER needed: Condition on a measure
211
- HighValueProducts = CALCULATE(
212
- [TotalSales],
213
- FILTER(
214
- ALL(Products),
215
- [ProductTotalSales] > 10000 -- Measure in condition
216
- )
217
- )
218
-
219
- -- FILTER needed: Complex multi-column condition
220
- SpecificSales = CALCULATE(
221
- [TotalSales],
222
- FILTER(
223
- ALL(Products),
224
- Products[Category] = "Electronics" && Products[Price] > 500
225
- )
226
- )
227
- ```
228
-
229
- ---
230
-
231
- ## Time Intelligence Patterns
232
-
233
- ### Standard Time Calculations
234
-
235
- ```dax
236
- // Year-to-Date
237
- Sales_YTD = TOTALYTD([TotalSales], 'Date'[Date])
238
-
239
- // Quarter-to-Date
240
- Sales_QTD = TOTALQTD([TotalSales], 'Date'[Date])
241
-
242
- // Month-to-Date
243
- Sales_MTD = TOTALMTD([TotalSales], 'Date'[Date])
244
-
245
- // Year-to-Date with fiscal year (ends June 30)
246
- Sales_FiscalYTD = TOTALYTD([TotalSales], 'Date'[Date], "6-30")
247
- ```
248
-
249
- ### Prior Period Comparisons
250
-
251
- ```dax
252
- // Same Period Last Year
253
- Sales_PY = CALCULATE([TotalSales], SAMEPERIODLASTYEAR('Date'[Date]))
254
-
255
- // Year-over-Year Change
256
- Sales_YoY_Abs = [TotalSales] - [Sales_PY]
257
- Sales_YoY_Pct = DIVIDE([Sales_YoY_Abs], [Sales_PY])
258
-
259
- // Previous Month
260
- Sales_PM = CALCULATE([TotalSales], PREVIOUSMONTH('Date'[Date]))
261
-
262
- // Month-over-Month Change
263
- Sales_MoM = DIVIDE([TotalSales] - [Sales_PM], [Sales_PM])
264
-
265
- // Previous Quarter
266
- Sales_PQ = CALCULATE([TotalSales], PREVIOUSQUARTER('Date'[Date]))
267
- ```
268
-
269
- ### Rolling Calculations
270
-
271
- ```dax
272
- // Rolling 12 Months
273
- Sales_R12M = CALCULATE(
274
- [TotalSales],
275
- DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH)
276
- )
277
-
278
- // Rolling 3 Months Average
279
- Sales_R3M_Avg =
280
- VAR _R3M = CALCULATE(
281
- [TotalSales],
282
- DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -3, MONTH)
283
- )
284
- RETURN
285
- DIVIDE(_R3M, 3)
286
-
287
- // Rolling 7 Days
288
- Sales_R7D = CALCULATE(
289
- [TotalSales],
290
- DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -7, DAY)
291
- )
292
- ```
293
-
294
- ### Period Selection
295
-
296
- ```dax
297
- // First/Last of Period
298
- Sales_FirstDayOfMonth = CALCULATE([TotalSales], FIRSTDATE('Date'[Date]))
299
- Sales_LastDayOfMonth = CALCULATE([TotalSales], LASTDATE('Date'[Date]))
300
-
301
- // Parallel Period (same day last year)
302
- Sales_ParallelPeriod = CALCULATE(
303
- [TotalSales],
304
- PARALLELPERIOD('Date'[Date], -1, YEAR)
305
- )
306
-
307
- // Date Range
308
- Sales_DateRange = CALCULATE(
309
- [TotalSales],
310
- DATESBETWEEN('Date'[Date], DATE(2024, 1, 1), DATE(2024, 6, 30))
311
- )
312
- ```
313
-
314
- ---
315
-
316
- ## Context Modification Patterns
317
-
318
- ### Removing Filters
319
-
320
- ```dax
321
- // Remove all filters from table
322
- AllSales = CALCULATE([TotalSales], ALL(Sales))
323
-
324
- // Remove filter from specific column
325
- AllProducts_SameRegion = CALCULATE([TotalSales], ALL(Products[Category]))
326
-
327
- // Remove filters except specified columns
328
- SalesInCategory = CALCULATE(
329
- [TotalSales],
330
- ALLEXCEPT(Products, Products[Category])
331
- )
332
-
333
- // Modern syntax: REMOVEFILTERS (more explicit)
334
- AllRegions = CALCULATE([TotalSales], REMOVEFILTERS(Region))
335
- ```
336
-
337
- ### Percentage Calculations
338
-
339
- ```dax
340
- // Percentage of Grand Total
341
- Pct_GrandTotal = DIVIDE(
342
- [TotalSales],
343
- CALCULATE([TotalSales], ALL(Sales))
344
- )
345
-
346
- // Percentage of Parent (within category)
347
- Pct_Category = DIVIDE(
348
- [TotalSales],
349
- CALCULATE([TotalSales], ALLEXCEPT(Products, Products[Category]))
350
- )
351
-
352
- // Percentage of Row Total (in matrix)
353
- Pct_RowTotal = DIVIDE(
354
- [TotalSales],
355
- CALCULATE([TotalSales], ALLSELECTED(Products[SubCategory]))
356
- )
357
-
358
- // Percentage of Column Total (in matrix)
359
- Pct_ColumnTotal = DIVIDE(
360
- [TotalSales],
361
- CALCULATE([TotalSales], ALLSELECTED('Date'[Month]))
362
- )
363
- ```
364
-
365
- ### KEEPFILTERS
366
-
367
- ```dax
368
- // Without KEEPFILTERS - replaces existing filter
369
- Sales_A = CALCULATE([TotalSales], Products[Category] = "A")
370
- // If visual is filtered to Category "B", returns sales for "A"
371
-
372
- // With KEEPFILTERS - intersects with existing filter
373
- Sales_A_KeepFilters = CALCULATE(
374
- [TotalSales],
375
- KEEPFILTERS(Products[Category] = "A")
376
- )
377
- // If visual is filtered to Category "B", returns BLANK (no intersection)
378
- ```
379
-
380
- ---
381
-
382
- ## Iterator Patterns
383
-
384
- ### SUMX / AVERAGEX / MAXX / MINX
385
-
386
- ```dax
387
- // Weighted Average
388
- WeightedAvgPrice = DIVIDE(
389
- SUMX(Sales, Sales[Quantity] * Sales[UnitPrice]),
390
- SUM(Sales[Quantity])
391
- )
392
-
393
- // Average of Positive Values Only
394
- AvgPositiveSales = AVERAGEX(
395
- FILTER(Sales, Sales[Amount] > 0),
396
- Sales[Amount]
397
- )
398
-
399
- // Maximum Sale Amount
400
- MaxSale = MAXX(Sales, Sales[Amount])
401
-
402
- // Minimum Non-Zero
403
- MinNonZero = MINX(
404
- FILTER(Sales, Sales[Amount] > 0),
405
- Sales[Amount]
406
- )
407
- ```
408
-
409
- ### COUNTX / COUNTAX
410
-
411
- ```dax
412
- // Count rows meeting condition
413
- LargeOrderCount = COUNTX(
414
- FILTER(Sales, Sales[Amount] > 1000),
415
- 1 -- Just counting rows
416
- )
417
-
418
- // Count non-blank results
419
- CustomersWithSales = COUNTAX(
420
- FILTER(Customers, [CustomerSales] > 0),
421
- Customers[CustomerKey]
422
- )
423
- ```
424
-
425
- ### Nested Iterators
426
-
427
- ```dax
428
- // Average sales per product per region
429
- AvgSalesProductRegion = AVERAGEX(
430
- CROSSJOIN(VALUES(Products[Product]), VALUES(Region[Region])),
431
- [TotalSales]
432
- )
433
-
434
- // Use with caution - can be slow on large datasets
435
- ```
436
-
437
- ---
438
-
439
- ## Ranking Patterns
440
-
441
- ### Basic Ranking
442
-
443
- ```dax
444
- // Dense rank (ties get same rank, no gaps)
445
- ProductRank = RANKX(
446
- ALL(Products[Product]),
447
- [TotalSales],
448
- , -- Skip value parameter
449
- DESC,
450
- DENSE
451
- )
452
-
453
- // Standard rank (gaps after ties)
454
- ProductRank_Standard = RANKX(
455
- ALL(Products[Product]),
456
- [TotalSales],
457
- ,
458
- DESC,
459
- SKIP
460
- )
461
- ```
462
-
463
- ### Ranking Within Groups
464
-
465
- ```dax
466
- // Rank within category
467
- RankInCategory = RANKX(
468
- ALLEXCEPT(Products, Products[Category]),
469
- [TotalSales],
470
- ,
471
- DESC,
472
- DENSE
473
- )
474
-
475
- // Dynamic ranking (respects slicer selection)
476
- RankSelected = RANKX(
477
- ALLSELECTED(Products[Product]),
478
- [TotalSales],
479
- ,
480
- DESC,
481
- DENSE
482
- )
483
- ```
484
-
485
- ### Top N Filtering
486
-
487
- ```dax
488
- // Is this product in Top 10?
489
- IsTop10 = IF(
490
- RANKX(ALL(Products[Product]), [TotalSales], , DESC, DENSE) <= 10,
491
- 1,
492
- 0
493
- )
494
-
495
- // Sales from Top 10 Products Only
496
- Top10ProductSales = CALCULATE(
497
- [TotalSales],
498
- TOPN(10, ALL(Products[Product]), [TotalSales], DESC)
499
- )
500
-
501
- // Bottom 10
502
- Bottom10ProductSales = CALCULATE(
503
- [TotalSales],
504
- TOPN(10, ALL(Products[Product]), [TotalSales], ASC)
505
- )
506
- ```
507
-
508
- ---
509
-
510
- ## Virtual Tables
511
-
512
- ### TREATAS
513
-
514
- ```dax
515
- // Apply filter from one table to another (no relationship needed)
516
- BudgetForActualProducts = CALCULATE(
517
- [TotalBudget],
518
- TREATAS(VALUES(Sales[ProductKey]), Budget[ProductKey])
519
- )
520
- ```
521
-
522
- ### SUMMARIZE
523
-
524
- ```dax
525
- // Create summary table
526
- SalesByCategory = SUMMARIZE(
527
- Sales,
528
- Products[Category],
529
- "Total", SUM(Sales[Amount]),
530
- "Count", COUNT(Sales[TransactionID])
531
- )
532
- ```
533
-
534
- ### ADDCOLUMNS
535
-
536
- ```dax
537
- // Add calculated columns to virtual table
538
- ProductsWithSales = ADDCOLUMNS(
539
- Products,
540
- "ProductSales", [TotalSales],
541
- "ProductRank", [ProductRank]
542
- )
543
- ```
544
-
545
- ### SELECTCOLUMNS
546
-
547
- ```dax
548
- // Select and rename columns
549
- SimplifiedProducts = SELECTCOLUMNS(
550
- Products,
551
- "ID", Products[ProductKey],
552
- "Name", Products[ProductName],
553
- "Sales", [TotalSales]
554
- )
555
- ```
556
-
557
- ---
558
-
559
- ## Error Handling
560
-
561
- ### IFERROR / ISERROR
562
-
563
- ```dax
564
- // Handle potential errors
565
- SafeCalculation = IFERROR([PotentiallyBadMeasure], 0)
566
-
567
- // Check before calculating
568
- ConditionalCalc = IF(
569
- ISERROR([PotentiallyBadMeasure]),
570
- "Error in calculation",
571
- [PotentiallyBadMeasure]
572
- )
573
- ```
574
-
575
- ### BLANK Handling
576
-
577
- ```dax
578
- // Replace BLANK with zero
579
- SalesOrZero = IF(ISBLANK([TotalSales]), 0, [TotalSales])
580
-
581
- // Shorter syntax
582
- SalesOrZero = [TotalSales] + 0 // Forces BLANK to 0
583
-
584
- // Check for BLANK
585
- HasSales = NOT(ISBLANK([TotalSales]))
586
- ```
587
-
588
- ---
589
-
590
- ## Anti-Patterns to Avoid
591
-
592
- ### Nested CALCULATE
593
-
594
- ```dax
595
- -- BAD: Nested CALCULATE
596
- Bad_Nested = CALCULATE(
597
- CALCULATE([Sales], Table1[Col] = "A"),
598
- Table2[Col] = "B"
599
- )
600
-
601
- -- GOOD: Flat filters
602
- Good_Flat = CALCULATE(
603
- [Sales],
604
- Table1[Col] = "A",
605
- Table2[Col] = "B"
606
- )
607
- ```
608
-
609
- ### FILTER on Large Tables
610
-
611
- ```dax
612
- -- BAD: FILTER iterates entire fact table
613
- Bad_Filter = CALCULATE(
614
- SUM(FactSales[Amount]),
615
- FILTER(FactSales, FactSales[Region] = "North")
616
- )
617
-
618
- -- GOOD: Filter on dimension
619
- Good_Filter = CALCULATE(
620
- SUM(FactSales[Amount]),
621
- DimRegion[Region] = "North"
622
- )
623
- ```
624
-
625
- ### Hardcoded Values
626
-
627
- ```dax
628
- -- BAD: Hardcoded year
629
- Sales2024 = CALCULATE([Sales], 'Date'[Year] = 2024)
630
-
631
- -- GOOD: Dynamic
632
- SalesCurrentYear = CALCULATE([Sales], 'Date'[Year] = YEAR(TODAY()))
633
-
634
- -- BETTER: Parameter table
635
- SalesSelectedYear = CALCULATE([Sales], 'Date'[Year] = SELECTEDVALUE(YearParameter[Year]))
636
- ```
637
-
638
- ### Calculated Columns for Display
639
-
640
- ```dax
641
- -- BAD: Calculated column for values that should be measures
642
- -- (increases model size, can't be filtered dynamically)
643
- Sales[Running_Total] = ... -- Don't do this
644
-
645
- -- GOOD: Measure instead
646
- Running_Total = CALCULATE(...) -- Use measures
647
- ```
648
-
649
- ---
650
-
651
- ## Performance Tips
652
-
653
- 1. **Variables cache results** - Calculate once, use many times
654
- 2. **Filter on dimensions** - Not fact tables
655
- 3. **Simple predicates first** - Column = Value is faster than FILTER
656
- 4. **Limit iterator scope** - Filter before iterating
657
- 5. **Avoid bi-directional** - Use measures instead
658
- 6. **Use SUMMARIZE carefully** - Can be slow; SUMMARIZECOLUMNS is internal only
659
- 7. **Test with DAX Studio** - Check Server Timings
660
-
661
- ### Performance Comparison
662
-
663
- | Pattern | Speed | Use When |
664
- |---------|-------|----------|
665
- | `Column = Value` | Fast | Simple equality |
666
- | `FILTER(dimension, ...)` | Medium | Complex conditions |
667
- | `FILTER(fact, ...)` | Slow | Avoid if possible |
668
- | `Nested CALCULATE` | Slow | Never |
669
-
670
- ---
671
-
672
- ## Formatting Standards
673
-
674
- ```dax
675
- -- Simple measure (one line, no VAR/RETURN needed)
676
- Sales = SUM(Sales[Amount])
677
-
678
- -- Medium complexity (few lines)
679
- Margin % = DIVIDE([Profit], [Revenue], 0)
680
-
681
- -- Complex measure (structured with comments, Result as final var)
682
- Sales YoY % =
683
- // Purpose: Year-over-year change with handling for missing prior year
684
- VAR CurrentSales = [Sales]
685
- VAR PriorYearSales =
686
- CALCULATE(
687
- [Sales],
688
- SAMEPERIODLASTYEAR('Date'[Date])
689
- )
690
- VAR Delta = CurrentSales - PriorYearSales
691
- VAR HasPriorYear = NOT(ISBLANK(PriorYearSales))
692
- VAR Result =
693
- IF(
694
- HasPriorYear,
695
- DIVIDE(Delta, PriorYearSales),
696
- BLANK()
697
- )
698
- RETURN Result
699
- ```
700
-
701
- ---
702
-
703
- ## Quick Reference
704
-
705
- | Need | DAX |
706
- |------|-----|
707
- | Sum | `SUM(Table[Column])` |
708
- | Count rows | `COUNTROWS(Table)` |
709
- | Distinct count | `DISTINCTCOUNT(Table[Column])` |
710
- | Average | `AVERAGE(Table[Column])` |
711
- | Safe division | `DIVIDE(num, denom, alt)` |
712
- | Year-to-date | `TOTALYTD([Measure], 'Date'[Date])` |
713
- | Prior year | `CALCULATE([Measure], SAMEPERIODLASTYEAR('Date'[Date]))` |
714
- | Remove filters | `CALCULATE([Measure], ALL(Table))` |
715
- | Keep some filters | `CALCULATE([Measure], ALLEXCEPT(Table, Table[Col]))` |
716
- | Rank | `RANKX(ALL(Table[Col]), [Measure], , DESC, DENSE)` |
717
- | Top N | `CALCULATE([Measure], TOPN(N, ALL(Table[Col]), [Measure]))` |
718
- | Rolling 12M | `CALCULATE([Measure], DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH))` |
719
-
720
- ---
721
-
722
- ## Complexity Adaptation
723
-
724
- Adjust depth based on `config.json → experienceLevel`:
725
- - **beginner**: Step-by-step with explanations, reference library examples
726
- - **intermediate**: Standard depth, explain non-obvious decisions
727
- - **advanced**: Concise, skip basics, focus on edge cases and optimization
728
-
729
- ## Related Skills
730
-
731
- - `/power-query` — Data transformation before DAX
732
- - `/data-modeling` — Model design affects DAX patterns
733
- - `/query-performance` — Optimize DAX performance
734
- - `/testing-validation` — Test DAX measures
735
- - `/dax-doctor` — Debug DAX issues
736
- - `/dax-udf` — User-defined functions for reusable DAX logic
737
-
738
- ---
739
-
740
- ## Related Resources
741
-
742
- - [Snippets: Time Intelligence](../../snippets/dax/time-intelligence.md)
743
- - [Snippets: Rankings](../../snippets/dax/rankings-and-topn.md)
744
- - [Snippets: CALCULATE Patterns](../../snippets/dax/calculate-patterns.md)
745
- - [Query Performance Skill](../query-performance/SKILL.md)
746
- - [Data Modeling Skill](../data-modeling/SKILL.md)