Arquivo da categoria: Bancos de Dados

Formatos dos Dados

Este assunto não é novidade, mas ainda é bastante incompreendido. Seja em um banco de dados, ou em arquivos (Big Data), podemos escolher basicamente dois tipos de formatos de dados: organizado por linhas, ou por colunas.

A organização por linhas é mais conhecida. Ela é principalmente utilizada em sistemas de informação transacionais: esses que as empresas usam no dia-a-dia. A orientação dos dados por linha é otimizada para esse tipo de sistema, porém, para ambientes analíticos, onde realizamos consultas com grandes agregações, esse formato não é realmente eficiente.

A organização dos dados por colunas é diametralmente o oposto: terrível para sistemas transacionais, e excelente para ambientes analíticos (data warehouse, data lakes e data lakehouses).

Neste post vou explicar por que e onde um tipo de formato é melhor que o outro.

Organização por Linha

Primeiro temos que entender quais são as características de um sistema transacional (OLTP) no contexto de um banco de dados.

Geralmente esses sistemas são caracterizados por requisições curtas, e com grande volume de acesso concorrente. Exemplos de requisições curtas:

  • Quais são os dados do cliente 1002;
  • Insira um produto novo na tabela;
  • Atualize a quantidade deste produto no estoque;
  • Me dê todas as informações do funcionário 8567.

Ou seja, requisições curtas são operações realizadas no banco de dados que envolvem um ou poucos registros por vez, seja para consulta, ou alteração, e em grande volume (muitos usuários concorrentes).

Outra característica importante é que praticamente todas as requisições nesse contexto utilizam (ou, deveriam utilizar) a chave primária (índice) da tabela, isto é, sempre estamos filtrando por um cliente, por um produto, por um funcionário…

Normalmente as tabelas em um sistema transacional são normalizadas (3FN), ou seja, os dados são espalhados em várias tabelas diferentes (conectados por chaves primárias e estrangeiras) por questões de integridade, e para evitar redundância de informação.

Dito isto, vamos para o formato de linha com um exemplo simples:

Nesta tabela temos 6 linhas, e 5 colunas, mas pense que em um banco de dados de verdade temos milhões ou bilhões de linhas. Fisicamente essas linhas são armazenadas, uma após a outra, em blocos, e cada bloco pode armazenar uma centena de linhas.

Neste exemplo, suponha que cada bloco consegue armazenar 2 linhas:

Esse formato de linha é extremamente eficiente sempre que você faz uma busca em SQL como esta abaixo (típica em ambientes transacionais):

SELECT * FROM CLIENTES WHERE ID = 103

O banco de dados, usando o índice sobre a coluna ID, vai identificar que o bloco 2 possui a linha 103. Esse bloco será colocado em memória (se já não estiver lá), e o bloco será literalmente “varrido” até chegar na linha 103. Neste caso, todas as colunas desta linha serão retornadas na consulta.

Por que esse formato de linha é extremamente eficiente para consultas como essa que mostrei acima?

  • Não importa a quantidade de linhas, com o índice e a estrutura de blocos, o tempo de busca sempre será consistente e próximo do ótimo;
  • O tempo de resposta será geralmente o mesmo não importa se você consultar uma, duas, três ou todas as colunas da tabela: uma vez identificada a linha, todas as colunas desta linha estão fisicamente juntas;
  • Como as linhas estão fisicamente dispostas em centenas ou milhares de blocos diferentes, mesmo com muitos usuários concorrentes é razoável observar que haverá pouca concorrência em cada bloco, logo, esse formato é capaz de suportar uma grande quantidade de usuários simultâneos.

Agora vamos para um outro exemplo onde esse formato de linha não é muito bom. Suponha que fiz a seguinte consulta em SQL:

SELECT CIDADE, COUNT(*) as TOTAL
FROM CLIENTES
WHERE PAIS = 'BRASIL'
GROUP BY CIDADE

