Query Store Service (#2171)

* Checkpoint

* Checkpoint

* Checkpoint

* checkpoint

* Hooking in calls to QueryExecutionService

* adding cases

* Fleshing out report handlers

* Adding parameter converters

* Adding sqlparam declarations for Top Resource Consumers and Forced Plans

* swapping to object-object to centralize conversion for sqlparams

* Adding sqlparams for GetTrackedQueries

* Added sqlparams for High Variation

* Added Overall ResourceConumption

* Adding params for regressed queries

* Removing WithWaitStats calls, since they're automatically used within QSM when waitstats is an available statistic#

* Adding PlanSummary handlers

* cleaning up orderable queries

* initial test mockout

* adding basic (incorrect) parameter translation

* first test passing, datetimeoffset swapped to ISO format

* Adding test baselines

* Updating nuget package

* Adding get/set

* Adding get/set for result object

* Switching to parameter-less constructor

* Swapping TimeInterval for string-based BasicTimeInterval

* Removing unnecessary usings

* Adding back params comments

* Fixing up request docstrings

* comment tweak

* fix tests failing in pipeline because of line endings not matching

* removing unnecessary usings

* Setting tests to generate queries in UTC for test stability

* Normalizing line endings

---------

Co-authored-by: Kim Santiago <kisantia@microsoft.com>
This commit is contained in:
Benjin Dubishar
2023-09-01 10:56:39 -07:00
committed by GitHub
parent c0a0f27e49
commit 25c18d3390
16 changed files with 2263 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ using Microsoft.SqlTools.ServiceLayer.NotebookConvert;
using Microsoft.SqlTools.ServiceLayer.ObjectManagement;
using Microsoft.SqlTools.ServiceLayer.Profiler;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryStore;
using Microsoft.SqlTools.ServiceLayer.SchemaCompare;
using Microsoft.SqlTools.ServiceLayer.Scripting;
using Microsoft.SqlTools.ServiceLayer.ServerConfigurations;
@@ -175,6 +176,9 @@ namespace Microsoft.SqlTools.ServiceLayer
SqlProjectsService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(SqlProjectsService.Instance);
QueryStoreService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(QueryStoreService.Instance);
serviceHost.InitializeRequestHandlers();
}

View File

@@ -46,6 +46,7 @@
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" />
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" />
<PackageReference Include="Microsoft.SqlServer.Management.QueryStoreModel" />
<PackageReference Include="Microsoft.SqlServer.Management.SmoMetadataProvider" />
<PackageReference Include="Microsoft.SqlServer.SqlManagementObjects" />
<PackageReference Include="Microsoft.SqlServer.Management.SqlParser" />

View File

@@ -0,0 +1,55 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using System;
using Microsoft.SqlServer.Management.QueryStoreModel.Common;
namespace Microsoft.SqlTools.ServiceLayer.QueryStore
{
/// <summary>
/// Represents a TimeInterval with strings for the start and end times instead of DateTimeOffsets for JRPC compatibility
/// </summary>
public class BasicTimeInterval
{
/// <summary>
/// Start time of this time interval, in ISO 8601 format (<code>ToString("O")</code>).
/// This property is ignored unless TimeIntervalOptions is set to Custom.
/// </summary>
public string StartDateTimeInUtc { get; set; } = null;
/// <summary>
/// End time of this time interval, in ISO 8601 format (<code>ToString("O")</code>).
/// This property is ignored unless TimeIntervalOptions is set to Custom.
/// </summary>
public string EndDateTimeInUtc { get; set; } = null;
/// <summary>
/// Time interval type. Unless set to Custom, then StartDateTimeInUtc and EndDateTimeInUtc are ignored.
/// </summary>
public TimeIntervalOptions TimeIntervalOptions { get; set; } = TimeIntervalOptions.Custom;
public TimeInterval Convert()
{
if (TimeIntervalOptions == TimeIntervalOptions.Custom
&& !String.IsNullOrWhiteSpace(StartDateTimeInUtc)
&& !String.IsNullOrWhiteSpace(EndDateTimeInUtc))
{
return new TimeInterval(DateTimeOffset.Parse(StartDateTimeInUtc), DateTimeOffset.Parse(EndDateTimeInUtc));
}
else if (TimeIntervalOptions != TimeIntervalOptions.Custom
&& String.IsNullOrWhiteSpace(StartDateTimeInUtc)
&& String.IsNullOrWhiteSpace(EndDateTimeInUtc))
{
return new TimeInterval(TimeIntervalOptions);
}
else
{
throw new InvalidOperationException($"{nameof(BasicTimeInterval)} was not populated correctly: '{TimeIntervalOptions}', '{StartDateTimeInUtc}' - '{EndDateTimeInUtc}'");
}
}
}
}

