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;
+ }
+ }
+}