一张表干不了两件事
快照刚上线时,我想用 Tablestore 一张表把所有事都干了:存原始响应、按关键词查历史、顺便跑模块统计。
前两件很顺,第三件很别扭。Tablestore 擅长按主键点查和范围扫描,GROUP BY 不是它的主场。写一个「过去 30 天 AI 概览出现率」的统计,要么全表扫,要么在应用层把数据拉回来自己算。
后来把统计拆到 AnalyticDB,两张表各干各的活,事情就顺了。
分工:存储与统计
- Tablestore:按
keyword + day + request_id存原始响应,负责「取某天、某词的快照」 - AnalyticDB:存摊平后的字段,负责「按天、按市场、按模块做聚合」
这个拆分的关键不是技术选型,是访问模式。快照是写多读少、按时间取;统计是批量扫、按维度聚合。两种模式塞进一个引擎,总有一边难受。
统计表长什么样
只放聚合需要的字段,不放大 JSON:
CREATE TABLE serp_snapshots (
keyword VARCHAR(128),
day DATE,
request_id VARCHAR(64),
status INT,
credits DECIMAL(10, 2),
has_ai BOOLEAN,
has_paa BOOLEAN,
top_domain VARCHAR(128),
best_rank INT
) DISTRIBUTED BY HASH(keyword);
分布键选 keyword,因为大多数查询会按关键词过滤;如果查询以「按天看全站」为主,分布键可以换成 day。ADB 的分布键决定了数据落在哪个分片,选错会让查询变成跨分片扫描。
credits 用 DECIMAL 存,别用浮点;has_ai/has_paa 在写入前就从模块字段算好,统计时不用再解析 JSON。
常用查询
模块出现率(按天):
SELECT day,
AVG(CASE WHEN has_ai THEN 1 ELSE 0 END) AS ai_rate,
AVG(CASE WHEN has_paa THEN 1 ELSE 0 END) AS paa_rate
FROM serp_snapshots
WHERE day >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
GROUP BY day
ORDER BY day;
域名集中度(按周):
SELECT top_domain, COUNT(*) AS n
FROM serp_snapshots
WHERE day >= DATE_SUB(CURRENT_DATE, INTERVAL 7 DAY)
GROUP BY top_domain
ORDER BY n DESC
LIMIT 10;
排名变化(按关键词):
SELECT keyword, day, best_rank
FROM serp_snapshots
WHERE keyword IN ('serp api', 'google search api')
AND day >= DATE_SUB(CURRENT_DATE, INTERVAL 14 DAY)
ORDER BY keyword, day;
这几条查询覆盖了大多数报表需求,而且都是列式扫描,比在宽列库里硬算快得多。
数据怎么进去
快照写入 Tablestore 之后,用一个批处理任务把当天数据摊平、写进 ADB:
- 按天扫 Tablestore(主键范围扫描)
- 解析模块字段,算出
has_ai/has_paa/top_domain/best_rank - 批量写入 ADB,按
request_id做幂等
批处理可以用 DataWorks、函数计算或一台定时 ECS。关键是按天批次,不要一条一条写——ADB 喜欢批量。
如果重来
我会在第一天就把存储和统计分开。第一版把统计压在 Tablestore 上,花了两天写应用层聚合,最后发现 SQL 三行就能解决。
另一个当时没想清楚的问题:ADB 要不要存全量历史。现在的做法是只留最近 180 天,更早的聚合结果留在报表库里,原始快照归档到 OSS。统计要的是趋势,不是每一条原始记录。
SerpBase 的响应字段见 SerpBase 文档;AnalyticDB 的分布键与查询语法见 阿里云官方文档。两张表各干各的活,比一张表硬撑所有场景轻松得多。