前言
先说一个方法论问题:「优缺点」这个说法脱离场景是没有意义的。SQLite 的「不支持高并发写」在桌面笔记应用里根本不是缺点,因为那里就不存在并发写;PostgreSQL 的「功能丰富」在一个只存几十行配置表的小工具里也换不来任何收益。所以本文不做「谁更好」的排名,只讲三者在架构上有什么本质差别,以及这些差别在什么场景下会变成你的问题。
写这类对比时最常见的三个错误说法,先纠正掉:
- 「SQLite 是小号的 MySQL」。不对。它们不是「同一类东西的不同规模」,而是两种架构:SQLite 是嵌入式库,你的进程直接读写数据库文件;MySQL / PostgreSQL 是客户端-服务器系统,你的进程通过协议和一个独立的服务端进程通信。这个差别决定了并发模型、部署方式和运维成本的完全不同。
- 「MySQL 默认存储引擎是 MyISAM」。这是十年前的说法。MySQL 5.5 起默认存储引擎就是 InnoDB,MySQL 8.4 的官方文档里仍然明确写着「InnoDB:MySQL 8.4 的默认存储引擎」。
- 「PostgreSQL 比 MySQL 慢」。这类笼统结论没有意义,性能取决于具体负载、索引设计、硬件和版本。不要相信没有测试条件的对比结论。
下面从架构、并发、SQL 能力、运维、Python 接入五个维度拆开讲。
一、架构定位:这是所有差异的源头
| 数据存放 | 一个文件(或内存) | 服务端管理的目录 | 服务端管理的目录 |
| 部署 | 零部署,import即用 | 需安装并启动服务端 | 需安装并启动服务端 |
| 账号与权限 | 文件系统权限,无内置用户体系 | 有完整的用户/授权体系 | 有完整的用户/授权体系 |
| 多用户 | 同一台机器、同一文件系统 | 网络内多机多用户 | 网络内多机多用户 |
| 典型场景 | 桌面应用、移动端、测试、单机工具 | Web 应用、中小型业务系统 | 复杂业务、分析型负载、地理信息等 |
这张表的第一行解释了其余所有行。嵌入式意味着没有网络往返、没有连接管理、没有服务端进程要运维;代价是没有网络访问、没有内置的账号体系、并发写入受限于文件锁。客户端-服务器意味着反过来的一切。
二、并发模型:最容易被低估的差异
SQLite 的写入是全局串行的。同一时刻只允许一个写事务,写操作期间数据库文件被锁住。开启 WAL(预写日志)模式后,读操作可以和写操作并发,这是 WAL 的主要价值,但它不会让多个写者并行。所以:
- 适合:单进程应用、读多写少、每台设备一个本地库。
- 不适合:多个应用服务器同时高频写同一个库文件(比如把 SQLite 文件放在网络共享盘上给多台机器写——这是典型的踩坑方式)。
MySQL 和 PostgreSQL 走的是另一条路:多版本并发控制(MVCC),读写互不阻塞,写与写之间靠行级锁(InnoDB)或更细粒度的锁竞争。既然服务端在处理并发,你的应用就可以横向扩展。
但要注意:数据库层的并发只是第一层。Python 侧的 GIL(全局解释器锁)意味着同一个进程内同一时刻只有一个线程在执行字节码,所以多线程对 I/O 密集的数据库操作有效(等待网络时释放 GIL),对 CPU 密集的处理无效。想用多核跑 CPU 密集任务,得用多进程。这两件事经常被混在一起谈,其实是两个独立层次的问题。
Python 标准库的sqlite3模块还多一层约束:连接对象默认不允许跨线程使用(check_same_thread=True)。真要跨线程,就每个线程各建一个连接。
三、SQL 能力与数据类型
| 类型系统 | 动态类型(类型亲和性),列声明是建议不是强制 | 静态类型,有严格的列类型 | 静态类型,类型最丰富 |
| 原生 JSON | json函数族 | JSON/JSONB类型(8.0 起) | json/jsonb类型,索引与运算符完善 |
| 修改数据后直接返回行 | 支持RETURNING(SQLite 3.35 起) | 不支持RETURNING | 支持RETURNING |
| 全文检索 | FTS5 扩展 | 内置全文索引 | tsvector/tsquery,长期支持 |
| 改表结构 | 能力有限:改名、加列、删列(3.35 起)等,复杂改动要重建表 | 支持较完整的ALTER TABLE | 支持较完整的ALTER TABLE |
几点展开:
- SQLite 的动态类型是个双刃剑。它的列上写的是「类型亲和性」(type affinity),不是强制约束——
INTEGER列里理论上可以塞字符串。这让建表很省事,也意味着数据质量要靠应用层或CHECK约束去保证。
- PostgreSQL 的
RETURNING很实用:INSERT ... RETURNING id能一次拿到新生成的主键,省掉一次回查。MySQL 没有这个能力,只能靠自增主键的LAST_INSERT_ID()(Python 驱动里通常表现为cursor.lastrowid),而且批量插入后这个值不可靠。
jsonb是 PostgreSQL 的强项:可以建索引、可以用运算符查询内部字段。MySQL 8.0 也有JSON类型,功能在持续补齐。
- SQLite 的外键默认关闭,且是每连接开关。这一条在「从 SQLite 迁到 PostgreSQL」时最容易出问题——在 SQLite 上跑得好好的删除逻辑,到了强制外键的 PostgreSQL 上直接报错。
四、Python 侧怎么接
| SQLite | sqlite3(标准库,import即用) | 无需安装;随 Python 分发 |
| MySQL | PyMySQL(纯 Python)、mysql-connector-python(官方)、mysqlclient(C 扩展) | 都要pip install,参数风格多为%s |
| PostgreSQL | psycopg(3.x 系列)、psycopg2 | 都要pip install,参数风格为%s |
三者的驱动都实现DB-API 2.0(PEP 249),所以接口形态基本一致:connect()→cursor()→execute(sql, params)→fetch*()→commit()/rollback()→close()。差异在细节:
| 占位符风格 | SQLite 用?或:name(不支持数字占位符);MySQL / PostgreSQL 驱动用%s |
| 数据库参数名 | MySQL 驱动常用database=;PostgreSQL 风格驱动常用dbname= |
| 自动提交 | SQLite 与各 MySQL 驱动默认不自动提交;PostgreSQL 驱动默认不自动提交 |
| 事务控制 | SQLite 有with con:但不关连接;MySQL 驱动也有类似语义 |
因此「换库」最麻烦的从来不是换驱动,而是 SQL 方言和事务语义。参数占位符从?换成%s是一行改动,但INSERT OR REPLACE、AUTOINCREMENT、LIMIT ? OFFSET ?这些写法各有各的方言,迁移时要逐个核对。
五、同一件事,三种写法
下面这段在三种库上都能跑(SQLite 部分可直接复制执行,因为它不需要任何外部依赖)。观察点是:流程完全一样,只有连接参数和占位符风格在变。
# 适用于 Python 3.8+(这一段用标准库 sqlite3,无需安装任何东西)
import sqlite3
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
try:
with conn.cursor() as cur:
cur.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)")
cur.execute("INSERT INTO t(name) VALUES (?)", ("示例",))
cur.execute("SELECT id, name FROM t WHERE name = ?", ("示例",))
row = cur.fetchone()
print(row["id"], row["name"])
conn.commit()
finally:
conn.close()
预期输出
1 示例
把这段代码换到另外两个库上,需要改的只有这几处:
| 需要改的地方 | SQLite | MySQL(PyMySQL) | PostgreSQL(psycopg 2) |
|---|
| 导入 | import sqlite3 | import pymysql | import psycopg2 |
| 建连接 | connect(":memory:") | connect(host=, user=, password=, database=, charset="utf8mb4") | connect(host=, user=, password=, dbname=) |
| 行工厂 | conn.row_factory = sqlite3.Row | cursorclass=pymysql.cursors.DictCursor | cursor_factory=psycopg2.extras.RealDictCursor |
| 自增主键 | INTEGER PRIMARY KEY | INT AUTO_INCREMENT | SERIAL或GENERATED AS IDENTITY |
| 上下文管理器是否关连接 | 否 | 否 | 否(psycopg 3 会关) |
这张表就是「DB-API 统一了什么、没统一什么」的实证:统一的是流程和异常层次,没统一的是连接参数名、占位符、行工厂钩子和 DDL 方言。
六、按场景选型
| 桌面/单机小工具、配置文件式存储 | SQLite | 零部署,一个文件带走 |
| 移动端 App 本地存储 | SQLite | 无服务端,省电省资源 |
| 单元测试、CI 里的临时库 | SQLite(:memory:) | 快、干净、无需外部依赖 |
| 中小型 Web 应用,读多写少 | MySQL 或 PostgreSQL | 有服务端,支持多用户与并发 |
| 复杂查询、JSON/数组/地理数据 | PostgreSQL | 类型和索引能力更强 |
| 团队已有 MySQL 生态与运维经验 | MySQL | 迁移成本往往比理论优势更重要 |
| 多台服务器同时高频写同一个库 | 必须用客户端-服务器方案 | SQLite 的写是全局串行的 |
一条经验:「先用 SQLite,等它真的成为瓶颈再换」通常比「一开始就上服务端」更划算,前提是你把数据访问层封好,切换时不至于满地改 SQL。反之,如果一开始就知道会出现多机并发写、需要账号权限体系、需要网络访问,那就直接上 MySQL 或 PostgreSQL。
常见坑点
1. 把 SQLite 当「小号 MySQL」用
❌ 把 SQLite 库文件放在网络共享盘上,让多台应用服务器同时写。
✅ SQLite 的写是全局串行的,且依赖本地文件锁;需要多机并发写就换成客户端-服务器方案。
2. 用「MySQL 默认是 MyISAM」的知识写新代码
❌ 建表时不指定引擎,又基于「MyISAM 不支持事务」的印象去写代码——实际默认是 InnoDB,事务是生效的。
✅ 现代 MySQL(5.5 起)默认 InnoDB,支持事务、行级锁和崩溃恢复。要显式指定就写ENGINE=InnoDB。
3. 迁移到 PostgreSQL 后外键开始报错
❌ 在 SQLite 上从来没开过PRAGMA foreign_keys=ON,删数据很随意;迁到 PostgreSQL 后被强制外键约束拦下。
✅ 迁移前先做一次数据一致性核查,把 SQLite 侧的孤儿数据清掉;在 SQLite 上也每个连接都开启外键,让行为提前对齐。
4. 指望用RETURNING拿 MySQL 的新主键
❌cur.execute("INSERT ... RETURNING id")在 MySQL 上直接是语法错误。
✅ MySQL 用自增主键 +cur.lastrowid;批量插入后用lastrowid不可靠,需要新主键就逐条插入或用业务唯一键回查。
5. 以为换了驱动就完成了「换库」
❌ 只把import pymysql改成import psycopg,SQL 里?占位符、INSERT OR REPLACE、AUTOINCREMENT通通没改。
✅ 迁移清单要包含:占位符风格、自增列写法、冲突处理语法、分页语法、日期函数、字符串函数。换驱动只是第一行。
6. 把「多线程」当成「解决数据库并发」的万能药
❌ 用threading开 20 个线程去跑 CPU 密集的数据处理,期望加速——受 GIL 限制,同一进程内同一时刻只有一个线程执行字节码。
✅ CPU 密集用multiprocessing;I/O 密集(等数据库响应)多线程才有效。数据库服务端的并发是另一层问题,两件事分开看。
7. 从 SQLite 复制一个库文件当备份
❌ 直接copy一个正在被写入的.db(尤其是开了 WAL 之后),可能拿到不一致的快照。
✅ 用sqlite3的Connection.backup()做在线备份,或先断开所有连接再整体复制。
8. 相信没有测试条件的性能对比结论
❌ 照着某篇「某某数据库快 3 倍」的文章做选型。
✅ 用你自己真实的数据量、查询模式和硬件做基准测试;只拿可复现的测试结果做决策。
总结
| 并发 | 单写者,WAL 下读写可并发 | MVCC + 行级锁 | MVCC,锁粒度更细 |
| 类型系统 | 动态(类型亲和性) | 静态 | 静态,类型丰富(数组、JSONB、范围) |
| 最适合 | 单机、桌面、测试 | Web 应用、成熟生态 | 复杂查询、分析、扩展需求 |
选型的心态应该是:先按架构差别排除不可能的选项,再在剩下的选项里比细节。SQLite 和另外两个不是一个量级上的同类产品,把它们并列比较本身就是一种误导;而 MySQL 与 PostgreSQL 的取舍,多半取决于团队手里的运维经验和既有生态,而不是纸面功能表上的几行差异。