Olá pessoal!
Como vocês estão ?

Neste post eu vou demonstrar a vocês como identificar e coletar informações de consultas demoradas utilizando Trace (SQL Server Profiler). Isso é muito útil para lhe auxiliar em análises de performance tuning, facilitando a identificação de rotinas que estejam com tempo de resposta alto, tanto execução de procedures quanto queries ad-hoc.

No dia a dia, utilizo muito esse recurso para me ajudar a identificar timeouts em aplicações (Duração da query = tempo máximo de timeout configurado na app) e possíveis consultas candidatas a realizar um trabalho de performance tuning.

O fluxo dessa rotina funciona da seguinte forma:

  • Verifica se o trace já está ativo.
  • Caso o trace esteja ativo, desativa o trace e fecha o arquivo
  • Cria a tabela de histórico das consultas (caso não exista).
  • Lê os dados do arquivo de trace e insere na tabela de histórico
  • Ativa o recurso xp_cmdshell dinamicamente (caso não esteja ativado)
  • Apaga o arquivo de trace
  • Desativa o recurso xp_cmdshell dinamicamente (caso não estava ativado antes)
  • Cria novamente o trace
  • Ativa o trace recém criado

A ideia é que seja criado um Job que é executado a cada X minutos que execute todo esse processo, limpando o arquivo de trace e inserindo os dados coletados na tabela de histórico para que os DBA’s possam consultar os dados lidos do arquivo de trace.

Caso você queira conhecer uma solução que utilize Extended Events (XE) ao invés do Trace, que é uma tecnologia mais moderna e intuitiva, dê uma lida no meu artigo SQL Server – Como identificar e coletar informações de consultas demoradas utilizando Extended Events (XE).

Código-fonte da rotina de coleta

--------------------------------------------------------
-- Armazena os resultados do Trace na tabela
--------------------------------------------------------

DECLARE @Trace_Id INT, @Path VARCHAR(MAX)

SELECT 
    @Trace_Id = id,
    @Path = [path]
FROM 
    sys.traces
WHERE 
    [path] LIKE '%Query_Demorada.trc'


