Problema com uma tabela com muitos registros?

Por João Chagas em 22 de fevereiro de 2023

Neste artigo procuro discorrer algumas das possibilidades para resolver esse problema, partindo do princípio que iremos tentar resolver sem fazer uma grande mudança na infraestrutura atual do projeto, sem precisar remodelar tudo.

Partiremos da ideia que em um projeto hipotético temos uma tabela com diversos registros, diríamos que algo em torno de 100 mil registros.Foi percebido que as buscas nessa tabela se tornaram extremamente lentas. Quais são as possibilidades que temos para diminuir a latência dessa busca?

Abaixo segue uma lista das opções mais simples:

  • A primeira e mais óbvia opção, mas que às vezes esquecida, acredito que vale a pena lembrar da mesma, é a boa e velha indexação, isso pode salvar consideravelmente a performance das suas buscas. No decorrer deste post explicarei como funcionam estas buscas. É importante lembrar que um campo indexado tem em média uma complexidade de O (logN), isso se você está fazendo a pesquisa por apenas um campo, mas ainda que seja mais de um, esta busca será otimizada, porque a partir desta indexação, não se faz mais necessário percorrer por todas os registros da tabela. É importante lembrar que dependendo se o banco é colunar ou baseado em linhas, terá impacto nessa busca também. Além da indexação de campos únicos, pode ser feita a indexação de mais colunas, o que terá um impacto positivo se tiver pesquisas por campos compostos frequentemente.

  • Cachear as informações, utilizando Redis ou algum outro tipo de banco em memória. O redis é extremamente rápido em suas buscas tendo uma complexidade de O (1), é como se você soubesse exatamente onde está o dado que você está procurando na memória.

  • Outra opção mais simples do que as que vem a seguir seria, se por acaso, o banco está demorando por causa de uma congestão na carga, várias operações ocorrendo em paralelo, vários pools de conexão etc. Uma opção ainda menos trabalhosa seria utilizar uma arquitetura master-slave para o banco, para que possa ser distribuída essa carga e isso impactaria numa melhora na velocidade das buscas.

É importante lembrar que na arquitetura master-slave, as escritas ficam todas na master e depois vão para o banco slave, é importante salientar que, sim…nesse caso, pode acontecer uma consistência eventual mesmo em banco relacional, principalmente quando se trata de microsserviços.

4

A replicação pode acontecer da maneira síncrona ou assíncrona, mas imagine que foi feito no exato momento uma escrita para o banco master e ao mesmo tempo uma busca para um banco slave, em consequência, os resultados não estarão o mais preciso possível. Existe a possibilidade de ter mais de um banco master, mas não entraremos nesta questão neste post.

  • O que podemos fazer para melhorar ainda mais a performance nesse caso é o que chamamos de partitioning, mas o que é esse cara?

Para isso vou explicar um pouco mais a fundo como funciona a estrutura de um banco e como essa informação é armazenada. Diversos bancos guardam as informações em uma b-tree, segue abaixo a representação gráfica de uma b-tree:

O que é uma b-tree?

B-tree é basicamente mais uma estrutura de dados que têm como base ser uma estrutura de árvore, onde cada nó tem um filho, entretanto, como pode ver na representação abaixo, na b-tree, cada nó pode ter até dois filhos, isso é feito para otimizar a leitura e escrita dos discos rígidos (HDD/SSD). Mas ainda assim as buscas acontecem em tempo logarítmico, ou seja, dá para usar uma busca binária.

3

Como você pode ver, é bem parecida como uma tree normal, a única diferença é que a mesma pode armazenar mais de um nó pode ter dois filhos.

Sabendo disso, vou explicar como funciona o partitioning, para fins educativos, diríamos que é como se você tivesse um HashMap (Dictionary) contendo essas binary trees dentro, ou seja, você pega sua tabela que antes era bem grande e agrupa-as por uma chave, que é a partitioning key.

5

Isto é o que vai passar a ser sua tabela, ou seja, quando você fizer uma busca, vamos olhar onde está a key e diminuir consideravelmente a quantidade de registros que devemos procurar. Podemos definir a key como qualquer valor.

Alguns bancos já têm essa função disponível para ser utilizada, no postgres por exemplo, basta você ativar a mesma.

  • Okay, vamos prestar atenção na remodelagem do banco. Isso pode ter um grande impacto em todas as buscas. Por exemplo, vamos dizer que estamos construindo uma feature para quantas pessoas curtiram uma postagem sua em qualquer rede social. Teríamos as seguintes tabelas:

2

Perceba que com o tempo essas tabelas tendem a crescer bastante, modelando dessa maneira, imagine quanto tempo iria levar para contar a quantidade de posts de um usuário, talvez seria melhor adicionar um campo na tabela post com o likes_count, algo do tipo. Mas ainda assim, possa ser que eu precise saber quem foi que curtiu o post, nesse caso, eu teria que fazer uma busca em post_likes que teria diversos registros, talvez o que poderia ser feito, é adicionar uma nova coluna na tabela post, contendo todos os usuários que curtiram, isso já iria remover a necessidade de mais uma tabela e consequentemente iria aumentar a performance da busca, mas por que eu fiz isso?

Para mostrar que a modelagem de um banco, tem impacto direto na performance das queries também.

Imagine o problema para registrar seguidores e pessoas que você está seguindo, seria uma opção adicionar uma coluna JSON, contendo estas informações do que fazer uma busca em uma tabela com milhões de registros.

  • A última opção ainda nos modelos relacionais é utilizar sharding, mas o que é sharding ?

Sharding deve ser uma das suas últimas alternativas, porém vale ressaltar que utilizar esse tipo de solução é extremamente trabalhoso, você vai enfrentar alguns desafios relacionados à falta de flexibilidade e também com a garantia de consistência. Talvez nesse caso seria melhor cogitar um banco não relacional.

1

Como você pode ver acima, sharding consiste em pegar um banco e repartir ele em pedaços, podemos usar uma key, nesse caso escolhi o banco, criando shards baseados em um range de id.Utilizei também uma arquitetura master slave no banco para garantir disponibilidade. Esse tipo de solução tem alguns prós e contras.

Pros:

  • Vai aumentar a velocidade das suas operações, sejam elas de leitura ou escrita.

Contras:

  • Como você pode ver a complexidade de dar manutenção em um ambiente como esse aumenta consideravelmente.

  • Outro problema é que o número de shards serão fixos.

  • Um problema que pode ocorrer é um join entre shards que pode vir a ser feito e isso vai ser feito utilizando a rede, ou seja, é extremamente custoso fazer essas operações.

Okay, chegamos até aqui e nada disso resolveu, o que eu faço? Bom, nesse caso, poderíamos cogitar a troca de um modelo relacional para um não relacional, mas por que?

Os bancos relacionais te garantem constância, garante as uniques, chaves primárias, relacionamento entre tabelas, transações entre outros, tudo isso tem um custo para o banco gerenciar tudo isso, vamos dizer que você pode ter uma constância eventual, poderíamos trocar este banco para um mongodb por exemplo, onde impactará numa pesquisa mais rápida, dependendo do que você está fazendo. Por exemplo, se você usar hashing no mongodb, esta busca terá uma complexidade de O(1) para cada registro buscado. Mas o ponto que quero salientar aqui é, Mongodb pode ser uma boa devido a sua flexibilidade.

// Livros recomendados relacionados ao assunto do post

// Compartilhe esse Post