Como li que as buscas de índice na maioria das vezes são preferidas às varreduras de índice, estou tentando algumas coisas.
Eu tenho uma consulta que faz 993 leituras (verificadas com o SQL Profiler) quando está usando uma varredura de índice. Ao usar uma busca de índice, ela precisa de 44.347 leituras. Parece que algo está errado, ou eu não entendo.
Esta é a consulta para varredura de índice:
select t5.Id as t5Id
from table1 t1
left join table2 t2 on t2.Table1Id = t1.Id
left join table3 t3 on t3.Table2Id = t2.Id
left join table4 t4 on t4.Table3Id = t3.Id
left join table5 t5 on t5.Table4Id = t4.Id
esta é a consulta para busca de índice:
select t5.Id as t5Id
from table1 t1
left join table2 t2 on t2.Table1Id = t1.Id
left join table3 t3 on t3.Table2Id = t2.Id
left join table4 t4 on t4.Table3Id = t3.Id
left join table5 t5 WITH (FORCESEEK) on t5.Table4Id = t4.Id
As tabelas são simples e diretas. No final, preencho-os com alguns dados fictícios, para que possam ser reproduzidos facilmente.
CREATE TABLE [dbo].[table1](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_table1] PRIMARY KEY CLUSTERED
(
[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] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[table2](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Table1Id] [bigint] NOT NULL,
CONSTRAINT [PK_table2] PRIMARY KEY CLUSTERED
(
[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]
ALTER TABLE [dbo].[table2] WITH CHECK
ADD CONSTRAINT [FK_table2_table1Id] FOREIGN KEY([table1Id])
REFERENCES [dbo].[table1] ([Id])
GO
ALTER TABLE [dbo].[table2]
CHECK CONSTRAINT [FK_table2_table1Id]
GO
CREATE NONCLUSTERED INDEX [IdxTable2_FKTable1Id] ON [dbo].[table2]
(
[Table1Id] ASC
)
INCLUDE ( [Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE TABLE [dbo].[table3](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Table2Id] [bigint] NOT NULL,
CONSTRAINT [PK_table3] PRIMARY KEY CLUSTERED
(
[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]
ALTER TABLE [dbo].[table3] WITH CHECK
ADD CONSTRAINT [FK_table3_table2Id] FOREIGN KEY([table2Id])
REFERENCES [dbo].[table2] ([Id])
GO
ALTER TABLE [dbo].[table3]
CHECK CONSTRAINT [FK_table3_table2Id]
GO
CREATE NONCLUSTERED INDEX [IdxTable3_FKTable2Id] ON [dbo].[table3]
(
[Table2Id] ASC
)
INCLUDE ( [Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE TABLE [dbo].[table4](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Table3Id] [bigint] NOT NULL,
CONSTRAINT [PK_table4] PRIMARY KEY CLUSTERED
(
[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]
ALTER TABLE [dbo].[table4] WITH CHECK
ADD CONSTRAINT [FK_table4_table3Id] FOREIGN KEY([table3Id])
REFERENCES [dbo].[table4] ([Id])
GO
ALTER TABLE [dbo].[table4]
CHECK CONSTRAINT [FK_table4_table3Id]
GO
CREATE NONCLUSTERED INDEX [IdxTable4_FKTable3Id] ON [dbo].[table4]
(
[Table3Id] ASC
)
INCLUDE ( [Id]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE TABLE [dbo].[table5](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Table4Id] [bigint] NOT NULL,
[Description] [nvarchar](2000) NOT NULL,
CONSTRAINT [PK_table5] PRIMARY KEY CLUSTERED
(
[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]
ALTER TABLE [dbo].[table5] WITH CHECK
ADD CONSTRAINT [FK_table5_table4Id] FOREIGN KEY([table4Id])
REFERENCES [dbo].[table5] ([Id])
GO
ALTER TABLE [dbo].[table5]
CHECK CONSTRAINT [FK_table5_table4Id]
GO
CREATE NONCLUSTERED INDEX [IdxTable5_FKTable4Id] ON [dbo].[table5]
(
[Table4Id] ASC
)
INCLUDE ( [Id], [Description]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
set nocount on
DECLARE @i INT = 0;
DECLARE @j INT = 0;
DECLARE @k INT = 0;
DECLARE @l INT = 10;
DECLARE @m INT = 0;
declare @table1Id bigint
declare @table2Id bigint
declare @table3Id bigint
declare @table4Id bigint
begin tran
WHILE @i < 10
BEGIN
INSERT INTO [dbo].[table1] ([Name]) VALUES (cast(@i as nvarchar(10)))
SELECT @table1Id = SCOPE_IDENTITY()
WHILE @j < 10
BEGIN
INSERT INTO [dbo].[table2] ([Table1Id]) VALUES (@table1Id)
SELECT @table2Id = SCOPE_IDENTITY()
WHILE @k < 10
BEGIN
INSERT INTO [dbo].[table3] ([Table2Id]) VALUES (@table2Id)
SELECT @table3Id = SCOPE_IDENTITY()
WHILE @l > 0
BEGIN
INSERT INTO [dbo].[table4] ([Table3Id]) VALUES (@table3Id)
SELECT @table4Id = SCOPE_IDENTITY()
WHILE @m < 10
BEGIN
INSERT INTO [dbo].[table5] ([Table4Id], [Description]) VALUES (@table4Id, 'Not so long description')
SET @m = @m + 1;
END;
SET @m = 0;
SET @l = @l - 1;
END;
SET @l = 10;
SET @k = @k + 1;
END;
SET @k = 0;
SET @j = @j + 1;
END;
SET @j = 0;
SET @i = @i + 1;
END;
commit
Primeiro, vamos obter uma estimativa do número de leituras necessárias para fazer uma varredura de índice para IdxTable5_FKTable4Id:
No meu sistema, os resultados dessa consulta sugerem que o SQL Server precisará de cerca de 900 leituras para ler o índice por completo. Para testar isso, executarei uma consulta simples que é implementada com mais eficiência como uma varredura de índice no SQL Server. A consulta abaixo precisa da coluna de ID para todas as linhas da tabela5. O SQL Server pode apenas consultar o índice para obter todos os dados necessários para a consulta. Como cada linha é necessária, não há trabalho desperdiçado aqui.
Agora vamos considerar sua primeira consulta de teste que não usa a dica. Estima-se que a tabela externa para a correspondência de hash final tenha 9657 linhas em meu sistema. O otimizador de consulta decidiu que uma varredura de índice em IdxTable5_FKTable4Id é um plano bom o suficiente. Aqui está a consulta que eu executei:
A tabela externa para o hash join na verdade tinha 10.000 linhas. No entanto, como essa é uma junção de hash, o SQL Server ainda precisava verificar todas as 100.000 linhas do índice, embora apenas 10.000 fossem necessárias. É por isso que as mesmas 900 leituras lógicas são necessárias para esta consulta como a primeira. Pode-se argumentar que isso é um esforço desperdiçado pelo otimizador de consulta. Poderia ser mais eficiente usar uma busca de índice para buscar apenas as 10.000 linhas necessárias do índice?
Primeiro vamos obter uma estimativa para o número de leituras necessárias. Seu índice tem uma profundidade de 3:
Também permitirei que o TF 8744 obtenha resultados mais limpos para o propósito desta demonstração:
Eu sei que existem 10.000 linhas da tabela externa, então uma estimativa para o número de leituras lógicas é 10.000 * (3 + 1) = 40.000. Por linha, são 3 para a profundidade do índice e 1 para obter os dados.
Aqui está a consulta que foi testada:
Isso é bem próximo da estimativa de 40.000.
O que aprendemos aqui? Para esse índice, há um custo de cerca de 4 leituras por busca de índice e um custo fixo de 900 leituras para fazer a varredura. Isso significa que, de uma perspectiva de IO, o uso de buscas de índice só será mais eficiente ao obter uma pequena porcentagem dos dados do índice. Caso contrário, obter todos os dados usando uma varredura de índice, mesmo que não seja necessário para retornar resultados corretos para a consulta, será mais eficiente.
Para um teste final, vamos tentar recuperar as primeiras 1.000 linhas da consulta de teste original. No meu sistema, o otimizador de consulta escolhe naturalmente uma busca de índice, mesmo sem a dica. Aqui está a consulta que eu executei:
Para essa consulta, as buscas de índice podem ser vistas como mais eficientes do que uma varredura de índice. Observe que a tabela externa tem 100 linhas, então 100 buscas foram feitas. Uma busca pode retornar mais de 1 linha. É por isso que as leituras lógicas estão próximas de 400 em vez de 4000.
Há muito, muito mais do que uma simples questão de "digitalizar versus buscar". O SQL pode funcionar bem ou mal, dependendo de outros fatores. Uma busca pode ser executada 900 vezes retornando uma linha de cada vez, enquanto uma varredura pode retornar todas as 900 linhas em uma visita. A varredura é melhor nesse cenário.
Você deve olhar para o plano de execução e se familiarizar com a forma de lê-los.
https://stackoverflow.com/questions/758912/how-to-read-an-execution-plan-in-sql-server
Eu vi a mesma coisa em algumas otimizações que estava fazendo. A criação de alguns índices de cobertura acelerou a consulta, mas resultou em um aumento de cerca de 10 vezes nas leituras lógicas.
Há um artigo muito bom sobre isso em http://www.dbsophic.com/learn-more/sql-server-articles/53-tip-comparing-db-sql-server-logical-reads-what-they-really -dizer
De acordo com Ami Levin, o motivo é que um índice bem criado resulta em uma busca de índice com junções de loop aninhadas, em vez de varreduras de índice com junções de hash. Aparentemente, o SQL não conta os testes de hash como leituras lógicas, o que significa que as leituras lógicas entre duas formas de plano diferentes podem realmente comparar maçãs e laranjas.