skills/postgres-query-expert/SKILL.md
A comprehensive guide for interacting with PostgreSQL 16 databases. Use this skill for constructing standard and advanced SQL queries, optimizing performance, debugging errors, managing schema objects, and introspecting database structure.
npx skillsauth add ratacat/claude-skills postgres-query-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill is a definitive reference for PostgreSQL 16, covering query construction, optimization, schema management, and system introspection.
DISTINCT ON, RETURNING, LATERAL, FILTER clauses) when they provide cleaner logic or better performance.snake_case for all identifiers. Only quote identifiers ("MyTable") if absolutely necessary; prefer lowercase unquoted names.$1, $2, …) for literal values. Never inject user input directly.SET LOCAL statement_timeout = '30s';.BEGIN and COMMIT blocks for multi-step operations.EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) to diagnose bottlenecks.Seq Scan on large tables, high Buffers: shared hit (RAM usage), or Disk: read (I/O).=, <, >) queries.JSONB (@>) or arrays (&&), and full-text search.WITH) for readability. In PG16+, these are optimized (inlined) by default unless MATERIALIZED is specified.When exploring a new database, use these queries to understand the schema.
SELECT n.nspname AS schema,
c.relname AS table,
obj_description(c.oid) AS description
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY 1, 2;
SELECT a.attname AS column,
format_type(a.atttypid, a.atttypmod) AS type,
a.attnotnull AS not_null,
col_description(a.attrelid, a.attnum) AS comment
FROM pg_attribute a
WHERE a.attrelid = 'public.target_table_name'::regclass
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum;
count(*) FILTER (WHERE status = 'active')GROUP BY GROUPING SETS ((brand), (brand, category), ())any_value(col) (PG16+) returns an arbitrary value from the group.Perform calculations across a set of table rows related to the current row.
SELECT dept,
emp_no,
salary,
-- Rank employees by salary within department
dense_rank() OVER (PARTITION BY dept ORDER BY salary DESC) AS rank,
-- Running total of salaries
sum(salary) OVER (
PARTITION BY dept
ORDER BY salary
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM employees;
col LIKE 'foo%' (simple wildcard).col ILIKE 'foo%' (case-insensitive).col SIMILAR TO '[a-c]%' (SQL-regex style).col ~ '^[a-z]+$'col ~* 'foo'Standard SQL method for inserting, updating, or deleting based on join conditions (PG15+).
MERGE INTO wine_stock ws
USING wine_shipments s
ON s.winery_id = ws.winery_id
AND s.year = ws.year
WHEN MATCHED THEN
UPDATE SET stock = ws.stock + s.count
WHEN NOT MATCHED THEN
INSERT (winery_id, year, stock)
VALUES (s.winery_id, s.year, s.count);
Postgres-specific, often more concise for simple unique-key conflicts.
INSERT INTO kv_store (key, value)
VALUES ('config', '{"a":1}')
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value;
Return data from modified rows immediately.
DELETE FROM archived_logs
WHERE created_at < NOW() - INTERVAL '1 year'
RETURNING id, created_at;
Prefer jsonb over json for storage and indexing.
| Operator | Description | Example |
|---|---|---|
| -> / ->> | Get element (JSON / text) | data->'key' |
| @> | Contains (indexable) | data @> '{"tag": "urgent"}' |
| ? | Key exists | data ? 'error' |
| #- | Delete path | data #- '{info, sensitive}' |
SQL/JSON path (PG12+):
-- Find all items with price > 10
SELECT jsonb_path_query(data, '$.items[*] ? (@.price > 10)')
FROM orders;
SELECT ARRAY[1,2,3]; -- Creation
SELECT (ARRAY[1,2,3])[1]; -- Access (1-based index)
SELECT 1 = ANY(arr_col); -- Check if value exists in array
SELECT unnest(arr_col) FROM t; -- Expand array to rows
Useful for scheduling and validity periods.
tstzrange: timestamp with time zone range.int4range, daterange: integer and date ranges.&&): checks if two ranges overlap.SELECT *
FROM reservations
WHERE duration && tstzrange('2023-01-01 10:00', '2023-01-01 12:00');
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'active'
AND pid <> pg_backend_pid()
AND query_start < NOW() - INTERVAL '5 minutes';
SELECT relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_relation_size(relid)) AS data_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;
Navigating an organizational hierarchy.
WITH RECURSIVE subordinates AS (
-- Base case: the manager
SELECT employee_id, manager_id, full_name, 0 AS level
FROM employees
WHERE employee_id = $1
UNION ALL
-- Recursive step: direct reports
SELECT e.employee_id, e.manager_id, e.full_name, s.level + 1
FROM employees e
INNER JOIN subordinates s ON s.employee_id = e.manager_id
)
SELECT *
FROM subordinates;
Efficiently getting the latest 3 posts for each user.
SELECT u.username, p.title, p.created_at
FROM users u
CROSS JOIN LATERAL (
SELECT title, created_at
FROM posts
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 3
) p
WHERE u.status = 'active';
Searching a blog table.
SELECT id,
title,
ts_rank(to_tsvector('english', title || ' ' || content), query) AS rank
FROM articles,
to_tsquery('english', 'postgres | optimization') query
WHERE to_tsvector('english', title || ' ' || content) @@ query
ORDER BY rank DESC;
tools
Build and test iOS apps on simulator using XcodeBuildMCP
development
Produces concise, clear documentation by applying Elements of Style principles. Use when writing or improving any technical documentation (READMEs, guides, API docs, architecture docs). Not for code comments.
testing
Use when user asks to create, write, edit, or test a skill. Also use when documenting reusable techniques, patterns, or workflows for future Claude instances.
testing
Execute work plans efficiently while maintaining quality and finishing features