Docs For AI
Design

Best Practices Checklist

Comprehensive checklist for reviewing database design against industry best practices

Best Practices Checklist

This comprehensive checklist helps you evaluate database design against industry best practices. Use it as a systematic review guide for new designs or existing databases.

Quick Assessment Checklist

Database Design Review Checklist:
┌─────────────────────────────────────────────────────────────┐
│  □ Naming Conventions                    Score: __/25       │
│  □ Schema Design                         Score: __/25       │
│  □ Performance Considerations            Score: __/25       │
│  □ Security & Maintenance               Score: __/25       │
│  ─────────────────────────────────────────────────────────  │
│  TOTAL SCORE:                                    __/100     │
└─────────────────────────────────────────────────────────────┘

1. Naming Conventions (25 points)

Table Naming

CheckPointsPass/Fail
Use plural nouns for table names (users, not user)2□
Use snake_case consistently2□
Avoid reserved SQL keywords2□
Names are descriptive and self-documenting2□
No prefixes like tbl_ or table_2□

Verification Query:

-- Check table naming patterns
SELECT
    table_name,
    CASE
        WHEN table_name ~ '^[a-z][a-z0-9_]*[s]$' THEN 'Good: plural snake_case'
        WHEN table_name ~ '^tbl_' THEN 'Bad: tbl_ prefix'
        WHEN table_name ~ '[A-Z]' THEN 'Bad: contains uppercase'
        WHEN table_name !~ 's$' THEN 'Warning: might not be plural'
        ELSE 'Review manually'
    END as naming_assessment
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;

Column Naming

CheckPointsPass/Fail
Use snake_case for column names2□
Primary key named id or {table}_id2□
Foreign keys named {referenced_table}_id2□
Boolean columns prefixed with is_, has_, can_2□
Timestamp columns: created_at, updated_at, deleted_at2□
Avoid abbreviations (use description, not desc)1□

Good vs Bad Naming Examples:

-- Bad naming
CREATE TABLE tblUser (
    UserID INT,
    fName VARCHAR(50),
    LName VARCHAR(50),
    isActive INT,  -- Should be BOOLEAN
    create_date TIMESTAMP
);

-- Good naming
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Index and Constraint Naming

CheckPointsPass/Fail
Index: idx_{table}_{columns}2□
Foreign Key: fk_{table}_{referenced_table}1□
Unique: uq_{table}_{columns}1□
Check: chk_{table}_{description}1□
-- Check constraint naming patterns
SELECT
    constraint_name,
    constraint_type,
    table_name,
    CASE
        WHEN constraint_type = 'PRIMARY KEY' AND constraint_name LIKE '%_pkey' THEN 'OK'
        WHEN constraint_type = 'FOREIGN KEY' AND constraint_name LIKE 'fk_%' THEN 'OK'
        WHEN constraint_type = 'UNIQUE' AND constraint_name LIKE 'uq_%' THEN 'OK'
        WHEN constraint_type = 'CHECK' AND constraint_name LIKE 'chk_%' THEN 'OK'
        ELSE 'Review naming'
    END as naming_check
FROM information_schema.table_constraints
WHERE table_schema = 'public'
ORDER BY table_name, constraint_type;

2. Schema Design (25 points)

Primary Keys

CheckPointsPass/Fail
Every table has a primary key5□
PKs are immutable (never updated)2□
Appropriate PK type (SERIAL/UUID/BIGINT)2□
No composite PKs unless necessary1□

Foreign Keys & Relationships

CheckPointsPass/Fail
All relationships have FK constraints3□
Appropriate ON DELETE actions defined2□
No circular dependencies2□
Junction tables for many-to-many2□

Check Relationship Coverage:

-- Find _id columns without FK constraints
SELECT
    c.table_name,
    c.column_name
FROM information_schema.columns c
WHERE c.table_schema = 'public'
AND c.column_name LIKE '%_id'
AND c.column_name != 'id'
AND NOT EXISTS (
    SELECT 1
    FROM information_schema.key_column_usage kcu
    JOIN information_schema.table_constraints tc
        ON kcu.constraint_name = tc.constraint_name
    WHERE kcu.table_name = c.table_name
    AND kcu.column_name = c.column_name
    AND tc.constraint_type = 'FOREIGN KEY'
)
ORDER BY c.table_name;

