Há uns dias, em conversa com colegas sobre o meu post em que recomendava o uso de constraints, surgiu uma pergunta pertinente:
“Então não bastava termos usado uns índices e o assunto ficaria resolvido?”
A minha resposta foi “Não. Mas não te fies na minha resposta, testa e vê com os teus próprios olhos, ou melhor ainda, com o teu management studio.”
Vamos lá revisitar o código do outro post com algumas melhorias para tirarmos as dúvidas.
Começamos pelas tais tabelas que só interessam para demos e a vista a juntar tudo
CREATE TABLE [dbo].[Dates2015]( id bigint primary key identity(1,1) not null , dt_ref date not null , TextCol nvarchar(50) not null ) ON [PRIMARY] GO CREATE TABLE [dbo].[Dates2016]( id bigint primary key identity(1,1) not null , dt_ref date not null , TextCol nvarchar(50) not null ) ON [PRIMARY] GO CREATE TABLE [dbo].[Dates2017]( id bigint primary key identity(1,1) not null , dt_ref date not null , TextCol nvarchar(50) not null ) ON [PRIMARY] GO CREATE TABLE [dbo].[Dates2018]( id bigint primary key identity(1,1) not null , dt_ref date not null , TextCol nvarchar(50) not null ) ON [PRIMARY] GO CREATE VIEW [dbo].[DatesAll] AS SELECT * FROM dbo.Dates2015 union all SELECT * from dbo.Dates2016 union all SELECT * from dbo.Dates2017 union all SELECT * from dbo.Dates2018 GO
Introduzimos agora uns valores nas tabelas para que elas não se sintam vazias, desprovidas de realidade…
insert into Dates2015 ( Dt_ref, TextCol) select DATEADD(day, ROUND(((DATEDIFF(day, '20150101', '20151231')-1) * RAND()), 0), '20150101') , CAST(newid() as varchar(50)) go 10000 insert into Dates2016 ( Dt_ref, TextCol) select DATEADD(day, ROUND(((DATEDIFF(day, '20160101', '20161231')-1) * RAND()), 0), '20160101') , CAST(newid() as varchar(50)) go 10000 insert into Dates2017 ( Dt_ref, TextCol) select DATEADD(day, ROUND(((DATEDIFF(day, '20170101', '20171231')-1) * RAND()), 0), '20170101') , CAST(newid() as varchar(50)) go 10000 insert into Dates2018 ( Dt_ref, TextCol) select DATEADD(day, ROUND(((DATEDIFF(day, '20180101', '20181231')-1) * RAND()), 0), '20180101') , CAST(newid() as varchar(50)) go 10000
Agora vamos lá verificar o estado sem índices nem constraints
set statistics time, io on select top 100 * from [dbo].[DatesAll] where dt_ref >= '2018-01-05'
Os resultados não interessam, mas o plano de execução e os resultados do tempo e i/o são esclarecedores

Vemos no plano de execução que andamos pelas 4 tabelas

Notamos bem que varremos as 4 tabelas e, de início parece estranho mas se pensarmos bem é o mais natural, faz menos reads na tabela onde sabemos ser a única que tem dados.
Os mais atentos notaram no missing index recomendation na tabela Dates2017, mas realmente ele pede-nos índices nas 3 tabelas onde teve mais problemas. Mais uma machadada para quem gosta de “seguir à risca” o que o optimizer sugere.

Quando há pouco disse que “é o mais natural”… é o mais natural porquê?
Vejamos o que o nosso amigo SQL Server sabe. Tem uma query a uma vista que é a concatenação de 4 tabelas. Não sabe grande coisa sobre essas tabelas. Vai à vista e expande o código, aparece-lhe a primeira tabela que é a Dates2015, varre-a toda à procura de 100 linhas que respondam à pergunta que lhes fizemos. Depois vai à seguinte… e à seguinte… e finalmente encontra uma onde consegue retirar de lá as 100 linhas sem ter que procurar mais.
“Ó António, estás-me a dizer que a ordem das tabelas na vista interessa?” Claro… mas como sempre… nada como experimentarmos com uma query que sabemos ser satisfeita com a primeira tabela da vista.
select top 100 * from [dbo].[DatesAll] where dt_ref <= '2015-10-05'


Ora aqui está. 4 reads na Dates2015 e não precisou de ir às outras 3 tabelas, apesar de ter pensado nisso aquando da optimização.
E, já que por aqui andamos, vamos fazer um join que sabemos ser impossível.
select * from Dates2015 a inner join Dates2016 b on a.Dt_ref = b.Dt_ref


Pois é, nós sabemos mas “ele” não sabe, e por isso anda à procura.
E agora vamos ao que interessa neste post, a resposta à pergunta “e com índices, pá”?
create nonclustered index ncidx on Dates2015 (Dt_ref) create nonclustered index ncidx on Dates2016 (Dt_ref) create nonclustered index ncidx on Dates2017 (Dt_ref) create nonclustered index ncidx on Dates2018 (Dt_ref)
select top 100 * from [dbo].[DatesAll] where dt_ref >= '2018-01-05'

Nada de muito óbvio, à parte de agora ser um index seek.

Agora já se vê alguma diferença. Quase não há reads, afinal andamos pelo índice e não pela tabela e ele, apesar de não saber tudo, já sabe procurar melhor e mais rápido.
E com o caso do join impossível? Também melhora?


Infelizmente não há muito que se tenha melhorado. “Não fez nada”, diríamos.
Pois não, não fez, porque o nosso amigo “select * ” não deixou. Havia pura e simplesmente muito trabalho a ir buscar os campos adicionais que não estavam no nosso índice e o optimizer preferiu usar a tabela toda. Bookmark lookups são ainda piores. Haveremos de ver isso em outro post.
Vamos lá repetir isto apenas a ir buscar a data.
select a.dt_ref from Dates2015 a inner join Dates2016 b on a.Dt_ref = b.Dt_ref


Em vez de um gastador Hash Match temos um Merge Join e mais rapidamente chegamos ao resultado.
E se criarmos as nossas constraints? Aquelas que ajudam muita gente…
alter table [dbo].[Dates2015] add constraint dt_2015 check ( dt_ref >= '2015-01-01' and dt_ref <= '2015-12-31' ) alter table [dbo].[Dates2016] add constraint dt_2016 check ( dt_ref >= '2016-01-01' and dt_ref <= '2016-12-31' ) alter table [dbo].[Dates2017] add constraint dt_2017 check ( dt_ref >= '2017-01-01' and dt_ref <= '2017-12-31' ) alter table [dbo].[Dates2018] add constraint dt_2018 check ( dt_ref >= '2018-01-01' and dt_ref <= '2018-12-31' )
vamos lá repetir as nossa queries
select top 100 * from [dbo].[DatesAll] where dt_ref >= '2018-01-05'


E aos joins?
select * from Dates2015 a inner join Dates2016 b on a.Dt_ref = b.Dt_ref


“Então não bastava termos usado uns índices e o assunto ficaria resolvido?”
Não.