sql >> Databasteknik >  >> RDS >> Database

Att analysera döden med tusen minskar arbetsbelastningen

Det finns flera metoder för att titta på dåligt presterande frågor i SQL Server, särskilt Query Store, Extended Events och dynamiska hanteringsvyer (DMV). Varje alternativ har för- och nackdelar. Extended Events tillhandahåller data om individuell exekvering av frågor, medan Query Store och DMV:s samlar prestandadata. För att kunna använda Query Store och Extended Events måste du konfigurera dem i förväg – antingen aktivera Query Store för din(a) databas(er), eller ställa in en XE-session och starta den. DMV-data är alltid tillgänglig, så väldigt ofta är det den enklaste metoden för att få en snabb första titt på frågeprestanda. Det är här Glenns DMV-frågor kommer väl till pass – i hans skript har han flera frågor som du kan använda för att hitta de bästa frågorna för instansen baserat på CPU, logisk I/O och varaktighet. Att rikta in sig på de mest resurskrävande frågorna är ofta en bra början vid felsökning, men vi kan inte glömma scenariot "död med tusen snitt" - frågan eller uppsättningen av frågor som körs MYCKET ofta - kanske hundratals eller tusentals gånger per minut. Glenn har en fråga i sitt set som listar de vanligaste frågorna för en databas baserat på antalet körningar, men enligt min erfarenhet ger den dig inte en fullständig bild av din arbetsbelastning.

Den huvudsakliga DMV som används för att titta på frågeprestandamått är sys.dm_exec_query_stats. Ytterligare data specifik för lagrade procedurer (sys.dm_exec_procedure_stats), funktioner (sys.dm_exec_function_stats) och triggers (sys.dm_exec_trigger_stats) är också tillgängliga, men överväg en arbetsbelastning som inte är enbart lagrade procedurer, funktioner och utlösare. Tänk på en blandad arbetsbelastning som har vissa ad hoc-frågor, eller kanske är helt ad hoc.

Exempelscenario

Genom att låna och anpassa kod från ett tidigare inlägg, Undersöka prestandaeffekten av en adhoc-arbetsbelastning, kommer vi först att skapa två lagrade procedurer. Den första, dbo.RandomSelects, genererar och exekverar en ad hoc-sats, och den andra, dbo.SPRandomSelects, genererar och exekverar en parametriserad fråga.

  USE [WideWorldImporters];
  GO
  DROP PROCEDURE IF EXISTS dbo.[RandomSelects];  
  GO
 
  CREATE PROCEDURE dbo.[RandomSelects]
    @NumRows INT
  AS
    DECLARE @ConcatString NVARCHAR(200);
    DECLARE @QueryString NVARCHAR(1000);
    DECLARE @RowLoop INT = 0;
    WHILE (@RowLoop < @NumRows)
    BEGIN
      SET @ConcatString = CAST((CONVERT (INT, RAND () * 2500) + 1) AS NVARCHAR(50))
        + CAST((CONVERT (INT, RAND () * 1000) + 1) AS NVARCHAR(50))
        + CAST((CONVERT (INT, RAND () * 500) + 1) AS NVARCHAR(50))
        + CAST((CONVERT (INT, RAND () * 1500) + 1) AS NVARCHAR(50));
 
      SELECT @QueryString = N'SELECT w.ColorID, s.StockItemName
        FROM Warehouse.Colors w
        JOIN Warehouse.StockItems s
        	ON w.ColorID = s.ColorID
        WHERE w.ColorName = ''' + @ConcatString + ''';';
 
      EXEC (@QueryString);
      SELECT @RowLoop = @RowLoop + 1;
    END
  GO
 
  DROP PROCEDURE IF EXISTS dbo.[SPRandomSelects];  
  GO
 
  CREATE PROCEDURE dbo.[SPRandomSelects]
    @NumRows INT
  AS
    DECLARE @ConcatString NVARCHAR(200);
    DECLARE @QueryString NVARCHAR(1000);
    DECLARE @RowLoop INT = 0;
    WHILE (@RowLoop < @NumRows)
    BEGIN
      SET @ConcatString = CAST((CONVERT (INT, RAND () * 2500) + 1) AS NVARCHAR(50))
        + CAST((CONVERT (INT, RAND () * 1000) + 1) AS NVARCHAR(50))
        + CAST((CONVERT (INT, RAND () * 500) + 1) AS NVARCHAR(50))
        + CAST((CONVERT (INT, RAND () * 1500) + 1) AS NVARCHAR(50))
 
      SELECT c.CustomerID, c.AccountOpenedDate, COUNT(ct.CustomerTransactionID)
  	FROM Sales.Customers c
  	JOIN Sales.CustomerTransactions ct
  		ON c.CustomerID = ct.CustomerID
  	WHERE c.CustomerName = @ConcatString
  	GROUP BY c.CustomerID, c.AccountOpenedDate; 
 
      SELECT @RowLoop = @RowLoop + 1;
    END
  GO

