From 934af6cf48982314e9749e172dce0d3dd81301a7 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 26 Jul 2021 16:17:55 +0300 Subject: [PATCH 01/23] v1.2 --- BestPracticeRules/BPARules.json | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/BestPracticeRules/BPARules.json b/BestPracticeRules/BPARules.json index e255276..01aaf45 100644 --- a/BestPracticeRules/BPARules.json +++ b/BestPracticeRules/BPARules.json @@ -38,7 +38,7 @@ "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.ToInt32(GetAnnotation(\"LongLengthRowCount\")) > 500000", + "Expression": "Convert.ToInt64(GetAnnotation(\"LongLengthRowCount\")) > 500000", "CompatibilityLevel": 1200 }, { @@ -68,7 +68,7 @@ "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.Replace(\" \",\"\"),\"\\W+(?i)RELATED\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)RELATED\\(\")", + "Expression": "RegEx.IsMatch(Expression,\"(?i)RELATED\\s*\\(\")", "CompatibilityLevel": 1200 }, { @@ -128,7 +128,7 @@ "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(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)RIGHT\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)RIGHT\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)LEFT\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)LEFT\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)UPPER\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)UPPER\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)LOWER\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)LOWER\\(\"))\r\nor\r\nRowLevelSecurity.Any(\nRegEx.IsMatch(it.Replace(\" \",\"\"),\"\\W+(?i)FIND\\(\"))\r\nor\r\nRowLevelSecurity.Any(RegEx.IsMatch(it.Replace(\" \",\"\"),\"^(?i)FIND\\(\"))", + "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", "CompatibilityLevel": 1200 }, { @@ -196,7 +196,7 @@ "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\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"CLOSINGBALANCEMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"CLOSINGBALANCEQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"CLOSINGBALANCEYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATEADD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESBETWEEN(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESINPERIOD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESMTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESQTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"DATESYTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"ENDOFMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"ENDOFQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"ENDOFYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"FIRSTDATE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"FIRSTNONBLANK(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"FIRSTNONBLANKVALUE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"LASTDATE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"LASTNONBLANK(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"LASTNONBLANKVALUE(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTDAY(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"NEXTYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"OPENINGBALANCEMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"OPENINGBALANCEQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"OPENINGBALANCEYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PARALLELPERIOD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSDAY(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"PREVIOUSYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"SAMEPERIODLASTYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"STARTOFMONTH(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"STARTOFQUARTER(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"STARTOFYEAR(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"TOTALMTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"TOTALQTD(\")\r\nor\r\nExpression.ToUpper().Replace(\" \",\"\").Contains(\"TOTALYTD(\")\r\n)", + "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)", "CompatibilityLevel": 1200 }, { @@ -220,13 +220,13 @@ "CompatibilityLevel": 1200 }, { - "ID": "CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)__IS_NECESSARY", - "Name": "[Performance] Check if dynamic row level security (RLS) is necessary", + "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/en-us/power-bi/admin/service-admin-rls", "Severity": 1, "Scope": "Table, CalculatedTable", - "Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)USERPRINCIPALNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)USERPRINCIPALNAME\\(\")", + "Expression": "RegEx.IsMatch(Expression,\"(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)USERPRINCIPALNAME\\(\")", "CompatibilityLevel": 1200 }, { @@ -235,7 +235,7 @@ "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": 3, - "Scope": "Measure, KPI, CalculationItem", + "Scope": "Measure, KPI, TablePermission, CalculationItem", "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", "CompatibilityLevel": 1200 }, @@ -348,6 +348,16 @@ "Expression": "string.IsNullOrWhiteSpace(Expression)", "CompatibilityLevel": 1200 }, + { + "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\"", + "CompatibilityLevel": 1200 + }, { "ID": "UNNECESSARY_COLUMNS", "Name": "[Maintenance] Remove unnecessary columns", @@ -551,10 +561,10 @@ "ID": "ADD_DATA_CATEGORY_FOR_COLUMNS", "Name": "[Formatting] Add data category for columns", "Category": "Formatting", - "Description": "Add Data Category property for appropriate columns. \r\nReference: https://docs.microsoft.com/en-us/power-bi/transform-model/desktop-data-categorization", + "Description": "Add Data Category property for appropriate columns.\r\n\r\nReference: https://docs.microsoft.com/en-us/power-bi/transform-model/desktop-data-categorization", "Severity": 1, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", - "Expression": "Name == \"Country\" \r\nor \r\n\nName == \"Continent\"\n \r\nor ((\nName == \"Latitude\" \n or \nName == \"Longitude\") and (DataType == DataType.Decimal or DataType == DataType.Double))", + "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)", "CompatibilityLevel": 1200 }, { @@ -586,7 +596,7 @@ "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": "(ReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"COUNT(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"COUNTBLANK(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"SUM(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"AVERAGE(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"VALUES(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"DISTINCT(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"DISTINCTCOUNT(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MIN(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MAX(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"COUNTA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"AVERAGEA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MAXA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\nor\nReferencedBy.AllMeasures.Any(Expression.Replace(\" \",\"\").ToUpper().Contains(\"MINA(\"+current.DaxObjectFullName.Replace(\" \",\"\").ToUpper()))\n)\nand IsHidden == false\nand (DataType == \"Int64\" || DataType == \"Decimal\" || DataType == \"Double\")", + "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 }, From 40ab19570c1420476780a7d60ade03ceb58b8767 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 26 Jul 2021 16:22:21 +0300 Subject: [PATCH 02/23] Update README.md --- BestPracticeRules/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 62f139c..a30d47d 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -82,6 +82,29 @@ w.DownloadFile(url, downloadLoc); ## Version History +* 2021-07-26 Version 1.2 + * New Rules + * [Error Prevention] Avoid structured data sources with provider partitions + * Modified Rules + * [DAX Expressions] Column references should be fully qualified + * Scope: Added 'Table Permissions' + * [Formatting] Hide fact table columns + * Simplified rule logic + * [Performance] Check if dynamic row level security (RLS) is necessary + * Simplified rule logic + * [Performance] Limit row level security (RLS) logic + * Simplified rule logic + * [Formatting] Add data category for columns + * Fixed rule logic + * [Performance] Measures using time intelligence and model is using Direct Query + * Simplified rule logic + * [Performance] Reduce usage of calculated columns that use the RELATED function + * Simplified rule logic + * [Performance] Reduce usage of long-length columns with high cardinality + * Updated rule logic to use Int64 + +![image](https://user-images.githubusercontent.com/29556918/126995487-981282dd-e3e1-4b3b-b825-ecf008535cb7.png) + * 2021-06-13 Version 1.1.2 * Modified Rules * [DAX Expressions] Use the DIVIDE function for division From 2a494f31137635a78b532455a1cb923a1ed584fe Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 26 Jul 2021 16:22:46 +0300 Subject: [PATCH 03/23] Update README.md --- BestPracticeRules/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index a30d47d..662977f 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -103,8 +103,6 @@ w.DownloadFile(url, downloadLoc); * [Performance] Reduce usage of long-length columns with high cardinality * Updated rule logic to use Int64 -![image](https://user-images.githubusercontent.com/29556918/126995487-981282dd-e3e1-4b3b-b825-ecf008535cb7.png) - * 2021-06-13 Version 1.1.2 * Modified Rules * [DAX Expressions] Use the DIVIDE function for division From 21f43409f67763fe7879b7ff6591ef864c92ca4e Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Tue, 27 Jul 2021 14:06:37 +0300 Subject: [PATCH 04/23] Update README.md --- BestPracticeRules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 662977f..9221685 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -84,7 +84,7 @@ w.DownloadFile(url, downloadLoc); * 2021-07-26 Version 1.2 * New Rules - * [Error Prevention] Avoid structured data sources with provider partitions + * [Error Prevention] Avoid structured data sources with provider partitions ([blog post](https://www.elegantbi.com/post/convertdatasources)) * Modified Rules * [DAX Expressions] Column references should be fully qualified * Scope: Added 'Table Permissions' From 4a78d409eaafed5076c5d3454e947af6fa2d4699 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Tue, 27 Jul 2021 17:02:04 +0300 Subject: [PATCH 05/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5420a9f..150a415 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Sample U-SQL scripts that demonstrate how to process a TPC-DS data set in Azure Python script to reassemble job graph events from Analysis Services. ## [Best Practice Rules](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules) -A curated set of rules covering best practices for tabular model performance and design which can be run from [Tabular Editor's](https://tabulareditor.com/ "Tabular Editor") [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). +A curated set of rules covering best practices for tabular model performance and design which can be run from [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). ## [Metadata Translator](https://github.com/microsoft/Analysis-Services/tree/master/MetadataTranslator) Metadata Translator can translate the names, descriptions, and display folders of the metadata objects in a semantic model by using Azure Cognitive Services. From d0b5d3d84a3af1492f18ddc4d2b07073e7d50944 Mon Sep 17 00:00:00 2001 From: Kay Unkroth Date: Mon, 2 Aug 2021 13:31:07 -0700 Subject: [PATCH 06/23] Small sample code for DAX REST API. --- BlogArticleUploads/DAX Rest API Program.cs | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 BlogArticleUploads/DAX Rest API Program.cs diff --git a/BlogArticleUploads/DAX Rest API Program.cs b/BlogArticleUploads/DAX Rest API Program.cs new file mode 100644 index 0000000..32af3c3 --- /dev/null +++ b/BlogArticleUploads/DAX Rest API Program.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Web.Script.Serialization; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace ConsoleApp1 +{ + public class QueryRequest + { + public class Query + { + public string query { get; set; } + } + + public List queries { get; set; } + + public QueryRequest() + { + } + + public QueryRequest(string daxQuery) + { + queries = new List { new Query { query = daxQuery } }; + } + } + + public class ParsedResponse + { + public class Result + { + public class Table + { + public List> rows { get; set; } + } + public List tables { get; set; } + } + public List results { get; set; } + } + + class Program + { + /// + /// Please fill in the application parameters. + /// + private static string clientId = ""; + private static string tenantID = ""; + private static string replyUrl = ""; + private static string resourceID = "https://analysis.windows.net/powerbi/api"; + + /// + /// Please provide the DAX Rest API parameters. + /// + private static Guid datasetId = new Guid(""); + private static string daxQuery = @""; + + static void Main(string[] args) + { + string authToken = GetToken().Result; + + + string jsonResponse; + if (QueryDataset(datasetId, daxQuery, authToken, out jsonResponse)) + { + var queryResponse = new JavaScriptSerializer().Deserialize(jsonResponse); + foreach (var row in queryResponse.results[0].tables[0].rows) + { + foreach (var keyValuePair in row) + { + Console.WriteLine($"{keyValuePair.Key}:{keyValuePair.Value}"); + } + Console.WriteLine(); + } + } + else + { + Console.WriteLine("The Web request did not succeed."); + } + + Console.WriteLine("Press [Enter] to exit the console app..."); + Console.ReadLine(); + } + + private static async Task GetToken() + { + IPublicClientApplication PublicClientApp = PublicClientApplicationBuilder.Create(clientId) + .WithRedirectUri(replyUrl) + .WithAuthority(AzureCloudInstance.AzurePublic, tenantID) + .Build(); + AuthenticationResult authResult = await PublicClientApp.AcquireTokenInteractive(scopes: new[] { resourceID + "/Dataset.Read.All" }) + .ExecuteAsync(); + + return authResult.AccessToken; + } + + static bool QueryDataset(Guid datasetId, string daxQuery, string authToken, out string jsonResponse) + { + + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", authToken); + + string url = $"https://api.powerbi.com/v1.0/myorg/datasets/{datasetId}/executeQueries"; + + var requestBody = new JavaScriptSerializer().Serialize( + new QueryRequest(daxQuery) + ); + + + var response = client.PostAsync(url, new StringContent(requestBody, UnicodeEncoding.UTF8, "application/json")).Result; + jsonResponse = response.Content.ReadAsStringAsync().Result; + + return response.IsSuccessStatusCode; + } + } +} From 8029e55c70df57fae8ae90626ac39ab919f1fa15 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Wed, 18 Aug 2021 14:54:55 +0300 Subject: [PATCH 07/23] Update README.md --- BestPracticeRules/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 9221685..c6533b0 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -65,6 +65,7 @@ w.DownloadFile(url, downloadLoc); * Large tables should be partitioned * * Reduce usage of long-length columns with high cardinality *^ * Split date and time *** + * Fix referential integrity violations * *These rules use [Vertipaq Analyzer](https://www.sqlbi.com/tools/vertipaq-analyzer/) data. There are 2 methods to load this data into Tabular Editor: @@ -82,6 +83,9 @@ w.DownloadFile(url, downloadLoc); ## Version History +* 2021-08-18 Version 1.2.1 + * New Rules + * [Maintenance] Fix referential integrity violations ([blog post](https://www.elegantbi.com/post/findblankrows)) * 2021-07-26 Version 1.2 * New Rules * [Error Prevention] Avoid structured data sources with provider partitions ([blog post](https://www.elegantbi.com/post/convertdatasources)) From 413ac88dc6bf5662a14976e63633cb5fb574bf0c Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Wed, 18 Aug 2021 14:55:22 +0300 Subject: [PATCH 08/23] v1.2.1 --- BestPracticeRules/BPARules.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/BestPracticeRules/BPARules.json b/BestPracticeRules/BPARules.json index 01aaf45..a5a3923 100644 --- a/BestPracticeRules/BPARules.json +++ b/BestPracticeRules/BPARules.json @@ -17,7 +17,7 @@ "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()", + "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()", "FixExpression": "IsAvailableInMDX = false", "CompatibilityLevel": 1200 }, @@ -365,7 +365,7 @@ "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\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not Table.RowLevelSecurity.Any(\n it <> null and \n it.IndexOf(\"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0\n))\n\nand (not Model.Roles.Any(RowLevelSecurity.Any(\n it <> null and \n (\n it.IndexOf(current.Table.Name + \"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0 or\n it.IndexOf(\"'\" + current.Table.Name + \"'[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") >= 0\n )\n)))", + "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)))", "FixExpression": "Delete()", "CompatibilityLevel": 1200 }, @@ -376,10 +376,20 @@ "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) and ReferencedBy.Count = 0", + "Expression": "(Table.IsHidden or IsHidden) \r\nand ReferencedBy.Count = 0", "FixExpression": "Delete()", "CompatibilityLevel": 1200 }, + { + "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", + "CompatibilityLevel": 1200 + }, { "ID": "REMOVE_DATA_SOURCES_NOT_REFERENCED_BY_ANY_PARTITIONS", "Name": "[Maintenance] Remove data sources not referenced by any partitions", From ca1937ec22212821efa4ce23603e57f8a76c815e Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 30 Aug 2021 18:55:09 +0300 Subject: [PATCH 09/23] Update README.md --- BestPracticeRules/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index c6533b0..10e08a5 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -6,7 +6,11 @@ And, check out the new [PowerBI.com blog post on v1.1](https://powerbi.microsoft Also, check out this [post](https://www.elegantbi.com/post/bestpracticerulesavings "Best Practice Rule Savings") for quantifying the savings of following specific Best Practice Rules. -Lastly, check out this [video](https://www.youtube.com/watch?v=5pu9FoTUpys) for an in-depth walkthrough of the Best Practice Rules and [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). +Lastly, check out the video below for an in-depth walkthrough of the Best Practice Rules and [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). + + ## Purpose From 35e3d6d63434762cb0bb016861d1c37d7fc325ab Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 30 Aug 2021 19:37:44 +0300 Subject: [PATCH 10/23] Update README.md --- BestPracticeRules/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 10e08a5..9bf4f8d 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -4,7 +4,10 @@ Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsof And, check out the new [PowerBI.com blog post on v1.1](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/, "PowerBI.com blog post"). -Also, check out this [post](https://www.elegantbi.com/post/bestpracticerulesavings "Best Practice Rule Savings") for quantifying the savings of following specific Best Practice Rules. +Also, check out this post for quantifying the savings of following specific Best Practice Rules. + +[![image](https://user-images.githubusercontent.com/29556918/131373174-19f31ecb-67b3-4515-83be-f6687d442053.jpg)](https://www.elegantbi.com/post/bestpracticerulesavings) + Lastly, check out the video below for an in-depth walkthrough of the Best Practice Rules and [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). From 3dca28d793f3969770aa575d3f11b4c20560af91 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 6 Sep 2021 13:34:22 +0300 Subject: [PATCH 11/23] Update README.md --- BestPracticeRules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 9bf4f8d..d1f86fa 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -27,7 +27,7 @@ If you find any issues or have any requests for new rules, please [submit an iss ## Setup (automated) -Following these steps will automatically load the Best Practice Rules into your local Tabular Editor. Note that this will overwrite the existing BPARules.json file (if you are already have one). +Following these steps will automatically load the Best Practice Rules into your local Tabular Editor. Note that this will overwrite the existing BPARules.json file (if you are already have one) so be sure to back up your existing rules file. 1. Open [Tabular Editor](https://tabulareditor.com/ "Tabular Editor"). 2. Connect to a model. From ca21b1f8710b43d0c6abc3e557426e4256ad5195 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 6 Sep 2021 13:35:04 +0300 Subject: [PATCH 12/23] Update README.md --- BestPracticeRules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index d1f86fa..a5eb01a 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -4,7 +4,7 @@ Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsof And, check out the new [PowerBI.com blog post on v1.1](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/, "PowerBI.com blog post"). -Also, check out this post for quantifying the savings of following specific Best Practice Rules. +Also, check out this to learn how to quantify the savings of following specific Best Practice Rules. [![image](https://user-images.githubusercontent.com/29556918/131373174-19f31ecb-67b3-4515-83be-f6687d442053.jpg)](https://www.elegantbi.com/post/bestpracticerulesavings) From fa1ab915fb7b02bf56addabcd19e61f74ea2b9e6 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 6 Sep 2021 13:35:25 +0300 Subject: [PATCH 13/23] Update README.md --- BestPracticeRules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index a5eb01a..e56324f 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -4,7 +4,7 @@ Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsof And, check out the new [PowerBI.com blog post on v1.1](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/, "PowerBI.com blog post"). -Also, check out this to learn how to quantify the savings of following specific Best Practice Rules. +Also, check out this blog post to learn how to quantify the savings of following specific Best Practice Rules. [![image](https://user-images.githubusercontent.com/29556918/131373174-19f31ecb-67b3-4515-83be-f6687d442053.jpg)](https://www.elegantbi.com/post/bestpracticerulesavings) From 5c9997b0038cd159d36c84db5d528175ffb8f602 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Mon, 6 Sep 2021 13:52:16 +0300 Subject: [PATCH 14/23] Update README.md --- BestPracticeRules/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index e56324f..8c3cd2d 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -8,7 +8,6 @@ Also, check out this blog post to learn how to quantify the savings of following [![image](https://user-images.githubusercontent.com/29556918/131373174-19f31ecb-67b3-4515-83be-f6687d442053.jpg)](https://www.elegantbi.com/post/bestpracticerulesavings) - Lastly, check out the video below for an in-depth walkthrough of the Best Practice Rules and [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). 'Manage BPA Rules...' -> 'Rules for the local user' -> Click on a rule -> Click 'Edit rule...'. + +RuleDescription35 + ## Setup (automated) Following these steps will automatically load the Best Practice Rules into your local Tabular Editor. Note that this will overwrite the existing BPARules.json file (if you are already have one) so be sure to back up your existing rules file. From 6023552eb66f9dd2c840cd1bc076dd4423fd0c99 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Sun, 10 Oct 2021 11:35:29 +0300 Subject: [PATCH 15/23] Update README.md --- BestPracticeRules/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 8c3cd2d..86f7b55 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -2,12 +2,16 @@ Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance/ "PowerBI.com blog post") on this topic! -And, check out the new [PowerBI.com blog post on v1.1](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/, "PowerBI.com blog post"). +Check out the [PowerBI.com blog post on v1.1](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/, "PowerBI.com blog post"). -Also, check out this blog post to learn how to quantify the savings of following specific Best Practice Rules. +Check out this blog post to learn how to quantify the savings of following specific Best Practice Rules. [![image](https://user-images.githubusercontent.com/29556918/131373174-19f31ecb-67b3-4515-83be-f6687d442053.jpg)](https://www.elegantbi.com/post/bestpracticerulesavings) +Check out this blog post to learn how to export the Best Practice Analyzer results into Excel. + +[![image](https://user-images.githubusercontent.com/29556918/136688637-f638405e-0bda-462d-8d95-00a0037000c6.jpg)](https://www.elegantbi.com/post/exportbparesults) + Lastly, check out the video below for an in-depth walkthrough of the Best Practice Rules and [Tabular Editor](https://tabulareditor.com/ "Tabular Editor")'s [Best Practice Analyzer](https://docs.tabulareditor.com/Best-Practice-Analyzer.html "Best Practice Analyzer"). 100000", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_USAGE_OF_LONG-LENGTH_COLUMNS_WITH_HIGH_CARDINALITY", + "Name": "[Prestazioni] Ridurre l'uso di colonne con stringhe lunghe e con elevata cardinalità", + "Category": "Prestazioni", + "Description": "È meglio evitare lunghe colonne di testo. Ciò è particolarmente vero se la colonna ha molti valori univoci. Questi tipi di colonne possono causare tempi di elaborazione più lunghi, dimensioni del modello eccessive e query utente più lente. La lunghezza lunga è definita come più di 100 caratteri.", + "Severity": 2, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Convert.ToInt64(GetAnnotation(\"LongLengthRowCount\")) > 500000", + "CompatibilityLevel": 1200 + }, + { + "ID": "SPLIT_DATE_AND_TIME", + "Name": "[Prestazioni] Dividi data e ora in colonne separate", + "Category": "Prestazioni", + "Description": "Questa regola trova le colonne datetime che hanno valori non a mezzanotte (00:00:00). Per massimizzare le prestazioni, l'elemento ora dovrebbe essere diviso dall'elemento data (oppure il componente ora dovrebbe essere arrotondato a mezzanotte in quanto ciò ridurrà la cardinalità della colonna). Riferimento: 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", + "CompatibilityLevel": 1200 + }, + { + "ID": "LARGE_TABLES_SHOULD_BE_PARTITIONED", + "Name": "[Prestazioni] Tabelle grandi dovrebbero partizionate", + "Category": "Prestazioni", + "Description": "Le tabelle di grandi dimensioni devono essere partizionate per ottimizzare l'elaborazione. Affinché questa regola venga eseguita correttamente, è necessario eseguire lo script mostrato qui: https://www.elegantbi.com/post/vertipaqintabulareditor", + "Severity": 2, + "Scope": "Table", + "Expression": "Convert.ToInt64(GetAnnotation(\"Vertipaq_RowCount\")) > 25000000\r\nand\r\nPartitions.Count = 1", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_USAGE_OF_CALCULATED_COLUMNS_THAT_USE_THE_RELATED_FUNCTION", + "Name": "[Prestazioni] Ridurre l'utilizzo di colonne calcolate che usano la funzione RELATED", + "Category": "Prestazioni", + "Description": "Le colonne calcolate non si comprimono così come le colonne di dati native e possono causare tempi di elaborazione più lunghi. Pertanto, le colonne calcolate dovrebbero essere evitate se possibile. Uno scenario in cui potrebbero essere più facili da evitare è se utilizzano la funzione RELATED. Riferimento: https://www.sqlbi.com/articles/storage-differences-between-calculated-columns-and-calculated-tables/", + "Severity": 2, + "Scope": "CalculatedColumn", + "Expression": "RegEx.IsMatch(Expression,\"(?i)RELATED\\s*\\(\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "SNOWFLAKE_SCHEMA_ARCHITECTURE", + "Name": "[Prestazioni] Considerare prima uno Star-Schema invece di un'architettura Snowflake", + "Category": "Prestazioni", + "Description": "In generale, uno schema a stella è l'architettura ottimale per i modelli tabulari. Stando così le cose, ci sono casi validi per utilizzare un approccio a fiocco di neve. Controlla il tuo modello e considera il passaggio a un'architettura con schema a stella. Riferimento: https://docs.microsoft.com/en-us/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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "MODEL_SHOULD_HAVE_A_DATE_TABLE", + "Name": "[Prestazioni] Il modello dovrebbe avere una tabella contrassegnate con la proprietà Date Table", + "Category": "Prestazioni", + "Description": "In generale, i modelli dovrebbero generalmente avere una tabella delle date. I modelli che non dispongono di una tabella delle date in genere non sfruttano funzionalità come l'intelligenza temporale o potrebbero non disporre di un'architettura adeguatamente strutturata.", + "Severity": 2, + "Scope": "Model", + "Expression": "Tables.Any(DataCategory == \"Time\" && Columns.Any(IsKey == true && DataType == \"DateTime\")) == false", + "CompatibilityLevel": 1200 + }, + { + "ID": "DATE/CALENDAR_TABLES_SHOULD_BE_MARKED_AS_A_DATE_TABLE", + "Name": "[Prestazioni] Le tabelle di tipo data/calendario dovrebbero essere contrassegnate come Date Table", + "Category": "Prestazioni", + "Description": "Questa regola cerca le tabelle che contengono le parole \"data\" o \"calendario\" poiché probabilmente dovrebbero essere contrassegnate come tabella di date. Riferimento: https://docs.microsoft.com/en-us/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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_AUTO-DATE_TABLE", + "Name": "[Prestazioni] Rimuovi le tabelle automatiche di data\\calendario (auto-date table)", + "Category": "Prestazioni", + "Description": "Evita di utilizzare tabelle di datazione automatica (Auto data-tables). Assicurati di disattivare la tabella di datazione automatica nelle impostazioni in Power BI Desktop. Ciò farà risparmiare risorse di memoria. Riferimento: 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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_EXCESSIVE_BI-DIRECTIONAL_OR_MANY-TO-MANY_RELATIONSHIPS", + "Name": "[Prestazioni] Evitare l'uso eccessivo di relazioni bidirezionali o molti-a-molti", + "Category": "Prestazioni", + "Description": "Limita l'uso delle relazioni b-direzionali e molti-a-molti. Questa regola contrassegna il modello se più del 30% delle relazioni è bi-direzionale o molti a molti. Riferimento: 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", + "CompatibilityLevel": 1200 + }, + { + "ID": "LIMIT_ROW_LEVEL_SECURITY_(RLS)_LOGIC", + "Name": "[Prestazioni] Limita le logiche di sicurezza a livello di riga (RLS)", + "Category": "Prestazioni", + "Description": "Prova a semplificare il DAX utilizzato per la sicurezza a livello di riga. L'utilizzo delle funzioni all'interno di questa regola può essere probabilmente scaricato sui sistemi upstream (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", + "CompatibilityLevel": 1200 + }, + { + "ID": "MODEL_USING_DIRECT_QUERY_AND_NO_AGGREGATIONS", + "Name": "[Prestazioni] Considerare l'utilizzo di aggregazioni se si utilizza Direct Query in Power BI", + "Category": "Prestazioni", + "Description": "Se usi Direct Query in Power BI Premium, potresti prendere in considerazione l'uso di aggregazioni per migliorare le prestazioni. Riferimento: https://docs.microsoft.com/en-us/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\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "MINIMIZE_POWER_QUERY_TRANSFORMATIONS", + "Name": "[Prestazioni] Minimizza l'utilizzo delle trasformazioni di Power Query", + "Category": "Prestazioni", + "Description": "Riduci al minimo le trasformazioni di Power Query per migliorare le prestazioni di elaborazione del modello. Se possibile, è consigliabile scaricare queste trasformazioni nel data warehouse. Inoltre, controlla se il raggruppamento delle query (Query Folding) si verifica all'interno del tuo modello. Fare riferimento all'articolo di seguito per ulteriori informazioni sul raggruppamento delle query. Riferimento: https://docs.microsoft.com/en-us/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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "UNPIVOT_PIVOTED_(MONTH)_DATA", + "Name": "[Prestazioni] Evitare di utilizzare i dati \"unpivottati\" nelle tabelle.", + "Category": "Prestazioni", + "Description": "Evita di utilizzare dati pivot nelle tabelle. Questa regola controlla specificamente i dati pivot per mese. Riferimento: 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))", + "CompatibilityLevel": 1200 + }, + { + "ID": "MANY-TO-MANY_RELATIONSHIPS_SHOULD_BE_SINGLE-DIRECTION", + "Name": "[Prestazioni] Le relazioni molti a molti dovrebbero essere monodirezionali", + "Category": "Prestazioni", + "Severity": 2, + "Scope": "Relationship", + "Expression": "FromCardinality == \"Many\"\nand\nToCardinality == \"Many\"\nand\nCrossFilteringBehavior == \"BothDirections\"", + "CompatibilityLevel": 1200, + "Description": "" + }, + { + "ID": "REDUCE_USAGE_OF_CALCULATED_TABLES", + "Name": "[Prestazioni] Ridurre l'utilizzo delle tabelle calcolate", + "Category": "Prestazioni", + "Description": "Migra la logica della tabella calcolata nel tuo data warehouse. Fare affidamento su tabelle calcolate porterà a debiti tecnici e potenziali disallineamenti se si dispone di più modelli sulla piattaforma.", + "Severity": 2, + "Scope": "CalculatedTable", + "Expression": "1=1", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_REDUNDANT_COLUMNS_IN_RELATED_TABLES", + "Name": "[Prestazioni] Rimuovi le colonne ridondanti nelle tabelle relazionate", + "Category": "Prestazioni", + "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))", + "CompatibilityLevel": 1200, + "Description": "" + }, + { + "ID": "MEASURES_USING_TIME_INTELLIGENCE_AND_MODEL_IS_USING_DIRECT_QUERY", + "Name": "[Prestazioni] Misura che usano la Time Intelligence non sono performanti in modelli Direct Query", + "Category": "Prestazioni", + "Description": "Al momento, è noto che le funzioni di Time Intelligence non funzionano altrettanto bene quando si utilizza Direct Query. Se riscontri problemi di prestazioni, potresti provare soluzioni alternative come l'aggiunta di colonne nella tabella dei fatti che mostrano i dati dell'anno precedente o del mese precedente.", + "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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "REDUCE_NUMBER_OF_CALCULATED_COLUMNS", + "Name": "[Prestazioni] Ridurre il numero di colonne calcolate", + "Category": "Prestazioni", + "Description": "Le colonne calcolate non si comprimono bene come le colonne di dati, quindi occupano più memoria. Inoltre, rallentano i tempi di elaborazione sia per la tabella che per il ricalcolo del processo. Delega la logica delle colonne calcolate nel tuo data warehouse e trasforma queste colonne calcolate in colonne di dati. Riferimento: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 2, + "Scope": "Model", + "Expression": "AllColumns.Where(Type.ToString() == \"Calculated\").Count() > 5", + "CompatibilityLevel": 1200 + }, + { + "ID": "CHECK_IF_BI-DIRECTIONAL_AND_MANY-TO-MANY_RELATIONSHIPS_ARE_VALID", + "Name": "[Prestazioni] Verifica se le relazioni bi-direzionali e molti-a-molti sono valide", + "Category": "Prestazioni", + "Description": "Le relazioni bidirezionali e molti-a-molti possono causare un degrado delle prestazioni o persino avere conseguenze indesiderate. Assicurati di controllare queste relazioni specifiche per assicurarti che funzionino come previsto e che siano effettivamente necessarie. Riferimento: 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", + "CompatibilityLevel": 1200 + }, + { + "ID": "CHECK_IF_DYNAMIC_ROW_LEVEL_SECURITY_(RLS)_IS_NECESSARY", + "Name": "[Prestazioni] Verifica se è necessaria la sicurezza dinamica sulle riga (Dynamic RLS)", + "Category": "Prestazioni", + "Description": "L'utilizzo della sicurezza dinamica a livello di riga (RLS) può aggiungere memoria e sovraccarico delle prestazioni. Si prega di ricercare i pro/contro del suo utilizzo. Riferimento: https://docs.microsoft.com/en-us/power-bi/admin/service-admin-rls", + "Severity": 1, + "Scope": "Table, CalculatedTable", + "Expression": "RegEx.IsMatch(Expression,\"(?i)USERNAME\\(\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)USERPRINCIPALNAME\\(\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "DAX_COLUMNS_FULLY_QUALIFIED", + "Name": "[Espressioni DAX] I riferimenti in colonna devono essere pienamente qualificati. Usare NomeTabella[NomeCampo]", + "Category": "Espressioni DAX", + "Description": "L'utilizzo di riferimenti di colonna completi semplifica la distinzione tra i riferimenti di colonna e di misura e consente inoltre di evitare determinati errori. Quando si fa riferimento a una colonna in DAX, specificare prima il nome della tabella, quindi specificare il nome della colonna tra parentesi quadre. Riferimento: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 3, + "Scope": "Measure, KPI, TablePermission, CalculationItem", + "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", + "CompatibilityLevel": 1200 + }, + { + "ID": "DAX_MEASURES_UNQUALIFIED", + "Name": "[Espressioni DAX] I riferimenti alle misure non dovrebbero essere qualificati. Usare solo il Nome Misura", + "Category": "Espressioni DAX", + "Description": "L'utilizzo di riferimenti a misure non qualificati semplifica la distinzione tra riferimenti di colonna e di misura e consente inoltre di evitare determinati errori. Quando si fa riferimento a una misura utilizzando DAX, non specificare il nome della tabella. Utilizzare solo il nome della misura tra parentesi quadre. Riferimento: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 3, + "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI, CalculationItem", + "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_DUPLICATE_MEASURES", + "Name": "[Espressioni DAX] Due misure non dovrebbero avere la stessa definizione", + "Category": "Espressioni DAX", + "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)", + "CompatibilityLevel": 1200, + "Description": "" + }, + { + "ID": "USE_THE_TREATAS_FUNCTION_INSTEAD_OF_INTERSECT", + "Name": "[Espressioni DAX] Usa la funzione TREATAS invece di INTERSECT per le relazioni virtuali", + "Category": "Espressioni DAX", + "Description": "La funzione TREATAS è più efficiente e fornisce prestazioni migliori rispetto alla funzione INTERSECT quando viene utilizzata nelle relazioni virtuali. Riferimento: https://www.sqlbi.com/articles/propagate-filters-using-treatas-in-dax/", + "Severity": 2, + "Scope": "Measure, CalculationItem", + "Expression": "RegEx.IsMatch(Expression,\"(?i)INTERSECT\\s*\\(\")", + "CompatibilityLevel": 1400 + }, + { + "ID": "USE_THE_DIVIDE_FUNCTION_FOR_DIVISION", + "Name": "[Espressioni DAX] Usa la funzione DIVIDE per la divisione", + "Category": "Espressioni DAX", + "Description": "Utilizzare la funzione DIVIDE invece di utilizzare \"/\". La funzione DIVIDE risolve i casi di divisione per zero. Pertanto, si consiglia di utilizzare per evitare errori. Riferimento: https://docs.microsoft.com/en-us/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*\\/(?!\\/)(?!\\*)\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_USING_THE_IFERROR_FUNCTION", + "Name": "[Espressioni DAX] Evitare di usare la funzione IFERROR", + "Category": "Espressioni DAX", + "Description": "Evitare di utilizzare la funzione IFERROR poiché potrebbe causare un degrado delle prestazioni. Se sei preoccupato per un errore di divisione per zero, usa la funzione DIVIDE poiché risolve naturalmente tali errori come vuoti (oppure puoi personalizzare ciò che dovrebbe essere mostrato in caso di tale errore). Riferimento: https://www.elegantbi.com/post/top10bestpractices", + "Severity": 2, + "Scope": "Measure, CalculatedColumn", + "Expression": "RegEx.IsMatch(Expression,\"(?i)IFERROR\\s*\\(\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "MEASURES_SHOULD_NOT_BE_DIRECT_REFERENCES_OF_OTHER_MEASURES", + "Name": "[Espressioni DAX] Le misure non dovrebbero essere riferimenti diretti ad altre misure", + "Category": "Espressioni DAX", + "Description": "Questa regola identifica le misure che sono semplicemente un riferimento ad un'altra misura. Ad esempio, si consideri un modello con due misure: [MisuraA] e [MisuraB]. Questa regola verrebbe attivata per MisuraB se il DAX di MisuraB fosse MisuraB:=[MisuraA]. Tali misure duplicate dovrebbero essere rimosse.", + "Severity": 2, + "Scope": "Measure", + "Expression": "Model.AllMeasures.Any(DaxObjectName == current.Expression)", + "CompatibilityLevel": 1200 + }, + { + "ID": "FILTER_COLUMN_VALUES", + "Name": "[Espressioni DAX] Filtra i valori delle colonne con la sintassi corretta", + "Category": "Espressioni DAX", + "Description": "Invece di utilizzare questa sintassi FILTER('Table','Table'[Column]=\"Value\") per i parametri di filtro di una funzione CALCULATE o CALCULATETABLE, utilizzare una delle opzioni seguenti. Per quanto riguarda l'utilizzo della funzione KEEPFILTERS, vedere il secondo collegamento di riferimento di seguito. Opzione 1: KEEPFILTERS('Table'[Column]=\"Value\") Opzione 2: 'Table'[Column]=\"Value\" Riferimento: https://docs.microsoft.com/power-bi/guidance/dax-avoid-avoid-filter-as-filter-argument Reference: 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 _]+\\]\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "FILTER_MEASURE_VALUES_BY_COLUMNS", + "Name": "[Espressioni DAX] Quando fai una misura prediligi il filtro per colonne, non per tabelle", + "Category": "Espressioni DAX", + "Description": "Invece di utilizzare questo modello FILTER('Table',[Measure]>Value) per i parametri di filtro di una funzione CALCULATE o CALCULATETABLE, utilizzare una delle opzioni seguenti (se possibile). Il filtraggio su una colonna specifica produrrà una tabella più piccola da elaborare per il motore, consentendo così prestazioni più veloci. L'utilizzo della funzione VALUES o della funzione ALL dipende dal risultato della misura desiderato. Opzione 1: FILTER(VALUES('Table'[Column]),[Measure] > Value) Opzione 2: FILTER(ALL('Table'[Column]),[Measure] > Value) Riferimento: 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*\\[\")", + "CompatibilityLevel": 1200 + }, + { + "ID": "INACTIVE_RELATIONSHIPS_THAT_ARE_NEVER_ACTIVATED", + "Name": "[Espressioni DAX] Relazioni inattive che non vengono mai attivate", + "Category": "Espressioni DAX", + "Description": "Le relazioni inattive vengono attivate utilizzando la funzione USERELATIONSHIP. Se una relazione inattiva non viene referenziata in alcun modo tramite questa funzione, la relazione non verrà utilizzata. Dovrebbe essere determinato se la relazione non è necessaria o attivare la relazione tramite questo metodo. Riferimento: https://docs.microsoft.com/power-bi/guidance/relationships-active-inactive Reference: 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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "DATA_COLUMNS_MUST_HAVE_A_SOURCE_COLUMN", + "Name": "[Prevenzione Errori] Le colonne di dati devono avere una colonna di origine", + "Category": "[Prevenzione errori]", + "Description": "Le colonne di dati devono avere una colonna di origine. Una colonna di dati senza una colonna di origine causerà un errore durante l'elaborazione del modello.", + "Severity": 3, + "Scope": "DataColumn", + "Expression": "string.IsNullOrWhitespace(SourceColumn)", + "CompatibilityLevel": 1200 + }, + { + "ID": "EXPRESSION_RELIANT_OBJECTS_MUST_HAVE_AN_EXPRESSION", + "Name": "[Prevenzione errori] Gli oggetti dipendenti dalle espressioni devono avere un'espressione", + "Category": "[Prevenzione errori]", + "Description": "Le colonne calcolate, gli elementi di calcolo e le misure devono avere un'espressione. Senza un'espressione, questi oggetti non mostreranno alcun valore.", + "Severity": 3, + "Scope": "Measure, CalculatedColumn, CalculationItem", + "Expression": "string.IsNullOrWhiteSpace(Expression)", + "CompatibilityLevel": 1200 + }, + { + "ID": "AVOID_STRUCTURED_DATA_SOURCES_WITH_PROVIDER_PARTITIONS", + "Name": "[Prevenzione errori] Evita origini dati strutturate con partizioni del provider", + "Category": "[Prevenzione errori]", + "Description": "Power BI non supporta le partizioni del provider (dette anche \"legacy\") che fanno riferimento a origini dati strutturate. Le partizioni che fanno riferimento a origini dati strutturate devono utilizzare il linguaggio M. In caso contrario, le partizioni \"provider\" devono fare riferimento a un'origine dati \"provider\". Questo può essere risolto convertendo l'origine dati strutturata in un'origine dati del fornitore (vedere il secondo collegamento di riferimento di seguito). Riferimento: https://docs.microsoft.com/power-bi/admin/service-premium-connect-tools#data-source-declaration Riferimento: https://www.elegantbi.com/post/convertdatasources", + "Severity": 2, + "Scope": "Partition", + "Expression": "SourceType == \"Query\"\r\nand\r\nDataSource.Type == \"Structured\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "UNNECESSARY_COLUMNS", + "Name": "[Manutenzione] Rimuovi colonne non necessarie", + "Category": "[Manutenzione]", + "Description": "Le colonne nascoste a cui non fanno riferimento espressioni DAX, relazioni, livelli gerarchici o proprietà \"Ordina per\" devono essere rimosse.", + "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)))", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "UNNECESSARY_MEASURES", + "Name": "[Manutenzione] Rimuovere le misure non necessarie", + "Category": "[Manutenzione]", + "Description": "Le misure nascoste a cui non fa riferimento alcuna espressione DAX dovrebbero essere rimosse per la manutenibilità", + "Severity": 2, + "Scope": "Measure", + "Expression": "(Table.IsHidden or IsHidden) \r\nand ReferencedBy.Count = 0", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "FIX_REFERENTIAL_INTEGRITY_VIOLATIONS", + "Name": "[Manutenzione] Correggi le violazioni dell'integrità referenziale", + "Category": "[Manutenzione]", + "Description": "Questa regola evidenzia le relazioni che presentano violazioni dell'integrità referenziale. Ciò indica che nella tabella sul lato \"da\" della relazione sono presenti valori che non esistono nella tabella sul lato \"a\" della relazione. Le violazioni dell'integrità referenziale produrranno anche il valore del membro \"vuoto\" nei filtri dei dati. Si consiglia di risolvere questi problemi assicurandosi che la colonna della chiave primaria della tabella \"a\" abbia tutti i valori nella colonna della chiave esterna della tabella \"da\". Riferimento: https://blog.enterprisedna.co/vertipaq-analyzer-tutorial-relationships-referential-integrity/", + "Severity": 2, + "Scope": "Relationship", + "Expression": "Convert.ToInt64(GetAnnotation(\"Vertipaq_RIViolationInvalidRows\")) > 0", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_DATA_SOURCES_NOT_REFERENCED_BY_ANY_PARTITIONS", + "Name": "[Manutenzione] Rimuovere le origini dati non referenziate da alcuna partizione", + "Category": "[Manutenzione]", + "Description": "Le origini dati a cui non fa riferimento alcuna partizione possono essere rimosse.", + "Severity": 1, + "Scope": "ProviderDataSource, StructuredDataSource", + "Expression": "UsedByPartitions.Count() == 0", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "REMOVE_ROLES_WITH_NO_MEMBERS", + "Name": "[Manutenzione] Rimuovi i ruoli senza membri", + "Category": "[Manutenzione]", + "Description": "Puoi rimuovere ruoli senza membri.", + "Severity": 1, + "Scope": "ModelRole", + "Expression": "Members.Count() == 0", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "ENSURE_TABLES_HAVE_RELATIONSHIPS", + "Name": "[Manutenzione] Assicurati che le tabelle abbiano relazioni", + "Category": "[Manutenzione]", + "Description": "Questa regola evidenzia le tabelle che non sono collegate a nessun'altra tabella nel modello con una relazione.", + "Severity": 1, + "Scope": "Table, CalculatedTable", + "Expression": "UsedInRelationships.Count() == 0", + "CompatibilityLevel": 1200 + }, + { + "ID": "OBJECTS_WITH_NO_DESCRIPTION", + "Name": "[Manutenzione] Oggetti visibili senza descrizione", + "Category": "[Manutenzione]", + "Description": "Aggiungi descrizioni agli oggetti. Queste descrizioni vengono visualizzate al passaggio del mouse nell'elenco dei campi in Power BI Desktop. Inoltre, puoi sfruttare queste descrizioni per creare un dizionario dati automatizzato (vedi link sotto). Riferimento: 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", + "CompatibilityLevel": 1200 + }, + { + "ID": "PERSPECTIVES_WITH_NO_OBJECTS", + "Name": "[Manutenzione] Prospettive senza oggetti", + "Category": "[Manutenzione]", + "Description": "Le prospettive che non contengono oggetti (tabelle) molto probabilmente non sono necessarie. In questa regola, è solo necessario controllare le tabelle poiché l'aggiunta di una colonna/misura/gerarchia a una prospettiva aggiunge anche la tabella alla prospettiva. Inoltre, le tabelle in generale coprono anche le tabelle calcolate e i gruppi di calcolo.", + "Severity": 1, + "Scope": "Perspective", + "Expression": "Model.Tables.Any(InPerspective[current.Name]) == false", + "FixExpression": "Delete()", + "CompatibilityLevel": 1200 + }, + { + "ID": "CALCULATION_GROUPS_WITH_NO_CALCULATION_ITEMS", + "Name": "[Manutenzione] Gruppi di calcolo senza elementi di calcolo", + "Category": "[Manutenzione]", + "Description": "I gruppi di calcolo non hanno alcuna funzione a meno che non dispongano di elementi di calcolo.", + "Severity": 2, + "Scope": "CalculationGroup", + "Expression": "CalculationItems.Count == 0", + "CompatibilityLevel": 1200 + }, + { + "ID": "PARTITION_NAME_SHOULD_MATCH_TABLE_NAME_FOR_SINGLE_PARTITION_TABLES", + "Name": "[Naming Conventions] Il nome della partizione deve corrispondere al nome della tabella per le singole tabelle delle partizioni", + "Category": "Naming Conventions", + "Description": "Le tabelle con una sola partizione dovrebbero corrispondere ai nomi delle tabelle e delle partizioni. Le tabelle con più di una partizione dovrebbero avere ogni nome di partizione che inizia con il nome della tabella.", + "Severity": 1, + "Scope": "Table", + "Expression": "(Partitions.Count = 1 and Partitions[0].Name <> Name)", + "FixExpression": "Partitions[0].Name = it.Name", + "CompatibilityLevel": 1200 + }, + { + "ID": "SPECIAL_CHARS_IN_OBJECT_NAMES", + "Name": "[Naming Conventions] I nomi degli oggetti non devono contenere caratteri speciali", + "Category": "Naming Conventions", + "Description": "Tabs, A capo, 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", + "CompatibilityLevel": 1200 + }, + { + "ID": "FORMAT_FLAG_COLUMNS_AS_YES/NO_VALUE_STRINGS", + "Name": "[Formattazione] Formatta le colonne flag come stringhe di valori Sì/No", + "Category": "Formattazione", + "Description": "I flag devono essere formattati correttamente come Sì/No poiché è più facile da leggere rispetto all'utilizzo di valori interi 0/1.", + "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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "OBJECTS_SHOULD_NOT_START_OR_END_WITH_A_SPACE", + "Name": "[Formattazione] Gli oggetti non devono iniziare o terminare con uno spazio", + "Category": "Formattazione", + "Description": "Gli oggetti non dovrebbero iniziare o finire con uno spazio", + "Severity": 3, + "Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn", + "Expression": "Name.StartsWith(\" \") or Name.EndsWith(\" \")", + "CompatibilityLevel": 1200 + }, + { + "ID": "DATECOLUMN_FORMATSTRING", + "Name": "[Formattazione] Fornisci la stringa di formato per le colonne \"Data\"", + "Category": "Formattazione", + "Description": "Le colonne di tipo \"DateTime\" che hanno \"Mese\" nel nome devono essere formattate come \"mm/gg/aaaa\".", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "Name.IndexOf(\"Date\", \"OrdinalIgnoreCase\") >= 0 and DataType = \"DateTime\" and FormatString <> \"mm/dd/yyyy\"", + "FixExpression": "FormatString = \"mm/dd/yyyy\"", + "CompatibilityLevel": 1200 + }, + { + "ID": "MONTHCOLUMN_FORMATSTRING", + "Name": "[Formattazione] Fornisci la stringa di formato per le colonne \"Mese\"", + "Category": "Formattazione", + "Description": "Le colonne di tipo \"DateTime\" che hanno \"Mese\" nel nome devono essere formattate come \"MMMM aaaa\".", + "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 + }, + { + "ID": "PROVIDE_FORMAT_STRING_FOR_MEASURES", + "Name": "[Formattazione] Fornire una stringa di formato per le misure", + "Category": "Formattazione", + "Description": "Le misure visibili dovrebbero avere la proprietà formato assegnata", + "Severity": 3, + "Scope": "Measure", + "Expression": "not IsHidden \r\nand not Table.IsHidden \r\nand string.IsNullOrWhitespace(FormatString)", + "CompatibilityLevel": 1200 + }, + { + "ID": "NUMERIC_COLUMN_SUMMARIZE_BY", + "Name": "[Formattazione] Non riepilogare colonne di tipo numerico", + "Category": "Formattazione", + "Description": "Le colonne numeriche (integer, decimal, double) devono avere la proprietà SummarizeBy impostata su \"Nessuno\" per evitare la somma accidentale in Power BI (creare invece misure).", + "Severity": 3, + "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 + }, + { + "ID": "PERCENTAGE_FORMATTING", + "Name": "[Formattazione] Le percentuali dovrebbero essere formattate con separatori delle migliaia e 1 decimale", + "Category": "Formattazione", + "Severity": 2, + "Scope": "Measure", + "Expression": "FormatString.Contains(\"%\") and FormatString <> \"#,0.0%;-#,0.0%;#,0.0%\"", + "FixExpression": "FormatString = \"#,0.0%;-#,0.0%;#,0.0%\"", + "CompatibilityLevel": 1200, + "Description": "" + }, + { + "ID": "INTEGER_FORMATTING", + "Name": "[Formattazione] I numeri interi devono essere formattati con separatori delle migliaia e senza decimali", + "Category": "Formattazione", + "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, + "Description": "" + }, + { + "ID": "RELATIONSHIP_COLUMNS_SHOULD_BE_OF_INTEGER_DATA_TYPE", + "Name": "[Formattazione] Le colonne delle relazioni devono essere di tipo intero", + "Category": "Formattazione", + "Description": "È consigliabile che le colonne di relazione siano di tipo intero. Questo vale non solo per il data warehousing, ma anche per la modellazione dei dati.", + "Severity": 1, + "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", + "Expression": "UsedInRelationships.Any()\n\nand \n\nDataType != DataType.Int64", + "CompatibilityLevel": 1200 + }, + { + "ID": "ADD_DATA_CATEGORY_FOR_COLUMNS", + "Name": "[Formattazione] Aggiungi categoria di dati per colonne", + "Category": "Formattazione", + "Description": "Aggiungi la proprietà Categoria dati per le colonne appropriate. Riferimento: https://docs.microsoft.com/en-us/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)", + "CompatibilityLevel": 1200 + }, + { + "ID": "HIDE_FOREIGN_KEYS", + "Name": "[Formattazione] Nascondi chiavi esterne", + "Category": "Formattazione", + "Description": "Le chiavi esterne dovrebbero essere sempre nascoste.", + "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 + }, + { + "ID": "MARK_PRIMARY_KEYS", + "Name": "[Formattazione] Contrassegna le chiavi primarie", + "Category": "Formattazione", + "Description": "Imposta la proprietà \"Key\" su \"Vero\" per le colonne della chiave primaria all'interno delle proprietà della colonna.", + "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 + }, + { + "ID": "HIDE_FACT_TABLE_COLUMNS", + "Name": "[Formattazione] Nascondi colonne della tabella dei fatti", + "Category": "Formattazione", + "Description": "È consigliabile nascondere le colonne della tabella dei fatti utilizzate per l'aggregazione nelle misure.", + "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 + }, + { + "ID": "FIRST_LETTER_OF_OBJECTS_MUST_BE_CAPITALIZED", + "Name": "[Formattazione] La prima lettera degli oggetti deve essere in maiuscolo", + "Category": "Formattazione", + "Severity": 1, + "Scope": "Table, Measure, Hierarchy, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup", + "Expression": "Name.Substring(0,1).ToUpper() != Name.Substring(0,1)", + "CompatibilityLevel": 1200, + "Description": "" + }, + { + "ID": "MONTH_(AS_A_STRING)_MUST_BE_SORTED", + "Name": "[Formattazione] Il mese (come stringa) deve essere ordinato", + "Category": "Formattazione", + "Description": "Questa regola evidenzia le colonne del mese che sono stringhe e non sono ordinate. Se non vengono ordinati, verranno ordinati in ordine alfabetico (ad es. aprile, agosto...). Assicurati di ordinare tali colonne in modo che vengano ordinate correttamente (gennaio, febbraio, marzo...).", + "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", + "CompatibilityLevel": 1200 + } +] \ No newline at end of file From abf6f7c86d56327f71d980377be98bfee8e0e802 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Thu, 21 Oct 2021 12:34:08 +0300 Subject: [PATCH 18/23] Delete a.txt --- BestPracticeRules/Italian/a.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 BestPracticeRules/Italian/a.txt diff --git a/BestPracticeRules/Italian/a.txt b/BestPracticeRules/Italian/a.txt deleted file mode 100644 index 8b13789..0000000 --- a/BestPracticeRules/Italian/a.txt +++ /dev/null @@ -1 +0,0 @@ - From 921ed4d5e89a7367fb65b27ccb93f6efb0a541e0 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Thu, 21 Oct 2021 12:49:14 +0300 Subject: [PATCH 19/23] Update README.md --- BestPracticeRules/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 86f7b55..a14440e 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -97,6 +97,13 @@ w.DownloadFile(url, downloadLoc); [Tabular Editor](https://tabulareditor.com/ "Tabular Editor") version 2.16.1 or higher. +## Languages + +* English +* Italian + +*Note: If you would like to volunteer to translate the Best Practice Rules into another language, please contact us at pbibestpractice@microsoft.com* + ## Version History * 2021-08-18 Version 1.2.1 @@ -123,6 +130,7 @@ w.DownloadFile(url, downloadLoc); * [Performance] Reduce usage of long-length columns with high cardinality * Updated rule logic to use Int64 +* 2021-10-21 Best Practice Rules now available in [Italian](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules/Italian)! * 2021-06-13 Version 1.1.2 * Modified Rules * [DAX Expressions] Use the DIVIDE function for division From 2a8d4428659dbf9ff494644aefab0b2459c1a251 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Thu, 21 Oct 2021 12:49:45 +0300 Subject: [PATCH 20/23] Update README.md --- BestPracticeRules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index a14440e..9e0f499 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -102,7 +102,7 @@ w.DownloadFile(url, downloadLoc); * English * Italian -*Note: If you would like to volunteer to translate the Best Practice Rules into another language, please contact us at pbibestpractice@microsoft.com* +*Note: If you would like to volunteer to translate the Best Practice Rules into another language, please contact us at pbibestpractice@microsoft.com.* ## Version History From d27e261a0678bf54f128033da2870c22e83a9ee4 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Thu, 21 Oct 2021 12:59:03 +0300 Subject: [PATCH 21/23] Update README.md --- BestPracticeRules/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index 9e0f499..cdc731c 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -106,6 +106,7 @@ w.DownloadFile(url, downloadLoc); ## Version History +* 2021-10-21 The Best Practice Rules are now available in [Italian](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules/Italian)! * 2021-08-18 Version 1.2.1 * New Rules * [Maintenance] Fix referential integrity violations ([blog post](https://www.elegantbi.com/post/findblankrows)) From 2ffd6b8878c8bd076a3beaff8b2b463a18990fec Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Thu, 21 Oct 2021 13:06:01 +0300 Subject: [PATCH 22/23] Update README.md --- BestPracticeRules/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index cdc731c..a72a115 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -58,6 +58,11 @@ if (version == "3") w.DownloadFile(url, downloadLoc); ``` +*Note: If you want to load the rules in [Italian](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules/Italian), replace the url parameter in the code above with the code below* +```C# +string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/Italian/BPARules.json"; +``` + 4. Close and reopen [Tabular Editor](https://tabulareditor.com/ "Tabular Editor"). 5. Connect to a model. 6. Select 'Tools' from the File menu and select 'Best Practice Analyzer'. @@ -100,7 +105,7 @@ w.DownloadFile(url, downloadLoc); ## Languages * English -* Italian +* [Italian](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules/Italian) *Note: If you would like to volunteer to translate the Best Practice Rules into another language, please contact us at pbibestpractice@microsoft.com.* From 6614a9217208b9d7ef6b218617cbd6992b1e4611 Mon Sep 17 00:00:00 2001 From: m-kovalsky Date: Thu, 21 Oct 2021 13:06:30 +0300 Subject: [PATCH 23/23] Update README.md --- BestPracticeRules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BestPracticeRules/README.md b/BestPracticeRules/README.md index a72a115..e863716 100644 --- a/BestPracticeRules/README.md +++ b/BestPracticeRules/README.md @@ -58,7 +58,7 @@ if (version == "3") w.DownloadFile(url, downloadLoc); ``` -*Note: If you want to load the rules in [Italian](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules/Italian), replace the url parameter in the code above with the code below* +*Note: If you want to load the rules in [Italian](https://github.com/microsoft/Analysis-Services/tree/master/BestPracticeRules/Italian), replace the url parameter in the code above with the code below.* ```C# string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/Italian/BPARules.json"; ```