| name | postgres-database |
| description | PostgreSQL schema design, indexing, and query patterns. Use when designing database schemas, writing migrations, optimizing queries, or working with any PostgreSQL database. |
PostgreSQL Database Patterns
Schema Design Principles
- Always add
created_at, updated_at timestamps to every table
- Use
UUID or BIGSERIAL for primary keys (UUID for distributed systems)
- Use foreign key constraints (never orphaned rows)
- Normalize to 3NF, then denormalize only for proven performance needs
- Soft delete with
deleted_at TIMESTAMP NULL instead of hard delete
Essential Indexes
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);
CREATE INDEX idx_users_active_email ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_posts_fts ON posts USING GIN(to_tsvector('english', title || ' ' || body));
Migration Pattern (Alembic)
def upgrade():
op.create_table('posts',
sa.Column('id', sa.BigInteger(), primary_key=True),
sa.Column('user_id', sa.BigInteger(), sa.ForeignKey('users.id'), nullable=False),
sa.Column('title', sa.String(255), nullable=False),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(timezone=True), onupdate=sa.func.now()),
)
op.create_index('idx_posts_user_id', 'posts', ['user_id'])
def downgrade():
op.drop_table('posts')
Query Patterns
SELECT * FROM posts
WHERE created_at < $1
ORDER BY created_at DESC
LIMIT 20;
SELECT u.*, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id;
INSERT INTO settings(user_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value;
Rules
- NEVER use SELECT * in production queries — list columns explicitly
- Always use parameterized queries — never f-string SQL (SQL injection risk)
- Use connection pooling (PgBouncer or SQLAlchemy pool_size)
- Run EXPLAIN ANALYZE on slow queries before adding indexes
- Use transactions for multi-table writes
- Vacuum and analyze after bulk operations
- Set statement_timeout to prevent runaway queries