Pular para o conteúdo
CESAR ZANIS
Tech Reference
Fundação ·Atualizado: Mai 2026

Database & Dados

O modelo que eu criava em 2013 é o mesmo que funciona em 2035. Boas práticas de banco de dados não mudam; o que muda é que ainda tem gente fazendo errado.

"CRUD é tudo. Todo sistema, no fundo, é criar, ler, atualizar e deletar. Quem domina isso, domina o alicerce. Em TI, tudo é nivelado pelo mais lento, e o mais lento sempre foi o banco."

Cesar Zanis
01/Inegociável desde sempre.

PRIMARY KEY: INT/BIGINT com IDENTITY ou SERIAL

O campo de chave primária é o eixo de rotação de toda tabela. Use INT (até ~2 bilhões de linhas) ou BIGINT (além disso). Sempre com geração automática: IDENTITY no SQL Server, SERIAL/GENERATED ALWAYS AS IDENTITY no PostgreSQL. Nunca exponha esse ID em APIs públicas ou URLs. Ele é interno, sequencial e previsível: ótimo para performance de B-Tree, péssimo para segurança.
SQLSQL padrão
-- PostgreSQL
CREATE TABLE orders (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    public_id  UUID NOT NULL DEFAULT gen_random_uuid(),
    -- ...
    CONSTRAINT uq_orders_public_id UNIQUE (public_id)
);

-- SQL Server
CREATE TABLE orders (
    id        BIGINT IDENTITY(1,1) PRIMARY KEY,
    public_id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
    -- ...
    CONSTRAINT uq_orders_public_id UNIQUE (public_id)
);

O índice UNIQUE na coluna public_id garante integridade sem custo de fragmentação no clustered index. É o melhor dos dois mundos: performance interna, segurança externa.

02/Escolha chaves UUID considerando armazenamento e padrões de acesso.

UUID: identificador público junto à chave primária

UUID v4 é aleatório. Como chave primária clustered em uma tabela grande, inserções aleatórias podem provocar divisão de páginas, fragmentação e maior armazenamento. O impacto depende do motor, dos índices e da carga. Uma opção é INT/BIGINT ordenado como chave primária e uma coluna UUID com índice UNIQUE para identificação pública e integrações. UUID v7, ordenado por tempo, também pode ser avaliado quando a geração distribuída de identificadores é necessária.
SQLSQL padrão
-- Exemplo de identificadores aleatórios
CREATE TABLE customers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid()
    -- Meça padrão de inserção, page splits e tamanho do índice
);

-- Alternativa: chave interna sequencial e UUID público
CREATE TABLE customers (
    id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    public_id UUID NOT NULL DEFAULT gen_random_uuid(),
    name      VARCHAR(200) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_customers_public_id UNIQUE (public_id)
);

Compare o desempenho dos JOINs com sua carga real. BIGINT ocupa oito bytes; UUID exige mais espaço. Os índices e o plano de execução determinam a diferença efetiva.

03/Preserve o histórico de negócio com uma política explícita de retenção.

Soft Delete: deleted_at ou is_deleted

DELETE físico é irreversível sem mecanismos de recuperação e pode remover histórico de negócio. Considere soft delete para registros transacionais como pedidos, clientes e produtos. deleted_at registra quando houve a exclusão, ao contrário de um booleano is_deleted. Os SELECTs pertinentes precisam filtrar WHERE deleted_at IS NULL; centralize essa regra em views ou filtros do ORM/repositório. Soft delete não substitui prazos legais de retenção nem a eliminação efetiva quando exigida.
SQLSQL padrão
-- Exemplo de estrutura
CREATE TABLE products (
    id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    public_id    UUID NOT NULL DEFAULT gen_random_uuid(),
    name         VARCHAR(300) NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at   TIMESTAMPTZ NULL, -- NULL = ativo, NOT NULL = deletado
    CONSTRAINT uq_products_public_id UNIQUE (public_id)
);

-- Índice filtrado para buscar só ativos (PostgreSQL)
-- Exclui registros deletados deste índice
CREATE INDEX ix_products_name_active
    ON products (name)
    WHERE deleted_at IS NULL;

-- Query padrão: sempre com o filtro
SELECT * FROM products WHERE deleted_at IS NULL AND name ILIKE '%notebook%';

Índices filtrados ou parciais com WHERE deleted_at IS NULL podem reduzir o tamanho do índice quando há muitos registros excluídos logicamente. Verifique o plano de execução antes de adotá-los.

04/Índice errado é pior que nenhum índice.

Índices: o que, quando e o custo

