Write SQL queries from natural language requirements with proper syntax, structure, and best practices.
Trigger Conditions
Activate this skill when the user:
- Says "write a query", "create SQL", "generate SQL"
- Describes data they want to retrieve
- Asks for INSERT, UPDATE, DELETE statements
- Needs DDL (CREATE, ALTER, DROP) statements
- Provides a schema and asks for queries against it
Query Writing Workflow
1. UNDERSTAND -> Parse requirements, identify tables/columns needed
2. IDENTIFY -> Determine query type (SELECT, INSERT, UPDATE, etc.)
3. STRUCTURE -> Plan JOINs, subqueries, CTEs as needed
4. WRITE -> Generate syntactically correct SQL
5. VALIDATE -> Check for common issues
6. OPTIMIZE -> Suggest indexes or improvements if relevantPre-Flight Checklist
Before writing any query:
- [ ] Identified the target database dialect
- [ ] Understood the table schema (columns, types, relationships)
- [ ] Clarified any ambiguous requirements
- [ ] Determined if query needs to handle NULLs
- [ ] Identified any performance considerationsSELECT Query Patterns
Basic SELECT
SELECT column1, column2, column3
FROM table_name
WHERE condition
ORDER BY column1;SELECT with JOINs
SELECT
a.column1,
b.column2,
c.column3
FROM table_a a
INNER JOIN table_b b ON a.id = b.a_id
LEFT JOIN table_c c ON b.id = c.b_id
WHERE a.status = 'active'
ORDER BY a.created_at DESC;SELECT with Aggregation
SELECT
category,
COUNT(*) AS total_count,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount
FROM transactions
WHERE transaction_date >= '2024-01-01'
GROUP BY category
HAVING SUM(amount) > 1000
ORDER BY total_amount DESC;SELECT with Subquery
SELECT *
FROM employees
WHERE department_id IN (
SELECT id
FROM departments
WHERE budget > 100000
);SELECT with CTE
WITH active_customers AS (
SELECT customer_id, SUM(order_total) AS total_spent
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '1 year'
GROUP BY customer_id
),
customer_tiers AS (
SELECT
customer_id,
total_spent,
CASE
WHEN total_spent >= 10000 THEN 'Gold'
WHEN total_spent >= 5000 THEN 'Silver'
ELSE 'Bronze'
END AS tier
FROM active_customers
)
SELECT c.name, c.email, ct.tier, ct.total_spent
FROM customers c
JOIN customer_tiers ct ON c.id = ct.customer_id
ORDER BY ct.total_spent DESC;INSERT Patterns
Single Row Insert
INSERT INTO table_name (column1, column2, column3)
VALUES ('value1', 'value2', 'value3');Multi-Row Insert
INSERT INTO table_name (column1, column2, column3)
VALUES
('value1a', 'value2a', 'value3a'),
('value1b', 'value2b', 'value3b'),
('value1c', 'value2c', 'value3c');Insert from SELECT
INSERT INTO archive_table (id, name, created_at)
SELECT id, name, created_at
FROM source_table
WHERE created_at < '2023-01-01';Insert with Returning (PostgreSQL)
INSERT INTO users (name, email)
VALUES ('John Doe', 'john@example.com')
RETURNING id, created_at;Upsert / Insert or Update
PostgreSQL:
INSERT INTO products (sku, name, price)
VALUES ('ABC123', 'Widget', 29.99)
ON CONFLICT (sku)
DO UPDATE SET
name = EXCLUDED.name,
price = EXCLUDED.price,
updated_at = CURRENT_TIMESTAMP;MySQL:
INSERT INTO products (sku, name, price)
VALUES ('ABC123', 'Widget', 29.99)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price),
updated_at = CURRENT_TIMESTAMP;SQL Server:
MERGE INTO products AS target
USING (VALUES ('ABC123', 'Widget', 29.99)) AS source (sku, name, price)
ON target.sku = source.sku
WHEN MATCHED THEN
UPDATE SET name = source.name, price = source.price
WHEN NOT MATCHED THEN
INSERT (sku, name, price) VALUES (source.sku, source.name, source.price);UPDATE Patterns
Basic Update
UPDATE table_name
SET column1 = 'new_value',
column2 = column2 + 1,
updated_at = CURRENT_TIMESTAMP
WHERE id = 123;Update with JOIN
PostgreSQL:
UPDATE orders o
SET status = 'shipped',
shipped_at = CURRENT_TIMESTAMP
FROM shipments s
WHERE s.order_id = o.id
AND s.status = 'delivered';MySQL:
UPDATE orders o
JOIN shipments s ON s.order_id = o.id
SET o.status = 'shipped',
o.shipped_at = CURRENT_TIMESTAMP
WHERE s.status = 'delivered';SQL Server:
UPDATE o
SET status = 'shipped',
shipped_at = GETDATE()
FROM orders o
JOIN shipments s ON s.order_id = o.id
WHERE s.status = 'delivered';Update with Subquery
UPDATE products
SET price = price * 1.10
WHERE category_id IN (
SELECT id FROM categories WHERE name = 'Electronics'
);DELETE Patterns
Basic Delete
DELETE FROM table_name
WHERE condition;Delete with JOIN
PostgreSQL:
DELETE FROM order_items oi
USING orders o
WHERE oi.order_id = o.id
AND o.status = 'cancelled';MySQL:
DELETE oi
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
WHERE o.status = 'cancelled';Soft Delete Pattern
UPDATE records
SET deleted_at = CURRENT_TIMESTAMP,
deleted_by = 'user_id'
WHERE id = 123;Truncate (Remove All Rows)
TRUNCATE TABLE table_name;
TRUNCATE TABLE table_name RESTART IDENTITY CASCADE; -- PostgreSQLDDL Patterns
Create Table
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- PostgreSQL
-- id INT AUTO_INCREMENT PRIMARY KEY, -- MySQL
-- id INT IDENTITY(1,1) PRIMARY KEY, -- SQL Server
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) DEFAULT 'active',
balance DECIMAL(10, 2) DEFAULT 0.00,
metadata JSONB, -- PostgreSQL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_status CHECK (status IN ('active', 'inactive', 'suspended'))
);Create Index
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status_created ON users(status, created_at);
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
CREATE INDEX idx_users_name_lower ON users(LOWER(name)); -- Functional indexAlter Table
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users DROP COLUMN deprecated_field;
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(200);
ALTER TABLE users ADD CONSTRAINT fk_department
FOREIGN KEY (department_id) REFERENCES departments(id);Create View
CREATE VIEW active_users_summary AS
SELECT
u.id,
u.name,
u.email,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.total), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
GROUP BY u.id, u.name, u.email;Complex Query Patterns
Pagination
Offset-based (simple but slow for large offsets):
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;Cursor-based (efficient for large datasets):
SELECT * FROM products
WHERE created_at < '2024-01-15 10:30:00'
ORDER BY created_at DESC
LIMIT 20;Hierarchical Data (Recursive CTE)
WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, ot.level + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT * FROM org_tree
ORDER BY level, name;Pivot Data
PostgreSQL:
SELECT
product_id,
SUM(CASE WHEN month = 1 THEN sales ELSE 0 END) AS jan,
SUM(CASE WHEN month = 2 THEN sales ELSE 0 END) AS feb,
SUM(CASE WHEN month = 3 THEN sales ELSE 0 END) AS mar
FROM monthly_sales
GROUP BY product_id;SQL Server:
SELECT product_id, [1] AS jan, [2] AS feb, [3] AS mar
FROM monthly_sales
PIVOT (SUM(sales) FOR month IN ([1], [2], [3])) AS pvt;Dynamic Date Ranges
SELECT
date_trunc('month', order_date) AS month,
COUNT(*) AS order_count,
SUM(total) AS revenue
FROM orders
WHERE order_date >= date_trunc('year', CURRENT_DATE)
GROUP BY date_trunc('month', order_date)
ORDER BY month;Anti-Patterns to Avoid
| Anti-Pattern | Problem | Better Approach |
|---|---|---|
| SELECT * | Returns unnecessary data | List specific columns |
| N+1 queries | Multiple round trips | Use JOINs or batch queries |
| Functions on indexed columns | Prevents index usage | Move function to value side |
| Implicit type conversion | Performance and accuracy issues | Use explicit CAST |
| Missing WHERE on UPDATE/DELETE | Affects all rows | Always include WHERE |
| Correlated subqueries | Executes per row | Use JOINs or CTEs |
Output Format
When writing queries, provide:
- The SQL query with proper formatting
- Brief explanation of what the query does
- Any assumptions made about the schema
- Performance considerations if relevant
- Alternative approaches if applicable
Scripts
Given the schema, let the planner find join paths instead of guessing them:
python scripts/plan_joins.py schema.sql --list # tables, keys, relationships
python scripts/plan_joins.py schema.sql --tables customers products \
--columns customers.email,products.name # adds bridge tables automaticallyIt parses CREATE TABLE statements (PostgreSQL, MySQL, SQL Server, SQLite), follows foreign keys with a shortest-path search and prints a SELECT skeleton with aliases and ON clauses. Exit code 1 means there is no foreign-key path, so ask the user how the tables relate rather than inventing a join.
Additional Resources
| File | Content |
|---|---|
| examples.md | Production-ready query examples by use case |
| sql-quick-reference.md | Dialects, clause order, JOIN types, operators, best practices and the pre-output checklist |
sql-analytics
Analytical and aggregation SQL — GROUP BY, ROLLUP, CUBE, GROUPING SETS, pivots, cohorts, funnels, retention, percentiles and statistical functions.
sql-data-quality
Profile and validate data with SQL — NULL analysis, duplicate detection, referential integrity, range and format checks, outliers, constraints and cleansing queries.
sql-debugger
Debug failing SQL or queries returning wrong results — interpret error messages, find causes of duplicates, missing rows, NULL surprises, fan-out joins and type mismatches, and produce a fixed query.
sql-dialect-translator
Translate SQL between dialects — PostgreSQL, MySQL, SQL Server (T-SQL), Oracle, SQLite — covering data types, functions, pagination, date handling, upserts and identity columns.
sql-optimizer
Rewrite slow SQL queries for performance — read execution plans (EXPLAIN / EXPLAIN ANALYZE), remove anti-patterns, make predicates sargable, choose JOIN/EXISTS strategies and suggest indexes.
sql-performance-tuning
Database-level SQL performance tuning — index design and maintenance, query profiling, statistics, partitioning, schema design, locking and configuration bottlenecks.