实战案例1:基于Web日志的PV/UV实时聚合

从原始表到物化视图的完整链路,咱们今天就走一遍。

说实话,很多刚接触ClickHouse的朋友,一上来就问我:“老师,PV/UV统计到底怎么用物化视图?” 我通常的回答是——你先别急着写SQL,先搞清楚数据长什么样,再想怎么聚合。

好,咱们直接开干。

1. 原始数据长什么样?

假设我们有一个Web日志表,记录每次用户访问。字段大概是这样:

字段名 类型 说明
event_time DateTime 访问时间
user_id String 用户ID
page_url String 访问页面
ip String 客户端IP
status UInt16 HTTP状态码

嗯,这里要注意:user_id 如果是未登录用户,可能是空字符串或者匿名标识。我在项目中遇到过,有些日志系统直接把未登录用户记为 null,结果物化视图里 uniq 函数算出来的UV直接翻倍——因为 null 被当成独立值了。所以,建议统一处理成 '' 或者 'anonymous'

2. 创建原始表

咱们用 MergeTree 引擎,按时间分区。我个人习惯按天分区,查询效率高,而且物化视图的增量更新也方便。

CREATE TABLE web_logs (
    event_time DateTime,
    user_id String,
    page_url String,
    ip String,
    status UInt16
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (event_time, user_id);

你想想看,为什么排序键要加 user_id?因为后面物化视图里要按用户去重,排序键里带上它,聚合时能减少数据扫描量。

3. 创建物化视图:按分钟聚合PV/UV

核心逻辑来了。我们要实时统计每分钟的PV(页面访问次数)和UV(独立用户数)。

ClickHouse 的物化视图,说白了就是一个“触发器”——数据写入原始表时,自动触发聚合计算,结果存到另一张表里。

CREATE MATERIALIZED VIEW mv_web_pv_uv_minute
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMMDD(minute)
ORDER BY (minute, page_url)
AS SELECT
    toStartOfMinute(event_time) AS minute,
    page_url,
    count() AS pv,
    uniq(user_id) AS uv
FROM web_logs
GROUP BY minute, page_url;

这里我用了 SummingMergeTree,它会在后台自动合并相同排序键的行,把 pvuv 累加起来。不过要注意:uniq 函数在物化视图里是近似去重,误差一般在1%以内。如果你的业务要求精确去重,可以用 uniqExact,但性能会下降不少。

小技巧: 如果你对UV精度要求极高,比如广告计费场景,建议用 uniqExact 配合 AggregatingMergeTree。但大部分Web分析场景,uniq 足够了。

4. 查询物化视图

数据写入原始表后,物化视图会自动更新。查询时直接查视图表,速度飞快。

SELECT 
    minute,
    page_url,
    pv,
    uv
FROM mv_web_pv_uv_minute
WHERE minute >= now() - INTERVAL 1 HOUR
ORDER BY minute DESC;

我曾经帮一个客户优化过类似的查询。他们原来直接查原始表,一小时的数据要跑3秒多。换成物化视图后,同样的查询只要20毫秒。你想想看,差了150倍!

5. 完整链路图

下面这张图,展示了从原始表到物化视图的完整数据流。我特意用SVG画的,方便你理解。

原始表 web_logs 物化视图 mv_web_pv_uv_minute 目标表 .inner.mv_web_pv_uv_minute 数据写入 查询 数据流方向:写入原始表 → 自动触发物化视图 → 聚合结果存入目标表 查询时直接读取目标表,无需扫描原始数据
核心要点: 物化视图不是“视图”,它是一张实实在在的表。数据写入原始表时,ClickHouse 自动执行 AS SELECT 中的查询,把结果写入目标表。查询时直接查目标表,速度极快。

6. 避坑指南

  • 数据延迟: 物化视图是异步的,数据写入后不会立即出现在视图里。我遇到过有人刚写入就查视图,结果查不到数据。其实等几秒就好了,或者用 OPTIMIZE TABLE 手动触发合并。
  • 重复数据: 如果原始表有重复写入,物化视图里的 count() 会累加。建议在原始表写入时做好去重,或者在物化视图里用 uniq 处理。
  • 分区对齐: 物化视图的分区键最好和原始表一致。我曾经图省事,用了不同的分区策略,结果数据合并时出了不少问题。
警告: 不要对物化视图的目标表直接执行 INSERTALTER 操作!它的数据完全由物化视图自动管理。手动修改会导致数据不一致,甚至视图崩溃。

7. 性能对比

咱们用数据说话。假设原始表有1亿行数据,按小时查询PV/UV:

查询方式 扫描数据量 响应时间
直接查原始表 1亿行 3~5秒
查物化视图 几千行 10~50毫秒

说白了,物化视图就是用空间换时间。多占一点存储,换来几百倍的查询加速。对于实时看板、监控报表这类场景,这笔买卖太划算了。

好,这个案例就到这里。你可以在自己的ClickHouse环境里跑一遍,感受一下从原始表到物化视图的完整链路。有什么问题,咱们下一章接着聊。


专注资料整理