Esta consulta funciona com o formato de linhas, mas não é realmente eficiente. Em outras palavras, dependendo de alguns fatores, o desempenho desta consulta pode ser desastroso:

  • Se a quantidade de linhas na tabela for muito grande, a coluna PAÍS poderá não filtrar (restringir) muitos dados, e o banco de dados terá que processar uma grande quantidade de linhas para fazer o COUNT;
  • Se a quantidade de colunas na tabela for muito grande muitas delas serão “varridas”, mas apenas duas serão úteis para o resultado: neste exemplo CIDADE e PAIS;
  • A consulta poderá até retornar poucas linhas, mas terá que ler muitas linhas para compor o resultado, e por isso se houver muitos usuários concorrentes, todos lendo muitas linhas ao mesmo tempo, com certeza todos serão impactados no tempo de resposta (pela concorrência que um usuário gerará sobre os demais usuários);
  • A compressão dos dados não é boa, porque dentro de um mesmo bloco uma linha tem várias colunas de vários tipos (pouca repetição dos dados). Algoritmos de compressão são eficientes quando há bastante repetição dos dados dentro do mesmo bloco.

Nota: geralmente os algoritmos de compressão consideram um bloco de dados como “limite” dos dados a serem usados para compressão, de forma que os valores repetidos em um mesmo bloco seja substituído por ponteiros, economizando espaço.

Em outras palavras, quanto mais restritivas são as consultas (usando filtros pelo índice da chave primária, por exemplo), melhor será o formato de dados orientado por linhas. Quanto menos restritivas, ou seja, quanto maior o volume de dados movimentado pela consulta, pior será este formato.

Organização por Coluna

Vamos entender primeiro quais são as características de um sistema que possui um perfil mais analítico.

Geralmente esse perfil se caracteriza por requisições mais complexas, consultas que agregam muitos dados (relatórios), e com cargas de dados de alto volume, porém, possuem menor concorrência de usuários comparado com os sistemas transacionais.

Para acelerar as consultas, o modelo de dados geralmente é composto por tabelas mais desnormalizadas (dados mais repetidos, sem necessidade de ter que fazer joins), organizadas segundo padrões de design de suporte a decisão, como star schema ou snow flake.

Exemplos típicos onde encontraremos esse tipo de sistema: data warehouses, data marts, data lakes e data lakehouses.

O formato colunar é diametralmente o oposto do formato orientado por linha. Neste formato, os dados são fisicamente organizados pelas colunas, e não pelas linhas:

Na figura podemos ver que os dados de cada coluna são armazenados fisicamente juntos, em blocos diferentes. Isto é, o ID 100 está mais próximo do ID 101 fisicamente do que do nome Maria.

Então consultas que buscam retornar todas as colunas são terríveis neste formato, mesmo filtrando pela chave primária:

SELECT * FROM CLIENTES WHERE ID = 103

Isto ocorre porque neste caso teremos que fazer pelo menos 5 leituras físicas diferentes, uma em cada bloco (sendo que no formato por linha, todas as colunas estão fisicamente no mesmo bloco).

Agora, para consultas com perfil mais analítico, onde buscamos apenas por algumas colunas, e descartamos todas as outras, nada é mais eficiente que o formato colunar:

SELECT CIDADE, COUNT(*) as TOTAL
FROM CLIENTES
WHERE PAIS = 'BRASIL'
GROUP BY CIDADE

Neste exemplo, vamos ler apenas os blocos 4 (CIDADE) e 5 (PAÍS).

Por que esse formato colunar é extremamente eficiente para consultas como essa que mostrei acima?

  • Reduz bastante a quantidade de I/O físico a ser feita, pois o I/O é feito apenas sobre os blocos das colunas usadas na consulta (menos dados para ler);
  • Como os dados do mesmo tipo (mesma coluna) estão juntos, no mesmo bloco, a probabilidade de repetição de valores aumenta, e portanto a compressão dos dados também aumenta (por haver maior repetição dos dados de uma mesma coluna): menos I/O;
  • Muitos bancos de dados usam instruções do tipo SIMD (Single Instruction Multiple Data) nos processadores, fazendo com que com uma única instrução de leitura, o processador seja capaz de ler “várias linhas colunares” ao mesmo tempo, aumentando a performance.

Vale lembrar que o formato dos dados, seja linha ou coluna, é algo físico, e não lógico. Em outras palavras, para o usuário-final, ou o sistema, é transparente: utiliza-se SQL.

Exemplos de Tecnologias para cada Formato de Dados

Alguns exemplos de armazenamento no formato de linha:

  • Bancos de dados: Oracle, SQL Server, MySQL, PostgreSQL;
  • Arquivos: AVRO, CSV.

