Skip to content
Back to skills

Postgres Patterns

ASecurity

PostgreSQL best practices for query optimization, schema design, indexing, RLS, and connection management. Includes Drizzle ORM patterns for TypeScript — schema definition, type inference, query builder style. Based on Supabase and lobehub guidelines.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
data-aitypescriptsqlexpressdatabasesecurity

Works with

  • cursor
  • cli

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add the-hugin/RSIm --skill postgres-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Postgres Patterns?

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

Security grade badge for Postgres Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/the-hugin-postgres-patterns/badge)](https://www.skillsdirectory.com/skills/the-hugin-postgres-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: postgres-patterns
description: PostgreSQL best practices for query optimization, schema design, indexing, RLS, and connection management. Includes Drizzle ORM patterns for TypeScript — schema definition, type inference, query builder style. Based on Supabase and lobehub guidelines.
origin: ECC
group: domain
triggers:
  - "postgresql"
  - "postgres индекс"
  - "RLS"
  - "postgres оптимизация"
  - "sql схема"
  - "drizzle"
  - "drizzle orm"
  - "pgTable"
output: "optimized PostgreSQL queries, schema design, index strategy, RLS policies, Drizzle ORM schema and query patterns"
calls: []
---

# PostgreSQL Patterns

Quick reference for PostgreSQL best practices.

## When to Activate

- Writing SQL queries or migrations
- Designing database schemas
- Troubleshooting slow queries
- Implementing Row Level Security
- Setting up connection pooling
- Defining Drizzle ORM schemas and relations (TypeScript)

## Index Cheat Sheet

| Query Pattern | Index Type | Example |
|---|---|---|
| `WHERE col = value` | B-tree (default) | `CREATE INDEX idx ON t (col)` |
| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` |
| `WHERE a = x AND b > y` | Composite | `CREATE INDEX idx ON t (a, b)` |
| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
| Time-series ranges | BRIN | `CREATE INDEX idx ON t USING brin (col)` |

## Data Type Quick Reference

| Use Case | Correct Type | Avoid |
|---|---|---|
| IDs | `bigint` | `int`, random UUID |
| Strings | `text` | `varchar(255)` |
| Timestamps | `timestamptz` | `timestamp` |
| Money | `numeric(10,2)` | `float` |
| Flags | `boolean` | `varchar`, `int` |

## Common Patterns

### Composite Index Order

```sql
-- Equality columns first, then range columns
CREATE INDEX idx ON orders (status, created_at);
-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01'
```

### Covering Index (avoids table lookup)

```sql
CREATE INDEX idx ON users (email) INCLUDE (name, created_at);
-- SELECT email, name, created_at — no heap fetch needed
```

### Partial Index (smaller, faster)

```sql
CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL;
```

### RLS Policy (optimized)

```sql
CREATE POLICY policy ON orders
  USING ((SELECT auth.uid()) = user_id);  -- Wrap in SELECT!
```

### UPSERT

```sql
INSERT INTO settings (user_id, key, value)
VALUES (123, 'theme', 'dark')
ON CONFLICT (user_id, key)
DO UPDATE SET value = EXCLUDED.value;
```

### Cursor Pagination (O(1) vs OFFSET O(n))

```sql
SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20;
```

### Queue Processing (skip locked)

```sql
UPDATE jobs SET status = 'processing'
WHERE id = (
  SELECT id FROM jobs WHERE status = 'pending'
  ORDER BY created_at LIMIT 1
  FOR UPDATE SKIP LOCKED
) RETURNING *;
```

## Diagnostics

```sql
-- Find unindexed foreign keys
SELECT conrelid::regclass, a.attname
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
  AND NOT EXISTS (
    SELECT 1 FROM pg_index i
    WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
  );

-- Find slow queries (requires pg_stat_statements)
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC;

-- Check table bloat
SELECT relname, n_dead_tup, last_vacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
```

## Configuration Template

```sql
-- Connection limits (tune for RAM)
ALTER SYSTEM SET max_connections = 100;
ALTER SYSTEM SET work_mem = '8MB';

-- Timeouts
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
ALTER SYSTEM SET statement_timeout = '30s';

-- Monitoring
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Security defaults
REVOKE ALL ON SCHEMA public FROM public;

SELECT pg_reload_conf();
```

## Anti-Patterns

| Anti-Pattern | Problem | Fix |
|---|---|---|
| `OFFSET N` pagination | O(n) table scan | Cursor pagination with `id > $last` |
| `varchar(255)` | Arbitrary limit, no benefit | Use `text` |
| `float` for money | Rounding errors | `numeric(10,2)` |
| `timestamp` | No timezone info | `timestamptz` |
| Missing FK indexes | Slow JOINs | Index all foreign keys |
| `SELECT *` in RLS | Forces expression eval per row | Wrap auth calls in `SELECT` subquery |

---

*Based on Supabase Agent Skills — MIT License*

---

## Drizzle ORM (TypeScript)

> Применяй когда схема и запросы пишутся на TypeScript через Drizzle ORM.

### Config

```typescript
// drizzle.config.ts
export default defineConfig({
  dialect: 'postgresql',
  schema: './src/database/schemas/*',
  out: './src/database/migrations',
  strict: true,
});
```

### Naming Conventions

| Объект | Формат | Пример |
|---|---|---|
| Таблицы | plural snake_case | `users`, `session_groups` |
| Колонки | snake_case | `user_id`, `created_at` |

### Column Patterns

**Primary Key** — prefixed text ID (читаемый, distinguishable по типу):

```typescript
id: text('id')
  .primaryKey()
  .$defaultFn(() => idGenerator('agents'))
  .notNull(),
// Для внутренних таблиц: uuid вместо text
```

**Foreign Key** с cascade:

```typescript
userId: text('user_id')
  .references(() => users.id, { onDelete: 'cascade' })
  .notNull(),
```

**Timestamps** — через хелперы, не вручную:

```typescript
// _helpers.ts
export const createdAt = () => timestamptz('created_at').defaultNow().notNull();
export const updatedAt = () => timestamptz('updated_at').$onUpdate(() => new Date());
export const timestamps = { createdAt: createdAt(), updatedAt: updatedAt() };

// В схеме:
...timestamps,
```

**Indexes** — возвращай массив (не объект):

```typescript
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
```

### Full Table Example

```typescript
export const agents = pgTable(
  'agents',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => idGenerator('agents'))
      .notNull(),
    slug: varchar('slug', { length: 100 })
      .$defaultFn(() => randomSlug(4))
      .unique(),
    userId: text('user_id')
      .references(() => users.id, { onDelete: 'cascade' })
      .notNull(),
    clientId: text('client_id'),
    config: jsonb('config').$type<AgentConfig>(),
    ...timestamps,
  },
  (t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
);
```

### Type Inference

```typescript
export const insertAgentSchema = createInsertSchema(agents);
export type NewAgent = typeof agents.$inferInsert;
export type AgentItem  = typeof agents.$inferSelect;
```

### Junction Table (Many-to-Many)

```typescript
export const agentsKnowledgeBases = pgTable(
  'agents_knowledge_bases',
  {
    agentId: text('agent_id')
      .references(() => agents.id, { onDelete: 'cascade' }).notNull(),
    knowledgeBaseId: text('knowledge_base_id')
      .references(() => knowledgeBases.id, { onDelete: 'cascade' }).notNull(),
    userId: text('user_id')
      .references(() => users.id, { onDelete: 'cascade' }).notNull(),
    enabled: boolean('enabled').default(true),
    ...timestamps,
  },
  (t) => [primaryKey({ columns: [t.agentId, t.knowledgeBaseId] })],
);
```

### Query Style — критическое правило

**Всегда `db.select()`. Никогда `db.query.*`.**

`db.query.findMany/findFirst/with:` генерирует сложные lateral joins с `json_build_array` — хрупкие и трудно отлаживаемые.

**Select single row:**

```typescript
// ✅
const [result] = await db.select().from(agents).where(eq(agents.id, id)).limit(1);

