# 数据库 SQL 约束与运维

## 提交前约束

新增或修改 SQL 必须满足：

1. 列表查询必须有明确的分页、最大数量或日期范围；禁止无界返回历史数据。
2. `GROUP BY`、`COUNT(DISTINCT)`、窗口函数和跨表聚合必须附带 `EXPLAIN (ANALYZE, BUFFERS)` 结果。
3. 高频接口禁止在公共 `SELECT` 中叠加未必要的 LATERAL、相关子查询或全表聚合。
4. 当前页关联数据必须按当前页 ID 批量查询，不得读取用户全部历史记录再在应用层过滤。
5. 新增索引必须通过数据库 migration 提交，并说明覆盖的查询条件；大表生产索引应评估锁影响。
6. SQL 必须使用参数绑定，禁止把请求参数直接拼接到 SQL 值中。

后端 `npm run check` 会执行 `npm run check:sql`，检查关键性能防线没有被重构移除。该检查不能替代真实数据量下的 `EXPLAIN`。

## 生产观测

生产 PostgreSQL 应启用 `pg_stat_statements` 和慢查询日志。Compose 默认通过 `POSTGRES_LOG_MIN_DURATION_STATEMENT_MS`（默认 500ms）配置 PostgreSQL 原生慢查询日志；应用默认将执行超过 `DATABASE_SLOW_QUERY_THRESHOLD_MS`（默认 500ms）的 SQL 记录为慢查询，只记录 SQL 形状、耗时和返回行数，不记录参数值。

常用排查 SQL：

```sql
SELECT calls, total_exec_time, mean_exec_time, rows, query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 30;

SELECT pid, state, wait_event_type, wait_event,
       now() - query_start AS duration, query
FROM pg_stat_activity
WHERE datname = current_database() AND state <> 'idle'
ORDER BY query_start;
```

## 容量边界

- `DATABASE_STATEMENT_TIMEOUT_MS` 防止单条 SQL 长时间占用连接。
- `DATABASE_MAX_CONNECTIONS` 按后端实例数总量评估；扩容后端实例时不能简单地每实例增加连接数。
- CPU 限额只能防止数据库拖垮整机，不能替代 SQL 优化。
- 数据量持续增长后，排行榜和历史统计应迁移到汇总表、缓存或只读副本，不应继续依赖在线全量聚合。

## 容器资源限制

Compose 对 postgres 与各服务已配置资源限额，默认值按 4 核 8G 且与其他服务共用的服务器设置，均可在 `.env` 覆盖（见 `.env.server.example` 的 `POSTGRES_*` / `BACKEND_MEM_LIMIT` / `WEB_MEM_LIMIT` 等）：

| 服务 | 限额 | 说明 |
| --- | --- | --- |
| postgres | `mem_limit` 1.5g、`cpus` 2、`shm_size` 512mb | `shm_size` 不足会导致并行查询/大排序报错；`stop_grace_period` 60s 保证检查点能完整落盘；`effective_cache_size` 只是规划器提示，不实际占用内存 |
| backend | `mem_limit` 768m、`cpus` 1.5 | `stop_grace_period` 30s 保证优雅关闭（HTTP 收尾 + 连接池释放）能跑完 |
| web | `mem_limit` 256m、`cpus` 0.5 | Caddy 静态服务，低占用 |
| dev postgres | 同生产默认 | 仅 postgres 受限；backend/frontend 开发容器不设限，避免 tsx watch / npm install 内存波动被误杀 |

调整原则：

- 内存限额不要低于 RSS 峰值的 1.3 倍，否则会触发 OOM 杀容器（`restart: unless-stopped` 会崩溃循环）。
- 内存类 PG 参数遵循大致比例：`shared_buffers` ≈ 内存的 1/8~1/4，`effective_cache_size` ≈ 内存的 1/2~3/4，且两者之和不宜超过可用内存的 80%。
- 修改限额后 `docker compose config` 会以明文展示最终值，可用它核对插值结果；变更仅对重建/重启的容器生效。
