Database
 sql >> база данни >  >> RDS >> Database

Анализирането на смъртта от хиляда съкращава натоварването

Има множество методи за разглеждане на лошо представящи се заявки в SQL Server, по-специално хранилище на заявки, разширени събития и динамични изгледи за управление (DMV). Всяка опция има плюсове и минуси. Разширените събития предоставят данни за индивидуалното изпълнение на заявките, докато Съхранението на заявки и DMV обобщават данни за ефективността. За да използвате хранилище на заявки и разширени събития, трябва да ги конфигурирате предварително – или да активирате хранилището на заявки за вашата база данни, или да настроите XE сесия и да я стартирате. DMV данните са винаги налични, така че много често това е най-лесният метод да получите бърз първи поглед върху ефективността на заявката. Това е мястото, където DMV заявките на Glenn са полезни – в неговия скрипт той има множество заявки, които можете да използвате, за да намерите най-добрите заявки за екземпляра въз основа на CPU, логически вход/изход и продължителност. Насочването към заявки с най-висока консумация на ресурси често е добро начало при отстраняване на неизправности, но не можем да забравим за сценария „смърт от хиляда порязвания“ – заявката или наборът от заявки, които се изпълняват МНОГО често – може би стотици или хиляди пъти на минута. Глен има заявка в своя набор, която изброява най-популярните заявки за база данни въз основа на броя на изпълненията, но според моя опит това не ви дава пълна картина на вашето работно натоварване.

Основният DMV, който се използва за разглеждане на показателите за ефективност на заявката, е sys.dm_exec_query_stats. Допълнителни данни, специфични за съхранените процедури (sys.dm_exec_procedure_stats), функции (sys.dm_exec_function_stats) и тригери (sys.dm_exec_trigger_stats), също са налични, но помислете за натоварване, което не е чисто съхранени процедури, функции и тригери. Помислете за смесено работно натоварване, което има някои ad hoc заявки или може би е изцяло ad hoc.

Примерен сценарий

Заемайки и адаптирайки код от предишна публикация, Изследване на въздействието върху производителността на Adhoc работно натоварване, първо ще създадем две съхранени процедури. Първият, dbo.RandomSelects, генерира и изпълнява ad hoc израз, а вторият, dbo.SPRandomSelects, генерира и изпълнява параметризирана заявка.

  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

Сега ще изпълним и двете съхранени процедури 1000 пъти, като използваме същия метод, описан в предишната ми публикация с .cmd файлове, извикващи .sql файлове със следните изрази:

Съдържание на файла Adhoc.sql:

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

Съдържание на Parameterized.sql файл:

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

Примерен синтаксис в .cmd файл, който извиква .sql файла:

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

Ако използваме вариация на заявката за най-доброто работно време на Glenn, за да разгледаме най-добрите заявки въз основа на работното време (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);

Виждаме изявлението от нашата съхранена процедура като заявката, която се изпълнява с най-голямо количество кумулативен процесор.

Ако изпълним вариация на заявката на Glenn's Query Execution Counts спрямо базата данни WideWorldImporters:

  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);

Виждаме и нашия оператор за съхранена процедура в горната част на списъка.

Но ad hoc заявката, която изпълнихме, въпреки че има различни буквални стойности, по същество беше същата оператор, изпълняван многократно, както можем да видим, като погледнем 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 е добавен в SQL Server 2008 и се основава на дървото на логическите оператори, генерирани от оптимизатора на заявки за текста на израза. Заявките, които имат подобен текст на израза, които генерират едно и също дърво от логически оператори, ще имат същия query_hash, дори ако литералните стойности в предиката на заявката са различни. Докато литералните стойности могат да бъдат различни, обектите и техните псевдоними трябва да са еднакви, както и подсказките за заявка и евентуално опциите SET. Съхранената процедура RandomSelects генерира заявки с различни литерални стойности:

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';

Но всяко изпълнение има точно същата стойност за query_hash, 0xB705BF8B5AD49F4C. За да разберем колко често се изпълнява една ad hoc заявка – и тези, които са еднакви по отношение на query_hash – трябва да групираме по реда на query_hash на този брой, вместо да гледаме execution_count в sys.dm_exec_query_stats (което често показва стойност 1).

Ако променим контекста към базата данни WideWorldImporters и потърсим водещи заявки въз основа на броя на изпълненията, където групираме по query_hash, вече можем да видим както съхранената процедура и нашата ad hoc заявка:

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

Забележка:DMV sys.dm_exec_function_stats беше добавен в SQL Server 2016. Изпълнението на тази заявка на SQL Server 2014 и по-стари изисква премахване на препратка към този DMV.

Този изход предоставя много по-изчерпателно разбиране за това кои заявки наистина се изпълняват най-често, тъй като се обобщава въз основа на query_hash, а не като просто разглежда execution_count в sys.dm_exec_query_stats, който може да има множество записи за един и същ query_hash, когато са различни литерални стойности използван. Изходът на заявката включва също query_plan_hash, който може да бъде различен за заявки със същия query_hash. Тази допълнителна информация е полезна при оценка на ефективността на плана за заявка. В примера по-горе всяка заявка има един и същ query_plan_hash, 0x299275DD475C4B17, демонстрирайки, че дори с различни входни стойности, оптимизаторът на заявки генерира един и същ план – той е стабилен. Когато съществуват множество стойности на query_plan_hash за един и същ query_hash, съществува променливост на плана. В сценарий, при който една и съща заявка, базирана на query_hash, се изпълнява хиляди пъти, обща препоръка е да параметризирате заявката. Ако можете да потвърдите, че не съществува променливост на плана, тогава параметризирането на заявката премахва времето за оптимизация и компилация за всяко изпълнение и може да намали общия процесор. В някои сценарии параметризирането на 5 до 10 ad hoc заявки може да подобри производителността на системата като цяло.

Резюме

За всяка среда е важно да разберете кои заявки са най-скъпи по отношение на използването на ресурси и кои заявки се изпълняват най-често. Същият набор от заявки може да се покаже и за двата типа анализ, когато се използва DMV скрипта на Glenn, което може да бъде подвеждащо. Поради това е важно да се установи дали работното натоварване е предимно процедурно, предимно ad hoc или смесено. Въпреки че има много документи за предимствата на съхранените процедури, намирам, че смесените или силно ad hoc работни натоварвания са много често срещани, особено при решения, които използват обектно-релационни карти (ORMs), като Entity Framework, NHibernate и LINQ to SQL. Ако не сте наясно с типа на натоварването на сървъра, изпълнението на горната заявка за преглед на най-изпълнените заявки въз основа на query_hash е добро начало. Когато започнете да разбирате работното натоварване и това, което съществува както за тежките нападатели, така и за смъртта от хиляда заявки за намаляване, можете да преминете към истинско разбиране на използването на ресурсите и въздействието, което тези заявки имат върху производителността на системата, и да насочите усилията си за настройка.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL SELECT INTO Инструкция

  2. Хекатон с обрат:TVP в паметта – част 3

  3. Как се класифицират SQL командите | UBIQ

  4. Предимствата на индексирането на чужди ключове

  5. Продуктова номенклатура и архитектура на IRI