Alguns exemplos de armazenamento no formato de coluna:

  • Bancos de dados: HBase, Oracle ADW, AWS Redshift, GCP BigQuery;
  • Arquivos: ORC, Parquet.

Existem bancos de dados que fornecem um modelo híbrido entre linha e coluna, onde os dados no formato de linha são replicados para um cache colunar, porém, esta replicação é transparente para o sistema (mesmo endpoint de conexão).

Exemplos de bancos de dados que tem a capacidade híbrida: MySQL HeatWeave e Oracle Database In-Memory.

O formato híbrido permite que um sistema transacional co-exista com cargas de relatórios ao mesmo tempo, sem impacto no desempenho, e sem ter que depender de uma engenharia de dados para movimentar os dados entre meios de armazenamento de formatos diferentes.

Conclusão

É importante entender as características de acesso de um sistema que você está criando para selecionar o formato mais apropriado: que traz boa performance e baixo consumo de armazenamento de dados (compressão).

O formato de linha é melhor para sistemas OLTP, enquanto que o formato colunar é melhor para Data Warehouses, Data Lakes e Data Lakehouses.

O formato híbrido (linha e coluna) é o melhor para casos mistos, onde há nitidamente a necessidade de existir transações e relatórios analíticos sob um mesmo banco de dados.

Publicidade

OCI NoSQL Tables

Geralmente temos servidores e storage, e então bancos de dados, e claro, tabelas. No entanto, com o avanço das arquiteturas cloud native existe uma outra abordagem: as tabelas sem bancos de dados e sem servidores (tabelas serverless).

Essas tabelas existem como um serviço gerenciado em diversos provedores de cloud, e somente nos provedores de cloud, pois elas são uma consequência das arquiteturas cloud native. Neste post vou escrever sobre o serviço NoSQL Tables, as tabelas Serverless da Oracle Cloud (OCI).

Vou explicar como elas funcionam, para que servem, casos de uso, e também vou mostrar um exemplo prático para ficar mais claro.

Principais Características

Apesar de você não enxergar os servidores, eles existem, claro! Mas o serviço expõe as tabelas com uma abordagem serverless. Do ponto de vista físico, as NoSQL Tables são tabelas que implementam o conceito de sharding, isto é, os dados são distribuídos de forma horizontal em servidores diferentes. Além disso, os dados de cada servidor também são replicados para outros servidores. Essa arquitetura distribuída, além de promover alta disponibilidade e durabilidade, garante que o tempo de resposta seja, sempre de forma estável e previsível, de poucos milisegundos.

Como os dados são distribuídos e replicados em servidores diferentes, você pode escolher se as leituras serão do tipo consistente ou eventual, e se vai utilizar ACID para as transações: para entender melhor este ponto (caso não conheça), veja este meu outro post onde abordo o Teorema CAP.

Há bastante flexibilidade para escolher o formato dos dados: existe suporte para dados em colunas (schema-full), tal como conhecemos no modelo relacional (ex: integer, string timestamp e Blob), mas adiciona tipos complexos, como Arrays, Records e Map. E claro, não menos importante, também suporta JSON nativamente (schema flexível).

Outras capacidades que também ajudam bastante em diversos casos é a possibilidade de configurar TTL (time-to-live, onde os dados são removidos automaticamente após o tempo que você especificar), criação de índices secundários (inclusive com JSON), e também a possibilidade de usar SQL ou API (com suporte a Spring Data).

Como não existe servidor, e se quer existe um banco de dados (pelo menos em termos de usabilidade), como será que esses serviços são dimensionados? Bom, aqui está a grande diferença: pelo workload! Isto é, você dimensiona uma NoSQL Table informando qual é a demanda de uso.

São três variáveis requeridas quando se cria uma NoSQL Table na OCI:

  • Quantidade máxima de leituras por segundo;
  • Quantidade máxima de escritas por segundo;
  • Espaço em Gbytes.

Essas variáveis podem ser alteradas de forma online enquanto a tabela existir (elástico, para cima ou para baixo, a qualquer tempo).

Nota: o billing deste serviço considera essas três variáveis: existe um preço por read unit, outro para write unit, e outro para gbytes utilizados. Em geral é um serviço considerado barato, e faz parte do free tier, mas é importante estimar o custo para o seu workload.

