skills/database/SKILL.md
Database design conventions for DynamoDB single-table design and SQL/PostgreSQL. Use when: modeling access patterns, designing DynamoDB tables and GSIs, writing SQL migrations, configuring connection pools, optimizing queries, choosing between SQL and NoSQL, designing data models for serverless applications, auditing existing database schemas for performance or design issues, or improving query patterns and indexing.
npx skillsauth add michaelsvanbeek/personal-agent-skills databaseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Factor | Choose DynamoDB | Choose PostgreSQL/SQL | |--------|----------------|----------------------| | Access patterns | Known, finite, high-volume | Ad-hoc, complex joins, evolving | | Scaling | Horizontal (single-digit ms at any scale) | Vertical (RDS, Aurora Serverless) | | Schema | Flexible, evolving attributes | Strict, relational integrity required | | Cost model | Pay-per-request works well | Provisioned instance always running | | Serverless fit | Native (no connection pool needed) | Needs RDS Proxy or connection pooling | | Transactions | Simple (up to 100 items, 4MB) | Complex multi-table ACID |
Default for homelab scripts: Use simple file-based storage (JSON, SQLite) or InfluxDB for time-series. DynamoDB/Postgres for production services.
Always start with access patterns — never start with an entity-relationship diagram:
PK SK Attributes
──────────────── ───────────────────── ──────────────────
USER#<id> PROFILE name, email, created_at
USER#<id> ORDER#<id> total, status, created_at
USER#<id> ORDER#<id>#ITEM#<id> product_id, qty, price
ORG#<id> META name, plan, created_at
ORG#<id> USER#<id> role, joined_at
Naming conventions:
ENTITY_TYPE#value (uppercase type, # separator).snake_case.gsi1, gsi2 (generic) with gsi1pk / gsi1sk attributes.{service}-{stage} (e.g., my-api-prod).One-to-many — parent entity PK, children share PK with different SK prefix:
# Get user profile: PK=USER#123, SK=PROFILE
# Get user orders: PK=USER#123, SK begins_with ORDER#
Many-to-many — use inverted GSI:
# PK=USER#1, SK=GROUP#A → "User 1 belongs to Group A"
# GSI1: gsi1pk=GROUP#A, gsi1sk=USER#1 → "Group A contains User 1"
Time-series queries — SK contains ISO 8601 timestamp:
# PK=DEVICE#abc, SK=READING#2024-01-15T10:30:00Z
# Query with SK between for time range
condition_expression for writes to prevent overwrites.UpdateExpression to modify attributes — never read-modify-write.LastEvaluatedKey in responses.batch_get_item, batch_write_item) for bulk access (max 25 writes, 100 reads per batch).import boto3
from boto3.dynamodb.conditions import Key
table = boto3.resource("dynamodb").Table("my-api-prod")
# Put with condition (prevent overwrite)
table.put_item(
Item={"pk": "USER#123", "sk": "PROFILE", "name": "Alice"},
ConditionExpression="attribute_not_exists(pk)",
)
# Query one-to-many
response = table.query(
KeyConditionExpression=Key("pk").eq("USER#123") & Key("sk").begins_with("ORDER#"),
ScanIndexForward=False, # newest first
Limit=20,
)
items = response["Items"]
# Paginate
while "LastEvaluatedKey" in response:
response = table.query(
KeyConditionExpression=Key("pk").eq("USER#123") & Key("sk").begins_with("ORDER#"),
ExclusiveStartKey=response["LastEvaluatedKey"],
)
items.extend(response["Items"])
snake_case, plural (users, orders, order_items).snake_case, no table prefix (id, not user_id in the users table).id (prefer bigint generated always as identity or uuid).<singular_table>_id (e.g., user_id in orders).created_at and updated_at with timestamptz.deleted_at timestamptz column — never physically delete user-facing data.is_ or has_ (e.g., is_active, has_verified_email).enum types or text with CHECK constraints. Never store magic integers.CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
name text NOT NULL,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
CREATE INDEX idx_users_email ON users (email) WHERE deleted_at IS NULL;
YYYYMMDDHHMMSS_description.sql or Alembic auto-generated._id columns used in JOINs or WHERE clauses.WHERE deleted_at IS NULL).EXPLAIN ANALYZE to verify query plans use indexes.For serverless (Lambda + RDS):
max_connections / expected_concurrency.For long-running processes:
pool_size, max_overflow, pool_recycle.pool_pre_ping=True to detect stale connections.from sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:pass@host/db",
pool_size=5,
max_overflow=10,
pool_recycle=3600,
pool_pre_ping=True,
)
SELECT ... FOR UPDATE when reading rows you intend to modify.For homelab monitoring (the primary time-series use case):
disk_health, cert_expiry, network_scan).host, device, script_name). Low cardinality only.temperature, days_remaining, response_time_ms). Not indexed.from influxdb_client import InfluxDBClient, Point
point = (
Point("disk_health")
.tag("host", "nas01")
.tag("device", "/dev/sda")
.field("temperature", 38)
.field("reallocated_sectors", 0)
.field("power_on_hours", 12345)
)
For homelab scripts that need local state persistence:
sqlite3 from stdlib — no extra dependencies.PRAGMA journal_mode=WAL;PRAGMA foreign_keys=ON; at connection time.with conn: context manager for automatic transaction commit/rollback./data/script-name/state.db).import sqlite3
from pathlib import Path
db_path = Path("/data/my-script/state.db")
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
with conn:
conn.execute(
"CREATE TABLE IF NOT EXISTS runs (id INTEGER PRIMARY KEY, ran_at TEXT, status TEXT)"
)
For database migration strategies and zero-downtime schema changes, see the data-migrations skill. For API design patterns that query these data models, see the api-design skill. For loading data into databases via dlt pipelines, see the data-pipelines skill.
development
TypeScript coding standards and type safety conventions. Use when: creating TypeScript files, defining interfaces and types, writing type-safe code, reviewing TypeScript for type correctness, auditing a codebase for type safety gaps, eliminating any or ts-ignore usage, or improving strict-mode compliance. Covers strict typing, avoiding any and ts-ignore, discriminated unions, Zod runtime validation, immutability patterns, and proper type definitions.
testing
Writing clear, actionable tickets in any issue tracker (Jira, Linear, GitHub Issues, ServiceNow, etc.). Use when: creating epics, stories, tasks, bugs, or spikes; writing acceptance criteria; decomposing work for a sprint; linking dependencies between tickets; auditing backlog items for clarity; or coaching a team on ticket quality. Covers title conventions, description templates, acceptance criteria, decomposition rules, dependency linking, and org-specific pluggable configuration.
development
Testing strategy, patterns, and evaluation for software and LLM/AI systems. Use when: writing tests, choosing test boundaries, designing test data, structuring test suites, evaluating LLM outputs, building evaluation pipelines, setting coverage thresholds, auditing test coverage gaps in existing projects, or improving test quality and structure.
development
Writing effective status updates for different audiences and cadences. Use when: writing a weekly status update, preparing a monthly summary, drafting a quarterly review, sending updates to leadership, sharing progress with stakeholders, or improving the clarity and impact of team communications. Covers weekly, monthly, and quarterly formats tailored for upward, lateral, and downward communication.