Database
 sql >> Base de Dados >  >> RDS >> Database

Mais sobre CXPACKET Waits:Paralelismo distorcido


No meu post anterior, discuti as esperas do CXPACKET e formas de prevenir ou limitar o paralelismo. Também expliquei como o thread de controle em uma operação paralela sempre registra uma espera de CXPACKET e que, às vezes, os threads que não são de controle também podem registrar esperas de CXPACKET. Isso pode acontecer se um dos encadeamentos estiver bloqueado aguardando um recurso (para que todos os outros encadeamentos terminem antes dele e o registro CXPACKET também aguarde) ou se as estimativas de cardinalidade estiverem incorretas. Neste post eu gostaria de explorar o último.

Quando as estimativas de cardinalidade estão incorretas, os threads paralelos que realizam o trabalho de consulta recebem quantidades desiguais de trabalho a fazer. O caso típico é onde um thread recebe todo o trabalho, ou muito mais trabalho do que os outros threads. Isso significa que aqueles encadeamentos que terminam de processar suas linhas (se tiverem alguma) antes do encadeamento mais lento registram um CXPACKET desde o momento em que terminam até o término do encadeamento mais lento. Este problema pode levar a uma aparente explosão nas esperas CXPACKET ocorrendo e é comumente chamado de paralelismo enviesado , porque a distribuição de trabalho entre os threads paralelos é distorcida, nem mesmo.

Observe que no SQL Server 2016 SP2 e no SQL Server 2017 RTM CU3, os threads do consumidor não registram mais as esperas CXPACKET. Eles registram esperas CXCONSUMER, que são benignas e podem ser ignoradas. Isso é para reduzir o número de esperas de CXPACKET sendo geradas, e as restantes têm maior probabilidade de serem acionáveis.

Exemplo de paralelismo enviesado


Mostrarei um exemplo inventado para mostrar como identificar esses casos.

Primeiro, vou criar um cenário em que uma tabela tem estatísticas extremamente imprecisas, definindo manualmente o número de linhas e páginas em um UPDATE STATISTICS declaração (não faça isso em produção!):
USE [master];
GO
 
IF DB_ID (N'ExecutionMemory') IS NOT NULL
BEGIN
    ALTER DATABASE [ExecutionMemory] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [ExecutionMemory];
END
GO
 
CREATE DATABASE [ExecutionMemory];
GO
USE [ExecutionMemory];
GO
 
CREATE TABLE dbo.[Test] (
    [RowID] INT IDENTITY,
    [ParentID] INT,
    [CurrentValue] NVARCHAR (100),
    CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED ([RowID]));
GO
 
INSERT INTO dbo.[Test] ([ParentID], [CurrentValue])
SELECT 
    CASE WHEN ([t1].[number] % 3 = 0)
        THEN [t1].[number] – [t1].[number] % 6
        ELSE [t1].[number] END, 
    'Test' + CAST ([t1].[number] % 2 AS VARCHAR(11))
FROM [master].[dbo].[spt_values] AS [t1]
WHERE [t1].[type] = 'P';
GO
 
UPDATE STATISTICS dbo.[Test] ([PK_Test]) WITH ROWCOUNT = 10000000, PAGECOUNT = 1000000;
GO

Então minha tabela tem apenas alguns milhares de linhas, mas eu fingi ter 10 milhões de linhas.

Agora vou criar uma consulta artificial para selecionar as 500 principais linhas, que serão paralelas, pois acredita que há milhões de linhas para verificar.
USE [ExecutionMemory];
GO
 
SET NOCOUNT ON;
GO
 
DECLARE @CurrentValue NVARCHAR (100);
 
WHILE (1=1)
SELECT TOP (500) 
    @CurrentValue = [CurrentValue]
FROM dbo.[Test]
ORDER BY NEWID() DESC;
GO

E coloque isso em execução.

Visualizando as esperas de CXPACKET


Agora posso ver as esperas de CXPACKET que estão ocorrendo usando um script simples para ver as sys.dm_os_waiting_tasks DMV:
SELECT
    [owt].[session_id],
    [owt].[exec_context_id],
    [owt].[wait_duration_ms],
    [owt].[wait_type],
    [owt].[blocking_session_id],
    [owt].[resource_description],
    [er].[database_id],
    [eqp].[query_plan]
