Estou tentando convencer um pouco mais de desempenho de uma consulta que está acessando uma tabela com ~ 250 milhões de registros. Na 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 verificação do índice sobre hugetable está sendo executada 480 vezes (para cada linha em #smalltable). Isso me parece inverso, 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 para definição completa) abrange as colunas fk (o predicado de junção), adicionado (usado na cláusula where) & id (inútil) em ordem crescente e inclui valor .
Quando faço isso, no entanto, a consulta sai de 2 1/2 minutos para mais de 9. Eu esperaria que as dicas forçassem uma junção mais eficiente, que apenas passasse uma única vez sobre cada tabela, mas claramente não.
Qualquer orientação é bem vinda. Informações adicionais fornecidas, se necessário.
Atualização (02/06/2011)
Tendo reorganizado a indexação na tabela, fiz incursões significativas no desempenho, no entanto, encontrei um novo obstáculo ao resumir os dados na enorme tabela. O resultado é uma 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);
No momento, o hugetable possui um índice agrupado pk_hugetable (added, fk)
(a chave primária) e um índice não agrupado na outra direção ix_hugetable (fk, added)
.
Sem a quarta coluna acima, o otimizador usa uma junção de loop aninhada como antes, usando #smalltable como entrada externa, e um índice não agrupado busca como 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 quarta coluna, o tempo de execução aumenta para 4 minutos. Desta vez, ele procura no índice clusterizado (2 execuções) pelo mesmo custo relativo (45%), agregado por meio de uma correspondência de hash (30%) e, em seguida, faz uma junção de hash em #smalltable (0%).
Não tenho certeza quanto ao meu próximo curso de ação. Minha preocupação é que nem a pesquisa por período nem o predicado de junção sejam garantidos ou mesmo o que provavelmente reduza drasticamente o conjunto de resultados. Na maioria dos casos, o período varia apenas entre 10 e 15% dos registros, e a junção interna no fk pode filtrar entre 20 e 30%.
Conforme solicitado por 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 em que todas as tabelas em todos os lugares deveriam ter um GUID, sem exceções.
fonte
Respostas:
Você
ix_hugetable
parece bastante inútil porque:Além disso: - adicionado ou fk deve ser o primeiro - o ID é o primeiro = pouco uso
Tente alterar a chave em cluster 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 instruções de ordem de tabela e sem dicas JOIN / INDEX. Tento não usar as dicas JOIN / INDEX pessoalmente, porque você remove as opções do otimizador. Há muitos anos, fui informado (seminário com um SQL Guru) que a dica do FORCE ORDER pode ajudar quando você tem uma mesa enorme. JOIN table pequena: YMMV 7 anos depois ...
Ah, e deixe-nos saber onde o DBA mora para que possamos providenciar alguns ajustes de percussão
Editar após atualização de 02 de junho
A quarta coluna não faz parte do índice não agrupado em cluster, portanto, usa o índice agrupado.
Tente alterar o índice NC para INCLUDE a coluna value, para que ele não precise acessar a coluna value para o índice em cluster
Nota: Se o valor 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ê alterar
COUNT(value)
paraCOUNT(DISTINCT value)
sem alterar o índice, ele deve interromper a consulta novamente, pois precisa processar o valor como um valor, não como uma existência.A consulta precisa de 3 colunas: adicionado, fk, valor. Os dois primeiros são filtrados / unidos, assim como as colunas principais. O valor é usado apenas para que possa ser incluído. Uso clássico de um índice de cobertura.
fonte
Defina um índice
hugetable
apenas naadded
coluna.Os bancos de dados usarão um índice de várias partes (várias colunas) apenas à 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, portanto ignora o índice.fonte
Essa é a ordem que eu esperaria que o otimizador de consultas usasse, assumindo que um loop se juntasse à escolha certa. A alternativa é fazer um loop de 250 milhões de vezes e executar 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 é de quase 250 milhões de linhas * 'o tamanho de cada linha' - não é pequeno, tem pelo menos alguns GB. A julgar pela
sp_spaceused
saída 'alguns GB' pode ser um eufemismo - a junção MERGE exige que você vasculhe o índice, o que exigirá muito I / O.fonte
Seu índice está incorreto. Veja os índices dos e donts .
No momento, seu único índice útil é o da chave primária da pequena tabela. O único plano razoável é, portanto, seq varrer a pequena mesa e aninhar a bagunça com a enorme.
Tente adicionar um índice clusterizado
hugetable(added, fk)
. Isso deve fazer com que o planejador procure linhas aplicáveis da tabela enorme e aninhe loop ou mesclagem e junte-as à tabela pequena.fonte