View File

@@ -0,0 +1,40 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Management.QueryStoreModel.ForcedPlanQueries;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
#nullable disable
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting a Forced Plan Queries report
/// </summary>
public class GetForcedPlanQueriesReportParams : OrderableQueryConfigurationParams<ForcedPlanQueriesConfiguration>
{
/// <summary>
/// Time interval for the report
/// </summary>
public BasicTimeInterval TimeInterval { get; set; }
public override ForcedPlanQueriesConfiguration Convert()
{
ForcedPlanQueriesConfiguration config = base.Convert();
config.TimeInterval = TimeInterval.Convert();
return config;
}
}
/// <summary>
/// Gets the query for a Forced Plan Queries report
/// </summary>
public class GetForcedPlanQueriesReportRequest
{
public static readonly RequestType<GetForcedPlanQueriesReportParams, QueryStoreQueryResult> Type
= RequestType<GetForcedPlanQueriesReportParams, QueryStoreQueryResult>.Create("queryStore/getForcedPlanQueriesReport");
}
}

View File

@@ -0,0 +1,49 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Management.QueryStoreModel.HighVariation;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
#nullable disable
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting a High Variation Queries report
/// </summary>
public class GetHighVariationQueriesReportParams : OrderableQueryConfigurationParams<HighVariationConfiguration>
{
/// <summary>
/// Time interval for the report
/// </summary>
public BasicTimeInterval TimeInterval { get; set; }
public override HighVariationConfiguration Convert()
{
HighVariationConfiguration config = base.Convert();
config.TimeInterval = TimeInterval.Convert();
return config;
}
}
/// <summary>
/// Gets the query for a High Variation Queries report
/// </summary>
public class GetHighVariationQueriesSummaryRequest
{
public static readonly RequestType<GetHighVariationQueriesReportParams, QueryStoreQueryResult> Type
= RequestType<GetHighVariationQueriesReportParams, QueryStoreQueryResult>.Create("queryStore/getHighVariationQueriesSummary");
}
/// <summary>
/// Gets the query for a detailed High Variation Queries report
/// </summary>
public class GetHighVariationQueriesDetailedSummaryRequest
{
public static readonly RequestType<GetHighVariationQueriesReportParams, QueryStoreQueryResult> Type
= RequestType<GetHighVariationQueriesReportParams, QueryStoreQueryResult>.Create("queryStore/getHighVariationQueriesDetailedSummary");
}
}

View File

@@ -0,0 +1,48 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Management.QueryStoreModel.Common;
using Microsoft.SqlServer.Management.QueryStoreModel.OverallResourceConsumption;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
#nullable disable
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting an Overall Resource Consumption report
/// </summary>
public class GetOverallResourceConsumptionReportParams : QueryConfigurationParams<OverallResourceConsumptionConfiguration>
{
/// <summary>
/// Time interval for the report
/// </summary>
public BasicTimeInterval SpecifiedTimeInterval { get; set; }
/// <summary>
/// Bucket interval for the report
/// </summary>
public BucketInterval SpecifiedBucketInterval { get; set; }
public override OverallResourceConsumptionConfiguration Convert()
{
OverallResourceConsumptionConfiguration result = base.Convert();
result.SpecifiedTimeInterval = SpecifiedTimeInterval.Convert();
result.SelectedBucketInterval = SpecifiedBucketInterval;
return result;
}
}
/// <summary>
/// Gets the query for an Overall Resource Consumption report
/// </summary>
public class GetOverallResourceConsumptionReportRequest
{
public static readonly RequestType<GetOverallResourceConsumptionReportParams, QueryStoreQueryResult> Type
= RequestType<GetOverallResourceConsumptionReportParams, QueryStoreQueryResult>.Create("queryStore/getOverallResourceConsumptionReport");
}
}

View File

