sql-development
Write, format, review, and optimize relational SQL queries following standard conventions. Use when composing complex SELECT queries with JOINs and CTEs, designing normalized schemas with primary/fore
SKILL.mdskills/sql-development/SKILL.md
GitHubRelational SQL Development and Query Design
Structured Query Language (SQL) is the foundational language of relational database management systems. Writing robust SQL requires clear formatting, precise JOIN semantics, proper use of Common Table Expressions (CTEs), strict referential integrity constraints, and disciplined transaction management.
Core Rules
- Always parameterize queries: Never concatenate user input into SQL strings. Use prepared statements and bind parameters (
$1,?, or:param). - Capitalize SQL keywords: Write
SELECT,FROM,WHERE,JOIN,GROUP BY,ORDER BYin uppercase for visual clarity and scanning speed. - Always use explicit table aliases in multi-table queries: Qualify every column with its table name or alias (e.g.
u.email,o.total_amount) to prevent ambiguous column errors. - Prefer CTEs (
WITHclauses) over deeply nested subqueries: CTEs break complex queries into readable, linear data-transformation steps. - Always specify explicit
JOINtypes: Never use implicit comma joins (FROM users, orders WHERE ...). Use explicitINNER JOIN,LEFT JOIN, etc. - Enforce data integrity at the database layer: Use
NOT NULL,CHECK,UNIQUE, andFOREIGN KEYconstraints rather than relying solely on application-level validation.
1. Clean Query Structure & Formatting
WITH active_customers AS (
SELECT
u.id AS user_id,
u.email,
u.created_at
FROM users AS u
WHERE u.status = 'ACTIVE'
AND u.deleted_at IS NULL
),
customer_order_aggregates AS (
SELECT
o.user_id,
COUNT(o.id) AS total_orders,
SUM(o.amount_cents) AS total_spend_cents,
MAX(o.ordered_at) AS latest_order_date
FROM orders AS o
WHERE o.status = 'COMPLETED'
GROUP BY o.user_id
)
SELECT
c.user_id,
c.email,
COALESCE(a.total_orders, 0) AS total_orders,
COALESCE(a.total_spend_cents, 0) AS total_spend_cents,
a.latest_order_date
FROM active_customers AS c
LEFT JOIN customer_order_aggregates AS a
ON c.user_id = a.user_id
ORDER BY total_spend_cents DESC, c.user_id ASC
LIMIT 50;
2. Choosing the Correct JOIN Type
| Join Type | Behavior | When to Use |
|---|---|---|
INNER JOIN |
Returns rows only when keys match in both tables | Filtering results where related entity must exist (e.g. Orders with Customers) |
LEFT JOIN |
Returns all rows from left table, with NULLs for unmatched right rows | Preserving parent entities even if they have zero children (e.g. Users without orders) |
FULL OUTER JOIN |
Returns rows when there is a match in either table | Reconciling and identifying discrepancies across two data sets |
CROSS JOIN |
Cartesian product (every row combined with every row) | Generating grids, permutations, or date dimension series |
3. Schema Design and Constraints
Enforce business rules directly in table definitions:
CREATE TABLE accounts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE RESTRICT,
account_number VARCHAR(32) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'EUR',
balance_cents BIGINT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Business constraints
CONSTRAINT chk_valid_status CHECK (status IN ('ACTIVE', 'SUSPENDED', 'CLOSED')),
CONSTRAINT chk_positive_balance CHECK (balance_cents >= 0),
CONSTRAINT uq_org_account_num UNIQUE (organization_id, account_number)
);
-- Indexing foreign keys (RDBMS does not automatically index FK columns!)
CREATE INDEX idx_accounts_organization_id ON accounts (organization_id);
4. ACID Transactions and Isolation
Transactions guarantee that multiple statements execute atomically:
BEGIN;
-- Deduct from source account
UPDATE accounts
SET balance_cents = balance_cents - 5000
WHERE id = 101 AND balance_cents >= 5000;
-- Ensure deduction occurred (check rows affected in application)
-- Add to target account
UPDATE accounts
SET balance_cents = balance_cents + 5000
WHERE id = 202;
-- Record audit ledger entry
INSERT INTO transfer_ledger (from_account_id, to_account_id, amount_cents)
VALUES (101, 202, 5000);
COMMIT;
5. Common Mistakes and Anti-Patterns
| Anti-Pattern | Performance / Bug Risk | Correct Practice |
|---|---|---|
SELECT * in application queries |
Unnecessary I/O, breaks when column order changes, blocks index-only scans | Explicitly select required columns: SELECT u.id, u.email |
Missing WHERE in UPDATE/DELETE |
Accidental catastrophic loss of all table data | Always double-check WHERE clause before execution |
Comma joins: FROM a, b WHERE a.id = b.a_id |
Accidental Cartesian products when WHERE conditions are omitted | Always use explicit JOIN ... ON ... |
Functions on indexed columns in WHERE |
Prevents index usage: WHERE YEAR(created_at) = 2026 |
Use range: WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01' |
Storing financial amounts as FLOAT |
Floating-point rounding inaccuracies | Use BIGINT (in cents) or NUMERIC(12, 2) |
6. Verification Checklist
- All dynamic variables are bound via parameters, not string concatenation.
- Columns are qualified with table names/aliases.
- No
SELECT *in production queries. - Foreign keys have matching B-tree indexes.
- Multi-step updates wrapped in explicit
BEGIN ... COMMITblocks.