# PostgreSQL 数据库配置指南

MoviePilot 现在支持 PostgreSQL 数据库，您可以根据需要选择使用 SQLite 或 PostgreSQL。

## 配置选项

### 1. 数据库类型选择

在 `config/app.env` 文件中设置：

```bash
# 使用 SQLite（默认）
DB_TYPE=sqlite

# 使用 PostgreSQL
DB_TYPE=postgresql
```

### 2. PostgreSQL 配置参数

当 `DB_TYPE=postgresql` 时，以下配置生效：

```bash
# PostgreSQL 主机地址
DB_POSTGRESQL_HOST=localhost

# PostgreSQL 端口；使用 Unix Socket 时可留空
DB_POSTGRESQL_PORT=5432

# PostgreSQL 数据库名
DB_POSTGRESQL_DATABASE=moviepilot

# PostgreSQL 用户名
DB_POSTGRESQL_USERNAME=moviepilot

# PostgreSQL 密码
DB_POSTGRESQL_PASSWORD=moviepilot

# PostgreSQL 连接池大小
DB_POSTGRESQL_POOL_SIZE=20

# PostgreSQL 连接池溢出数量
DB_POSTGRESQL_MAX_OVERFLOW=30
```

### 3. Unix Socket 连接

如果 PostgreSQL 通过 Unix Socket 暴露，可以把 `DB_POSTGRESQL_HOST` 设置为套接字目录。

```bash
DB_TYPE=postgresql
DB_POSTGRESQL_HOST=/var/run/postgresql
DB_POSTGRESQL_PORT=
DB_POSTGRESQL_DATABASE=moviepilot
DB_POSTGRESQL_USERNAME=moviepilot
DB_POSTGRESQL_PASSWORD=moviepilot
```

如需显式指定 socket 端口，也可以保留 `DB_POSTGRESQL_PORT`，程序会生成带 `host=/path/to/socket` 查询参数的 PostgreSQL URL。

## Docker 部署

### 使用外部 PostgreSQL

如果您想使用外部的 PostgreSQL 服务：

1. 确保外部 PostgreSQL 服务已启动并可访问
2. 设置环境变量指向外部服务：
```bash
DB_TYPE=postgresql
DB_POSTGRESQL_HOST=your-postgresql-host
DB_POSTGRESQL_PORT=5432
DB_POSTGRESQL_DATABASE=moviepilot
DB_POSTGRESQL_USERNAME=your-username
DB_POSTGRESQL_PASSWORD=your-password
```

## 数据迁移

### 从 SQLite 迁移到 PostgreSQL（以在 Windows 下操作为例）

