agentsclimarketplace

Sql patterns

Skill Tibsfox/gsd-skill-creator/examples/skills/patterns/sql-patterns

Introduces a comprehensive agent-based framework for guided software development (GSD)

Install
npx -y skills add Tibsfox/gsd-skill-creator --skill sql-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

What its author says it does

Copied from the file, not written here

Provides SQL best practices for query optimization, schema design, migrations, transactions, and indexing. Use when writing database queries, designing schemas, creating migrations, or when user mentions 'SQL', 'database', 'query', 'schema', 'migration', 'index', 'transaction'.

SKILL.md

12.8 KB, as published. Nobody here has run it

SQL Patterns

Best practices for writing safe, performant, and maintainable SQL. All examples use parameterized queries to prevent SQL injection.

Security-First Principles

Every query MUST use parameterized inputs. Never concatenate user input into SQL strings.

-- DANGEROUS: SQL injection vulnerability
SELECT * FROM users WHERE email = '" + email + "';

-- SAFE: Parameterized query
SELECT id, name, email FROM users WHERE email = $1;

Destructive Operation Safety

Never run destructive operations without a WHERE clause and explicit confirmation.

OperationRisk LevelRequired Safeguards
DROP TABLECRITICALBackup first, use IF EXISTS, require approval
TRUNCATE TABLECRITICALBackup first, confirm table name, check foreign keys
DELETE without WHERECRITICALAlways add WHERE clause, run SELECT first to preview
UPDATE without WHERECRITICALAlways add WHERE clause, run SELECT first to preview
ALTER TABLE DROP COLUMNHIGHBackup first, check dependencies, use migration
DROP INDEXMEDIUMVerify query plans won't degrade

Always preview before modifying:

-- Step 1: Preview what will be affected
SELECT id, email, status FROM users WHERE status = 'inactive';

-- Step 2: Verify the count
SELECT COUNT(*) FROM users WHERE status = 'inactive';

-- Step 3: Only then modify (parameterized)
DELETE FROM users WHERE status = $1;  -- $1 = 'inactive'

Query Optimization

Using EXPLAIN

Always analyze query plans before deploying queries that touch large tables.

-- Basic explain
EXPLAIN SELECT id, name FROM users WHERE email = $1;

-- With execution stats (PostgreSQL)
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.id, u.name, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at > $1;

Reading EXPLAIN Output

TermMeaningAction
Seq ScanFull table scanAdd an index on filter columns
Index ScanUsing an indexGood -- verify it's the right index
Index Only ScanAnswered from index aloneBest case for read queries
Nested LoopRow-by-row joinFine for small result sets, bad for large
Hash JoinHash-based joinGood for medium-large joins
SortIn-memory or disk sortAdd index if used with ORDER BY/DISTINCT
Bitmap Heap ScanIndex + table lookupNormal for multi-condition queries

Selective Column Queries

-- BAD: SELECT * fetches unnecessary data, breaks if schema changes
SELECT * FROM users WHERE status = $1;

-- GOOD: Explicit columns, only what you need
SELECT id, name, email, status FROM users WHERE status = $1;

Pagination

-- BAD: OFFSET-based pagination degrades with large offsets
SELECT id, name FROM products ORDER BY id LIMIT 20 OFFSET 10000;

-- GOOD: Cursor-based pagination using the last seen ID
SELECT id, name FROM products
WHERE id > $1
ORDER BY id
LIMIT 20;

Indexing Guide

Index Types

TypeUse CaseExample
B-tree (default)Equality, range, sorting, LIKE 'prefix%'CREATE INDEX idx_email ON users (email)
HashEquality only (rare, B-tree usually better)CREATE INDEX idx_code ON codes USING hash (code)
GINFull-text search, JSONB, arraysCREATE INDEX idx_tags ON posts USING gin (tags)
GiSTGeometric, range types, full-textCREATE INDEX idx_location ON places USING gist (coords)
PartialSubset of rows matching a conditionCREATE INDEX idx_active ON users (email) WHERE active = true

Composite Indexes

Column order matters. Place equality columns first, then range columns.

-- Query pattern: WHERE status = $1 AND created_at > $2
-- Index matches the query pattern: equality first, range second
CREATE INDEX idx_status_created ON orders (status, created_at);

