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.