@@ -0,0 +1,62 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Management.QueryStoreModel.RegressedQueries;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
#nullable disable
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting a Regressed Queries report
/// </summary>
public class GetRegressedQueriesReportParams : QueryConfigurationParams<RegressedQueriesConfiguration>
{
/// <summary>
/// Time interval during which to look for performance regressions for the report
/// </summary>
public BasicTimeInterval TimeIntervalRecent { get; set; }
/// <summary>
/// Time interval during which to establish baseline performance for the report
/// </summary>
public BasicTimeInterval TimeIntervalHistory { get; set; }
/// <summary>
/// Minimum number of executions for a query to be included
/// </summary>
public long MinExecutionCount { get; set; }
public override RegressedQueriesConfiguration Convert()
{
RegressedQueriesConfiguration result = base.Convert();
result.TimeIntervalRecent = TimeIntervalRecent.Convert();
result.TimeIntervalHistory = TimeIntervalHistory.Convert();
result.MinExecutionCount = MinExecutionCount;
return result;
}
}
/// <summary>
/// Gets the query for a Regressed Queries report
/// </summary>
public class GetRegressedQueriesSummaryRequest
{
public static readonly RequestType<GetRegressedQueriesReportParams, QueryStoreQueryResult> Type
= RequestType<GetRegressedQueriesReportParams, QueryStoreQueryResult>.Create("queryStore/getRegressedQueriesSummary");
}
/// <summary>
/// Gets the query for a detailed Regressed Queries report
/// </summary>
public class GetRegressedQueriesDetailedSummaryRequest
{
public static readonly RequestType<GetRegressedQueriesReportParams, QueryStoreQueryResult> Type
= RequestType<GetRegressedQueriesReportParams, QueryStoreQueryResult>.Create("queryStore/getRegressedQueriesDetailedSummary");
}
}

View File

@@ -0,0 +1,49 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Management.QueryStoreModel.TopResourceConsumers;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
#nullable disable
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting a Top Resource Consumers report
/// </summary>
public class GetTopResourceConsumersReportParams : OrderableQueryConfigurationParams<TopResourceConsumersConfiguration>
{
/// <summary>
/// Time interval for the report
/// </summary>
public BasicTimeInterval TimeInterval { get; set; }
public override TopResourceConsumersConfiguration Convert()
{
TopResourceConsumersConfiguration result = base.Convert();
result.TimeInterval = TimeInterval.Convert();
return result;
}
}
/// <summary>
/// Gets the query for a Top Resource Consumers report
/// </summary>
public class GetTopResourceConsumersSummaryRequest
{
public static readonly RequestType<GetTopResourceConsumersReportParams, QueryStoreQueryResult> Type
= RequestType<GetTopResourceConsumersReportParams, QueryStoreQueryResult>.Create("queryStore/getTopResourceConsumersSummary");
}
/// <summary>
/// Gets the query for a detailed Top Resource Consumers report
/// </summary>
public class GetTopResourceConsumersDetailedSummaryRequest
{
public static readonly RequestType<GetTopResourceConsumersReportParams, QueryStoreQueryResult> Type
= RequestType<GetTopResourceConsumersReportParams, QueryStoreQueryResult>.Create("queryStore/getTopResourceConsumersDetailedSummary");
}
}

View File

@@ -0,0 +1,31 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
#nullable disable
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting a Tracked Queries report
/// </summary>
public class GetTrackedQueriesReportParams
{
/// <summary>
/// Search text for a query
/// </summary>
public string QuerySearchText { get; set; }
}
/// <summary>
/// Gets the query for a Tracked Queries report
/// </summary>
public class GetTrackedQueriesReportRequest
{
public static readonly RequestType<GetTrackedQueriesReportParams, QueryStoreQueryResult> Type
= RequestType<GetTrackedQueriesReportParams, QueryStoreQueryResult>.Create("queryStore/getTrackedQueriesReport");
}
}

View File