-- This index will NOT help the query above efficiently:
CREATE INDEX idx_created_status ON orders (created_at, status);

When to Add Indexes

SignalAction
Seq Scan on large table in EXPLAINAdd index on filter/join columns
Slow ORDER BYAdd index on sort columns
Slow JOINAdd index on foreign key columns
Frequent WHERE on same columnsAdd composite index
Low selectivity column (e.g., boolean)Partial index instead of full

When NOT to Index

  • Tables with fewer than ~1,000 rows (seq scan is faster)
  • Columns that are rarely queried
  • Columns with very low cardinality on large tables (unless partial index)
  • Write-heavy tables where index maintenance cost exceeds read benefit

Schema Design

Normalization Quick Reference

Normal FormRuleExample Violation
1NFAtomic values, no repeating groupstags: "a,b,c" in one column
2NFNo partial dependencies on composite keyNon-key column depends on part of key
3NFNo transitive dependencieszip_code determines city in users

When to Denormalize

Denormalize only when you have measured performance problems and understand the tradeoff.

ScenarioDenormalization StrategyTradeoff
Frequent join of 3+ tablesMaterialized viewStale data, refresh cost
Read-heavy reportingSummary tableWrite complexity, eventual consistency
Display name with foreign keyCache column + triggerMust keep in sync
Nested JSON responsesJSONB columnHarder to query, larger rows

Foreign Key Design

-- Always define foreign keys with explicit actions
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Index foreign keys (PostgreSQL does NOT auto-index them)
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_product_id ON orders (product_id);
ON DELETEBehaviorUse When
RESTRICTBlock delete if referencedParent record must exist (orders -> users)
CASCADEDelete children tooChildren are meaningless without parent (comments -> post)
SET NULLSet FK to NULLRelationship is optional (assigned_to -> users)
NO ACTIONLike RESTRICT but deferredChecking at transaction commit

Migration Best Practices

Reversible Migrations

Every migration should have a rollback strategy.

-- Migration: up
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Migration: down (rollback)
ALTER TABLE users DROP COLUMN phone;

Zero-Downtime Migration Patterns

Adding a column (safe):

-- Step 1: Add nullable column (no lock, no rewrite)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Step 2: Backfill in batches (avoid locking entire table)
UPDATE users SET phone = $1 WHERE id BETWEEN $2 AND $3;

-- Step 3: Add NOT NULL constraint after backfill (if needed)
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

Renaming a column (requires coordination):

-- Step 1: Add new column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

-- Step 2: Backfill (batched)
UPDATE users SET full_name = name WHERE id BETWEEN $1 AND $2;

-- Step 3: Application reads from both, writes to both
-- Step 4: Switch application to new column only
-- Step 5: Drop old column in later migration
ALTER TABLE users DROP COLUMN name;

Migration Checklist

  • Migration has a rollback script
  • Large table changes use batched updates
  • No ALTER TABLE ... ADD COLUMN ... DEFAULT on large tables (pre-PG 11)
  • Index creation uses CONCURRENTLY on production tables
  • Foreign key columns are indexed
  • Migration tested on a copy of production data
  • No data loss on rollback
-- SAFE: Concurrent index creation (doesn't lock writes)
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);

-- UNSAFE: Locks the table during creation
CREATE INDEX idx_users_email ON users (email);

Transaction Patterns

Isolation Levels

LevelDirty ReadNon-Repeatable ReadPhantom ReadUse Case
READ UNCOMMITTEDPossiblePossiblePossibleAlmost never (analytics only)
READ COMMITTEDNoPossiblePossibleDefault for most apps
REPEATABLE READNoNoPossible (not in PG)Financial calculations
SERIALIZABLENoNoNoCritical consistency (transfers)
-- Set isolation level for a transaction
BEGIN ISOLATION LEVEL SERIALIZABLE;

UPDATE accounts SET balance = balance - $1 WHERE id = $2;
UPDATE accounts SET balance = balance + $1 WHERE id = $3;

COMMIT;

Avoiding Deadlocks

Deadlocks occur when two transactions wait for each other's locks.

-- BAD: Transaction A locks row 1 then 2; Transaction B locks row 2 then 1
-- Transaction A:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- Transaction B (concurrent):
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- DEADLOCK!

