关于 db-defaults
db-defaults 面向个人、本地优先的 Python 项目,帮助按用途选择存储方案,避免每次从头比较。它提供各类默认选项、升级路径和代码片段,并强调可在本地 pytest 中测试。
主要功能
- 01按数据用途提供存储默认选项
- 02说明规模变化时的升级路径
- 03为各类别提供 Python 代码片段
- 04将本地 pytest 可测试性纳入标准
- 05建议不要模拟可实际运行的存储
适用场景
- 01为本地 Python 项目选择缓存
- 02为项目数据选择关系型存储
- 03选择本地全文或向量搜索方案
- 04判断存储需求何时超出本地默认方案
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ theodoread/ agent-skills --skill db-defaults安装后在 Claude Code 中输入/db-defaults确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/rationale.mdreferences/snippets/analytical-olap.pyblob-storage.pycache.pycron-scheduler.pydocument-store.pyfull-text-search.pygraph-data.pyin-process-state.pyjob-queue.pypubsub.pyrelational-oltp.pyrelational-simple.pytime-series.pyvector-search.py详细介绍
各用例的默认存储技术
面向个人、以 Python 为先、本地优先的项目。在没有明确要求分析替代方案的情况下,需要开始处理存储/缓存需求时适用——从此表中选择,不要每次会话都从头重新评估。若某个类别确实不适用,或需求超出默认方案的规模,则可以偏离默认方案(参见各类别的“升级到”说明)——这样做的目的是避免新的会话/模型无缘无故地悄悄选用其他方案,而不是禁止酌情判断。
以下每项的选择标准:仅采用 MIT/Apache-2.0/BSD 风格的宽松许可证;广受欢迎且积极维护(通过 GitHub/PyPI 上的实际活动验证,而不是凭感觉);可在普通的 pytest 运行中完全测试——使用内存或限定在 tmp_path 范围内,不需要 Docker/云账户/CI 服务;API 样板代码少,编码 Agent 无需太多仪式或间接操作就能正确使用。选择侧重于为每项需求选用最合适的工具,而非尽量减少项目中的技术种类——不要强行整合下方各类别并未要求整合的方案。
切实利用可测试性标准:不要模拟这些方案。 选择一个测试套件可以启动和停止的存储方案,意义就在于让测试运行真实实例;在这里使用替身会浪费这项选择带来的好处。python-conventions 中的“不要替代测试套件能够实际运行的任何内容”说明了这条规则及其边界。
安全性有意不作为选择因素。 此处的所有默认方案都是为本地、个人规模的使用而选择——本页的任何内容都不应被视为生产环境/多租户/面向互联网的建议。遇到这些情况时,应采用各类别“升级到”所列的方案。
进程内临时状态
- 示例:
references/snippets/in-process-state.py - 默认方案:纯标准库(
threading.Lock、time.monotonic、字典)——无需库 - 原因:无需持久化;对于“保存一个数字,并用一个锁保护它”这种需求,使用库只会增加纯粹的开销
- 升级到:不适用——一旦需要在重启后保留数据,就应改用缓存或关系型类别
缓存(TTL/驱逐)
- 示例:
references/snippets/cache.py - 默认方案:
diskcache(Apache-2.0,纯 Python) - 原因:基于磁盘,可处理 TTL/驱逐,无需自行实现;在 pytest 中使用
tmp_path夹具非常简单 - 升级到:Redis——一旦多个进程/机器需要共享同一个缓存
关系型——简单(表少,形似键值)
- 示例:
references/snippets/relational-simple.py - 默认方案:标准库
sqlite3、原生 SQL - 原因:零依赖;对于如此简单的数据结构,无需 ORM 的繁琐流程;可在 pytest 中使用
:memory:或tmp_path - 升级到:Postgres
关系型 — 复杂 / OLTP(许多表、真正的联接、迁移、频繁写入)
- 示例:
references/snippets/relational-oltp.py - 默认方案:
sqlalchemy+alembic(均为 MIT 许可证) - 原因:确实需要 Engine/Session/声明式模型这一套流程,但它带来了迁移和关系安全保障;对于真正需要事务的多表结构,其他更轻量的方案都无法提供这些保障。
- 通过声明式基类的
type_annotation_map为普通类型定义列类型词汇表,并用Annotated别名指定需要逐列参数的类型。 这样,每个模型中的每个Mapped[Decimal]都会按相同方式解析,不会有人因为凭记忆在另一个文件中编写而绕过这一规则。这也诚实地回答了“为什么用 ORM 而不是 Core”——Core 也能生成相同的 SQL,只不过需要在每一列上重复写出类型词汇表。 - 升级到:Postgres
- SQLite 层会悄无声息地破坏
Decimal精度并丢弃tzinfo,而升级到的 Postgres 不会出现这两种问题——这一点在此尤其重要,因为“可以完全在普通的pytest运行中测试”正是每个采用此方案的项目都使用 SQLite 的原因。以 SQLAlchemy 2.0.52 为准:Numeric会通过浮点数往返转换Decimal,且不会发出警告:1234567890123456789.000000001会变成1234567890123456768.0000000000。问题出在库本身——engine/default.py将supports_native_decimal设为False,而 SQLite 方言并未覆盖它,因此sqltypes.py会执行注释为“DBAPI returns floats, convert”的分支,并量化到 10 位小数。解决方法是使用存储精确字符串的TypeDecorator。- 带时区的 datetime 取出后会变成不带时区的值。这不是 SQLite 的限制:
dialects/sqlite/base.py中的DATETIME.bind_processor根本不会读取tzinfo。 - 陷阱在于它们很容易被掩盖。首次测试
Decimal时,使用 10 位数字的值会_通过_,因为该值可以在浮点数中保持精确——于是,使用真实金额作为测试数据的项目会得出Numeric没问题的结论,然后上线。请使用足够长、能触发错误的值进行测试。
- 在
__table_args__中传入sqlite_strict=True,让测试方言不再比它所模拟的 Postgres 更宽松,避免测试专用方言带来的失效模式。这不是可以随意添加的额外关键字参数,示例展示了完整做法:STRICT 只接受INT/INTEGER/REAL/TEXT/BLOB/ANY,因此create_all遇到NUMERIC(38, 10)、DATETIME,以及普通String(64)所生成的VARCHAR(64)时都会直接失败(这三项均在 2.0.52 上实测)。String(64).with_variant(Text(), "sqlite")可在 Postgres 上保留真实的长度限制,而上述Decimal/datetimeTypeDecorator则可使另外两列满足要求。因此,采用 STRICT 并排除这两个陷阱是同一项工作;这也正是应该一次性都做,而不是都不做的理由。 - Alembic 的 SQLite 批量模式会埋下数据完整性隐患。 在 SQLite 上,
batch_alter_table通过重建表来实现变更,这会删除该表的触发器;而 Alembic 对触发器完全没有感知,搜索整个包中的“trigger”只会找到一条无关注释。此外,根据batch.py自身的TODO,它也会跳过既被反射又未命名的CheckConstraint。有两种缓解方式,任选其一都可以:为每个约束命名,或者决定 SQLite 层永不执行迁移,而始终根据当前元数据创建。
分析型 / OLAP(针对结构化数据的读取 / 聚合密集型查询)
- 代码片段:
references/snippets/analytical-olap.py - 默认选择:
duckdb(MIT — 知识产权由一家非营利基金会持有,专门用于确保其「永久」采用 MIT 许可) - 原因:
duckdb.connect(":memory:")可通过普通连接直接使用原生 SQL,无需 ORM 繁琐配置——对于这种使用场景,样板代码远少于 SQLAlchemy,而且连接和聚合的速度确实很快。仅支持单写入者(适合个人规模的本地使用);多写入者「Quack」协议于 2026 年 5 月发布,但仍是新技术 - 升级选择:Postgres+DuckDB 扩展,或 MotherDuck
文档存储(无模式 / 半结构化 JSON)
- 代码片段:
references/snippets/document-store.py - 默认选择:
tinydb(MIT,纯 Python) - 原因:提供真正类似 dict 的 API(
db.insert({...})、db.search(Query().field == x))——完全不用 SQL;使用MemoryStorage()或tmp_path进行 pytest 测试。维护者称其处于「维护模式」(功能完备且稳定)——仍会发布错误修复版本,并未被弃用 - 替代方案:
sqlitedict(Apache-2.0),适用于纯粹的键→对象存储且无需字段查询的情况——使用前请注意其数年来没有提交记录 - 升级选择:MongoDB
全文搜索
- 代码片段:
references/snippets/full-text-search.py - 默认选择:SQLite
FTS5虚拟表——随标准库sqlite3一同提供,无需新增依赖,内置bm25()排序 - 替代方案:
bm25s(MIT),适用于与数据存储解耦的独立 RAG 风格评分器;tantivy(MIT,为 Rusttantivy搜索引擎提供 Python 绑定——使用pip install tantivy,而非tantivy-py,后者是另一个过时的软件包),适用于需要比 FTS5 更丰富的功能集时进行更重型的类 Lucene 搜索 - 避免使用:
Whoosh——长期无人维护 - 升级选择:Elasticsearch 或 Meilisearch(两者都需要运行服务,因此不作为本地默认选择)
向量 / 嵌入相似性搜索
- 代码片段:
references/snippets/vector-search.py - 默认选择:
qdrant-client(Apache-2.0) - 原因:
QdrantClient(":memory:")或QdrantClient(path=tmp_path)完全无需服务;之后升级时,只需更换构造函数参数(url=.../云凭据)——类相同、方法相同,且已根据 Qdrant 官方文档验证。通过一个 API 处理向量、元数据和持久化 - 替代方案:
chromadb(Apache-2.0)——社区规模 / 影响力大得多,初次上手的 API 可以说更简单(add()/query());如果上手方便比经过验证的无须重写升级路径更重要,这是合理的选择 - 升级选择:Qdrant Cloud(客户端代码无需更改)或 Pinecone/Weaviate
后台任务 / 消息队列
- 代码片段:
references/snippets/job-queue.py - 默认选择:
huey(MIT) - 原因:
SqliteHuey(..., immediate=True)会在进程内同步运行@huey.task()函数——无需消费者子进程,也无需编排,普通的 pytest 测试只需直接调用函数即可。基于装饰器的 API 简洁,拥有 15 年的使用记录,待处理的未解决问题几乎为零 - 升级选择:Celery/RQ/Dramatiq——都需要实际的消息代理(Redis/RabbitMQ);当你需要多个工作进程 / 多台机器时,这正是选择它们的原因
Cron/定时重复任务
- 代码片段:
references/snippets/cron-scheduler.py - 默认选择:
apscheduler3.x(MIT) - 原因:指向
sqlite:///URL 的SQLAlchemyJobStore可以在重启后保留计划任务,不需要外部代理;即使完全不需要调度,也可以单纯用作队列。需要了解真实的单点维护风险,但这并不是完全避开它的理由:一位占主导地位的维护者(1,134 次提交,而贡献量第二高的贡献者只有 6 次),约 2 年前提交且未经审查的 PR,以及从 2020 年起就一直处于 alpha 阶段、至今没有稳定版的 v4——3.x 仍是唯一实用的选择,并且仍在积极修补(最新发布版本截至 2026 年 6 月) - 替代方案:
schedule(MIT)——零依赖,进程内 API 简单易用,但不支持持久化(重启会丢失所有计划任务),而且自 2024 年 5 月以来就没有更新;适用于纯进程内场景的轻量备用方案,不是受治理因素驱动的替代品 - 升级选择:当重复任务需要在一台以上的机器上持续运行时,改用托管调度器(例如云端 cron)
发布/订阅/事件流
- 代码片段:
references/snippets/pubsub.py - 默认选择:
blinker(MIT)——进程内信号/观察者分发(signal.connect(receiver)、signal.send(sender)) - 原因:Flask 自身也用这个库处理信号;无需繁琐配置,可在进程内向多个订阅者分发,无需服务端,并且极易测试
- 升级选择:NATS(Apache-2.0)——它是真正不同的工具,而非这个工具的更大版本:需要单独配置
nats-server进程,而且没有官方维护的 Python pytest fixture 包可用于本地测试(两个社区尝试都已停止维护),因此即使它在实际运行服务端后是个出色的产品,也不符合这里的本地默认选择条件
图数据
- 代码片段:
references/snippets/graph-data.py - 默认选择:
ladybug(MIT,pip install ladybug)——嵌入式,使用 Cypher 查询语言,无需服务端 - 原因:这是 Kuzu 的活跃维护延续版本,由社区在 Kuzu 于 2025 年 10 月归档(Apple 收购)后 3 天分叉而来——底层引擎相同,维护团队换新,并非重写。每天都有提交,有 MIT 许可证,并明确列出了活跃维护者。作为项目名称还不到一年,因此应视为前景不错但尚年轻,而非像 SQLite 那样经过实战检验
- 轻量替代方案:如果关系建模不需要真正的图遍历性能或查询语言,那么在关系型存储中使用普通的
edges(src, dst, relation)表仍然更简单——只有在需要以超出玩具规模的实际图算法/遍历时,才选择ladybug。两种方案都可以配合networkx(BSD-3-Clause),对已加载到内存中的数据运行图算法 - 升级选择:Neo4j Aura、Amazon Neptune
Blob/对象存储
- 代码片段:
references/snippets/blob-storage.py - 默认选择:直接使用
pathlib.Path写入文件——无需库 - 原因:零依赖,使用
tmp_path测试非常简单;尚无具体计划就为云存储添加抽象,正是 YAGNI 原则所针对的情况 - 升级选择:当云存储成为实际计划而非可能选项时,使用
fsspec(BSD-3-Clause)+s3fs/gcsfs——调用方式仍为fs.open()/fs.ls(),只需更改协议字符串
时间序列数据
- 代码片段:
references/snippets/time-series.py - 默认选择:
duckdb(MIT)——与上方分析/OLAP 类别使用同一工具 - 原因:DuckDB 的窗口函数、
time_bucket、缺口填补和 ASOF 联接在时间序列分析方面确实很出色;与用原始 SQL 手动编写相同查询相比,能显著减少样板代码——对于这一特定需求,它是更好的工具,无论项目是否也需要它来处理 OLAP 工作 - 轻量替代方案:使用带时间戳索引列的普通 SQLite 表,前提是需求确实只是“存储带时间戳的行并按范围筛选”,不涉及重采样或窗口计算
- 升级选择:InfluxDB、TimescaleDB
编辑此技能
安装时,此文件会被_复制_到 ~/.agents/skills/db-defaults,而不是创建符号链接,因此编辑已部署的副本会造成本地偏差——这正是此技能要防止的情况,而且修改不会影响其他机器。请在安装此技能的源代码仓库中编辑,然后推送,并重新运行 skills add <that source> --global --skill db-defaults,以刷新每个项目的副本。如果你安装的是其他人的仓库,而不是自己的分叉仓库,那么源代码属于对方:请在那里提交 issue 或拉取请求。
入门代码片段
references/snippets/ 中为上述每个类别提供了一个真实、通过 ruff 检查且独立完整的 Python 文件——每个文件的文档字符串中都包含经过验证的 pip install 命令,并提供一个可运行的 test_* 函数来展示 pytest 本地测试模式;这些代码可以直接复制粘贴,无需像教程那样修改。上方每个类别的“代码片段:”行都直接链接到对应文件,因此无需打开其他文件。这里还记录了编写时发现的两个真实命名陷阱,并在相关位置加以说明:full-text-search.py(tantivy 与 tantivy-py 的 PyPI 混淆)和 cron-scheduler.py(不存在的 SQLiteJobStore 类名)。
完整说明
请参阅 references/rationale.md——其中包含 GitHub/PyPI 证据、每个类别中经过考虑后予以排除的选项,以及上方每个分支/替代方案背后的理由。