@@ -0,0 +1,115 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using Microsoft.SqlServer.Management.QueryStoreModel.Common;
using Microsoft.SqlServer.Management.QueryStoreModel.PlanSummary;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using static Microsoft.SqlServer.Management.QueryStoreModel.PlanSummary.PlanSummaryConfiguration;
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Parameters for getting a Plan Summary
/// </summary>
public class GetPlanSummaryParams : TypedQueryStoreReportParams<PlanSummaryConfiguration>
{
/// <summary>
/// Query ID to view a summary of plans for
/// </summary>
public long QueryId { get; set; }
/// <summary>
/// Mode of the time interval search
/// </summary>
public PlanTimeIntervalMode TimeIntervalMode { get; set; }
/// <summary>
/// Time interval for the report
/// </summary>
public BasicTimeInterval TimeInterval { get; set; }
/// <summary>
/// Metric to summarize
/// </summary>
public Metric SelectedMetric { get; set; }
/// <summary>
/// Statistic to calculate on SelecticMetric
/// </summary>
public Statistic SelectedStatistic { get; set; }
public override PlanSummaryConfiguration Convert() => new()
{
QueryId = QueryId,
TimeIntervalMode = TimeIntervalMode,
TimeInterval = TimeInterval.Convert(),
SelectedMetric = SelectedMetric,
SelectedStatistic = SelectedStatistic
};
}
/// <summary>
/// Parameters for getting the grid view of a Plan Summary
/// </summary>
public class GetPlanSummaryGridViewParams : GetPlanSummaryParams, IOrderableQueryParams
{
/// <summary>
/// Name of the column to order results by
/// </summary>
public string OrderByColumnId { get; set; }
/// <summary>
/// Direction of the result ordering
/// </summary>
public bool Descending { get; set; }
public string GetOrderByColumnId() => OrderByColumnId;
}
/// <summary>
/// Parameters for getting the forced plan for a query
/// </summary>
public class GetForcedPlanParams : QueryStoreReportParams
{
/// <summary>
/// Query ID to view the plan for
/// </summary>
public long QueryId { get; set; }
/// <summary>
/// Plan ID to view
/// </summary>
public long PlanId { get; set; }
}
/// <summary>
/// Gets the query for a Plan Summary chart view
/// </summary>
public class GetPlanSummaryChartViewRequest
{
public static readonly RequestType<GetPlanSummaryParams, QueryStoreQueryResult> Type
= RequestType<GetPlanSummaryParams, QueryStoreQueryResult>.Create("queryStore/getPlanSummaryChartView");
}
/// <summary>
/// Gets the query for a Plan Summary grid view
/// </summary>
public class GetPlanSummaryGridViewRequest
{
public static readonly RequestType<GetPlanSummaryGridViewParams, QueryStoreQueryResult> Type
= RequestType<GetPlanSummaryGridViewParams, QueryStoreQueryResult>.Create("queryStore/getPlanSummaryGridView");
}
/// <summary>
/// Gets the query to view a forced plan
/// </summary>
public class GetForcedPlanRequest // there's also GetForcedPlanQueries (plural) in QSM; how is that not confusing...
{
public static readonly RequestType<GetForcedPlanParams, QueryStoreQueryResult> Type
= RequestType<GetForcedPlanParams, QueryStoreQueryResult>.Create("queryStore/getForcedPlan");
}
}

View File