Data Types

CheckPointsPass/Fail
Appropriate data types for each column2□
VARCHAR lengths are reasonable1□
Using ENUM for fixed value sets1□
Timestamps use TIMESTAMPTZ1□
Money uses DECIMAL/NUMERIC, not FLOAT1□

Data Type Recommendations:

DataRecommended TypeAvoid
CurrencyDECIMAL(19,4)FLOAT, REAL
EmailVARCHAR(255)TEXT
UUIDUUIDVARCHAR(36)
BooleanBOOLEANINT, CHAR(1)
TimestampTIMESTAMPTZTIMESTAMP
JSON dataJSONBTEXT, JSON
IP AddressINETVARCHAR

3. Performance Considerations (25 points)

Indexing Strategy

CheckPointsPass/Fail
PKs are indexed (automatic)2□
FKs have indexes3□
Frequently queried columns indexed3□
No redundant/duplicate indexes2□
Composite indexes follow query patterns2□

Check FK Index Coverage:

-- Foreign keys without indexes
SELECT
    tc.table_name,
    kcu.column_name as fk_column,
    'Missing Index' as status
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
    ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND NOT EXISTS (
    SELECT 1
    FROM pg_indexes pi
    WHERE pi.tablename = tc.table_name
    AND pi.indexdef LIKE '%' || kcu.column_name || '%'
);

Query Optimization

CheckPointsPass/Fail
No SELECT * in production code2□
Pagination implemented properly2□
N+1 queries identified and fixed3□
Complex queries have covering indexes2□

Table Structure

CheckPointsPass/Fail
Large TEXT/BLOB columns in separate table2□
Appropriate partitioning for large tables2□

4. Security & Maintenance (25 points)

Security

CheckPointsPass/Fail
Sensitive data encrypted at rest3□
No passwords stored in plain text3□
Least privilege access controls2□
Audit columns present (created_at, updated_at)2□
Soft delete implemented where needed2□

Verify Audit Columns:

-- Check for audit columns
SELECT
    t.table_name,
    BOOL_OR(c.column_name = 'created_at') as has_created_at,
    BOOL_OR(c.column_name = 'updated_at') as has_updated_at,
    BOOL_OR(c.column_name = 'deleted_at') as has_deleted_at
FROM information_schema.tables t
LEFT JOIN information_schema.columns c
    ON t.table_name = c.table_name
WHERE t.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
GROUP BY t.table_name
ORDER BY t.table_name;

Maintainability

CheckPointsPass/Fail
Database schema is documented3□
Migration scripts are versioned2□
Seed data for development1□
Backup strategy defined2□
Monitoring alerts configured2□

Default Values & Constraints

CheckPointsPass/Fail
Appropriate DEFAULT values set2□
NOT NULL on required columns1□
-- Columns that might need defaults
SELECT
    table_name,
    column_name,
    data_type,
    is_nullable,
    column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_default IS NULL
AND is_nullable = 'NO'
AND column_name NOT IN ('id')
ORDER BY table_name, column_name;

Complete Assessment Script

-- Comprehensive Best Practices Assessment
DO $$
DECLARE
    v_count INTEGER;
    v_total INTEGER;
    v_score INTEGER := 0;
