诊断 PostgreSQL 慢查询、设计表结构与索引,并在生产库上安全执行迁移。
设计与多媒体
SQL
编写、审阅、调优 MySQL、SQLite、MariaDB、SQL Server 上的 SQL,并安全迁移。
它能做什么
生成和改写查询、JOIN、CTE、窗口函数、upsert、索引,以及建表、约束、迁移 DDL。每条 SQL 输出前都经过预检:值用占位符、动态标识符走白名单、破坏性 DML 必须带 WHERE、LIMIT 前必须有确定性 ORDER BY、对在线表执行 ALTER 前必须设置 lock_timeout。目标方言可配置为 PostgreSQL、MySQL、MariaDB、SQLite 或 SQL Server,并主动指出各引擎陷阱,如 MySQL 的 3 字节 utf8、PostgreSQL 非 C 区域下 LIKE 索引失效、PostgreSQL/SQLite/SQL Server 不自动给外键建索引。
什么时候用它
- 用 EXPLAIN 诊断慢查询或 JOIN 后行数翻倍
- 为新业务设计表结构、主键、索引与约束
- 为在线表规划扩-迁-收式 ALTER 流程
- 跨引擎迁移数据或把 CSV 灌入生产库
技能文档
User preferences and memory live in ~/Clawic/data/sql/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/sql/ or ~/clawic/sql/), move it to ~/Clawic/data/sql/.
When To Use
- Writing, reviewing, or optimizing SQL: queries, joins, CTEs, window functions, upserts
- Designing tables, keys, types, indexes, and constraints, or normalizing an existing model
- Diagnosing a slow query, a deadlock, a lock timeout, wrong totals, or duplicated rows
- Planning migrations and DDL that must not take a live database down
- Operating a database: backups, restores, monitoring, pooling, replication, partitioning
- Moving data in or out: CSV loads, dumps, engine-to-engine migrations
- Not for PostgreSQL server internals (vacuum tuning,
work_mem, xid wraparound) — that ispg; not for ORM-level modeling in a framework — that isprisma
Quick Reference
| Situation | Play |
|---|---|
| Query slow, cause unknown | EXPLAIN (ANALYZE, BUFFERS), fix the worst node first (→ Reading EXPLAIN, then performance.md) |
| Query was fast yesterday | Stats, data growth, or plan flip — the regression chain in debug.md |
| Index exists but is not used | Function on the column, type mismatch, wrong column order, or low selectivity (→ Traps, performance.md) |
| Totals inflated after adding a JOIN | 1:N fan-out — aggregate before joining (→ Traps) |
| Rows missing after adding a JOIN | LEFT JOIN filtered in WHERE became an inner join (→ Traps) |
| Paginating past the first few thousand rows | Keyset, never OFFSET (patterns.md) |
| Deadlock, lock timeout, or "could not obtain lock" | transactions.md — lock ordering and isolation |
| "Too many connections" / app hangs on connect | Pool sizing before max_connections (operations.md, orm.md) |
| Read-modify-write race, job queue | SELECT ... FOR UPDATE, add SKIP LOCKED for queues (patterns.md) |
| Schema change on a live table | Expand → migrate → contract, lock_timeout first (operations.md) |
| Designing a model from scratch | Keys, cardinality, normal forms, when to denormalize (modeling.md) |
| Known shape needed (tenants, tags, audit, state, history) | schemas.md |
| Storing or querying JSON / semi-structured data | json.md |
| Cohorts, funnels, retention, rollups, materialized views | analytics.md |
| Loading a CSV, dump/restore, engine-to-engine move | data-loading.md |
| Timestamps off by hours, DST, week/fiscal boundaries | datetime.md |
| Statement works on one engine, fails on another | dialects.md |
| Grants, least privilege, RLS, PII erasure, encryption | security.md |
| Seeding fixtures, isolating tests, testing a migration | testing.md |
| ORM emits terrible SQL, N+1, mystery transactions | orm.md |
| Single node at its limit: replicas, sharding, caching | scaling.md |
| Choosing an engine | SQLite embedded/local · PostgreSQL default for servers · MySQL when the platform dictates it · SQL Server in .NET/Windows shops (dialects.md) |
| Anything else | Reproduce on the smallest table that shows it, then: schema-shaped → modeling.md/schemas.md · query-shaped → patterns.md · slow → performance.md · ops-shaped → operations.md |
Core Rules
- Parameterize values; allowlist identifiers. Placeholders (
?,$1) stop injection for values, but table/column names cannot be bound — when those are dynamic, check them against a hardcoded allowlist, never interpolate user input. Full attack surface, includingLIKEandORDER BYinjection:security.md. - BIGINT (or UUIDv7) primary keys by default.
INToverflows at 2,147,483,647 — at a sustained 100 inserts/s that is 2.1B ÷ 100/s ≈ 248 days, and the fix is an outage-grade type change. Random UUIDv4 keys fragment the B-tree; UUIDv7/ULID keep insert locality (modeling.md). - Index for the query shape: equality columns first, then range/sort.
(user_id, created_at)servesWHERE user_id = ? AND created_at > ?andWHERE user_id = ?alone — nevercreated_atalone. A sequential scan on a filter matching more than roughly 5-10% of rows is the planner being right, not broken. - Index every foreign key column yourself. MySQL/InnoDB creates the index automatically; PostgreSQL, SQLite, and SQL Server do not. Without it, every join on the FK and every parent
DELETE(worse withON DELETE CASCADE) scans the whole child table — the slowest delete in most schemas is this one missing index. - Transactions stay short and never wait on the outside world. No HTTP calls, no user input inside
BEGIN...COMMIT: open transactions hold locks, and in PostgreSQL they also block vacuum, causing table bloat. Anything open past the >1 min monitoring threshold (operations.md) gets investigated. - NULL is three-valued.
NOT IN (subquery)returns zero rows if the subquery yields a single NULL — useNOT EXISTS.x = NULLis never true — useIS NULL.COUNT(col)skips NULLs;COUNT(*)counts rows. Aggregates over zero rows return NULL, not 0 — wrap inCOALESCEwhen a chart or invariant expects a number. - Types that avoid the next migration. Money →
NUMERIC/DECIMAL(float money loses cents in aggregation); timestamps →TIMESTAMPTZstored as UTC (datetime.md); strings →TEXTin PostgreSQL and SQLite (varchar(255)is a cargo-cult limit you will later raise); MySQL charset →utf8mb4(MySQL'sutf8is 3-byte and rejects emoji). - Migrations are additive first. Rename/retype/drop happens over multiple deploys with both versions live in between (expand-migrate-contract,
operations.md). A single-deploy column rename breaks every instance still running old code. - Rank before you tune.
pg_stat_statementsordered bytotal_exec_time(or the MySQL slow query log digested bypt-query-digest) tells you which query costs the most overall — usually not the one someone complained about. Total cost = mean latency × call count: a 5 ms query called 10,000×/min (50 s/min) outranks a 2 s report run hourly. Optimizing an unranked query is guessing.
Reading EXPLAIN
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 5; -- PostgreSQL
EXPLAIN ANALYZE SELECT ...; -- MySQL >=8.0.18
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 5; -- SQLite
SET STATISTICS PROFILE ON; -- SQL Server (or the graphical plan)
Read actual behavior, not just the plan. Plain EXPLAIN shows estimates only, and estimates are the part that lies.
Seq Scan/type: ALLon a large table with a selective filter → missing or unusable index (→ Traps for what disables one)Rows Removed by Filterhigh → the index found candidates but the filter did the work; extend the index to cover the filter- Estimated vs actual rows off by more than 10× → stale stats: run
ANALYZE tablename;. Still off → the planner assumes column independence; declare the correlation (CREATE STATISTICSon PostgreSQL >=10, histogram on MySQL 8) Buffers: readlarge vshit→ data is coming from disk; recheck on a warm cache before concluding- Nested Loop over thousands of outer rows → usually the >10× misestimate above feeding a bad join choice
- Node-by-node interpretation, join algorithms, and what to change for each:
performance.md
Index Strategy
-- Composite: equality columns first, range/sort last (rule 3)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Covering: index-only scan, no heap fetch (PostgreSQL >=11, SQL Server INCLUDE)
CREATE INDEX idx_orders_user ON orders(user_id) INCLUDE (total);
-- Partial/filtered: index only the rows you query (PostgreSQL, SQLite, SQL Server)
CREATE INDEX idx_orders_pending ON orders(user_id) WHERE status = 'pending';
-- Expression: make a function sargable (MySQL >=8.0.13 supports functional indexes)
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
- A plain B-tree on a low-cardinality column (
statuswith 5 values) rarely helps; a partial index on the rare value you actually query does. - An index on
(a, b)already servesWHERE a = ?— creating a second index on(a)alone adds write cost and reclaims nothing. Check for redundant prefixes before adding. - PostgreSQL with a non-C locale ignores B-tree indexes for
LIKE 'term%'— add thetext_pattern_opsopclass for prefix search. - Index-only scans still hit the heap for pages not marked all-visible; if
EXPLAINshowsHeap Fetcheshigh, the table needs aVACUUM(operations.md). - Every index taxes writes and consumes disk: drop unused ones (
pg_stat_user_indexeswhereidx_scan = 0), but only after uptime covers a full business cycle — month-end reports use indexes nothing else does.
Portability
| Feature | PostgreSQL | MySQL | SQLite | SQL Server |
|---|---|---|---|---|
| Limit | LIMIT n | LIMIT n | LIMIT n | TOP n / OFFSET-FETCH |
| Upsert | ON CONFLICT | ON DUPLICATE KEY | ON CONFLICT | MERGE |
| Boolean | true/false | 1/0 (TINYINT) | 1/0 | 1/0 (BIT) |
| Concat | || | CONCAT() | || | + or CONCAT() |
| Auto-id | GENERATED / SERIAL | AUTO_INCREMENT | INTEGER PRIMARY KEY | IDENTITY |
| Returning rows from DML | RETURNING | — (MariaDB has it) | RETURNING (>=3.35) | OUTPUT |
| Aggregate FILTER | Yes | CASE only | Yes (>=3.30) | CASE only |
| Transactional DDL | Yes | No (implicit commit) | Yes | Yes |
| Default string compare | Case-sensitive | Case-insensitive (_ci collations) | Case-sensitive | Case-insensitive by default |
Date functions, quoting, NULL sort order, collation, and the rest of the divergences: dialects.md.
Output Gates
Before emitting SQL, verify:
- Every value is a placeholder, and every dynamic identifier came from an allowlist?
UPDATE/DELETEhas aWHERE, or the full-table effect is explicitly intended?- Destructive DML was previewed as the equivalent
SELECTfirst? - No 1:N join feeds an aggregate without pre-aggregation, and no
DISTINCTis papering over one? LIMIT/TOPhas a deterministicORDER BYwith a unique tiebreaker?- New table: primary key type per rule 2, timestamps with zone, uniqueness scoped to the right columns, every FK column indexed (rule 4)?
- DDL against a live table:
lock_timeoutset, and the change is expand-only? - Every construct used exists in the target engine (→ Portability,
dialects.md)?
Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/sql/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| dialect | postgres | mysql | mariadb | sqlite | sqlserver | postgres | Syntax of every emitted statement and which caveats from Portability and dialects.md are surfaced |
| engine_version | text (e.g. 15, 8.0.35) | none | Gates features with a version floor (RETURNING, functional indexes, NULLS NOT DISTINCT); with none set, the conservative form is emitted |
| identifier_style | snake_case | camelCase | PascalCase | snake_case | Naming of tables, columns, indexes, and constraints in generated DDL |
| table_naming | plural | singular | plural | Whether generated tables read users or user |
| pk_type | bigint-identity | uuidv7 | uuidv4 | natural | bigint-identity | The primary key emitted by every CREATE TABLE (rule 2, modeling.md) |
| destructive_guard | bool | true | When true, UPDATE/DELETE without WHERE, DROP, and TRUNCATE are emitted as a transaction-wrapped preview with the matching SELECT first |
| migration_tool | text (flyway, alembic, golang-migrate, sqitch, ...) | none | File naming and up/down structure of generated migrations (operations.md) |
| timezone_policy | utc | local | utc | Whether timestamps are stored and compared as UTC and how datetime.md examples render |
| lock_timeout | text (duration: 500ms, 2s, 10s) | 2s | The SET lock_timeout emitted before every DDL statement against a live table (→ Traps, operations.md) |
| batch_size | number (rows, 100-100000) | 5000 | Chunk size for batched DELETE/UPDATE, backfills, and bulk loads (patterns.md, data-loading.md, json.md) |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:
- Tooling — client (psql, mysql, sqlite3, sqlcmd, a GUI), migration runner, SQL linter/formatter, local database as a container vs installed service — affects
operations.mdandtesting.mdexamples - Conventions — index and constraint naming, keyword casing, CTE vs subquery preference, comment style in DDL — affects every emitted statement
- Platform — managed service (RDS, Aurora, Cloud SQL, Neon, PlanetScale) vs self-hosted, available extensions, collation and locale — affects which features may be assumed
- Safety posture — how much confirmation destructive DDL/DML needs, whether production credentials are ever used directly, read-only-by-default sessions — affects
operations.mdand thedestructive_guardgate - Output format — full runnable script vs snippet, whether a down/rollback migration accompanies every up, inline comments — affects the shape of every deliverable
- Work order — schema-first vs query-first design, review gate before a migration reaches production — affects the sequence in
modeling.mdandoperations.md - Integrations — ORM in use, warehouse/BI target, monitoring stack — affects
orm.mdandanalytics.mdadvice - Constraints — vetoed features (no triggers, no stored procedures, no vendor extensions), compliance regime (retention windows, GDPR erasure) — affects
schemas.mdandsecurity.mdchoices - Thresholds — the operational budgets a shop usually standardizes:
lock_timeout,batch_size, retry cap on deadlock/serialization failures, pool sizing rule, and the alert levels (connection saturation, disk free, transaction age, backup age) — affectstransactions.mdretry loops and the Alert Thresholds table inoperations.md - Cadence — how often scheduled work runs: restore drills, rollup and materialized-view refresh, the trailing recompute window, data-quality assertion runs, automated partition creation, backup interval — affects
operations.md,analytics.md,schemas.md, andtesting.mdschedules
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
WHERE YEAR(created_at) = 2024 | Function on the column hides it from the index | Range: created_at >= '2024-01-01' AND created_at < '2025-01-01' |
BETWEEN '2024-01-01' AND '2024-01-31' on a timestamp | Upper bound is midnight — the whole last day is excluded with no error | Half-open range: >= '2024-01-01' AND < '2024-02-01' |
WHERE varchar_col = 123 | Implicit cast applies to the column, disabling its index | Match types: quote the literal or fix the schema |
LEFT JOIN t ... WHERE t.col = x | WHERE runs after the join and filters the NULL rows out → an INNER JOIN, with no error | Move the predicate into ON, or test t.id IS NULL for an anti-join |
Join 1:N then SUM/COUNT | Fan-out duplicates left-side rows before aggregation | Aggregate in a subquery/CTE, then join the result |
DISTINCT added to make duplicates go away | Hides a fan-out bug and forces a sort of the whole result | Find the join that multiplies rows; DISTINCT is a diagnosis, not a fix |
LIKE '%term' | Leading wildcard defeats B-tree ordering | Full-text search (schemas.md) or a trigram index (pg_trgm) |
Composite (a, b) for WHERE b = ? | B-tree is sorted by a first; b alone is unordered | Separate index on b, or reorder if a is always filtered |
WHERE a = ? OR b = ? | One index cannot serve two independent predicates; usually a full scan | UNION ALL of two indexed queries, deduplicated if needed |
LIMIT 10 without ORDER BY | Row order is undefined — "top 10" changes between runs and after a vacuum | Always ORDER BY with a unique tiebreaker column |
ORDER BY random() LIMIT n | Full scan plus sort of the entire table | TABLESAMPLE or a random-key probe (patterns.md) |
SELECT * in application code | Blocks index-only scans, fetches columns you drop, breaks on schema change | Name the columns |
DDL without lock_timeout | ALTER TABLE queues behind one long query and every new query queues behind it — brief lock, full outage | SET lock_timeout (default 2s, → Configuration) then retry (operations.md) |
Wrapping TRUNCATE/DDL in a transaction on MySQL | MySQL commits implicitly on DDL: the rollback you planned does not exist | Take a backup or use a copy table; transactional DDL is PostgreSQL/SQLite/SQL Server only |
Where Experts Disagree
- Surrogate vs natural keys. Surrogate (
BIGINT/UUID) is the default: natural keys change, and a changing PK cascades into every child row. Natural keys legitimately win on pure junction tables and on immutable code tables (ISO currency, country) where the extra id buys nothing (modeling.md). - Foreign keys at scale. Some high-write shops drop FK constraints because they add per-write lock and index cost and complicate online schema-change tooling. Default: keep them — orphan cleanup costs more than the writes saved. Drop only with a measured write bottleneck and integrity enforced elsewhere.
- Logic in the database. Triggers and stored procedures give atomicity nothing in the app can match and centralize rules across many clients; they are also invisible to code review, hard to test, and version-controlled poorly. Default: constraints and simple audit triggers in the database, business workflows in the application (
schemas.md). - Soft delete everywhere. Undo and audit are real requirements, but a
deleted_aton every table poisons every query and every unique constraint. Default: hard delete plus an audit log; soft-delete only the tables users actually restore (schemas.md).
Related Skills
More Clawic skills, get them at https://clawic.com/skills/sql (install if the user confirms):
pg— PostgreSQL server internals: vacuum, work_mem, wraparoundmysql— MySQL and InnoDB specificssqlite— SQLite concurrency, pragmas, and type affinityprisma— Node.js ORM modelingdbt— warehouse transformations and tests
Feedback
- If useful, star it: https://clawic.com/skills/sql
- Latest version: https://clawic.com/skills/sql
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/sql.
相关技能
为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。
围绕 explain 执行计划与具体规则,给出 MongoDB 模式设计、查询调优与故障处置方案。
诊断 GraphQL 缺陷并加固 schema、resolver 与客户端,处理 N+1、null 传播、订阅失效与联邦拆分。
调试、编写和审查 Go 代码,覆盖 goroutine、错误处理、模块与标准库的实践指导。
为 Google Cloud 架构选型、部署调试、安全加固与成本优化,每次建议都附带每月美元估算。