Skip to content
CESAR ZANIS
Tech Reference
Foundation ·Updated: May 2026

Database & Data

The model I designed in 2013 is the one that works in 2035. Good database practices endure; the challenge is applying them correctly.

"CRUD is everything. At its core, every system creates, reads, updates and deletes. Whoever masters that masters the foundations. In IT, everything is limited by the slowest part, and that has always been the database."

Cesar Zanis
01/A foundation worth getting right.

PRIMARY KEY: INT/BIGINT with IDENTITY or SERIAL

The primary key anchors every table. Use INT (up to roughly two billion rows) or BIGINT beyond that, with automatic generation: IDENTITY in SQL Server or SERIAL/GENERATED ALWAYS AS IDENTITY in PostgreSQL. Keep sequential internal identifiers out of public APIs and URLs. Their predictable order is useful for B-tree performance, but should not become an access-control assumption.
SQLStandard 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)
);

A UNIQUE index on public_id preserves uniqueness while separating the public identifier from the clustered primary key. This combines efficient internal access with an independent public identifier.

02/Choose UUID primary keys with their storage and access patterns in mind.

UUID: a public identifier alongside the primary key

UUID v4 values are random. When used as a clustered primary key in a large table, random inserts can cause page splits, fragmentation and additional storage. The effect depends on the database engine, indexes and workload. One option is an ordered INT/BIGINT primary key plus a separate UUID column with a UNIQUE index for public identifiers and integrations. Time-ordered UUID v7 is another option to evaluate when distributed identifier generation matters.
SQLStandard SQL
-- Example of random identifiers
CREATE TABLE customers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid()
    -- Measure insertion patterns, page splits and index size
);

-- Alternative: sequential internal key and public UUID
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 join performance using your real workload. BIGINT is compact at eight bytes; UUID uses more space. Index design and the query plan determine the actual difference.

03/Preserve business history with an explicit retention policy.

Soft Delete: deleted_at or is_deleted

A physical DELETE is irreversible without recovery mechanisms and may remove business history. Consider soft delete for transactional records such as orders, customers and products. A deleted_at timestamp records when deletion happened, unlike a boolean is_deleted. Every relevant SELECT must then filter WHERE deleted_at IS NULL; centralize that rule in views or repository/ORM filters. Soft delete does not replace legal retention limits or actual erasure when required.
SQLStandard SQL
-- Example schema
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 = active, NOT NULL = deleted
    CONSTRAINT uq_products_public_id UNIQUE (public_id)
);

-- Partial index for active records (PostgreSQL)
-- Excludes deleted records from this index
CREATE INDEX ix_products_name_active
    ON products (name)
    WHERE deleted_at IS NULL;

-- Standard query: always include the filter
SELECT * FROM products WHERE deleted_at IS NULL AND name ILIKE '%notebook%';

Filtered or partial indexes using WHERE deleted_at IS NULL can reduce index size when many records are soft-deleted. Check the query plan before adopting them.

04/An unsuitable index can cost more than it saves.

Indexes: purpose, timing and cost

Indexes accelerate reads at the cost of writes and storage. Each additional index must be maintained on INSERT, UPDATE and DELETE. Evaluate query frequency, selectivity and the execution plan. Low-cardinality columns may not benefit from a simple index, although filtered or composite indexes can still help. Covering indexes with INCLUDE can avoid extra table lookups when they contain all the columns a query needs.
SQLStandard SQL
-- A simple index may require lookups for additional columns
CREATE INDEX ix_orders_customer_id ON orders (customer_id);

-- Covering index: includes the columns this query needs
CREATE INDEX ix_orders_customer_id_cover
    ON orders (customer_id)
    INCLUDE (status, total_amount, created_at);

-- Check whether the execution plan uses the covering index:
SELECT status, total_amount, created_at
FROM orders
WHERE customer_id = 42 AND deleted_at IS NULL;

-- Composite index: column order matters
-- Choose column order from predicates and the execution plan
CREATE INDEX ix_orders_status_created
    ON orders (status, created_at DESC)
    WHERE deleted_at IS NULL;

Do not add indexes to boolean or low-cardinality columns by habit. Validate their usefulness with execution plans and representative data.

05/A database without maintenance accumulates hidden technical debt.

Maintenance Routines (PostgreSQL)

PostgreSQL uses MVCC: updates create new row versions, and obsolete versions need cleanup. Heavy-update tables such as queues and sessions can accumulate dead tuples if vacuuming falls behind. Autovacuum handles many cases, but critical tables need monitoring and tuning. ANALYZE keeps planner statistics current so the optimizer can choose suitable execution plans.
SQLStandard SQL
-- Manual maintenance examples: evaluate before running
-- Schedule according to workload and maintenance needs
VACUUM (VERBOSE, ANALYZE) orders;

-- Inspect dead tuples by table
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;

-- Inspect index usage before considering removal
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;

Consider log_min_duration_statement = 500 in PostgreSQL to record queries taking at least 500 ms. Tune the threshold and logging volume for your environment.

06/Review fragmentation alongside page density and workload.

Maintenance Routines (SQL Server)

In SQL Server, index maintenance should consider page density, fragmentation, index size and observed workload. Do not rebuild solely because a fixed percentage was exceeded. Measure whether reorganizing or rebuilding improves the queries that matter, and account for locking, log growth and resource use. Keep statistics current so the optimizer can estimate row counts accurately.
SQLStandard SQL
-- Inspect fragmentation by index
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;

-- Reorganize: evaluate workload, page density and benefits first
ALTER INDEX ix_orders_customer_id ON orders REORGANIZE;

-- Rebuild: evaluate costs and locking for this edition
ALTER INDEX ix_orders_customer_id ON orders REBUILD WITH (ONLINE = ON);

-- Update statistics for a table
UPDATE STATISTICS orders WITH FULLSCAN;

-- Inspect expensive queries (top 10 by 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;

Automate monitoring and schedule necessary maintenance outside peak hours. Choose the frequency from observed workload and results.

Following these patterns but still facing performance or modeling problems?

Talk to Cesar Zanis: a direct technical diagnosis