最近把我的博客(基于Typecho )的数据库从 MySQL 迁移到了 PostgreSQL。迁移本身不算复杂,但真正容易出问题的地方不在“能不能导进去”,而在这些细节:
1. 迁移期间是否停止写入,避免新文章或新评论丢失
2. pgloader 默认会修改标识符大小写,可能让 Typecho 找不到驼峰字段
3. pgloader 可能把表导入到非 public schema,而 Typecho 的 PostgreSQL 适配器更适合 public schema
4. PostgreSQL 序列、主键、索引和权限需要单独确认
5. 迁移命令、备份文件和调试信息都可能泄露敏感信息这篇文章记录一次基于 1Panel、Docker、MySQL、PostgreSQL、pgAdmin 和 pgloader 的完整迁移过程。它不是唯一做法,但会尽量把生产环境里容易踩的坑提前写清楚。
一、迁移环境
本次迁移环境如下:
服务器面板:1Panel
网站程序:Typecho,程序文件在宿主机网站目录
PHP 环境:1Panel 创建的 PHP 容器
MySQL:Docker 容器
PostgreSQL:Docker 容器
PostgreSQL 管理工具:pgAdmin
Docker 网络:1panel-network需要特别注意:
Typecho 程序本身不是容器
真正执行 PHP 的是 1Panel 的 PHP 容器
真正需要安装 pdo_pgsql 扩展的是 PHP 容器
真正需要重启的也是 PHP 容器
pgAdmin 只是管理工具,不是 PostgreSQL 服务本体也就是说,Typecho 文件在宿主机网站目录中,PHP 由 1Panel 的 PHP 容器提供运行环境,MySQL 和 PostgreSQL 都是 Docker 容器。
二、迁移前的安全准备
正式迁移前,建议先把站点切到维护窗口。至少要做到:
1. 暂停发布文章、评论、注册、后台修改配置等写入操作
2. 通知自己或站点用户迁移期间不要提交内容
3. 备份 MySQL 数据库和网站文件
4. 确认备份文件不放在 Web 可访问目录
5. 新建一个空的 PostgreSQL 数据库作为目标库不要把数据库备份、迁移脚本、包含密码的文件放在 /www/sites/... 这类网站目录里。数据库备份里可能包含用户邮箱、密码哈希、登录令牌、评论 IP、插件配置等敏感信息。
三、备份 MySQL 数据库和网站文件
先备份 MySQL 数据库:
mysqldump --single-transaction --routines --triggers --events --default-character-set=utf8mb4 \
-u root -p typecho > /root/typecho_mysql_backup.sql其中 typecho 是原 MySQL 数据库名,根据实际情况修改。
如果 MySQL 在 Docker 容器里,也可以在容器内执行 mysqldump,再把结果重定向到宿主机文件。不要把真实密码直接写进公开笔记或博客示例里。
再备份网站文件:
tar -czvf /root/typecho_files_backup.tar.gz /www/sites/你的站点目录
chmod 600 /root/typecho_mysql_backup.sql /root/typecho_files_backup.tar.gz重点备份这些内容:
/usr/uploads
/usr/themes
/usr/plugins
config.inc.php建议迁移前至少确认备份文件大小正常,并保留一份可以回滚的 MySQL 配置。
四、安装 PHP PostgreSQL 扩展
Typecho 使用 PostgreSQL 时,需要 PHP 支持 PostgreSQL。1Panel 的 PHP 扩展管理中建议安装:
pdo_pgsql
pgsql其中最关键的是:
pdo_pgsql因为 Typecho 配置 PostgreSQL 时通常使用:
$db = new \Typecho\Db('Pdo_Pgsql', 'typecho_');安装完成后,重启该站点实际使用的 PHP 容器。可以先查看容器名:
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Networks}}\t{{.Ports}}"然后进入 PHP 容器检查扩展是否生效:
docker exec -it PHP容器名 php -m | grep -iE "pdo|pgsql"正常应该能看到类似:
PDO
pdo_pgsql
pgsql如果没有 pdo_pgsql,说明扩展没有装到当前站点实际使用的 PHP 容器里。
五、创建 PostgreSQL 数据库和用户
可以在 pgAdmin 中创建 PostgreSQL 数据库和用户,也可以使用 SQL。下面的密码只是占位符,正式操作时请替换成强密码:
CREATE USER typecho WITH PASSWORD 'REPLACE_WITH_STRONG_PASSWORD';
CREATE DATABASE typecho OWNER typecho ENCODING 'UTF8' TEMPLATE template0;本次示例使用:
数据库名:typecho
用户名:typecho
数据库拥有者:typecho
编码:UTF8更安全的做法是区分迁移用户和运行用户:
迁移用户:负责建表、导入数据、调整 schema、调整序列
运行用户:只负责 Typecho 日常 SELECT / INSERT / UPDATE / DELETE个人小站为了简单可以先使用同一个 typecho 用户,但迁移完成后仍建议收敛权限,不要额外授予不必要的 CREATE 权限。
六、用 pgloader 迁移数据
pgloader 可以把 MySQL 的表结构、数据、索引、主键、序列等迁移到 PostgreSQL。因为 MySQL 和 PostgreSQL 都在 Docker 里,运行 pgloader 时需要加入同一个 Docker 网络:
1panel-network这里不推荐把真实密码直接写在一条命令里。命令行参数可能进入 shell history,也可能被进程列表或日志看到。建议创建一个临时目录和 pgloader 配置文件,迁移完成后删除。
mkdir -p /root/typecho-migration
chmod 700 /root/typecho-migration创建 /root/typecho-migration/typecho.load:
LOAD DATABASE
FROM mysql://MySQL用户名:MySQL密码@MySQL容器名:3306/typecho
INTO postgresql://typecho:PostgreSQL密码@PostgreSQL容器名:5432/typecho
WITH include drop, create tables, create indexes, reset sequences,
foreign keys, quote identifiers, uniquify index names
SET maintenance_work_mem to '128MB',
work_mem to '12MB';然后限制文件权限:
chmod 600 /root/typecho-migration/typecho.load这里有几个重点:
include drop:会删除目标库里同名表,只能对新建空库或确认可清空的库使用
quote identifiers:保留 MySQL 中的字段大小写,避免 authCode 被转成 authcode
reset sequences:导入后重置 PostgreSQL 序列Typecho 的 MySQL 表中有这些驼峰字段:
authCode
screenName
authorId
ownerId
commentsNum
allowComment
allowPing
allowFeedTypecho 的 PostgreSQL 适配器查询时也会使用带大小写的字段名。如果 pgloader 默认把字段名转成小写,后面很容易出现:
column "authCode" of relation "typecho_users" does not exist执行迁移:
docker run --rm --network 1panel-network \
-v /root/typecho-migration/typecho.load:/typecho.load:ro \
dimitri/pgloader:latest \
pgloader /typecho.load正式写进运维文档时,建议把 dimitri/pgloader:latest 换成自己测试过的固定版本或镜像 digest。latest 方便一次性迁移,但不利于复现。
迁移完成后,重点看 pgloader 输出里的 errors 是否为 0,而不是只看最后有没有 Total import time。
七、检查表所在 schema
pgloader 可能会把表导入到源 MySQL 数据库同名的 schema 下。比如源库叫 blog,导入后可能出现:
blog.typecho_contents
blog.typecho_options而不是:
public.typecho_contents
public.typecho_options先在 PostgreSQL 中检查:
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_name LIKE 'typecho_%'
ORDER BY table_schema, table_name;Typecho 的 PostgreSQL 适配器更适合在 public schema 中工作,尤其是插入数据时会查 public schema 下的主键信息。所以如果表在 blog 这类非 public schema 下,建议移动到 public。
假设当前 schema 是 blog,可以执行:
DO $$
DECLARE
source_schema text := 'blog';
r record;
BEGIN
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = source_schema
AND tablename LIKE 'typecho\_%' ESCAPE '\'
LOOP
EXECUTE format('ALTER TABLE %I.%I SET SCHEMA public;', source_schema, r.tablename);
END LOOP;
END $$;再移动 Typecho 相关序列:
DO $$
DECLARE
source_schema text := 'blog';
r record;
BEGIN
FOR r IN
SELECT sequence_name
FROM information_schema.sequences
WHERE sequence_schema = source_schema
AND sequence_name LIKE 'typecho\_%' ESCAPE '\'
LOOP
EXECUTE format('ALTER SEQUENCE %I.%I SET SCHEMA public;', source_schema, r.sequence_name);
END LOOP;
END $$;再次确认:
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_name LIKE 'typecho_%'
ORDER BY table_schema, table_name;理想情况下,Typecho 的核心表都在 public schema 下。
八、如果字段已经被转成小写
如果前面没有使用 quote identifiers,pgloader 可能已经把驼峰字段转成了小写。例如:
authCode -> authcode
screenName -> screenname
authorId -> authorid
ownerId -> ownerid
commentsNum -> commentsnum
allowComment -> allowcomment可以先检查:
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name IN ('typecho_users', 'typecho_contents', 'typecho_comments')
AND column_name IN (
'authcode', 'screenname', 'authorid', 'ownerid',
'commentsnum', 'allowcomment', 'allowping', 'allowfeed'
)
ORDER BY table_name, column_name;如果确实存在这些小写字段,可以执行下面的补救 SQL:
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_users'
AND column_name = 'authcode'
) THEN
ALTER TABLE public.typecho_users RENAME COLUMN authcode TO "authCode";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_users'
AND column_name = 'screenname'
) THEN
ALTER TABLE public.typecho_users RENAME COLUMN screenname TO "screenName";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_contents'
AND column_name = 'authorid'
) THEN
ALTER TABLE public.typecho_contents RENAME COLUMN authorid TO "authorId";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_contents'
AND column_name = 'commentsnum'
) THEN
ALTER TABLE public.typecho_contents RENAME COLUMN commentsnum TO "commentsNum";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_contents'
AND column_name = 'allowcomment'
) THEN
ALTER TABLE public.typecho_contents RENAME COLUMN allowcomment TO "allowComment";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_contents'
AND column_name = 'allowping'
) THEN
ALTER TABLE public.typecho_contents RENAME COLUMN allowping TO "allowPing";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_contents'
AND column_name = 'allowfeed'
) THEN
ALTER TABLE public.typecho_contents RENAME COLUMN allowfeed TO "allowFeed";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_comments'
AND column_name = 'authorid'
) THEN
ALTER TABLE public.typecho_comments RENAME COLUMN authorid TO "authorId";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_comments'
AND column_name = 'ownerid'
) THEN
ALTER TABLE public.typecho_comments RENAME COLUMN ownerid TO "ownerId";
END IF;
END $$;再检查一次:
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'typecho_users'
ORDER BY ordinal_position;应该能看到:
screenName
authCode九、检查并修复序列
PostgreSQL 中自增字段依赖序列。迁移后如果序列值小于当前表里的最大主键,后续新增文章、评论或用户时可能出现主键冲突。
先检查 Typecho 常见自增字段:
SELECT 'contents' AS table_name, MAX("cid") AS max_id FROM public.typecho_contents
UNION ALL
SELECT 'comments', MAX("coid") FROM public.typecho_comments
UNION ALL
SELECT 'metas', MAX("mid") FROM public.typecho_metas
UNION ALL
SELECT 'users', MAX("uid") FROM public.typecho_users;然后使用 PostgreSQL 的 pg_get_serial_sequence() 找到列绑定的序列并自动修复:
DO $$
DECLARE
r record;
max_id bigint;
seq_name text;
BEGIN
FOR r IN
SELECT
table_name,
column_name
FROM (VALUES
('typecho_contents', 'cid'),
('typecho_comments', 'coid'),
('typecho_metas', 'mid'),
('typecho_users', 'uid')
) AS t(table_name, column_name)
LOOP
seq_name := pg_get_serial_sequence(format('public.%I', r.table_name), r.column_name);
IF seq_name IS NULL THEN
RAISE NOTICE 'No sequence found for %.%', r.table_name, r.column_name;
CONTINUE;
END IF;
EXECUTE format('SELECT COALESCE(MAX(%I), 0) FROM public.%I', r.column_name, r.table_name)
INTO max_id;
IF max_id < 1 THEN
EXECUTE format('SELECT setval(%L::regclass, 1, false)', seq_name);
ELSE
EXECUTE format('SELECT setval(%L::regclass, %s, true)', seq_name, max_id);
END IF;
END LOOP;
END $$;如果看到 No sequence found 提示,说明 pgloader 创建的序列没有和列默认值绑定,需要手动查看表结构后处理:
SELECT table_name, column_name, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name LIKE 'typecho_%'
AND column_default LIKE 'nextval(%';十、授权给 Typecho 用户
如果迁移和运行都使用 typecho 用户,可以执行:
GRANT USAGE ON SCHEMA public TO typecho;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO typecho;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO typecho;如果之前为了迁移临时授予了更大的权限,迁移完成后应当收敛。日常运行的 Typecho 用户一般不需要在 public schema 中创建新表。
十一、对比源库和目标库数据量
不要只看 PostgreSQL 里有没有表。迁移完成后至少要对比核心表行数。
MySQL 中执行:
SELECT 'contents' AS table_name, COUNT(*) AS rows_count FROM typecho_contents
UNION ALL
SELECT 'comments', COUNT(*) FROM typecho_comments
UNION ALL
SELECT 'options', COUNT(*) FROM typecho_options
UNION ALL
SELECT 'users', COUNT(*) FROM typecho_users
UNION ALL
SELECT 'metas', COUNT(*) FROM typecho_metas
UNION ALL
SELECT 'fields', COUNT(*) FROM typecho_fields
UNION ALL
SELECT 'relationships', COUNT(*) FROM typecho_relationships;PostgreSQL 中执行:
SELECT 'contents' AS table_name, COUNT(*) AS rows_count FROM public.typecho_contents
UNION ALL
SELECT 'comments', COUNT(*) FROM public.typecho_comments
UNION ALL
SELECT 'options', COUNT(*) FROM public.typecho_options
UNION ALL
SELECT 'users', COUNT(*) FROM public.typecho_users
UNION ALL
SELECT 'metas', COUNT(*) FROM public.typecho_metas
UNION ALL
SELECT 'fields', COUNT(*) FROM public.typecho_fields
UNION ALL
SELECT 'relationships', COUNT(*) FROM public.typecho_relationships;如果行数不一致,先不要切换 Typecho 配置。应该回到 pgloader 输出和日志中查 errors,确认是否有数据类型、编码、非法日期、字段长度等问题。
十二、修改 Typecho 配置文件
Typecho 的配置文件在宿主机网站目录中,例如:
/www/sites/你的域名/index/config.inc.php原来的 MySQL 配置可能是:
$db = new \Typecho\Db('Pdo_Mysql', 'typecho_');改成 PostgreSQL:
$db = new \Typecho\Db('Pdo_Pgsql', 'typecho_');
$db->addServer(array (
'host' => 'PostgreSQL容器名',
'port' => 5432,
'user' => 'typecho',
'password' => '你的PostgreSQL密码',
'charset' => 'utf8',
'database' => 'typecho',
), \Typecho\Db::READ | \Typecho\Db::WRITE);
\Typecho\Db::set($db);因为 PHP 是容器,PostgreSQL 也是容器,所以 host 应该填写 PHP 容器能够访问到的 PostgreSQL 容器名或 Docker 网络内服务名,例如:
'host' => 'postgresql',不要写成 pgAdmin,也不一定能写 127.0.0.1。127.0.0.1 在 PHP 容器里通常代表 PHP 容器自己,不代表 PostgreSQL 容器。
表前缀保持:
'typecho_'不要写成:
'blog.typecho_'schema 问题应该在 PostgreSQL 里解决,而不是把 schema 拼进表前缀。
十三、在 PHP 容器里测试 PostgreSQL 连接
真正执行 Typecho 的是 PHP 容器,所以连接测试也应该进入 PHP 容器执行:
docker exec -it PHP容器名 php -r '
try {
$pdo = new PDO("pgsql:host=PostgreSQL容器名;port=5432;dbname=typecho", "typecho", "你的密码");
echo "PostgreSQL connect ok\n";
} catch (Throwable $e) {
echo $e->getMessage() . "\n";
}
'如果输出:
PostgreSQL connect ok说明:
PHP 容器里的 pdo_pgsql 正常
PHP 容器能访问 PostgreSQL 容器
PostgreSQL 用户名、密码、数据库名正确十四、排查 Database Query Error
迁移后访问网站,页面可能出现:
Database Query Error排查时不要直接在公网生产页面长时间开启错误显示。更稳妥的做法是:
1. 先查看 PHP 容器日志、1Panel 网站日志、Nginx 日志
2. 如果必须开启 Typecho 调试,只在维护窗口内临时开启
3. 最好限制访问来源,只允许自己的 IP 访问
4. 定位问题后立即关闭调试临时调试代码如下:
define('__TYPECHO_DEBUG__', true);
ini_set('display_errors', 'On');
error_reporting(E_ALL);如果看到:
SQLSTATE[42703]: Undefined column: 7 ERROR:
column "authCode" of relation "typecho_users" does not exist一般说明连接已经正常,表也找到了,但字段大小写不符合 Typecho 的 PostgreSQL 查询方式。回到前面的“字段已经被转成小写”一节处理即可。
十五、重启 PHP 容器并验证站点
修改 PHP 扩展、数据库配置或 config.inc.php 后,需要重启该站点使用的 PHP 容器。
查看容器名:
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Networks}}\t{{.Ports}}"重启:
docker restart PHP容器名也可以在 1Panel 面板中重启该网站对应的 PHP 运行环境。
然后逐项检查:
首页是否正常
文章页是否正常
独立页面是否正常
分类和标签是否正常
评论是否正常显示
后台是否能登录
能否新建草稿
能否发布或更新文章
能否新增评论
附件是否正常显示
主题是否正常
插件是否正常
固定链接是否正常
RSS 是否正常重点测试“新增”操作,因为序列问题通常不会在只读浏览时暴露,只有插入新文章、新评论、新用户时才会出现。
十六、插件兼容性
如果某些插件报错,常见原因是插件里写了 MySQL 专属 SQL,例如反引号:
SELECT * FROM `typecho_contents`PostgreSQL 不支持 MySQL 的反引号语法。这种情况需要修改插件代码,或者禁用不兼容插件。
还要留意这些 MySQL 专属写法:
反引号 `table`
INSERT IGNORE
REPLACE INTO
ON DUPLICATE KEY UPDATE
LIMIT offset, size
NOW()、FROM_UNIXTIME() 等函数差异Typecho 核心可以使用 PostgreSQL,但插件未必都兼容。
十七、迁移后的收尾
确认网站正常后,记得做这些收尾工作:
1. 关闭 Typecho 调试模式和 display_errors
2. 删除 /root/typecho-migration/typecho.load
3. 删除或加密保存数据库备份和文件备份
4. 撤销迁移临时用户或收敛权限
5. 记录 PostgreSQL 容器名、数据库名、备份位置和回滚方案
6. 观察一段时间 PostgreSQL、PHP、Nginx 日志删除临时迁移文件:
rm -f /root/typecho-migration/typecho.load
rmdir /root/typecho-migration如果迁移过程中临时暴露过密码,建议迁移完成后修改 PostgreSQL 密码,并同步更新 config.inc.php。
总结
本次 Typecho 从 MySQL 迁移到 PostgreSQL,主流程是:
1. 进入维护窗口,暂停写入
2. 备份 MySQL 数据库和 Typecho 网站文件
3. 在 PHP 容器中安装 pdo_pgsql
4. 创建 PostgreSQL 空库和用户
5. 使用 pgloader 迁移,并显式启用 quote identifiers
6. 检查表是否在 public schema
7. 必要时移动表和序列到 public
8. 检查字段大小写,必要时修复驼峰字段
9. 检查并修复 PostgreSQL 序列
10. 对比 MySQL 和 PostgreSQL 核心表行数
11. 修改 Typecho config.inc.php
12. 在 PHP 容器中测试 PostgreSQL 连接
13. 重启 PHP 容器并验证前台、后台、发布、评论、附件和插件
14. 关闭调试,删除迁移文件,收敛权限这次迁移里最关键的坑有三个:
1. pgloader 默认可能把驼峰字段名转成小写
2. pgloader 可能把表导入到非 public schema
3. 迁移后必须确认序列值,否则新增内容可能主键冲突如果只记住一句话:Typecho 迁移到 PostgreSQL,不要只验证“页面能打开”,还要验证“后台能新增内容”。前者只能证明读数据大致正常,后者才能暴露 schema、字段大小写、权限和序列这类真正容易遗漏的问题。