Subconsulta de baixo desempenho com comparações de datas

15

Ao usar uma subconsulta para encontrar a contagem total de todos os registros anteriores com um campo correspondente, o desempenho é terrível em uma tabela com apenas 50 mil registros. Sem a subconsulta, a consulta é executada em alguns milissegundos. Com a subconsulta, o tempo de execução é superior a um minuto.

Para esta consulta, o resultado deve:

  • Inclua apenas os registros em um determinado período.
  • Inclua uma contagem de todos os registros anteriores, sem incluir o registro atual, independentemente do período.

Esquema de tabela básica

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Dados de exemplo

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

resultados esperados

Para o período de 2017-05-29até2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

Os registros 96 e 95 são excluídos do resultado, mas estão incluídos no PriorCount subconsulta

Consulta atual

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Índice atual

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Questão

  • Quais estratégias podem ser usadas para melhorar o desempenho desta consulta?

Editar 1
Em resposta à pergunta sobre o que posso modificar no banco de dados: posso modificar os índices, mas não a estrutura da tabela.

Editar 2
Agora adicionei um índice básico na Addresscoluna, mas isso não parece melhorar muito. Atualmente, estou encontrando um desempenho muito melhor com a criação de uma tabela temporária e a inserção dos valores sem o PriorCounte atualizando cada linha com suas contagens específicas.

Editar 3
O Spool do Índice Joe Obbish (resposta aceita) encontrado foi o problema. Depois que adicionei um novo nonclustered index [xyz] on [Activity] (Address) include (ActionDate), os tempos de consulta diminuíram de mais de um minuto para menos de um segundo sem usar uma tabela temporária (consulte a edição 2).

Metro Smurf
fonte

Respostas:

17

Com a definição de índice que você possui IDX_my_nme, o SQL Server poderá procurar usando a ActionDatecoluna, mas não com a Addresscoluna. O índice contém todas as colunas necessárias para cobrir a subconsulta, mas provavelmente não é muito seletivo para essa subconsulta. Suponha que quase todos os dados na tabela tenham um ActionDatevalor anterior a '2017-05-30'. Uma busca ActionDate < '2017-05-30'retornará quase todas as linhas do índice, que serão filtradas posteriormente depois que a linha for buscada no índice. Se sua consulta retornar 200 linhas, provavelmente você faria quase 200 verificações de índice completas emIDX_my_nme , o que significa que você lerá cerca de 50000 * 200 = 10 milhões de linhas no índice.

É provável que a busca Addressseja muito mais seletiva para sua subconsulta, embora você não tenha nos fornecido informações estatísticas completas sobre a consulta, portanto essa é uma suposição da minha parte. No entanto, suponha que você crie um índice apenas Addresse sua tabela tenha 10k valores exclusivos para Address. Com o novo índice, o SQL Server precisará buscar apenas 5 linhas do índice para cada execução da subconsulta, para que você leia cerca de 200 * 5 = 1000 linhas do índice.

Estou testando no SQL Server 2016, portanto, pode haver algumas pequenas diferenças de sintaxe. Abaixo estão alguns dados de amostra nos quais eu fiz suposições semelhantes às anteriores para distribuição de dados:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

Eu criei seu índice conforme descrito na pergunta. Estou testando contra esta consulta que retorna os mesmos dados que os da pergunta:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

Eu recebo um spool de índice. O que isso significa em um nível básico é que o otimizador de consulta cria um índice temporário imediatamente, porque nenhum dos índices existentes na tabela era adequado.

carretel de índice

A consulta ainda termina rapidamente para mim. Talvez você não esteja obtendo a otimização do spool de índice em seu sistema ou haja algo diferente na definição da tabela ou na consulta. Para fins educacionais, posso usar um recurso não documentado OPTION (QUERYRULEOFF BuildSpool)para desativar o spool de índice. Aqui está a aparência do plano:

busca de índice ruim

Não se deixe enganar pela aparência de uma simples busca de índice. O SQL Server lê quase 10 milhões de linhas no índice:

10 milhões de linhas do índice

Se eu estiver executando a consulta mais de uma vez, provavelmente não faz sentido para o otimizador de consulta criar um índice cada vez que é executado. Eu poderia criar um índice inicial que seria mais seletivo para esta consulta:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

O plano é semelhante ao anterior:

busca de índice

No entanto, com o novo índice, o SQL Server lê apenas 1000 linhas do índice. 800 das linhas são retornadas para serem contadas. O índice pode ser definido para ser mais seletivo, mas isso pode ser bom o suficiente, dependendo da sua distribuição de dados.

boa procura

Se você não conseguir definir nenhum índice adicional na tabela, consideraria o uso das funções da janela. O seguinte parece funcionar:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

Essa consulta faz uma única varredura dos dados, mas faz uma classificação cara e calcula a ROW_NUMBER()função para cada linha da tabela; portanto, parece que há algum trabalho extra feito aqui:

tipo ruim

No entanto, se você realmente gosta desse padrão de código, pode definir um índice para torná-lo mais eficiente:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Isso move o tipo para o final, que será muito menos caro:

bom tipo

Se nada disso ajudar, você precisará adicionar mais informações à pergunta, de preferência incluindo planos de execução reais.

Joe Obbish
fonte
1
O spool de índice que você encontrou foi o problema. Depois de adicionar um novo nonclustered index [xyz] on [Activity] (Address) include (ActionDate), os tempos de consulta diminuíram de mais de um minuto para menos de um segundo. +10 se eu pudesse. Obrigado!
Metro Smurf