采集的数据存哪?搜索数据存储选型与实践
采集搜索数据,数据量不大时「存哪都行」,但一进入「每天采集、要查趋势、要回溯、要审计」的状态,存储就成了架构问题。这篇记录我的存储选型过程:从 SQLite 起步,到分库/分层的完整方案,以及 schema 设计和增长管理。
搜索数据的存储特点
先认清搜索数据的数据特征:
- 写多读少:每次采集大量写入,查询相对少
- 时间序列:几乎都是带时间戳的快照
- 不可变:历史快照基本不修改,只有新增
- 体积可控:单条 JSON 几 KB,但长期累积会膨胀
这些特征决定了选型方向:能支撑「时间维度的查询」+「增量增长」。
阶段一:SQLite 起步(个人/小团队)
数据量不大(几万条以内),SQLite 是最佳起点——零运维、单文件、够用:
import sqlite3
con = sqlite3.connect("serp.db")
con.execute("""CREATE TABLE IF NOT EXISTS snapshots (
keyword TEXT,
day TEXT,
rank INTEGER,
title TEXT,
url TEXT,
snippet TEXT,
PRIMARY KEY (keyword, day, url)
)""")
con.execute("CREATE INDEX IF NOT EXISTS idx_day ON snapshots(day)")
- 单文件,备份就是复制文件
(keyword, day, url)主键天然去重- 按天加索引,时间查询够快
什么时候离开 SQLite:并发写入变多(多任务同时写会锁库)、数据量到百万级、要跨机器访问。
阶段二:PostgreSQL/MySQL(团队/生产)
升到 PostgreSQL 或 MySQL,核心是「表设计按时间序列来」:
-- 时间序列表:按天分区
CREATE TABLE serp_snapshots (
keyword text NOT NULL,
fetched_at timestamptz NOT NULL,
rank int,
title text,
url text,
snippet text,
PRIMARY KEY (keyword, fetched_at, url)
) PARTITION BY RANGE (fetched_at);
-- 按天分区
CREATE TABLE serp_20260821 PARTITION OF serp_snapshots
FOR VALUES FROM ('2026-08-21') TO ('2026-08-22');
- 按天分区:查询按天裁剪,删除旧数据就是 drop 分区,快且省
- 索引策略:
(keyword, fetched_at)复合索引覆盖「某词的趋势」查询 - 数据与元数据分离:业务数据一张表,
request_id/credits_charged等元数据一张表(对账、排查用)
阶段三:对象存储冷归档(历史数据)
历史数据(比如 90 天前的原始响应)不适合一直在热库里占空间,挪到对象存储:
# 伪代码:历史数据归档到对象存储
def archive_old(days=90):
cutoff = date.today() - timedelta(days=days)
rows = db.query("SELECT * FROM snapshots WHERE fetched_at < ?", cutoff)
with open(f"archive_{cutoff}.jsonl", "w") as f:
for r in rows:
f.write(json.dumps(r) + "\n")
upload_to_object_storage(f"archive_{cutoff}.jsonl")
db.delete("snapshots WHERE fetched_at < ?", cutoff)
- 热库只留近期:查询快
- 对象存储存全部历史:便宜、可恢复(配合备份篇的思路)
- 原始响应单独归档:带
request_id的原始 JSON 是最终证据
Schema 设计要点
不管用哪个库,三个 schema 经验:
- 主键包含时间:
(keyword, day, url)或(keyword, fetched_at, url),天然防重复 + 支持时间查询 - 数据与元数据分离:业务字段和
request_id/credits_charged分开,各表各用 - 原始响应单独存:清洗后的数据和原始 JSON 分开,重处理有原料
-- 元数据表:对账、排查、审计
CREATE TABLE billing_audit (
keyword text, endpoint text, request_id text,
credits numeric, elapsed_ms int, status int, ts timestamptz
);
增长管理:数据会膨胀
采集数据的增长是「只增不减」,要主动管理:
- 按天分区 + 定期归档:热库瘦身
- 增量版本化(增量篇):快照只存变化,省一半以上
- 保留策略:业务数据 90 天热存、历史归档永久(备份篇的保留策略)
- 监控表大小:超过阈值告警,别等磁盘报警
选型决策表
| 场景 | 选型 |
|---|---|
| 个人/小团队、几万条 | SQLite |
| 团队生产、百万级、时间查询多 | PostgreSQL 按天分区 |
| 大规模/多机器 | PostgreSQL + 对象存储冷归档 |
| 只需要「近期 + 简单查」 | SQLite/MySQL 足够 |
原则:别一上来就上重存储。SQLite 起步,数据量/并发到了瓶颈再升级,升级的抓手是「分区 + 归档」。
踩坑记录
坑 1:SQLite 并发写锁死。 多任务同时写,database is locked。方案:单写者模式(所有写入走一个队列)或提前上 PostgreSQL。
坑 2:主键漏了时间。 用 (keyword, url) 做主键,跨天的数据互相覆盖。加上 day 才正确。
坑 3:索引乱建。 一开始给所有字段建索引,写放大严重。按「查询模式」只建 (keyword, fetched_at) 复合索引。
工程清单沉淀
- SQLite 起步,数据量大再升级
- 主键含时间,天然防重复 + 时间查询
- 数据与元数据分离,原始响应单独归档
- 按天分区 + 冷归档,控制增长
- 保留策略明确,热库瘦身
存储是采集系统的「容器」——选对、设计对,趋势查询、对账、回溯都有据可查;选错了,数据越多越乱。
接口字段结构(哪些是业务数据、哪些是元数据)在 SerpBase 官方文档,设计 schema 时对照着分。你的采集数据存哪?评论区聊聊各自的存储方案。

浙公网安备 33010602011771号