Índices aceleram leituras ao custo de escritas e armazenamento. Cada índice precisa ser mantido em INSERT, UPDATE e DELETE. Avalie frequência, seletividade e plano de execução. Colunas de baixa cardinalidade podem não se beneficiar de um índice simples, embora índices filtrados ou compostos possam ajudar. Índices de cobertura com INCLUDE podem evitar acessos adicionais à tabela quando contêm todas as colunas necessárias à consulta.
SQLSQL padrão
-- Índice simples pode exigir lookups para outras colunas
CREATE INDEX ix_orders_customer_id ON orders (customer_id);

-- Covering index: inclui as colunas desta query
CREATE INDEX ix_orders_customer_id_cover
    ON orders (customer_id)
    INCLUDE (status, total_amount, created_at);

-- Confira se o plano de execução usa o covering index:
SELECT status, total_amount, created_at
FROM orders
WHERE customer_id = 42 AND deleted_at IS NULL;

-- ✅ Índice composto: ordem importa
-- Escolha a ordem pelos predicados e pelo plano de execução
CREATE INDEX ix_orders_status_created
    ON orders (status, created_at DESC)
    WHERE deleted_at IS NULL;

Não adicione índices a colunas booleanas ou de baixa cardinalidade por hábito. Valide sua utilidade com planos de execução e dados representativos.

05/Banco sem manutenção é dívida técnica silenciosa.

Rotinas de Manutenção (PostgreSQL)

PostgreSQL usa MVCC: atualizações criam novas versões das linhas, e versões obsoletas precisam de limpeza. Tabelas com muitas atualizações, como filas e sessões, podem acumular dead tuples se VACUUM ficar para trás. Autovacuum atende muitos casos, mas tabelas críticas precisam de monitoramento e ajuste. ANALYZE mantém estatísticas atualizadas para o otimizador escolher planos adequados.
SQLSQL padrão
-- Exemplos de manutenção manual: avalie antes de executar
-- Agende conforme a carga e a necessidade de manutenção
VACUUM (VERBOSE, ANALYZE) orders;

-- Ver dead tuples por tabela (monitorar proativamente)
SELECT
    schemaname,
    relname AS table_name,
    n_dead_tup AS dead_tuples,
    n_live_tup AS live_tuples,
    ROUND(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct,
    last_vacuum,
    last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY dead_pct DESC;

-- Ver uso dos índices antes de considerar remoção
SELECT
    indexrelname AS index_name,
    relname AS table_name,
    idx_scan AS times_used
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexrelname NOT LIKE '%pkey%'
ORDER BY relname;

Configure log_min_duration_statement = 500 no PostgreSQL para capturar queries lentas automaticamente. É o primeiro passo para qualquer otimização baseada em evidência.

06/Revise fragmentação junto à densidade das páginas e à carga.

Rotinas de Manutenção (SQL Server)

No SQL Server, a manutenção de índices deve considerar densidade das páginas, fragmentação, tamanho e carga observada. Não reconstrua apenas porque um percentual fixo foi ultrapassado. Meça se reorganizar ou reconstruir melhora as consultas relevantes e considere bloqueios, crescimento do log e consumo de recursos. Mantenha estatísticas atualizadas para o otimizador estimar o número de linhas corretamente.
SQLSQL padrão
-- Verificar fragmentação por índice
SELECT
    OBJECT_NAME(ips.object_id) AS table_name,
    i.name AS index_name,
    ips.avg_fragmentation_in_percent AS fragmentation_pct,
    ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 5
  AND ips.page_count > 100
ORDER BY ips.avg_fragmentation_in_percent DESC;

-- Reorganizar: avalie carga, densidade de páginas e benefícios
ALTER INDEX ix_orders_customer_id ON orders REORGANIZE;

-- Reconstruir: avalie custos e bloqueios nesta edição
ALTER INDEX ix_orders_customer_id ON orders REBUILD WITH (ONLINE = ON);

-- Atualizar estatísticas de uma tabela
UPDATE STATISTICS orders WITH FULLSCAN;

-- Ver queries mais lentas (Top 10 por CPU)
SELECT TOP 10
    qs.total_elapsed_time / qs.execution_count / 1000 AS avg_ms,
    qs.execution_count,
    SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
          ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY avg_ms DESC;

Automatize o monitoramento e programe a manutenção necessário fora dos horários de pico. Defina a frequência pela carga observada e pelos resultados.

Aplica esses padrões e ainda está com problema de performance ou modelagem?

Falar com Cesar Zanis: diagnóstico direto, sem enrolação