Skip to content

Commit 99c6f25

Browse files
feat(root): add postgres best practices skill
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent ce2b271 commit 99c6f25

32 files changed

Lines changed: 3072 additions & 0 deletions

.agent/skills/postgres-best-practices/AGENTS.md

Lines changed: 1490 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
name: supabase-postgres-best-practices
3+
description: Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.
4+
license: MIT
5+
metadata:
6+
author: supabase
7+
version: "1.0.0"
8+
---
9+
10+
# Supabase Postgres Best Practices
11+
12+
Comprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.
13+
14+
## When to Apply
15+
16+
Reference these guidelines when:
17+
- Writing SQL queries or designing schemas
18+
- Implementing indexes or query optimization
19+
- Reviewing database performance issues
20+
- Configuring connection pooling or scaling
21+
- Optimizing for Postgres-specific features
22+
- Working with Row-Level Security (RLS)
23+
24+
## Rule Categories by Priority
25+
26+
| Priority | Category | Impact | Prefix |
27+
|----------|----------|--------|--------|
28+
| 1 | Query Performance | CRITICAL | `query-` |
29+
| 2 | Connection Management | CRITICAL | `conn-` |
30+
| 3 | Security & RLS | CRITICAL | `security-` |
31+
| 4 | Schema Design | HIGH | `schema-` |
32+
| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |
33+
| 6 | Data Access Patterns | MEDIUM | `data-` |
34+
| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |
35+
| 8 | Advanced Features | LOW | `advanced-` |
36+
37+
## How to Use
38+
39+
Read individual rule files for detailed explanations and SQL examples:
40+
41+
```
42+
rules/query-missing-indexes.md
43+
rules/schema-partial-indexes.md
44+
rules/_sections.md
45+
```
46+
47+
Each rule file contains:
48+
- Brief explanation of why it matters
49+
- Incorrect SQL example with explanation
50+
- Correct SQL example with explanation
51+
- Optional EXPLAIN output or metrics
52+
- Additional context and references
53+
- Supabase-specific notes (when applicable)
54+
55+
## Full Compiled Document
56+
57+
For the complete guide with all rules expanded: `AGENTS.md`
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Use tsvector for Full-Text Search
3+
impact: MEDIUM
4+
impactDescription: 100x faster than LIKE, with ranking support
5+
tags: full-text-search, tsvector, gin, search
6+
---
7+
8+
## Use tsvector for Full-Text Search
9+
10+
LIKE with wildcards can't use indexes. Full-text search with tsvector is orders of magnitude faster.
11+
12+
**Incorrect (LIKE pattern matching):**
13+
14+
```sql
15+
-- Cannot use index, scans all rows
16+
select * from articles where content like '%postgresql%';
17+
18+
-- Case-insensitive makes it worse
19+
select * from articles where lower(content) like '%postgresql%';
20+
```
21+
22+
**Correct (full-text search with tsvector):**
23+
24+
```sql
25+
-- Add tsvector column and index
26+
alter table articles add column search_vector tsvector
27+
generated always as (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,''))) stored;
28+
29+
create index articles_search_idx on articles using gin (search_vector);
30+
31+
-- Fast full-text search
32+
select * from articles
33+
where search_vector @@ to_tsquery('english', 'postgresql & performance');
34+
35+
-- With ranking
36+
select *, ts_rank(search_vector, query) as rank
37+
from articles, to_tsquery('english', 'postgresql') query
38+
where search_vector @@ query
39+
order by rank desc;
40+
```
41+
42+
Search multiple terms:
43+
44+
```sql
45+
-- AND: both terms required
46+
to_tsquery('postgresql & performance')
47+
48+
-- OR: either term
49+
to_tsquery('postgresql | mysql')
50+
51+
-- Prefix matching
52+
to_tsquery('post:*')
53+
```
54+
55+
Reference: [Full Text Search](https://supabase.com/docs/guides/database/full-text-search)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
title: Index JSONB Columns for Efficient Querying
3+
impact: MEDIUM
4+
impactDescription: 10-100x faster JSONB queries with proper indexing
5+
tags: jsonb, gin, indexes, json
6+
---
7+
8+
## Index JSONB Columns for Efficient Querying
9+
10+
JSONB queries without indexes scan the entire table. Use GIN indexes for containment queries.
11+
12+
**Incorrect (no index on JSONB):**
13+
14+
```sql
15+
create table products (
16+
id bigint primary key,
17+
attributes jsonb
18+
);
19+
20+
-- Full table scan for every query
21+
select * from products where attributes @> '{"color": "red"}';
22+
select * from products where attributes->>'brand' = 'Nike';
23+
```
24+
25+
**Correct (GIN index for JSONB):**
26+
27+
```sql
28+
-- GIN index for containment operators (@>, ?, ?&, ?|)
29+
create index products_attrs_gin on products using gin (attributes);
30+
31+
-- Now containment queries use the index
32+
select * from products where attributes @> '{"color": "red"}';
33+
34+
-- For specific key lookups, use expression index
35+
create index products_brand_idx on products ((attributes->>'brand'));
36+
select * from products where attributes->>'brand' = 'Nike';
37+
```
38+
39+
Choose the right operator class:
40+
41+
```sql
42+
-- jsonb_ops (default): supports all operators, larger index
43+
create index idx1 on products using gin (attributes);
44+
45+
-- jsonb_path_ops: only @> operator, but 2-3x smaller index
46+
create index idx2 on products using gin (attributes jsonb_path_ops);
47+
```
48+
49+
Reference: [JSONB Indexes](https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: Configure Idle Connection Timeouts
3+
impact: HIGH
4+
impactDescription: Reclaim 30-50% of connection slots from idle clients
5+
tags: connections, timeout, idle, resource-management
6+
---
7+
8+
## Configure Idle Connection Timeouts
9+
10+
Idle connections waste resources. Configure timeouts to automatically reclaim them.
11+
12+
**Incorrect (connections held indefinitely):**
13+
14+
```sql
15+
-- No timeout configured
16+
show idle_in_transaction_session_timeout; -- 0 (disabled)
17+
18+
-- Connections stay open forever, even when idle
19+
select pid, state, state_change, query
20+
from pg_stat_activity
21+
where state = 'idle in transaction';
22+
-- Shows transactions idle for hours, holding locks
23+
```
24+
25+
**Correct (automatic cleanup of idle connections):**
26+
27+
```sql
28+
-- Terminate connections idle in transaction after 30 seconds
29+
alter system set idle_in_transaction_session_timeout = '30s';
30+
31+
-- Terminate completely idle connections after 10 minutes
32+
alter system set idle_session_timeout = '10min';
33+
34+
-- Reload configuration
35+
select pg_reload_conf();
36+
```
37+
38+
For pooled connections, configure at the pooler level:
39+
40+
```ini
41+
# pgbouncer.ini
42+
server_idle_timeout = 60
43+
client_idle_timeout = 300
44+
```
45+
46+
Reference: [Connection Timeouts](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
title: Set Appropriate Connection Limits
3+
impact: CRITICAL
4+
impactDescription: Prevent database crashes and memory exhaustion
5+
tags: connections, max-connections, limits, stability
6+
---
7+
8+
## Set Appropriate Connection Limits
9+
10+
Too many connections exhaust memory and degrade performance. Set limits based on available resources.
11+
12+
**Incorrect (unlimited or excessive connections):**
13+
14+
```sql
15+
-- Default max_connections = 100, but often increased blindly
16+
show max_connections; -- 500 (way too high for 4GB RAM)
17+
18+
-- Each connection uses 1-3MB RAM
19+
-- 500 connections * 2MB = 1GB just for connections!
20+
-- Out of memory errors under load
21+
```
22+
23+
**Correct (calculate based on resources):**
24+
25+
```sql
26+
-- Formula: max_connections = (RAM in MB / 5MB per connection) - reserved
27+
-- For 4GB RAM: (4096 / 5) - 10 = ~800 theoretical max
28+
-- But practically, 100-200 is better for query performance
29+
30+
-- Recommended settings for 4GB RAM
31+
alter system set max_connections = 100;
32+
33+
-- Also set work_mem appropriately
34+
-- work_mem * max_connections should not exceed 25% of RAM
35+
alter system set work_mem = '8MB'; -- 8MB * 100 = 800MB max
36+
```
37+
38+
Monitor connection usage:
39+
40+
```sql
41+
select count(*), state from pg_stat_activity group by state;
42+
```
43+
44+
Reference: [Database Connections](https://supabase.com/docs/guides/platform/performance#connection-management)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
title: Use Connection Pooling for All Applications
3+
impact: CRITICAL
4+
impactDescription: Handle 10-100x more concurrent users
5+
tags: connection-pooling, pgbouncer, performance, scalability
6+
---
7+
8+
## Use Connection Pooling for All Applications
9+
10+
Postgres connections are expensive (1-3MB RAM each). Without pooling, applications exhaust connections under load.
11+
12+
**Incorrect (new connection per request):**
13+
14+
```sql
15+
-- Each request creates a new connection
16+
-- Application code: db.connect() per request
17+
-- Result: 500 concurrent users = 500 connections = crashed database
18+
19+
-- Check current connections
20+
select count(*) from pg_stat_activity; -- 487 connections!
21+
```
22+
23+
**Correct (connection pooling):**
24+
25+
```sql
26+
-- Use a pooler like PgBouncer between app and database
27+
-- Application connects to pooler, pooler reuses a small pool to Postgres
28+
29+
-- Configure pool_size based on: (CPU cores * 2) + spindle_count
30+
-- Example for 4 cores: pool_size = 10
31+
32+
-- Result: 500 concurrent users share 10 actual connections
33+
select count(*) from pg_stat_activity; -- 10 connections
34+
```
35+
36+
Pool modes:
37+
38+
- **Transaction mode**: connection returned after each transaction (best for most apps)
39+
- **Session mode**: connection held for entire session (needed for prepared statements, temp tables)
40+
41+
Reference: [Connection Pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: Use Prepared Statements Correctly with Pooling
3+
impact: HIGH
4+
impactDescription: Avoid prepared statement conflicts in pooled environments
5+
tags: prepared-statements, connection-pooling, transaction-mode
6+
---
7+
8+
## Use Prepared Statements Correctly with Pooling
9+
10+
Prepared statements are tied to individual database connections. In transaction-mode pooling, connections are shared, causing conflicts.
11+
12+
**Incorrect (named prepared statements with transaction pooling):**
13+
14+
```sql
15+
-- Named prepared statement
16+
prepare get_user as select * from users where id = $1;
17+
18+
-- In transaction mode pooling, next request may get different connection
19+
execute get_user(123);
20+
-- ERROR: prepared statement "get_user" does not exist
21+
```
22+
23+
**Correct (use unnamed statements or session mode):**
24+
25+
```sql
26+
-- Option 1: Use unnamed prepared statements (most ORMs do this automatically)
27+
-- The query is prepared and executed in a single protocol message
28+
29+
-- Option 2: Deallocate after use in transaction mode
30+
prepare get_user as select * from users where id = $1;
31+
execute get_user(123);
32+
deallocate get_user;
33+
34+
-- Option 3: Use session mode pooling (port 5432 vs 6543)
35+
-- Connection is held for entire session, prepared statements persist
36+
```
37+
38+
Check your driver settings:
39+
40+
```sql
41+
-- Many drivers use prepared statements by default
42+
-- Node.js pg: { prepare: false } to disable
43+
-- JDBC: prepareThreshold=0 to disable
44+
```
45+
46+
Reference: [Prepared Statements with Pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool-modes)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: Batch INSERT Statements for Bulk Data
3+
impact: MEDIUM
4+
impactDescription: 10-50x faster bulk inserts
5+
tags: batch, insert, bulk, performance, copy
6+
---
7+
8+
## Batch INSERT Statements for Bulk Data
9+
10+
Individual INSERT statements have high overhead. Batch multiple rows in single statements or use COPY.
11+
12+
**Incorrect (individual inserts):**
13+
14+
```sql
15+
-- Each insert is a separate transaction and round trip
16+
insert into events (user_id, action) values (1, 'click');
17+
insert into events (user_id, action) values (1, 'view');
18+
insert into events (user_id, action) values (2, 'click');
19+
-- ... 1000 more individual inserts
20+
21+
-- 1000 inserts = 1000 round trips = slow
22+
```
23+
24+
**Correct (batch insert):**
25+
26+
```sql
27+
-- Multiple rows in single statement
28+
insert into events (user_id, action) values
29+
(1, 'click'),
30+
(1, 'view'),
31+
(2, 'click'),
32+
-- ... up to ~1000 rows per batch
33+
(999, 'view');
34+
35+
-- One round trip for 1000 rows
36+
```
37+
38+
For large imports, use COPY:
39+
40+
```sql
41+
-- COPY is fastest for bulk loading
42+
copy events (user_id, action, created_at)
43+
from '/path/to/data.csv'
44+
with (format csv, header true);
45+
46+
-- Or from stdin in application
47+
copy events (user_id, action) from stdin with (format csv);
48+
1,click
49+
1,view
50+
2,click
51+
\.
52+
```
53+
54+
Reference: [COPY](https://www.postgresql.org/docs/current/sql-copy.html)

0 commit comments

Comments
 (0)