前言
Memos 是一个非常好用的开源、自托管的笔记服务。可以说从它刚在Github上发布的时候我就一直在用它了。
但是随着它不断的更新,逐步开发了MySQL以及Postgres数据库的支持,但是一直未推出完善可靠的数据库迁移服务。
最近打算将之前的MySQL迁移到Postgres时,使用pgloader等工具,总能出现各种问题,不尽人意,因此只能手动对其数据迁移。
说明
Memos的本质是笔记服务,因此我的数据基本上都是文字形式,即使有图片,大多数都是以外链形式存在,即Memos本身并不存储图片,从而使得迁移工作相对简单。
如果你的数据中有很多图片,以下方法可能不适合你。
准备
Navicat数据库管理软件
步骤
基于我的Memos是Docker compose搭建的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| services: mysql: image: mysql:latest restart: unless-stopped volumes: - ./data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: memos MYSQL_USER: memos MYSQL_PASSWORD: password memos: image: neosmemo/memos:stable restart: always depends_on: - mysql ports: - "5230:5230" environment: - MEMOS_DRIVER=mysql - MEMOS_DSN=memos:password@(mysql:3306)/memos
|
第一步: 修改compose文件
添加Postgres
开放对应的数据库端口
最终的 compose 文件基本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| services: mysql: image: mysql:latest restart: unless-stopped ports: - "10001:3306" volumes: - ./data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: memos MYSQL_USER: memos MYSQL_PASSWORD: password postgres: image: postgres:15 restart: unless-stopped ports: - "10002:5432" environment: POSTGRES_DB: memos POSTGRES_USER: memos POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data
memos: image: neosmemo/memos:stable restart: always depends_on: - postgres ports: - "5230:5230" environment: - MEMOS_DRIVER=postgres - MEMOS_DSN=postgresql://memos:password@postgres:5432/memos?sslmode=disable
volumes: postgres_data:
|
我们分析一下上面的代码:
确定这些后,通过 docker compose up -d 启动容器。

第二步: 初始化Memos
说是初始化,其实也就是因为使用了Postgres,导致这个容器是全新的,登陆页面后会要求创建用户,正常创建即可,尝试发布几个笔记。正常的话进入下一步。
第三步:数据库连接
打开Navicat软件,新建数据库连接,将上述的MySQL和Postgres分别添加进去。

连接前检查服务器或者服务器提供商的防火墙是否开放了对外的 10001 和 10002 端口
最终可以看到:
MySQL的数据储存在【memos】-【memos】中:

Postgres的数据储存在【memos】-【public】-【Tables】-【memos】中:

对比2个数据库的差异,可以看得出来结构一致,区别在与 created_ts 以及 updated_ts ,在MySQL中使用的是日期格式(YYYY-MM-DD HH:MM:SS),而Postgres中使用的是Unix 时间戳 (Unix Timestamp),还有个不同在于 pinned (置顶),MySQL的 0 或者 1 ,与Postgres的 f 以及 t,实际上无需转换。
PostgreSQL 中 BOOLEAN 类型的字段可以接受以下所有输入形式,并自动把它们转换为标准的 boolean:
| 输入值 (不区分大小写) |
Postgres 理解为 |
1, yes, on, true, t, y |
True (t) |
0, no, off, false, f, n |
False (f) |
通过这一步可以确认,我们只需将MySQL中的笔记按照Posters的格式导入到现在的数据库中应该就能正常识别。
第四步:导出MySQL数据库中的笔记
右键【Memos】-【Export Wizard…】,选择 csv 格式,一路下一步,直到导出得到 memo.csv。



第五步:转换memo.csv中的时间戳
将memo.csv上传至服务器或者有python环境的系统中,在同一目录创建一个python脚本convert.py,内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import csv import time from datetime import datetime, timezone, timedelta
input_file = 'memo.csv' output_file = 'memos_converted.csv'
date_format = "%d/%m/%Y %H:%M:%S"
tz_beijing = timezone(timedelta(hours=8))
def to_timestamp(date_str): try: dt = datetime.strptime(date_str, date_format) dt = dt.replace(tzinfo=tz_beijing) return int(dt.timestamp()) except ValueError: return date_str
print("正在转换...")
with open(input_file, mode='r', encoding='utf-8', newline='') as infile, \ open(output_file, mode='w', encoding='utf-8', newline='') as outfile: reader = csv.reader(infile) writer = csv.writer(outfile, quoting=csv.QUOTE_ALL)
headers = next(reader) writer.writerow(headers)
for row in reader: row[3] = to_timestamp(row[3]) row[4] = to_timestamp(row[4]) writer.writerow(row)
print(f"转换完成!文件已保存为: {output_file}")
|
在该目录输入 python3 convert.py 运行该转换脚本。

查看 memos_converted.csv 文件,时间戳已经转换成功:

第六步:导入Postgres数据库中
来到Navicat,清空之前测试的几条数据:(或者在表中直接选中那几条数据进行删除也行)

删除后需要点击底部的 Refresh 刷新才能更新状态
然后点击上方的 【Import】 图标-【csv文件】-【Add File…】选择转换后的memos_converted.csv

一路下一步,在下面界面中选择全部:

继续下一步,直至结束:

完成后还是点击查看底部工具栏的刷新图标,确认导入完成即可。

验证
回到网页,进行刷新,以前的数据已经出现,状态也正确。

🛑🛑🛑特别注意:
测试中这一步我可以正常发布,但是在生产过程中,有几百几千条笔记,每个笔记都有一个uid,因此很可能会遇到在发布的时候会提示:该uid已被使用的错误提示!
强烈建议在Navicat中,点击左上角的 【新建查询】 (New Query),运行以下这条命令:
1 2
| SELECT setval('memo_id_seq', (SELECT MAX(id) FROM memo));
|
确定没问题之后,恢复成正常的compose配置文件即可(去掉mysql,2个端口映射):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| services: postgres: image: postgres:15 restart: unless-stopped environment: POSTGRES_DB: memos POSTGRES_USER: memos POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data
memos: image: neosmemo/memos:stable restart: always depends_on: - postgres ports: - "5230:5230" environment: - MEMOS_DRIVER=postgres - MEMOS_DSN=postgresql://memos:password@postgres:5432/memos?sslmode=disable
volumes: postgres_data:
|
最后
即使这个方法有一丁点的局限性,无法迁移导出对应上传的图片等资源,仅仅针对文字类的。但对我个人而已已经够用了。
我现在仅演示了从MySQL到Postgres的过程,但是如果是SQLite的话也是一样的流程,具体没有尝试,最多由于表的独特性,可能python脚本会相应变更,这部分交给各类AI工具就能搞定。