// ❌
return db.query.agents.findFirst({ where: eq(agents.id, id) });
```

**Select with JOIN:**

```typescript
// ✅
const rows = await db
  .select({
    runId: evalRunTopics.runId,
    score: evalRunTopics.score,
    testCase: evalTestCases,
  })
  .from(evalRunTopics)
  .leftJoin(evalTestCases, eq(evalRunTopics.testCaseId, evalTestCases.id))
  .where(eq(evalRunTopics.runId, runId))
  .orderBy(asc(evalRunTopics.createdAt));

// ❌
return db.query.evalRunTopics.findMany({
  where: eq(evalRunTopics.runId, runId),
  with: { testCase: true },
});
```

**Aggregation:**

```typescript
// ✅
const rows = await db
  .select({
    id: datasets.id,
    name: datasets.name,
    count: count(testCases.id).as('count'),
  })
  .from(datasets)
  .leftJoin(testCases, eq(datasets.id, testCases.datasetId))
  .groupBy(datasets.id);
```

**One-to-Many — два отдельных запроса:**

```typescript
// ✅ Два простых запроса вместо relational with:
const [parent] = await db.select().from(datasets).where(eq(datasets.id, id)).limit(1);
if (!parent) return undefined;

const children = await db
  .select()
  .from(testCases)
  .where(eq(testCases.datasetId, id))
  .orderBy(asc(testCases.sortOrder));

return { ...parent, testCases: children };
```

**UPSERT:**

```typescript
await db
  .insert(settings)
  .values({ userId, key, value })
  .onConflictDoUpdate({
    target: [settings.userId, settings.key],
    set: { value: sql`excluded.value` },
  });
```

**Cursor pagination в Drizzle:**

```typescript
const rows = await db
  .select()
  .from(products)
  .where(gt(products.id, lastId))
  .orderBy(asc(products.id))
  .limit(20);
```

**Транзакции:**

```typescript
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values(userData).returning();
  await tx.insert(profiles).values({ userId: user.id, ...profileData });
});
```

### Drizzle Anti-Patterns

| Anti-Pattern | Проблема | Fix |
|---|---|---|
| `db.query.*` с `with:` | Lateral joins, хрупко | `db.select()` + `leftJoin()` |
| One-to-many через `with:` | Сложный JSON в памяти | Два отдельных запроса |
| Indexes как объект | Deprecated синтаксис | Возвращай массив `(t) => [...]` |
| Ручные timestamp поля | Несогласованность | Хелперы `createdAt()`, `updatedAt()` |
| `$inferInsert` не экспортируется | Дублирование типов вручную | Всегда экспортируй `NewX` и `XItem` |

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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.