Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use this skill when designing or reviewing a PostgreSQL-specific schema. Covers best-practices, data types, indexing, constraints, performance patterns, and advanced features
.claude/skills/wshobson-postgresql-table-design/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 94% | 20 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 55% | 0% |
The rules and decision points for a PostgreSQL schema. The full data-type catalog, workload patterns (update-heavy, insert-heavy, upsert, schema evolution), extensions, JSONB indexing, and worked DDL examples are in references/details.md; open it when a section below points there.
BIGINT GENERATED ALWAYS AS IDENTITY; use UUID only when global uniqueness/opacity is needed.NUMERIC for exact decimal arithmetic).snake_case.UNIQUE NULLS NOT DISTINCT (...) (PG15+) to restrict to one NULL.NUMERIC(2,0) fails, unlike databases that silently truncate or round.CLUSTER is a one-off reorganization, not maintained on later inserts.BIGINT GENERATED ALWAYS AS IDENTITY; UUID for distributed or opaque IDs, generated with uuidv7() (PG18+) or gen_random_uuid().BIGINT unless storage is critical; DOUBLE PRECISION over REAL; NUMERIC(p,s) for money and exact decimals.TEXT, with CHECK (LENGTH(col) <= n) when a limit is needed; BYTEA for binary. Case-insensitive lookups: expression index on LOWER(col), or CITEXT when a constraint must be case-insensitive.TIMESTAMPTZ, DATE, INTERVAL. now() is transaction start; clock_timestamp() is wall clock.BOOLEAN NOT NULL unless tri-state is required.CREATE TYPE ... AS ENUM only for small, stable sets; evolving business values get TEXT + CHECK or a lookup table.references/details.md.| Avoid | Use instead | |---|---| | timestamp (without time zone) | timestamptz | | char(n), varchar(n) | text (+ CHECK on length if needed) | | money | numeric | | timetz | timestamptz | | timestamptz(0) or any precision | timestamptz | | serial | generated always as identity |
ON DELETE/UPDATE (CASCADE, RESTRICT, SET NULL, SET DEFAULT). Index the referencing column. Use DEFERRABLE INITIALLY DEFERRED for circular dependencies checked at commit.NULLS NOT DISTINCT (PG15+). Prefer NULLS NOT DISTINCT unless duplicate NULLs are wanted.NOT NULL: price NUMERIC NOT NULL CHECK (price > 0).EXCLUDE USING gist (room_id WITH =, booking_period WITH &&) stops double-booking. Needs a GiST-capable type.=, <, >, BETWEEN, ORDER BY).WHERE a = ? AND b > ? uses (a,b); WHERE b = ? does not). Most selective columns first.CREATE INDEX ON tbl (id) INCLUDE (name, email) for index-only scans.CREATE INDEX ON tbl (user_id) WHERE status = 'active'.CREATE INDEX ON tbl (LOWER(email)); the query must use the same expression.PARTITION BY RANGE (created_at); TimescaleDB automates it with retention and compression), LIST for discrete values, HASH for even distribution without a natural key.CHECK constraints; declarative partitioning (PG10+) creates them for you.sqlCREATE TABLE users ( user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX ON users (LOWER(email)); CREATE INDEX ON users (created_at);
sqlCREATE TABLE orders ( order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(user_id), status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','PAID','CANCELED')), total NUMERIC(10,2) NOT NULL CHECK (total > 0), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX ON orders (user_id); CREATE INDEX ON orders (created_at);
sql-- JSONB attributes with a generated, indexable scalar CREATE TABLE profiles ( user_id BIGINT PRIMARY KEY REFERENCES users(user_id), attrs JSONB NOT NULL DEFAULT '{}', theme TEXT GENERATED ALWAYS AS (attrs->>'theme') STORED ); CREATE INDEX profiles_attrs_gin ON profiles USING GIN (attrs);
references/details.md holds the material this file only names:
TEMPORARY, UNLOGGED) and row-level security.pg_trgm, citext, timescaledb, postgis, pgvector, and more).jsonb_path_ops and extracted B-tree columns.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 29,417 | 27,327 | -7% | 1 | 1 | 0% | 5,090 | 7,575 | +49% | 0 | 0 | — |
case-02 | fail→pass | 24,385 | 38,770 | +59% | 1 | 1 | 0% | 3,526 | 5,024 | +42% | 0 | 0 | — |
case-03 | fail→pass | 26,658 | 23,907 | -10% | 1 | 1 | 0% | 3,452 | 5,393 | +56% | 0 | 0 | — |
case-04 | fail→fail | 24,992 | 35,973 | +44% | 1 | 1 | 0% | 3,789 | 5,010 | +32% | 0 | 0 | — |
case-05 | pass→pass | 11,823 | 14,826 | +25% | 1 | 1 | 0% | 1,294 | 3,716 | +187% | 0 | 0 | — |
case-06 | pass→pass | 17,404 | 14,611 | -16% | 1 | 1 | 0% | 2,028 | 3,706 | +83% | 0 | 0 | — |
case-07 | fail→pass | 29,289 | 18,490 | -37% | 1 | 1 | 0% | 1,896 | 4,472 | +136% | 0 | 0 | — |
case-08 | fail→pass | 23,912 | 19,702 | -18% | 1 | 1 | 0% | 3,114 | 4,837 | +55% | 0 | 0 | — |
case-09 | pass→pass | 19,840 | 15,081 | -24% | 1 | 1 | 0% | 1,339 | 3,148 | +135% | 0 | 0 | — |
case-10 | fail→pass | 18,013 | 20,343 | +13% | 1 | 1 | 0% | 2,622 | 5,122 | +95% | 0 | 0 | — |
case-11 | pass→pass | 19,720 | 16,941 | -14% | 1 | 1 | 0% | 2,559 | 4,100 | +60% | 0 | 0 | — |
case-12 | pass→pass | 11,538 | 10,893 | -6% | 1 | 1 | 0% | 1,170 | 2,975 | +154% | 0 | 0 | — |
case-13 | pass→pass | 10,992 | 11,730 | +7% | 1 | 1 | 0% | 997 | 2,957 | +197% | 0 | 0 | — |
case-14 | pass→pass | 17,430 | 12,396 | -29% | 1 | 1 | 0% | 2,287 | 3,290 | +44% | 0 | 0 | — |
case-15 | pass→pass | 18,316 | 20,045 | +9% | 1 | 1 | 0% | 2,341 | 4,738 | +102% | 0 | 0 | — |
case-16 | pass→pass | 19,652 | 19,555 | -0% | 1 | 1 | 0% | 2,572 | 4,788 | +86% | 0 | 0 | — |
case-17 | pass→pass | 13,699 | 11,425 | -17% | 1 | 1 | 0% | 1,450 | 3,191 | +120% | 0 | 0 | — |
case-18 | pass→pass | 26,168 | 22,772 | -13% | 1 | 1 | 0% | 3,534 | 5,173 | +46% | 0 | 0 | — |
case-19 | pass→pass | 15,066 | 14,120 | -6% | 1 | 1 | 0% | 1,726 | 3,545 | +105% | 0 | 0 | — |
case-20 | pass→pass | 14,965 | 16,471 | +10% | 1 | 1 | 0% | 1,823 | 4,442 | +144% | 0 | 0 | — |
case-21 | pass→pass | 26,299 | 20,584 | -22% | 1 | 1 | 0% | 3,666 | 5,181 | +41% | 0 | 0 | — |
case-22 | pass→pass | 23,744 | 22,508 | -5% | 1 | 1 | 0% | 3,273 | 5,135 | +57% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/7/2026 | +18% |
Other measured skills in the registry, with their headline benchmark lift.