No final do dia NoSQL Tables é um serviço gerenciado, então operações como provisionamento de infraestrutura, backup, patching, configurações de alta disponibilidade e segurança, enfim, tudo aquilo que é necessário para deixá-lo no ar e disponível é gerenciado pela própria OCI.

Casos de Uso e Utilidade

Por ser simples, permitir um schema flexível, com baixa latência previsível, e ser completamente gerenciado, as tabelas serveless podem ser bastante úteis nos casos de uso abaixo e quaisquer outros casos similares a estes:

  • Perfis de usuário;
  • Eventos de qualquer tipo (usuários, clicks, etc) e IoT;
  • Carrinho de compras;
  • Catálogo de produtos;
  • Cache de dados para web servers;
  • Armazenamento de metadados e parâmetros;
  • Personalização de anúncios;
  • Detecção de fraude em tempo real.

Claro que existem também os casos onde elas não são úteis. Vou colocar abaixo os principais casos onde você deve fugir das tabelas serverless:

  • Migrar um banco de dados legado existente as-is (a não ser que você queira fazer um grande refactoring);
  • Necessita de um modelo de dados mais complexo, que demanda por um schema com muitas tabelas e relacionamentos. Se não for possível colocar tudo em poucas tabelas, ou se não for compatível com workloads do tipo JSON, não use;
  • No caso onde os limites do serviço gerenciado são insuficientes para o seu caso de uso. À propósito, isto vale para qualquer serviço gerenciado.

Na Prática

As NoSQL Tables podem ser usadas de diversas formas: pelo console gráfico da OCI, pela CLI (Command Line Interface), e também pelas SDKs nas diversas liguagens suportadas, como Java, Node.js, Python, .Net e Go.

Vou utilizar a CLI para criá-las, e para isso você precisa configurar a OCI CLI previamente.

Nota: no arquivo de configuração da OCI CLI você pode colocar alguns parâmetros que utiliza com frequência para evitar de repeti-los toda vez que faz uma chamada CLI. Nos exemplos a seguir eu fiz isso para o parâmetro compartment_id, que é obrigátorio e é responsável por agrupar logicamente os recursos que você cria na OCI).

Com a CLI configurada, veja abaixo como é simples criar uma tabela serverless na OCI. Primeiro vamos especificar os limites da tabela, criando este arquivo table_limits.json:

{
  "maxReadUnits": 2,
  "maxStorageInGBs": 1,
  "maxWriteUnits": 2
}

Com este arquivo como exemplo, nossa tabela permitirá no máximo 2 leituras (maxReadUnits) e 2 escritas (maxWriteUnits) concorrentes, e até 1 GByte de espaço (maxStorageInGBs).

Depois podemos utilizá-lo no comando abaixo:

oci nosql table create --ddl-statement "CREATE TABLE usuarios (id integer, nome string, PRIMARY KEY (SHARD(id)))" --name usuarios --table-limits file://table_limits.json

A tabela chama-se Usuarios, e coloquei apenas duas colunas: id (integer) e nome (string), onde id é a chave primária, e também é a coluna que distribui os dados horizontalmente (shard).

Se eu quisesse utilizar JSON para a coluna nome, bastava substituir string por json na criação da tabela, ou ainda adicionar mais colunas de outros tipos.

Para listar suas tabelas criadas, você pode executar o seguinte comando:

oci nosql table list

Este comando vai retornar um output JSON com a lista de tabelas e suas propriedades.

Apesar de eu não achar apropriado, podemos também manipular e consultar dados pelo OCI CLI. Este comando abaixo insere um registro:

oci nosql query execute --statement "INSERT INTO usuarios (id, nome) VALUES (1,\"Fernando\")"

É possível consultar a tabela com SQL, utilizando este comando:

oci nosql query execute --statement "SELECT * FROM usuarios"

Para remover a tabela, você pode fazer desta forma:

oci nosql table delete --table-name-or-id usuarios

Bom, utilizar a OCI CLI para criar e remover tabelas tudo bem, mas para manipular dados com certeza é melhor utilizar alguma SDK, pois é através dela que você utilizará as tabelas serverless no seu sistema.