IF (@Trace_Id IS NOT NULL)
BEGIN


    -- Interrompe o rastreamento especificado.
    EXEC sys.sp_trace_setstatus
        @Trace_Id = @Trace_Id, 
        @status = 0


    -- Fecha o rastreamento especificado e exclui sua definição do servidor.
    EXEC sys.sp_trace_setstatus 
        @Trace_Id = @Trace_Id,
        @status = 2


    IF (OBJECT_ID('dbo.Historico_Query_Demorada') IS NULL)
    BEGIN

        CREATE TABLE [dbo].[Historico_Query_Demorada] (
            [TextData] [text] NULL,
            [NTUserName] [varchar] (128) NULL,
            [HostName] [varchar] (128) NULL,
            [ApplicationName] [varchar] (128) NULL,
            [LoginName] [varchar] (128) NULL,
            [SPID] [int] NULL,
            [Duration] [numeric] (15, 2) NULL,
            [StartTime] [datetime] NULL,
            [EndTime] [datetime] NULL,
            [Reads] [int] NULL,
            [Writes] [int] NULL,
            [CPU] [int] NULL,
            [ServerName] [varchar] (128) NULL,
            [DataBaseName] [varchar] (128) NULL,
            [RowCounts] [int] NULL,
            [SessionLoginName] [varchar] (128) NULL
        )
        WITH ( DATA_COMPRESSION = PAGE )

        CREATE CLUSTERED INDEX [SK01_Traces] ON [dbo].[Historico_Query_Demorada] ([StartTime]) WITH (FILLFACTOR=80, STATISTICS_NORECOMPUTE=ON, DATA_COMPRESSION = PAGE) ON [PRIMARY]
    
    END

    
    INSERT INTO dbo.Historico_Query_Demorada (
        TextData, 
        NTUserName, 
        HostName, 
        ApplicationName, 
        LoginName, 
        SPID, 
        Duration, 
        StartTime,
        EndTime, 
        Reads,
        Writes, 
        CPU, 
        ServerName, 
        DataBaseName, 
        RowCounts, 
        SessionLoginName
    )
    SELECT
        TextData,
        NTUserName,
        HostName,
        ApplicationName,
        LoginName,
        SPID,
        CAST(Duration / 1000 / 1000.00 AS NUMERIC(15, 2)) Duration,
        StartTime,
        EndTime,
        Reads,
        Writes,
        CPU,
        ServerName,
        DatabaseName,
        RowCounts,
        SessionLoginName
    FROM
        ::fn_trace_gettable(@Path, DEFAULT)
    WHERE
        Duration IS NOT NULL
        AND Reads < 100000000
    ORDER BY
        StartTime;


    --------------------------------------------------------
    -- Apaga o arquivo de trace
    --------------------------------------------------------
    
    DECLARE @Fl_Xp_CmdShell_Ativado BIT = (SELECT (CASE WHEN CAST([value] AS VARCHAR(MAX)) = '1' THEN 1 ELSE 0 END) FROM sys.configurations WHERE [name] = 'xp_cmdshell')
 
    IF (@Fl_Xp_CmdShell_Ativado = 0)
    BEGIN
 
        EXECUTE sp_configure 'show advanced options', 1;
        RECONFIGURE WITH OVERRIDE;
    
        EXEC sp_configure 'xp_cmdshell', 1;
        RECONFIGURE WITH OVERRIDE;
    
    END


    DECLARE @Cmd VARCHAR(4000) = 'del ' + @Path + ' /Q'
    EXEC sys.xp_cmdshell @Cmd


    IF (@Fl_Xp_CmdShell_Ativado = 0)
    BEGIN
 
        EXEC sp_configure 'xp_cmdshell', 0;
        RECONFIGURE WITH OVERRIDE;
 
        EXECUTE sp_configure 'show advanced options', 0;
        RECONFIGURE WITH OVERRIDE;
 
    END


END



--------------------------------------------------------
-- Ativa o trace novamenmte
--------------------------------------------------------

DECLARE
    @resource INT,
    @maxfilesize BIGINT = 50,
    @on BIT = 1, -- Habilitado
    @bigintfilter BIGINT = (1000000 * 7) -- 7 segundos


-- Criação do trace
SET @Trace_Id = NULL

EXEC @resource = sys.sp_trace_create @Trace_Id OUTPUT, 0, N'C:\Traces\Query_Demorada', @maxfilesize, NULL 

IF (@resource = 0)
BEGIN

    EXEC sys.sp_trace_setevent @Trace_Id, 10, 1, @on  
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 6, @on  
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 8, @on  
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 10, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 11, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 12, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 13, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 14, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 15, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 16, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 17, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 18, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 26, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 35, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 40, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 48, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 10, 64, @on 

    EXEC sys.sp_trace_setevent @Trace_Id, 12, 1,  @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 6,  @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 8,  @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 10, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 11, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 12, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 13, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 14, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 15, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 16, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 17, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 18, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 26, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 35, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 40, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 48, @on 
    EXEC sys.sp_trace_setevent @Trace_Id, 12, 64, @on 


    -- Aqui é onde filtramos o tempo da query que irá cair no trace
    EXEC sys.sp_trace_setfilter @Trace_Id, 13, 0, 4, @bigintfilter -- O 4 significa >= @bigintfilter 


    -- Ativa o trace
    EXEC sys.sp_trace_setstatus @Trace_Id, 1


END

Resultado:

Na minha rotina, eu configurei para coletar qualquer query que execute por mais de 7 segundos. Fique à vontade para alterar e aplicar filtros conforme sua necessidade.

É isso aí, pessoal!
Um abraço e até a próxima.