1. 关闭 SQLite 的 WAL 模式（如果此前已经开启），并关闭 MoviePilot
2. 备份现有的 SQLite 数据库文件（`config/user.db`）
3. 按照上述要求修改配置为 PostgreSQL
4. 注意，由于 SQLite 与 PostgreSQL 对部分字段的类型（例如`json`类型）定义不同，请勿通过`user.db`在迁移阶段直接在空数据库的基础上创建表结构，而应按照下一条要求通过 MoviePilot 的初始化自动创建正确的表结构，只有在这种情况下迁移工具才能正确处理数据类型
5. 启动应用，让 MoviePilot 自动创建表结构，确认创建完成后关闭 MoviePilot
6. 使用如下 SQL 语句清理所有初始化完成的表数据，只保留表结构，避免默认的初始化数据干扰迁移
```sql
TRUNCATE TABLE agentchat, agenttask, alembic_version, downloadfailure, downloadfiles, downloadhistory, mediaserveritem, message, passkey, plugindata, site, siteicon, sitestatistic, siteuserdata, subscribe, subscribehistory, systemconfig, transferhistory, "user", userconfig, workflow RESTART IDENTITY CASCADE;
```
6. 安装 Java 21 或更新的版本，并下载 dimitri/pgloader 中的 v4 版本 jar 包，即`pgloader.jar`
7. 创建`migrate.load`文件，并编辑如下内容，注意：Windows 下本地`.db`文件路径引用需要有 3 个斜线；`userrequest`表已经废弃，是 v1 阶段的残留物，下列配置文件将会自动去除
```
LOAD DATABASE
     FROM sqlite:///X:/path/to/user.db
     INTO postgresql://moviepilot:your-password@host:5432/moviepilot

WITH
     data only,
     reset sequences

EXCLUDING TABLE NAMES LIKE 'userrequest'

SET work_mem TO '16MB',
    maintenance_work_mem TO '512MB'

CAST
     type integer to boolean when (= precision 1)
;
```
8. 使用`java --enable-native-access=ALL-UNNAMED -jar .\pgloader.jar .\migrate.load`启动迁移。迁移过程中可能产生大量警告和报错，主要与孤儿索引`idx__PRIMARY`相关，形如`WARN  [main] pgloader.core - Primary Keys failed (skipping): ERROR: index "idx__PRIMARY" does not belong to table "plugindata"  浣嶇疆锛?9`
9. 清理孤儿索引：`DROP INDEX IF EXISTS "idx__PRIMARY";`
10. 修正 Alembic 版本号：`UPDATE alembic_version SET version_num = 'd58298a0879f';`
11. 由于 SQLite 与 PostgreSQL 的自增序列原理不同，直接导入会出现冲突，需手动更新自增序列，执行：
```sql
-- MoviePilot PostgreSQL 自增序列修复脚本
-- 用途：修复从 SQLite 迁移到 PostgreSQL 后的自增 ID 冲突
-- 问题背景：
--   SQLite 的 AUTOINCREMENT 不依赖独立序列对象，数据行带着具体 id 值进入 PostgreSQL。
--   但 PostgreSQL 使用独立的 Sequence（如 plugindata_id_seq）生成新 ID。
--   迁移后 Sequence 仍停留在初始值（如 1），而表中已有 id=10186 的数据，
--   导致新插入时报 "duplicate key value violates unique constraint"。
-- 执行时机：
--   1. 迁移完成后、应用启动前（必做）
--   2. 运行中报主键冲突时（应急）
--   3. 从备份恢复数据后（建议做）
-- 安全说明：
--   本脚本只读表内最大 id 并调整序列，不修改任何数据行，可重复执行。

DO $$
DECLARE
    rec RECORD;
    seq_name TEXT;
    max_id BIGINT;
    tbl_full TEXT;
    fix_count INT := 0;
    err_count INT := 0;
BEGIN
    RAISE NOTICE '开始扫描 public schema 下的自增序列...';

    -- 遍历 public schema 下所有带自增序列的列
    -- 通过 pg_get_serial_sequence() 直接获取序列名，不依赖 pg_depend，兼容性好
    FOR rec IN
        SELECT
            n.nspname AS schema_name,
            c.relname AS table_name,
            a.attname AS column_name
        FROM pg_class c
        JOIN pg_namespace n ON c.relnamespace = n.oid
        JOIN pg_attribute a ON a.attrelid = c.oid
        WHERE n.nspname = 'public'
          AND c.relkind = 'r'                    -- 只处理普通表，排除视图/系统表
          AND a.attnum > 0                       -- attnum <= 0 是系统隐藏列
          AND NOT a.attisdropped                 -- 排除已删除但未清理的列
          AND c.relname NOT LIKE 'pg_%'          -- 排除 PostgreSQL 系统表
          AND c.relname NOT LIKE 'sql_%'
          AND pg_get_serial_sequence(
                quote_ident(n.nspname) || '.' || quote_ident(c.relname),
                a.attname
              ) IS NOT NULL                      -- 只保留确实有自增序列关联的列
        ORDER BY c.relname, a.attname
    LOOP
        BEGIN
            -- 构造带 schema 前缀的表名，quote_ident 自动处理 user 等关键字
            tbl_full := quote_ident(rec.schema_name) || '.' || quote_ident(rec.table_name);

            -- 获取该列当前最大值。COALESCE 处理空表（无数据时返回 0）
            EXECUTE format('SELECT COALESCE(MAX(%I), 0) FROM %s', rec.column_name, tbl_full)
                INTO max_id;

            -- 获取序列完整名称（含 schema）
            seq_name := pg_get_serial_sequence(tbl_full, rec.column_name);

            -- 重置序列：
            --   参数1: 序列名
            --   参数2: 重置到的值（max_id + 1，空表时为 1）
            --   参数3: is_called = false，表示 nextval() 下次直接返回该值，不再递增
            -- 效果：下一条 INSERT 拿到的 id 正好是表中未使用的最小值
            PERFORM setval(seq_name, max_id + 1, false);

            RAISE NOTICE '已修复: %.% (列 %) -> 序列 % 设为 % (表内最大 id = %)',
                rec.schema_name, rec.table_name, rec.column_name,
                seq_name, max_id + 1, max_id;
            fix_count := fix_count + 1;

        EXCEPTION WHEN OTHERS THEN
            -- 单表异常不阻断整体流程，记录后继续下一张表
            RAISE NOTICE '跳过 %.%: %', rec.schema_name, rec.table_name, SQLERRM;
            err_count := err_count + 1;
        END;
    END LOOP;

    RAISE NOTICE '完成。修复 % 个序列，跳过/错误 % 个。', fix_count, err_count;
END $$;

-- 验证：查看所有序列当前值，确认 last_value 大于对应表的最大 id
SELECT
    sequencename,
    last_value
FROM pg_sequences
WHERE schemaname = 'public'
ORDER BY sequencename;
```
12. 启动 MoviePilot，如果迁移成功，你应当在日志中看到类似下面的信息：
```
INFO:    [moviepilot] 5b3355c964bb_2_2_0.py - 发现 21 个表需要检查序列
INFO:    [moviepilot] a946dae52526_2_2_1.py - 开始PostgreSQL数据库userid字段迁移...
INFO:    [moviepilot] a946dae52526_2_2_1.py - PostgreSQL数据库userid字段迁移完成
INFO:    [moviepilot] 41ef1dd7467c_2_2_2.py - SystemConfig 表去重操作已完成。
INFO:     Started server process [129]
```

