- Add ClickHouse patterns skill - Add PostgreSQL patterns skill - Add SQLite patterns skill - Update backend-developer agent to reference PostgreSQL and SQLite skills - Update go-developer agent to reference ClickHouse, PostgreSQL, and SQLite skills - Update capability-index.yaml with database integration capabilities
9.8 KiB
9.8 KiB
PostgreSQL Patterns Skill
Comprehensive guide to PostgreSQL database patterns and best practices.
Overview
PostgreSQL is a powerful, open-source object-relational database system. This skill covers schema design, indexing, querying, transactions, and performance optimization.
Connection Management
Basic Connection (using pgx driver)
import (
"context"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// ✅ Good: Connection pool with context
func NewPostgreSQLPool(ctx context.Context, connString string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(connString)
if err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
// Customize pool configuration
config.MinConns = 10
config.MaxConns = 100
config.MaxConnLifetime = time.Hour
config.HealthCheckPeriod = time.Minute
config.ConnConfig.ConnectTimeout = 5 * time.Second
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
// Verify connection
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping pool: %w", err)
}
return pool, nil
}
Schema Design
Data Types
-- ✅ Good: Appropriate data types
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
age INTEGER CHECK (age >= 0 AND age <= 150),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
profile JSONB, -- For flexible schema
settings HSTORE -- Key-value store
);
-- Indexes for common queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_profile ON users USING GIN (profile);
Constraints
-- ✅ Good: Using constraints for data integrity
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
order_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')),
total_amount DECIMAL(10, 2) NOT NULL CHECK (total_amount >= 0),
order_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
shipped_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
metadata JSONB
);
-- Partial index for active orders
CREATE INDEX idx_orders_active ON orders(id)
WHERE status IN ('pending', 'processing', 'shipped');
-- Expression index for case-insensitive search
CREATE INDEX idx_users_email_lower ON users((LOWER(email)));
Query Patterns
Basic CRUD
// ✅ Good: Using pgx with context and proper error handling
func GetUserByID(ctx context.Context, pool *pgxpool.Pool, id UUID) (*User, error) {
var user User
err := pool.QueryRow(ctx, `
SELECT id, email, first_name, last_name, age, created_at, updated_at, is_active
FROM users
WHERE id = $1
`, id).Scan(
&user.ID, &user.Email, &user.FirstName, &user.LastName,
&user.Age, &user.CreatedAt, &user.UpdatedAt, &user.IsActive,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("get user: %w", err)
}
return &user, nil
}
func CreateUser(ctx context.Context, pool *pgxpool.Pool, user *User) (*User, error) {
err := pool.QueryRow(ctx, `
INSERT INTO users (email, password_hash, first_name, last_name, age)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at, updated_at
`, user.Email, user.PasswordHash, user.FirstName, user.LastName, user.Age).
Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
Transactions
// ✅ Good: Using tx with context and proper rollback
func TransferFunds(ctx context.Context, pool *pgxpool.Pool, fromID, toID UUID, amount Decimal) error {
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
// Ensure rollback on error
defer func() {
if err != nil {
tx.Rollback(ctx)
}
}()
// Check sender balance
var fromBalance Decimal
err := tx.QueryRow(ctx, `
SELECT balance FROM accounts WHERE user_id = $1 FOR UPDATE
`, fromID).Scan(&fromBalance)
if err != nil {
return fmt.Errorf("get sender balance: %w", err)
}
if fromBalance < amount {
return ErrInsufficientFunds
}
// Update sender
_, err = tx.Exec(ctx, `
UPDATE accounts SET balance = balance - $1 WHERE user_id = $2
`, amount, fromID)
if err != nil {
return fmt.Errorf("update sender: %w", err)
}
// Update receiver
_, err = tx.Exec(ctx, `
UPDATE accounts SET balance = balance + $1 WHERE user_id = $2
`, amount, toID)
if err != nil {
return fmt.Errorf("update receiver: %w", err)
}
// Commit transaction
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit transaction: %w", err)
}
return nil
}
Performance Optimization
Indexing Strategies
-- ✅ Good: B-tree index for equality and range queries
CREATE INDEX idx_orders_date ON orders(order_date);
-- ✅ Good: GIN index for JSONB containment queries
CREATE INDEX idx_orders_metadata ON orders USING GIN (metadata);
-- ✅ Good: BRIN index for large tables with natural ordering
CREATE INDEX idx_large_table_time ON large_table USING BRIN (created_at);
-- ✅ Good: Partial index for infrequent values
CREATE INDEX idx_orders_cancelled ON orders(id)
WHERE status = 'cancelled';
-- ✅ Good: Covering index (include columns)
CREATE INDEX idx_orders_covering ON orders(user_id)
INCLUDE (order_number, total_amount, order_date);
Query Optimization
-- ✅ Good: Using EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT o.id, o.total_amount, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days'
AND o.status = 'shipped'
ORDER BY o.order_date DESC
LIMIT 100;
-- ✅ Good: Using CTE for readability and optimization
WITH recent_orders AS (
SELECT id, user_id, total_amount, order_date
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
AND status = 'shipped'
)
SELECT o.id, o.total_amount, u.email
FROM recent_orders o
JOIN users u ON o.user_id = u.id
ORDER BY o.order_date DESC
LIMIT 100;
Connection Pooling
// ✅ Good: Using pgxpool for connection pooling
// Already demonstrated in NewPostgreSQLPool function
// ✅ Good: Using prepared statements implicitly through pgx
// pgx automatically prepares and caches statements
Testing
Testcontainers for PostgreSQL
// ✅ Good: Using testcontrollers for integration tests
func setupPostgreSQL(t *testing.T) *pgxpool.Pool {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:15-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "test",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
t.Cleanup(func() {
container.Terminate(ctx)
})
host, err := container.Host(ctx)
require.NoError(t, err)
port, err := container.MappedPort(ctx, "5432")
require.NoError(t, err)
connString := fmt.Sprintf("host=%s port=%s user=test password=test dbname=test sslmode=disable",
host, port.Port())
pool, err := pgxpool.New(ctx, connString)
require.NoError(t, err)
return pool
}
Best Practices
❌ Bad Patterns
-- ❌ Bad: Using SELECT * in production
SELECT * FROM orders WHERE order_date > NOW() - INTERVAL '7 days'
-- ❌ Bad: Not using parameterized queries (SQL injection risk)
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
-- ❌ Bad: No connection pooling
// Creating new connection for every query
✅ Good Patterns
-- ✅ Good: Explicit column selection
SELECT id, order_number, total_amount, order_date
FROM orders
WHERE order_date > NOW() - INTERVAL '7 days'
-- ✅ Good: Using parameterized queries
pool.QueryRow(ctx, "SELECT * FROM users WHERE email = $1", email)
-- ✅ Good: Using connection pool
// Reuse pool across multiple requests
Common Operations
Backup and Restore
# ✅ Good: Logical backup with pg_dump
pg_dump -h localhost -U test -d test > backup.sql
# Restore
psql -h localhost -U test -d test < backup.sql
# ✅ Good: Custom format with compression
pg_dump -Fc -z 9 -f backup.dump -U test test
pg_restore -U test -d test backup.dump
Monitoring
-- ✅ Good: Checking active queries
SELECT pid, age(clock_timestamp(), query_start), state, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start DESC;
-- ✅ Good: Index usage statistics
SELECT
schemaname,
tablename,
indexname,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_tup_read DESC;
-- ✅ Good: Table size
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;