每个账号只能有一条有效记录:用 SQLite 部分唯一索引保留历史
当系统允许一个账号保留多条历史订阅,但只允许一条有效订阅时,可以用 SQLite 部分唯一索引限制有效行。把校验交给数据库后,多个写入入口使用同一个约束,应用只需处理约束错误。
1. 验证三种状态转换
保存为 partial_unique_lab.py,用 Python 运行。例中 ended_at IS NULL 表示订阅有效。
import sqlite3
from contextlib import closing
with closing(sqlite3.connect(":memory:")) as db:
db.executescript("""
CREATE TABLE subscription (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL,
ended_at TEXT
);
CREATE UNIQUE INDEX one_active_per_account
ON subscription(account_id) WHERE ended_at IS NULL;
INSERT INTO subscription VALUES
(1, 42, '2026-01-01'), (2, 42, '2026-01-01'), (3, 42, NULL);
""")
try:
with db:
db.execute("INSERT INTO subscription VALUES (4, 42, NULL)")
except sqlite3.IntegrityError as exc:
assert exc.sqlite_errorname == "SQLITE_CONSTRAINT_UNIQUE"
print("second active subscription:", exc.sqlite_errorname)
else:
raise AssertionError("duplicate active subscription accepted")
with db:
updated = db.execute("""
UPDATE subscription SET ended_at = '2026-11-13'
WHERE id = 3 AND account_id = 42 AND ended_at IS NULL
""")
if updated.rowcount != 1:
raise RuntimeError("the expected active subscription changed")
db.execute("INSERT INTO subscription VALUES (4, 42, NULL)")
rows = db.execute("""
SELECT id, ended_at IS NULL FROM subscription
WHERE account_id = 42 ORDER BY id
""").fetchall()
assert rows == [(1, 0), (2, 0), (3, 0), (4, 1)]
print("rows (id, active):", rows)
second active subscription: SQLITE_CONSTRAINT_UNIQUE
rows (id, active): [(1, 0), (2, 0), (3, 0), (4, 1)]
2. 迁移之前查旧记录
建索引前用以下查询定位已违反新规则的账号。修复规则应来自业务,例如哪条订阅实际仍在生效,再由迁移验证索引能否建成。
SELECT account_id, count(*) AS active_count
FROM subscription
WHERE ended_at IS NULL
GROUP BY account_id
HAVING count(*) > 1;
替换有效订阅时,在同一事务中先结束旧订阅,再写入新订阅。上例用旧行 ID 和状态做条件更新,避免迟到的操作结束另一条有效记录。索引表达的是“至多一条”。如果某些账号必须始终存在一条有效记录,仍需在业务事务中保证创建、替换和删除符合这条约束。
小结
把部分唯一索引用在有明确状态条件的唯一约束上。迁移时检查历史数据,状态替换时用同一事务提交两次写入。
数据来源与检索时间
| 来源 | URL | 检索日期 | 对应内容 |
|---|---|---|---|
| SQLite Partial Indexes | https://www.sqlite.org/partialindex.html#unique_partial_indexes | 2026-11-13 | 部分唯一索引的作用范围 |
| SQLite Transaction | https://www.sqlite.org/lang_transaction.html | 2026-11-13 | 事务边界与写入事务行为 |
核验说明
- Windows、Python 3.11.9、SQLite 3.45.1 验证历史记录共存、重复有效行被拒以及事务内替换。样本使用内存库和单个连接。