Adding recommendations to query plan (#1373)

* Adding recommendations

* Adding raw graph type in execution plan graph contracts

* Fixing function name and concising string formatting

* Converting localized string to a function

* Using better names in contract props
Formatting names in a better way

* Getting rid of unnecessary getter, setters and private props

* Fixing localized strings, comments and imports

* Fixing some contracts

* Fixing csproj formatting

* Fixing var names

* Fixing xml comments
This commit is contained in:
Aasim Khan
2022-01-28 11:35:48 -08:00
committed by GitHub
parent a98c266791
commit 92a7248455
13 changed files with 2167 additions and 59 deletions

View File

@@ -8925,6 +8925,16 @@ namespace Microsoft.SqlTools.ServiceLayer
return Keys.GetString(Keys.ActualOfEstimated, actual, estimated, percent);
}
public static string MissingIndexFormat(string impact, string queryText)
{
return Keys.GetString(Keys.MissingIndexFormat, impact, queryText);
}
public static string MissingIndexDetailsTitle(string fileName, string impact)
{
return Keys.GetString(Keys.MissingIndexDetailsTitle, fileName, impact);
}
public static string TableNotInitializedException(string tableId)
{
return Keys.GetString(Keys.TableNotInitializedException, tableId);
@@ -12272,6 +12282,12 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string ActualOfEstimated = "ActualOfEstimated";
public const string MissingIndexFormat = "MissingIndexFormat";
public const string MissingIndexDetailsTitle = "MissingIndexDetailsTitle";
public const string TableNotInitializedException = "TableNotInitializedException";

View File

@@ -4605,6 +4605,19 @@
{1} ({2}%)</value>
<comment>.
Parameters: 0 - actual (string), 1 - estimated (string), 2 - percent (decimal) </comment>
</data>
<data name="MissingIndexFormat" xml:space="preserve">
<value>Missing Index (Impact {0}): {1}</value>
<comment>&quot;Missing Index (Impact {0}): {1}&quot; format string for showplan.
Parameters: 0 - impact (string), 1 - queryText (string) </comment>
</data>
<data name="MissingIndexDetailsTitle" xml:space="preserve">
<value>/*
Missing Index Details from {0}
The Query Processor estimates that implementing the following index could improve the query cost by {1}%.
*/</value>
<comment>title of missing index details.
Parameters: 0 - fileName (string), 1 - impact (string) </comment>
</data>
<data name="TableNotInitializedException" xml:space="preserve">
<value>Initialization is not properly done for table with id &apos;{0}&apos;</value>

View File

@@ -2219,6 +2219,11 @@ SizeInTeraBytesFormat = {0} TB
OperatorDisplayCost(double cost, int percentage) = {0:0.#######} ({1}%)
#Would like to display actual rows and estimated rows in two lines: <number_actual_rows> of\n <number_estimated_rows> (xx%)
ActualOfEstimated(string actual, string estimated, decimal percent) = {0} of\n{1} ({2}%)
;"Missing Index (Impact {0}): {1}" format string for showplan
MissingIndexFormat(string impact, string queryText) = Missing Index (Impact {0}): {1}
;title of missing index details
MissingIndexDetailsTitle(string fileName, string impact) = /*\r\nMissing Index Details from {0}\r\nThe Query Processor estimates that implementing the following index could improve the query cost by {1}%.\r\n*/
############################################################################
# Table Designer

View File

@@ -5742,6 +5742,22 @@
<target state="new">Columns</target>
<note></note>
</trans-unit>
<trans-unit id="MissingIndexFormat">
<source>Missing Index (Impact {0}): {1}</source>
<target state="new">Missing Index (Impact {0}): {1}</target>
<note>"Missing Index (Impact {0}): {1}" format string for showplan</note>
</trans-unit>
<trans-unit id="MissingIndexDetailsTitle">
<source>/*
Missing Index Details from {0}
The Query Processor estimates that implementing the following index could improve the query cost by {1}%.
*/</source>
<target state="new">/*
Missing Index Details from {0}
The Query Processor estimates that implementing the following index could improve the query cost by {1}%.
*/</target>
<note>title of missing index details</note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -908,7 +908,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
var xmlString = r.GetRow(0)[0].DisplayValue;
try
{
plans = ShowPlanGraphUtils.CreateShowPlanGraph(xmlString);
plans = ShowPlanGraphUtils.CreateShowPlanGraph(xmlString, Path.GetFileName(ownerUri));
}
catch (Exception ex)
{

View File

@@ -21,9 +21,13 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan
/// </summary>
public string Query { get; set; }
/// <summary>
/// Underlying xml string used for generating execution plan graph
/// Graph file that used to generate ExecutionPlanGraph
/// </summary>
public string XmlString { get; set; }
public ExecutionPlanGraphFile GraphFile { get; set; }
/// <summary>
/// Index recommendations given by show plan to improve query performance
/// </summary>
public List<ExecutionPlanRecommendation> Recommendations { get; set; }
}
public class ExecutionPlanNode
@@ -113,12 +117,39 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan
/// <summary>
/// Size of the rows returned by the subtree of the edge.
/// </summary>
/// <value></value>
public double RowSize { get; set; }
/// <summary>
/// Edge properties to be shown in the tooltip.
/// </summary>
/// <value></value>
public List<ExecutionPlanGraphPropertyBase> Properties { get; set; }
}
public class ExecutionPlanRecommendation
{
/// <summary>
/// Text displayed in the show plan graph control
/// </summary>
public string DisplayString { get; set; }
/// <summary>
/// Raw query that is recommended to the user
/// </summary>
public string Query { get; set; }
/// <summary>
/// Query that will be opened in a new file once the user click on the recommendation
/// </summary>
public string QueryWithDescription { get; set; }
}
public class ExecutionPlanGraphFile
{
/// <summary>
/// File contents
/// </summary>
public string GraphFileContent { get; set; }
/// <summary>
/// File type for execution plan. This will be the file type of the editor when the user opens the graph file
/// </summary>
public string GraphFileType { get; set; }
}
}