FROM sys.dm_os_waiting_tasks [owt]
INNER JOIN sys.dm_exec_sessions [es] ON
    [owt].[session_id] = [es].[session_id]
INNER JOIN sys.dm_exec_requests [er] ON
    [es].[session_id] = [er].[session_id]
OUTER APPLY sys.dm_exec_sql_text ([er].[sql_handle]) [est]
OUTER APPLY sys.dm_exec_query_plan ([er].[plan_handle]) [eqp]
WHERE
    [es].[is_user_process] = 1
ORDER BY
    [owt].[session_id],
    [owt].[exec_context_id];

Se eu executar isso algumas vezes, eventualmente vejo alguns resultados mostrando paralelismo distorcido (retirei o link do identificador do plano de consulta e reduzi a descrição do recurso, para maior clareza, e observei que coloquei o código para pegar o texto SQL se você quiser isso também):
session_id exec_context_id wait_duration_ms wait_type blocking_session_id resource_description database_id
56 0 1 CXPACKET NULL exchangeEvent 13
56 1 1 CXPACKET 56 exchangeEvent 13
56 3 1 CXPACKET 56 exchangeEvent 13
56 4 1 CXPACKET 56 exchangeEvent 13
56 5 1 CXPACKET 56 exchangeEvent 13
56 6 1 CXPACKET 56 exchangeEvent 13
56 7 1 CXPACKET 56 exchangeEvent 13

Resultados mostrando paralelismo distorcido em ação

O thread de controle é aquele com exec_context_id definido como 0. Os outros threads paralelos são aqueles com exec_context_id superior a 0, e todos eles estão mostrando esperas CXPACKET além de um (observe que exec_context_id = 2 está faltando na lista). Você notará que todos eles listam seus próprios session_id como aquele que os está bloqueando, e isso está correto porque todos os threads estão esperando por outro thread de seu próprio session_id completar. O database_id é o banco de dados em cujo contexto a consulta está sendo executada, não necessariamente o banco de dados onde está o problema, mas geralmente é, a menos que a consulta esteja usando nomenclatura de três partes para executar em um banco de dados diferente.

Visualizando o problema de estimativa de cardinalidade


Com o query_plan coluna na saída da consulta (que eu removi para maior clareza), você pode clicar nela para abrir o plano gráfico e, em seguida, clicar com o botão direito do mouse e selecionar Exibir com SQL Sentry Plan Explorer. Isso mostra como abaixo:



Posso ver imediatamente que há um problema de estimativa de cardinalidade, pois as Linhas Reais para a Verificação de Índice Agrupado são apenas 2.048, em comparação com 10.000.000 Linhas Est (Estimadas).

Se eu rolar, posso ver a distribuição de linhas nos threads paralelos que foram usados:



Veja bem, apenas um único thread estava fazendo algum trabalho durante a parte paralela do plano - aquele que não apareceu no sys.dm_os_waiting_tasks saída acima.

Nesse caso, a correção é atualizar as estatísticas da tabela.

No meu exemplo artificial, isso não funcionará, pois não houve nenhuma modificação na tabela, então executarei novamente o script de configuração, deixando de fora as UPDATE STATISTICS demonstração.

O plano de consulta torna-se então:



Onde não há problema de cardinalidade e nem paralelismo – problema resolvido!

Resumo


Se você vir esperas CXPACKET ocorrendo, é fácil verificar paralelismo distorcido, usando o método descrito acima. Todos os casos que vi foram devidos a problemas de estimativa de cardinalidade de um tipo ou de outro, e muitas vezes é simplesmente um caso de atualização de estatísticas.

No que diz respeito às estatísticas gerais de espera, você pode encontrar mais informações sobre como usá-las para solucionar problemas de desempenho em:
  • Minha série de postagens do blog SQLskills, começando com estatísticas de espera, ou por favor me diga onde dói
  • Minha biblioteca de tipos de espera e classes de trava aqui
  • Meu curso de treinamento online Pluralsight SQL Server:Solução de problemas de desempenho usando estatísticas de espera
  • SQL Sentinela

Até a próxima, feliz solução de problemas!