# Postgresql Patterns

> PostgreSQL patterns, optimization, and best practices for production

- **Type:** Skill
- **Install:** `agentstack add skill-jonathan0823-opencode-config-postgresql-patterns`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Jonathan0823](https://agentstack.voostack.com/s/jonathan0823)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Jonathan0823](https://github.com/Jonathan0823)
- **Source:** https://github.com/Jonathan0823/opencode-config/tree/main/skills/postgresql-patterns

## Install

```sh
agentstack add skill-jonathan0823-opencode-config-postgresql-patterns
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# PostgreSQL Patterns Skill

## Overview

This skill provides guidelines for designing, querying, and optimizing PostgreSQL databases for production applications.

## Schema Design

### 1. Table Design

```sql
-- DO: Use appropriate data types
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL UNIQUE,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(100),
    age INTEGER CHECK (age >= 0 AND age = 0),
    status VARCHAR(20) DEFAULT 'pending' 
        CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Many-to-Many with junction table
CREATE TABLE products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10, 2) NOT NULL CHECK (price >= 0)
);

CREATE TABLE order_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    price_at_time DECIMAL(10, 2) NOT NULL,
    UNIQUE(order_id, product_id) -- Prevent duplicates
);

-- Self-referential (hierarchical)
CREATE TABLE categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(100) NOT NULL,
    parent_id UUID REFERENCES categories(id) ON DELETE CASCADE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```

### 3. Indexes

```sql
-- DO: Index frequently queried columns
CREATE INDEX idx_users_email ON users(email);

-- DO: Composite indexes for multi-column queries
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- DO: Partial indexes for filtered queries
CREATE INDEX idx_orders_pending ON orders(created_at) 
    WHERE status = 'pending';

-- DO: Expression indexes
CREATE INDEX idx_users_email_lower ON users(LOWER(email));

-- DO: GIN index for JSONB/array
CREATE INDEX idx_users_metadata ON users USING GIN(metadata);

-- DO: Index for text search
CREATE INDEX idx_products_name_trgm ON products 
    USING gin(name gin_trgm_ops);

-- DO: BRIN index for large, naturally ordered tables
CREATE INDEX idx_events_created_brin ON events 
    USING BRIN(created_at) WITH (pages_per_range = 128);

-- DON'T: Over-index
-- Every index slows down writes
-- Monitor with: SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;
```

## Query Patterns

### 1. Select Queries

```sql
-- DO: Select only needed columns
SELECT id, email, full_name FROM users WHERE id = 'uuid';

-- DON'T: SELECT * in production
SELECT * FROM users; -- Avoid this

-- DO: Use LIMIT with large datasets
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20;

-- DO: Use OFFSET for pagination (consider keyset for large datasets)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 40;

-- DO: Use keyset pagination for better performance
SELECT * FROM orders 
WHERE created_at  0
ORDER BY total_spent DESC;

-- DO: Use CTEs for complex queries
WITH user_stats AS (
    SELECT 
        user_id,
        COUNT(*) as order_count,
        SUM(total_amount) as total_revenue
    FROM orders
    WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
    GROUP BY user_id
)
SELECT 
    u.email,
    us.order_count,
    us.total_revenue
FROM users u
JOIN user_stats us ON u.id = us.user_id
WHERE us.total_revenue > 1000;
```

### 3. Insert/Update/Delete

```sql
-- DO: Use INSERT with ON CONFLICT (upsert)
INSERT INTO users (email, username, password_hash)
VALUES ('test@example.com', 'testuser', 'hash')
ON CONFLICT (email) DO UPDATE SET
    password_hash = EXCLUDED.password_hash,
    updated_at = CURRENT_TIMESTAMP
RETURNING id;

-- DO: Batch inserts
INSERT INTO order_items (order_id, product_id, quantity, price_at_time)
VALUES 
    ('order-1', 'product-1', 2, 29.99),
    ('order-1', 'product-2', 1, 49.99),
    ('order-1', 'product-3', 3, 9.99);

-- DO: Use UPDATE with RETURNING
UPDATE users 
SET last_login = CURRENT_TIMESTAMP
WHERE id = 'user-id'
RETURNING *;