@@ -0,0 +1,123 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using Microsoft.SqlServer.Management.QueryStoreModel.Common;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts
{
/// <summary>
/// Base class for a Query Store report parameters
/// </summary>
public abstract class QueryStoreReportParams
{
/// <summary>
/// Connection URI for the database
/// </summary>
public string ConnectionOwnerUri { get; set; }
}
/// <summary>
/// Base class for Query Store report parameters that can be converted to a configuration object for use in QSM query generators
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class TypedQueryStoreReportParams<T> : QueryStoreReportParams
{
/// <summary>
/// Converts this SQL Tools Service parameter object to the QSM configuration object
/// </summary>
/// <returns></returns>
public abstract T Convert();
}
/// <summary>
/// Base class for parameters for a report type that uses QueryConfigurationBase for its configuration
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class QueryConfigurationParams<T> : TypedQueryStoreReportParams<T> where T : QueryConfigurationBase, new()
{
/// <summary>
/// Metric to summarize
/// </summary>
public Metric SelectedMetric { get; set; }
/// <summary>
/// Statistic to calculate on SelecticMetric
/// </summary>
public Statistic SelectedStatistic { get; set; }
/// <summary>
/// Number of queries to return if ReturnAllQueries is not set
/// </summary>
public int TopQueriesReturned { get; set; }
/// <summary>
/// True to include all queries in the report; false to only include the top queries, up to the value specified by TopQueriesReturned
/// </summary>
public bool ReturnAllQueries { get; set; }
/// <summary>
/// Minimum number of query plans for a query to included in the report
/// </summary>
public int MinNumberOfQueryPlans { get; set; }
public override T Convert() => new T()
{
SelectedMetric = SelectedMetric,
SelectedStatistic = SelectedStatistic,
TopQueriesReturned = TopQueriesReturned,
ReturnAllQueries = ReturnAllQueries,
MinNumberOfQueryPlans = MinNumberOfQueryPlans
};
}
/// <summary>
/// Base class for parameters for a report that can be ordered by a specified column
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class OrderableQueryConfigurationParams<T> : QueryConfigurationParams<T>, IOrderableQueryParams where T : QueryConfigurationBase, new()
{
/// <summary>
/// Name of the column to order results by
/// </summary>
public string OrderByColumnId { get; set; }
/// <summary>
/// Direction of the result ordering
/// </summary>
public bool Descending { get; set; }
/// <summary>
/// Gets the name of the column to order the report results by
/// </summary>
/// <returns></returns>
public string GetOrderByColumnId() => OrderByColumnId;
}
/// <summary>
/// Result containing a finalized query for a report
/// </summary>
public class QueryStoreQueryResult : ResultStatus
{
/// <summary>
/// Finalized query for a report
/// </summary>
public string Query { get; set; }
}
/// <summary>
/// Interface for parameters for a report that can be ordered by a specific column
/// </summary>
public interface IOrderableQueryParams
{
/// <summary>
/// Gets the name of the column to order the report results by
/// </summary>
/// <returns></returns>
string GetOrderByColumnId();
}
}

View File