-- GOOD: Always lock rows in consistent order (by ID)
-- Both transactions lock id=1 first, then id=2
BEGIN;
SELECT id FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - $1 WHERE id = 1;
UPDATE accounts SET balance = balance + $1 WHERE id = 2;
COMMIT;

Advisory Locks

Use for application-level mutual exclusion without table locks.

-- Acquire an advisory lock (blocks until available)
SELECT pg_advisory_lock($1);

-- Do exclusive work...

-- Release the lock
SELECT pg_advisory_unlock($1);

-- Non-blocking variant (returns true/false)
SELECT pg_try_advisory_lock($1);

Anti-Patterns

Anti-PatternProblemFix
SELECT *Fetches unneeded data, breaks on schema changeList explicit columns
N+1 queriesLoop fires one query per rowUse JOIN or batch IN query
String concatenation in SQLSQL injection vulnerabilityUse parameterized queries ($1, ?)
Missing indexes on foreign keysSlow JOINs and cascading deletesAdd index on every FK column
DELETE FROM table (no WHERE)Deletes ALL rowsAlways include WHERE clause
LIKE '%term%'Cannot use B-tree index, full scanUse full-text search (GIN index)
Storing CSV in one columnViolates 1NF, impossible to queryUse a junction/join table
ORDER BY RANDOM()Scans and sorts entire tableUse TABLESAMPLE or app-level random
UUID v4 as primary keyFragmented B-tree insertsUse UUID v7 (time-ordered) or SERIAL
N+1 in application codefor user in users: query(user.id)WHERE id IN ($1, $2, ...)
COUNT(*) for existence checkCounts all rowsSELECT EXISTS (SELECT 1 FROM ... LIMIT 1)

Parameterized Query Examples

Node.js (pg)

// SAFE: Parameterized query
const result = await pool.query(
  'SELECT id, name, email FROM users WHERE email = $1 AND active = $2',
  [email, true]
);

// SAFE: INSERT with RETURNING
const { rows } = await pool.query(
  'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id',
  [name, email]
);

Python (psycopg2)

# SAFE: Parameterized query
cursor.execute(
    "SELECT id, name, email FROM users WHERE email = %s AND active = %s",
    (email, True)
)

# SAFE: Batch insert
from psycopg2.extras import execute_values
execute_values(
    cursor,
    "INSERT INTO users (name, email) VALUES %s",
    [(name, email) for name, email in user_data]
)

Go (database/sql)

// SAFE: Parameterized query
row := db.QueryRowContext(ctx,
    "SELECT id, name, email FROM users WHERE email = $1",
    email,
)

Quick Reference: Safe Defaults

ConcernDefault Choice
Primary keySERIAL or BIGSERIAL (UUID v7 if distributed)
TimestampsTIMESTAMPTZ (always with timezone)
Money/currencyNUMERIC(19,4) (never FLOAT)
String storageVARCHAR(n) with reasonable limit or TEXT
BooleanBOOLEAN (not integer flags)
Foreign keysAlways define with ON DELETE behavior
IndexesB-tree default, GIN for JSONB/arrays/full-text
TransactionsREAD COMMITTED default, SERIALIZABLE for money
MigrationsAlways reversible, always batched for large tables

Gives 3 of the 12 instructions most databases sql skills give

Counted across 589 of the 662 authors here whose files we hold, read 2026-08-06

  • use parameterized querieshere, and in 36 of 589, across 32 files
  • use timestamptz for timestampsin 30 of 589, across 12 files
  • create indexes concurrentlyhere, and in 29 of 589, across 23 files
  • index foreign keyshere, and in 28 of 589, across 17 files
  • use numeric type for moneyin 25 of 589, across 8 files
  • select only required columnsin 24 of 589, across 19 files
  • use cursor pagination instead of OFFSETin 23 of 589, across 15 files
  • add indexes manually on foreign key columnsin 22 of 589, across 11 files
  • read individual rule files for detailed explanationsin 18 of 589, across 4 files
  • configure connection poolingin 18 of 589, across 16 files
  • put equality columns before range columns in indexesin 17 of 589, across 9 files
  • normalize to third normal formin 17 of 589, across 8 files

Said here and by no other author read

  • include a WHERE clause in destructive operations
  • preview affected rows before modifying data
  • analyze query plans before deploying large queries

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.