Vou utilizar como exemplo a SDK em Python. Veja aqui os requerimentos para instalar os drivers (biblioteca borneo) no seu sistema.

Abaixo vou fazer uma consulta simples pela chave usando API, mas antes preciso importar a biblioteca com os objetos que usaremos, e também fazer a configuração de conexão e autenticação:

from borneo import NoSQLHandle, NoSQLHandleConfig, Regions, Consistency
from borneo.iam import SignatureProvider

handleConfig = NoSQLHandleConfig(Regions.US_ASHBURN_1)
config = handleConfig.set_authorization_provider(SignatureProvider())
config.set_default_compartment('dev')
config.set_consistency(Consistency.EVENTUAL) # ou Consistency.ABSOLUTE
conn = NoSQLHandle(config)

Importei da biblioteca borneo (que utilizamos para manipular as NoSQL Tables) todos os objetos que vou utilizar nos exemplos seguintes.

config é minha configuração de conexão e autenticação, que recebe a região da OCI que usei para criar a tabela (us_ashburn_1) e meus dados de acesso com SignatureProvider(): esse método retorna minhas credenciais que estão configuradas na minha OCI CLI — há várias outras formas de autentição também, veja aqui.

dev é o compartimento onde criei a tabela, e Consistency.EVENTUAL configuro as leituras para serem eventuais (Availability + Partition Tolerance, do Teorema CAP), mas poderia configurar como Consistency.Absolute (Strong Consistency + Partition Tolerance, do Teorema CAP).

O objeto conn (criado a partir de config) é utilizado para as demais operações sobre a tabela.

Bom, vamos então para a consulta simples pela chave usando API:


from borneo import GetRequest

request = GetRequest().set_table_name('usuarios')
request.set_key({'id': 1}) 
result = conn.get(request) 
if result.get_value() is not None:
    print(result)   

Primeiro informei no objeto request qual é o nome da minha tabela, e depois consigo consultá-la informando qual é a chave de busca pelo método set_key. O objeto result recebe o resultado da consulta.

O código abaixo faz a mesma coisa, porém utiliza SQL, e Prepared Statements:

from borneo import QueryRequest, PrepareRequest

sql = 'declare $myKey integer; select * from usuarios where id = $myKey'
request = PrepareRequest().set_statement(sql)
prepStmt = conn.prepare(request).get_prepared_statement()
prepStmt.set_variable('$myKey', 1)
queryRequest = QueryRequest().set_prepared_statement(prepStmt)
while True:
    result = conn.query(queryRequest)
    resultList = result.get_results()
    if queryRequest.is_done():
        break

print(resultList)

PreparedStatements servem para otimizar a execução das instruções SQL em um banco de dados, de forma que faça apenas um parse e múltiplas execuções (ao invés de fazer um parse para cada execução).

Inicialmente criei uma string SQL utilizando $mykey como variável. Depois preparei o statement, e depois fiz a operação de bind, que é colocar o valor na variável $mykey. Por fim a query é executada e o resultado é atribuído ao objeto resultList.

E quanto as transações? Certo! É possível inserir, atualizar e remover dados, normalmente.

Veja abaixo como faço para inserir mais um registro na minha tabela:

from borneo import PutRequest

request = PutRequest().set_table_name('usuarios')
request.set_value({'id': 2, 'nome': 'Paulo'})
result = handle.put(request)
if result.get_version() is not None:
   print('success')

Basta criar um request utilizando o objeto PutRequest, atribuir o valor, e executar com put. Também é possível fazer com SQL, e também é possível fazer várias mudanças com uma simples operação (WriteMultipleRequest).

Neste post eu fiz um overview bem resumido do que é possível fazer com as tabelas serverless. Em outros posts vou mostrar outras capacidades deste serviço. Stay tuned!

Conclusão

As NoSQL Tables são mais robustas do que parecem. Você pode criar tabelas serverless com múltiplos formatos, incluindo JSON e SQL (com ACID e opção de escolha entre consistência forte ou eventual), índices secundários e TTL. É muito fácil de começar, e não há muito o que aprender, pois o serviço é totalmente gerenciado.

Referências

Você pode me seguir neste blog:

Disclaimer: As opiniões expressas neste blog são minhas, e não são necessariamente as mesmas opiniões do meu empregador.