-- DO: Use DELETE with LIMIT and RETURNING
DELETE FROM temp_logs 
WHERE created_at >'user_id'));

-- Query JSONB
SELECT * FROM events 
WHERE payload @> '{"event": "login"}';

-- Extract values
SELECT 
    event_type,
    payload->>'user_id' as user_id,
    payload->'details'->>'ip' as ip_address
FROM events
WHERE created_at > CURRENT_DATE - INTERVAL '1 day';

-- Update JSONB
UPDATE events 
SET payload = payload || '{"processed": true}'::jsonb
WHERE id = 'event-id';
```

### 3. Window Functions

```sql
-- Running totals
SELECT 
    user_id,
    order_date,
    amount,
    SUM(amount) OVER (
        PARTITION BY user_id 
        ORDER BY order_date
    ) as running_total
FROM orders;

-- Ranking
SELECT 
    product_id,
    sales_amount,
    RANK() OVER (ORDER BY sales_amount DESC) as rank,
    DENSE_RANK() OVER (ORDER BY sales_amount DESC) as dense_rank
FROM product_sales;

-- Lag/Lead for comparisons
SELECT 
    user_id,
    login_date,
    LAG(login_date) OVER (PARTITION BY user_id ORDER BY login_date) as prev_login,
    login_date - LAG(login_date) OVER (PARTITION BY user_id ORDER BY login_date) as days_between
FROM user_logins;
```

## Optimization

### 1. Query Analysis

```sql
-- Analyze query execution
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';

-- Check for seq scans
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE user_id = 'uuid';

-- Look for high-cost operations
-- - Seq Scan on large tables (should be index scan)
-- - Nested Loop with high row counts (consider hash join)
-- - Sort operations (consider index for ordering)
```

### 2. Performance Tips

```sql
-- DO: Use prepared statements (automatic in most ORMs)
PREPARE get_user (UUID) AS
    SELECT * FROM users WHERE id = $1;

EXECUTE get_user('uuid');

-- DO: Vacuum and analyze regularly
VACUUM ANALYZE users;

-- DO: Partition large tables
CREATE TABLE events_2024 PARTITION OF events
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

-- DO: Use connection pooling (PgBouncer)
-- DO: Set appropriate work_mem for complex queries
SET work_mem = '256MB';
```

### 3. Monitoring Queries

```sql
-- Find slow queries
SELECT 
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;

-- Check index usage
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
```

## Migration Patterns

### 1. Zero-Downtime Migrations

```sql
-- Phase 1: Add column (nullable)
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);

-- Phase 2: Dual-write in application code
-- Write to both first_name/last_name AND full_name

-- Phase 3: Backfill data
UPDATE users 
SET full_name = CONCAT(first_name, ' ', last_name)
WHERE full_name IS NULL;

-- Phase 4: Make NOT NULL after backfill
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;

-- Phase 5: Update application to read from full_name only

-- Phase 6: Remove old columns
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;
```

### 2. Transaction Safety

```sql
-- DO: Use transactions for related changes
BEGIN;

INSERT INTO orders (user_id, total_amount) 
VALUES ('user-uuid', 99.99)
RETURNING id;

INSERT INTO order_items (order_id, product_id, quantity)
VALUES ('order-uuid', 'product-uuid', 1);

UPDATE inventory 
SET quantity = quantity - 1 
WHERE product_id = 'product-uuid';

COMMIT;

-- DO: Use savepoints for partial rollback
BEGIN;

SAVEPOINT before_payment;

-- Try payment processing
-- If fails:
ROLLBACK TO SAVEPOINT before_payment;

-- Continue with other operations

COMMIT;
```

## When to Use

Use this skill when:
- Designing PostgreSQL schemas
- Writing complex SQL queries
- Optimizing query performance
- Implementing migrations
- Working with JSONB data
- Setting up full-text search
- Analyzing query performance

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Jonathan0823](https://github.com/Jonathan0823)
- **Source:** [Jonathan0823/opencode-config](https://github.com/Jonathan0823/opencode-config)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-jonathan0823-opencode-config-postgresql-patterns
- Seller: https://agentstack.voostack.com/s/jonathan0823
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
