该用peewee时:需快速建模、表少、不换数据库、团队不愿配sqlalchemy;它是带类型检查的sql拼接器,适合内部工具/cli/小后台,不适用高并发web或依赖数据库特性的场景。

什么时候该用 peewee,而不是 SQLAlchemy 或原生 sqlite3
当你需要快速建模、写少量表、不打算换数据库后端,且团队里没人想花三天配 SQLAlchemy 的 session 和 relationship 配置时,peewee 才真正省力。它不是“简化版 SQLAlchemy”,而是另一条路:把 ORM 当作带类型检查的 SQL 拼接器来用。
常见错误现象:peewee 项目后期硬加复杂联查、多级嵌套事务、或突然要切到 postgresql 分区表——这时你会卡在 Model.select().join() 返回字段模糊、fn.count() 不支持子查询别名、甚至 database 实例无法共享连接池上。
- 适用场景:内部工具、CLI 脚本、小后台(如日志归档、配置管理、爬虫结果存本地 SQLite)
- 不适用场景:高并发 Web API、需精细控制执行计划的报表系统、依赖数据库特有功能(如 mysql json 函数、PostgreSQL array_agg)
- 性能影响:默认每次
select()都走一次execute(),没内置批量插入优化;insert_many()虽有,但字段顺序必须严格匹配Model._meta.fields声明顺序
Model 定义里哪些字段声明会悄悄改变查询行为
peewee 的字段类型不只是校验用,它直接决定生成的 SQL 类型、索引策略,甚至 WHERE 条件的默认比较方式。比如 CharField(max_length=255) 在 SQLite 下其实没意义(SQLite 不校验长度),但在 PostgreSQL 下会生成 VARCHAR(255);而 TextField() 在 SQLite 是 TEXT,在 MySQL 却可能变成 MEDIUMTEXT,影响排序和索引效率。
-
primary_key=True字段若没显式设default或sequence,SQLite 下自动用Integer PRIMARY KEY(即 rowid 别名),但 PostgreSQL 必须配SerialField,否则插入报NULL value not allowed -
null=False的字段,.create()时不传值会直接抛IntegrityError,而不是静默填默认值——这点和 django ORM 不同,容易在迁移脚本里漏掉初始化 -
index=True只建 B-tree 索引,不支持唯一索引或函数索引;要唯一约束得单独写Meta.constraints = [SQL('UNIQUE (name, category)')]
为什么 select().where() 有时查不到数据,但 raw SQL 能查到
最常踩的坑是 python 层面的 None、空字符串、时区、大小写被悄悄转换了。比如 where(User.name == '') 在 SQLite 下等价于 WHERE name = '',但某些版本的 MySQL 驱动会把空字符串转成 NULL;又比如 dateTimeField 存的是 naive datetime,但 where(User.created > datetime.now()) 如果本地时区和数据库时区不一致,条件就永远不成立。
立即学习“Python免费学习笔记(深入)”;
- 字符串比较默认区分大小写(SQLite 默认不区分,但加了
COLLATE NOCASE就区分了),要用fn.LOWER(User.name) == 'admin'才稳妥 -
where(User.status.is_null())不能写成User.status == None,后者生成的 SQL 是= NULL(永远为 false) - 日期字段慎用
datetime.date.today(),它没有时间部分,而数据库里存的是完整datetime,建议统一用fn.DATE(User.created)做截断比较
如何避免 peewee 连接泄漏和事务卡死
peewee 默认不自动管理连接生命周期,所有 Database 实例都是长连接,select() 后不手动 .close(),或者 with db.atomic(): 里异常没被捕获,连接就会一直挂着。尤其在 flask/fastapi 这类框架里,容易出现“最多 100 个连接已用完”的错误。
- Web 场景下必须配连接池:
PooledSqliteDatabase或PooledPostgresqlDatabase,并设max_connections和stale_timeout - 事务块务必用
try/except包裹,或确保atomic()上下文退出时能正常释放;不要在atomic()外再调db.commit(),会触发OperationalError: no transaction is active - CLI 脚本可以接受每次执行都
db.connect()+db.close(),但要注意db.close()必须在finally块里,否则异常时连接就泄露了
最麻烦的是跨线程复用同一个 Database 实例——peewee 不是线程安全的,即使开了连接池,也要确保每个线程用自己实例,或加锁。这点很容易被忽略,直到压测时开始报 InterfaceError: connection already closed。