BEGIN
    RAISE NOTICE '╔══════════════════════════════════════════════════════════╗';
    RAISE NOTICE '║        DATABASE BEST PRACTICES ASSESSMENT                ║';
    RAISE NOTICE '╚══════════════════════════════════════════════════════════╝';

    -- 1. NAMING CONVENTIONS
    RAISE NOTICE '';
    RAISE NOTICE '1. NAMING CONVENTIONS';
    RAISE NOTICE '─────────────────────';

    -- Snake case tables
    SELECT COUNT(*) INTO v_total FROM information_schema.tables
    WHERE table_schema = 'public' AND table_type = 'BASE TABLE';

    SELECT COUNT(*) INTO v_count FROM information_schema.tables
    WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
    AND table_name ~ '^[a-z][a-z0-9_]*$';

    RAISE NOTICE '   Snake case tables: %/%', v_count, v_total;
    IF v_count = v_total THEN v_score := v_score + 5; END IF;

    -- 2. SCHEMA DESIGN
    RAISE NOTICE '';
    RAISE NOTICE '2. SCHEMA DESIGN';
    RAISE NOTICE '────────────────';

    -- Tables with PK
    SELECT COUNT(*) INTO v_count FROM information_schema.tables t
    WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
    AND EXISTS (
        SELECT 1 FROM information_schema.table_constraints tc
        WHERE tc.table_name = t.table_name
        AND tc.constraint_type = 'PRIMARY KEY'
    );
    RAISE NOTICE '   Tables with PK: %/%', v_count, v_total;
    IF v_count = v_total THEN v_score := v_score + 10; END IF;

    -- FK count
    SELECT COUNT(*) INTO v_count FROM information_schema.table_constraints
    WHERE constraint_type = 'FOREIGN KEY' AND table_schema = 'public';
    RAISE NOTICE '   Foreign Keys defined: %', v_count;
    IF v_count > 0 THEN v_score := v_score + 5; END IF;

    -- 3. PERFORMANCE
    RAISE NOTICE '';
    RAISE NOTICE '3. PERFORMANCE';
    RAISE NOTICE '──────────────';

    SELECT COUNT(*) INTO v_count FROM pg_indexes WHERE schemaname = 'public';
    RAISE NOTICE '   Total Indexes: %', v_count;
    IF v_count >= v_total THEN v_score := v_score + 5; END IF;

    -- 4. SECURITY & MAINTENANCE
    RAISE NOTICE '';
    RAISE NOTICE '4. SECURITY & MAINTENANCE';
    RAISE NOTICE '─────────────────────────';

    -- Audit columns
    SELECT COUNT(DISTINCT table_name) INTO v_count
    FROM information_schema.columns
    WHERE table_schema = 'public'
    AND column_name IN ('created_at', 'updated_at');
    RAISE NOTICE '   Tables with audit columns: %/%', v_count, v_total;
    IF v_count = v_total THEN v_score := v_score + 5; END IF;

    RAISE NOTICE '';
    RAISE NOTICE '══════════════════════════════════════════════════════════';
    RAISE NOTICE 'PRELIMINARY SCORE: %/30 (simplified assessment)', v_score;
    RAISE NOTICE '══════════════════════════════════════════════════════════';
END $$;

Checklist Summary Card

┌─────────────────────────────────────────────────────────────┐
│              DATABASE DESIGN CHECKLIST                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  NAMING (25 pts)                                            │
│  □ Snake_case for tables and columns                        │
│  □ Plural table names (users, orders)                       │
│  □ Descriptive, no abbreviations                            │
│  □ Standard FK naming ({table}_id)                          │
│  □ Boolean prefix (is_, has_, can_)                         │
│                                                             │
│  SCHEMA (25 pts)                                            │
│  □ Every table has primary key                              │
│  □ All relationships have foreign keys                      │
│  □ Appropriate data types                                   │
│  □ NOT NULL on required fields                              │
│  □ CHECK constraints for validation                         │
│                                                             │
│  PERFORMANCE (25 pts)                                       │
│  □ Indexes on foreign keys                                  │
│  □ Indexes on frequently queried columns                    │
│  □ No redundant indexes                                     │
│  □ Appropriate use of composite indexes                     │
│  □ Large data in separate tables                            │
│                                                             │
│  SECURITY & OPS (25 pts)                                    │
│  □ Sensitive data encrypted                                 │
│  □ Audit columns (created_at, updated_at)                   │
│  □ Soft delete where appropriate                            │
│  □ Schema documented                                        │
│  □ Migrations versioned                                     │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  SCORE: ___/100                                             │
│  90-100: Excellent | 70-89: Good | 50-69: Fair | <50: Poor  │
└─────────────────────────────────────────────────────────────┘

Common Anti-Patterns to Avoid

Anti-PatternProblemSolution
God TableOne table with 50+ columnsSplit into related tables
EAV ModelEntity-Attribute-Value everywhereUse proper columns/JSONB
No ConstraintsData integrity relies on appAdd DB-level constraints
Stringly TypedEverything is VARCHARUse proper data types
Magic NumbersStatus codes without meaningUse ENUM or lookup table
No Audit TrailCan't track changesAdd audit columns/triggers
Implicit SchemaNo documentationDocument all tables/columns

On this page