一键导入
data-modeling
Expert reference for relational and document data modeling — schema design, normalization, indexing strategy, and common pitfalls
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Expert reference for relational and document data modeling — schema design, normalization, indexing strategy, and common pitfalls
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Expert reference for application security — OWASP Top 10 mitigations, auth/authz, secrets management, cryptography, input validation, dependency hygiene, and secure-by-default code patterns
Expert reference for token counting, prompt compression, cost estimation, and quality preservation when optimizing prompts for Claude models
Experimentation design and A/B testing standards for product teams
Expert reference for digital accessibility — WCAG conformance, ARIA, and inclusive design patterns
Authoritative reference for agent architecture selection, multi-agent orchestration, tool design, memory systems, and failure mode prevention
Expert reference for evaluating LLM systems, RAG pipelines, and AI features in production
| name | data-modeling |
| description | Expert reference for relational and document data modeling — schema design, normalization, indexing strategy, and common pitfalls |
| tags | ["database","sql","schema","modeling","postgres","mongodb"] |
A data model is a set of promises your system makes about data integrity, access patterns, and consistency. Design for the queries you will run, not the objects you think you have.
Three questions before any schema decision:
id BIGSERIAL or id UUID. Natural keys as primary keys create coupling and migration pain.user_id without a FK constraint is a lie.created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() and updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(). Without them, debugging and auditing become archaeology.NOT NULL DEFAULT '' instead of nullable strings when absence is representable as empty.NUMERIC(19,4). Never FLOAT or DOUBLE for currency.CHECK constraint on a VARCHAR. Enums are hard to extend without migrations.| Form | Rule | Violation Example |
|---|---|---|
| 1NF | No repeating groups, atomic values | tags VARCHAR storing "a,b,c" |
| 2NF | No partial dependencies (on composite key) | order_items storing product_name (depends only on product_id) |
| 3NF | No transitive dependencies | employees storing department_name when department_id is present |
| BCNF | Every determinant is a candidate key | Rare — only relevant with overlapping composite keys |
Denormalize when:
cached_total_amount)deleted_at TIMESTAMPTZ, never use a boolean is_deletedposition INTEGER in the junction/child table, never derive order from created_atpassword_hash TEXT with bcrypt/argon2INTEGER (cents) or NUMERIC(19,4), never FLOATCreate an index when:
WHERE clause with equality or rangeORDER BY on large tablesIndex types (PostgreSQL):
-- Default — btree, good for equality and range
CREATE INDEX idx_users_email ON users(email);
-- Partial index — subset of rows, smaller and faster
CREATE INDEX idx_orders_pending ON orders(created_at) WHERE status = 'pending';
-- Composite index — covers multi-column WHERE
CREATE INDEX idx_events_user_time ON events(user_id, occurred_at DESC);
-- Unique constraint (also creates index)
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
Never:
CREATE TABLE document_history (
id BIGSERIAL PRIMARY KEY,
document_id BIGINT NOT NULL REFERENCES documents(id),
changed_by BIGINT NOT NULL REFERENCES users(id),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
field_name VARCHAR(100) NOT NULL,
old_value TEXT,
new_value TEXT
);
-- BAD: polymorphic with nullable FKs
CREATE TABLE comments (
id BIGSERIAL PRIMARY KEY,
post_id BIGINT REFERENCES posts(id), -- nullable
video_id BIGINT REFERENCES videos(id), -- nullable
body TEXT NOT NULL
);
-- GOOD: separate association tables
CREATE TABLE post_comments (comment_id BIGINT REFERENCES comments(id), post_id BIGINT REFERENCES posts(id), PRIMARY KEY (comment_id));
CREATE TABLE video_comments (comment_id BIGINT REFERENCES comments(id), video_id BIGINT REFERENCES videos(id), PRIMARY KEY (comment_id));
-- Use CHECK constraint, not ENUM
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','confirmed','shipped','delivered','cancelled'))
);
-- Document valid transitions in application layer, not DB
-- BAD — EAV makes every query a nightmare
CREATE TABLE attributes (entity_id INT, key VARCHAR, value TEXT);
-- GOOD — use JSONB for flexible attributes with known query patterns
ALTER TABLE products ADD COLUMN metadata JSONB;
-- Or use proper subtype tables if structure is known
-- BAD
INSERT INTO posts (tags) VALUES ('sql,database,tutorial');
-- GOOD (PostgreSQL)
CREATE TABLE post_tags (post_id BIGINT REFERENCES posts(id), tag VARCHAR(50) NOT NULL, PRIMARY KEY (post_id, tag));
-- Or for unstructured tags:
ALTER TABLE posts ADD COLUMN tag_ids BIGINT[];
-- BAD — email can be NULL, but a user without email is undefined
CREATE TABLE users (id BIGSERIAL PRIMARY KEY, email VARCHAR(255));
-- GOOD
CREATE TABLE users (id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL);
CREATE UNIQUE INDEX ON users(email);
-- BAD
price FLOAT -- 0.1 + 0.2 = 0.30000000000000004
-- GOOD
price_cents INTEGER NOT NULL -- store 1999 for $19.99
-- Or:
price NUMERIC(19,4) NOT NULL
-- BAD — orphaned rows accumulate silently
CREATE TABLE order_items (order_id BIGINT REFERENCES orders(id));
-- GOOD — explicit cascade decision
CREATE TABLE order_items (order_id BIGINT REFERENCES orders(id) ON DELETE CASCADE);
-- or ON DELETE RESTRICT if orphans should be prevented
BAD: User profile with everything in one table
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR,
email VARCHAR,
billing_street VARCHAR,
billing_city VARCHAR,
billing_country VARCHAR,
shipping_street VARCHAR,
shipping_city VARCHAR,
subscription_plan VARCHAR,
subscription_expires DATE,
stripe_customer_id VARCHAR
);
GOOD: Normalized
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX ON users(email);
CREATE TABLE addresses (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL CHECK (type IN ('billing','shipping')),
street VARCHAR(255) NOT NULL,
city VARCHAR(100) NOT NULL,
country CHAR(2) NOT NULL, -- ISO 3166
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE subscriptions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
plan VARCHAR(50) NOT NULL,
expires_at TIMESTAMPTZ,
stripe_customer_id VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
deleted_at)