View File

@@ -3,6 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
{
public class Description
@@ -14,7 +17,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
get { return this.title; }
set
{
this.title = value.Trim().Replace(NewLine, " ");
this.title = value.Trim().Replace(Environment.NewLine, " ");
}
}
@@ -24,7 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
set
{
string text = value.Trim();
this.queryText = text.Replace(NewLine, " ");
this.queryText = text.Replace(Environment.NewLine, " ");
}
}
@@ -33,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
get { return this.clusteredMode; }
set
{
this.clusteredMode = value.Trim().Replace(NewLine, " ");
this.clusteredMode = value.Trim().Replace(Environment.NewLine, " ");
}
}
@@ -45,44 +48,25 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
}
}
public bool HasMissingIndex
{
get { return this.hasMissingIndex; }
}
public string MissingIndexQueryText
{
get { return this.missingIndexQueryText; }
}
public string MissingIndexImpact
{
get { return this.missingIndexImpact; }
}
public string MissingIndexDatabase
{
get { return this.missingIndexDatabase; }
}
public List<MissingIndex> MissingIndices { get; set; }
#endregion
#region Member variables
private string title = string.Empty;
private string queryText = string.Empty;
private string toolTipQueryText = string.Empty;
private string clusteredMode = string.Empty;
private bool isClusteredMode = false;
private bool hasMissingIndex = false;
private string missingIndexCaption = string.Empty; // actual caption text that will be displayed on the screen
private string missingIndexQueryText = string.Empty; // create index query
private string missingIndexImpact = string.Empty; // impact
private string missingIndexDatabase = string.Empty; // database context
private const string NewLine = "\r\n";
private bool isClusteredMode = false;
#endregion
}
public class MissingIndex
{
public string MissingIndexCaption { get; set; }
public string MissingIndexQueryText { get; set; }
public string MissingIndexImpact { get; set; }
public string MissingIndexDatabase { get; set; }
}
}

View File

