PostgreSQL optimization
Optimize ${selection} or the current project by applying PostgreSQL-specific data types, operators, indexes, query plans, extensions, and maintenance practices, then return paste-ready SQL and evidence-backed recommendations.
When to invoke
- "Optimize this PostgreSQL query."
- "Design indexes for these JSONB and array filters."
- "Review this EXPLAIN ANALYZE output."
- "Use PostgreSQL full-text search or range types here."
- "Improve our pg_stat_statements slow queries."
PostgreSQL feature map
| Need |
PostgreSQL capability |
Use |
| Semi-structured data |
JSONB, @>, ?, #>>, jsonb_agg, GIN |
Query structured metadata without text casts. |
| Tags and many values |
Arrays, ANY, &&, @>, array_length, array_agg, unnest |
Model short bounded value lists and query with GIN when selective. |
| Analytics |
SUM() OVER, AVG() OVER, DENSE_RANK, ROW_NUMBER, LAG, LEAD, frames like ROWS BETWEEN 2 PRECEDING AND CURRENT ROW |
Running totals, moving averages, rankings, and comparisons. |
| Search |
tsvector, to_tsvector, plainto_tsquery, @@, ts_rank |
Full-text search with ranking and GIN indexes. |
| Reusable validation |
CREATE DOMAIN, CHECK, custom composite types, ENUM |
Encode business constraints in schema. |
| Time or numeric intervals |
tstzrange, numrange, overlap &&, EXCLUDE USING gist |
Reservations, prices, validity periods, non-overlap rules. |
| Spatial-lite data |
POINT, CIRCLE, POLYGON, <->, GiST |
Basic geometric distance and containment without PostGIS. |
| Cryptography and fuzzy text |
uuid-ossp, pgcrypto, unaccent, pg_trgm, btree_gin |
UUIDs, crypt, gen_salt, accent handling, trigram similarity, GIN support. |
Query and index patterns
| Pattern |
Avoid |
Prefer |
| JSON search |
data::text LIKE '%admin%' |
CREATE INDEX idx_users_data_gin ON users USING gin(data); then data @> '{"role": "admin"}'. |
| JSON path read |
Repeated text extraction without index |
Use data #>> '{user,role}' for scalar reads and containment for indexed filtering. |
| Array membership |
Relying only on 'postgresql' = ANY(tags) for large tables |
Add CREATE INDEX ... USING gin(tags) and use tags @> ARRAY['postgresql'] or tags && ARRAY['database','sql']. |
| Pagination |
ORDER BY id OFFSET 10000 LIMIT 20 |
Cursor pagination: WHERE id > $last_id ORDER BY id LIMIT 20. |
| Recent aggregation |
Full scan for a bounded date range |
Partial index: CREATE INDEX idx_orders_recent ON orders(user_id) WHERE order_date >= '2024-01-01';. |
| Case-insensitive lookup |
lower(email) scan |
Expression index: CREATE INDEX idx_users_lower_email ON users(lower(email));. |
| Covering read |
Index lookup plus table fetch for hot columns |
CREATE INDEX idx_orders_covering ON orders(user_id, status) INCLUDE (total, created_at);. |
| Multi-column filters |
Separate single-column indexes for correlated predicates |
Composite index such as CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);. |
| Overlapping reservations |
Application-only overlap checks |
EXCLUDE USING gist (room_id WITH =, reservation_period WITH &&). |
Performance workflow
- Capture the slow SQL and its parameters; never concatenate user input into SQL.
- Run
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) on representative data.
- Inspect sequential scans on large tables, join algorithms, sort and aggregate nodes, row estimate errors, filter selectivity, and buffer reads.
- Check
pg_stat_statements for query, calls, total_time, mean_time, rows, and cache hit percentage:
SELECT query, calls, total_time, mean_time, rows,
100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
- Propose the smallest safe change: rewrite SQL, add
CREATE INDEX, add constraints, adjust schema types, or schedule maintenance.
- Validate with a before/after plan, expected trade-offs, and rollback-safe SQL.
Schema and data type guidance
CREATE TABLE events (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_events_data_gin ON events USING gin(data);
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
CREATE DOMAIN email_address AS TEXT
CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
email email_address NOT NULL,
status order_status DEFAULT 'pending'
);
Use TIMESTAMPTZ for instants, TEXT when length is not a business rule, CITEXT or an expression index for case-insensitive text, domains for reusable validation, and declarative partitioning for large time- or tenant-partitioned tables.
Monitoring and maintenance
| Task |
Query or action |
| Connections |
SELECT count(*) AS connections, state FROM pg_stat_activity GROUP BY state; |
| Memory settings |
SELECT name, setting, unit FROM pg_settings WHERE name IN ('shared_buffers', 'work_mem', 'maintenance_work_mem'); |
| Database size |
SELECT pg_size_pretty(pg_database_size(current_database())) AS db_size; |
| Table sizes |
Use `pg_total_relation_size(schemaname |
| Unused indexes |
Query pg_stat_user_indexes WHERE idx_scan = 0. |
| Routine maintenance |
Run regular VACUUM and ANALYZE; review PostgreSQL logs; monitor index bloat and fragmentation. |
| High concurrency |
Use connection pooling such as pgbouncer; monitor pool usage and saturation. |
Security rules
- Use parameterized queries exclusively; placeholders such as
$last_id are acceptable, string concatenation is not.
- Implement proper access controls and row-level security where needed.
- Audit sensitive data access and use secure connection methods.
- Prefer
pgcrypto for database-side cryptographic functions only when the architecture justifies it; avoid storing plaintext secrets.
Technical index
Preserve these PostgreSQL identifiers, plan terms, and example names when producing SQL: ALTER, CONSTRAINT, COUNT, DISTINCT, EXISTS, EXTENSION, EXTRACT, GOOD, INTEGER, JOIN, LEFT, OLAP, OLTP, PARTITION, RECURSIVE, UNION, UPDATE, UUID, Lag/Lead, address_type, category_tree, parent_id, postal_code, price_range, product_id, sale_date, search_vector, service_area, running_total, moving_avg, monthly_rank, prev_amount, order_count, idx_active_users, idx_documents_search, idx_locations_coords, idx_table_column, idx_tup_read, idx_tup_fetch, no_overlap, uuid_generate_v4, high-concurrency, and multi-column.
Output template
## Query Performance Analysis
**Status:** optimized | needs data | blocked
**Original Query:**
```sql
<original SQL>
Evidence:
- Plan reviewed:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
- Main issue: <sequential scan, missing index, join order, sort, row estimate, lock, or maintenance issue>
Issues Identified:
- <issue with cost, rows, buffers, or table size evidence>
Optimized Query:
<improved SQL>
Recommended Indexes / Schema Changes:
CREATE INDEX <index_name> ON <table>(<columns>);
Performance Impact:
Validation: <before/after command or reason it could not be run>
## Quality gate
- [ ] Every SQL change is PostgreSQL-specific where PostgreSQL features add value.
- [ ] Expensive queries are backed by `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)` or a stated blocker.
- [ ] Index recommendations name the index type, columns/order, predicate, and write/storage trade-off.
- [ ] JSONB, arrays, full-text search, ranges, geometric types, custom types, and extensions are used only when they fit the data model.
- [ ] Security guidance uses parameterized queries and avoids SQL injection.
- [ ] Maintenance recommendations cover `VACUUM`, `ANALYZE`, `pg_stat_statements`, connection usage, or log review when relevant.
- [ ] Output includes paste-ready SQL and does not claim measured speedups without evidence.
1---2name: postgresql-optimization3description: Design, tune, and modernize PostgreSQL SQL, schemas, indexes, functions, and maintenance workflows using PostgreSQL-specific capabilities. Use this skill when the user asks for PostgreSQL optimization, query tuning, JSONB or array design, EXPLAIN ANALYZE interpretation, index strategy, full-text search, custom types, range/geometric types, pg_stat_statements, VACUUM, ANALYZE, partitioning, or secure parameterized database development.4---56<!-- Generated from harness/github-copilot/skills/postgresql-optimization/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->78# PostgreSQL optimization910Optimize `${selection}` or the current project by applying PostgreSQL-specific data types, operators, indexes, query plans, extensions, and maintenance practices, then return paste-ready SQL and evidence-backed recommendations.1112## When to invoke1314- "Optimize this PostgreSQL query."15- "Design indexes for these JSONB and array filters."16- "Review this EXPLAIN ANALYZE output."17- "Use PostgreSQL full-text search or range types here."18- "Improve our pg_stat_statements slow queries."1920## PostgreSQL feature map2122| Need | PostgreSQL capability | Use |23| --- | --- | --- |24| Semi-structured data | `JSONB`, `@>`, `?`, `#>>`, `jsonb_agg`, GIN | Query structured metadata without text casts. |25| Tags and many values | Arrays, `ANY`, `&&`, `@>`, `array_length`, `array_agg`, `unnest` | Model short bounded value lists and query with GIN when selective. |26| Analytics | `SUM() OVER`, `AVG() OVER`, `DENSE_RANK`, `ROW_NUMBER`, `LAG`, `LEAD`, frames like `ROWS BETWEEN 2 PRECEDING AND CURRENT ROW` | Running totals, moving averages, rankings, and comparisons. |27| Search | `tsvector`, `to_tsvector`, `plainto_tsquery`, `@@`, `ts_rank` | Full-text search with ranking and GIN indexes. |28| Reusable validation | `CREATE DOMAIN`, `CHECK`, custom composite types, `ENUM` | Encode business constraints in schema. |29| Time or numeric intervals | `tstzrange`, `numrange`, overlap `&&`, `EXCLUDE USING gist` | Reservations, prices, validity periods, non-overlap rules. |30| Spatial-lite data | `POINT`, `CIRCLE`, `POLYGON`, `<->`, GiST | Basic geometric distance and containment without PostGIS. |31| Cryptography and fuzzy text | `uuid-ossp`, `pgcrypto`, `unaccent`, `pg_trgm`, `btree_gin` | UUIDs, `crypt`, `gen_salt`, accent handling, trigram `similarity`, GIN support. |3233## Query and index patterns3435| Pattern | Avoid | Prefer |36| --- | --- | --- |37| JSON search | `data::text LIKE '%admin%'` | `CREATE INDEX idx_users_data_gin ON users USING gin(data);` then `data @> '{"role": "admin"}'`. |38| JSON path read | Repeated text extraction without index | Use `data #>> '{user,role}'` for scalar reads and containment for indexed filtering. |39| Array membership | Relying only on `'postgresql' = ANY(tags)` for large tables | Add `CREATE INDEX ... USING gin(tags)` and use `tags @> ARRAY['postgresql']` or `tags && ARRAY['database','sql']`. |40| Pagination | `ORDER BY id OFFSET 10000 LIMIT 20` | Cursor pagination: `WHERE id > $last_id ORDER BY id LIMIT 20`. |41| Recent aggregation | Full scan for a bounded date range | Partial index: `CREATE INDEX idx_orders_recent ON orders(user_id) WHERE order_date >= '2024-01-01';`. |42| Case-insensitive lookup | `lower(email)` scan | Expression index: `CREATE INDEX idx_users_lower_email ON users(lower(email));`. |43| Covering read | Index lookup plus table fetch for hot columns | `CREATE INDEX idx_orders_covering ON orders(user_id, status) INCLUDE (total, created_at);`. |44| Multi-column filters | Separate single-column indexes for correlated predicates | Composite index such as `CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);`. |45| Overlapping reservations | Application-only overlap checks | `EXCLUDE USING gist (room_id WITH =, reservation_period WITH &&)`. |4647## Performance workflow48491. Capture the slow SQL and its parameters; never concatenate user input into SQL.502. Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)` on representative data.513. Inspect sequential scans on large tables, join algorithms, sort and aggregate nodes, row estimate errors, filter selectivity, and buffer reads.524. Check `pg_stat_statements` for `query`, `calls`, `total_time`, `mean_time`, `rows`, and cache hit percentage:5354```sql55SELECT query, calls, total_time, mean_time, rows,56 100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent57FROM pg_stat_statements58ORDER BY total_time DESC59LIMIT 10;60```61625. Propose the smallest safe change: rewrite SQL, add `CREATE INDEX`, add constraints, adjust schema types, or schedule maintenance.636. Validate with a before/after plan, expected trade-offs, and rollback-safe SQL.6465## Schema and data type guidance6667```sql68CREATE TABLE events (69 id SERIAL PRIMARY KEY,70 data JSONB NOT NULL,71 created_at TIMESTAMPTZ DEFAULT NOW()72);73CREATE INDEX idx_events_data_gin ON events USING gin(data);7475CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');76CREATE DOMAIN email_address AS TEXT77CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');7879CREATE TABLE customers (80 id SERIAL PRIMARY KEY,81 email email_address NOT NULL,82 status order_status DEFAULT 'pending'83);84```8586Use `TIMESTAMPTZ` for instants, `TEXT` when length is not a business rule, `CITEXT` or an expression index for case-insensitive text, domains for reusable validation, and declarative partitioning for large time- or tenant-partitioned tables.8788## Monitoring and maintenance8990| Task | Query or action |91| --- | --- |92| Connections | `SELECT count(*) AS connections, state FROM pg_stat_activity GROUP BY state;` |93| Memory settings | `SELECT name, setting, unit FROM pg_settings WHERE name IN ('shared_buffers', 'work_mem', 'maintenance_work_mem');` |94| Database size | `SELECT pg_size_pretty(pg_database_size(current_database())) AS db_size;` |95| Table sizes | Use `pg_total_relation_size(schemaname||'.'||tablename)` from `pg_tables`. |96| Unused indexes | Query `pg_stat_user_indexes WHERE idx_scan = 0`. |97| Routine maintenance | Run regular `VACUUM` and `ANALYZE`; review PostgreSQL logs; monitor index bloat and fragmentation. |98| High concurrency | Use connection pooling such as `pgbouncer`; monitor pool usage and saturation. |99100## Security rules101102- Use parameterized queries exclusively; placeholders such as `$last_id` are acceptable, string concatenation is not.103- Implement proper access controls and row-level security where needed.104- Audit sensitive data access and use secure connection methods.105- Prefer `pgcrypto` for database-side cryptographic functions only when the architecture justifies it; avoid storing plaintext secrets.106107## Technical index108109Preserve these PostgreSQL identifiers, plan terms, and example names when producing SQL: `ALTER`, `CONSTRAINT`, `COUNT`, `DISTINCT`, `EXISTS`, `EXTENSION`, `EXTRACT`, `GOOD`, `INTEGER`, `JOIN`, `LEFT`, `OLAP`, `OLTP`, `PARTITION`, `RECURSIVE`, `UNION`, `UPDATE`, `UUID`, `Lag/Lead`, `address_type`, `category_tree`, `parent_id`, `postal_code`, `price_range`, `product_id`, `sale_date`, `search_vector`, `service_area`, `running_total`, `moving_avg`, `monthly_rank`, `prev_amount`, `order_count`, `idx_active_users`, `idx_documents_search`, `idx_locations_coords`, `idx_table_column`, `idx_tup_read`, `idx_tup_fetch`, `no_overlap`, `uuid_generate_v4`, `high-concurrency`, and `multi-column`.110111## Output template112113```markdown114## Query Performance Analysis115116**Status:** optimized | needs data | blocked117**Original Query:**118```sql119<original SQL>120```121122**Evidence:**123- Plan reviewed: `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)`124- Main issue: <sequential scan, missing index, join order, sort, row estimate, lock, or maintenance issue>125126**Issues Identified:**127- <issue with cost, rows, buffers, or table size evidence>128129**Optimized Query:**130```sql131<improved SQL>132```133134**Recommended Indexes / Schema Changes:**135```sql136CREATE INDEX <index_name> ON <table>(<columns>);137```138139**Performance Impact:** <expected impact and trade-off>140**Validation:** <before/after command or reason it could not be run>141```142143## Quality gate144145- [ ] Every SQL change is PostgreSQL-specific where PostgreSQL features add value.146- [ ] Expensive queries are backed by `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)` or a stated blocker.147- [ ] Index recommendations name the index type, columns/order, predicate, and write/storage trade-off.148- [ ] JSONB, arrays, full-text search, ranges, geometric types, custom types, and extensions are used only when they fit the data model.149- [ ] Security guidance uses parameterized queries and avoids SQL injection.150- [ ] Maintenance recommendations cover `VACUUM`, `ANALYZE`, `pg_stat_statements`, connection usage, or log review when relevant.151- [ ] Output includes paste-ready SQL and does not claim measured speedups without evidence.