A idéia desse artigo é verificar o desempenho do uso do Linq para realização de consulta a uma banco de dados Sql Server. Para isto irei utilizar Linq to Sql, consultando um banco Sql Server 2008. A database de teste aqui utilizada é AdventureWorksLite que pode ser baixada no endereço: http://www.codeplex.com/MSFTDBProdSamples.
Nesse artigo, foram realizadas varias consultas em Linq, desde simples consultas até consulta mais complexas. Utilizei uma ferramenta do Sql Server para capturar as querys enviadas ao banco de dados na execução da aplicação e as mesmas serão expostas aqui.
Em muitas dessas consultas o Linq to Sql utiliza a procedure do Sql Server sp_executesql. Com esta procedure podemos passar para o SQL Server um string contendo um comando ou batch de comandos que serão executados no banco de dados como se fossem digitados diretamente no SSMS. A vantagem dessa procedure é o fato da mesma ainda suportar a criação de parâmetros.
No projeto utilizado para o desenvolvimento desse artigo foi criado um DataBaseContext, com da imagem:

Realizando uma consulta simples
Primeiro foi realizado uma simples consulta da tabelas Customer com o código:
BlogPerformanceDataContext dbContext = new LinqPerformanceLibrary.BlogPerformanceDataContext();
Table<Customer> tbCustomer = dbContext.Customers;
foreach(Customer cust intbCustomer)
Console.WriteLine(cust.FirstName);
A idéia seria listar todos os clientes cadastrados, exibindo apenas o primeiro nome dos mesmos. O SQL criado pelo Linq foi:
SELECT [t0].[CustomerID],
[t0].[NameStyle], [t0].[Title],
[t0].[FirstName], [t0].[MiddleName],
[t0].[LastName], [t0].[Suffix], [t0].[CompanyName],
[t0].[SalesPerson], [t0].[EmailAddress], [t0].[Phone],
[t0].[PasswordHash], [t0].[PasswordSalt], [t0].[rowguid], [t0].[ModifiedDate]
FROM [SalesLT].[Customer] AS [t0]
Esta consulta não é muito diferente do que se esperava – mas vale ressaltar aqui esta consulta foi enviada ao banco de dados apenas quando o código atingiu a instrução de loop e não na declaração do Linq (no Linq esta propriedade é chamada de Deferred Loading)
Realizando uma consulta com filtro simples
Vamos realizar uma consulta utilizando um filtro simples, como no exemplo de código:
BlogPerformanceDataContext dbContext = new BlogPerformanceDataContext();
var query = from cust in dbContext.Customers
where cust.FirstName == "John"
select cust.LastName;
foreach (string Nome in query)
Console.WriteLine(Nome);
Console.ReadLine();
Nessa consulta estou filtrando dos os clientes cujo o primeiro nome seja John, o SQL criado foi:
exec sp_executesql
N'SELECT [t0].[LastName]
FROM [SalesLT].[Customer] AS [t0]
WHERE [t0].[FirstName] = @p0',N'@p0 nvarchar(4)',@p0=N'John'
Aqui passa a se usar a procedure SP_executesql. Reparem que o filtro é passado como parâmetro para a consulta, podemos re-escrever esta consulta para o formato que será enviado ao engine do banco de dados:
SELECT [t0].[LastName]
FROM [SalesLT].[Customer] AS [t0]
WHERE [t0].[FirstName] = N'John'
Obtendo os registro pais e filhos
Vamos listar um cliente (filtrando pelo CustemerID) e os seus respectivos pedidos usando o código:
BlogPerformanceDataContext dbContext = new
BlogPerformanceDataContext();
var query = from c in dbContext.Customers
where c.CustomerID == 1
select c;
foreach (Customer cust in query)
{
Console.WriteLine(cust.LastName);
foreach (SalesOrderHeader sales in cust.SalesOrderHeaders)
Console.WriteLine("\t{0} - {1} - {2}", sales.SalesOrderNumber, sales.ShipDate, sales.SubTotal);
}
Nesse caso temos o seguinte:
Quando o código atinge o primeiro loop (em Customers) a seguinte query é enviada ao banco de dados:
exec
sp_executesql
N'SELECT [t0].[CustomerID], [t0].[NameStyle],
[t0].[Title], [t0].[FirstName],
[t0].[MiddleName], [t0].[LastName],
[t0].[Suffix], [t0].[CompanyName],
[t0].[SalesPerson], [t0].[EmailAddress],
[t0].[Phone], [t0].[PasswordHash],
[t0].[PasswordSalt],
[t0].[rowguid],
[t0].[ModifiedDate]
FROM [SalesLT].[Customer] AS [t0]
WHERE [t0].[CustomerID] = @p0',N'@p0 int',@p0=1
E, para o loop de pedidos temos a consulta:
exec
sp_executesql
N'SELECT [t0].[SalesOrderID],
[t0].[RevisionNumber],
[t0].[OrderDate], [t0].[DueDate],
[t0].[ShipDate], [t0].[Status],
[t0].[OnlineOrderFlag], [t0].[SalesOrderNumber],
[t0].[PurchaseOrderNumber], [t0].[AccountNumber],
[t0].[CustomerID], [t0].[ShipToAddressID],
[t0].[BillToAddressID], [t0].[ShipMethod],
[t0].[CreditCardApprovalCode], [t0].[SubTotal],
[t0].[TaxAmt], [t0].[Freight], [t0].[TotalDue],
[t0].[Comment], [t0].[rowguid], [t0].[ModifiedDate]
FROM [SalesLT].[SalesOrderHeader] AS [t0]
WHERE [t0].[CustomerID] = @p0',N'@p0 int',@p0=1
Se a consulta inicial retorna-se mais de um registro, a segunda consulta seria executada para cada um desses registros. Ou seja, se a consulta inicial retorna 10 registros, para cada um desses 10 registros será executada mais uma query para obter os pedidos. Isto em um cenário de alta concorrência poderá ser um fator de contenção e degradação.
Utilizando o DataLoadOptions
Realizando uma consulta semelhante a anterior mas agora utilizando o DataLoadOption:
BlogPerformanceDataContext dbContext = new
BlogPerformanceDataContext();
DataLoadOptions dOptions = new
DataLoadOptions();
dOptions.LoadWith<Customer>(c => c.SalesOrderHeaders);
dbContext.LoadOptions = dOptions;
var query = from c in dbContext.Customers
where c.FirstName == "Walter"
select c;
foreach (Customer cust in query)
{
Console.WriteLine(cust.LastName);
foreach (SalesOrderHeader sales in cust.SalesOrderHeaders)
Console.WriteLine("\t{0} - {1} - {2}", sales.SalesOrderNumber, sales.ShipDate, sales.SubTotal);
}
O interessante que agora é realizada apenas uma única consulta ao banco dados, quando o código do primeiro loop é executado:
exec
sp_executesql
N'SELECT [t0].[CustomerID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName],
[t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[CompanyName],
[t0].[SalesPerson], [t0].[EmailAddress], [t0].[Phone], [t0].[PasswordHash],
[t0].[PasswordSalt], [t0].[rowguid], [t0].[ModifiedDate], [t1].[SalesOrderID],
[t1].[RevisionNumber], [t1].[OrderDate], [t1].[DueDate], [t1].[ShipDate],
[t1].[Status], [t1].[OnlineOrderFlag], [t1].[SalesOrderNumber],
[t1].[PurchaseOrderNumber], [t1].[AccountNumber], [t1].[CustomerID] AS [CustomerID2],
[t1].[ShipToAddressID], [t1].[BillToAddressID], [t1].[ShipMethod],
[t1].[CreditCardApprovalCode], [t1].[SubTotal], [t1].[TaxAmt],
[t1].[Freight], [t1].[TotalDue], [t1].[Comment],
[t1].[rowguid] AS [rowguid2], [t1].[ModifiedDate] AS [ModifiedDate2], (
SELECT COUNT(*)
FROM [SalesLT].[SalesOrderHeader] AS [t2]
WHERE [t2].[CustomerID] = [t0].[CustomerID]
) AS [value]
FROM [SalesLT].[Customer] AS [t0]
LEFT OUTER JOIN [SalesLT].[SalesOrderHeader] AS [t1] ON [t1].[CustomerID] = [t0].[CustomerID]
WHERE [t0].[LastName] = @p0
ORDER BY [t0].[CustomerID], [t1].[SalesOrderID]',N'@p0 nvarchar(4)',@p0=N'King'
O que está bem mais coerente ao esperado e apresenta uma performance melhor do que o exemplo anterior. Em um primeiro momento podemos notar o impacto da utilização ou não do DataLoadOption para performance de consultas Linq.
Continuaremos em outros artigos….
T+