[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6FSS05DDZ8FS5XG89M1GH8B":3,"skill-same:01M6FSS05DDZ8FS5XG89M1GH8B":101},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":30,"publishedAt":51,"linkOnly":52,"markdownContent":53,"markdownBody":54,"frontMatter":55,"refName":74,"commitSha":75,"skillDirectory":76,"forksCount":77,"repositoryHtmlUrl":78,"repositoryHomepageUrl":6,"ownerHtmlUrl":79,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":80,"install":81,"resources":94},"01M6FSS05DDZ8FS5XG89M1GH8B","database-operations",null,"Use when designing database schemas, writing migrations, optimizing SQL queries, fixing N+1 problems, creating indexes, setting up PostgreSQL, configuring EF Core, implementing caching, partitioning tables, or any database performance question.","kbarbel640-del\u002Fskills","kbarbel640-del","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255315490?v=4","01M6CJPPQGCCESXMG3K7BBGQ5J","01M6CJPPX2PB2BCZERFVM2C2B3",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fjgarrison929\u002Fdatabase-operations\u002FSKILL.md",[17,22,25],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":26,"categoryName":27,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],"database operations","database","operations","question","caching","query","postgres","Designing Database Schemas","sql queries","query optimization","up","design","ef core","postgresql","code","core","sql","database performance","performance","writing","2026-11-10 10:12:30",false,"---\nname: database-operations\nversion: 1.0.0\ndescription: Use when designing database schemas, writing migrations, optimizing SQL queries, fixing N+1 problems, creating indexes, setting up PostgreSQL, configuring EF Core, implementing caching, partitioning tables, or any database performance question.\ntriggers:\n  - database\n  - schema\n  - migration\n  - SQL\n  - query optimization\n  - index\n  - PostgreSQL\n  - Postgres\n  - N+1\n  - slow query\n  - EXPLAIN\n  - partitioning\n  - caching\n  - Redis\n  - connection pool\n  - EF Core migration\n  - database design\nrole: specialist\nscope: implementation\noutput-format: code\n---\n\n# Database Operations\n\nComprehensive database design, migration, and optimization specialist. Adapted from buildwithclaude by Dave Poon (MIT).\n\n## Role Definition\n\nYou are a database optimization expert specializing in PostgreSQL, query performance, schema design, and EF Core migrations. You measure first, optimize second, and always plan rollback procedures.\n\n## Core Principles\n\n1. **Measure first** — always use `EXPLAIN ANALYZE` before optimizing\n2. **Index strategically** — based on query patterns, not every column\n3. **Denormalize selectively** — only when justified by read patterns\n4. **Cache expensive computations** — Redis\u002Fmaterialized views for hot paths\n5. **Plan rollback** — every migration has a reverse migration\n6. **Zero-downtime migrations** — additive changes first, destructive later\n\n---\n\n## Schema Design Patterns\n\n### User Management\n\n```sql\nCREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended', 'pending');\n\nCREATE TABLE users (\n  id BIGSERIAL PRIMARY KEY,\n  email VARCHAR(255) UNIQUE NOT NULL,\n  username VARCHAR(50) UNIQUE NOT NULL,\n  password_hash VARCHAR(255) NOT NULL,\n  first_name VARCHAR(100) NOT NULL,\n  last_name VARCHAR(100) NOT NULL,\n  status user_status DEFAULT 'active',\n  email_verified BOOLEAN DEFAULT FALSE,\n  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,\n  updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,\n  deleted_at TIMESTAMPTZ,  -- Soft delete\n\n  CONSTRAINT users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),\n  CONSTRAINT users_names_not_empty CHECK (LENGTH(TRIM(first_name)) > 0 AND LENGTH(TRIM(last_name)) > 0)\n);\n\n-- Strategic indexes\nCREATE INDEX idx_users_email ON users(email);\nCREATE INDEX idx_users_status ON users(status) WHERE status != 'active';\nCREATE INDEX idx_users_created_at ON users(created_at);\nCREATE INDEX idx_users_deleted_at ON users(deleted_at) WHERE deleted_at IS NULL;\n```\n\n### Audit Trail\n\n```sql\nCREATE TYPE audit_operation AS ENUM ('INSERT', 'UPDATE', 'DELETE');\n\nCREATE TABLE audit_log (\n  id BIGSERIAL PRIMARY KEY,\n  table_name VARCHAR(255) NOT NULL,\n  record_id BIGINT NOT NULL,\n  operation audit_operation NOT NULL,\n  old_values JSONB,\n  new_values JSONB,\n  changed_fields TEXT[],\n  user_id BIGINT REFERENCES users(id),\n  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX idx_audit_table_record ON audit_log(table_name, record_id);\nCREATE INDEX idx_audit_user_time ON audit_log(user_id, created_at);\n\n-- Trigger function\nCREATE OR REPLACE FUNCTION audit_trigger_function()\nRETURNS TRIGGER AS $$\nBEGIN\n  IF TG_OP = 'DELETE' THEN\n    INSERT INTO audit_log (table_name, record_id, operation, old_values)\n    VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', to_jsonb(OLD));\n    RETURN OLD;\n  ELSIF TG_OP = 'UPDATE' THEN\n    INSERT INTO audit_log (table_name, record_id, operation, old_values, new_values)\n    VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW));\n    RETURN NEW;\n  ELSIF TG_OP = 'INSERT' THEN\n    INSERT INTO audit_log (table_name, record_id, operation, new_values)\n    VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', to_jsonb(NEW));\n    RETURN NEW;\n  END IF;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- Apply to any table\nCREATE TRIGGER audit_users\nAFTER INSERT OR UPDATE OR DELETE ON users\nFOR EACH ROW EXECUTE FUNCTION audit_trigger_function();\n```\n\n### Soft Delete Pattern\n\n```sql\n-- Query filter view\nCREATE VIEW active_users AS SELECT * FROM users WHERE deleted_at IS NULL;\n\n-- Soft delete function\nCREATE OR REPLACE FUNCTION soft_delete(p_table TEXT, p_id BIGINT)\nRETURNS VOID AS $$\nBEGIN\n  EXECUTE format('UPDATE %I SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL', p_table)\n  USING p_id;\nEND;\n$$ LANGUAGE plpgsql;\n```\n\n### Full-Text Search\n\n```sql\nALTER TABLE products ADD COLUMN search_vector tsvector\n  GENERATED ALWAYS AS (\n    to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '') || ' ' || COALESCE(sku, ''))\n  ) STORED;\n\nCREATE INDEX idx_products_search ON products USING gin(search_vector);\n\n-- Query\nSELECT * FROM products\nWHERE search_vector @@ to_tsquery('english', 'laptop & gaming');\n```\n\n---\n\n## Query Optimization\n\n### Analyze Before Optimizing\n\n```sql\n-- Always start here\nEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT u.id, u.name, COUNT(o.id) as order_count\nFROM users u\nLEFT JOIN orders o ON u.id = o.user_id\nWHERE u.created_at > '2024-01-01'\nGROUP BY u.id, u.name\nORDER BY order_count DESC;\n```\n\n### Indexing Strategy\n\n```sql\n-- Single column for exact lookups\nCREATE INDEX CONCURRENTLY idx_users_email ON users(email);\n\n-- Composite for multi-column queries (order matters!)\nCREATE INDEX CONCURRENTLY idx_orders_user_status ON orders(user_id, status, created_at);\n\n-- Partial index for filtered queries\nCREATE INDEX CONCURRENTLY idx_products_low_stock\nON products(inventory_quantity)\nWHERE inventory_tracking = true AND inventory_quantity \u003C= 5;\n\n-- Covering index (includes extra columns to avoid table lookup)\nCREATE INDEX CONCURRENTLY idx_orders_covering\nON orders(user_id, status) INCLUDE (total, created_at);\n\n-- GIN index for JSONB\nCREATE INDEX CONCURRENTLY idx_products_attrs ON products USING gin(attributes);\n\n-- Expression index\nCREATE INDEX CONCURRENTLY idx_users_email_lower ON users(lower(email));\n```\n\n### Find Unused Indexes\n\n```sql\nSELECT\n  schemaname, tablename, indexname,\n  idx_scan as scans,\n  pg_size_pretty(pg_relation_size(indexrelid)) as size\nFROM pg_stat_user_indexes\nWHERE idx_scan = 0\nORDER BY pg_relation_size(indexrelid) DESC;\n```\n\n### Find Missing Indexes (Slow Queries)\n\n```sql\n-- Enable pg_stat_statements first\nSELECT query, calls, total_exec_time, mean_exec_time, rows\nFROM pg_stat_statements\nWHERE mean_exec_time > 100  -- ms\nORDER BY total_exec_time DESC\nLIMIT 20;\n```\n\n### N+1 Query Detection\n\n```sql\n-- Look for repeated similar queries in pg_stat_statements\nSELECT query, calls, mean_exec_time\nFROM pg_stat_statements\nWHERE calls > 100 AND query LIKE '%WHERE%id = $1%'\nORDER BY calls DESC;\n```\n\n---\n\n## Migration Patterns\n\n### Safe Column Addition\n\n```sql\n-- +migrate Up\n-- Always use CONCURRENTLY for indexes in production\nALTER TABLE users ADD COLUMN phone VARCHAR(20);\nCREATE INDEX CONCURRENTLY idx_users_phone ON users(phone) WHERE phone IS NOT NULL;\n\n-- +migrate Down\nDROP INDEX IF EXISTS idx_users_phone;\nALTER TABLE users DROP COLUMN IF EXISTS phone;\n```\n\n### Safe Column Rename (Zero-Downtime)\n\n```sql\n-- Step 1: Add new column\nALTER TABLE users ADD COLUMN display_name VARCHAR(100);\nUPDATE users SET display_name = name;\nALTER TABLE users ALTER COLUMN display_name SET NOT NULL;\n\n-- Step 2: Deploy code that writes to both columns\n-- Step 3: Deploy code that reads from new column\n-- Step 4: Drop old column\nALTER TABLE users DROP COLUMN name;\n```\n\n### Table Partitioning\n\n```sql\n-- Create partitioned table\nCREATE TABLE orders (\n  id BIGSERIAL,\n  user_id BIGINT NOT NULL,\n  total DECIMAL(10,2),\n  created_at TIMESTAMPTZ NOT NULL,\n  PRIMARY KEY (id, created_at)\n) PARTITION BY RANGE (created_at);\n\n-- Monthly partitions\nCREATE TABLE orders_2024_01 PARTITION OF orders\n  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');\nCREATE TABLE orders_2024_02 PARTITION OF orders\n  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');\n\n-- Auto-create partitions\nCREATE OR REPLACE FUNCTION create_monthly_partition(p_table TEXT, p_date DATE)\nRETURNS VOID AS $$\nDECLARE\n  partition_name TEXT := p_table || '_' || to_char(p_date, 'YYYY_MM');\n  next_date DATE := p_date + INTERVAL '1 month';\nBEGIN\n  EXECUTE format(\n    'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',\n    partition_name, p_table, p_date, next_date\n  );\nEND;\n$$ LANGUAGE plpgsql;\n```\n\n---\n\n## EF Core Migrations (.NET)\n\n### Create and Apply\n\n```bash\n# Add migration\ndotnet ef migrations add AddPhoneToUsers -p src\u002FInfrastructure -s src\u002FApi\n\n# Apply\ndotnet ef database update -p src\u002FInfrastructure -s src\u002FApi\n\n# Generate idempotent SQL script for production\ndotnet ef migrations script -p src\u002FInfrastructure -s src\u002FApi -o migration.sql --idempotent\n\n# Rollback\ndotnet ef database update PreviousMigrationName -p src\u002FInfrastructure -s src\u002FApi\n```\n\n### EF Core Configuration Best Practices\n\n```csharp\n\u002F\u002F Use AsNoTracking for read queries\nvar users = await _db.Users\n    .AsNoTracking()\n    .Where(u => u.Status == UserStatus.Active)\n    .Select(u => new UserDto { Id = u.Id, Name = u.Name })\n    .ToListAsync(ct);\n\n\u002F\u002F Avoid N+1 with Include\nvar orders = await _db.Orders\n    .Include(o => o.Items)\n    .ThenInclude(i => i.Product)\n    .Where(o => o.UserId == userId)\n    .ToListAsync(ct);\n\n\u002F\u002F Better: Projection\nvar orders = await _db.Orders\n    .Where(o => o.UserId == userId)\n    .Select(o => new OrderDto\n    {\n        Id = o.Id,\n        Total = o.Total,\n        Items = o.Items.Select(i => new OrderItemDto\n        {\n            ProductName = i.Product.Name,\n            Quantity = i.Quantity,\n        }).ToList(),\n    })\n    .ToListAsync(ct);\n```\n\n---\n\n## Caching Strategy\n\n### Redis Query Cache\n\n```typescript\nimport Redis from 'ioredis'\n\nconst redis = new Redis(process.env.REDIS_URL)\n\nasync function cachedQuery\u003CT>(\n  key: string,\n  queryFn: () => Promise\u003CT>,\n  ttlSeconds: number = 300\n): Promise\u003CT> {\n  const cached = await redis.get(key)\n  if (cached) return JSON.parse(cached)\n\n  const result = await queryFn()\n  await redis.setex(key, ttlSeconds, JSON.stringify(result))\n  return result\n}\n\n\u002F\u002F Usage\nconst products = await cachedQuery(\n  `products:category:${categoryId}:page:${page}`,\n  () => db.product.findMany({ where: { categoryId }, skip, take }),\n  300 \u002F\u002F 5 minutes\n)\n\n\u002F\u002F Invalidation\nasync function invalidateProductCache(categoryId: string) {\n  const keys = await redis.keys(`products:category:${categoryId}:*`)\n  if (keys.length) await redis.del(...keys)\n}\n```\n\n### Materialized Views\n\n```sql\nCREATE MATERIALIZED VIEW monthly_sales AS\nSELECT\n  DATE_TRUNC('month', created_at) as month,\n  category_id,\n  COUNT(*) as order_count,\n  SUM(total) as revenue,\n  AVG(total) as avg_order_value\nFROM orders\nWHERE created_at >= DATE_TRUNC('year', CURRENT_DATE)\nGROUP BY 1, 2;\n\nCREATE UNIQUE INDEX idx_monthly_sales ON monthly_sales(month, category_id);\n\n-- Refresh (can be scheduled via pg_cron)\nREFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales;\n```\n\n---\n\n## Connection Pool Configuration\n\n### Node.js (pg)\n\n```typescript\nimport { Pool } from 'pg'\n\nconst pool = new Pool({\n  max: 20,                      \u002F\u002F Max connections\n  idleTimeoutMillis: 30000,     \u002F\u002F Close idle connections after 30s\n  connectionTimeoutMillis: 2000, \u002F\u002F Fail fast if can't connect in 2s\n  maxUses: 7500,                \u002F\u002F Refresh connection after N uses\n})\n\n\u002F\u002F Monitor pool health\nsetInterval(() => {\n  console.log({\n    total: pool.totalCount,\n    idle: pool.idleCount,\n    waiting: pool.waitingCount,\n  })\n}, 60000)\n```\n\n---\n\n## Monitoring Queries\n\n### Active Connections\n\n```sql\nSELECT count(*), state\nFROM pg_stat_activity\nWHERE datname = current_database()\nGROUP BY state;\n```\n\n### Long-Running Queries\n\n```sql\nSELECT pid, now() - query_start AS duration, query, state\nFROM pg_stat_activity\nWHERE (now() - query_start) > interval '5 minutes'\nAND state = 'active';\n```\n\n### Table Sizes\n\n```sql\nSELECT\n  relname AS table,\n  pg_size_pretty(pg_total_relation_size(relid)) AS total_size,\n  pg_size_pretty(pg_relation_size(relid)) AS data_size,\n  pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_size\nFROM pg_catalog.pg_statio_user_tables\nORDER BY pg_total_relation_size(relid) DESC\nLIMIT 20;\n```\n\n### Table Bloat\n\n```sql\nSELECT\n  tablename,\n  pg_size_pretty(pg_total_relation_size(tablename::regclass)) as size,\n  n_dead_tup,\n  n_live_tup,\n  CASE WHEN n_live_tup > 0\n    THEN round(n_dead_tup::numeric \u002F n_live_tup, 2)\n    ELSE 0\n  END as dead_ratio\nFROM pg_stat_user_tables\nWHERE n_dead_tup > 1000\nORDER BY dead_ratio DESC;\n```\n\n---\n\n## Anti-Patterns\n\n1. ❌ `SELECT *` — always specify needed columns\n2. ❌ Missing indexes on foreign keys — always index FK columns\n3. ❌ `LIKE '%search%'` — use full-text search or trigram indexes instead\n4. ❌ Large `IN` clauses — use `ANY(ARRAY[...])` or join a values list\n5. ❌ No `LIMIT` on unbounded queries — always paginate\n6. ❌ Creating indexes without `CONCURRENTLY` in production\n7. ❌ Running migrations without testing rollback\n8. ❌ Ignoring `EXPLAIN ANALYZE` output — always verify execution plans\n9. ❌ Storing money as `FLOAT` — use `DECIMAL(10,2)` or integer cents\n10. ❌ Missing `NOT NULL` constraints — be explicit about nullability\n","# Database Operations\n\nComprehensive database design, migration, and optimization specialist. Adapted from buildwithclaude by Dave Poon (MIT).\n\n## Role Definition\n\nYou are a database optimization expert specializing in PostgreSQL, query performance, schema design, and EF Core migrations. You measure first, optimize second, and always plan rollback procedures.\n\n## Core Principles\n\n1. **Measure first** — always use `EXPLAIN ANALYZE` before optimizing\n2. **Index strategically** — based on query patterns, not every column\n3. **Denormalize selectively** — only when justified by read patterns\n4. **Cache expensive computations** — Redis\u002Fmaterialized views for hot paths\n5. **Plan rollback** — every migration has a reverse migration\n6. **Zero-downtime migrations** — additive changes first, destructive later\n\n---\n\n## Schema Design Patterns\n\n### User Management\n\n```sql\nCREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended', 'pending');\n\nCREATE TABLE users (\n  id BIGSERIAL PRIMARY KEY,\n  email VARCHAR(255) UNIQUE NOT NULL,\n  username VARCHAR(50) UNIQUE NOT NULL,\n  password_hash VARCHAR(255) NOT NULL,\n  first_name VARCHAR(100) NOT NULL,\n  last_name VARCHAR(100) NOT NULL,\n  status user_status DEFAULT 'active',\n  email_verified BOOLEAN DEFAULT FALSE,\n  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,\n  updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,\n  deleted_at TIMESTAMPTZ,  -- Soft delete\n\n  CONSTRAINT users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'),\n  CONSTRAINT users_names_not_empty CHECK (LENGTH(TRIM(first_name)) > 0 AND LENGTH(TRIM(last_name)) > 0)\n);\n\n-- Strategic indexes\nCREATE INDEX idx_users_email ON users(email);\nCREATE INDEX idx_users_status ON users(status) WHERE status != 'active';\nCREATE INDEX idx_users_created_at ON users(created_at);\nCREATE INDEX idx_users_deleted_at ON users(deleted_at) WHERE deleted_at IS NULL;\n```\n\n### Audit Trail\n\n```sql\nCREATE TYPE audit_operation AS ENUM ('INSERT', 'UPDATE', 'DELETE');\n\nCREATE TABLE audit_log (\n  id BIGSERIAL PRIMARY KEY,\n  table_name VARCHAR(255) NOT NULL,\n  record_id BIGINT NOT NULL,\n  operation audit_operation NOT NULL,\n  old_values JSONB,\n  new_values JSONB,\n  changed_fields TEXT[],\n  user_id BIGINT REFERENCES users(id),\n  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX idx_audit_table_record ON audit_log(table_name, record_id);\nCREATE INDEX idx_audit_user_time ON audit_log(user_id, created_at);\n\n-- Trigger function\nCREATE OR REPLACE FUNCTION audit_trigger_function()\nRETURNS TRIGGER AS $$\nBEGIN\n  IF TG_OP = 'DELETE' THEN\n    INSERT INTO audit_log (table_name, record_id, operation, old_values)\n    VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', to_jsonb(OLD));\n    RETURN OLD;\n  ELSIF TG_OP = 'UPDATE' THEN\n    INSERT INTO audit_log (table_name, record_id, operation, old_values, new_values)\n    VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW));\n    RETURN NEW;\n  ELSIF TG_OP = 'INSERT' THEN\n    INSERT INTO audit_log (table_name, record_id, operation, new_values)\n    VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', to_jsonb(NEW));\n    RETURN NEW;\n  END IF;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- Apply to any table\nCREATE TRIGGER audit_users\nAFTER INSERT OR UPDATE OR DELETE ON users\nFOR EACH ROW EXECUTE FUNCTION audit_trigger_function();\n```\n\n### Soft Delete Pattern\n\n```sql\n-- Query filter view\nCREATE VIEW active_users AS SELECT * FROM users WHERE deleted_at IS NULL;\n\n-- Soft delete function\nCREATE OR REPLACE FUNCTION soft_delete(p_table TEXT, p_id BIGINT)\nRETURNS VOID AS $$\nBEGIN\n  EXECUTE format('UPDATE %I SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL', p_table)\n  USING p_id;\nEND;\n$$ LANGUAGE plpgsql;\n```\n\n### Full-Text Search\n\n```sql\nALTER TABLE products ADD COLUMN search_vector tsvector\n  GENERATED ALWAYS AS (\n    to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '') || ' ' || COALESCE(sku, ''))\n  ) STORED;\n\nCREATE INDEX idx_products_search ON products USING gin(search_vector);\n\n-- Query\nSELECT * FROM products\nWHERE search_vector @@ to_tsquery('english', 'laptop & gaming');\n```\n\n---\n\n## Query Optimization\n\n### Analyze Before Optimizing\n\n```sql\n-- Always start here\nEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)\nSELECT u.id, u.name, COUNT(o.id) as order_count\nFROM users u\nLEFT JOIN orders o ON u.id = o.user_id\nWHERE u.created_at > '2024-01-01'\nGROUP BY u.id, u.name\nORDER BY order_count DESC;\n```\n\n### Indexing Strategy\n\n```sql\n-- Single column for exact lookups\nCREATE INDEX CONCURRENTLY idx_users_email ON users(email);\n\n-- Composite for multi-column queries (order matters!)\nCREATE INDEX CONCURRENTLY idx_orders_user_status ON orders(user_id, status, created_at);\n\n-- Partial index for filtered queries\nCREATE INDEX CONCURRENTLY idx_products_low_stock\nON products(inventory_quantity)\nWHERE inventory_tracking = true AND inventory_quantity \u003C= 5;\n\n-- Covering index (includes extra columns to avoid table lookup)\nCREATE INDEX CONCURRENTLY idx_orders_covering\nON orders(user_id, status) INCLUDE (total, created_at);\n\n-- GIN index for JSONB\nCREATE INDEX CONCURRENTLY idx_products_attrs ON products USING gin(attributes);\n\n-- Expression index\nCREATE INDEX CONCURRENTLY idx_users_email_lower ON users(lower(email));\n```\n\n### Find Unused Indexes\n\n```sql\nSELECT\n  schemaname, tablename, indexname,\n  idx_scan as scans,\n  pg_size_pretty(pg_relation_size(indexrelid)) as size\nFROM pg_stat_user_indexes\nWHERE idx_scan = 0\nORDER BY pg_relation_size(indexrelid) DESC;\n```\n\n### Find Missing Indexes (Slow Queries)\n\n```sql\n-- Enable pg_stat_statements first\nSELECT query, calls, total_exec_time, mean_exec_time, rows\nFROM pg_stat_statements\nWHERE mean_exec_time > 100  -- ms\nORDER BY total_exec_time DESC\nLIMIT 20;\n```\n\n### N+1 Query Detection\n\n```sql\n-- Look for repeated similar queries in pg_stat_statements\nSELECT query, calls, mean_exec_time\nFROM pg_stat_statements\nWHERE calls > 100 AND query LIKE '%WHERE%id = $1%'\nORDER BY calls DESC;\n```\n\n---\n\n## Migration Patterns\n\n### Safe Column Addition\n\n```sql\n-- +migrate Up\n-- Always use CONCURRENTLY for indexes in production\nALTER TABLE users ADD COLUMN phone VARCHAR(20);\nCREATE INDEX CONCURRENTLY idx_users_phone ON users(phone) WHERE phone IS NOT NULL;\n\n-- +migrate Down\nDROP INDEX IF EXISTS idx_users_phone;\nALTER TABLE users DROP COLUMN IF EXISTS phone;\n```\n\n### Safe Column Rename (Zero-Downtime)\n\n```sql\n-- Step 1: Add new column\nALTER TABLE users ADD COLUMN display_name VARCHAR(100);\nUPDATE users SET display_name = name;\nALTER TABLE users ALTER COLUMN display_name SET NOT NULL;\n\n-- Step 2: Deploy code that writes to both columns\n-- Step 3: Deploy code that reads from new column\n-- Step 4: Drop old column\nALTER TABLE users DROP COLUMN name;\n```\n\n### Table Partitioning\n\n```sql\n-- Create partitioned table\nCREATE TABLE orders (\n  id BIGSERIAL,\n  user_id BIGINT NOT NULL,\n  total DECIMAL(10,2),\n  created_at TIMESTAMPTZ NOT NULL,\n  PRIMARY KEY (id, created_at)\n) PARTITION BY RANGE (created_at);\n\n-- Monthly partitions\nCREATE TABLE orders_2024_01 PARTITION OF orders\n  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');\nCREATE TABLE orders_2024_02 PARTITION OF orders\n  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');\n\n-- Auto-create partitions\nCREATE OR REPLACE FUNCTION create_monthly_partition(p_table TEXT, p_date DATE)\nRETURNS VOID AS $$\nDECLARE\n  partition_name TEXT := p_table || '_' || to_char(p_date, 'YYYY_MM');\n  next_date DATE := p_date + INTERVAL '1 month';\nBEGIN\n  EXECUTE format(\n    'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',\n    partition_name, p_table, p_date, next_date\n  );\nEND;\n$$ LANGUAGE plpgsql;\n```\n\n---\n\n## EF Core Migrations (.NET)\n\n### Create and Apply\n\n```bash\n# Add migration\ndotnet ef migrations add AddPhoneToUsers -p src\u002FInfrastructure -s src\u002FApi\n\n# Apply\ndotnet ef database update -p src\u002FInfrastructure -s src\u002FApi\n\n# Generate idempotent SQL script for production\ndotnet ef migrations script -p src\u002FInfrastructure -s src\u002FApi -o migration.sql --idempotent\n\n# Rollback\ndotnet ef database update PreviousMigrationName -p src\u002FInfrastructure -s src\u002FApi\n```\n\n### EF Core Configuration Best Practices\n\n```csharp\n\u002F\u002F Use AsNoTracking for read queries\nvar users = await _db.Users\n    .AsNoTracking()\n    .Where(u => u.Status == UserStatus.Active)\n    .Select(u => new UserDto { Id = u.Id, Name = u.Name })\n    .ToListAsync(ct);\n\n\u002F\u002F Avoid N+1 with Include\nvar orders = await _db.Orders\n    .Include(o => o.Items)\n    .ThenInclude(i => i.Product)\n    .Where(o => o.UserId == userId)\n    .ToListAsync(ct);\n\n\u002F\u002F Better: Projection\nvar orders = await _db.Orders\n    .Where(o => o.UserId == userId)\n    .Select(o => new OrderDto\n    {\n        Id = o.Id,\n        Total = o.Total,\n        Items = o.Items.Select(i => new OrderItemDto\n        {\n            ProductName = i.Product.Name,\n            Quantity = i.Quantity,\n        }).ToList(),\n    })\n    .ToListAsync(ct);\n```\n\n---\n\n## Caching Strategy\n\n### Redis Query Cache\n\n```typescript\nimport Redis from 'ioredis'\n\nconst redis = new Redis(process.env.REDIS_URL)\n\nasync function cachedQuery\u003CT>(\n  key: string,\n  queryFn: () => Promise\u003CT>,\n  ttlSeconds: number = 300\n): Promise\u003CT> {\n  const cached = await redis.get(key)\n  if (cached) return JSON.parse(cached)\n\n  const result = await queryFn()\n  await redis.setex(key, ttlSeconds, JSON.stringify(result))\n  return result\n}\n\n\u002F\u002F Usage\nconst products = await cachedQuery(\n  `products:category:${categoryId}:page:${page}`,\n  () => db.product.findMany({ where: { categoryId }, skip, take }),\n  300 \u002F\u002F 5 minutes\n)\n\n\u002F\u002F Invalidation\nasync function invalidateProductCache(categoryId: string) {\n  const keys = await redis.keys(`products:category:${categoryId}:*`)\n  if (keys.length) await redis.del(...keys)\n}\n```\n\n### Materialized Views\n\n```sql\nCREATE MATERIALIZED VIEW monthly_sales AS\nSELECT\n  DATE_TRUNC('month', created_at) as month,\n  category_id,\n  COUNT(*) as order_count,\n  SUM(total) as revenue,\n  AVG(total) as avg_order_value\nFROM orders\nWHERE created_at >= DATE_TRUNC('year', CURRENT_DATE)\nGROUP BY 1, 2;\n\nCREATE UNIQUE INDEX idx_monthly_sales ON monthly_sales(month, category_id);\n\n-- Refresh (can be scheduled via pg_cron)\nREFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales;\n```\n\n---\n\n## Connection Pool Configuration\n\n### Node.js (pg)\n\n```typescript\nimport { Pool } from 'pg'\n\nconst pool = new Pool({\n  max: 20,                      \u002F\u002F Max connections\n  idleTimeoutMillis: 30000,     \u002F\u002F Close idle connections after 30s\n  connectionTimeoutMillis: 2000, \u002F\u002F Fail fast if can't connect in 2s\n  maxUses: 7500,                \u002F\u002F Refresh connection after N uses\n})\n\n\u002F\u002F Monitor pool health\nsetInterval(() => {\n  console.log({\n    total: pool.totalCount,\n    idle: pool.idleCount,\n    waiting: pool.waitingCount,\n  })\n}, 60000)\n```\n\n---\n\n## Monitoring Queries\n\n### Active Connections\n\n```sql\nSELECT count(*), state\nFROM pg_stat_activity\nWHERE datname = current_database()\nGROUP BY state;\n```\n\n### Long-Running Queries\n\n```sql\nSELECT pid, now() - query_start AS duration, query, state\nFROM pg_stat_activity\nWHERE (now() - query_start) > interval '5 minutes'\nAND state = 'active';\n```\n\n### Table Sizes\n\n```sql\nSELECT\n  relname AS table,\n  pg_size_pretty(pg_total_relation_size(relid)) AS total_size,\n  pg_size_pretty(pg_relation_size(relid)) AS data_size,\n  pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_size\nFROM pg_catalog.pg_statio_user_tables\nORDER BY pg_total_relation_size(relid) DESC\nLIMIT 20;\n```\n\n### Table Bloat\n\n```sql\nSELECT\n  tablename,\n  pg_size_pretty(pg_total_relation_size(tablename::regclass)) as size,\n  n_dead_tup,\n  n_live_tup,\n  CASE WHEN n_live_tup > 0\n    THEN round(n_dead_tup::numeric \u002F n_live_tup, 2)\n    ELSE 0\n  END as dead_ratio\nFROM pg_stat_user_tables\nWHERE n_dead_tup > 1000\nORDER BY dead_ratio DESC;\n```\n\n---\n\n## Anti-Patterns\n\n1. ❌ `SELECT *` — always specify needed columns\n2. ❌ Missing indexes on foreign keys — always index FK columns\n3. ❌ `LIKE '%search%'` — use full-text search or trigram indexes instead\n4. ❌ Large `IN` clauses — use `ANY(ARRAY[...])` or join a values list\n5. ❌ No `LIMIT` on unbounded queries — always paginate\n6. ❌ Creating indexes without `CONCURRENTLY` in production\n7. ❌ Running migrations without testing rollback\n8. ❌ Ignoring `EXPLAIN ANALYZE` output — always verify execution plans\n9. ❌ Storing money as `FLOAT` — use `DECIMAL(10,2)` or integer cents\n10. ❌ Missing `NOT NULL` constraints — be explicit about nullability\n",{"name":5,"role":56,"scope":57,"version":58,"triggers":59,"description":7,"output-format":45},"specialist","implementation","1.0.0",[32,60,61,62,40,63,64,65,66,67,68,69,35,70,71,72,73],"schema","migration","SQL","index","PostgreSQL","Postgres","N+1","slow query","EXPLAIN","partitioning","Redis","connection pool","EF Core migration","database design","main","e7e336ac628c3e3bd771aff1aaa469c93778f94a","skills\u002Fjgarrison929\u002Fdatabase-operations",0,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del","2026-08-23 22:39:37",{"source":8,"skillName":5,"commands":82,"prompt":92,"zipAvailable":93},[83,86,89],{"tool":84,"command":85},"npx","npx skills add kbarbel640-del\u002Fskills --skill database-operations",{"tool":87,"command":88},"bunx","bunx skills add kbarbel640-del\u002Fskills --skill database-operations",{"tool":90,"command":91},"pnpm","pnpm dlx skills add kbarbel640-del\u002Fskills --skill database-operations","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fjgarrison929\u002Fdatabase-operations\u002FSKILL.md",true,[95],{"path":96,"resourceType":97,"resourceTypeDesc":98,"sizeBytes":99,"sourceHtmlUrl":100},"_meta.json","REFERENCE","参考文档",296,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fjgarrison929\u002Fdatabase-operations\u002F_meta.json",[102,130,156,177,183,203],{"skillNo":103,"name":104,"nameZh":6,"description":105,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":111,"publishedAt":51},"01M6G8J55967D6281ACMKAKTEQ","crowd-prompting","A marketplace where AI agents improve prompts, system instructions, tool descriptions, and other text-based content with domain expertise from real-world operations — and earn tokens for valuable contributions.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzzadrian\u002Fcrowd-prompting\u002FSKILL.md",[108],{"categoryNo":109,"categoryName":110,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000036","内容创作者",[112,113,114,115,116,117,118,119,33,120,121,122,123,124,125,126,127,128,129],"crowd prompting","prompting","api","with","tokens","domain","world","prompts","instructions","text","tool","system","improve","productivity","marketplace","https","content","ai agents",{"skillNo":131,"name":132,"nameZh":6,"description":133,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":134,"categories":135,"keywords":139,"publishedAt":51},"01M6G8J0PKPAYVET6BWE8W4C26","dilemm-ai","Compete in DilemmAI, the prisoner's dilemma AI arena at dilemm.ai. Use when an OpenClaw agent wants to create an account, design and submit strategy prompts for their bot, enter matchmaking, analyze historical matches and leaderboards, and iterate on bot strategy. Requires headless browser automation (no API available yet).","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzyori\u002Fdilemm-ai\u002FSKILL.md",[136],{"categoryNo":137,"categoryName":138,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",[140,114,141,142,143,144,145,146,147,148,149,42,150,151,152,153,154,119,155],"dilemm ai","arena","analyze","browser","openclaw agent","create","automation","browser automation","bot","openclaw","strategy","headless","compete","submit","matchmaking","use",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":165,"publishedAt":51},"01M6G8HWAF3K9SW7NP3Z83CPJ6","arxiv-paper-reviews","Interact with arXiv Crawler API to fetch papers, read reviews, and submit comments. Use when working with arXiv papers, fetching paper lists by date\u002Fcategory\u002Finterest, viewing paper details with comments, or submitting paper reviews via API at http:\u002F\u002F150.158.152.82:8000.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Fweak-accept\u002FSKILL.md",[162],{"categoryNo":163,"categoryName":164,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000021","资料检索与研究",[166,167,168,169,170,171,172,115,173,153,155,174,114,175,176],"arxiv paper reviews","reviews","arxiv","paper","crawler","papers","comments","fetch","read","http","via",{"skillNo":178,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":182,"publishedAt":51},"01M6G8HQJ7N7WMY6YV7D7SSJ16","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Farxiv-paper-reviews\u002FSKILL.md",[181],{"categoryNo":163,"categoryName":164,"taxonomyType":20,"taxonomyTypeDesc":21},[169,168,167,166,176,171,172,115,170,153,155,173,174,114,175],{"skillNo":184,"name":185,"nameZh":6,"description":186,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":187,"categories":188,"keywords":189,"publishedAt":51},"01M6G8HM95Y64JNVFC2HN4E2BG","git-summary","Get a quick summary of the current Git repository including status, recent commits, branches, and contributors.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzweack\u002Fgit-summary\u002FSKILL.md",[],[190,191,192,193,194,195,196,197,198,199,200,201,202],"git","summary","git summary","commits","repository","win32","status","linux","contributors","true","quick","get","branches",{"skillNo":204,"name":205,"nameZh":6,"description":206,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":207,"categories":208,"keywords":213,"publishedAt":51},"01M6G8HGEGF05CNEJCGVYFSC4R","agent-constitution","Interact with AgentConstitution governance contracts on Base Sepolia. Check compliance, read rules, log actions, query governance state.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fztsalexey\u002Fagent-constitution\u002FSKILL.md",[209,210],{"categoryNo":137,"categoryName":138,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":211,"categoryName":212,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000037","律师与法务",[214,215,216,217,115,36,218,74,219,174,220,221,222,223,224,127,225,226,227],"agent constitution","constitution","contracts","tree","base","log","check","Rules","projects","compliance","actions","state","governance","github"]