Quando trabalhamos com bancos de dados relacionais, é comum nos depararmos com situações onde precisamos contar registros relacionados em uma tabela.
Primeiramente vamos considerar as seguintes tabelas:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
order_date DATE,
customer VARCHAR(100)
);
CREATE TABLE order_items (
item_id SERIAL PRIMARY KEY,
order_id INT,
product VARCHAR(100),
quantity INT,
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
Temos uma tabela de orders
e queremos contar quantos itens cada pedido possui. Uma abordagem comum seria usar uma subquery com COUNT:
SELECT
o.order_id,(SELECT
COUNT(*) FROM order_items oi
WHERE oi.order_id = o.order_id
) AS total_items
FROM
orders o;
Essa query funciona, mas pode se tornar ineficiente em cenários com grandes volumes de dados, pois a subquery é executada para cada linha da tabela orders
.
Aqui entra a desnormalização!
A desnormalização em bancos de dados é o processo de introduzir redundância em um banco de dados previamente normalizado, com o objetivo de melhorar o desempenho em operações de leitura (consultas). Envolve a adição de dados duplicados ou a combinação de tabelas para reduzir o número de operações necessárias (joins, counts...), o que pode acelerar a recuperação de informações em sistemas onde a leitura é mais frequente que a escrita.
Agora sabemos o que é desnormalização como podemos simplificar nossa query?
Em vez de contar os itens toda vez que executamos a query, podemos adicionar um campo total_items diretamente na tabela orders. E sempre que um item for adicionado ou removido, incrementamos ou decrementamos esse campo.
Como fazer isso?
- Adicione o campo
total_items
na tabelaorders
:
ALTER TABLE orders ADD COLUMN total_items INT DEFAULT 0;
Atualize o campo
total_items
sempre que um item for adicionado ou removidoSua query fica muito mais simples e eficiente:
SELECT
order_id,
total_items
FROM
orders;
Vamos inserir dados para podermos validar nossa proposta, podemos inserir uma massa de dados com a seguinte função:
CREATE OR REPLACE FUNCTION generate_sample_data()
RETURNS VOID AS $$
DECLARE
order_count INT := 200;
items_per_order INT := 10;
current_order_id INT;
random_customer VARCHAR(100);
random_product VARCHAR(100);
random_quantity INT;
BEGIN
FOR i IN 1..order_count LOOP
random_customer := 'Customer ' || (floor(random() * 1000)::INT);
INSERT INTO orders (order_date, customer, total_items)
VALUES (current_date - (floor(random() * 365)::INT), random_customer, 10)
RETURNING order_id INTO current_order_id;
FOR j IN 1..items_per_order LOOP
random_product := 'Product ' || (floor(random() * 100)::INT);
random_quantity := (floor(random() * 10)::INT + 1;
INSERT INTO order_items (order_id, product, quantity)
VALUES (current_order_id, random_product, random_quantity);
END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;
SELECT generate_sample_data();
No script anterior basicamente criamos 200 registros na tabela orders
e 10 registros na tabela order_items
para cada order
.
Para compararmos e validarmos nossa hipótese vamos executar nossas queries com explain analyse
e teremos o seguinte resultado:
Utilizando a subquery:
Podemos ver que executando a consulta com a subquery o execution time é igual a 28.983ms
Utilizando o campo criado na desnormalização:
Sem a subquery nosso resultado é 0.073ms. Sendo assim 397 vezes mais rápido e vale ressaltar que criamos apenas 200 registros na tabela orders
, quanto mais registros criarmos mais a query com subquery será lenta e a query desnormalizada irá se manter na mesma média.
Vantagens:
Performance: Evita a execução de subqueries repetidas.
Simplicidade: A query principal fica mais limpa e fácil de entender.
Controle: Você tem um controle direto sobre o valor do contador.
Cuidados:
Consistência: É preciso garantir que o campo total_items seja sempre atualizado corretamente.
Concorrência: Em sistemas com alta concorrência, é importante garantir que as atualizações sejam atômicas para evitar inconsistências.
Espero que esse conteúdo tenha sido útil para você!
Top comments (0)