Estou tentando obter um pouco mais de desempenho de uma consulta que está acessando uma tabela com ~ 250 milhões de registros. Da minha leitura do plano de execução real (não estimado), o primeiro gargalo é uma consulta que se parece com isso:
select
b.stuff,
a.added,
a.value
from
dbo.hugetable a
inner join
#smalltable b on a.fk = b.pk
where
a.added between @start and @end;
Veja mais abaixo as definições das tabelas e índices envolvidos.
O plano de execução indica que um loop aninhado está sendo usado em #smalltable, e que a varredura de índice sobre enorme tabela está sendo executada 480 vezes (para cada linha em #smalltable). Isso parece inverso para mim, então tentei forçar uma junção de mesclagem a ser usada:
select
b.stuff,
a.added,
a.value
from
dbo.hugetable a with(index = ix_hugetable)
inner merge join
#smalltable b with(index(1)) on a.fk = b.pk
where
a.added between @start and @end;
O índice em questão (veja abaixo a definição completa) abrange as colunas fk (o predicado de junção), adicionado (usado na cláusula where) e id (inútil) em ordem crescente e inclui value .
Quando faço isso, no entanto, a consulta passa de 2 1/2 minutos para mais de 9. Eu esperava que as dicas forçassem uma junção mais eficiente que fizesse apenas uma única passagem por cada tabela, mas claramente não.
Qualquer orientação é bem-vinda. Informações adicionais fornecidas, se necessário.
Atualização (2011/06/02)
Tendo reorganizado a indexação na tabela, fiz incursões significativas de desempenho, mas encontrei um novo obstáculo quando se trata de resumir os dados na tabela enorme. O resultado é um resumo por mês, que atualmente se parece com o seguinte:
select
b.stuff,
datediff(month, 0, a.added),
count(a.value),
sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
dbo.hugetable a
inner join
#smalltable b on a.fk = b.pk
group by
b.stuff,
datediff(month, 0, a.added);
Atualmente, o enormetable tem um índice clusterizado pk_hugetable (added, fk)
(a chave primária) e um índice não clusterizado indo para o outro lado ix_hugetable (fk, added)
.
Sem a 4ª coluna acima, o otimizador usa uma junção de loop aninhado como antes, usando #smalltable como a entrada externa e uma busca de índice não clusterizada como o loop interno (executando 480 vezes novamente). O que me preocupa é a disparidade entre as linhas estimadas (12.958,4) e as linhas reais (74.668.468). O custo relativo dessas buscas é de 45%. No entanto, o tempo de execução é inferior a um minuto.
Com a 4ª coluna, o tempo de execução aumenta para 4 minutos. Desta vez, ele busca no índice clusterizado (2 execuções) pelo mesmo custo relativo (45%), agrega por meio de uma correspondência de hash (30%), depois faz uma junção de hash em #smalltable (0%).
Estou inseguro quanto ao meu próximo curso de ação. Minha preocupação é que nem a pesquisa de intervalo de datas nem o predicado de junção sejam garantidos ou mesmo tudo o que provavelmente reduzirá drasticamente o conjunto de resultados. O intervalo de datas na maioria dos casos apenas cortará talvez 10-15% dos registros, e a junção interna em fk pode filtrar talvez 20-30%.
Conforme solicitado pelo Will A, os resultados de sp_spaceused
:
name | rows | reserved | data | index_size | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB
#smalltable é definido como:
create table #endpoints (
pk uniqueidentifier primary key clustered,
stuff varchar(6) null
);
Enquanto dbo.hugetable é definido como:
create table dbo.hugetable (
id uniqueidentifier not null,
fk uniqueidentifier not null,
added datetime not null,
value decimal(13, 3) not null,
constraint pk_hugetable primary key clustered (
fk asc,
added asc,
id asc
)
with (
pad_index = off, statistics_norecompute = off,
ignore_dup_key = off, allow_row_locks = on,
allow_page_locks = on
)
on [primary]
)
on [primary];
Com o seguinte índice definido:
create nonclustered index ix_hugetable on dbo.hugetable (
fk asc, added asc, id asc
) include(value) with (
pad_index = off, statistics_norecompute = off,
sort_in_tempdb = off, ignore_dup_key = off,
drop_existing = off, online = off,
allow_row_locks = on, allow_page_locks = on
)
on [primary];
O campo id é redundante, um artefato de um DBA anterior que insistia que todas as tabelas em todos os lugares deveriam ter um GUID, sem exceções.
Sua
ix_hugetable
aparência é bastante inútil porque:Além disso: - adicionado ou fk deve ser o primeiro - ID é o primeiro = não é muito útil
Tente alterar a chave clusterizada para
(added, fk, id)
e solteix_hugetable
. Você já tentou(fk, added, id)
. Se nada mais, você economizará muito espaço em disco e manutenção de índiceOutra opção pode ser tentar a dica FORCE ORDER com a ordem da tabela de duas maneiras e sem dicas de JOIN/INDEX. Eu tento não usar dicas de JOIN/INDEX pessoalmente porque você remove as opções do otimizador. Muitos anos atrás me disseram (seminário com um SQL Guru) que a dica FORCE ORDER pode ajudar quando você tem uma mesa enorme JOIN mesa pequena: YMMV 7 anos depois...
Ah, e deixe-nos saber onde mora o DBA para que possamos providenciar alguns ajustes de percussão
Editar, após atualização de 02 de junho
A 4ª coluna não faz parte do índice não clusterizado, portanto, usa o índice clusterizado.
Tente alterar o índice NC para INCLUIR a coluna de valor para que ele não precise acessar a coluna de valor para o índice clusterizado
Observação: se value não for anulável, será o mesmo que
COUNT(*)
semanticamente. Mas para SUM ele precisa do valor real , não da existência .Por exemplo, se você mudar
COUNT(value)
paraCOUNT(DISTINCT value)
sem alterar o índice, ele deve quebrar a consulta novamente porque tem que processar valor como um valor, não como existência.A consulta precisa de 3 colunas: adicionado, fk, valor. Os 2 primeiros são filtrados/unidos, assim como as colunas-chave. valor é apenas usado para que possa ser incluído. Uso clássico de um índice de cobertura.
Defina um índice
hugetable
apenas naadded
coluna.Os bancos de dados usarão um índice de várias partes (várias colunas) apenas na extrema direita da lista de colunas, pois possui valores contados a partir da esquerda. Sua consulta não especifica
fk
na cláusula where da primeira consulta, então ela ignora o índice.Esta é a ordem que eu espero que o otimizador de consulta use, supondo que um loop se junte à escolha certa. A alternativa é fazer um loop de 250 milhões de vezes e realizar uma pesquisa na tabela #temp a cada vez - o que pode levar horas / dias.
O índice que você está forçando a ser usado na junção MERGE é praticamente 250 milhões de linhas * 'o tamanho de cada linha' - não é pequeno, pelo menos alguns GB. A julgar pela
sp_spaceused
saída 'um par de GB' pode ser um eufemismo - a junção MERGE requer que você vasculhe o índice que será muito intensivo em E/S.Seu índice está incorreto. Veja os índices dos e donts .
Do jeito que as coisas estão, seu único índice útil é aquele na chave primária da pequena tabela. O único plano razoável é, portanto, escanear seq a mesa pequena e aninhar a bagunça com a enorme.
Tente adicionar um índice clusterizado em
hugetable(added, fk)
. Isso deve fazer com que o planejador procure as linhas aplicáveis da tabela enorme e aninhar o loop ou mesclar juntá-las à tabela pequena.