# 数据库操作规范

**对象**：阿里云 AnalyticDB for PostgreSQL（基于 Greenplum，MPP 架构）

**用途**：写 migration、运维脚本、排查数据库卡死问题时**先读这篇**。所有条目都来自踩过的坑，不是推测。

---

## 1. 致命操作（永远不要做）

| 操作 | 后果 | 替代方案 |
|------|------|---------|
| `ALTER TABLE ... RENAME` | 表损坏，需重启实例 | `CREATE TABLE AS SELECT` → `DROP TABLE 旧表` |
| `ALTER TABLE ... DROP COLUMN` | 表损坏 | 同上 |
| 事务里执行 DDL | DDL 回滚不完整，部分持久化 | `autocommit=True`，每条 DDL 独立 |
| `FOREIGN KEY ... ON DELETE CASCADE` | 不支持（底层依赖 trigger） | 应用层级联：`knowhub_db/cascade.py` |
| `ON CONFLICT DO UPDATE` on 加过新列的表 | AnalyticDB beam 表报错 `modification of part columns...` | 用 `DELETE WHERE id=X; INSERT ...` 代替 |

**改表结构的安全模式**：`CREATE TABLE 新表 → INSERT 数据 → DROP TABLE 旧表`。参考 `migrate_v3_junction_tables.py`。

---

## 2. DDL 需谨慎的操作

| 操作 | 风险 | 推荐做法 |
|------|------|---------|
| `ADD COLUMN ... NOT NULL DEFAULT 'X'` | 连接可能被服务端杀掉 | 拆成两步：`ADD COLUMN ... DEFAULT 'X'` → 另起连接 `ALTER COLUMN ... SET NOT NULL` |
| 任何 DDL（ALTER/CREATE INDEX 等） | 需要 `AccessExclusiveLock`，任何 `idle in transaction` 会话都会阻塞无限等 | 跑前先 kill `idle in transaction`；用 `statement_timeout='30s'` 防挂起 |
| 批量 DDL 连发 | 每次开新连接会累积 TCP 会话 | 一个长连接跑全部 DDL |

---

## 3. Store / 应用端连接规范

### 3.1 永远 `autocommit = True`

```python
self.conn = psycopg2.connect(...)
self.conn.autocommit = True   # 必须
```

**原因**：`autocommit = False` 下，执行一次 SELECT 就会开启一个隐式事务，如果不显式 commit/rollback，连接停在 `idle in transaction` 状态——**永久持有 AccessShareLock**，阻塞后续所有需要 AccessExclusiveLock 的 DDL。这是最难诊断、最容易卡整个系统的坑。

**代价**：多语句写（entity INSERT + 若干 junction 写）失去事务原子性。但我们的写模式是 `DELETE + INSERT ON CONFLICT DO NOTHING`——幂等，失败重跑即可。

详见 `decisions.md §17`。

### 3.2 Migration 脚本模板

```python
import os, time, psycopg2
from dotenv import load_dotenv
load_dotenv()

conn = psycopg2.connect(..., connect_timeout=10)
conn.autocommit = True      # 不要开事务
cur = conn.cursor()
cur.execute("SET statement_timeout = '30s'")  # 卡超过 30s 自动失败

# 动手前先清 idle-in-tx，防止 DDL 等锁无限阻塞
cur.execute("""SELECT pid FROM pg_stat_activity WHERE state='idle in transaction' 
               AND pid!=pg_backend_pid() AND datname=current_database()""")
for (pid,) in cur.fetchall():
    cur.execute("SELECT pg_terminate_backend(%s)", (pid,))

# 每条 DDL 前打 flush 的 print（否则卡住时看不到到哪一步）
for t in TARGETS:
    print(f"[{time.strftime('%H:%M:%S')}] ALTER {t}...", flush=True)
    cur.execute(f"ALTER TABLE {t} ...")
    print(f"  ✓ done", flush=True)
```

**关键**：
- `flush=True` 永远带上——挂起时最需要看到卡在哪一步
- `SET statement_timeout`——宁可快速失败，不要让 client 无限等
- 先清 idle-in-tx——否则你做的 DDL 会被别人的长事务卡住
- 每条 DDL 前后打时间戳

---

## 4. 排查手册

### 症状：连接失败 `remaining connection slots are reserved`

**原因**：连接池打满。通常因为：
- 有 Python 脚本被 kill 但 TCP 会话服务端未释放（等 idle timeout，可能十几分钟）
- 生产服务（knowhub server）连接泄漏

**排查**：
```bash
lsof -i | grep gpdbmaster   # 看本地有没有残留连接
```

**恢复**（按成本从低到高）：
1. **等**：Alibaba 的 idle session timeout 会自然释放——但"active"或"idle in transaction"不走超时
2. **阿里云控制台**：实例管理 → 会话管理 → 手动 terminate。**首选**
3. **重启实例**：所有连接清零，影响 1-2 分钟

### 症状：DDL 挂起几十秒后 timeout 或被连接断开

**原因 99%**：有 `idle in transaction` 会话持有目标表的 `AccessShareLock`。

**查证**：
```sql
SELECT l.pid, l.mode, l.granted, c.relname, a.state, now()-a.query_start AS dur
FROM pg_locks l
JOIN pg_class c ON l.relation=c.oid
LEFT JOIN pg_stat_activity a ON l.pid=a.pid
WHERE c.relname IN ('your_target_table')
  AND l.pid != pg_backend_pid();
```

**解决**：
```sql
SELECT pg_terminate_backend(<pid>);
```

### 症状：脚本静默挂住，没有任何输出

**原因**：Python 的 print buffer 没 flush，execute() 已经在 wait 了。

**预防**：所有 print 带 `flush=True`；`python -u` 或设 `PYTHONUNBUFFERED=1`。

---

## 5. 诊断脚本（都在 `knowhub/knowhub_db/scripts/`）

| 脚本 | 用途 |
|------|------|
| `kill_db_locks.py` | 列出所有非 idle 会话 + 杀 idle-in-tx |
| `clear_locks.py` | 清锁（轻量版） |
| `check_table_structure.py` | 看表结构和行数 |
| `check_extensions.py` | 看 PG 扩展（pgvector/fastann 等） |

---

## 6. 金句（30 秒能记住的）

- **DDL 前先 `autocommit=True`**；改 store 也必须 `autocommit=True`
- **每个 print 带 `flush=True`**
- **`SET statement_timeout='30s'`**——宁可失败别挂死
- **跑 DDL 前先 kill idle-in-tx**
- **禁用 RENAME / DROP COLUMN**
- **禁用 FK ON DELETE CASCADE**——用 `cascade.py`
- **别在事务里跑 DDL**
- 连接满了？**去控制台杀会话**，本地 kill 进程只关本地 socket
