本节摘要:数据库不是"更高级的文件",而是自带并发管控、数据约束、事务保证和索引加速的专业存储系统。本节先讲清 JSON 文件在多用户场景下必然翻车的四个原因,再认识关系型数据库的核心概念——表、行、列、主键、外键,用 SQLite 亲手建出轻记账的第一张表并写几条 SQL。概念落到手上,下一节的 ORM 才不会变成黑话。
阅读完本节,你应当能够:
复盘一个真实的翻车现场:两人同时用轻记账,甲记一笔"午饭 20",乙记一笔"电影 40"。程序各自的流程都是"读文件到内存 → 追加 → 整体写回"。时序稍不巧:乙在甲"读之后写之前"也完成了读——两人都在旧数据上追加,后写回的把先写回的整个覆盖,甲那笔账人间蒸发。这不是粗心,是文件存储的结构性缺陷:

关系型数据库把数据组织成一张张二维表。轻记账的账目表长这样:
| id | item | amount | date | user_id |
|---|---|---|---|---|
| 1 | 早饭 | 12.5 | 2026-09-01 | 1 |
| 2 | 地铁 | 4.0 | 2026-09-01 | 1 |
| 3 | 电影 | 40.0 | 2026-09-02 | 2 |
五个概念各就各位:列是字段,建表时声明类型;行是一条记录;主键是每行的身份证号(id),绝不重复;外键是表与表的纽带——user_id 指向用户表的 id,声明"这笔账属于谁",数据库负责检查指向的人真的存在;查询语言 SQL 是通用操作语言,增删改查各一个动词。
生产级数据库(PostgreSQL、MySQL)要安装服务、配置账号;SQLite 是"嵌进程序里的数据库",整个库就是一个文件,Python 标准库自带驱动,零配置——学习与中小项目的事实标准:
import sqlite3 # 连接(文件不存在会自动创建) conn = sqlite3.connect("ledger.db") cur = conn.cursor() # 建表:类型与约束在这里立规矩 cur.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, item TEXT NOT NULL, amount REAL NOT NULL CHECK (amount > 0), date TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ) """) # 插入:问号占位符传参,杜绝拼接注入 cur.execute( "INSERT INTO records (item, amount, date, user_id) VALUES (?, ?, ?, ?)", ("午饭", 20.0, "2026-09-02", 1), ) conn.commit() # 提交事务 # 查询:九月的账目,按日期倒序 rows = cur.execute( "SELECT item, amount FROM records WHERE date LIKE ? ORDER BY date DESC", ("2026-09%",), ).fetchall() print(rows) # [('午饭', 20.0)] conn.close()
三处工程要点:占位符问号由驱动安全传参,绝不用字符串拼接拼 SQL——那是注入攻击的头号入口,6.6 安全节展开;commit 与 execute 分离,你不提交就不落盘,事务的原子性正来自于此——同一事务里多条语句要么全部生效要么全部作废,"记账成功但统计没更新"的矛盾数据从根上杜绝;CHECK 约束让负数金额在数据库层面就进不了门,与应用层校验构成双保险。
主流分两大阵营:关系型(表、SQL、强约束)与非关系型(文档、键值、灵活结构)。选型一句话版:数据有清晰关系、要一致性保证,选关系型——账目、订单、用户,几乎一切业务系统的主干;结构多变、追求极致读写吞吐,考虑非关系型——缓存(第 6 章会用的 Redis 就是键值型)、日志、消息队列。轻记账毫无疑问用关系型,但你的工具箱里该知道还有另一格抽屉。
给 SQLite 库补一张 users 表(id、username 两列即可),插入两个用户,把上面的账目分别挂到两人名下,再用 JOIN 查询"每个用户各自的合计":
rows = cur.execute(""" SELECT u.username, SUM(r.amount) FROM users u JOIN records r ON r.user_id = u.id GROUP BY u.username """).fetchall() print(rows) # [('甲', 16.5), ('乙', 40.0)]
变式一:往表里插入一条负数金额,观察 CHECK 约束怎样当场拦截。变式二:连续插入两条不 commit 就 close,重开库验证数据没有进去——亲手确认"不提交不落盘"。
表建好了、SQL 会写了,但业务代码里到处写 SQL 字符串很快会烦。下一节 ORM,用对象说话。