@@ -6,7 +6,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Xml;
@@ -45,9 +44,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
{
plan = ReadXmlShowPlan(dataSource);
}
List<ShowPlanGraph> graphs = new List<ShowPlanGraph>();
int statementIndex = 0;
foreach (BaseStmtInfoType statement in EnumStatements(plan))
{
// Reset currentNodeId (used through Context) and create new context
@@ -55,8 +54,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
NodeBuilderContext context = new NodeBuilderContext(new ShowPlanGraph(), this.showPlanType, this);
// Parse the statement block
XmlPlanParser.Parse(statement, null, null, context);
// Get the statement XML for the graph.
context.Graph.XmlDocument = GetSingleStatementXml(dataSource, statementIndex);
// Parse the graph description.
context.Graph.Description = ParseDescription(context.Graph);
// Add graph to the list
graphs.Add(context.Graph);
// Incrementing statement index
statementIndex++;
}
return graphs.ToArray();
@@ -79,11 +84,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
// Now make the new plan based on the existing one that contains only one statement.
ShowPlanXML plan = ReadXmlShowPlan(dataSource);
plan.BatchSequence = new StmtBlockType[][]
plan.BatchSequence = new StmtBlockType[][]
{
new StmtBlockType[] { newStatementBlock }
};
// Serialize the new plan.
StringBuilder stringBuilder = new StringBuilder();
Serializer.Serialize(new StringWriter(stringBuilder), plan);
@@ -370,6 +375,116 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
}
}
}
private Description ParseDescription(ShowPlanGraph graph)
{
XmlDocument stmtXmlDocument = new XmlDocument();
stmtXmlDocument.LoadXml(graph.XmlDocument);
var nsMgr = new XmlNamespaceManager(stmtXmlDocument.NameTable);
//Manually add our showplan namespace since the document won't have it in the default NameTable
nsMgr.AddNamespace("shp", "http://schemas.microsoft.com/sqlserver/2004/07/showplan");
//The root node in this case is the statement node
XmlNode rootNode = stmtXmlDocument.DocumentElement;
if(rootNode == null)
{
//Couldn't find our statement node, this should never happen in a properly formed document
throw new ArgumentNullException("StatementNode");
}
XmlNode missingIndexes = rootNode.SelectSingleNode("descendant::shp:MissingIndexes", nsMgr);
List<MissingIndex> parsedIndexes = new List<MissingIndex>();
// Not all plans will have a missing index. For those plans, just return the description.
if (missingIndexes != null)
{
// check Memory Optimized table.
bool memoryOptimzed = false;
XmlNode scan = rootNode.SelectSingleNode("descendant::shp:IndexScan", nsMgr);
if (scan == null)
{
scan = rootNode.SelectSingleNode("descendant::shp:TableScan", nsMgr);
}
if (scan != null && scan.Attributes["Storage"] != null)
{
if (0 == string.Compare(scan.Attributes["Storage"].Value, "MemoryOptimized", StringComparison.Ordinal))
{
memoryOptimzed = true;
}
}
// getting all the indexgroups from the plan. A plan can have multiple missing index groups.
XmlNodeList indexGroups = missingIndexes.SelectNodes("descendant::shp:MissingIndexGroup", nsMgr);
// missing index template
const string createIndexTemplate = "CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]\r\nON {0}.{1} ({2})\r\n";
const string addIndexTemplate = "ALTER TABLE {0}.{1}\r\nADD INDEX [<Name of Missing Index, sysname,>]\r\nNONCLUSTERED ({2})\r\n";
const string includeTemplate = "INCLUDE ({0})";
// iterating over all missing index groups
foreach (XmlNode indexGroup in indexGroups)
{
// we only have one missing index per index group
XmlNode missingIndex = indexGroup.SelectSingleNode("descendant::shp:MissingIndex", nsMgr);
string database = missingIndex.Attributes["Database"].Value;
string schemaName = missingIndex.Attributes["Schema"].Value;
string tableName = missingIndex.Attributes["Table"].Value;
string indexColumns = string.Empty;
string includeColumns = string.Empty;
// populate index columns and include columns
XmlNodeList columnGroups = missingIndex.SelectNodes("shp:ColumnGroup", nsMgr);
foreach (XmlNode columnGroup in columnGroups)
{
foreach (XmlNode column in columnGroup.ChildNodes)
{
string columnName = column.Attributes["Name"].Value;
if (0 != string.Compare(columnGroup.Attributes["Usage"].Value, "INCLUDE", StringComparison.Ordinal))
{
if (indexColumns == string.Empty)
indexColumns = columnName;
else
indexColumns = $"{indexColumns},{columnName}";
}
else if (!memoryOptimzed)
{
if (includeColumns == string.Empty)
includeColumns = columnName;
else
includeColumns = $"{indexColumns},{columnName}";
}
}
}
// for memory optimized we just alter the existing index where as for non optimized tables we create a new one.
string queryText = string.Format((memoryOptimzed) ? addIndexTemplate : createIndexTemplate, schemaName, tableName, indexColumns);
if (!string.IsNullOrEmpty(includeColumns))
{
queryText += string.Format(includeTemplate, includeColumns);
}
string impact = indexGroup.Attributes["Impact"].Value;
string caption = SR.MissingIndexFormat(impact, queryText);
parsedIndexes.Add(new MissingIndex()
{
MissingIndexDatabase = database,
MissingIndexQueryText = queryText,
MissingIndexImpact = impact,
MissingIndexCaption = caption
});
}
}
Description description = new Description
{
QueryText = graph.Statement,
MissingIndices = parsedIndexes,
};
return description;
}
#endregion

View File

@@ -55,6 +55,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan.ShowPlanGraph
}
}
/// <summary>
/// Contains the raw xml document for the graph. Used to save graphs.
/// </summary>
public string XmlDocument { get; set; }
/// <summary>
/// The QueryPlanHash as recorded in the RootNode for this graph, null if not available
/// </summary>

View File

