diff --git a/Packages.props b/Packages.props index cdd5afdb7c..61be96ad98 100644 --- a/Packages.props +++ b/Packages.props @@ -29,6 +29,7 @@ + diff --git a/bin/nuget/Microsoft.SqlServer.Management.QueryStoreModel.163.26.1.nupkg b/bin/nuget/Microsoft.SqlServer.Management.QueryStoreModel.163.26.1.nupkg new file mode 100644 index 0000000000..ba61aa4023 Binary files /dev/null and b/bin/nuget/Microsoft.SqlServer.Management.QueryStoreModel.163.26.1.nupkg differ diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index 1fca38afb3..fd066b5da3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -32,6 +32,7 @@ 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 @@ private static void InitializeRequestHandlersAndServices(ServiceHost serviceHost SqlProjectsService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(SqlProjectsService.Instance); + QueryStoreService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(QueryStoreService.Instance); + serviceHost.InitializeRequestHandlers(); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index fcf8bc2da8..378cd2490c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/BasicTimeInterval.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/BasicTimeInterval.cs new file mode 100644 index 0000000000..3eb77cd77f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/BasicTimeInterval.cs @@ -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 +{ + /// + /// Represents a TimeInterval with strings for the start and end times instead of DateTimeOffsets for JRPC compatibility + /// + public class BasicTimeInterval + { + /// + /// Start time of this time interval, in ISO 8601 format (ToString("O")). + /// This property is ignored unless TimeIntervalOptions is set to Custom. + /// + public string StartDateTimeInUtc { get; set; } = null; + + /// + /// End time of this time interval, in ISO 8601 format (ToString("O")). + /// This property is ignored unless TimeIntervalOptions is set to Custom. + /// + public string EndDateTimeInUtc { get; set; } = null; + + /// + /// Time interval type. Unless set to Custom, then StartDateTimeInUtc and EndDateTimeInUtc are ignored. + /// + 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}'"); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetForcedPlanQueriesReport.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetForcedPlanQueriesReport.cs new file mode 100644 index 0000000000..f6a9e0426e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetForcedPlanQueriesReport.cs @@ -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 +{ + /// + /// Parameters for getting a Forced Plan Queries report + /// + public class GetForcedPlanQueriesReportParams : OrderableQueryConfigurationParams + { + /// + /// Time interval for the report + /// + public BasicTimeInterval TimeInterval { get; set; } + + public override ForcedPlanQueriesConfiguration Convert() + { + ForcedPlanQueriesConfiguration config = base.Convert(); + config.TimeInterval = TimeInterval.Convert(); + + return config; + } + } + + /// + /// Gets the query for a Forced Plan Queries report + /// + public class GetForcedPlanQueriesReportRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getForcedPlanQueriesReport"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetHighVariationQueriesReport.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetHighVariationQueriesReport.cs new file mode 100644 index 0000000000..bf274250b5 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetHighVariationQueriesReport.cs @@ -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 +{ + /// + /// Parameters for getting a High Variation Queries report + /// + public class GetHighVariationQueriesReportParams : OrderableQueryConfigurationParams + { + /// + /// Time interval for the report + /// + public BasicTimeInterval TimeInterval { get; set; } + + public override HighVariationConfiguration Convert() + { + HighVariationConfiguration config = base.Convert(); + config.TimeInterval = TimeInterval.Convert(); + + return config; + } + } + + /// + /// Gets the query for a High Variation Queries report + /// + public class GetHighVariationQueriesSummaryRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getHighVariationQueriesSummary"); + } + + /// + /// Gets the query for a detailed High Variation Queries report + /// + public class GetHighVariationQueriesDetailedSummaryRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getHighVariationQueriesDetailedSummary"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetOverallResourceConsumptionReport.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetOverallResourceConsumptionReport.cs new file mode 100644 index 0000000000..8a2ae01752 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetOverallResourceConsumptionReport.cs @@ -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 +{ + /// + /// Parameters for getting an Overall Resource Consumption report + /// + public class GetOverallResourceConsumptionReportParams : QueryConfigurationParams + { + /// + /// Time interval for the report + /// + public BasicTimeInterval SpecifiedTimeInterval { get; set; } + + /// + /// Bucket interval for the report + /// + public BucketInterval SpecifiedBucketInterval { get; set; } + + public override OverallResourceConsumptionConfiguration Convert() + { + OverallResourceConsumptionConfiguration result = base.Convert(); + + result.SpecifiedTimeInterval = SpecifiedTimeInterval.Convert(); + result.SelectedBucketInterval = SpecifiedBucketInterval; + + return result; + } + } + + /// + /// Gets the query for an Overall Resource Consumption report + /// + public class GetOverallResourceConsumptionReportRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getOverallResourceConsumptionReport"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetRegressedQueriesReport.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetRegressedQueriesReport.cs new file mode 100644 index 0000000000..decb820fc5 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetRegressedQueriesReport.cs @@ -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 +{ + /// + /// Parameters for getting a Regressed Queries report + /// + public class GetRegressedQueriesReportParams : QueryConfigurationParams + { + /// + /// Time interval during which to look for performance regressions for the report + /// + public BasicTimeInterval TimeIntervalRecent { get; set; } + + /// + /// Time interval during which to establish baseline performance for the report + /// + public BasicTimeInterval TimeIntervalHistory { get; set; } + + /// + /// Minimum number of executions for a query to be included + /// + 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; + } + } + + /// + /// Gets the query for a Regressed Queries report + /// + public class GetRegressedQueriesSummaryRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getRegressedQueriesSummary"); + } + + /// + /// Gets the query for a detailed Regressed Queries report + /// + public class GetRegressedQueriesDetailedSummaryRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getRegressedQueriesDetailedSummary"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetTopResourceConsumersReport.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetTopResourceConsumersReport.cs new file mode 100644 index 0000000000..dc8a32cc9e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetTopResourceConsumersReport.cs @@ -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 +{ + /// + /// Parameters for getting a Top Resource Consumers report + /// + public class GetTopResourceConsumersReportParams : OrderableQueryConfigurationParams + { + /// + /// Time interval for the report + /// + public BasicTimeInterval TimeInterval { get; set; } + + public override TopResourceConsumersConfiguration Convert() + { + TopResourceConsumersConfiguration result = base.Convert(); + result.TimeInterval = TimeInterval.Convert(); + + return result; + } + } + + /// + /// Gets the query for a Top Resource Consumers report + /// + public class GetTopResourceConsumersSummaryRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getTopResourceConsumersSummary"); + } + + /// + /// Gets the query for a detailed Top Resource Consumers report + /// + public class GetTopResourceConsumersDetailedSummaryRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getTopResourceConsumersDetailedSummary"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetTrackedQueryReport.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetTrackedQueryReport.cs new file mode 100644 index 0000000000..ce9d373fbe --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/GetTrackedQueryReport.cs @@ -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 +{ + /// + /// Parameters for getting a Tracked Queries report + /// + public class GetTrackedQueriesReportParams + { + /// + /// Search text for a query + /// + public string QuerySearchText { get; set; } + } + + /// + /// Gets the query for a Tracked Queries report + /// + public class GetTrackedQueriesReportRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getTrackedQueriesReport"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/PlanSummaryReportParams.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/PlanSummaryReportParams.cs new file mode 100644 index 0000000000..02e3b76eec --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/PlanSummaryReportParams.cs @@ -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 +{ + /// + /// Parameters for getting a Plan Summary + /// + public class GetPlanSummaryParams : TypedQueryStoreReportParams + { + /// + /// Query ID to view a summary of plans for + /// + public long QueryId { get; set; } + + /// + /// Mode of the time interval search + /// + public PlanTimeIntervalMode TimeIntervalMode { get; set; } + + /// + /// Time interval for the report + /// + public BasicTimeInterval TimeInterval { get; set; } + + /// + /// Metric to summarize + /// + public Metric SelectedMetric { get; set; } + + /// + /// Statistic to calculate on SelecticMetric + /// + public Statistic SelectedStatistic { get; set; } + + public override PlanSummaryConfiguration Convert() => new() + { + QueryId = QueryId, + TimeIntervalMode = TimeIntervalMode, + TimeInterval = TimeInterval.Convert(), + SelectedMetric = SelectedMetric, + SelectedStatistic = SelectedStatistic + }; + } + + /// + /// Parameters for getting the grid view of a Plan Summary + /// + public class GetPlanSummaryGridViewParams : GetPlanSummaryParams, IOrderableQueryParams + { + /// + /// Name of the column to order results by + /// + public string OrderByColumnId { get; set; } + + /// + /// Direction of the result ordering + /// + public bool Descending { get; set; } + + public string GetOrderByColumnId() => OrderByColumnId; + } + + /// + /// Parameters for getting the forced plan for a query + /// + public class GetForcedPlanParams : QueryStoreReportParams + { + /// + /// Query ID to view the plan for + /// + public long QueryId { get; set; } + + /// + /// Plan ID to view + /// + public long PlanId { get; set; } + } + + /// + /// Gets the query for a Plan Summary chart view + /// + public class GetPlanSummaryChartViewRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getPlanSummaryChartView"); + } + + /// + /// Gets the query for a Plan Summary grid view + /// + public class GetPlanSummaryGridViewRequest + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getPlanSummaryGridView"); + } + + /// + /// Gets the query to view a forced plan + /// + public class GetForcedPlanRequest // there's also GetForcedPlanQueries (plural) in QSM; how is that not confusing... + { + public static readonly RequestType Type + = RequestType.Create("queryStore/getForcedPlan"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/QueryStoreReportParams.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/QueryStoreReportParams.cs new file mode 100644 index 0000000000..2adcb500ec --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/Contracts/QueryStoreReportParams.cs @@ -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 +{ + /// + /// Base class for a Query Store report parameters + /// + public abstract class QueryStoreReportParams + { + /// + /// Connection URI for the database + /// + public string ConnectionOwnerUri { get; set; } + } + + /// + /// Base class for Query Store report parameters that can be converted to a configuration object for use in QSM query generators + /// + /// + public abstract class TypedQueryStoreReportParams : QueryStoreReportParams + { + /// + /// Converts this SQL Tools Service parameter object to the QSM configuration object + /// + /// + public abstract T Convert(); + } + + /// + /// Base class for parameters for a report type that uses QueryConfigurationBase for its configuration + /// + /// + public abstract class QueryConfigurationParams : TypedQueryStoreReportParams where T : QueryConfigurationBase, new() + { + /// + /// Metric to summarize + /// + public Metric SelectedMetric { get; set; } + + /// + /// Statistic to calculate on SelecticMetric + /// + public Statistic SelectedStatistic { get; set; } + + /// + /// Number of queries to return if ReturnAllQueries is not set + /// + public int TopQueriesReturned { get; set; } + + /// + /// True to include all queries in the report; false to only include the top queries, up to the value specified by TopQueriesReturned + /// + public bool ReturnAllQueries { get; set; } + + /// + /// Minimum number of query plans for a query to included in the report + /// + public int MinNumberOfQueryPlans { get; set; } + + public override T Convert() => new T() + { + SelectedMetric = SelectedMetric, + SelectedStatistic = SelectedStatistic, + TopQueriesReturned = TopQueriesReturned, + ReturnAllQueries = ReturnAllQueries, + MinNumberOfQueryPlans = MinNumberOfQueryPlans + }; + } + + /// + /// Base class for parameters for a report that can be ordered by a specified column + /// + /// + public abstract class OrderableQueryConfigurationParams : QueryConfigurationParams, IOrderableQueryParams where T : QueryConfigurationBase, new() + { + /// + /// Name of the column to order results by + /// + public string OrderByColumnId { get; set; } + + /// + /// Direction of the result ordering + /// + public bool Descending { get; set; } + + /// + /// Gets the name of the column to order the report results by + /// + /// + public string GetOrderByColumnId() => OrderByColumnId; + } + + /// + /// Result containing a finalized query for a report + /// + public class QueryStoreQueryResult : ResultStatus + { + /// + /// Finalized query for a report + /// + public string Query { get; set; } + } + + /// + /// Interface for parameters for a report that can be ordered by a specific column + /// + public interface IOrderableQueryParams + { + /// + /// Gets the name of the column to order the report results by + /// + /// + string GetOrderByColumnId(); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryStore/QueryStoreService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/QueryStoreService.cs new file mode 100644 index 0000000000..db167da4f2 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryStore/QueryStoreService.cs @@ -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 +{ + /// + /// Main class for Query Store service + /// + public class QueryStoreService : BaseService + { + private static readonly Lazy instance = new Lazy(() => new QueryStoreService()); + + /// + /// Gets the singleton instance object + /// + public static QueryStoreService Instance => instance.Value; + + /// + /// Instance of the connection service, used to get the connection info for a given owner URI + /// + private ConnectionService ConnectionService { get; } + + public QueryStoreService() + { + ConnectionService = ConnectionService.Instance; + } + + /// + /// Initializes the service instance + /// + /// + 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 requestContext) + { + await RunWithErrorHandling(() => + { + TopResourceConsumersConfiguration config = requestParams.Convert(); + TopResourceConsumersQueryGenerator.TopResourceConsumersSummary(config, out IList columns); + ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns); + + string query = TopResourceConsumersQueryGenerator.TopResourceConsumersSummary(config, orderByColumn, requestParams.Descending, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + TopResourceConsumersConfiguration config = requestParams.Convert(); + TopResourceConsumersQueryGenerator.TopResourceConsumersDetailedSummary(GetAvailableMetrics(requestParams), config, out IList columns); + ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns); + + string query = TopResourceConsumersQueryGenerator.TopResourceConsumersDetailedSummary(GetAvailableMetrics(requestParams), config, orderByColumn, requestParams.Descending, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + ForcedPlanQueriesConfiguration config = requestParams.Convert(); + ForcedPlanQueriesQueryGenerator.ForcedPlanQueriesSummary(config, out IList columns); + ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns); + + string query = ForcedPlanQueriesQueryGenerator.ForcedPlanQueriesSummary(config, orderByColumn, requestParams.Descending, out IList _); + + 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 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 requestContext) + { + await RunWithErrorHandling(() => + { + HighVariationConfiguration config = requestParams.Convert(); + HighVariationQueryGenerator.HighVariationSummary(config, out IList columns); + ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns); + + string query = HighVariationQueryGenerator.HighVariationSummary(config, orderByColumn, requestParams.Descending, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + HighVariationConfiguration config = requestParams.Convert(); + IList availableMetrics = GetAvailableMetrics(requestParams); + HighVariationQueryGenerator.HighVariationDetailedSummary(availableMetrics, config, out IList columns); + ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns); + + string query = HighVariationQueryGenerator.HighVariationDetailedSummary(availableMetrics, config, orderByColumn, requestParams.Descending, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + OverallResourceConsumptionConfiguration config = requestParams.Convert(); + string query = OverallResourceConsumptionQueryGenerator.GenerateQuery(GetAvailableMetrics(requestParams), config, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + RegressedQueriesConfiguration config = requestParams.Convert(); + string query = RegressedQueriesQueryGenerator.RegressedQuerySummary(config, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + RegressedQueriesConfiguration config = requestParams.Convert(); + string query = RegressedQueriesQueryGenerator.RegressedQueryDetailedSummary(GetAvailableMetrics(requestParams), config, out _); + + Dictionary 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 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 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 requestContext) + { + await RunWithErrorHandling(() => + { + PlanSummaryConfiguration config = requestParams.Convert(); + + PlanSummaryQueryGenerator.PlanSummaryGridView(config, out IList columns); + ColumnInfo orderByColumn = GetOrderByColumn(requestParams, columns); + + string query = PlanSummaryQueryGenerator.PlanSummaryGridView(config, orderByColumn, requestParams.Descending, out _); + + Dictionary 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 requestContext) + { + await RunWithErrorHandling(() => + { + string query = PlanSummaryQueryGenerator.GetForcedPlanQuery(); + Dictionary 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 columnInfoList) + { + return requestParams.GetOrderByColumnId() != null ? columnInfoList.First(col => col.GetQueryColumnLabel() == requestParams.GetOrderByColumnId()) : columnInfoList[0]; + } + + internal virtual IList 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}'"); + } + } + + /// + /// Prepends declarations and definitions of to + /// + /// + /// + /// + private static string PrependSqlParameters(string query, Dictionary 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(); + } + + /// + /// 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 + /// + /// + /// data type and value portions of a parameter declaration, in the form "INT = 999" + 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 + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryStore/QueryStoreBaselines.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryStore/QueryStoreBaselines.cs new file mode 100644 index 0000000000..8e7a1394f5 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryStore/QueryStoreBaselines.cs @@ -0,0 +1,849 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryStore +{ + internal static class QueryStoreBaselines + { + public const string HandleGetTopResourceConsumersSummaryReportRequest = +@"DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +With wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.wait_category, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time_ms))*1,2) min_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time_ms))*1,2) max_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.wait_category +) +SELECT + p.query_id query_id, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE NOT (ws.first_execution_time > @interval_end_time OR ws.last_execution_time < @interval_start_time) +GROUP BY p.query_id, qt.query_sql_text, q.object_id +HAVING COUNT(distinct p.plan_id) >= 1 +ORDER BY query_id DESC"; + + public const string HandleGetTopResourceConsumersDetailedSummaryReportRequest = +@"DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +With wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.wait_category, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.wait_category +), +top_wait_stats AS +( +SELECT + p.query_id query_id, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE NOT (ws.first_execution_time > @interval_end_time OR ws.last_execution_time < @interval_start_time) +GROUP BY p.query_id, qt.query_sql_text, q.object_id +), +top_other_stats AS +( +SELECT + p.query_id query_id, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_clr_time*rs.stdev_clr_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_clr_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_cpu_time*rs.stdev_cpu_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_cpu_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_dop*rs.stdev_dop*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_dop, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_duration*rs.stdev_duration*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_duration, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_reads*rs.stdev_logical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_writes*rs.stdev_logical_io_writes*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_writes, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_log_bytes_used*rs.stdev_log_bytes_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.0009765625,2) stdev_log_bytes_used, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_query_max_used_memory*rs.stdev_query_max_used_memory*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_query_max_used_memory, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_physical_io_reads*rs.stdev_physical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_physical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_rowcount*rs.stdev_rowcount*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_rowcount, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_tempdb_space_used*rs.stdev_tempdb_space_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_tempdb_space_used, + SUM(rs.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_runtime_stats rs + JOIN sys.query_store_plan p ON p.plan_id = rs.plan_id + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE NOT (rs.first_execution_time > @interval_end_time OR rs.last_execution_time < @interval_start_time) +GROUP BY p.query_id, qt.query_sql_text, q.object_id +) +SELECT + A.query_id query_id, + A.object_id object_id, + A.object_name object_name, + A.query_sql_text query_sql_text, + A.stdev_clr_time stdev_clr_time, + A.stdev_cpu_time stdev_cpu_time, + A.stdev_dop stdev_dop, + A.stdev_duration stdev_duration, + A.stdev_logical_io_reads stdev_logical_io_reads, + A.stdev_logical_io_writes stdev_logical_io_writes, + A.stdev_log_bytes_used stdev_log_bytes_used, + A.stdev_query_max_used_memory stdev_query_max_used_memory, + A.stdev_physical_io_reads stdev_physical_io_reads, + A.stdev_rowcount stdev_rowcount, + A.stdev_tempdb_space_used stdev_tempdb_space_used, + ISNULL(B.stdev_query_wait_time,0) stdev_query_wait_time, + A.count_executions count_executions, + A.num_plans num_plans +FROM top_other_stats A LEFT JOIN top_wait_stats B on A.query_id = B.query_id and A.query_sql_text = B.query_sql_text and A.object_id = B.object_id +WHERE A.num_plans >= 1 +ORDER BY query_id DESC"; + + public const string HandleGetForcedPlanQueriesReportRequest = + @"WITH +A AS +( +SELECT + p.query_id query_id, + qt.query_sql_text query_sql_text, + p.plan_id plan_id, + p.force_failure_count force_failure_count, + p.last_force_failure_reason_desc last_force_failure_reason_desc, + p.last_execution_time last_execution_time, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + p.last_compile_start_time last_compile_start_time +FROM sys.query_store_plan p + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +where p.is_forced_plan = 1 +), +B AS +( +SELECT + p.query_id query_id, + MAX(p.last_execution_time) last_execution_time, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_plan p +GROUP BY p.query_id +HAVING MAX(CAST(p.is_forced_plan AS tinyint)) = 1 +) +SELECT + A.query_id, + A.query_sql_text, + A.plan_id, + A.force_failure_count, + A.last_compile_start_time, + A.last_force_failure_reason_desc, + B.num_plans, + B.last_execution_time, + A.last_execution_time, + A.object_id, + A.object_name +FROM A JOIN B ON A.query_id = B.query_id +WHERE B.num_plans >= 1 +ORDER BY query_id DESC"; + + public const string HandleGetTrackedQueriesReportRequest = +@"DECLARE @QuerySearchText NVARCHAR(max) = N'test search text'; + +SELECT TOP 500 q.query_id, q.query_text_id, qt.query_sql_text +FROM sys.query_store_query_text qt JOIN sys.query_store_query q ON q.query_text_id = qt.query_text_id +WHERE qt.query_sql_text LIKE ('%' + @QuerySearchText + '%')"; + + public const string HandleGetHighVariationQueriesSummaryReportRequest = +@"DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +With wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.wait_category, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time_ms))*1,2) min_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time_ms))*1,2) max_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.wait_category +) +SELECT + p.query_id query_id, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.avg_query_wait_time*ws.count_executions))/NULLIF(SUM(ws.count_executions), 0)*1,2) avg_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE NOT (ws.first_execution_time > @interval_end_time OR ws.last_execution_time < @interval_start_time) +GROUP BY p.query_id, qt.query_sql_text, q.object_id +HAVING COUNT(distinct p.plan_id) >= 1 AND SUM(ws.count_executions) > 1 +ORDER BY query_id DESC"; + + public const string HandleGetHighVariationQueriesDetailedSummaryReportRequest = +@"DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +With wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.wait_category, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.wait_category +), +wait_stats_variation AS +( +SELECT + p.query_id query_id, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE NOT (ws.first_execution_time > @interval_end_time OR ws.last_execution_time < @interval_start_time) +GROUP BY p.query_id, qt.query_sql_text, q.object_id +), +other_stats_variation AS +( +SELECT + p.query_id query_id, + q.object_id object_id, + ISNULL(OBJECT_NAME(q.object_id),'') object_name, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_clr_time*rs.stdev_clr_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_clr_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_cpu_time*rs.stdev_cpu_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_cpu_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_dop*rs.stdev_dop*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_dop, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_duration*rs.stdev_duration*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_duration, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_reads*rs.stdev_logical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_writes*rs.stdev_logical_io_writes*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_writes, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_log_bytes_used*rs.stdev_log_bytes_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.0009765625,2) stdev_log_bytes_used, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_query_max_used_memory*rs.stdev_query_max_used_memory*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_query_max_used_memory, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_physical_io_reads*rs.stdev_physical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_physical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_rowcount*rs.stdev_rowcount*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_rowcount, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_tempdb_space_used*rs.stdev_tempdb_space_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_tempdb_space_used, + SUM(rs.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_runtime_stats rs + JOIN sys.query_store_plan p ON p.plan_id = rs.plan_id + JOIN sys.query_store_query q ON q.query_id = p.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE NOT (rs.first_execution_time > @interval_end_time OR rs.last_execution_time < @interval_start_time) +GROUP BY p.query_id, qt.query_sql_text, q.object_id +) +SELECT + A.query_id query_id, + A.object_id object_id, + A.object_name object_name, + A.query_sql_text query_sql_text, + A.stdev_clr_time stdev_clr_time, + A.stdev_cpu_time stdev_cpu_time, + A.stdev_dop stdev_dop, + A.stdev_duration stdev_duration, + A.stdev_logical_io_reads stdev_logical_io_reads, + A.stdev_logical_io_writes stdev_logical_io_writes, + A.stdev_log_bytes_used stdev_log_bytes_used, + A.stdev_query_max_used_memory stdev_query_max_used_memory, + A.stdev_physical_io_reads stdev_physical_io_reads, + A.stdev_rowcount stdev_rowcount, + A.stdev_tempdb_space_used stdev_tempdb_space_used, + ISNULL(B.stdev_query_wait_time,0) stdev_query_wait_time, + A.count_executions count_executions, + A.num_plans num_plans +FROM other_stats_variation A LEFT JOIN wait_stats_variation B on A.query_id = B.query_id and A.query_sql_text = B.query_sql_text and A.object_id = B.object_id +WHERE A.num_plans >= 1 AND A.count_executions > 1 +ORDER BY query_id DESC"; + + public const string HandleGetOverallResourceConsumptionReportRequest = +@"DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +WITH DateGenerator AS +( +SELECT CAST(@interval_start_time AS DATETIME) DatePlaceHolder +UNION ALL +SELECT DATEADD(hh, 1, DatePlaceHolder) +FROM DateGenerator +WHERE DATEADD(hh, 1, DatePlaceHolder) < @interval_end_time +), WaitStats AS +( +SELECT + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY DATEDIFF(hh, 0, itvl.end_time) +), +UnionAll AS +( +SELECT + ROUND(CONVERT(float, SUM(rs.avg_clr_time*rs.count_executions))*0.001,2) as total_clr_time, + ROUND(CONVERT(float, SUM(rs.avg_cpu_time*rs.count_executions))*0.001,2) as total_cpu_time, + ROUND(CONVERT(float, SUM(rs.avg_dop*rs.count_executions))*1,0) as total_dop, + ROUND(CONVERT(float, SUM(rs.avg_duration*rs.count_executions))*0.001,2) as total_duration, + CONVERT(float, SUM(rs.count_executions)) as total_count_executions, + ROUND(CONVERT(float, SUM(rs.avg_logical_io_reads*rs.count_executions))*8,2) as total_logical_io_reads, + ROUND(CONVERT(float, SUM(rs.avg_logical_io_writes*rs.count_executions))*8,2) as total_logical_io_writes, + ROUND(CONVERT(float, SUM(rs.avg_log_bytes_used*rs.count_executions))*0.0009765625,2) as total_log_bytes_used, + ROUND(CONVERT(float, SUM(rs.avg_query_max_used_memory*rs.count_executions))*8,2) as total_query_max_used_memory, + ROUND(CONVERT(float, SUM(rs.avg_physical_io_reads*rs.count_executions))*8,2) as total_physical_io_reads, + ROUND(CONVERT(float, SUM(rs.avg_rowcount*rs.count_executions))*1,0) as total_rowcount, + ROUND(CONVERT(float, SUM(rs.avg_tempdb_space_used*rs.count_executions))*8,2) as total_tempdb_space_used, + DATEADD(hh, ((DATEDIFF(hh, 0, rs.last_execution_time))),0 ) as bucket_start, + DATEADD(hh, (1 + (DATEDIFF(hh, 0, rs.last_execution_time))), 0) as bucket_end +FROM sys.query_store_runtime_stats rs +WHERE NOT (rs.first_execution_time > @interval_end_time OR rs.last_execution_time < @interval_start_time) +GROUP BY DATEDIFF(hh, 0, rs.last_execution_time) +) +SELECT + total_clr_time, + total_cpu_time, + total_dop, + total_duration, + total_count_executions, + total_logical_io_reads, + total_logical_io_writes, + total_log_bytes_used, + total_query_max_used_memory, + total_physical_io_reads, + total_rowcount, + total_tempdb_space_used, + total_query_wait_time, + SWITCHOFFSET(bucket_start, DATEPART(tz, @interval_start_time)) , SWITCHOFFSET(bucket_end, DATEPART(tz, @interval_start_time)) +FROM +( +SELECT *, ROW_NUMBER() OVER (PARTITION BY bucket_start ORDER BY bucket_start, total_duration DESC) AS RowNumber +FROM UnionAll , WaitStats +) as UnionAllResults +WHERE UnionAllResults.RowNumber = 1 +OPTION (MAXRECURSION 0)"; + + public const string HandleGetRegressedQueriesSummaryReportRequest = +@"DECLARE @recent_start_time DATETIMEOFFSET = '2023-06-17T11:34:56.0000000+00:00'; +DECLARE @recent_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; +DECLARE @history_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @history_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; +DECLARE @min_exec_count BIGINT = 1; + +WITH wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.wait_category, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time_ms))*1,2) min_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time_ms))*1,2) max_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @history_end_time OR itvl.end_time < @history_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.wait_category +), +hist AS +( +SELECT + p.query_id query_id, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id +WHERE NOT (ws.first_execution_time > @history_end_time OR ws.last_execution_time < @history_start_time) +GROUP BY p.query_id +), +recent AS +( +SELECT + p.query_id query_id, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id +WHERE NOT (ws.first_execution_time > @recent_end_time OR ws.last_execution_time < @recent_start_time) +GROUP BY p.query_id +) +SELECT + results.query_id query_id, + results.object_id object_id, + ISNULL(OBJECT_NAME(results.object_id),'') object_name, + results.query_sql_text query_sql_text, + results.query_wait_time_regr_perc_recent query_wait_time_regr_perc_recent, + results.stdev_query_wait_time_recent stdev_query_wait_time_recent, + results.stdev_query_wait_time_hist stdev_query_wait_time_hist, + ISNULL(results.count_executions_recent, 0) count_executions_recent, + ISNULL(results.count_executions_hist, 0) count_executions_hist, + queries.num_plans num_plans +FROM +( +SELECT + hist.query_id query_id, + q.object_id object_id, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, recent.stdev_query_wait_time-hist.stdev_query_wait_time)/NULLIF(hist.stdev_query_wait_time,0)*100.0, 2) query_wait_time_regr_perc_recent, + ROUND(recent.stdev_query_wait_time, 2) stdev_query_wait_time_recent, + ROUND(hist.stdev_query_wait_time, 2) stdev_query_wait_time_hist, + recent.count_executions count_executions_recent, + hist.count_executions count_executions_hist +FROM hist + JOIN recent ON hist.query_id = recent.query_id + JOIN sys.query_store_query q ON q.query_id = hist.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE + recent.count_executions >= @min_exec_count +) AS results +JOIN +( +SELECT + p.query_id query_id, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_plan p +GROUP BY p.query_id +HAVING COUNT(distinct p.plan_id) >= 1 +) AS queries ON queries.query_id = results.query_id +WHERE query_wait_time_regr_perc_recent > 0 +OPTION (MERGE JOIN)"; + + public const string HandleGetRegressedQueriesDetailedSummaryReportRequest = +@"DECLARE @recent_start_time DATETIMEOFFSET = '2023-06-17T11:34:56.0000000+00:00'; +DECLARE @recent_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; +DECLARE @history_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @history_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; +DECLARE @min_exec_count BIGINT = 1; + +WITH +wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.wait_category, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time_ms))*1,2) min_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time_ms))*1,2) max_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time +FROM sys.query_store_wait_stats ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @history_end_time OR itvl.end_time < @history_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.wait_category +), +wait_stats_hist AS +( +SELECT + p.query_id query_id, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id +WHERE NOT (ws.first_execution_time > @history_end_time OR ws.last_execution_time < @history_start_time) +GROUP BY p.query_id +), +other_hist AS +( +SELECT + p.query_id query_id, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_clr_time*rs.stdev_clr_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_clr_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_cpu_time*rs.stdev_cpu_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_cpu_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_dop*rs.stdev_dop*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_dop, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_duration*rs.stdev_duration*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_duration, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_reads*rs.stdev_logical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_writes*rs.stdev_logical_io_writes*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_writes, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_log_bytes_used*rs.stdev_log_bytes_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.0009765625,2) stdev_log_bytes_used, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_query_max_used_memory*rs.stdev_query_max_used_memory*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_query_max_used_memory, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_physical_io_reads*rs.stdev_physical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_physical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_rowcount*rs.stdev_rowcount*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_rowcount, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_tempdb_space_used*rs.stdev_tempdb_space_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_tempdb_space_used, + SUM(rs.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_runtime_stats rs + JOIN sys.query_store_plan p ON p.plan_id = rs.plan_id +WHERE NOT (rs.first_execution_time > @history_end_time OR rs.last_execution_time < @history_start_time) +GROUP BY p.query_id +), +hist AS +( +SELECT + other_hist.query_id, + other_hist.stdev_clr_time stdev_clr_time, + other_hist.stdev_cpu_time stdev_cpu_time, + other_hist.stdev_dop stdev_dop, + other_hist.stdev_duration stdev_duration, + other_hist.stdev_logical_io_reads stdev_logical_io_reads, + other_hist.stdev_logical_io_writes stdev_logical_io_writes, + other_hist.stdev_log_bytes_used stdev_log_bytes_used, + other_hist.stdev_query_max_used_memory stdev_query_max_used_memory, + other_hist.stdev_physical_io_reads stdev_physical_io_reads, + other_hist.stdev_rowcount stdev_rowcount, + other_hist.stdev_tempdb_space_used stdev_tempdb_space_used, + ISNULL(wait_stats_hist.stdev_query_wait_time, 0) stdev_query_wait_time, + other_hist.count_executions, + wait_stats_hist.count_executions wait_stats_count_executions, + other_hist.num_plans +FROM other_hist + LEFT JOIN wait_stats_hist ON wait_stats_hist.query_id = other_hist.query_id +), +wait_stats_recent AS +( +SELECT + p.query_id query_id, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) stdev_query_wait_time, + MAX(ws.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id +WHERE NOT (ws.first_execution_time > @recent_end_time OR ws.last_execution_time < @recent_start_time) +GROUP BY p.query_id +), +other_recent AS +( +SELECT + p.query_id query_id, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_clr_time*rs.stdev_clr_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_clr_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_cpu_time*rs.stdev_cpu_time*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_cpu_time, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_dop*rs.stdev_dop*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_dop, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_duration*rs.stdev_duration*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.001,2) stdev_duration, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_reads*rs.stdev_logical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_logical_io_writes*rs.stdev_logical_io_writes*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_logical_io_writes, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_log_bytes_used*rs.stdev_log_bytes_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*0.0009765625,2) stdev_log_bytes_used, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_query_max_used_memory*rs.stdev_query_max_used_memory*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_query_max_used_memory, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_physical_io_reads*rs.stdev_physical_io_reads*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_physical_io_reads, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_rowcount*rs.stdev_rowcount*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*1,0) stdev_rowcount, + ROUND(CONVERT(float, SQRT( SUM(rs.stdev_tempdb_space_used*rs.stdev_tempdb_space_used*rs.count_executions)/NULLIF(SUM(rs.count_executions), 0)))*8,2) stdev_tempdb_space_used, + SUM(rs.count_executions) count_executions, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_runtime_stats rs + JOIN sys.query_store_plan p ON p.plan_id = rs.plan_id +WHERE NOT (rs.first_execution_time > @recent_end_time OR rs.last_execution_time < @recent_start_time) +GROUP BY p.query_id +), +recent AS +( +SELECT + other_recent.query_id, + other_recent.stdev_clr_time stdev_clr_time, + other_recent.stdev_cpu_time stdev_cpu_time, + other_recent.stdev_dop stdev_dop, + other_recent.stdev_duration stdev_duration, + other_recent.stdev_logical_io_reads stdev_logical_io_reads, + other_recent.stdev_logical_io_writes stdev_logical_io_writes, + other_recent.stdev_log_bytes_used stdev_log_bytes_used, + other_recent.stdev_query_max_used_memory stdev_query_max_used_memory, + other_recent.stdev_physical_io_reads stdev_physical_io_reads, + other_recent.stdev_rowcount stdev_rowcount, + other_recent.stdev_tempdb_space_used stdev_tempdb_space_used, + ISNULL(wait_stats_recent.stdev_query_wait_time, 0) stdev_query_wait_time, + other_recent.count_executions, + wait_stats_recent.count_executions wait_stats_count_executions, + other_recent.num_plans +FROM other_recent + LEFT JOIN wait_stats_recent ON wait_stats_recent.query_id = other_recent.query_id +) +SELECT + results.query_id query_id, + results.object_id object_id, + ISNULL(OBJECT_NAME(results.object_id),'') object_name, + results.query_sql_text query_sql_text, + results.clr_time_regr_perc_recent clr_time_regr_perc_recent, + results.stdev_clr_time_recent stdev_clr_time_recent, + results.stdev_clr_time_hist stdev_clr_time_hist, + results.cpu_time_regr_perc_recent cpu_time_regr_perc_recent, + results.stdev_cpu_time_recent stdev_cpu_time_recent, + results.stdev_cpu_time_hist stdev_cpu_time_hist, + results.dop_regr_perc_recent dop_regr_perc_recent, + results.stdev_dop_recent stdev_dop_recent, + results.stdev_dop_hist stdev_dop_hist, + results.duration_regr_perc_recent duration_regr_perc_recent, + results.stdev_duration_recent stdev_duration_recent, + results.stdev_duration_hist stdev_duration_hist, + results.logical_io_reads_regr_perc_recent logical_io_reads_regr_perc_recent, + results.stdev_logical_io_reads_recent stdev_logical_io_reads_recent, + results.stdev_logical_io_reads_hist stdev_logical_io_reads_hist, + results.logical_io_writes_regr_perc_recent logical_io_writes_regr_perc_recent, + results.stdev_logical_io_writes_recent stdev_logical_io_writes_recent, + results.stdev_logical_io_writes_hist stdev_logical_io_writes_hist, + results.log_bytes_used_regr_perc_recent log_bytes_used_regr_perc_recent, + results.stdev_log_bytes_used_recent stdev_log_bytes_used_recent, + results.stdev_log_bytes_used_hist stdev_log_bytes_used_hist, + results.query_max_used_memory_regr_perc_recent query_max_used_memory_regr_perc_recent, + results.stdev_query_max_used_memory_recent stdev_query_max_used_memory_recent, + results.stdev_query_max_used_memory_hist stdev_query_max_used_memory_hist, + results.physical_io_reads_regr_perc_recent physical_io_reads_regr_perc_recent, + results.stdev_physical_io_reads_recent stdev_physical_io_reads_recent, + results.stdev_physical_io_reads_hist stdev_physical_io_reads_hist, + results.rowcount_regr_perc_recent rowcount_regr_perc_recent, + results.stdev_rowcount_recent stdev_rowcount_recent, + results.stdev_rowcount_hist stdev_rowcount_hist, + results.tempdb_space_used_regr_perc_recent tempdb_space_used_regr_perc_recent, + results.stdev_tempdb_space_used_recent stdev_tempdb_space_used_recent, + results.stdev_tempdb_space_used_hist stdev_tempdb_space_used_hist, + results.query_wait_time_regr_perc_recent query_wait_time_regr_perc_recent, + results.stdev_query_wait_time_recent stdev_query_wait_time_recent, + results.stdev_query_wait_time_hist stdev_query_wait_time_hist, + ISNULL(results.count_executions_recent, 0) count_executions_recent, + ISNULL(results.count_executions_hist, 0) count_executions_hist, + queries.num_plans num_plans +FROM +( +SELECT + hist.query_id query_id, + q.object_id object_id, + qt.query_sql_text query_sql_text, + ROUND(CONVERT(float, recent.stdev_clr_time-hist.stdev_clr_time)/NULLIF(hist.stdev_clr_time,0)*100.0, 2) clr_time_regr_perc_recent, + ROUND(recent.stdev_clr_time, 2) stdev_clr_time_recent, + ROUND(hist.stdev_clr_time, 2) stdev_clr_time_hist, + ROUND(CONVERT(float, recent.stdev_cpu_time-hist.stdev_cpu_time)/NULLIF(hist.stdev_cpu_time,0)*100.0, 2) cpu_time_regr_perc_recent, + ROUND(recent.stdev_cpu_time, 2) stdev_cpu_time_recent, + ROUND(hist.stdev_cpu_time, 2) stdev_cpu_time_hist, + ROUND(CONVERT(float, recent.stdev_dop-hist.stdev_dop)/NULLIF(hist.stdev_dop,0)*100.0, 2) dop_regr_perc_recent, + ROUND(recent.stdev_dop, 2) stdev_dop_recent, + ROUND(hist.stdev_dop, 2) stdev_dop_hist, + ROUND(CONVERT(float, recent.stdev_duration-hist.stdev_duration)/NULLIF(hist.stdev_duration,0)*100.0, 2) duration_regr_perc_recent, + ROUND(recent.stdev_duration, 2) stdev_duration_recent, + ROUND(hist.stdev_duration, 2) stdev_duration_hist, + ROUND(CONVERT(float, recent.stdev_logical_io_reads-hist.stdev_logical_io_reads)/NULLIF(hist.stdev_logical_io_reads,0)*100.0, 2) logical_io_reads_regr_perc_recent, + ROUND(recent.stdev_logical_io_reads, 2) stdev_logical_io_reads_recent, + ROUND(hist.stdev_logical_io_reads, 2) stdev_logical_io_reads_hist, + ROUND(CONVERT(float, recent.stdev_logical_io_writes-hist.stdev_logical_io_writes)/NULLIF(hist.stdev_logical_io_writes,0)*100.0, 2) logical_io_writes_regr_perc_recent, + ROUND(recent.stdev_logical_io_writes, 2) stdev_logical_io_writes_recent, + ROUND(hist.stdev_logical_io_writes, 2) stdev_logical_io_writes_hist, + ROUND(CONVERT(float, recent.stdev_log_bytes_used-hist.stdev_log_bytes_used)/NULLIF(hist.stdev_log_bytes_used,0)*100.0, 2) log_bytes_used_regr_perc_recent, + ROUND(recent.stdev_log_bytes_used, 2) stdev_log_bytes_used_recent, + ROUND(hist.stdev_log_bytes_used, 2) stdev_log_bytes_used_hist, + ROUND(CONVERT(float, recent.stdev_query_max_used_memory-hist.stdev_query_max_used_memory)/NULLIF(hist.stdev_query_max_used_memory,0)*100.0, 2) query_max_used_memory_regr_perc_recent, + ROUND(recent.stdev_query_max_used_memory, 2) stdev_query_max_used_memory_recent, + ROUND(hist.stdev_query_max_used_memory, 2) stdev_query_max_used_memory_hist, + ROUND(CONVERT(float, recent.stdev_physical_io_reads-hist.stdev_physical_io_reads)/NULLIF(hist.stdev_physical_io_reads,0)*100.0, 2) physical_io_reads_regr_perc_recent, + ROUND(recent.stdev_physical_io_reads, 2) stdev_physical_io_reads_recent, + ROUND(hist.stdev_physical_io_reads, 2) stdev_physical_io_reads_hist, + ROUND(CONVERT(float, recent.stdev_rowcount-hist.stdev_rowcount)/NULLIF(hist.stdev_rowcount,0)*100.0, 2) rowcount_regr_perc_recent, + ROUND(recent.stdev_rowcount, 2) stdev_rowcount_recent, + ROUND(hist.stdev_rowcount, 2) stdev_rowcount_hist, + ROUND(CONVERT(float, recent.stdev_tempdb_space_used-hist.stdev_tempdb_space_used)/NULLIF(hist.stdev_tempdb_space_used,0)*100.0, 2) tempdb_space_used_regr_perc_recent, + ROUND(recent.stdev_tempdb_space_used, 2) stdev_tempdb_space_used_recent, + ROUND(hist.stdev_tempdb_space_used, 2) stdev_tempdb_space_used_hist, + ROUND(CONVERT(float, recent.stdev_query_wait_time-hist.stdev_query_wait_time)/NULLIF(hist.stdev_query_wait_time,0)*100.0, 2) query_wait_time_regr_perc_recent, + ROUND(recent.stdev_query_wait_time, 2) stdev_query_wait_time_recent, + ROUND(hist.stdev_query_wait_time, 2) stdev_query_wait_time_hist, + recent.count_executions count_executions_recent, + hist.count_executions count_executions_hist +FROM hist + JOIN recent ON hist.query_id = recent.query_id + JOIN sys.query_store_query q ON q.query_id = hist.query_id + JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id +WHERE + recent.count_executions >= @min_exec_count +) AS results +JOIN +( +SELECT + p.query_id query_id, + COUNT(distinct p.plan_id) num_plans +FROM sys.query_store_plan p +GROUP BY p.query_id +HAVING COUNT(distinct p.plan_id) >= 1 +) AS queries ON queries.query_id = results.query_id +OPTION (MERGE JOIN)"; + + public const string HandleGetPlanSummaryChartViewRequest = +@"DECLARE @query_id BIGINT = 97; +DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +WITH wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.execution_type, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time_ms))*1,2) min_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time_ms))*1,2) max_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time + FROM + ( + SELECT *, LAST_VALUE(last_query_wait_time_ms) OVER (order by plan_id, runtime_stats_interval_id, execution_type, wait_category) last_query_wait_time + FROM sys.query_store_wait_stats + ) +AS ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.execution_type, ws.wait_category +), + bucketizer as + ( + SELECT + ws.plan_id as plan_id, + ws.execution_type as execution_type, + MAX(ws.count_executions) count_executions, + DATEADD(d, ((DATEDIFF(d, 0, ws.last_execution_time))),0 ) as bucket_start, + DATEADD(d, (1 + (DATEDIFF(d, 0, ws.last_execution_time))), 0) as bucket_end, + ROUND(CONVERT(float, SUM(ws.avg_query_wait_time*ws.count_executions))/NULLIF(SUM(ws.count_executions), 0)*1,2) as avg_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time))*1,2) as max_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time))*1,2) as min_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2) as stdev_query_wait_time, + ISNULL(ROUND(CONVERT(float, (SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0))*SUM(ws.count_executions)) / NULLIF(SUM(ws.avg_query_wait_time*ws.count_executions), 0)),2), 0) as variation_query_wait_time, + ROUND(CONVERT(float, SUM(ws.avg_query_wait_time*ws.count_executions))*1,2) as total_query_wait_time + FROM + wait_stats ws + JOIN sys.query_store_plan p ON p.plan_id = ws.plan_id + WHERE + p.query_id = @query_id + AND NOT (ws.first_execution_time > @interval_end_time OR ws.last_execution_time < @interval_start_time) + GROUP BY + ws.plan_id, + ws.execution_type, + DATEDIFF(d, 0, ws.last_execution_time) + ), + is_forced as + ( + SELECT is_forced_plan, plan_id + FROM sys.query_store_plan + ) +SELECT b.plan_id as plan_id, + is_forced_plan, + execution_type, + count_executions, + SWITCHOFFSET(bucket_start, DATEPART(tz, @interval_start_time)) AS bucket_start, + SWITCHOFFSET(bucket_end, DATEPART(tz, @interval_start_time)) AS bucket_end, + avg_query_wait_time, + max_query_wait_time, + min_query_wait_time, + stdev_query_wait_time, + variation_query_wait_time, + total_query_wait_time +FROM bucketizer b +JOIN is_forced f ON f.plan_id = b.plan_id"; + + public const string HandleGetPlanSummaryGridViewRequest = +@"DECLARE @query_id BIGINT = 97; +DECLARE @interval_start_time DATETIMEOFFSET = '2023-06-10T12:34:56.0000000+00:00'; +DECLARE @interval_end_time DATETIMEOFFSET = '2023-06-17T12:34:56.0000000+00:00'; + +WITH wait_stats AS +( +SELECT + ws.plan_id plan_id, + ws.execution_type, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms)/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))*1,2) avg_query_wait_time, + ROUND(CONVERT(float, MIN(ws.min_query_wait_time_ms))*1,2) min_query_wait_time, + ROUND(CONVERT(float, MAX(ws.max_query_wait_time_ms))*1,2) max_query_wait_time, + ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time_ms*ws.stdev_query_wait_time_ms*(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms))/SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms)))*1,2) stdev_query_wait_time, + ROUND(CONVERT(float, SUM(ws.total_query_wait_time_ms))*1,2) total_query_wait_time, + ROUND(CONVERT(float, MIN(ws.last_query_wait_time))*1,2) last_query_wait_time, + CAST(ROUND(SUM(ws.total_query_wait_time_ms/ws.avg_query_wait_time_ms),0) AS BIGINT) count_executions, + MAX(itvl.end_time) last_execution_time, + MIN(itvl.start_time) first_execution_time + FROM + ( + SELECT *, LAST_VALUE(last_query_wait_time_ms) OVER (order by plan_id, runtime_stats_interval_id, execution_type, wait_category) last_query_wait_time + FROM sys.query_store_wait_stats + ) +AS ws + JOIN sys.query_store_runtime_stats_interval itvl ON itvl.runtime_stats_interval_id = ws.runtime_stats_interval_id +WHERE NOT (itvl.start_time > @interval_end_time OR itvl.end_time < @interval_start_time) +GROUP BY ws.plan_id, ws.runtime_stats_interval_id, ws.execution_type, ws.wait_category +), + last_table AS + ( + SELECT + p.plan_id plan_id, + first_value(ws.last_query_wait_time) OVER (PARTITION BY p.plan_id ORDER BY ws.last_execution_time DESC) last_value + FROM + wait_stats ws + JOIN + sys.query_store_plan p ON p.plan_id = ws.plan_id + WHERE + p.query_id = @query_id + ) +SELECT p.plan_id, + MAX(CONVERT(int, p.is_forced_plan)) is_forced_plan, + SUM(distinct ws.execution_type) execution_type, + MAX(ws.count_executions) count_executions, + ROUND(ROUND(CONVERT(float, MIN(ws.min_query_wait_time))*1,2), 2) min_query_wait_time, + ROUND(ROUND(CONVERT(float, MAX(ws.max_query_wait_time))*1,2), 2) max_query_wait_time, + ROUND(ROUND(CONVERT(float, SUM(ws.avg_query_wait_time*ws.count_executions))/NULLIF(SUM(ws.count_executions), 0)*1,2), 2) avg_query_wait_time, + ROUND(ROUND(CONVERT(float, SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0)))*1,2), 2) stdev_query_wait_time, + ROUND(ISNULL(ROUND(CONVERT(float, (SQRT( SUM(ws.stdev_query_wait_time*ws.stdev_query_wait_time*ws.count_executions)/NULLIF(SUM(ws.count_executions), 0))*SUM(ws.count_executions)) / NULLIF(SUM(ws.avg_query_wait_time*ws.count_executions), 0)),2), 0), 2) variation_query_wait_time, + ROUND(max(l.last_value), 2) last_query_wait_time, + ROUND(ROUND(CONVERT(float, SUM(ws.avg_query_wait_time*ws.count_executions))*1,2), 2) total_query_wait_time, + SWITCHOFFSET(MIN(ws.first_execution_time), DATEPART(tz, @interval_start_time)) first_execution_time, + SWITCHOFFSET(MAX(ws.last_execution_time), DATEPART(tz, @interval_start_time)) last_execution_time +FROM + wait_stats ws +JOIN + sys.query_store_plan p ON p.plan_id = ws.plan_id +JOIN + last_table l ON p.plan_id = l.plan_id +WHERE p.query_id = @query_id + AND NOT (ws.first_execution_time > @interval_end_time OR ws.last_execution_time < @interval_start_time) +GROUP BY p.plan_id, ws.execution_type +ORDER BY count_executions DESC"; + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryStore/QueryStoreTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryStore/QueryStoreTests.cs new file mode 100644 index 0000000000..a762719041 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/QueryStore/QueryStoreTests.cs @@ -0,0 +1,284 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlServer.Management.QueryStoreModel.Common; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.QueryStore; +using Microsoft.SqlTools.ServiceLayer.QueryStore.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking; +using Moq; +using NUnit.Framework; +using static Microsoft.SqlServer.Management.QueryStoreModel.PlanSummary.PlanSummaryConfiguration; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.QueryStore +{ + public class QueryStoreTests : TestBase + { + private const string TestConnectionOwnerUri = "FakeConnectionOwnerUri"; + private static DateTimeOffset TestWindowStart = DateTimeOffset.Parse("6/10/2023 12:34:56 PM +0:00"); + private static DateTimeOffset TestWindowEnd = TestWindowStart.AddDays(7); + private static DateTimeOffset TestWindowRecentStart = TestWindowEnd.AddHours(-1); + private static BasicTimeInterval TestTimeInterval => new BasicTimeInterval() + { + StartDateTimeInUtc = TestWindowStart.ToString("O"), + EndDateTimeInUtc = TestWindowEnd.ToString("O") + }; + + private static BasicTimeInterval RecentTestTimeInterval => new BasicTimeInterval() + { + StartDateTimeInUtc = TestWindowRecentStart.ToString("O"), + EndDateTimeInUtc = TestWindowEnd.ToString("O") + }; + + [SetUp] + public void Setup() + { + QueryStoreCommonConfiguration.DisplayTimeKind = DateTimeKind.Utc; + } + + [Test] + public async Task TopResourceConsumers() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetTopResourceConsumersSummaryReportRequest(new GetTopResourceConsumersReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + OrderByColumnId = "query_id", + Descending = true, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + TimeInterval = TestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetTopResourceConsumersSummaryReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetTopResourceConsumersSummaryReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + + request = new(); + await service.HandleGetTopResourceConsumersDetailedSummaryReportRequest(new GetTopResourceConsumersReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + OrderByColumnId = "query_id", + Descending = true, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + TimeInterval = TestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetTopResourceConsumersDetailedSummaryReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetTopResourceConsumersDetailedSummaryReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + [Test] + public async Task ForcedPlanQueries() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetForcedPlanQueriesReportRequest(new GetForcedPlanQueriesReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + OrderByColumnId = "query_id", + Descending = true, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + TimeInterval = TestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetForcedPlanQueriesReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetForcedPlanQueriesReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + [Test] + public async Task TrackedQueries() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetTrackedQueriesReportRequest(new GetTrackedQueriesReportParams() + { + QuerySearchText = "test search text" + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetTrackedQueriesReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetTrackedQueriesReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + [Test] + public async Task HighVariationQueries() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetHighVariationQueriesSummaryReportRequest(new GetHighVariationQueriesReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + OrderByColumnId = "query_id", + Descending = true, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + TimeInterval = TestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetHighVariationQueriesSummaryReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetHighVariationQueriesSummaryReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + + request = new(); + await service.HandleGetHighVariationQueriesDetailedSummaryReportRequest(new GetHighVariationQueriesReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + OrderByColumnId = "query_id", + Descending = true, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + TimeInterval = TestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetHighVariationQueriesDetailedSummaryReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetHighVariationQueriesDetailedSummaryReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + [Test] + public async Task OverallResourceConsumption() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetOverallResourceConsumptionReportRequest(new GetOverallResourceConsumptionReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + SpecifiedTimeInterval = TestTimeInterval, + SpecifiedBucketInterval = BucketInterval.Hour + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetOverallResourceConsumptionReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetOverallResourceConsumptionReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + [Test] + public async Task RegressedQueries() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetRegressedQueriesSummaryReportRequest(new GetRegressedQueriesReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + MinExecutionCount = 1, + TimeIntervalHistory = TestTimeInterval, + TimeIntervalRecent = RecentTestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetRegressedQueriesSummaryReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetRegressedQueriesSummaryReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + + request = new(); + await service.HandleGetRegressedQueriesDetailedSummaryReportRequest(new GetRegressedQueriesReportParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + ReturnAllQueries = true, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + MinNumberOfQueryPlans = 1, + TopQueriesReturned = 50, + MinExecutionCount = 1, + TimeIntervalHistory = TestTimeInterval, + TimeIntervalRecent = RecentTestTimeInterval + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetRegressedQueriesDetailedSummaryReportRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetRegressedQueriesDetailedSummaryReportRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + [Test] + public async Task PlanSummary() + { + QueryStoreService service = GetMock(); + + MockRequest request = new(); + await service.HandleGetPlanSummaryChartViewRequest(new GetPlanSummaryParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + QueryId = 97, + TimeInterval = TestTimeInterval, + TimeIntervalMode = PlanTimeIntervalMode.SpecifiedRange, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetPlanSummaryChartViewRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetPlanSummaryChartViewRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + + request = new(); + await service.HandleGetPlanSummaryGridViewRequest(new GetPlanSummaryGridViewParams() + { + ConnectionOwnerUri = TestConnectionOwnerUri, + QueryId = 97, + TimeInterval = TestTimeInterval, + TimeIntervalMode = PlanTimeIntervalMode.SpecifiedRange, + SelectedMetric = Metric.WaitTime, + SelectedStatistic = Statistic.Stdev, + OrderByColumnId = "count_executions", + Descending = true + }, request.Object); + + request.AssertSuccess(nameof(service.HandleGetPlanSummaryGridViewRequest)); + Assert.AreEqual(QueryStoreBaselines.HandleGetPlanSummaryGridViewRequest.ReplaceLineEndings(), request.Result.Query.ReplaceLineEndings()); + } + + private QueryStoreService GetMock() + { + Mock mock = new Mock(); + mock.Setup(s => s.GetAvailableMetrics(It.IsAny())) + .Returns(new List() + { + Metric.ClrTime, + Metric.CPUTime, + Metric.Dop, + Metric.Duration, + Metric.ExecutionCount, + Metric.LogicalReads, + Metric.LogicalWrites, + Metric.LogMemoryUsed, + Metric.MemoryConsumption, + Metric.PhysicalReads, + Metric.RowCount, + Metric.TempDbMemoryUsed, + Metric.WaitTime + }); + + return mock.Object; + } + } +}