Lógica de programação: Otimizando fluxos e estruturas

Lucas Silva de Freitas
luizalabs
Published in
5 min readAug 2, 2023

Um dos principais problemas que ocorrem em softwares quando são enviados para ambientes de produção não são causados de imediato e sim coisas que só são vistas ao longo do tempo pois o código foi escrito de uma forma não otimizada. Quanto mais o tempo passa, mais a aplicação fica lenta e onerosa para o servidor.

Quando isso acontece, o primeiro pensamento de muitos é “É uma app legado, precisa ser reescrita”, porém, isso pode ser evitado apenas tomando alguns cuidados básicos.

Vamos a alguns exemplos clássicos que podem passar despercebidos durante a escrita de uma aplicação e que ao longo do tempo tem o potencial de degradar o serviço absurdamente.

Chamadas ao banco de dados dentro de loops

public void SaveAll(Person[] people)
{
foreach (Person person in people)
{
long? cityId = _context.Cities.FirstOrDefault(x => x.Name.ToLower() == person.CityName.ToLower())?.Id;

person.CityId = cityId;
_context.People.Add(person);
_context.SaveChanges();
}
}

No exemplo acima temos 2 erros muito comuns:

1 — Chamada de uma consulta de banco de dados dentro do loop
2 — Persistência e commit dentro do loop

Em cenários onde a lista people é pequena (o que é o cenário padrão em um ambiente de desenvolvimento) o código vai rodar bem. O problema começa a ocorrer conforme a lista cresce.

Vamos calcular com números.

Hipoteticamente, imagine que o filtro da tabela de cidades demore 200ms para executar (entre buscar uma conexão no pool, buscar na tabela e carregar o resultado).

Agora, imagine que a tabela de pessoas já tenha, por exemplo, 20 milhões de linhas. Cada chamada ao _context.SaveChanges() vai realizar um commit nessa tabela, vamos imaginar que isso demore por exemplo, 500ms.

No cenário acima, temos um total de 700ms por registro persistido. Em uma lista com 20 clientes, levaria um total de 14000ms ou 14 segundos.
Em um cenário de desenvolvimento, isso pode passar despercebido, mas conforme a lista cresce, nosso problema exponencia. Imagine a mesma lista agora com 1000 registros ? 700.000ms, quase 12 minutos.

Vamos agora a melhor maneira de realizar o exemplo acima. (Exemplo desconsiderando recursos específicos da linguagem!)

public void SaveAll(Person[] people)
{
var allCityNames = people.Select(s => s.CityName.ToLower()).ToArray();
var cities = _context.Cities.Where(x => allCityNames.Contains(x.Name.ToLower())).ToList();

foreach (Person person in people)
{
var cityId = cities.FirstOrDefault(x => x.Name.ToLower() == person.CityName.ToLower())?.Id;

person.CityId = cityId;
_context.People.Add(person);
}

_context.SaveChanges();
}

1 — Primeiro, geramos um array com todos os nomes de cidade possíveis
2 — Em seguida, em uma única conexão ao banco de dados, carregamos todas as cidades possíveis ( que, em teoria, nunca será maior que 200…300 registros )
3 — Dentro do loop, utilizamos agora essa lista de memória, evitando assim conexões ao banco
4 — Movemos o _context.SaveChanges() para fora, garantindo assim que o commit só ocorra no final do processo ( e persistindo todos atrás de uma única transação )

No nosso cenário hipotético, a busca por todas as cidades deve performar no mesmo tempo (considerando a existência de um índice), levando em média 200ms.

Ao mover o SaveChangespara fora, o codigo no loop não vai realizar vários commits, deixando apenas para o final. Vamos considerar que o commit ao final demore em média 2 segundos.

Tendo essas várias em mente, nosso código passou de 12 minutos em uma lista com 1000 registros para um menos de 3 segundos, pois a execução do loop vai ser basicamente instantânea;

Loops dentro de loops ou loops desnecessários

Esse é um cenário que é muito comum ocorrer, principalmente com iniciantes na programação. As vezes a falta de conhecimento em outras formas ou estruturas de dados nos fazem realizar algumas tarefas usando listas e loops, o que é um grande ofensor da performance das apps.

Quando se utiliza um loop dentro de outro, o cenário exponencial multiplica o número de execuções de acordo com o conteúdo das listas.

Quando se utiliza loops sequências, que poderiam ter sido realizados dentro de um único, estamos gastando muito mais processamento do que seria realmente necessário.
Através do Big-O conseguimos ter uma visão clara do crescimento dessa complexidade

Bem como o custo operacional de acordo com o tipo de estrutura de dados que decidimos utilizar

Agora, um exemplo de loops sequênciais e nested.

public decimal GetTotal(Order[] orders)
{
foreach (var order in orders)
{
foreach(var item in order.Items)
{
order.Total += item.Value * item.Quantity;
}
}

decimal total= 0;

foreach (var order in orders)
{
total += order.Total;
}

return total;
}

No exemplo acima temos um loop sequencial desnecessário. No primeiro loop nós passamos por todos os pedidos e, dentro dele, por todos os items daquele pedido, somando o valor do item ao valor total do pedido. Na sequência, passamos novamente por todos os pedidos, totalizando os valores em uma variável e retornando para a chamada de nosso método.

Por mais que pareça claro o erro no exemplo acima, muitas vezes o mesmo passa despercebido por ter um pouco mais de código entre os dois loops.

Para resolver o problema, poderíamos melhorar nossa lógica já realizando a totalização dentro do primeiro loop e removendo o segundo totalmente.

public decimal GetTotal(Order[] orders)
{
decimal total = 0;

foreach (var order in orders)
{
foreach(var item in order.Items)
{
order.Total += item.Value * item.Quantity;
}

total += order.Total;
}

return orderTotal;
}

Dessa forma, após ter o total do pedido, já somamos ele na nossa variável e tendo ela disponível.

Com um pouco mais de refactor, poderíamos inclusive evitar os 2 loops, caso nossa propriedade order.Total já tivesse o valor populado no momento de sua criação.

public decimal GetTotal(Order[] orders)
{
decimal total = 0;

foreach (var order in orders)
{
total += order.Total;
}

return orderTotal;
}

Estrutura de dados desproporcional

Outro erro comum é criar estruturas de dados desnecessárias, principalmente quando olhamos para o banco de dados. Colunas do tipo string/varchar sem limitação de caracteres ou desproporcional a finalidade podem levar a uma lentidão absurda conforme a sua app/db cresce.

PostalCode field with 200 length

No exemplo acima temos um campo PostalCode com um tamanho máximo possivel de 200, sendo que na prática esse campo nunca vai ser maior que 10 caracteres. A cada registro, temos mais de 190 bytes de desperdício.

Conclusão

Sempre avalie e revise sua lógica para garantir que está realizando os fluxos da maneira mais otimizada possível. Avalie e pegue exemplos dos dados a serem utilizados e armazenados, evitando assim alocações desnecessárias e consumos de recursos excessivos.

--

--

Lucas Silva de Freitas
luizalabs
Writer for

CEO @ Dreamer Studios & Tech Leader @ Luizalabs, follow me for more content about development! https://www.instagram.com/lucasfreitas.live/