@@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -12,14 +13,19 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan
{
public class ShowPlanGraphUtils
{
public static List<ExecutionPlanGraph> CreateShowPlanGraph(string xml)
public static List<ExecutionPlanGraph> CreateShowPlanGraph(string xml, string fileName)
{
ShowPlanGraph.ShowPlanGraph[] graphs = ShowPlanGraph.ShowPlanGraph.ParseShowPlanXML(xml, ShowPlanGraph.ShowPlanType.Unknown);
return graphs.Select(g => new ExecutionPlanGraph
{
Root = ConvertShowPlanTreeToExecutionPlanTree(g.Root),
Query = g.Statement,
XmlString = xml
GraphFile = new ExecutionPlanGraphFile
{
GraphFileContent = xml,
GraphFileType = "xml"
},
Recommendations = ParseRecommendations(g, fileName)
}).ToList();
}
@@ -85,5 +91,36 @@ namespace Microsoft.SqlTools.ServiceLayer.ShowPlan
}
return propsList;
}
private static List<ExecutionPlanRecommendation> ParseRecommendations(ShowPlanGraph.ShowPlanGraph g, string fileName)
{
return g.Description.MissingIndices.Select(mi => new ExecutionPlanRecommendation
{
DisplayString = mi.MissingIndexCaption,
Query = mi.MissingIndexQueryText,
QueryWithDescription = ParseMissingIndexQueryText(fileName, mi.MissingIndexImpact, mi.MissingIndexDatabase, mi.MissingIndexQueryText)
}).ToList();
}
/// <summary>
/// Creates query file text for the recommendations. It has the missing index query along with some lines of description.
/// </summary>
/// <param name="fileName">query file name that has generated the plan</param>
/// <param name="impact">impact of the missing query on performance</param>
/// <param name="database">database name to create the missing index in</param>
/// <param name="query">actual query that will be used to create the missing index</param>
/// <returns></returns>
private static string ParseMissingIndexQueryText(string fileName, string impact, string database, string query)
{
return $@"{SR.MissingIndexDetailsTitle(fileName, impact)}
/*
{string.Format("USE {0}", database)}
GO
{string.Format("{0}", query)}
GO
*/
";
}
}
}

View File

@@ -30,7 +30,11 @@
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<ItemGroup>
<Content Remove=".\ShowPlan\TestExecution.xml" />
<EmbeddedResource Include=".\ShowPlan\TestExecutionPlan.xml" />
<Content Remove=".\ShowPlan\TestExecution.xml" />
<EmbeddedResource Include=".\ShowPlan\TestExecutionPlan.xml" />
</ItemGroup>
</Project>
<ItemGroup>
<Content Remove=".\ShowPlan\TestExecutionPlanRecommendations.xml" />
<EmbeddedResource Include=".\ShowPlan\TestExecutionPlanRecommendations.xml" />
</ItemGroup>
</Project>

View File

@@ -4,6 +4,7 @@ using System;
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using NUnit.Framework;
@@ -16,20 +17,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan
{
private string queryPlanFileText;
[SetUp]
public void LoadQueryPlanBeforeEachTest()
{
Assembly assembly = Assembly.GetAssembly(typeof(ShowPlanXMLTests));
Stream scriptStream = assembly.GetManifestResourceStream(assembly.GetName().Name + ".ShowPlan.TestExecutionPlan.xml");
StreamReader reader = new StreamReader(scriptStream);
queryPlanFileText = reader.ReadToEnd();
}
[Test]
public void ParseXMLFileReturnsValidShowPlanGraph()
{
var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText);
ReadFile(".ShowPlan.TestExecutionPlan.xml");
var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql");
Assert.AreEqual(1, showPlanGraphs.Count, "exactly one show plan graph should be returned");
Assert.NotNull(showPlanGraphs[0], "graph should not be null");
Assert.NotNull(showPlanGraphs[0].Root, "graph should have a root");
@@ -38,8 +30,9 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan
[Test]
public void ParsingNestedProperties()
{
ReadFile(".ShowPlan.TestExecutionPlan.xml");
string[] commonNestedPropertiesNames = { "MemoryGrantInfo", "OptimizerHardwareDependentProperties" };
var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText);
var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql");
ExecutionPlanNode rootNode = showPlanGraphs[0].Root;
rootNode.Properties.ForEach(p =>
{
@@ -49,5 +42,24 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan
}
});
}
[Test]
public void ParseXMLFileWithRecommendations()
{
//The first graph in this execution plan has 3 recommendations
ReadFile(".ShowPlan.TestExecutionPlanRecommendations.xml");
string[] commonNestedPropertiesNames = { "MemoryGrantInfo", "OptimizerHardwareDependentProperties" };
var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql");
List<ExecutionPlanRecommendation> rootNode = showPlanGraphs[0].Recommendations;
Assert.AreEqual(3, rootNode.Count, "3 recommendations should be returned by the showplan parser");
}
private void ReadFile(string fileName)
{
Assembly assembly = Assembly.GetAssembly(typeof(ShowPlanXMLTests));
Stream scriptStream = assembly.GetManifestResourceStream(assembly.GetName().Name + fileName);
StreamReader reader = new StreamReader(scriptStream);
queryPlanFileText = reader.ReadToEnd();
}
}
}