Hola a todos!!!
Después de bastante tiempo sin escribir por diversos líos que llevo de los que pronto empezaré a escribir, voy a retomar el tema para que no se me oxide la pluma. Imaginaros si estaba desconectado que hasta Jose Bonnin (@wasat) me lanzó un guante el otro día para que escribiese más 😉
Este corto post viene de una discusión que he tenido con Pedro (@_pedrohurtado) y Carry (@3lcarry) sobre cómo escribir las sentencias SQL, y de paso aprovecharé para explicar un poco cómo funciona SQL Server arriesgándome a que algún crack de los que tanto aprecio me tire de las orejas.
Para empezar describiré un poco el contexto y el origen de este extraño post de SQL Server. La discusión viene de una sentencia Linq que habíamos escrito más o menos así (elimino cosas sin interés y cambio los nombres para que sea autoexplicativo):
using (var modelo = new PruebasEntities())
{
var result1 = (
from l in modelo.Linea
where l.Factura.cliente == "Cliente 10000"
select l.articulo
).ToList();
}
Pedro comentaba que esto es (o como mínimo era) ineficiente y que no aplicaría bien el índice, ya que el filtro se hace sobre un campo Cliente de Factura y debería empezar siempre poniendo el primer FROM sobre esa tabla (la de Factura). Por ello, él proponía hacerlo de la siguiente manera aun sabiendo que era más clara y legible la primera:
using (var modelo = new PruebasEntities())
{
var result2 = (
from f in modelo.Factura
from l in f.Lineas
where f.cliente == "Cliente 10000"
select l.articulo
).ToList();
}
Así que vamos a analizar un poco lo que sucede para resolver nuestras dudas.
Montaje del entorno
Lo primero que he hecho es montar todo el entorno en SQL Server y para ello en contra de la opinión de mucha gente he ejecutado un script para crear la estructura, he rellenado unos poquitos datos (más de 1MM de filas), y he levantado el EF con Data First.
create database Pruebas
go
use Pruebas
go
create table Factura (
id int identity(1, 1) primary key,
cliente nvarchar(max)
)
create table Linea (
id int identity(1, 1) primary key,
factura_id int not null references Factura (id),
articulo nvarchar(max)
)
CREATE NONCLUSTERED INDEX FK_Linea_Factura ON Linea (factura_id)
go
set identity_insert Factura on
go
declare @i integer
set @i = 1
while (@i <= 10000) begin
insert Factura (id, cliente) values (@i, 'Cliente ' + CAST(@i as nvarchar(max)))
set @i = @i + 1
end
go
set identity_insert Factura off
go
set identity_insert Linea on
go
declare @i integer
set @i = 1
while (@i <= 1000000) begin
insert Linea (id, factura_id, articulo) values (@i, (@i % 10000) + 1, 'Articulo ' + CAST(@i as nvarchar(max)))
set @i = @i + 1
end
go
set identity_insert Linea off
Sentencia generada por Entity Framework
Después he decidido utilizar nuestro querido SQL Server Profiler para ver qué genera en cada caso. La idea que perseguía era ver si el generador de EF ejecutaba la misma SQL y por tanto problema resulto.
Pero no, en el primer caso ha lanzado la siguiente consulta:
SELECT [Extent1].[articulo] AS [articulo]
FROM [dbo].[Linea] AS [Extent1]
INNER JOIN [dbo].[Factura] AS [Extent2] ON [Extent1].[factura_id] = [Extent2].[id]
WHERE N'Cliente 10000' = [Extent2].[cliente]
y en el segundo:
SELECT [Extent2].[articulo] AS [articulo]
FROM [dbo].[Factura] AS [Extent1]
INNER JOIN [dbo].[Linea] AS [Extent2] ON [Extent1].[id] = [Extent2].[factura_id]
WHERE N'Cliente 10000' = [Extent1].[cliente]
Así que la primera conclusión es que la sintaxis se propaga y en el primer caso se empieza por Linea y en el segundo por Factura.
Plan de ejecución ejecutado
Lo siguiente es ver qué sucede internamente en Sql Server e intentaré ver si es el propio Sql Server el que resuelve nuestra duda, y para ello ejecutaré las siguientes sentencias y veré los planes de ejecución que se generan:
Y ahora sí… voilá.
Sucedió lo que tenia que suceder: SQL Server sigue como era de esperar la teoría de conjuntos y por tanto poco importa el orden de los FROM ni de los WHERE, ni si es mejor poner un JOIN o una “,” con un WHERE… da igual. El plan de ejecución que se genera a partir de la sentencia es el mismo.
SQL Server sigue la teoría de conjuntos y no se puede suponer nunca el orden de nada si no se explicita. Es decir, si pides que te venga la población de una tabla, lo más lógico es que esperes recibir los datos por id o fecha de creación, pero no tiene porqué ser así. Lo más usual es que sí lo sea, ya que el motor recorrerá el índice clustered que está ordenado por id, pero ¿quien dice que no recorra otro filtro? ¿o al hacer JOIN los datos venga por Foreign Key porque para hacer un JOIN ha preferido ordenar? De esto solo podemos sacar una conclusión: Si quieres un orden has de ponerlo, aunque sea por id, sino espérate a recibir los datos como a SQL Server le sea más óptimo.
De igual manera pasa con los FROM, WHERE, … SQL Server los ejecuta en el orden que cree más optimo basándose en cosas como estadísticas, filtros disponibles, … pero no el orden en el que se escriba la sentencia.
Espero haber aclarado algunas cosillas y que me haya explicado con suficiente claridad.
Au
PD: Para los que al ver las sentencias y los planes de ejecución han visto algo extraño… enhorabuena y sí… tenéis razón: El índice que he creado podría haberlo hecho de forma que cubriese bien la sentencia y evitar la búsqueda por el índice clustered pero el objetivo era mostrar este caso y no optimizar la consulta 😉
kiquenet
Fantástico !! Saludos.
victorgonzalez
A lo mejor no lo veo correctamente, pero me da la sensación que el plan de ejecución es de la misma sentencia.