CZANIX
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

PRIMARY KEY: INT/BIGINT com IDENTITY ou SERIAL

Inegociável desde sempre.

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 — o que é ótimo para performance de B-Tree, péssimo para segurança.

SQL
-- 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

UUID: campo de exposição, nunca PRIMARY KEY

UUID como PK é erro clássico que custa caro depois.

UUID v4 é aleatório. Quando você usa UUID como PRIMARY KEY em uma tabela grande, cada INSERT fragmenta o índice clustered porque o novo valor não vai para o final — vai para um lugar aleatório no meio. Em volume, isso custa entre 30-50% de performance de escrita e aumenta o espaço em disco de forma significativa.

A solução correta: INT/BIGINT como PK (ordenado, sem fragmentação) + UUID em coluna separada com índice UNIQUE (para exposição pública e integração entre sistemas).

Se precisar de UUID ordenado por tempo, use UUID v7 — ele resolve o problema de fragmentação mantendo a universalidade.

SQL
-- ❌ O erro clássico
CREATE TABLE customers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid()
    -- Cada insert fragmenta o B-Tree. Em 500k linhas você vai sentir.
);

-- ✅ O padrão correto
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,
    CONSTRAINT uq_customers_public_id UNIQUE (public_id)
);

-- A API expõe public_id. O banco trabalha com id.

Tabelas com menos de 10k linhas? UUID como PK é aceitável por simplicidade. O problema aparece em volume. Use o contexto.

03

Soft Delete: deleted_at ou is_deleted

Dado deletado que importa para negócio nunca some de verdade.

Hard delete (DELETE físico) é irreversível e quebra integridade referencial histórica. Para qualquer tabela transacional — pedidos, clientes, produtos — use soft delete.

A coluna deleted_at (com timestamp) é superior ao is_deleted (boolean) porque você sabe quando foi deletado, não só se foi. Isso vale para auditoria, LGPD e relatórios de BI.

A contrapartida é que todo SELECT precisa filtrar WHERE deleted_at IS NULL. Crie uma view ou use filtros globais no seu ORM/repositório. Se você esquecer esse filtro uma vez, vai exibir dados deletados em produção.

SQL
-- Estrutura recomendada
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)
-- Ignora registros deletados no índice — menor, mais rápido
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 (WHERE deleted_at IS NULL) são ordens de magnitude menores que índices completos em tabelas com muitos registros deletados. Use-os.

04

Índices: o que, quando e o custo

Índice errado é pior que nenhum índice.

Índices aceleram leitura e custam escrita. Cada índice adicional em uma tabela aumenta o custo de INSERT, UPDATE e DELETE — porque o banco precisa manter o índice atualizado.

O custo vale quando a query é frequente e a seletividade é alta (poucos resultados para muitos candidatos). Não vale para colunas com poucos valores distintos (boolean, status com 3 opções).

Covering index (com INCLUDE) elimina Key Lookup — quando o índice já contém todas as colunas que a query precisa, o banco não volta à tabela. É a otimização mais impactante para queries de leitura frequente.

SQL
-- ❌ Índice simples — força Key Lookup se a query pede outras colunas
CREATE INDEX ix_orders_customer_id ON orders (customer_id);

-- ✅ Covering index — a query encontra tudo no índice
CREATE INDEX ix_orders_customer_id_cover
    ON orders (customer_id)
    INCLUDE (status, total_amount, created_at);

-- A query abaixo não toca na tabela principal:
SELECT status, total_amount, created_at
FROM orders
WHERE customer_id = 42 AND deleted_at IS NULL;

-- ✅ Índice composto: ordem importa
-- Coluna mais seletiva primeiro
CREATE INDEX ix_orders_status_created
    ON orders (status, created_at DESC)
    WHERE deleted_at IS NULL;

Não indexe colunas booleanas ou colunas com baixa cardinalidade. O banco pode ignorar o índice e fazer table scan mesmo assim.

05

Rotinas de Manutenção (PostgreSQL)

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

O PostgreSQL tem MVCC — cada UPDATE cria uma nova versão da linha, a versão antiga fica como "dead tuple" até o VACUUM rodar. Em tabelas com alto volume de UPDATE (logs, filas, sessões), a tabela pode inchar 3-5x o tamanho real se o VACUUM não rodar corretamente.

O autovacuum cuida da maioria dos casos, mas tabelas críticas precisam de atenção manual. ANALYZE mantém as estatísticas do query planner atualizadas — sem ele, o banco pode escolher planos de execução ruins mesmo com índices corretos.

SQL
-- Manutenção manual em tabelas críticas
-- Roda fora do horário de pico
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 índices não utilizados (candidatos à 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

Rotinas de Manutenção (SQL Server)

Fragmentação de índice mata performance em silêncio.

No SQL Server, índices se fragmentam com o tempo — especialmente em tabelas com muitos INSERT e DELETE. Fragmentação acima de 30% degrada performance de leitura significativamente. Abaixo de 5%, ignore. Entre 5-30%, reorganize (online). Acima de 30%, reconstrua.

As estatísticas desatualizadas fazem o query optimizer tomar decisões ruins — escolher um índice ineficiente porque não sabe quantos registros existem de verdade. Update Statistics resolve isso.

SQL
-- 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 (online, baixo impacto) — fragmentação 5-30%
ALTER INDEX ix_orders_customer_id ON orders REORGANIZE;

-- Reconstruir (mais agressivo) — fragmentação > 30%
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 check de fragmentação como job semanal. Em produção com alto volume, rode diariamente fora do horário de pico.

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

Fale com o Nix — diagnóstico direto, sem enrolação →