Conforme escrevi na introdução do texto sobre formas normais, bancos de dados relacionais já foram dominantes na minha vida profissional.
Nessa época, tínhamos a tendência de achar que os bancos relacionais parariam o motor da História do armazenamento de dados. Isso acontecia em parte porque trabalhávamos com ERP e o cachimbo entorta a boca, e em parte porque bancos SQL eram o hype da época.
Toda e qualquer necessidade de ingestão, armazenamento e consulta deveriam caber no paradigma relacional. Se não coubessem, era culpa do desenvolvedor; que projetasse melhor o banco de dados, aplicasse mais formas normais, até atingir o sucesso.
Porém, nem sempre o modelo relacional é a resposta. Muitas vezes, há um descasamento de impedância. Às vezes, é simplesmente impossível usar um banco relacional.
Já tentei, por exemplo, há alguns anos atrás, armazenar dados estilo "time series" num MySQL da vida. No início, parece que funciona. Em pouco tempo, fica lento; e não é imediatamente óbvio por quê.
Bancos de dados relacionais, e até mesmo as formas normais que guiam o projeto do banco de dados, presumem que haverá muitas consultas, e relativamente poucas inserções ou alterações.
Se o caso de uso tiver o perfil oposto, usar um banco de dados relacional ainda pode funcionar, mas vai trabalhar forçado, num ambiente que não é o seu, até ficar impossivelmente lento.
O banco relacional preza pela exatidão e durabilidade dos dados. Para aplicações "caretas" como ERPs, isto é bom. Um banco que preservasse apenas 99,9% dos cadastros de clientes não seria aceitável. Neste caso de uso, precisamos de 100,0%.
Mas essa garantia de 100% tem um custo recorrente, que nem sempre precisaria ser pago. Em alguns casos de uso, a durabilidade e exatidão podem ser flexibilizadas em nome da performance, em particular da performance de escrita.
Se um sensor de temperatura gera 1 bilhão de amostras por mês, dificilmente você precisa consultar uma amostra em particular. Se for uma amostra velha, mais improvável ainda. A média da temperatura por minuto ou por hora é suficiente. Perder 0,1% das amostras de forma aleatória não seria um problema.
Bancos relacionais presumem dados bem-comportados, enquadráveis em tabelas, linhas e colunas. Mas nem sempre os dados são bem-comportados. Documentos JSON, XML, logs, textos, não cabem no paradigma relacional. O esquema interno desses documentos pode mudar o tempo todo, em particular quando são gerados por terceiros.
Mesmo sendo mal-comportados, ainda teremos de lidar com esses tipos de dados. Para isto, vamos precisar de ferramentas que bancos relacionais comuns não oferecem, por exemplo: busca por palavras soltas, busca por padronagem JSON ou XML (usando linguagens como JSONPath ou XPath), etc.
Bancos relacionais não combinam com "Big Data". Big Data é a disciplina que lida com volumes de dados que tendem ao infinito, o que exige técnicas especiais.
Se o volume de dados tende ao infinito, isto significa automaticamente que haverá muito mais escritas que consultas — que, como dito há pouco, vai de encontro à filosofia do banco relacional.
Um volume de dados infinitamente grande também significa que não faz sentido recuperar linhas ou registros individuais. Sempre vamos consultar o banco usando alguma modalidade de agregação: soma, média, mediana, quantis, p90, p50, etc. (Não sabe o que é mediana, p50, p90? Confira no final deste texto.)
Correlato ao conceito de "Big Data", temos o "data lake", ou almoxarifado de dados. Numa grande organização, é comum acumular enormes quantidades de dados, vindos de inúmeras fontes, muitas vezes de terceiros, que serão utilizadas em consultas, relatórios e estatísticas num segundo momento.
Num mundo ideal, esses dados seriam consultados diretamente das fontes quando fosse necessário. Mas isto é impraticável: as fontes de dados podem não estar disponíveis 100% do tempo, as fontes de dados não aceitam consultas "miúdas" ou demoram muito para responder a cada consulta. A solução é transferir periodicamente um lote de dados para o "data lake" local.
Podemos usar um banco de dados relacional como "data lake"? Talvez sim, mas como dito antes o banco relacional é otimizado para ingestão esporádica e incremental de dados, não para receber um enorme lote periódico. O ideal seria desacoplar ingestão de consulta.
Os bancos de dados não-relacionais são conhecidos pela sigla "NoSQL", cujo significado original é óbvio: "nada de SQL". Ultimamente se diz que NoSQL significa "Not Only SQL"...
O fato é que muitos devs têm má vontade com SQL e bancos de dados relacionais, e se empolgaram bastante com o NoSQL. A modinha era abandonar o bom e velho LAMP (Linux, Apache, MySQL, PHP) por MEAN (MongoDB, Express, Angular, Node.js), sem realmente considerar se era uma boa ideia.
Suspeito que a má vontade era mais com o SQL enquanto linguagem de consulta do que com o fato do banco relacional ser relacional. Vide a popularidade dos ORMs. O que é irônico, porque muitos bancos NoSQL entendem algum dialeto de SQL. Muitos tentaram oferecer linguagens de consulta especiais, em tese mais otimizadas, mas que nunca "pegaram".
A reinterpretação da sigla NoSQL como "não apenas SQL" faz um certo sentido porque, por um lado, os bancos NoSQL reabilitaram o SQL, e por outro lado os bancos relacionais (em particular o PostgreSQL) muniram-se de recursos típicos de NoSQL, borrando a linha demarcatória.
É meio covarde dizer que "NoSQL é melhor que SQL", pois você está contrapondo o modelo relacional com potencialmente infinitos outros modelos. Não sou muito fã da sigla "NoSQL" justamente por isso: porque ela joga no mesmo balaio uma enorme variedade de bancos de dados. Cada tipo de banco de dados, cada modelo, é uma ciência à parte e merece ser estudada em separado.
Em tese, são infinitos. Mas no momento eles são agrupados em cinco tipos:
Bancos de dados key-value ou KV estão para bancos relacionais assim como dragsters estão para automóveis comuns. Eles são otimizados para ler ou escrever dados associados a uma chave. Nada mais.
Usar um banco KV é basicamente o mesmo que usar um dicionário do Python ou array associativa do Javascript. Não raro a interface de acesso é idêntica a de um dicionário, o que a faz muito ergonômica e familiar. Nada de SQL, nada de ORM!
Para o banco KV, o conteúdo ou significado do valor é opaco e irrelevante. Em geral, o banco KV é destituído de todos os recursos típicos de um banco relacional: não há linguagem de consulta, não há como procurar dados pelo conteúdo do valor. Pode ou não haver o recurso de navegar chaves segundo uma ordem.
A simplicidade do KV entrega performance máxima e escalabilidade infinita naquilo que ele faz bem.
A Amazon AWS oferece o famoso DynamoDB. No mundo open-source, o Redis e o etcd são bem conhecidos. Também é digno de nota o DBM, que antes do SQLite era a forma predileta de um programa armazenar dados locais.
Um subtipo importante do banco KV é o banco em memória, caso do Redis. Neste tipo de banco, a RAM do servidor tem de ser suficientemente grande para conter todos os dados. Pode haver, se necessário, persistência em disco para que os dados sobrevivam a um reboot.
Este tipo de banco é largamente usado como cache, bem como para armazenar dados cujo acesso tenha de ser instantâneo.
O banco de dados orientado a documento é superficialmente semelhante ao KV: ele armazena valores associados a chaves de busca. Porém, aqui o valor não é mais opaco; ele é um documento com formato conhecido, que o banco sabe interpretar.
O "documento" é quase sempre no formato JSON ou XML. O banco permite consultas que busquem por atributos dentro dos documentos. Também permite criar índices com base nesses atributos para acelerar as buscas. Permite ainda estabelecer "schemas", ou seja, impor que os documentos sigam uma determinada estrutura.
A grande vantagem do banco de dados orientado a documentos é sua flexibilidade. O engenheiro escolhe quão rígido ou quão frouxo o "schema" deve ser. O banco acomoda "schemas" que se modificam com o tempo.
Também é muito mais natural armazenar valores compostos, como arrays, vetores, matrizes, dicionários, blobs, etc. num banco de documentos que num banco relacional. (Embora hoje em dia todo banco de dados relacional possui suporte a colunas JSON.) É muito simples serializar um objeto para um documento e vice-versa.
O MongoDB é de longe o mais famoso produto desta classe. Foi com ele que os devs acharam que iam escantear os bancos relacionais de uma vez por todas. A linguagem de consulta do MongoDB não é SQL, é um schema JSON.
Na teoria, um banco de documentos pode fazer tudo que um banco relacional faz. Na prática, redescobrimos que a disciplina imposta pelo modelo relacional é benéfica no longo prazo, e que a linguagem SQL faz falta. Sob qualquer ótica, fazer um JOIN com SQL é melhor que usar $lookup.
O banco de documentos brilha quando os dados simplesmente não se encaixam no modelo relacional e.g. documentos com estrutura interna altamente variável. Ou como base de projetos em fase de protótipo, onde o projeto de banco de dados ainda não está sacramentado.
Pessoalmente, não sou fã de MongoDB e assemelhados. Tentaria antes usar os recursos de documentos do PostgreSQL, para ter o melhor de dois mundos.
Bancos de dados time-series ou TSDB são otimizados para armazenar dados temporais, cujo índice primário é o tempo. Por exemplo, dados de observabilidade: métricas, sensores, eventos, etc.
Num banco TSDB típico, cada amostra consiste de uma chave que identifica a métrica, um timestamp, e um conjunto variável de tags (rótulos) que qualificam a amostra. Por exemplo, se há 3 sensores de temperatura num ambiente, a chave poderia identificar o ambiente e um tag poderia identificar o sensor individual.
A principal força do banco TSDB é a capacidade de agregação. Dificilmente os clientes do banco estão interessados em amostras individuais, e muito menos em amostras individuais antigas. As consultas quase sempre envolverão agregação: soma, média, p50, p90, max, min, etc.
Por exemplo, se você pede um gráfico mensal de temperatura, cada ponto do gráfico vai ter uma "largura" de quase 1h, e você provavelmente espera que esse ponto seja a média de temperatura do intervalo que ele representa. Já num gráfico de uso de CPU, você provavelmente deseja que cada ponto seja o máximo do intervalo, supondo que o objetivo do gráfico é denunciar picos de uso.
Isto quer dizer que o banco TSDB deve ser capaz de calcular essas agregações rapidamente, ou já ter calculado em segundo plano.
É um dos motivos que o banco relacional sofre com time series. Você pode fazer consultas com agregação (COUNT, SUM, AVG, MAX), mas o banco relacional calcula tudo na hora, visitando milhares ou milhões de linhas. Vai demorar e/ou exigir muitos recursos de máquina.
Além de calcular agregações em segundo plano, o banco TSDB pode "comprimir" amostras conforme envelhecem, ou seja, descartar amostras e mesmo agregações miúdas em favor de agregações mais esparsas. Até mesmo aproximações podem ser utilizadas (e.g. para percentis) quando o valor 100% exato custaria muito caro para ser determinado.
Em muitos sistemas TSDB, utiliza-se UDP para reportar métricas, pois a perda esporádica de uma amostra é tolerável em muitos casos de uso. É outra diferença crucial em relação a bancos relacionais, que se esmeram em ingerir e armazenar cada amostra de forma igualmente durável.
Um banco de dados relacional tende a ser "orientado a linha". Isto significa que, no armazenamento de massa (disco ou arquivo), os dados são tradicionalmente gravados uma linha por vez. Todas as células daquela linha estão adjacentes. Isto facilita inserções de novas linhas, bem como UPDATEs posteriores. (A história é bem mais complicada que isto num banco relacional moderno, mas desconsidere isto por um instante.)
Num banco de dados orientado a coluna, também chamado de wide-column, os dados são gravados uma coluna por vez. Isto significa que as células de uma determinada linha estão espalhadas. Neste formato, consultar os dados de uma coluna é muito mais rápido. Porém a inserção de novas linhas é difícil ou impossível. Este formato é ideal para dados produzidos em lotes fechados que nunca mudam.
Essa questão do formato de armazenamento no disco é mais histórica que real. Na prática, o que distingue o banco wide-column é que ele é apenas-leitura. A troco disto, ele é extremamente rápido em realizar consultas sobre enormes volumes de dados.
Um banco wide-column bem conhecido é o Apache Hive. Os lotes de dados são tipicamente arquivos "Parquet", gerados por qualquer programa externo (não pelo Hive) e armazenados em discos locais ou na Amazon S3. É comum usar Python Pandas para gerar esses arquivos, pois o Pandas oferece suporte nativo à geração de parquets.
Os arquivos parquet são lotes de dados completamente autocontidos: possuem dados, agregações, métricas, índices... em resumo, tudo que o Hive precisa para executar rapidamente qualquer consulta sobre esses dados.
Esse tipo de banco de dados, e o Hive em particular, é muito popular em "data lakes". Armazenar uma coleção de arquivos parquet é muitíssimo mais fácil do que manter rodando um banco de dados relacional.
Um banco orientado a grafos é otimizado para dados cujos relacionamentos sejam mais bem representados por grafos. Um exemplo muito citado é o seu perfil do Facebook. Você é um nó, as arestas são suas relações de amizade, e a totalidade das relações de amizade entre todos os perfis do Facebook forma um enorme e intrincado grafo. Outro exemplo canônico é a coleção de dados geográricos, com localidades (nós) e suas distâncias até as localidades adjacentes (arestas com atributos).
É perfeitamente possível expressar um grafo usando um banco de dados relacional. E isso é feito regularmente: a relação do cliente com seus pedidos é um grafo — embora simples, hierárquico e sem ciclos fechados. Funcionários são relacionados a seus chefes, que também são funcionários, usando relacionamento reflexivo. Não é preciso usar um banco de dados NoSQL para casos tão simples.
O problema começa quando o grafo é grande, cíclico, e desejamos navegá-lo usando algoritmos feitos para grafos. Numa representação relacional, precisaríamos de uma consulta SQL para explorar cada aresta (ou pelo menos cada "n" arestas). Provavelmente ficaria lento bem depressa, e há um descasamento de impedância. Fora o leva-e-traz de dados. Num banco de dados orientado a grafo, o algoritmo roda no próprio servidor.
Nesta classe, o banco mais conhecido é o Neo4j. Mas diversos bancos relacionais possuem suporte a grafos via plug-ins, inclusive o PostgreSQL (sempre ele...).
Lembra quando diziam que a programação orientada a objetos iria acabar com a fome e a miséria do mundo? Desta época também são os OODBs. Eram considerados a "próxima grande coisa" lá pela virada do milênio.
A principal característica dos OODBs é eliminar o "descasamento de impedância" entre código e dados. Um objeto no seu programa, que seja marcado como persistente (e.g. por herança de uma classe abstrata) está automaticamente no banco de dados. Aquilo que os ORMs simulam, o OODB simplesmente "é".
No mundo open-source, um nome bem conhecido é o ZODB (Zope Database), que possui muitos recursos tentadores.
Um problema dos OODBs é o vínculo com a linguagem de programação. Classes e objetos significam coisas muito diferentes em C++, Java, Python ou Smalltalk. É difícil conceber um OODB neutro em relação à linguagem de programação. Por exemplo, o ZODB foi feito para ser usado apenas com Python.
O problema fica mais evidente se desejarmos invocar métodos dos objetos contidos no banco. Onde este método vai rodar? No servidor ou no cliente? Em que linguagens de programação esse método poderia ser implementado? O código vai ser armazenado no banco? Como implementar garantias ACID nesta situação? Num OODB distribuído, o método poderia rodar em qualquer servidor?
A ideia é incrível, mas ela carreia uma lista enorme de perguntas e detalhes de implementação.
Apesar de ainda existirem em alguns nichos, de maneira geral os OODBs foram sucedidos pelos bancos orientados a documentos, que entregam muitas características desejáveis dos OODBs, mantendo a neutralidade em relação à linguagem de programação.
LSM não é exatamente um tipo de banco de dados, é uma técnica empregada em Big Data para ingestão de grandes volumes de dados de forma eficiente. O LSM é usado como base de diversos bancos de dados NoSQL.
Estou citando o LSM por conta dos logs. Logs ou registros de acesso são um problema difícil de tratar em grandes sistemas online. É um "bando de dados", disforme, volumoso, quase intratável, que não pode ser ignorado.
Imagine um Facebook da vida, com bilhões de acessos por dia. Cada log de acesso precisa ser armazenado, incluindo todos os dados auxiliares do acesso. Não precisa armazenar para sempre, mas por tempo suficiente para eventual debugging ou auditoria. Imagine o custo de ingerir, armazenar e permitir consultas a tudo isso.
O formato interno do LSM é key-value, mas no caso de logs é necessário disponibilizar procura por fragmentos do conteúdo, inclusive palavras soltas. Para viabilizar esse tipo de consulta, produtos como Apache Lucene ou AWS ElasticSearch usam o chamado "índice invertido". Funciona de forma análoga a um índice remissivo, embora seja muito mais sofisticado, aceitando buscas por expressões regulares, matches parciais e faixas.
Bancos de dados relacionais procuram abstrair a questão do armazenamento. Você não precisa tomar conhecimento onde estão os arquivos no disco que sustentam a coisa toda. Até preferem que você não saiba, para você não tentar algo estúpido — como tentar fazer backup do banco de dados copiando esses arquivos. (Um backup feito desse jeito provavelmente estará corrompido.)
Lidar com bancos wide-column no trabalho tem um retrogosto de nostalgia, porque com eles é o contrário: meu código gera um arquivo parquet e joga-o numa pasta do sistema de arquivos (AWS S3). As partições do banco são sufixos dos próprios nomes dos arquivos. O que era escondido no banco relacional, aqui é explícito, escrachado até.
Como dito antes, o parquet é autosuficiente: contém dados, índices, estatísticas e agregações. No "meu tempo", no tempo do COBOL e dBase/Clipper, os "bancos de dados" usavam alguma variante do esquema ISAM.
A ideia do ISAM é simples: para cada tabela, existe um arquivo de dados. Esse arquivo contém apenas dados, gravados linha a linha. As linhas têm tamanho fixo. Novas linhas sempre estendem o arquivo. Deleções apenas marcam a linha como apagada, mas ela fica lá. É um formato surpreendentemente eficiente em acesso sequencial.
No esquema ISAM, os índices vão em arquivos separados. Em geral, numa queda de luz ou falha de hardware, apenas os arquivos de índice eram corrompidos, mas podiam ser facilmente reconstruídos com base no arquivo de dados. Podia ser um arquivo para todos os índices da tabela, ou um arquivo por índice, isso depende da variante. A tecnologia de índice pode ser trocada sem afetar o arquivo de dados original.
O formato DBF do dBase possui um cabeçalho contendo o schema da tabela, tornando-o autossuficiente, sem depender de código para ser legível. Os índices existiam em diversos formatos: NDX (dBase III), MDX (dBase IV), NTX (Clipper) e CDX (FoxPro, mas também popular entre Clippeiros).
Nos anos 1970, ISAM era considerado algo moderno, um passo firme na direção do banco de dados relacional. Antes disso, na era do mainframe, havia os bancos de dados hierárquico e de rede.
Primeiro, é preciso esclarecer que mainframes não têm arquivos tais como os conhecemos. Num mainframe, você tem áreas de disco (datasets) designadas para determinado uso. É como se fosse um único arquivão de tamanho fixo.
(Isso é até onde eu ouvi falar. Nunca trabalhei com mainframe, não sou tão velho assim. E ainda houve uma era anterior, onde os mainframes não tinham discos, apenas fitas e cartões perfurados, onde todo o processamento de dados era por lote.)
Os bancos de dados de época (hierárquico e de rede) não têm o conceito de tabela. Todos os registros de todos os tipos convivem no mesmo dataset, formando uma "sopa de dados". Os relacionamentos entre registros são literalmente ponteiros, que indicam em que posição do disco está o registro apontado. É meio parecido com uma coleção de estruturas em RAM, com ponteiros entre si.
Num banco de dados hierárquico, os relacionamentos têm natureza hierárquica e unidirecional. Cada registro só pode ser apontado por um "pai", e só pode apontar para "filhos" ou para "irmãos", formando uma lista encadeada. O grafo formado pelos registros e ponteiros será uma árvore invertida, que só pode ser navegada de cima para baixo, ou para os lados.
O esquema tem a vantagem óbvia de ser simples e eficiente, e a desvantagem óbvia de ser muito rígido e difícil de modificar.
Já num banco de dados baseado em rede, os relacionamentos não precisam ser hierárquicos. Um registro pode apontar e ser apontado por vários outros. O grafo formado pelos registros pode ter qualquer formato e pode conter ciclos. Uma vez que ele permite expressar relacionamentos mais livremente, é considerado o precursor do banco de dados relacional.
Ainda existe uma certa rigidez nos relacionamentos — afinal de contas, eles têm de ser previstos e expressos como ponteiros em disco, enquanto num banco relacional o relacionamento só é especificado ad-hoc na hora da consulta SQL. Ms é um avanço considerável em relação ao modelo hierárquico. E é claramente semelhante ao moderno banco NoSQL de grafo.
Apesar de influente, nunca chegou a ser o mais utilizado; a maioria dos usuários ficou com o modelo hierárquico até migrar diretamente para o modelo relacional.
Em big tech, utiliza-se muito agregadores na forma de percentil: p50 (50%), p90 (90%), p99 (99%), etc. Para explicá-los, vou começar com o conceito de mediana (p50).
Todo mundo sabe o que é média. Numa distribuição normal, a média particiona a população exatamente no meio: metade está abaixo da média, metade acima.
Porém, muitas métricas não seguem a distribuição normal. Por exemplo, uma distribuição de salários tem uma cauda longa. O fato dos juízes ganharem penduricalhos faz a média salarial subir, mas isto não significa necessariamente uma massa salarial saudável.
Para salários, um agregador melhor é a mediana. Colocamos os salários em ordem crescente e pegamos aquele que está exatamente no meio da lista. Este valor representa muito melhor o salário do povo em geral. A mediana também é conhecida como p50.
Estatísticos da velha guarda falam de quartis. Os quartis dividem a população em quatro partes e obtém a métrica (e.g. salário) a cada 25%. Tais agregadores também são conhecidos como p0, p25, p50, p75 e p100. Note que p0 seria o menor valor da métrica, sendo equivalente ao agregador MIN. Já p100 é o maior valor, equivalente a MAX.
Outra expressão da velha guarda é "decis", semelhante aos quartis, porém dividem a população em dez partes. Hoje em dia é mais comum falar-se em quantis e percentis, que generalizam o conceito.
Em métricas de latência, uso de RAM, uso de CPU, tempo de resposta do suporte etc. é comum utilizar percentis como p90, p95 ou p99. Por exemplo uma latência p99 de 300ms significa que, em 99% dos casos, a latência está abaixo de 300ms. O mérito destes percentis é excluir amostras excepcionalmente ruins, que são raras mas existem, e que distorcem os agregadores "comuns": média, desvio-padrão e MAX (p100).
Um banco de dados TSDB descarta amostras velhas e retém apenas os agregadores. Isso é trivial fazer com média, mínimo e máximo; mas os percentis são um desafio. Se não for necessário que o percentil seja exato até o último decimal (geralmente não é) existem algoritmos como t-Digest e uddsketch que geram boas aproximações.