Et af de mere forvirrende problemer at fejlfinde i SQL Server kan være dem, der er relateret til hukommelsesbevillinger. Nogle forespørgsler har brug for mere hukommelse end andre for at udføre, baseret på hvilke handlinger der skal udføres (f.eks. sortering, hash). SQL Servers optimizer estimerer, hvor meget hukommelse der er behov for, og forespørgslen skal opnå hukommelsesbevillingen for at kunne begynde at udføre. Det gælder denne bevilling for varigheden af forespørgselsudførelsen - hvilket betyder, at hvis optimeringsværktøjet overvurderer hukommelsen, kan du løbe ind i samtidighedsproblemer. Hvis det undervurderer hukommelsen, så kan du se spild i tempdb. Ingen af delene er ideel, og når du simpelthen har for mange forespørgsler, der beder om mere hukommelse, end der er tilgængeligt at tildele, vil du se, at RESOURCE_SEMAPHORE venter. Der er flere måder at angribe dette problem på, og en af mine nye foretrukne metoder er at bruge Query Store.
Opsætning
Vi vil bruge en kopi af WideWorldImporters, som jeg har oppustet ved hjælp af DataLoadSimulation.DailyProcessToCreateHistory lagrede procedure. Tabellen Sales.Ordre har omkring 4,6 millioner rækker, og tabellen Salg.Ordrelinjer har omkring 9,2 millioner rækker. Vi vil gendanne sikkerhedskopien og aktivere Query Store og slette alle gamle Query Store-data, så vi ikke ændrer nogen metrics for denne demo.
Påmindelse:Kør ikke ALTER DATABASE
USE [master]; GO RESTORE DATABASE [WideWorldImporters] FROM DISK = N'C:\Backups\WideWorldImporters.bak' WITH FILE = 1, MOVE N'WWI_Primary' TO N'C:\Databases\WideWorldImporters\WideWorldImporters.mdf', MOVE N'WWI_UserData' TO N'C:\Databases\WideWorldImporters\WideWorldImporters_UserData.ndf', MOVE N'WWI_Log' TO N'C:\Databases\WideWorldImporters\WideWorldImporters.ldf', NOUNLOAD, REPLACE, STATS = 5 GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE = ON; GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE ( OPERATION_MODE = READ_WRITE, INTERVAL_LENGTH_MINUTES = 10 ); GO ALTER DATABASE [WideWorldImporters] SET QUERY_STORE CLEAR; GO
Den lagrede procedure, vi vil bruge til at teste forespørgsler om de førnævnte ordrer og ordrelinjer baseret på et datointerval:
USE [WideWorldImporters]; GO DROP PROCEDURE IF EXISTS [Sales].[usp_OrderInfo_OrderDate]; GO CREATE PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate]; GO
Test
Vi vil udføre den lagrede procedure med tre forskellige sæt inputparametre:
EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO
Den første udførelse returnerer 1958 rækker, den anden returnerer 267.268 rækker, og den sidste returnerer over 2,2 millioner rækker. Hvis du ser på datointervallerne, er dette ikke overraskende – jo større datointerval, desto flere data returneres.
Fordi dette er en lagret procedure, bestemmer de anvendte inputparametre til at begynde med planen, såvel som den hukommelse, der skal tildeles. Hvis vi ser på den faktiske udførelsesplan for den første udførelse, ser vi indlejrede loops og en hukommelsesbevilling på 2656 KB.
Efterfølgende henrettelser har den samme plan (da det var det, der blev gemt) og den samme hukommelsesbevilling, men vi får et fingerpeg om, at det ikke er nok, fordi der er en slags advarsel.
Hvis vi kigger i Query Store efter denne lagrede procedure, ser vi tre udførelser og de samme værdier for UsedKB-hukommelse, uanset om vi ser på gennemsnit, minimum, maksimum, sidste eller standardafvigelse. Bemærk:hukommelsestildelingsoplysninger i Query Store rapporteres som antallet af 8KB sider.
SELECT [qst].[query_sql_text], [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], [rs].[last_execution_time], [rs].[avg_duration], [rs].[avg_logical_io_reads], [rs].[avg_query_max_used_memory] * 8 AS [AvgUsedKB], [rs].[min_query_max_used_memory] * 8 AS [MinUsedKB], --memory grant (reported as the number of 8 KB pages) for the query plan within the aggregation interval [rs].[max_query_max_used_memory] * 8 AS [MaxUsedKB], [rs].[last_query_max_used_memory] * 8 AS [LastUsedKB], [rs].[stdev_query_max_used_memory] * 8 AS [StDevUsedKB], TRY_CONVERT(XML, [qsp].[query_plan]) AS [QueryPlan_XML] FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] = [qst].[query_text_id] JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] = [qsp].[query_id] JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] = [rs].[plan_id] WHERE [qsq].[object_id] = OBJECT_ID(N'Sales.usp_OrderInfo_OrderDate');
Hvis vi leder efter problemer med hukommelsesbevillinger i dette scenarie – hvor en plan cachelagres og genbruges – hjælper Query Store os ikke.
Men hvad hvis den specifikke forespørgsel kompileres ved udførelse, enten på grund af et RECOMPILE-tip eller fordi det er ad-hoc?
Vi kan ændre proceduren for at tilføje RECOMPILE-hintet til sætningen (som anbefales frem for at tilføje RECOMPILE på procedureniveauet eller køre proceduren MED RECOMIPLE):
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate] OPTION (RECOMPILE); GO
Nu vil vi køre vores procedure igen med de samme inputparametre som før, og kontrollere outputtet:
Bemærk, at vi har et nyt query_id – forespørgselsteksten er ændret, fordi vi tilføjede OPTION (RECOMPILE) til den – og vi har også to nye plan_id værdier, og vi har forskellige hukommelsesbevillingsnumre for en af vores planer. For plan_id 5 er der kun én udførelse, og hukommelsesbevillingsnumrene matcher den indledende udførelse - så planen er for det lille datointerval. De to større datointervaller genererede den samme plan, men der er betydelig variation i hukommelsesbevillingerne - 94.528 for minimum og 573.568 for maksimum.
Hvis vi ser på hukommelsesbevillingsoplysninger ved hjælp af Query Store-rapporterne, viser denne variabilitet sig lidt anderledes. Når vi åbner rapporten Topressourceforbrugere fra databasen og derefter ændrer metrikken til at være Hukommelsesforbrug (KB) og Gns., kommer vores forespørgsel med RECOMPILE øverst på listen.
I dette vindue er metrics aggregeret efter forespørgsel, ikke plan. Forespørgslen, vi udførte direkte mod Query Store-visningerne, viste ikke kun query_id, men også plan_id. Her kan vi se, at forespørgslen har to planer, og vi kan se dem begge i planoversigtsvinduet, men metrikkerne er kombineret for alle planer i denne visning.
Variationen i hukommelsesbevillinger er indlysende, når vi ser direkte på synspunkterne. Vi kan finde forespørgsler med variabilitet ved at bruge brugergrænsefladen ved at ændre statistikken fra Avg til StDev:
Vi kan finde de samme oplysninger ved at forespørge i Query Store-visningerne og bestille efter stdev_query_max_used_memory faldende. Men vi kan også søge baseret på forskellen mellem minimum og maksimum hukommelsesbevilling eller en procentdel af forskellen. Hvis vi for eksempel var bekymrede over sager, hvor forskellen i bevillingerne var større end 512 MB, kunne vi køre:
SELECT [qst].[query_sql_text], [qsq].[query_id], [qsp].[plan_id], [qsq].[object_id], [rs].[count_executions], [rs].[last_execution_time], [rs].[avg_duration], [rs].[avg_logical_io_reads], [rs].[avg_query_max_used_memory] * 8 AS [AvgUsedKB], [rs].[min_query_max_used_memory] * 8 AS [MinUsedKB], [rs].[max_query_max_used_memory] * 8 AS [MaxUsedKB], [rs].[last_query_max_used_memory] * 8 AS [LastUsedKB], [rs].[stdev_query_max_used_memory] * 8 AS [StDevUsedKB], TRY_CONVERT(XML, [qsp].[query_plan]) AS [QueryPlan_XML] FROM [sys].[query_store_query] [qsq] JOIN [sys].[query_store_query_text] [qst] ON [qsq].[query_text_id] = [qst].[query_text_id] JOIN [sys].[query_store_plan] [qsp] ON [qsq].[query_id] = [qsp].[query_id] JOIN [sys].[query_store_runtime_stats] [rs] ON [qsp].[plan_id] = [rs].[plan_id] WHERE ([rs].[max_query_max_used_memory]*8) - ([rs].[min_query_max_used_memory]*8) > 524288;
De af jer, der kører SQL Server 2017 med Columnstore-indekser, og som har fordelen ved Memory Grant-feedback, kan også bruge disse oplysninger i Query Store. Vi vil først ændre vores ordretabel for at tilføje et klynget Columnstore-indeks:
ALTER TABLE [Sales].[Invoices] DROP CONSTRAINT [FK_Sales_Invoices_OrderID_Sales_Orders]; GO ALTER TABLE [Sales].[Orders] DROP CONSTRAINT [FK_Sales_Orders_BackorderOrderID_Sales_Orders]; GO ALTER TABLE [Sales].[OrderLines] DROP CONSTRAINT [FK_Sales_OrderLines_OrderID_Sales_Orders]; GO ALTER TABLE [Sales].[Orders] DROP CONSTRAINT [PK_Sales_Orders] WITH ( ONLINE = OFF ); GO CREATE CLUSTERED COLUMNSTORE INDEX CCI_Orders ON [Sales].[Orders];
Derefter indstiller vi databasekombabilitetstilstanden til 140, så vi kan udnytte hukommelsesbevillingsfeedback:
ALTER DATABASE [WideWorldImporters] SET COMPATIBILITY_LEVEL = 140; GO
Til sidst ændrer vi vores lagrede procedure for at fjerne OPTION (RECOMPILE) fra vores forespørgsel og derefter køre den et par gange med de forskellige inputværdier:
ALTER PROCEDURE [Sales].[usp_OrderInfo_OrderDate] @StartDate DATETIME, @EndDate DATETIME AS SELECT [o].[CustomerID], [o].[OrderDate], [o].[ContactPersonID], [ol].[Quantity] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [OrderDate] BETWEEN @StartDate AND @EndDate ORDER BY [OrderDate]; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-06-30'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-01-08'; GO EXEC [Sales].[usp_OrderInfo_OrderDate] '2016-01-01', '2016-12-31'; GO
Inden for Query Store ser vi følgende:
Vi har en ny plan for query_id =1, som har andre værdier for hukommelsesbevillingsmetrikken, og en lidt lavere StDev, end vi havde med plan_id 6. Hvis vi kigger i planen i Query Store, ser vi, at den får adgang til det klyngede Columnstore-indeks :
Husk, at planen i Query Store er den, der blev udført, men den indeholder kun estimater. Selvom planen i plancachen har hukommelsesbevillingsoplysninger opdateret, når hukommelsesfeedback forekommer, bliver disse oplysninger ikke anvendt på den eksisterende plan i Query Store.
Oversigt
Her er, hvad jeg kan lide ved at bruge Query Store til at se på forespørgsler med variable hukommelsesbevillinger:dataene bliver automatisk indsamlet. Hvis dette problem dukker op uventet, behøver vi ikke at sætte noget på plads for at prøve at indsamle oplysninger, vi har allerede fanget det i Query Store. I det tilfælde, hvor en forespørgsel er parametriseret, kan det være sværere at finde hukommelsesbevillingsvariabilitet på grund af potentialet for statiske værdier på grund af plan-caching. Vi kan dog også opdage, at forespørgslen på grund af rekompilering har flere planer med ekstremt forskellige hukommelsesbevillingsværdier, som vi kunne bruge til at spore problemet. Der er en række forskellige måder at undersøge problemet på ved hjælp af de data, der er fanget i Query Store, og det giver dig mulighed for at se på problemer både proaktivt og reaktivt.