@@ -0,0 +1,552 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
using Microsoft.SqlServer.Management.QueryStoreModel.Common;
using Microsoft.SqlServer.Management.QueryStoreModel.ForcedPlanQueries;
using Microsoft.SqlServer.Management.QueryStoreModel.HighVariation;
using Microsoft.SqlServer.Management.QueryStoreModel.OverallResourceConsumption;
using Microsoft.SqlServer.Management.QueryStoreModel.PlanSummary;
using Microsoft.SqlServer.Management.QueryStoreModel.RegressedQueries;
using Microsoft.SqlServer.Management.QueryStoreModel.TopResourceConsumers;
using Microsoft.SqlServer.Management.QueryStoreModel.TrackedQueries;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.QueryStore
{
/// <summary>
/// Main class for Query Store service
/// </summary>
public class QueryStoreService : BaseService
{
private static readonly Lazy<QueryStoreService> instance = new Lazy<QueryStoreService>(() => new QueryStoreService());
/// <summary>
/// Gets the singleton instance object
/// </summary>
public static QueryStoreService Instance => instance.Value;
/// <summary>
/// Instance of the connection service, used to get the connection info for a given owner URI
/// </summary>
private ConnectionService ConnectionService { get; }
public QueryStoreService()
{
ConnectionService = ConnectionService.Instance;
}
/// <summary>
/// Initializes the service instance
/// </summary>
/// <param name="serviceHost"></param>
public void InitializeService(ServiceHost serviceHost)
{
// Top Resource Consumers report
serviceHost.SetRequestHandler(GetTopResourceConsumersSummaryRequest.Type, HandleGetTopResourceConsumersSummaryReportRequest, isParallelProcessingSupported: true);
serviceHost.SetRequestHandler(GetTopResourceConsumersDetailedSummaryRequest.Type, HandleGetTopResourceConsumersDetailedSummaryReportRequest, isParallelProcessingSupported: true);
// Forced Plan Queries report
serviceHost.SetRequestHandler(GetForcedPlanQueriesReportRequest.Type, HandleGetForcedPlanQueriesReportRequest, isParallelProcessingSupported: true);
// Tracked Queries report
serviceHost.SetRequestHandler(GetTrackedQueriesReportRequest.Type, HandleGetTrackedQueriesReportRequest, isParallelProcessingSupported: true);
// High Variation Queries report
serviceHost.SetRequestHandler(GetHighVariationQueriesSummaryRequest.Type, HandleGetHighVariationQueriesSummaryReportRequest, isParallelProcessingSupported: true);
serviceHost.SetRequestHandler(GetHighVariationQueriesDetailedSummaryRequest.Type, HandleGetHighVariationQueriesDetailedSummaryReportRequest, isParallelProcessingSupported: true);
// Overall Resource Consumption report
serviceHost.SetRequestHandler(GetOverallResourceConsumptionReportRequest.Type, HandleGetOverallResourceConsumptionReportRequest, isParallelProcessingSupported: true);
// Regressed Queries report
serviceHost.SetRequestHandler(GetRegressedQueriesSummaryRequest.Type, HandleGetRegressedQueriesSummaryReportRequest, isParallelProcessingSupported: true);
serviceHost.SetRequestHandler(GetRegressedQueriesDetailedSummaryRequest.Type, HandleGetRegressedQueriesDetailedSummaryReportRequest, isParallelProcessingSupported: true);
// Plan Summary report
serviceHost.SetRequestHandler(GetPlanSummaryChartViewRequest.Type, HandleGetPlanSummaryChartViewRequest, isParallelProcessingSupported: true);
serviceHost.SetRequestHandler(GetPlanSummaryGridViewRequest.Type, HandleGetPlanSummaryGridViewRequest, isParallelProcessingSupported: true);
serviceHost.SetRequestHandler(GetForcedPlanRequest.Type, HandleGetForcedPlanRequest, isParallelProcessingSupported: true);
}
#region Handlers
/*
* General process is to:
* 1. Convert the ADS config to the QueryStoreModel config format
* 2. Call the unordered query generator to get the list of columns
* 3. Select the intended ColumnInfo for sorting
* 4. Call the ordered query generator to get the actual query
* 5. Prepend any necessary TSQL parameters to the generated query
* 6. Return the query text to ADS for execution
*/
#region Top Resource Consumers report
internal async Task HandleGetTopResourceConsumersSummaryReportRequest(GetTopResourceConsumersReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
TopResourceConsumersConfiguration config = requestParams.Convert();
TopResourceConsumersQueryGenerator.TopResourceConsumersSummary(config, out IList<ColumnInfo> columns);
ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns);
string query = TopResourceConsumersQueryGenerator.TopResourceConsumersSummary(config, orderByColumn, requestParams.Descending, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterIntervalStartTime] = config.TimeInterval.StartDateTimeOffset,
[QueryGeneratorUtils.ParameterIntervalEndTime] = config.TimeInterval.EndDateTimeOffset
};
if (!config.ReturnAllQueries)
{
sqlParams[QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query,
};
}, requestContext);
}
internal async Task HandleGetTopResourceConsumersDetailedSummaryReportRequest(GetTopResourceConsumersReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
TopResourceConsumersConfiguration config = requestParams.Convert();
TopResourceConsumersQueryGenerator.TopResourceConsumersDetailedSummary(GetAvailableMetrics(requestParams), config, out IList<ColumnInfo> columns);
ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns);
string query = TopResourceConsumersQueryGenerator.TopResourceConsumersDetailedSummary(GetAvailableMetrics(requestParams), config, orderByColumn, requestParams.Descending, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterIntervalStartTime] = config.TimeInterval.StartDateTimeOffset,
[QueryGeneratorUtils.ParameterIntervalEndTime] = config.TimeInterval.EndDateTimeOffset
};
if (!config.ReturnAllQueries)
{
sqlParams[QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#region Forced Plans report
internal async Task HandleGetForcedPlanQueriesReportRequest(GetForcedPlanQueriesReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
ForcedPlanQueriesConfiguration config = requestParams.Convert();
ForcedPlanQueriesQueryGenerator.ForcedPlanQueriesSummary(config, out IList<ColumnInfo> columns);
ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns);
string query = ForcedPlanQueriesQueryGenerator.ForcedPlanQueriesSummary(config, orderByColumn, requestParams.Descending, out IList<ColumnInfo> _);
if (!config.ReturnAllQueries)
{
query = PrependSqlParameters(query, new() { [QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned.ToString() });
}
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#region Tracked Queries report
internal async Task HandleGetTrackedQueriesReportRequest(GetTrackedQueriesReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
string query = QueryIDSearchQueryGenerator.GetQuery();
query = PrependSqlParameters(query, new() { [QueryIDSearchQueryGenerator.QuerySearchTextParameter] = requestParams.QuerySearchText });
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#region High Variation Queries report
internal async Task HandleGetHighVariationQueriesSummaryReportRequest(GetHighVariationQueriesReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
HighVariationConfiguration config = requestParams.Convert();
HighVariationQueryGenerator.HighVariationSummary(config, out IList<ColumnInfo> columns);
ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns);
string query = HighVariationQueryGenerator.HighVariationSummary(config, orderByColumn, requestParams.Descending, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterIntervalStartTime] = config.TimeInterval.StartDateTimeOffset,
[QueryGeneratorUtils.ParameterIntervalEndTime] = config.TimeInterval.EndDateTimeOffset
};
if (!config.ReturnAllQueries)
{
sqlParams[QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
internal async Task HandleGetHighVariationQueriesDetailedSummaryReportRequest(GetHighVariationQueriesReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
HighVariationConfiguration config = requestParams.Convert();
IList<Metric> availableMetrics = GetAvailableMetrics(requestParams);
HighVariationQueryGenerator.HighVariationDetailedSummary(availableMetrics, config, out IList<ColumnInfo> columns);
ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns);
string query = HighVariationQueryGenerator.HighVariationDetailedSummary(availableMetrics, config, orderByColumn, requestParams.Descending, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterIntervalStartTime] = config.TimeInterval.StartDateTimeOffset,
[QueryGeneratorUtils.ParameterIntervalEndTime] = config.TimeInterval.EndDateTimeOffset
};
if (!config.ReturnAllQueries)
{
sqlParams[QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#region Overall Resource Consumption report
internal async Task HandleGetOverallResourceConsumptionReportRequest(GetOverallResourceConsumptionReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
OverallResourceConsumptionConfiguration config = requestParams.Convert();
string query = OverallResourceConsumptionQueryGenerator.GenerateQuery(GetAvailableMetrics(requestParams), config, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterIntervalStartTime] = config.SpecifiedTimeInterval.StartDateTimeOffset,
[QueryGeneratorUtils.ParameterIntervalEndTime] = config.SpecifiedTimeInterval.EndDateTimeOffset
};
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#region Regressed Queries report
internal async Task HandleGetRegressedQueriesSummaryReportRequest(GetRegressedQueriesReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
RegressedQueriesConfiguration config = requestParams.Convert();
string query = RegressedQueriesQueryGenerator.RegressedQuerySummary(config, out _);
Dictionary<string, object> sqlParams = new()
{
[RegressedQueriesQueryGenerator.ParameterRecentStartTime] = config.TimeIntervalRecent.StartDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterRecentEndTime] = config.TimeIntervalRecent.EndDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterHistoryStartTime] = config.TimeIntervalHistory.StartDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterHistoryEndTime] = config.TimeIntervalHistory.EndDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterMinExecutionCount] = config.MinExecutionCount
};
if (!config.ReturnAllQueries)
{
sqlParams[QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
internal async Task HandleGetRegressedQueriesDetailedSummaryReportRequest(GetRegressedQueriesReportParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
RegressedQueriesConfiguration config = requestParams.Convert();
string query = RegressedQueriesQueryGenerator.RegressedQueryDetailedSummary(GetAvailableMetrics(requestParams), config, out _);
Dictionary<string, object> sqlParams = new()
{
[RegressedQueriesQueryGenerator.ParameterRecentStartTime] = config.TimeIntervalRecent.StartDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterRecentEndTime] = config.TimeIntervalRecent.EndDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterHistoryStartTime] = config.TimeIntervalHistory.StartDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterHistoryEndTime] = config.TimeIntervalHistory.EndDateTimeOffset,
[RegressedQueriesQueryGenerator.ParameterMinExecutionCount] = config.MinExecutionCount
};
if (!config.ReturnAllQueries)
{
sqlParams[QueryGeneratorUtils.ParameterResultsRowCount] = config.TopQueriesReturned;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#region Plan Summary report
internal async Task HandleGetPlanSummaryChartViewRequest(GetPlanSummaryParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
PlanSummaryConfiguration config = requestParams.Convert();
BucketInterval bucketInterval = BucketInterval.Hour;
// if interval is specified then select a 'good' interval
if (config.UseTimeInterval)
{
TimeSpan duration = config.TimeInterval.TimeSpan;
bucketInterval = BucketIntervalUtils.CalculateGoodSubInterval(duration);
}
string query = PlanSummaryQueryGenerator.PlanSummaryChartView(config, bucketInterval, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterQueryId] = config.QueryId
};
if (config.UseTimeInterval)
{
sqlParams[QueryGeneratorUtils.ParameterIntervalStartTime] = config.TimeInterval.StartDateTimeOffset;
sqlParams[QueryGeneratorUtils.ParameterIntervalEndTime] = config.TimeInterval.EndDateTimeOffset;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
internal async Task HandleGetPlanSummaryGridViewRequest(GetPlanSummaryGridViewParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
PlanSummaryConfiguration config = requestParams.Convert();
PlanSummaryQueryGenerator.PlanSummaryGridView(config, out IList<ColumnInfo> columns);
ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns);
string query = PlanSummaryQueryGenerator.PlanSummaryGridView(config, orderByColumn, requestParams.Descending, out _);
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterQueryId] = config.QueryId
};
if (config.UseTimeInterval)
{
sqlParams[QueryGeneratorUtils.ParameterIntervalStartTime] = config.TimeInterval.StartDateTimeOffset;
sqlParams[QueryGeneratorUtils.ParameterIntervalEndTime] = config.TimeInterval.EndDateTimeOffset;
}
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
internal async Task HandleGetForcedPlanRequest(GetForcedPlanParams requestParams, RequestContext<QueryStoreQueryResult> requestContext)
{
await RunWithErrorHandling(() =>
{
string query = PlanSummaryQueryGenerator.GetForcedPlanQuery();
Dictionary<string, object> sqlParams = new()
{
[QueryGeneratorUtils.ParameterQueryId] = requestParams.QueryId,
[QueryGeneratorUtils.ParameterPlanId] = requestParams.PlanId,
};
query = PrependSqlParameters(query, sqlParams);
return new QueryStoreQueryResult()
{
Success = true,
ErrorMessage = null,
Query = query
};
}, requestContext);
}
#endregion
#endregion
#region Helpers
private ColumnInfo GetOrderByColumn(IOrderableQueryParams requestParams, IList<ColumnInfo> columnInfoList)
{
return requestParams.GetOrderByColumnId() != null ? columnInfoList.First(col => col.GetQueryColumnLabel() == requestParams.GetOrderByColumnId()) : columnInfoList[0];
}
internal virtual IList<Metric> GetAvailableMetrics(QueryStoreReportParams requestParams)
{
ConnectionService.TryFindConnection(requestParams.ConnectionOwnerUri, out ConnectionInfo connectionInfo);
if (connectionInfo != null)
{
using (SqlConnection connection = ConnectionService.OpenSqlConnection(connectionInfo, "QueryStoreService available metrics"))
{
return QdsMetadataMapper.GetAvailableMetrics(connection);
}
}
else
{
throw new InvalidOperationException($"Unable to find connection for '{requestParams.ConnectionOwnerUri}'");
}
}
/// <summary>
/// Prepends declarations and definitions of <paramref name="sqlParams"/> to <paramref name="query"/>
/// </summary>
/// <param name="query"></param>
/// <param name="sqlParams"></param>
/// <returns></returns>
private static string PrependSqlParameters(string query, Dictionary<string, object> sqlParams)
{
StringBuilder sb = new StringBuilder();
foreach (string key in sqlParams.Keys)
{
sb.AppendLine($"DECLARE {key} {GetTSqlRepresentation(sqlParams[key])};");
}
sb.AppendLine();
sb.AppendLine(query);
return sb.ToString().Trim();
}
/// <summary>
/// Converts an object (that would otherwise be set as a SqlParameter value) to an entirely TSQL representation.
/// Only handles the same subset of object types that Query Store query generators use:
/// int, long, string, and DateTimeOffset
/// </summary>
/// <param name="paramValue"></param>
/// <returns>data type and value portions of a parameter declaration, in the form "INT = 999"</returns>
internal static string GetTSqlRepresentation(object paramValue)
{
switch (paramValue)
{
case int i:
return $"INT = {i}";
case long l:
return $"BIGINT = {l}";
case string s:
return $"NVARCHAR(max) = N'{s.Replace("'", "''")}'";
case DateTimeOffset dto:
return $"DATETIMEOFFSET = '{dto.ToString("O", CultureInfo.InvariantCulture)}'"; // "O" = ISO 8601 standard datetime format
default:
Debug.Fail($"Unhandled TSQL parameter type: '{paramValue.GetType()}'");
return $"= {paramValue}";
}
}
#endregion
}
}