1. 为什么需要从Postgres迁移到MySQL最近在改造Dify知识库项目时遇到了一个棘手的问题项目默认使用PostgreSQL数据库但团队的技术栈和运维环境都是基于MySQL的。这让我不得不考虑进行数据库迁移。相信很多开发者都遇到过类似的技术栈适配问题今天我就把整个迁移过程记录下来希望能帮到有同样需求的同学。PostgreSQL和MySQL都是优秀的开源关系型数据库但它们的差异确实不小。PostgreSQL以功能丰富著称支持JSON、GIS等高级特性而MySQL则以简单易用、性能稳定见长。在Dify项目中PostgreSQL的UUID类型、时区处理等特性都给迁移带来了挑战。不过好在项目使用了SQLAlchemy ORM框架这为我们的迁移工作提供了很大便利。2. 迁移前的准备工作2.1 环境配置检查首先需要确保开发环境已经准备好。除了安装MySQL服务器外还需要安装Python的MySQL客户端库。我推荐使用mysqlclient它是MySQL官方推荐的Python驱动pip install mysqlclient2.2.1同时检查SQLAlchemy的版本建议使用较新的稳定版pip install sqlalchemy1.4.462.2 数据库连接配置修改Dify项目原来的PostgreSQL连接配置通常长这样SQLALCHEMY_DATABASE_URI postgresql://user:passwordlocalhost/dify需要改为MySQL的连接方式SQLALCHEMY_DATABASE_URI mysql://user:passwordlocalhost/dify?charsetutf8mb4注意添加了charsetutf8mb4参数这是为了支持完整的Unicode字符集避免存储emoji等特殊字符时出现问题。3. 处理数据类型兼容性问题3.1 UUID主键的处理PostgreSQL原生支持UUID类型但MySQL没有。在Dify项目中很多表使用UUID作为主键。原来的模型定义可能是这样的from sqlalchemy.dialects.postgresql import UUID class User(db.Model): id db.Column(UUID, primary_keyTrue, defaultlambda: uuid.uuid4())迁移到MySQL时需要修改为class User(db.Model): id db.Column(db.String(36), primary_keyTrue, defaultlambda: str(uuid.uuid4()))对应的MySQL表结构应该是id varchar(36) NOT NULL PRIMARY KEY3.2 时区函数的调整PostgreSQL有丰富的时区处理函数比如项目中用到的DATE_TRUNC(day, m.created_at AT TIME ZONE UTC AT TIME ZONE :tz)在MySQL中需要简化为DATE(created_at) AS date对应的Python代码中也需要移除时区相关的参数# 原来的参数 arg_dict {tz: account.timezone, app_id: app_model.id} # 修改后 arg_dict {app_id: app_model.id}4. 二进制数据的存储方案4.1 序列化数据存储问题在迁移过程中最棘手的问题是二进制数据的存储。Dify项目中有些表需要存储embedding向量原来使用PostgreSQL的bytea类型embedding db.Column(db.LargeBinary, nullableFalse)但在MySQL中直接存储pickle序列化的二进制数据会报错(MySQLdb.OperationalError) (1366, Incorrect string value: \\x80\\x05\\x95\\x07$\\x00... for column embedding at row 1)4.2 JSON序列化方案解决方案是将二进制数据转为JSON字符串存储。首先修改模型定义embedding db.Column(db.Text, nullableFalse) # 改为TEXT类型然后重写相关的setter和getter方法def set_embedding(self, embedding_data: list[float]): self.embedding json.dumps(embedding_data) def get_embedding(self) - list[float]: return json.loads(self.embedding)这样修改后数据就能正常存储和读取了。虽然JSON序列化会比二进制格式占用更多空间但换来了更好的兼容性和可读性。5. 迁移后的验证与测试5.1 数据库结构检查迁移完成后建议使用工具如MySQL Workbench或命令行检查表结构是否正确。特别注意主键类型是否从UUID变为VARCHAR二进制字段是否已改为TEXT索引是否正常创建字段长度是否足够5.2 功能回归测试需要重点测试以下功能点用户注册和登录知识库文档的上传和查询包含特殊字符的数据存储大量数据的插入和查询性能我在测试时发现原来依赖PostgreSQL特定功能的查询都需要重写。比如一些使用了PostgreSQL特有函数或语法的复杂查询在MySQL中需要找到对应的替代方案。6. 部署与运维注意事项6.1 Docker环境调整如果使用Docker部署需要修改docker-compose.yml文件中的数据库配置services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: dify ports: - 3306:33066.2 迁移脚本编写对于已有数据的迁移可以编写专门的迁移脚本。基本思路是从PostgreSQL导出数据进行必要的数据转换导入到MySQL一个简单的数据迁移示例# 从PostgreSQL读取 with postgres_engine.connect() as conn: data conn.execute(SELECT * FROM documents).fetchall() # 转换数据 converted [] for row in data: converted.append({ id: str(row.id), content: row.content, embedding: json.dumps(pickle.loads(row.embedding)) if row.embedding else None }) # 写入MySQL with mysql_engine.connect() as conn: for item in converted: conn.execute( INSERT INTO documents (id, content, embedding) VALUES (:id, :content, :embedding), item )7. 性能优化建议迁移到MySQL后可能会发现一些性能差异。以下是几个优化建议为常用查询字段添加索引调整MySQL的缓冲池大小考虑使用连接池管理数据库连接对大表考虑分区策略特别是在处理embedding这类大文本字段时要注意查询效率。可以通过以下方式优化# 只查询需要的字段避免返回大文本 session.query(Document.id, Document.title).filter(...)8. 常见问题解决方案在实际迁移过程中我遇到了几个典型问题编码问题确保MySQL使用utf8mb4字符集以支持完整的Unicode事务隔离级别PostgreSQL和MySQL的默认隔离级别不同可能导致并发问题布尔类型处理MySQL的TINYINT(1)与PostgreSQL的BOOLEAN需要特别注意自增ID如果项目中有使用自增ID需要注意MySQL的auto_increment特性对于事务问题可以通过显式设置隔离级别来解决engine create_engine( SQLALCHEMY_DATABASE_URI, isolation_levelREPEATABLE READ )整个迁移过程虽然遇到不少挑战但SQLAlchemy的ORM抽象确实帮了大忙。大部分业务代码不需要修改只需要调整模型定义和少数SQL语句。最终我们的Dify知识库成功运行在MySQL上性能表现也很稳定。