-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathRules-Dataset.json
925 lines (925 loc) · 58.4 KB
/
Rules-Dataset.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
[
{
"ID": "AVOID_FLOATING_POINT_DATA_TYPES",
"Name": "[Performance] Do not use floating point data types",
"Category": "Performance",
"Description": "The \"Double\" floating point data type should be avoided, as it can result in unpredictable roundoff errors and decreased performance in certain scenarios. Use \"Int64\" or \"Decimal\" where appropriate (but note that \"Decimal\" is limited to 4 digits after the decimal sign).",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "DataType = \"Double\"",
"FixExpression": "DataType = DataType.Decimal",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "ISAVAILABLEINMDX_FALSE_NONATTRIBUTE_COLUMNS",
"Name": "[Performance] Set IsAvailableInMdx to false on non-attribute columns",
"Category": "Performance",
"Description": "To speed up processing time and conserve memory after processing, attribute hierarchies should not be built for columns that are never used for slicing by MDX clients. In other words, all hidden columns that are not used as a Sort By Column or referenced in user hierarchies should have their IsAvailableInMdx property set to false.\r\nReference: https://blog.crossjoin.co.uk/2018/07/02/isavailableinmdx-ssas-tabular/",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "IsAvailableInMDX\r\nand\r\n\n(IsHidden or Table.IsHidden)\r\nand\r\n\nnot UsedInSortBy.Any() \r\nand\r\n\nnot UsedInHierarchies.Any()\r\nand\r\nnot UsedInVariations.Any()\r\nand\r\nSortByColumn = null",
"FixExpression": "IsAvailableInMDX = false",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_BI-DIRECTIONAL_RELATIONSHIPS_AGAINST_HIGH-CARDINALITY_COLUMNS",
"Name": "[Performance] Avoid bi-directional relationships against high-cardinality columns",
"Category": "Performance",
"Description": "For best performance, it is recommended to avoid using bi-directional relationships against high-cardinality columns. In order to run this rule, you must first run the script shown here: https://www.elegantbi.com/post/vertipaqintabulareditor",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any(CrossFilteringBehavior == CrossFilteringBehavior.BothDirections)\n\nand\n\nConvert.ToInt64(GetAnnotation(\"Vertipaq_Cardinality\")) > 100000",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REDUCE_USAGE_OF_LONG-LENGTH_COLUMNS_WITH_HIGH_CARDINALITY",
"Name": "[Performance] Reduce usage of long-length columns with high cardinality",
"Category": "Performance",
"Description": "It is best to avoid lengthy text columns. This is especially true if the column has many unique values. These types of columns can cause longer processing times, bloated model sizes, as well as slower user queries. Long length is defined as more than 100 characters.",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "Convert.ToInt64(GetAnnotation(\"LongLengthRowCount\")) > 500000",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "SPLIT_DATE_AND_TIME",
"Name": "[Performance] Split date and time",
"Category": "Performance",
"Description": "This rule finds datetime columns that have values not at midnight. To maximize performance, the time element should be split from date element (or the time component should be rounded to midnight as this will reduce column cardinality).\r\nReference: https://www.sqlbi.com/articles/separate-date-and-time-in-powerpivot-and-bism-tabular/",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "Convert.ToInt32(GetAnnotation(\"DateTimeWithHourMinSec\")) > 0",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "LARGE_TABLES_SHOULD_BE_PARTITIONED",
"Name": "[Performance] Large tables should be partitioned",
"Category": "Performance",
"Description": "Large tables should be partitioned in order to optimize processing. In order for this rule to run properly, you must run the script shown here: https://www.elegantbi.com/post/vertipaqintabulareditor",
"Severity": 2,
"Scope": "Table",
"Expression": "Convert.ToInt64(GetAnnotation(\"Vertipaq_RowCount\")) > 25000000\r\nand\r\nPartitions.Count = 1",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REDUCE_USAGE_OF_CALCULATED_COLUMNS_THAT_USE_THE_RELATED_FUNCTION",
"Name": "[Performance] Reduce usage of calculated columns that use the RELATED function",
"Category": "Performance",
"Description": "Calculated columns do not compress as well as data columns and may cause longer processing times. As such, calculated columns should be avoided if possible. One scenario where they may be easier to avoid is if they use the RELATED function.\r\nReference: https://www.sqlbi.com/articles/storage-differences-between-calculated-columns-and-calculated-tables/",
"Severity": 2,
"Scope": "CalculatedColumn",
"Expression": "RegEx.IsMatch(Expression,\"(?i)RELATED\\s*\\(\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "SNOWFLAKE_SCHEMA_ARCHITECTURE",
"Name": "[Performance] Consider a star-schema instead of a snowflake architecture",
"Category": "Performance",
"Description": "Generally speaking, a star-schema is the optimal architecture for tabular models. That being the case, there are valid cases to use a snowflake approach. Please check your model and consider moving to a star-schema architecture.\r\nReference: https://docs.microsoft.com/power-bi/guidance/star-schema",
"Severity": 2,
"Scope": "Table, CalculatedTable",
"Expression": "UsedInRelationships.Any(current.Name == FromTable.Name)\r\nand\r\nUsedInRelationships.Any(current.Name == ToTable.Name)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MODEL_SHOULD_HAVE_A_DATE_TABLE",
"Name": "[Performance] Model should have a date table",
"Category": "Performance",
"Description": "Generally speaking, models should generally have a date table. Models that do not have a date table generally are not taking advantage of features such as time intelligence or may not have a properly structured architecture.",
"Severity": 2,
"Scope": "Model",
"Expression": "Tables.Any(DataCategory == \"Time\" && Columns.Any(IsKey == true && DataType == \"DateTime\")) == false",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "DATE/CALENDAR_TABLES_SHOULD_BE_MARKED_AS_A_DATE_TABLE",
"Name": "[Performance] Date/calendar tables should be marked as a date table",
"Category": "Performance",
"Description": "This rule looks for tables that contain the words 'date' or 'calendar' as they should likely be marked as a date table.\r\nReference: https://docs.microsoft.com/power-bi/transform-model/desktop-date-tables",
"Severity": 2,
"Scope": "Table, CalculatedTable",
"Expression": "(Name.ToUpper().Contains(\"DATE\") or Name.ToUpper().Contains(\"CALENDAR\"))\n\nand\n\n(\nDataCategory <> \"Time\"\n\nor\n\nColumns.Any(IsKey == true && DataType == \"DateTime\") == false\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REMOVE_AUTO-DATE_TABLE",
"Name": "[Performance] Remove auto-date table",
"Category": "Performance",
"Description": "Avoid using auto-date tables. Make sure to turn off auto-date table in the settings in Power BI Desktop. This will save memory resources. \r\nReference: https://www.youtube.com/watch?v=xu3uDEHtCrg",
"Severity": 2,
"Scope": "Table, CalculatedTable",
"Expression": "ObjectTypeName == \"Calculated Table\"\n\r\nand\r\n\n(\nName.StartsWith(\"DateTableTemplate_\") \n\nor \n\nName.StartsWith(\"LocalDateTable_\")\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_EXCESSIVE_BI-DIRECTIONAL_OR_MANY-TO-MANY_RELATIONSHIPS",
"Name": "[Performance] Avoid excessive bi-directional or many-to-many relationships",
"Category": "Performance",
"Description": "Limit use of b-di and many-to-many relationships. This rule flags the model if more than 30% of relationships are bi-di or many-to-many.\r\nReference: https://www.sqlbi.com/articles/bidirectional-relationships-and-ambiguity-in-dax/",
"Severity": 2,
"Scope": "Model",
"Expression": "(\r\n\nRelationships.Where(CrossFilteringBehavior == CrossFilteringBehavior.BothDirections).Count()\r\n\n+\r\n\nRelationships.Where(FromCardinality.ToString() == \"Many\" && ToCardinality.ToString() == \"Many\").Count()\r\n\n)\r\n\n\n/\r\n\n\nMath.Max(Convert.ToDecimal(Relationships.Count)\n\n,1)> 0.3",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "LIMIT_ROW_LEVEL_SECURITY_(RLS)_LOGIC",
"Name": "[Performance] Limit row level security (RLS) logic",
"Category": "Performance",
"Description": "Try to simplify the DAX used for row level security. Usage of the functions within this rule can likely be offloaded to the upstream systems (data warehouse).",
"Severity": 2,
"Scope": "Table, CalculatedTable",
"Expression": "RowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"(?i)RIGHT\\s*\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"(?i)LEFT\\s*\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"(?i)UPPER\\s*\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"(?i)LOWER\\s*\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"(?i)FIND\\s*\\(\"))\r\n",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MODEL_USING_DIRECT_QUERY_AND_NO_AGGREGATIONS",
"Name": "[Performance] Consider using aggregations if using Direct Query in Power BI",
"Category": "Performance",
"Description": "If using Direct Query in Power BI Premium, you may want to consider using aggregations in order to boost performance.\r\nReference: https://docs.microsoft.com/power-bi/transform-model/desktop-aggregations",
"Severity": 1,
"Scope": "Model",
"Expression": "Tables.Any(ObjectTypeName == \"Table (DirectQuery)\")\r\nand\r\n\n\nAllColumns.Any(AlternateOf != null) == false\r\nand \r\nDefaultPowerBIDataSourceVersion.ToString() == \"PowerBI_V3\"",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MINIMIZE_POWER_QUERY_TRANSFORMATIONS",
"Name": "[Performance] Minimize Power Query transformations",
"Category": "Performance",
"Description": "Minimize Power Query transformations in order to improve model processing performance. It is a best practice to offload these transformations to the data warehouse if possible. Also, please check whether query folding is occurring within your model. Please reference the article below for more information on query folding.\r\nReference: https://docs.microsoft.com/power-query/power-query-folding",
"Severity": 2,
"Scope": "Partition",
"Expression": "\nSourceType.ToString() = \"M\"\r\nand\r\n(\r\nQuery.Contains(\"Table.Combine(\")\r\nor\r\n\nQuery.Contains(\"Table.Join(\")\r\nor\r\n\nQuery.Contains(\"Table.NestedJoin(\")\r\nor\r\nQuery.Contains(\"Table.AddColumn(\")\r\nor\r\nQuery.Contains(\"Table.Group(\")\r\nor\r\nQuery.Contains(\"Table.Sort(\")\r\nor\r\nQuery.Contains(\"Table.Pivot(\")\r\nor\r\nQuery.Contains(\"Table.Unpivot(\")\r\nor\r\nQuery.Contains(\"Table.UnpivotOtherColumns(\")\r\nor\r\nQuery.Contains(\"Table.Distinct(\")\r\nor\r\nQuery.Contains(\"[Query=\"\"SELECT\")\r\nor\r\nQuery.Contains(\"Value.NativeQuery\")\r\nor\r\nQuery.Contains(\"OleDb.Query\")\r\nor\r\nQuery.Contains(\"Odbc.Query\")\r\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_USING_MANY-TO-MANY_RELATIONSHIPS_ON_TABLES_USED_FOR_DYNAMIC_ROW_LEVEL_SECURITY",
"Name": "[Performance] Avoid using many-to-many relationships on tables used for dynamic row level security",
"Category": "Performance",
"Description": "Using many-to-many relationships on tables which use dynamic row level security can cause serious query performance degradation. This pattern's performance problems compound when snowflaking multiple many-to-many relationships against a table which contains row level security. Instead, use one of the patterns shown in the article below where a single dimension table relates many-to-one to a security table.\r\n\r\nReference: https://www.elegantbi.com/post/dynamicrlspatterns",
"Severity": 3,
"Scope": "Table",
"Expression": "UsedInRelationships.Any(FromCardinality == \"Many\" and ToCardinality== \"Many\")\r\nand\r\nRowLevelSecurity.Any(it.Length > 0)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "UNPIVOT_PIVOTED_(MONTH)_DATA",
"Name": "[Performance] Unpivot pivoted (month) data",
"Category": "Performance",
"Description": "Avoid using pivoted data in your tables. This rule checks specifically for pivoted data by month.\r\nReference: https://www.elegantbi.com/post/top10bestpractices",
"Severity": 2,
"Scope": "Table, CalculatedTable",
"Expression": "Columns.Any(Name.ToUpper().Contains(\"JAN\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"FEB\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"MAR\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"APR\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"MAY\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))\nand\nColumns.Any(Name.ToUpper().Contains(\"JUN\") && (DataType == DataType.Int64 || DataType == DataType.Decimal || DataType == DataType.Double))",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MANY-TO-MANY_RELATIONSHIPS_SHOULD_BE_SINGLE-DIRECTION",
"Name": "[Performance] Many-to-many relationships should be single-direction",
"Category": "Performance",
"Description": null,
"Severity": 2,
"Scope": "Relationship",
"Expression": "FromCardinality == \"Many\"\n\r\nand\r\n\nToCardinality == \"Many\"\r\n\nand\r\n\nCrossFilteringBehavior == \"BothDirections\"",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REDUCE_USAGE_OF_CALCULATED_TABLES",
"Name": "[Performance] Reduce usage of calculated tables",
"Category": "Performance",
"Description": "Migrate calculated table logic to your data warehouse. Reliance on calculated tables will lead to technical debt and potential misalignments if you have multiple models on your platform.",
"Severity": 2,
"Scope": "CalculatedTable",
"Expression": "1=1",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REMOVE_REDUNDANT_COLUMNS_IN_RELATED_TABLES",
"Name": "[Performance] Remove redundant columns in related tables",
"Category": "Performance",
"Description": "Removing unnecessary columns reduces model size and speeds up data loading.",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any() == false \r\nand\r\nModel.AllColumns.Any(Name == current.Name and Table.Name != current.Table.Name and Table.UsedInRelationships.Any(FromTable.Name == current.Table.Name))",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MEASURES_USING_TIME_INTELLIGENCE_AND_MODEL_IS_USING_DIRECT_QUERY",
"Name": "[Performance] Measures using time intelligence and model is using Direct Query",
"Category": "Performance",
"Description": "At present, time intelligence functions are known to not perform as well when using Direct Query. If you are having performance issues, you may want to try alternative solutions such as adding columns in the fact table that show previous year or previous month data.",
"Severity": 2,
"Scope": "Measure, CalculationItem",
"Expression": "Model.Tables.Any(ObjectTypeName == \"Table (DirectQuery)\")\r\nand\r\n(\r\nRegEx.IsMatch(Expression,\"CLOSINGBALANCEMONTH\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"CLOSINGBALANCEQUARTER\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"CLOSINGBALANCEYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"DATEADD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"DATESBETWEEN\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"DATESINPERIOD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"DATESMTD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"DATESQTD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"DATESYTD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"ENDOFMONTH\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"ENDOFQUARTER\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"ENDOFYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"FIRSTDATE\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"FIRSTNONBLANK\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"FIRSTNONBLANKVALUE\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"LASTDATE\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"LASTNONBLANK\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"LASTNONBLANKVALUE\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"NEXTDAY\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"NEXTMONTH\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"NEXTQUARTER\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"NEXTYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"OPENINGBALANCEMONTH\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"OPENINGBALANCEQUARTER\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"OPENINGBALANCEYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"PARALLELPERIOD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"PREVIOUSDAY\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"PREVIOUSMONTH\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"PREVIOUSQUARTER\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"PREVIOUSYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"SAMEPERIODLASTYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"STARTOFMONTH\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"STARTOFQUARTER\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"STARTOFYEAR\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"TOTALMTD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"TOTALQTD\\s*\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"TOTALYTD\\s*\\(\")\r\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REDUCE_NUMBER_OF_CALCULATED_COLUMNS",
"Name": "[Performance] Reduce number of calculated columns",
"Category": "Performance",
"Description": "Calculated columns do not compress as well as data columns so they take up more memory. They also slow down processing times for both the table as well as process recalc. Offload calculated column logic to your data warehouse and turn these calculated columns into data columns.\r\nReference: https://www.elegantbi.com/post/top10bestpractices",
"Severity": 2,
"Scope": "Model",
"Expression": "AllColumns.Where(Type.ToString() == \"Calculated\").Count() > 5",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "CHECK_IF_BI-DIRECTIONAL_AND_MANY-TO-MANY_RELATIONSHIPS_ARE_VALID",
"Name": "[Performance] Check if bi-directional and many-to-many relationships are valid",
"Category": "Performance",
"Description": "Bi-directional and many-to-many relationships may cause performance degradation or even have unintended consequences. Make sure to check these specific relationships to ensure they are working as designed and are actually necessary.\r\nReference: https://www.sqlbi.com/articles/bidirectional-relationships-and-ambiguity-in-dax/",
"Severity": 1,
"Scope": "Relationship",
"Expression": "FromCardinality.ToString() = \"Many\" and ToCardinality.ToString() = \"Many\"\r\nor\r\nCrossFilteringBehavior == CrossFilteringBehavior.BothDirections",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)_IS_NECESSARY",
"Name": "[Performance] Check if dynamic row level security (RLS) is necessary",
"Category": "Performance",
"Description": "Usage of dynamic row level security (RLS) can add memory and performance overhead. Please research the pros/cons of using it.\r\nReference: https://docs.microsoft.com/power-bi/admin/service-admin-rls",
"Severity": 1,
"Scope": "TablePermission",
"Expression": "RegEx.IsMatch(Expression,\"(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)USERPRINCIPALNAME\\(\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "DAX_COLUMNS_FULLY_QUALIFIED",
"Name": "[DAX Expressions] Column references should be fully qualified",
"Category": "DAX Expressions",
"Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors. When referencing a column in DAX, first specify the table name, then specify the column name in square brackets.\r\nReference: https://www.elegantbi.com/post/top10bestpractices",
"Severity": 2,
"Scope": "Measure, KPI, TablePermission, CalculationItem",
"Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "DAX_MEASURES_UNQUALIFIED",
"Name": "[DAX Expressions] Measure references should be unqualified",
"Category": "DAX Expressions",
"Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors. When referencing a measure using DAX, do not specify the table name. Use only the measure name in square brackets.\r\nReference: https://www.elegantbi.com/post/top10bestpractices",
"Severity": 3,
"Scope": "Measure, CalculatedColumn, CalculatedTable, KPI, CalculationItem",
"Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_DUPLICATE_MEASURES",
"Name": "[DAX Expressions] No two measures should have the same definition",
"Category": "DAX Expressions",
"Description": "Two measures with different names and defined by the same DAX expression should be avoided to reduce redundancy.",
"Severity": 2,
"Scope": "Measure",
"Expression": "Model.AllMeasures.Any(Expression.Replace(\" \",\"\").Replace(\"\\n\",\"\").Replace(\"\\r\",\"\").Replace(\"\\t\",\"\") = outerIt.Expression.Replace(\" \",\"\").Replace(\"\\n\",\"\").Replace(\"\\r\",\"\").Replace(\"\\t\",\"\") and it <> outerIt)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "USE_THE_TREATAS_FUNCTION_INSTEAD_OF_INTERSECT",
"Name": "[DAX Expressions] Use the TREATAS function instead of INTERSECT for virtual relationships",
"Category": "DAX Expressions",
"Description": "The TREATAS function is more efficient and provides better performance than the INTERSECT function when used in virutal relationships.\r\nReference: https://www.sqlbi.com/articles/propagate-filters-using-treatas-in-dax/",
"Severity": 2,
"Scope": "Measure, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"(?i)INTERSECT\\s*\\(\")",
"FixExpression": null,
"CompatibilityLevel": 1400,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "USE_THE_DIVIDE_FUNCTION_FOR_DIVISION",
"Name": "[DAX Expressions] Use the DIVIDE function for division",
"Category": "DAX Expressions",
"Description": "Use the DIVIDE function instead of using \"/\". The DIVIDE function resolves divide-by-zero cases. As such, it is recommended to use to avoid errors.\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/dax-divide-function-operator",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"\\]\\s*\\/(?!\\/)(?!\\*)\")\r\nor\r\nRegEx.IsMatch(Expression,\"\\)\\s*\\/(?!\\/)(?!\\*)\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_USING_THE_IFERROR_FUNCTION",
"Name": "[DAX Expressions] Avoid using the IFERROR function",
"Category": "DAX Expressions",
"Description": "Avoid using the IFERROR function as it may cause performance degradation. If you are concerned about a divide-by-zero error, use the DIVIDE function as it naturally resolves such errors as blank (or you can customize what should be shown in case of such an error).\r\nReference: https://www.elegantbi.com/post/top10bestpractices",
"Severity": 2,
"Scope": "Measure, CalculatedColumn",
"Expression": "RegEx.IsMatch(Expression,\"(?i)IFERROR\\s*\\(\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MEASURES_SHOULD_NOT_BE_DIRECT_REFERENCES_OF_OTHER_MEASURES",
"Name": "[DAX Expressions] Measures should not be direct references of other measures",
"Category": "DAX Expressions",
"Description": "This rule identifies measures which are simply a reference to another measure. As an example, consider a model with two measures: [MeasureA] and [MeasureB]. This rule would be triggered for MeasureB if MeasureB's DAX was MeasureB:=[MeasureA]. Such duplicative measures should be removed.",
"Severity": 2,
"Scope": "Measure",
"Expression": "Model.AllMeasures.Any(DaxObjectName == current.Expression)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "FILTER_COLUMN_VALUES",
"Name": "[DAX Expressions] Filter column values with proper syntax",
"Category": "DAX Expressions",
"Description": "Instead of using this pattern FILTER('Table','Table'[Column]=\"Value\") for the filter parameters of a CALCULATE or CALCULATETABLE function, use one of the options below. As far as whether to use the KEEPFILTERS function, see the second reference link below.\r\n\r\nOption 1: KEEPFILTERS('Table'[Column]=\"Value\")\r\nOption 2: 'Table'[Column]=\"Value\"\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/dax-avoid-avoid-filter-as-filter-argument\r\nReference: https://www.sqlbi.com/articles/using-keepfilters-in-dax/",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"(?i)CALCULATE\\s*\\(\\s*[^,]+,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+'*\\s*,\\s*\\'*[A-Za-z0-9 _]+\\'*\\[[A-Za-z0-9 _]+\\]\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)CALCULATETABLE\\s*\\([^,]*,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*,\\s*\\'*[A-Za-z0-9 _]+\\'*\\[[A-Za-z0-9 _]+\\]\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "FILTER_MEASURE_VALUES_BY_COLUMNS",
"Name": "[DAX Expressions] Filter measure values by columns, not tables",
"Category": "DAX Expressions",
"Description": "Instead of using this pattern FILTER('Table',[Measure]>Value) for the filter parameters of a CALCULATE or CALCULATETABLE function, use one of the options below (if possible). Filtering on a specific column will produce a smaller table for the engine to process, thereby enabling faster performance. Using the VALUES function or the ALL function depends on the desired measure result.\r\n\r\nOption 1: FILTER(VALUES('Table'[Column]),[Measure] > Value)\r\nOption 2: FILTER(ALL('Table'[Column]),[Measure] > Value)\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/dax-avoid-avoid-filter-as-filter-argument",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"(?i)CALCULATE\\s*\\(\\s*[^,]+,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*\\s*,\\s*\\[[^\\]]+\\]\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)CALCULATETABLE\\s*\\([^,]*,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*,\\s*\\[\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "INACTIVE_RELATIONSHIPS_THAT_ARE_NEVER_ACTIVATED",
"Name": "[DAX Expressions] Inactive relationships that are never activated",
"Category": "DAX Expressions",
"Description": "Inactive relationships are activated using the USERELATIONSHIP function. If an inactive relationship is not referenced in any measure via this function, the relationship will not be used. It should be determined whether the relationship is not necessary or to activate the relationship via this method.\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/relationships-active-inactive\r\nReference: https://dax.guide/userelationship/",
"Severity": 2,
"Scope": "Relationship",
"Expression": "IsActive == false\r\nand not\r\n(\r\nModel.AllMeasures.Any(RegEx.IsMatch(Expression,\r\n\"(?i)USERELATIONSHIP\\s*\\(\\s*\\'*\" +\r\ncurrent.FromTable.Name + \"\\'*\\[\" + \r\ncurrent.FromColumn.Name + \"\\]\\s*,\\s*\\'*\" +\r\ncurrent.ToTable.Name + \"\\'*\\[\" +\r\ncurrent.ToColumn.Name + \"\\]\"))\r\nor\r\nModel.AllCalculationItems.Any(RegEx.IsMatch(Expression,\r\n\"(?i)USERELATIONSHIP\\s*\\(\\s*\\'*\" +\r\ncurrent.FromTable.Name + \"\\'*\\[\" + \r\ncurrent.FromColumn.Name + \"\\]\\s*,\\s*\\'*\" +\r\ncurrent.ToTable.Name + \"\\'*\\[\" +\r\ncurrent.ToColumn.Name + \"\\]\"))\r\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_USING_'1-(X/Y)'_SYNTAX",
"Name": "[DAX Expressions] Avoid using '1-(x/y)' syntax",
"Category": "DAX Expressions",
"Description": "Instead of using the '1-(x/y)' or '1+(x/y)' syntax to achieve a percentage calculation, use the basic DAX functions (as shown below). Using the improved syntax will generally improve the performance. The '1+/-...' syntax always returns a value whereas the solution without the '1+/-...' does not (as the value may be 'blank'). Therefore the '1+/-...' syntax may return more rows/columns which may result in a slower query speed.\r\n\r\nLet's clarify with an example:\r\n\r\nAvoid this: 1 - SUM ( 'Sales'[CostAmount] ) / SUM( 'Sales'[SalesAmount] )\r\nBetter: DIVIDE ( SUM ( 'Sales'[SalesAmount] ) - SUM ( 'Sales'[CostAmount] ), SUM ( 'Sales'[SalesAmount] ) )\r\nBest: VAR x = SUM ( 'Sales'[SalesAmount] ) RETURN DIVIDE ( x - SUM ( 'Sales'[CostAmount] ), x )",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"[0-9]+\\s*[-+]\\s*[\\(]*\\s*(?i)SUM\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*\\s*\\[[A-Za-z0-9 _]+\\]\\s*\\)\\s*\\/\")\r\nor\r\nRegEx.IsMatch(Expression,\"[0-9]+\\s*[-+]\\s*(?i)DIVIDE\\s*\\(\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "EVALUATEANDLOG_SHOULD_NOT_BE_USED_IN_PRODUCTION_MODELS",
"Name": "[DAX Expressions] The EVALUATEANDLOG function should not be used in production models",
"Category": "DAX Expressions",
"Description": "The EVALUATEANDLOG function is meant to be used only in development/test environments and should not be used in production models.\r\n\r\nReference: https://pbidax.wordpress.com/2022/08/16/introduce-the-dax-evaluateandlog-function/",
"Severity": 1,
"Scope": "Measure",
"Expression": "RegEx.IsMatch(Expression,\"(?i)EVALUATEANDLOG\\s*\\(\")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "DATA_COLUMNS_MUST_HAVE_A_SOURCE_COLUMN",
"Name": "[Error Prevention] Data columns must have a source column",
"Category": "Error Prevention",
"Description": "Data columns must have a source column. A data column without a source column will cause an error when processing the model.",
"Severity": 3,
"Scope": "DataColumn",
"Expression": "string.IsNullOrWhitespace(SourceColumn)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "EXPRESSION_RELIANT_OBJECTS_MUST_HAVE_AN_EXPRESSION",
"Name": "[Error Prevention] Expression-reliant objects must have an expression",
"Category": "Error Prevention",
"Description": "Calculated columns, calculation items and measures must have an expression. Without an expression, these objects will not show any values.",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "string.IsNullOrWhiteSpace(Expression)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_STRUCTURED_DATA_SOURCES_WITH_PROVIDER_PARTITIONS",
"Name": "[Error Prevention] Avoid structured data sources with provider partitions",
"Category": "Error Prevention",
"Description": "Power BI does not support provider (a.k.a. 'legacy') partitions which reference structured data sources. Partitions which reference structured data sources must use the M-language. Otherwise, 'provider' partitions must reference a 'provider' data source. This can be resolved by converting the structured data source into a provider data source (see 2nd reference link below).\r\n\r\nReference: https://docs.microsoft.com/power-bi/admin/service-premium-connect-tools#data-source-declaration\r\nReference: https://www.elegantbi.com/post/convertdatasources",
"Severity": 2,
"Scope": "Partition",
"Expression": "SourceType == \"Query\"\r\nand\r\nDataSource.Type == \"Structured\"",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_THE_USERELATIONSHIP_FUNCTION_AND_RLS_AGAINST_THE_SAME_TABLE",
"Name": "[Error Prevention] Avoid the USERELATIONSHIP function and RLS against the same table",
"Category": "Error Prevention",
"Description": "The USERELATIONSHIP function may not be used against a table which also leverages row-level security (RLS). This will generate an error when using the particular measure in a visual. This rule will highlight the table which is used in a measure's USERELATIONSHIP function as well as RLS.\r\n\r\nReference: https://blog.crossjoin.co.uk/2013/05/10/userelationship-and-tabular-row-security/",
"Severity": 3,
"Scope": "Table, CalculatedTable",
"Expression": "Model.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)USERELATIONSHIP\\s*\\(\\s*.+?(?=])\\]\\s*,\\s*'*\" + current.Name + \"'*\\[\"))\r\nand\r\nRowLevelSecurity.Any(it <> null)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "RELATIONSHIP_COLUMNS_SAME_DATA_TYPE",
"Name": "[Error Prevention] Relationship columns should be of the same data type",
"Category": "Error Prevention",
"Description": "Columns used in a relationship should be of the same data type. Ideally, they will be of integer data type (see the related rule '[Formatting] Relationship columns should be of integer data type'). Having columns within a relationship which are of different data types may lead to various issues.",
"Severity": 3,
"Scope": "Relationship",
"Expression": "FromColumn.DataType != ToColumn.DataType",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_INVALID_NAME_CHARACTERS",
"Name": "[Error Prevention] Avoid invalid characters in names",
"Category": "Error Prevention",
"Description": "This rule identifies if a name for a given object in your model (i.e. table/column/measure) which contains an invalid character. Invalid characters will cause an error when deploying the model (and failure to deploy). This rule has a fix expression which converts the invalid character into a space, resolving the issue.",
"Severity": 3,
"Scope": "Table, Measure, Hierarchy, Level, Perspective, Partition, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, KPI, ModelRole, CalculationGroup, CalculationItem",
"Expression": "Name.ToCharArray().Any(char.IsControl(it) and !char.IsWhiteSpace(it))",
"FixExpression": "Name = string.Concat( it.Name.ToCharArray().Select( c => (char.IsControl(c) && !char.IsWhiteSpace(c)) ? ' ': c ))",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "AVOID_INVALID_DESCRIPTION_CHARACTERS",
"Name": "[Error Prevention] Avoid invalid characters in descriptions",
"Category": "Error Prevention",
"Description": "This rule identifies if a description for a given object in your model (i.e. table/column/measure) which contains an invalid character. Invalid characters will cause an error when deploying the model (and failure to deploy). This rule has a fix expression which converts the invalid character into a space, resolving the issue.",
"Severity": 3,
"Scope": "Table, Measure, Hierarchy, Level, Perspective, Partition, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, KPI, ModelRole, CalculationGroup, CalculationItem",
"Expression": "Description.ToCharArray().Any(char.IsControl(it) and !char.IsWhiteSpace(it))",
"FixExpression": "Description = string.Concat( it.Description.ToCharArray().Select( c => (char.IsControl(c) && !char.IsWhiteSpace(c)) ? ' ': c ))",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "SET_ISAVAILABLEINMDX_TO_TRUE_ON_NECESSARY_COLUMNS",
"Name": "[Error Prevention] Set IsAvailableInMdx to true on necessary columns",
"Category": "Error Prevention",
"Description": "In order to avoid errors, ensure that attribute hierarchies are enabled if a column is used for sorting another column, used in a hierarchy, used in variations, or is sorted by another column.",
"Severity": 3,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "IsAvailableInMDX = false\r\n\r\nand\r\n(\r\nUsedInSortBy.Any()\r\nor\r\nUsedInHierarchies.Any()\r\nor\r\nUsedInVariations.Any()\r\nor\r\nSortByColumn != null\r\n)",
"FixExpression": "IsAvailableInMDX = true",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "UNNECESSARY_COLUMNS",
"Name": "[Maintenance] Remove unnecessary columns",
"Category": "Maintenance",
"Description": "Hidden columns that are not referenced by any DAX expressions, relationships, hierarchy levels or Sort By-properties should be removed.",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "(IsHidden or Table.IsHidden)\n\n\r\nand ReferencedBy.Count = 0\r\n\n\nand (not UsedInRelationships.Any())\n\n\r\nand (not UsedInSortBy.Any())\n\n\r\nand (not UsedInHierarchies.Any())\n\n\r\nand (not Table.RowLevelSecurity.Any(\nit <> null and it.IndexOf(\"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0\n))\n\n and (not Model.Roles.Any(RowLevelSecurity.Any(\nit <> null and \n(\nit.IndexOf(current.Table.Name + \"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0 or\n it.IndexOf(\"'\" + current.Table.Name + \"'[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0\n )\n)))\r\nand not (\r\nObjectLevelSecurity.Any(it.ToString() == \"None\"))\r\nand not (\r\nTable.ObjectLevelSecurity.Any(it.ToString() == \"None\"))",
"FixExpression": "Delete()",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "UNNECESSARY_MEASURES",
"Name": "[Maintenance] Remove unnecessary measures",
"Category": "Maintenance",
"Description": "Hidden measures that are not referenced by any DAX expressions should be removed for maintainability",
"Severity": 2,
"Scope": "Measure",
"Expression": "(Table.IsHidden or IsHidden) \r\nand ReferencedBy.Count = 0",
"FixExpression": "Delete()",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "FIX_REFERENTIAL_INTEGRITY_VIOLATIONS",
"Name": "[Maintenance] Fix referential integrity violations",
"Category": "Maintenance",
"Description": "This rule highlights relationships which have referential integrity violations. This indicates that there are values in the table on the 'from' side of the relationship which do not exist in the table on the 'to' side of the relationship. Referential integrity violations will also produce the 'blank' member value in slicers. It is recommended to fix these issues by ensuring that the 'to' table's primary key column has all the values in the 'from' table's foreign key column.\r\n\r\nReference: https://blog.enterprisedna.co/vertipaq-analyzer-tutorial-relationships-referential-integrity/",
"Severity": 2,
"Scope": "Relationship",
"Expression": "Convert.ToInt64(GetAnnotation(\"Vertipaq_RIViolationInvalidRows\")) > 0",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REMOVE_DATA_SOURCES_NOT_REFERENCED_BY_ANY_PARTITIONS",
"Name": "[Maintenance] Remove data sources not referenced by any partitions",
"Category": "Maintenance",
"Description": "Data sources which are not referenced by any partitions may be removed.",
"Severity": 1,
"Scope": "ProviderDataSource, StructuredDataSource",
"Expression": "UsedByPartitions.Count() == 0\r\nand not Model.Tables.Any(SourceExpression.Contains(OuterIt.Name))\r\nand not Model.AllPartitions.Any(Query.Contains(OuterIt.Name))",
"FixExpression": "Delete()",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "REMOVE_ROLES_WITH_NO_MEMBERS",
"Name": "[Maintenance] Remove roles with no members",
"Category": "Maintenance",
"Description": "May remove roles with no members.",
"Severity": 1,
"Scope": "ModelRole",
"Expression": "Members.Count() == 0",
"FixExpression": "Delete()",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "ENSURE_TABLES_HAVE_RELATIONSHIPS",
"Name": "[Maintenance] Ensure tables have relationships",
"Category": "Maintenance",
"Description": "This rule highlights tables which are not connected to any other table in the model with a relationship.",
"Severity": 1,
"Scope": "Table, CalculatedTable",
"Expression": "UsedInRelationships.Count() == 0",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "OBJECTS_WITH_NO_DESCRIPTION",
"Name": "[Maintenance] Visible objects with no description",
"Category": "Maintenance",
"Description": "Add descriptions to objects. These descriptions are shown on hover within the Field List in Power BI Desktop. Additionally, you can leverage these descriptions to create an automated data dictionary (see link below).\r\nReference: https://www.elegantbi.com/post/datadictionary",
"Severity": 1,
"Scope": "Table, Measure, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup",
"Expression": "string.IsNullOrWhitespace(Description)\r\nand\r\nIsHidden == false",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "PERSPECTIVES_WITH_NO_OBJECTS",
"Name": "[Maintenance] Perspectives with no objects",
"Category": "Maintenance",
"Description": "Perspectives that contain no objects (tables) are most likely not necessary. In this rule, it is only necessary to check tables as adding a column/measure/hierarchy to a perspective also adds the table to the perspective. Additionally, tables in general covers calculated tables and calculation groups as well.",
"Severity": 1,
"Scope": "Perspective",
"Expression": "Model.Tables.Any(InPerspective[current.Name]) == false",
"FixExpression": "Delete()",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "CALCULATION_GROUPS_WITH_NO_CALCULATION_ITEMS",
"Name": "[Maintenance] Calculation groups with no calculation items",
"Category": "Maintenance",
"Description": "Calculation groups have no function unless they have calculation items.",
"Severity": 2,
"Scope": "CalculationGroup",
"Expression": "CalculationItems.Count == 0",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "PARTITION_NAME_SHOULD_MATCH_TABLE_NAME_FOR_SINGLE_PARTITION_TABLES",
"Name": "[Naming Conventions] Partition name should match table name for single partition tables",
"Category": "Naming Conventions",
"Description": "Tables with just one partition should match their table and partition names.Tables with more than one partition should have each partition name starting with the table name.",
"Severity": 1,
"Scope": "Table",
"Expression": "(Partitions.Count = 1 and Partitions[0].Name <> Name)",
"FixExpression": "Partitions[0].Name = it.Name",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "SPECIAL_CHARS_IN_OBJECT_NAMES",
"Name": "[Naming Conventions] Object names must not contain special characters",
"Category": "Naming Conventions",
"Description": "Tabs, line breaks, etc.",
"Severity": 2,
"Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup, CalculationItem",
"Expression": "Name.IndexOf(char(9)) > -1\r\nor\r\n\nName.IndexOf(char(10)) > -1 \r\nor\r\n\nName.IndexOf(char(13)) > -1",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "TRIM_OBJECT_NAMES",
"Name": "[Naming Conventions] Trim object names",
"Category": "Naming Conventions",
"Description": "Unintentionally leaving a trailing space in an object name is a common occurrence when copying/duplicating objects in Tabular Editor.",
"Severity": 1,
"Scope": "Model, Table, Measure, Hierarchy, Level, Perspective, Partition, ProviderDataSource, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, StructuredDataSource, NamedExpression, ModelRole, CalculationGroup, CalculationItem",
"Expression": "Name.StartsWith(\" \") or Name.EndsWith(\" \")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "FORMAT_FLAG_COLUMNS_AS_YES/NO_VALUE_STRINGS",
"Name": "[Formatting] Format flag columns as Yes/No value strings",
"Category": "Formatting",
"Description": "Flags must be properly formatted as Yes/No as this is easier to read than using 0/1 integer values.",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "(\nName.StartsWith(\"Is\") and \nDataType = \"Int64\" and \nnot (IsHidden or Table.IsHidden)\n) \r\nor\r\n\n(\nName.EndsWith(\" Flag\") and \nDataType <> \"String\" and \nnot (IsHidden or Table.IsHidden)\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "OBJECTS_SHOULD_NOT_START_OR_END_WITH_A_SPACE",
"Name": "[Formatting] Objects should not start or end with a space",
"Category": "Formatting",
"Description": "Objects should not start or end with a space",
"Severity": 3,
"Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn",
"Expression": "Name.StartsWith(\" \") or Name.EndsWith(\" \")",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "DATECOLUMN_FORMATSTRING",
"Name": "[Formatting] Provide format string for \"Date\" columns",
"Category": "Formatting",
"Description": "Columns of type \"DateTime\" that have \"Month\" in their names should be formatted as \"mm/dd/yyyy\".",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "Name.IndexOf(\"Date\", \"OrdinalIgnoreCase\") >= 0 \r\nand \r\nDataType = \"DateTime\" \r\nand \r\nFormatString <> \"mm/dd/yyyy\"",
"FixExpression": "FormatString = \"mm/dd/yyyy\"",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MONTHCOLUMN_FORMATSTRING",
"Name": "[Formatting] Provide format string for \"Month\" columns",
"Category": "Formatting",
"Description": "Columns of type \"DateTime\" that have \"Month\" in their names should be formatted as \"MMMM yyyy\".",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "Name.IndexOf(\"Month\", \"OrdinalIgnoreCase\") >= 0 and DataType = \"DateTime\" and FormatString <> \"MMMM yyyy\"",
"FixExpression": "FormatString = \"MMMM yyyy\"",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "PROVIDE_FORMAT_STRING_FOR_MEASURES",
"Name": "[Formatting] Provide format string for measures",
"Category": "Formatting",
"Description": "Visible measures should have their format string property assigned",
"Severity": 2,
"Scope": "Measure",
"Expression": "not IsHidden \r\nand not Table.IsHidden \r\nand string.IsNullOrWhitespace(FormatString)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "NUMERIC_COLUMN_SUMMARIZE_BY",
"Name": "[Formatting] Do not summarize numeric columns",
"Category": "Formatting",
"Description": "Numeric columns (integer, decimal, double) should have their SummarizeBy property set to \"None\" to avoid accidental summation in Power BI (create measures instead).",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "(\r\nDataType = \"Int64\"\r\nor \r\nDataType=\"Decimal\" \r\nor \r\nDataType=\"Double\"\r\n)\n\r\nand \r\nSummarizeBy <> \"None\"\r\n\nand not (IsHidden or Table.IsHidden)",
"FixExpression": "SummarizeBy = AggregateFunction.None",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "PERCENTAGE_FORMATTING",
"Name": "[Formatting] Percentages should be formatted with thousands separators and 1 decimal",
"Category": "Formatting",
"Description": null,
"Severity": 2,
"Scope": "Measure",
"Expression": "FormatString.Contains(\"%\") and FormatString <> \"#,0.0%;-#,0.0%;#,0.0%\"",
"FixExpression": "FormatString = \"#,0.0%\\u003B-#,0.0%\\u003B#,0.0%\"",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "INTEGER_FORMATTING",
"Name": "[Formatting] Whole numbers should be formatted with thousands separators and no decimals",
"Category": "Formatting",
"Description": null,
"Severity": 2,
"Scope": "Measure",
"Expression": "not FormatString.Contains(\"$\") and not FormatString.Contains(\"%\") and not (FormatString = \"#,0\" or FormatString = \"#,0.0\")",
"FixExpression": "FormatString = \"#,0\"",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "RELATIONSHIP_COLUMNS_SHOULD_BE_OF_INTEGER_DATA_TYPE",
"Name": "[Formatting] Relationship columns should be of integer data type",
"Category": "Formatting",
"Description": "It is a best practice for relationship columns to be of integer data type. This applies not only to data warehousing but data modeling as well.",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any()\n\nand \n\nDataType != DataType.Int64",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "ADD_DATA_CATEGORY_FOR_COLUMNS",
"Name": "[Formatting] Add data category for columns",
"Category": "Formatting",
"Description": "Add Data Category property for appropriate columns.\r\n\r\nReference: https://docs.microsoft.com/power-bi/transform-model/desktop-data-categorization",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "string.IsNullOrWhitespace(DataCategory)\r\nand\r\n(\r\n(\r\nName.ToLower().Contains(\"country\")\r\nor \r\n\nName.ToLower().Contains(\"continent\"\n)\r\nor\r\nName.ToLower().Contains(\"city\")\r\n)\r\nand DataType == \"String\"\r\n)\r\nor \r\n(\r\n(\nName.ToLower() == \"latitude\" \n or \nName.ToLower() == \"longitude\")\r\nand (DataType == DataType.Decimal or DataType == DataType.Double)\r\n)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "HIDE_FOREIGN_KEYS",
"Name": "[Formatting] Hide foreign keys",
"Category": "Formatting",
"Description": "Foreign keys should always be hidden.",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any(FromColumn.Name == current.Name and FromCardinality == \"Many\")\n\r\nand\r\n\nIsHidden == false",
"FixExpression": "IsHidden = true",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MARK_PRIMARY_KEYS",
"Name": "[Formatting] Mark primary keys",
"Category": "Formatting",
"Description": "Set the 'Key' property to 'True' for primary key columns within the column properties.",
"Severity": 1,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any(ToTable.Name == current.Table.Name and ToColumn.Name == current.Name and ToCardinality == \"One\")\r\n\nand\r\n\nIsKey == false\r\nand\r\ncurrent.Table.DataCategory != \"Time\"",
"FixExpression": "IsKey = true",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "HIDE_FACT_TABLE_COLUMNS",
"Name": "[Formatting] Hide fact table columns",
"Category": "Formatting",
"Description": "It is a best practice to hide fact table columns that are used for aggregation in measures.",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "(\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)COUNT\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)COUNTBLANK\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)SUM\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)AVERAGE\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)VALUES\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)DISTINCT\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)DISTINCTCOUNT\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\n\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)MIN\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\n\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)MAX\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)COUNTA\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\n\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)AVERAGEA\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)MAXA\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n\nor\r\nReferencedBy.AllMeasures.Any(RegEx.IsMatch(Expression,\"(?i)MINA\\s*\\(\\s*\\'*\" + outerit.Table.Name + \"\\'*\\[\" + outerit.Name + \"\\]\\s*\\)\"))\r\n)\r\n\nand IsHidden == false\r\n\nand (DataType == \"Int64\" || DataType == \"Decimal\" || DataType == \"Double\")",
"FixExpression": "IsHidden = true",
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "FIRST_LETTER_OF_OBJECTS_MUST_BE_CAPITALIZED",
"Name": "[Formatting] First letter of objects must be capitalized",
"Category": "Formatting",
"Description": null,
"Severity": 1,
"Scope": "Table, Measure, Hierarchy, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup",
"Expression": "Name.Substring(0,1).ToUpper() != Name.Substring(0,1)",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
},
{
"ID": "MONTH_(AS_A_STRING)_MUST_BE_SORTED",
"Name": "[Formatting] Month (as a string) must be sorted",
"Category": "Formatting",
"Description": "This rule highlights month columns which are strings and are not sorted. If left unsorted, they will sort alphabetically (i.e. April, August...). Make sure to sort such columns so that they sort properly (January, February, March...).",
"Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "Name.ToUpper().Contains(\"MONTH\")\r\nand\r\n! Name.ToUpper().Contains(\"MONTHS\") \r\nand \r\n\n\nDataType == DataType.String \r\nand \r\nSortByColumn == null",
"FixExpression": null,
"CompatibilityLevel": 1200,
"ObjectCount": 0,
"ErrorMessage": null
}
]