Que eu gosto de acompanhar blogs não é nenhum segredo, e recentemente vi esse post bem interessante que o Dennes Torres escreveu: http://cidadaocarioca.blogspot.com/2009/03/sql-performance-de-query.html.
É interessante notar que, conhecendo a estrutura do seu banco de dados e seu negócio, é possível melhorar as consultas através de otimizações diversas, sendo uma delas exatamente como o Dennes colocou. Uma otimização limpa e que pode melhorar bastante o resultado.
Quando o SQL Server está montando o plano de execução ele também tenta sempre aplicar os filtros o quanto antes, além de deixar os joins com estimativas mais pesadas para o fim, tentando minimizar ao máximo a quantidade de registros que seguem entre uma operação e outra.
Então você pode estar se perguntando porque o LinqToSql ou o SQL Server não podem fazer a mesma otimização sugerida? Em defesa deles, escrevo um pequeno post. :-)
Não determinismo do resultado
Vamos ao primeiro ponto, que mostra claramente uma característica do banco de dados que muitos esquecem. Dependendo da maneira que o SQL Server monta o plano de execução, o resultado pode ser um pouco diferente, quer ver? Para nosso exemplo, vou inserir um novo registro com informações semelhantes ao quinto item da tabela orders (ordenado por data ASC).
select top 10 * from orders
order by orderdate
/*
10248 VINET 5
10249 TOMSP 6
10250 HANAR 4
10251 VICTE 3
10252 SUPRD 4
*/
INSERT INTO Orders
SELECT
'ALFKI',
3,
OrderDate,
RequiredDate,
ShippedDate,
ShipVia,
Freight,
ShipName,
ShipAddress,
ShipCity,
ShipRegion,
ShipPostalCode,
ShipCountry
FROM Orders
WHERE OrderID = 10252
Se executarmos as duas consultas descritas no post, o resultado é exatamente o mesmo, sendo que a segunda é mais otimizada. Veja abaixo, tudo certo…
-- SELECT gerado pelo LinqToSQL
select top 5 * from orders
inner join customers
on orders.customerid=customers.customerid
inner join employees
on orders.employeeid=employees.employeeid
order by orderdate
/*
10248 VINET 5
10249 TOMSP 6
10250 HANAR 4
10251 VICTE 3
10252 SUPRD 4
*/
-- SELECT gerado na mão
select * FROM
(select top 5 * from orders
order by orderdate) as X
inner join customers
on X.customerid=customers.customerid
inner join employees
on X.employeeid=employees.employeeid
/*
10248 VINET 5
10249 TOMSP 6
10250 HANAR 4
10251 VICTE 3
10252 SUPRD 4
*/
Agora, supondo que o SQL Server resolvesse utilizar uma outra técnica de join para resolver essa consulta, qual seria o resultado? Uma distribuição dos dados diferente, novas estatísticas, falta de índices e outras razões poderiam causar isso. Para simularmos a situação, vou usar uma hint de merge join…
select top 5 * from orders
inner merge join customers
on orders.customerid=customers.customerid
inner join employees
on orders.employeeid=employees.employeeid
order by orderdate
/*
10248 VINET 5
10249 TOMSP 6
10251 VICTE 3
10250 HANAR 4
11081 ALFKI 3
*/
select * FROM
(select top 5 * from orders
order by orderdate) as X
inner merge join customers
on X.customerid=customers.customerid
inner join employees
on X.employeeid=employees.employeeid
/*
10250 HANAR 4
10252 SUPRD 4
10249 TOMSP 6
10251 VICTE 3
10248 VINET 5
*/
O que temos? Resultados diferentes, pois no primeiro o top 5 é aplicado em cima de uma ordenação dos dados resultante de dois merge joins, enquanto no segundo o join já é feito sobre um conjunto de dados de cinco registros, que trouxe o VINET ao invés do ALFKI.
Essa é uma característica do SQL Server e você deve estar ciente do que pode acontecer. Se fosse utilizado o TOP N WITH TIES, aí o resultado seria composto dos mesmos registros, em ordem diferente, mas você veria todo mundo.
Questão do campo NULL
Outro aspecto que merece atenção é o cenário onde existe um pedido feito pela Internet (suposição), que não possui empregado associado. Vamos ver o que acontece com as duas consultas?
select top 10 * from orders
order by orderdate
SELECT * FROM Orders
WHERE OrderID = 10249
-- Inserindo um registro de compra feito pela internet (sem empregado relacionado)
INSERT INTO Orders
SELECT
'ALFKI',
null,
OrderDate,
RequiredDate,
ShippedDate,
ShipVia,
Freight,
ShipName,
ShipAddress,
ShipCity,
ShipRegion,
ShipPostalCode,
ShipCountry
FROM Orders
WHERE OrderID = 10249
-- SELECT gerado pelo LinqToSQL
select top 5 OrderID, orders.CustomerID, orders.EmployeeID from orders
inner join customers
on orders.customerid=customers.customerid
inner join employees
on orders.employeeid=employees.employeeid
order by orderdate
/*
10248 VINET 5
10249 TOMSP 6
10250 HANAR 4
10251 VICTE 3
10252 SUPRD 4
*/
-- SELECT gerado na mão
select OrderID, X.CustomerID, X.EmployeeID FROM
(select top 5 * from orders
order by orderdate) as X
inner join customers
on X.customerid=customers.customerid
inner join employees
on X.employeeid=employees.employeeid
/*
10248 VINET 5
10249 TOMSP 6
10250 HANAR 4
10251 VICTE 3
*/
Ops, esse caso é mais grave! Como na segunda consulta o TOP 5 é feito antes do JOIN e existe um registro que não se enquadra no resultado por ser um INNER JOIN, acabamos com somente quatro registros retornados.
Conclusão
É importante sabermos dos possíveis efeitos colaterais da otimização acima, por isso mencionei que é necessário conhecer o negócio para saber se é factível de ser implementado. O primeiro realmente é uma situação muito específica e talvez não traga impacto, mas o segundo pode trazer problemas maiores, por retornar um registro a menos.
Então no fim, o SQL Server e o LinqtoSQL precisam de manter a consulta original, pois é semanticamente incorreto fazerem esse tipo de otimização. Já o DBA não, se ele tiver controle do banco, pode usar esse tipo de estratégia bem destacada pelo Dennes, que ajuda MUITO o SQL Server.
Isso significa que você não deve usar uma tecnologia de mapeamento objeto/relacional? Besteira! Porém saiba que sempre existe um trade-off, mas no fim acredito que os ganhos de produtividade da equipe de desenvolvimento serão maiores do que um eventual ajuste de desempenho feito pelo seu time.
Pronto! O primeiro post sobre SQL Server no novo blog. :-)
[]s
Luciano Caixeta Moreira
luticm79@hotmail.com