Skip to content
Back to skills

Sql Patterns

ASecurity

Use when writing SQL queries — optimization, indexing, CTE, window functions, query plans, warehouse-specific patterns (Postgres, BigQuery, Snowflake, ClickHouse, DuckDB). Trigger on "sql", "query", "select", "join", "explain analyze", "CTE".

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 19, 2026
ai-agentspythongosqlexpressdatabaseperformance

Works with

  • cursor
  • cli

Security analysis

A100/100

Scanned September 19, 2026

npx -y skills add Fqih/.mycc-setup --skill sql-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sql Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sql Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/fqih-sql-patterns/badge)](https://www.skillsdirectory.com/skills/fqih-sql-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: sql-patterns
description: Use when writing SQL queries — optimization, indexing, CTE, window functions, query plans, warehouse-specific patterns (Postgres, BigQuery, Snowflake, ClickHouse, DuckDB). Trigger on "sql", "query", "select", "join", "explain analyze", "CTE".
---

# sql-patterns

SQL patterns untuk query yang benar (correctness) + cepat (performance). Database-agnostic + per-engine optimization.

## Query writing principles

1. **Read query plan first** (`EXPLAIN ANALYZE`) — optimize yang jalan, bukan yang terlihat
2. **Filter early** (WHERE) — kurangi row count sebelum JOIN/AGGREGATE
3. **Project early** (SELECT specific columns, bukan `*`) — kurangi I/O
4. **Index what you query** — B-tree untuk equality/range, GIN untuk JSON/full-text, BRIN untuk time-series
5. **Avoid N+1** — satu JOIN atau aggregated query, bukan loop di app

## CTE vs subquery

```sql
-- ❌ Subquery (hard to read, often re-evaluated)
SELECT *
FROM orders o
WHERE user_id IN (SELECT id FROM users WHERE country = 'ID');

-- ✅ CTE (readable, optimizer-friendly in modern engines)
WITH id_users AS (
    SELECT id FROM users WHERE country = 'ID'
)
SELECT o.*
FROM orders o
JOIN id_users u ON o.user_id = u.id;
```

CTE `WITH ... AS` chain untuk complex queries. PostgreSQL 12+ inlines simple CTEs (treated as subquery, no materialization overhead).

## Window functions

```sql
-- Ranking
SELECT
    user_id,
    order_date,
    amount,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn,
    RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS amt_rank
FROM orders;

-- Running totals
SELECT
    order_date,
    amount,
    SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM orders;

-- Lag/lead
SELECT
    order_date,
    amount,
    LAG(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS prev_amount,
    amount - LAG(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS diff
FROM orders;
```

## Pagination

```sql
-- ❌ OFFSET (slow untuk large offsets — DB scans + skips rows)
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100000;

-- ✅ Cursor-based (stable, fast)
SELECT * FROM users WHERE id > :last_seen_id ORDER BY id LIMIT 20;
```

Cursor pagination ideal untuk infinite scroll, large datasets.

## Anti-patterns

### ❌ SELECT *
```sql
-- Bad
SELECT * FROM users;

-- Good (jauh lebih cepat di large tables)
SELECT id, name, email FROM users;
```

### ❌ Function on indexed column
```sql
-- Bad (index tidak dipakai)
SELECT * FROM users WHERE LOWER(email) = 'foo@bar.com';

-- Good (functional index atau normalized data)
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
-- atau
SELECT * FROM users WHERE email = 'foo@bar.com';  -- store normalized
```

### ❌ OR conditions
```sql
-- Bad (often full table scan)
SELECT * FROM users WHERE name = 'X' OR email = 'X';

-- Good (UNION ALL atau IN)
SELECT * FROM users WHERE name = 'X'
UNION ALL
SELECT * FROM users WHERE email = 'X' AND name != 'X';
```

### ❌ NOT IN with NULLs
```sql
-- Bad (returns no rows if any NULL)
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM banned);

-- Good
SELECT * FROM users WHERE id NOT IN (
    SELECT user_id FROM banned WHERE user_id IS NOT NULL
);
-- atau lebih clean
SELECT u.* FROM users u
LEFT JOIN banned b ON u.id = b.user_id
WHERE b.user_id IS NULL;
```

### ❌ Implicit type cast
```sql
-- Bad (idx_users_phone tidak terpakai karena cast)
SELECT * FROM users WHERE phone = 6281234567890;

-- Good (matching type)
SELECT * FROM users WHERE phone = '6281234567890';
```

### ❌ Cartesian explosion
```sql
-- Bad (no join condition)
SELECT * FROM orders, users;

-- Good
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
```

## Index strategy

```sql
-- B-tree (default): equality, range
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date DESC);

-- Composite index order matters
-- idx_orders_user_date berguna untuk:
--   WHERE user_id = X
--   WHERE user_id = X AND order_date BETWEEN ... AND ...
--   WHERE user_id = X ORDER BY order_date DESC
-- TIDAK berguna untuk:
--   WHERE order_date BETWEEN ... (user_id harus leading)

-- Partial index (subset of rows)
CREATE INDEX idx_active_users ON users (email) WHERE deleted_at IS NULL;

-- GIN (JSONB, full-text, array)
CREATE INDEX idx_users_metadata ON users USING gin (metadata);
CREATE INDEX idx_articles_fts ON articles USING gin (to_tsvector('english', body));

-- BRIN (large time-series tables)
CREATE INDEX idx_events_time ON events USING brin (created_at);
```

## Query plan reading

```sql
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.country = 'ID'
GROUP BY u.id;
```

Output (Postgres):
```
HashAggregate  (cost=X..Y rows=Z) (actual time=A..B rows=C)
  Group Key: u.id
  ->  Hash Left Join  (cost=X..Y rows=Z) (actual time=A..B rows=C)
        Hash Cond: (o.user_id = u.id)
        ->  Seq Scan on orders o  ← ⚠️ full table scan
        ->  Hash  (cost=X..Y rows=Z) (actual time=A..B rows=C)
              ->  Seq Scan on users u
                    Filter: (country = 'ID')
```

Red flags:
- **Seq Scan** pada large tables (perlu index)
- **Nested Loop** dengan outer table besar (consider Hash Join)
- **Sort** di memory high (`Sort Method: external merge Disk:`)
- **Actual rows** jauh lebih besar dari **estimated rows** (stale stats — run `ANALYZE`)

## Common optimization patterns

### Batch delete/update
```sql
-- Bad (locks table, large transaction)
DELETE FROM events WHERE created_at < '2020-01-01';

-- Good (batch by batch)
DELETE FROM events
WHERE id IN (
    SELECT id FROM events
    WHERE created_at < '2020-01-01'
    LIMIT 10000
);
-- Repeat sampai 0 rows affected
```

### Upsert
```sql
INSERT INTO users (id, name, email)
VALUES (1, 'X', 'x@bar.com')
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name, email = EXCLUDED.email;
```

### Recursive CTE (tree traversal)
```sql
WITH RECURSIVE org_tree AS (
    SELECT id, name, manager_id, 1 AS depth
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id, t.depth + 1
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree ORDER BY depth, name;
```

## Per-engine notes

### PostgreSQL

```sql
-- JSONB query
SELECT * FROM events
WHERE metadata @> '{"source": "web"}';

-- Full-text search
SELECT * FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'database & optimization');

-- Upsert with RETURNING
INSERT INTO ... ON CONFLICT ... DO UPDATE ...
RETURNING id, created_at;

-- Listen/notify untuk real-time
LISTEN new_order;
```

### BigQuery

```sql
-- Partitioning + clustering
CREATE TABLE events (
    id INT64,
    event_date DATE,
    user_id INT64
)
PARTITION BY event_date
CLUSTER BY user_id;

-- Approximate count (cheap)
SELECT APPROX_COUNT_DISTINCT(user_id) FROM events;

-- Window with QUALIFY
SELECT *
FROM (
    SELECT
        user_id,
        event_date,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_date DESC) AS rn
    FROM events
)
WHERE rn = 1
QUALIFY rn = 1;  -- BigQuery-specific
```

BigQuery optimization:
- Avoid `SELECT *` (full scan even of partitioned columns)
- Use partition pruning (`WHERE event_date BETWEEN ...`)
- Cluster by frequently filtered columns
- Use `APPROX_*` functions for large cardinality counts

### ClickHouse

```sql
-- Specialized aggregations
SELECT
    quantile(0.95)(response_time) AS p95,
    quantile(0.99)(response_time) AS p99,
    uniqExact(user_id) AS dau
FROM events;

-- ReplacingMergeTree untuk upsert
CREATE TABLE users (
    id UInt64,
    name String,
    updated_at DateTime
) ENGINE = ReplacingMergeTree(updated_at);

-- FINAL keyword after merge
SELECT * FROM users FINAL WHERE id = 1;
```

ClickHouse optimization:
- `PREWHERE` untuk filter sebelum SELECT
- `SAMPLE` untuk approximate queries di huge tables
- Materialized views untuk pre-aggregation

### DuckDB (in-process analytical)

```sql
-- Direct Parquet query tanpa load
SELECT * FROM read_parquet('events.parquet')
WHERE event_date >= '2024-01-01';

-- Export to Parquet
COPY (SELECT * FROM events) TO 'export.parquet' (FORMAT PARQUET);
```

DuckDB: in-memory analytics, embedded di Python/R/Julia, perfect untuk local data exploration.

## JSON/JSONB patterns

```sql
-- PostgreSQL JSONB
SELECT
    metadata->>'source' AS source,           -- text
    metadata->'tags' AS tags_json,           -- jsonb
    jsonb_array_length(metadata->'tags') AS tag_count
FROM events
WHERE metadata @> '{"priority": "high"}'
  AND metadata->>'source' IN ('web', 'mobile');
```

```sql
-- Create expression index
CREATE INDEX idx_events_priority ON events ((metadata->>'priority'));
```

## Common pitfalls

| Pitfall | Fix |
|---|---|
| Query plan stale | Run `ANALYZE table_name` (Postgres) atau equivalent |
| N+1 di app code | Aggregate di SQL, return 1 row per group |
| Implicit cast | Match types exactly di WHERE |
| LIKE '%foo%' | Full table scan, consider FTS index atau trigram |
| Large OFFSET pagination | Cursor-based: `WHERE id > :last_id` |
| Correlated subquery slow | Rewrite sebagai JOIN |
| NULL handling | Use `IS NULL` / `IS NOT NULL`, not `= NULL` |
| Date timezone issues | Store UTC, convert di app |

## Integration

- **ORM**: SQLAlchemy, Drizzle, Prisma, Knex — generate SQL + escape params (SQL injection safe)
- **Migration**: alembic (Python), drizzle-kit, prisma migrate
- **Lint**: `sqlfluff` untuk style + correctness
- **Performance**: `EXPLAIN ANALYZE`, pg_stat_statements (Postgres)

## Invokation

Auto-trigger:
- Edit file dengan `.sql` extension atau query string panjang
- Reference ke "optimize query", "slow query", "explain"
- ORM raw query / complex join

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…