今天不写新功能,做一次"搬家 ":把早报站的数据从SQLite搬进PostgreSQL------这是整个二季的地基工程。
🎯 本篇产出:一个跑在Docker里的PostgreSQL、一份可重复执行的数据迁移脚本、以及"为什么换"的完整决策链。含代码约60行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | SQLite → PostgreSQL 数据迁移 |
| 代码行数 | ~60行(迁移脚本) |
| 依赖 | psycopg(Python驱动)+ postgres:18-alpine Docker镜像 |
| 核心功能 | Docker起库 + 数据搬家 + 幂等脚本 |
| 跑起来的命令 | docker run ... → python migrate_sqlite_to_pg.py |
| 核心知识点 | DSN连接串、SQL方言差异、ON CONFLICT DO NOTHING |
| 做完你能得到 | 一个生产级数据库 + 可重复执行的迁移脚本 |
⚠️ 诚实声明范围:今天完成"环境 + 数据",明天(下一篇)完成"代码"。
一、先回答"为什么换"------SQLite的三个天花板
第28篇选型时我说"SQLite够了"------当时是真的够:单机、单写者、每天5篇文章。但产品要面对真实用户了,三个天花板会依次撞上:
| # | 天花板 | 具体表现 |
|---|---|---|
| ① | 并发写锁 | SQLite同一时刻只允许一个写入者,两个用户同时操作就是database is locked |
| ② | 没有迁移体系 | 改表结构靠删库重建(第17篇⑥),表越多这招越危险 |
| ③ | 类型与特性 | 类型宽松、缺JSONB/数组/全文检索,数据一复杂就捉襟见肘 |
🎯 决策框架
📌 技术选型不是永恒决定,是"当时场景"的最优解。
场景变了(单机玩具 → 公网产品),选型就该升级------推翻自己不是打脸,是成长。
PostgreSQL是这个场景下的生产标配:并发读写、完整的类型系统、成熟的迁移生态。
二、两分钟概念课:从"文件库"到"服务"
SQLite和PostgreSQL最本质的区别,一张图说清:
text
SQLite:程序直接读写的"文件" PostgreSQL:一个独立运行的"服务"
┌─────────────┐ ┌──────────┐ ┌──────────┐
│ your.py │──读写──→ daily.db │ your.py │──→│ PG 服务 │──→ data/
└─────────────┘ └──────────┘ └──────────┘
(客户端) (端口 5432)
📌 SQLite是嵌在你程序里的一个文件 ;PostgreSQL是独立运行的数据库服务,你的程序通过网络连它。
换成PG后多了两个概念
| 概念 | 说明 |
|---|---|
| 连接串(DSN) | postgresql://用户名:密码@主机:5432/库名------五段式解剖:协议 / 用户 / 密码 / 主机:端口 / 数据库 |
| 客户端 | 连上服务的工具------命令行psql、图形化DBeaver、或你代码里的驱动psycopg |
三、第1步:Docker起PostgreSQL
Docker技能:
bash
docker run -d --name pg-daily \
-e POSTGRES_PASSWORD=你的密码 \
-e POSTGRES_DB=daily \
-v pg-daily-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:18-alpine
💡 为什么用
18-alpine而不是18?alpine变体体积更小(约60MB),生产环境推荐固定小版本标签(如18.6-alpine)避免意外升级。学习阶段用18-alpine即可。
📖 逐参数拆解
| 参数 | 作用 |
|---|---|
POSTGRES_PASSWORD |
设超级用户密码 |
POSTGRES_DB=daily |
启动时自动建一个叫daily的库 |
-v pg-daily-data:/var/lib/postgresql/data |
数据卷------数据放卷里,代码放镜像里,容器删了数据还在 |
-p 5432:5432 |
把服务的默认端口映射出来 |
✅ 验证服务活着
bash
docker exec -it pg-daily psql -U postgres -d daily -c "\dt"
能进入psql交互界面(哪怕显示Did not find any tables),服务就绪。
四、第2步:安装驱动 + 建表------SQL方言的第一课
安装Python驱动
bash
pip install psycopg
pip freeze > requirements.txt
建表SQL的方言差异对照
把早报站的建表语句翻译成PG版,顺便认识方言差异:
| 写法 | SQLite(旧) | PostgreSQL(新) |
|---|---|---|
| 自增主键 | INTEGER PRIMARY KEY AUTOINCREMENT |
INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY |
| 占位符 | ?(sqlite3) |
%s(psycopg) |
| 去重插入 | INSERT OR IGNORE |
INSERT ... ON CONFLICT (url) DO NOTHING |
| 类型检查 | 宽松(类型亲和) | 严格(类型不对直接拒绝) |
🚨 最后一条要单独强调 :SQLite的宽松是"惯着你",PG的严格是"保护你"------数字列塞文本当场报错,数据质量问题在入库前就暴露,而不是三个月后在报表里。
五、第3步:数据搬家脚本------可重复执行的迁移
新建migrate_sqlite_to_pg.py:
python
"""migrate_sqlite_to_pg.py ------ 把早报站数据从SQLite搬进PostgreSQL"""
import sqlite3
import psycopg
PG_DSN = "postgresql://postgres:你的密码@localhost:5432/daily"
SQLITE_FILE = "daily.db"
def migrate() -> None:
# ① 从SQLite读出全部旧数据
src = sqlite3.connect(SQLITE_FILE)
rows = src.execute(
"SELECT url, title, date, summary FROM articles").fetchall()
src.close()
print(f"从SQLite读出 {len(rows)} 条")
# ② 建表(幂等)+ 逐条写入(url冲突自动跳过)
with psycopg.connect(PG_DSN) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS articles (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
url TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
date TEXT,
summary TEXT DEFAULT ''
)""")
with conn.cursor() as cur:
for row in rows:
cur.execute(
"INSERT INTO articles (url, title, date, summary) "
"VALUES (%s, %s, %s, %s) "
"ON CONFLICT (url) DO NOTHING", row)
conn.commit()
# ③ 验证:总数对得上吗?
n = conn.execute("SELECT COUNT(*) FROM articles").fetchone()[0]
print(f"迁移完成:PostgreSQL中共 {n} 条")
if __name__ == "__main__":
migrate()
📌 三个细节
| # | 细节 | 说明 |
|---|---|---|
| ① | %s占位符 |
psycopg用%s(SQLite是?,方言差异见第4节) |
| ② | ON CONFLICT (url) DO NOTHING |
让脚本重复运行不出错(幂等思想) |
| ③ | 密码特殊字符 | 密码里的特殊字符(如@)在DSN里要URL转义,或改用环境变量+关键字参数连接 |
✅ 运行验证
bash
python migrate_sqlite_to_pg.py
# 从SQLite读出812条
# 迁移完成:PostgreSQL中共812条
六、第4步:验收清单
bash
1. docker ps → pg-daily容器Up状态
2. psql查询:SELECT COUNT(*) FROM articles → 与SQLite原数量一致
3. 抽查3行中文内容无乱码
4. 删掉容器重建(数据卷还在)→ 数据完整 → 卷持久化生效
5. 脚本重跑 → 数量不变(幂等)
6. 全程无报错后提交Git
bash
git add .
git commit -m "数据迁移:SQLite → PostgreSQL(脚本可重复执行)"
七、常见报错:这6个,迁移日的标配(重点!)
① psql: error: connection refused
🔍 原因 :容器没起来,或-p 5432:5432忘了映射。
✅ 解法:
bash
docker ps # 看容器状态和端口映射
docker logs pg-daily # 看启动日志
② password authentication failed for user "postgres"
🔍 原因 :密码不对,或DSN里密码含@ : /等特殊字符没转义。
✅ 解法:
- 先用纯字母数字密码跑通;
- 确需特殊字符,用URL编码(
@→%40)。
③ relation "articles" does not exist
🔍 原因:连接到了别的库,或建表没执行。
✅ 解法:psql里------
bash
\dt # 看当前库的表
\l # 列出所有库
\c daily # 切库
📌 先确认"人在哪家银行",再谈余额。
④ 占位符混用:?写进PG的SQL直接报语法错
🔍 原因 :sqlite3用?,psycopg用%s------方言不同。
✅ 解法:本篇对照表背下来;后续SQLAlchemy会替你抹平差异(下一篇的主角)。
⑤ psycopg.errors.InvalidTextRepresentation
🔍 原因:PG类型严格------往数字列塞了文本、或空字符串塞进了非文本列。
✅ 解法:对照建表语句检查数据类型。
📌 这正是换PG的价值:坏数据当场拦截。
⑥ 早报站还在读SQLite
🔍 原因 :不是bug------代码层的连接切换在下一篇(SQLAlchemy 2.0)完成,本篇只搬数据。
✅ 解法:无。
📌 诚实声明范围:今天完成"环境 + 数据",明天完成"代码"。
八、 PostgreSQL 18值得关注的新特性
| 特性 | 说明 |
|---|---|
| 异步I/O(AIO) | 存储读取性能最高提升3倍 |
| UUIDv7 | 新增uuidv7()函数,时间戳排序与唯一性兼得 |
| B-tree Skip Scan | 多列索引查询优化,减少全表扫描 |
| 并行GIN索引构建 | 大表索引创建更快 |
💡 这些特性对早报站当前规模影响不大,但了解它们有助于你理解PostgreSQL的演进方向------性能优化和开发者体验是持续投入的重点。
九、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | psql三连 :SELECT COUNT(*)、按来源分组统计、找出最新的3篇文章 |
⭐⭐ | 全部在psql里完成 |
| 2 | 泛化脚本 :把migrate脚本参数化(python migrate.py 源.db 目标dsn) |
⭐⭐⭐ | 变成通用小工具 |
| 3 | 卷实验 :docker rm -f pg-daily后重建容器(同名数据卷)→ 数据完整 |
⭐⭐ | 亲手验证第27篇的"数据放卷里" |
| 4(选做) | pgloader对比:了解一键迁移工具pgloader,对比手写脚本 | ⭐⭐⭐ | 知道工具,才知道自己在简化什么 |
📦 配套代码
迁移脚本与建表SQL已上传Git(01-pg-migration/):【gitee仓库地址】