Nu kommer vi att köra båda lagrade procedurerna 1000 gånger, med samma metod som beskrivs i mitt tidigare inlägg med .cmd-filer som anropar .sql-filer med följande uttalanden:

Adhoc.sql-filinnehåll:

  EXEC [WideWorldImporters].dbo.[RandomSelects] @NumRows = 1000;

Parameterized.sql-filinnehåll:

  EXEC [WideWorldImporters].dbo.[SPRandomSelects] @NumRows = 1000;

Exempelsyntax i .cmd-fil som anropar .sql-filen:

  sqlcmd -S WIN2016\SQL2017 -i"Adhoc.sql"
  exit

Om vi ​​använder en variant av Glenns Top Worker Time-fråga för att titta på de vanligaste frågorna baserat på worker time (CPU):

  -- Get top total worker time queries for entire instance (Query 44) (Top Worker Time Queries)
  SELECT TOP(50) DB_NAME(t.[dbid]) AS [Database Name], 
  REPLACE(REPLACE(LEFT(t.[text], 255), CHAR(10),''), CHAR(13),'') AS [Short Query Text],  
  qs.total_worker_time AS [Total Worker Time], 
  qs.execution_count AS [Execution Count],
  qs.total_worker_time/qs.execution_count AS [Avg Worker Time],
  qs.creation_time AS [Creation Time]
  FROM sys.dm_exec_query_stats AS qs WITH (NOLOCK)
  CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS t 
  CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qp 
  ORDER BY qs.total_worker_time DESC OPTION (RECOMPILE);

Vi ser uttalandet från vår lagrade procedur som den fråga som körs med den högsta mängden kumulativ CPU.

Om vi ​​kör en variant av Glenns Query Execution Counts-fråga mot WideWorldImporters-databasen:

  USE [WideWorldImporters];
   GO
 
  -- Get most frequently executed queries for this database (Query 57) (Query Execution Counts)
  SELECT TOP(50) LEFT(t.[text], 50) AS [Short Query Text], qs.execution_count AS [Execution Count],
  qs.total_logical_reads AS [Total Logical Reads],
  qs.total_logical_reads/qs.execution_count AS [Avg Logical Reads],
  qs.total_worker_time AS [Total Worker Time],
  qs.total_worker_time/qs.execution_count AS [Avg Worker Time], 
  qs.total_elapsed_time AS [Total Elapsed Time],
  qs.total_elapsed_time/qs.execution_count AS [Avg Elapsed Time]
  FROM sys.dm_exec_query_stats AS qs WITH (NOLOCK)
  CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS t 
  CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qp 
  WHERE t.dbid = DB_ID()
  ORDER BY qs.execution_count DESC OPTION (RECOMPILE);

Vi ser också vår lagrade procedursats överst på listan.

Men ad hoc-frågan som vi körde, även om den har olika bokstavliga värden, var i huvudsak samma satsen körs upprepade gånger, som vi kan se genom att titta på query_hash:

  SELECT TOP(50) DB_NAME(t.[dbid]) AS [Database Name], 
  REPLACE(REPLACE(LEFT(t.[text], 255), CHAR(10),''), CHAR(13),'') AS [Short Query Text],  
  qs.query_hash AS [query_hash],
  qs.creation_time AS [Creation Time]
  FROM sys.dm_exec_query_stats AS qs WITH (NOLOCK)
  CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS t 
  CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qp 
  ORDER BY [Short Query Text];

query_hash lades till i SQL Server 2008 och är baserat på trädet för de logiska operatorerna som genereras av Query Optimizer för satstexten. Frågor som har en liknande satstext som genererar samma träd av logiska operatorer kommer att ha samma query_hash, även om de bokstavliga värdena i frågepredikatet är olika. Även om de bokstavliga värdena kan vara olika, måste objekten och deras alias vara desamma, liksom frågetips och eventuellt SET-alternativen. Den lagrade proceduren RandomSelects genererar frågor med olika bokstavliga värden:

SELECT w.ColorID, s.StockItemName
      FROM Warehouse.Colors w
      JOIN Warehouse.StockItems s
      ON w.ColorID = s.ColorID
      WHERE w.ColorName = '1005451175198';
 
SELECT w.ColorID, s.StockItemName
      FROM Warehouse.Colors w
      JOIN Warehouse.StockItems s
      ON w.ColorID = s.ColorID
      WHERE w.ColorName = '1006416358897';

Men varje exekvering har exakt samma värde för query_hash, 0xB705BF8B5AD49F4C. För att förstå hur ofta en ad hoc-fråga – och de som är desamma när det gäller query_hash – körs, måste vi gruppera efter query_hash-ordningen på det antalet, snarare än att titta på execution_count i sys.dm_exec_query_stats (som ofta visar en värde på 1).