### 从 PostgreSQL 迁移到 SQLite

1. 导出 PostgreSQL 数据
2. 修改配置为 SQLite
3. 启动应用，数据库表会自动创建
4. 导入数据到 SQLite

## 数据备份

### PostgreSQL 数据备份

PostgreSQL 数据存储在 `${CONFIG_DIR}/postgresql/` 目录中，您可以通过以下方式进行备份：

#### 1. 文件级备份
```bash
# 备份整个PostgreSQL数据目录
tar -czf postgresql_backup_$(date +%Y%m%d_%H%M%S).tar.gz config/postgresql/
```

#### 2. 数据库级备份
```bash
# 进入容器
docker exec -it moviepilot bash

# 使用pg_dump备份
pg_dump -h localhost -U moviepilot -d moviepilot > /config/moviepilot_backup.sql

# 或使用pg_dumpall备份所有数据库
pg_dumpall -h localhost -U moviepilot > /config/all_databases_backup.sql
```

#### 3. 恢复数据
```bash
# 恢复单个数据库
psql -h localhost -U moviepilot -d moviepilot < /config/moviepilot_backup.sql

# 恢复所有数据库
psql -h localhost -U moviepilot < /config/all_databases_backup.sql
```

## 性能优化

### PostgreSQL 优化建议

1. **连接池配置**：
   - 根据应用负载调整 `DB_POSTGRESQL_POOL_SIZE`
   - 设置合适的 `DB_POSTGRESQL_MAX_OVERFLOW`

2. **数据库配置**：
   - 调整 `shared_buffers`
   - 配置 `work_mem`
   - 设置合适的 `maintenance_work_mem`

3. **索引优化**：
   - 为常用查询字段添加索引
   - 定期执行 `VACUUM` 和 `ANALYZE`

## 故障排除

### 常见问题

1. **连接失败**：
   - 检查 PostgreSQL 服务是否启动
   - 验证连接参数是否正确
   - 确认网络连接和防火墙设置

2. **权限问题**：
   - 确保用户有足够的数据库权限
   - 检查 `pg_hba.conf` 配置

3. **性能问题**：
   - 监控连接池使用情况
   - 检查慢查询日志
   - 优化数据库配置

### 日志查看

PostgreSQL 相关日志可以在以下位置查看：

- Docker 容器：`${CONFIG_DIR}/postgresql/logs/`
- 系统日志：`journalctl -u postgresql`

## 注意事项

1. **兼容性**：PostgreSQL 支持从 MoviePilot v2.0 开始
2. **备份**：建议定期备份数据库
3. **版本**：建议使用 PostgreSQL 12 或更高版本
4. **字符集**：确保使用 UTF-8 字符集

## 技术支持

如果遇到问题，请：

1. 查看应用日志
2. 检查 PostgreSQL 日志
3. 在 GitHub Issues 中报告问题
