Em programação, as abstrações são coisas poderosas:
Joel Spolsky tem um artigo no qual ele afirma que
Todas as abstrações não triviais, em algum grau, têm vazamentos.
Isso é excessivamente dogmático – por exemplo, as classes bignum são exatamente as mesmas, independentemente da multiplicação nativa de números inteiros. Ignorando isso, essa afirmação é essencialmente verdadeira, mas é um tanto insana e não atinge o objetivo. Sem abstrações, todo o nosso código seria completamente interdependente e impossível de ser mantido, e as abstrações fazem um trabalho notável para limpar isso. É uma prova do poder da abstração e do quanto a tomamos como certa que tal afirmação possa ser feitacomo se sempre esperássemos ser capazes de escrever grandes partes de software de forma sustentável.
Mas eles podem causar seus próprios problemas. Vamos considerar um caso específico de LINQ para SQL projetada para recuperar as 48 perguntas mais recentes do Stack Overflow.
var posts = (from p in DB.Posts where p.PostTypeId == PostTypeId.Question && p.DeletionDate == null && p.Score >= minscore orderby p.LastActivityDate descending select p). Take(maxposts);
O grande gancho aqui é que o esse é um código que o compilador realmente entende. O senhor obtém o recurso autocompletar código, erros do compilador se renomear um campo do banco de dados ou digitar incorretamente a sintaxe, e assim por diante. Talvez o melhor de tudo seja que o senhor obtém uma versão honesta do post
como saída! Assim, o senhor pode se virar e fazer imediatamente coisas como esta:
foreach (var post in posts.ToList()) { Render(post.Body); }
Muito legal, não é?
Bem, essa consulta Linq to SQL é funcionalmente equivalente a esse blob SQL antigo. Mais do que funcionalmente, ela é literalmente idênticose o senhor examinar a string SQL que o LINQ gera nos bastidores:
string query = "select top 48 * from Posts where PostTypeId = 1 and DeletionDate is null and Score >= -4 order by LastActivityDate desc";
Essa bolha de texto é, obviamente, totalmente opaca para o compilador. Se houver um erro de sintaxe aqui, o senhor não descobrirá até o tempo de execução. Mesmo que seja executado sem erros de tempo de execução, o processamento da saída da consulta é complicado. São necessárias referências no nível da linha e uma série de conversões de dados tediosas para obter os dados subjacentes.
var posts = DB.ExecuteQuery(query); foreach (var post in posts.ToList()); { Render(post["Body"].ToString()); }
Portanto, o LINQ to SQL é uma abstração – estamos abstraindo o SQL bruto e o acesso ao banco de dados em favor de construções e objetos da linguagem nativa. Eu diria que o Linq to SQL é um bom abstração. Caramba, é exatamente o que eu pedi há cinco anos.
Mas até mesmo uma boa abstração pode se desfazer de maneiras inesperadas.
Considere esta otimização, que é trivial no código SQL blob antigo: em vez de extrair todos os campos dos registros de postagem, por que não extrair apenas o número de identificação? Faz sentido, se é só isso que preciso. E é mais rápido, muito mais rápido!
selecione os 48 melhores * de Posts | 827 ms |
selecione os 48 melhores Id de Posts | 260 ms |
Selecionar todas as colunas com o operador estrela (*) é caro, e é isso que o LINQ to SQL sempre faz por padrão. Sim, o senhor pode especificar o carregamento preguiçoso, mas não por consulta. Normalmente, isso não é um problema, pois selecionar todas as colunas para consultas simples não é tudo que caro. E o senhor acha que puxar para baixo 48 míseros registros de postagens estaria diretamente na categoria “não é caro”!
Então, vamos comparar maçãs com maçãs. E se obtivéssemos apenas os números de identificação e depois recuperássemos os dados completos de cada linha?
select top 48 Id from Posts | 260 ms |
select * from Posts where Id = 12345 | 3 ms |
Agora, recuperar 48 registros individuais um a um é meio bobo, porque o senhor poderia facilmente construir um único where Id in (1,2,3..,47,48)
que pegaria todos os 48 posts de uma só vez. Mas mesmo que fizéssemos isso dessa forma ingênua, o tempo total de execução ainda seria bastante razoável (48 * 3 ms) + 260 ms = 404 ms. Ou seja metade do tempo do SQL select-star padrão emitido pelo LINQ to SQL!
Um extra de 400 milissegundos não parece muito, mas o páginas lentas perdem usuários. E por que o senhor faria uma consulta lenta ao banco de dados em cada página do seu site quando não é necessário?
É tentador culpar a Linq, mas será que a culpa é realmente da Linq? Isso parece ser idênticos para mim:
1. Forneça todas as colunas de dados das 48 principais postagens.
ou
1. O senhor pode me fornecer apenas os IDs das 48 principais postagens.
2. Recupere todas as colunas de dados para cada um desses 48 ids.
Então, por que no vasto, vasto mundo dos esportes um desses operações aparentemente idênticas ser duas vezes mais lenta que a outra?
O problema não é o Linq to SQL. O problema é que o estamos tentando criar uma abstração limpa e agradável sobre um banco de dados repleto de comportamentos altamente irregulares e incomuns no mundo real. Bancos de dados que:
- podem não ter os índices corretos
- pode interpretar mal sua consulta e gerar um plano de consulta ineficiente
- estiver tentando executar uma operação que não se encaixa bem na memória disponível
- estão paginando dados de discos que podem estar ocupados naquele momento específico
- podem conter tipos de dados de colunas de tamanho irregular
É isso que é tão frustrante. Não podemos simplesmente fingir que todos os nossos dados estão formatados em estruturas de dados organizadas e arrumadas na memória, alinhadas em pequenas filas convenientes para que possamos pegá-los casualmente. Como demonstrei, até mesmo as consultas triviais podem ter um comportamento bizarro e características de desempenho que não são nada claras.
Para seu crédito, o Linq to SQL é bastante flexível: podemos usar consultas com tipagem forte ou podemos usar consultas de blob SQL que convertemos para o tipo de objeto correto. Essa flexibilidade é fundamental, pois o grande parte do nosso desempenho depende dessas peculiaridades do banco de dados. Usamos como padrão as construções de linguagem Linq incorporadas e passamos a ajustar manualmente os antigos blobs SQL onde os rastreamentos de desempenho nos dizem que é necessário.
De qualquer forma, está claro que o senhor tem saber o que está acontecendo no banco de dados em cada etapa do processo para começar a entender o desempenho do seu aplicativo, muito menos para solucionar problemas.
Acho que o senhor poderia argumentar de forma bastante sólida que o Linq to SQL é, na verdade, uma abstração com vazamentos e falhas. Exatamente o tipo de coisa de que Joel estava reclamando. Mas eu também argumentaria que virtualmente todos boas abstrações de programação são abstrações fracassadas. Acho que nunca usei uma que não vazasse como uma peneira. Mas acho que essa é uma péssima arquitetura astronauta maneira de ver as coisas. Em vez disso, vamos nos fazer uma pergunta mais pragmática:
Essa abstração faz com que nosso código seja pelo menos um pouco mais fácil de escrever? Para entender? Para solucionar problemas? É melhor para nós com com essa abstração do que sem ela?
Nosso trabalho, como programadores modernos, não é abandonar as abstrações devido a essas deficiências, mas adotar os elementos úteis delas, adaptar as partes que funcionam e construir cada vez mais levemente menos abstrações com vazamentos e quebradas ao longo do tempo. Como cidadãos desesperados que estão trabalhando em um dique em uma tempestade de categoria 5, nós, programadores, continuamos a empilhar essas abstrações com vazamentos, nos apoiando da melhor forma possível, tentando desesperadamente ficar à frente das águas infinitamente crescentes da complexidade.
Por mais que eu amaldiçoe a Linq to SQL como mais uma abstração fracassada, continuarei a usá-la. Sim, talvez eu acabe ficando encharcado e irritado às vezes. Mas com certeza o heck é melhor que o afogamento.