Om vi ​​ändrar kontext till WideWorldImporters-databasen och letar efter toppfrågor baserat på antal körningar, där vi grupperar på query_hash, kan vi nu se både den lagrade proceduren och vår ad hoc-fråga:

;WITH qh AS
(
    SELECT TOP (25) query_hash, COUNT(*) AS COUNT
    FROM sys.dm_exec_query_stats
    GROUP BY query_hash
    ORDER BY COUNT(*) DESC
),
qs AS
(
    SELECT obj = COALESCE(ps.object_id,   fs.object_id,   ts.object_id),
            db = COALESCE(ps.database_id, fs.database_id, ts.database_id),
            qs.query_hash, qs.query_plan_hash, qs.execution_count,
            qs.sql_handle, qs.plan_handle
    FROM sys.dm_exec_query_stats AS qs
    INNER JOIN qh ON qs.query_hash = qh.query_hash
    LEFT OUTER JOIN sys.dm_exec_procedure_stats AS [ps]
      ON [qs].[sql_handle] = [ps].[sql_handle]
    LEFT OUTER JOIN sys.dm_exec_function_stats AS [fs]
      ON [qs].[sql_handle] = [fs].[sql_handle]
    LEFT OUTER JOIN sys.dm_exec_trigger_stats AS [ts]
      ON [qs].[sql_handle] = [ts].[sql_handle]
)
SELECT TOP (50)
  OBJECT_NAME(qs.obj, qs.db), 
  query_hash,
  query_plan_hash,
  SUM([qs].[execution_count]) AS [ExecutionCount],
  MAX([st].[text]) AS [QueryText]
  FROM qs 
  CROSS APPLY sys.dm_exec_sql_text ([qs].[sql_handle]) AS [st]
  CROSS APPLY sys.dm_exec_query_plan ([qs].[plan_handle]) AS [qp]
  GROUP BY qs.obj, qs.db, qs.query_hash, qs.query_plan_hash
  ORDER BY ExecutionCount DESC;

Obs:sys.dm_exec_function_stats DMV lades till i SQL Server 2016. Att köra den här frågan på SQL Server 2014 och tidigare kräver att referensen till denna DMV tas bort.

Denna utdata ger en mycket mer omfattande förståelse av vilka frågor som verkligen körs oftast, eftersom den aggregeras baserat på query_hash, inte genom att bara titta på execution_count i sys.dm_exec_query_stats, som kan ha flera poster för samma query_hash när olika bokstavliga värden är Begagnade. Frågeutgången inkluderar också query_plan_hash, som kan vara annorlunda för frågor med samma query_hash. Denna ytterligare information är användbar när du utvärderar planprestanda för en fråga. I exemplet ovan har varje fråga samma query_plan_hash, 0x299275DD475C4B17, vilket visar att även med olika ingångsvärden genererar Query Optimizer samma plan – den är stabil. När flera query_plan_hash-värden finns för samma query_hash, existerar planvariabilitet. I ett scenario där samma fråga, baserat på query_hash, körs tusentals gånger, är en allmän rekommendation att parametrisera frågan. Om du kan verifiera att det inte finns någon planvariabilitet, tar parametrering av frågan bort optimerings- och kompileringstiden för varje exekvering och kan minska den totala CPU:n. I vissa scenarier kan parametrering av fem till tio ad hoc-frågor förbättra systemets prestanda som helhet.

Sammanfattning

För alla miljöer är det viktigt att förstå vilka frågor som är dyrast när det gäller resursanvändning och vilka frågor som körs oftast. Samma uppsättning frågor kan dyka upp för båda typerna av analys när du använder Glenns DMV-skript, vilket kan vara missvisande. Som sådan är det viktigt att fastställa om arbetsbelastningen mestadels är procedurmässig, mestadels ad hoc eller en mix. Även om det finns mycket dokumenterat om fördelarna med lagrade procedurer, tycker jag att blandade eller mycket ad hoc-arbetsbelastningar är mycket vanliga, särskilt med lösningar som använder objektrelationsmappare (ORM) som Entity Framework, NHibernate och LINQ till SQL. Om du är osäker på typen av arbetsbelastning för en server är det en bra början att köra ovanstående fråga för att titta på de mest körda frågorna baserade på en query_hash. När du börjar förstå arbetsbelastningen och vad som finns för både tunga och döda med tusentals nedskärningar, kan du gå vidare till att verkligen förstå resursanvändningen och vilken inverkan dessa frågor har på systemets prestanda, och rikta dina ansträngningar för att trimma.


  1. Storleksgräns för JSON-datatyp i PostgreSQL

  2. MySQL-utlösare vid Infoga/Uppdatera händelser

  3. Cloud Backup-alternativ för PostgreSQL

  4. DISTINCT för endast en kolumn