全文共 5,637 字 预计阅读 17 分钟
bg

bg14.数据服务

第三产业这一块

数据都在 Iceberg 表里了,为什么不让所有人直接查它?

因为湖表满足不了消费方的要求,三条独立的原因:

原因一:延迟。 一次湖上查询的时间构成是:

查询规划(读 manifest,筛选文件)  +  从对象存储拉取文件  +  解压解码  +  计算
      100 ms ~ 数秒                    数百 ms ~ 数秒        …          …

前两项在对象存储上都是百毫秒起步(13 篇 §6.2 讲过对象存储的单次请求延迟)。一个报表查询跑 3 秒可以接受,但一个用户打开 App 时要调的接口跑 3 秒,产品就没法用了。

原因二:并发。 湖上查询走的是"起一批计算资源、扫一批文件"的路子,单次查询就要占用可观的算力。BI 报表几十个并发没问题,但一个面向 C 端的接口可能要扛几万 QPS——按湖上查询的资源消耗模型,这个量级的并发在成本上不成立。

原因三:稳定性隔离。 湖是所有人共用的。一个分析师写了个全表扫描的 SELECT *,占满了集群资源,此时如果线上接口也在查同一个湖,接口会被拖垮。线上服务的稳定性不能依赖"没人写烂查询"。

所以交付层要解决什么

把"能算出来"变成"在要求的延迟、并发、稳定性下能算出来"。

这句话里的三个词——延迟、并发、稳定性——就是后面所有选型的判据。

数据湖(data lake,这里特指基于开放表格式的湖):数据以开放格式(Parquet)存在对象存储上,由表格式(Iceberg)提供表语义和事务。计算引擎是外部的、可替换的。

数据仓库 / OLAP 引擎(这里合称"仓"):数据被导入引擎自有的存储格式,引擎同时掌管存储和计算,因而可以做深度的存储侧优化(索引、排序、预聚合、编码)。ByteHouse、StarRocks、ClickHouse 属于这一类。

湖上直查(query the lake directly / external table):用一个查询引擎(Presto/Trino、StarRocks 的外部 Catalog 等)直接查湖表,不搬运数据。

这三者定位接近、经常被混用,逐维度对一次:

维度 数据湖(Iceberg on 对象存储) 仓 / OLAP(ByteHouse 等) 湖上直查(Presto/Trino)
数据存在哪 对象存储,开放格式 引擎自有格式(云上多为存算分离形态) 对象存储,开放格式
谁能读 任何支持 Iceberg 的引擎 只有该引擎(或其兼容协议) 任何支持该引擎的客户端
典型查询延迟 秒~分钟 亚秒~秒 秒~分钟
支持的并发 低(几~几十) 低~中
写入成本 (直接写文件) 高(要导入、建索引、可能要重排序) ——
索引能力 弱(只有文件级 min/max 统计) (主键、前缀索引、倒排、跳数索引、预聚合) 同湖
数据新鲜度 取决于写入方(13 篇:分钟级) 取决于导入链路 同湖
存储成本 最低 较高 同湖
灵活性 最高(schema 演进容易,随便加字段) 较低(表结构和索引一旦定下,改动有成本) 同湖

核心问题:为什么仓比湖快?

这是本篇必须讲清因果链的地方。"仓更快"不是一句结论,它由五条独立的机制构成,每条都有对应的代价。

机制一:存储介质与数据局部性

仓通常把数据放在计算节点的本地 NVMe SSD 上,或者用本地盘做热数据缓存。对象存储的单次请求延迟比本地 SSD 高一到两个数量级。

  • 代价:本地盘容量有限、成本高;扩缩容时数据要搬迁,弹性变差。云上的仓多用"存算分离 + 本地缓存"来折中,但缓存未命中时就退回对象存储的延迟。

机制二:精细索引

湖表只有文件级的统计信息(每个 Parquet 文件里每列的 min/max)。这意味着谓词下推的最小粒度是"跳过整个文件"。仓可以做到行级或块级的索引——主键索引、前缀索引、跳数索引、倒排索引

这四个名词里只有跳数索引在基础篇出现过,其余三个先各给一句定义(都属一次性提及,本系列不深入其实现):

  • 主键索引:按主键排好序并建查找结构,给一个主键值能直接定位到行。语义上和 MySQL 的聚簇索引同类
  • 前缀索引:只对某几列(或某列的前 N 个字节)建的稀疏索引——不是每行一条记录,而是每隔若干行记一条,用来快速跳到大致位置再顺序扫。ClickHouse 系引擎的主要索引形态就是这个。
  • 倒排索引(inverted index):把"词 → 出现该词的行号列表"反向建成一张表,用于文本检索。你用过的 Elasticsearch,其检索能力的核心就是它——这里只需知道它让"某列包含某个词"这类过滤不必扫全表。
  • 跳数索引(skipping index):为每个数据块记一份摘要(min/max、bloom filter 等),查询时先看摘要决定这块能否整块跳过。

举例:查 WHERE user_id = 100238841

能跳过多少 实际要读
湖表(未按 user_id 排序) 几乎跳不掉——每个文件的 user_id min/max 范围都很宽,都得读 接近全量
湖表(按 user_id 排序后) 能跳掉绝大部分文件 少数几个文件
仓(有主键索引) 直接定位 几行
  • 代价:索引要占存储、要在写入时维护,写入变慢。这就是"写入成本"的来源。

机制三:数据组织可控(排序 / 分桶)

仓可以强制数据按某几列排序或分桶存放。排序让 min/max 统计变紧凑(过滤效果好),分桶让 join 时同 key 的数据落在同一节点(避免 shuffle)。

  • 代价:写入时要做排序或重分布,写入吞吐下降;而且只能按一组列优化,按其他列查询时优势消失。

机制四:预聚合

仓可以把 GROUP BY 的结果预先算好。查询时扫的是已经聚合过的小表,扫描量下降几个数量级。

  • 代价:见后文的三个代价。

机制五:查询引擎本身的优化程度

仓的优化器能拿到自己存储的精确统计信息,做出更好的执行计划;向量化执行、编译执行等技术在自有格式上更容易做到极致。

  • 代价:这些优化和它的存储格式深度耦合,换不了引擎。你的数据被锁在这个产品里了。

名词补充:CBO(Cost-Based Optimizer,基于代价的优化器)会估算每种执行计划的开销、挑最便宜的那个,05 篇讲过。与之相对的 RBO(Rule-Based Optimizer,基于规则的优化器)不看数据分布,只按固定规则改写 SQL(比如"过滤条件尽量下推到扫描算子")。现代引擎两者都用:先 RBO 做确定性改写,再 CBO 在剩下的选择里挑代价最低的。CBO 的效果完全依赖统计信息是否准确、是否新鲜——这恰恰是仓相对湖的优势所在:它掌管自己的存储,统计信息可以做得又细又新;湖表的统计信息由写入方(可能是好几个不同的引擎)各自生成,质量参差。

把五条合起来看,"仓更快"的总代价是一句话

用「数据要搬一遍 + 写入变慢 + 存储变贵 + 灵活性下降 + 绑定单一引擎」,换「查询延迟低一到两个数量级 + 高并发」。

这笔交易划不划算,完全取决于这份数据被查询的频率和方式。冷数据搬进仓是纯亏损——搬运成本和存储成本都付了,但没人查。

决策:给定一个需求怎么选

不要凭感觉。问这五个问题:

# 问题 如果答案是… 倾向
1 延迟要求:这个查询允许跑多久? > 10 秒 湖 / 湖上直查
< 1 秒
2 并发要求:同时有多少个这样的查询? < 20 湖上直查够用
> 100,或面向 C 端 仓(甚至要再往前推到 KV,见 §4.2)
3 查询模式:查询条件和聚合维度是固定的还是发散的? 固定(比如永远按用户 ID 查) 仓(可以针对性建索引/预聚合)
发散(分析师今天想看这个明天想看那个) 湖上直查(不值得为每种查询建索引)
4 数据新鲜度:能接受多久的延迟? T+1 都行
分钟级 湖(Flink 直接写)或仓(实时导入)
5 数据量与访问频率:这份数据多大、多久被查一次? 大且很少查(冷数据、明细日志) 只留湖。搬进仓是纯浪费
不大但天天查
                        ┌─────────────────────────┐
                        │  一个新的数据消费需求     │
                        └───────────┬─────────────┘
                                    ▼
                    ┌───────────────────────────────┐
                    │ 延迟要求 < 1 秒 或 并发 > 100? │
                    └───────┬───────────────┬───────┘
                          是│               │否
                            ▼               ▼
              ┌──────────────────┐   ┌──────────────────────┐
              │ 查询模式固定吗?   │   │ 查询发散 + 数据量大? │
              └───┬──────────┬───┘   └────┬─────────────┬───┘
                是│          │否          │是           │否
                  ▼          ▼            ▼             ▼
        ┌──────────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐
        │ 预聚合到 KV   │ │ 进仓,  │ │ 湖上直查 │ │ 湖上直查 │
        │ (§4.2)       │ │ 建索引 │ │ (不搬运) │ │ 或小仓表 │
        └──────────────┘ └────────┘ └──────────┘ └──────────┘

一个反直觉但重要的判断:默认答案应该是"留在湖里",只有明确通过了上面的检验才搬进仓。因为搬进仓的成本(存储 + 导入算力 + 链路维护 + 口径可能分裂)是持续支付的,而收益只在被高频查询时才兑现。

四种交付形态

同一份 DWS 数据,交付给四种消费方,技术要求完全不同。

flowchart LR
    DWS[("DWS 汇总层<br/>dws_user_cate_click_1d")]

    DWS --> R["① 报表 / BI<br/>并发几十 · 秒级<br/><b>查询发散</b>"]
    DWS --> A["② 数据 API<br/>并发万级 · 毫秒级<br/><b>查询固定</b>"]
    DWS --> C["③ 人群包<br/>亿级集合运算<br/><b>只要 ID 集合</b>"]
    DWS --> F["④ 特征<br/>离线批 + 在线点查<br/><b>两边必须一致</b>"]

    R --> R2["仓 / 湖上直查<br/>靠通用扫描聚合能力"]
    A --> A2["预推到线上 KV<br/><b>不可直连数仓</b>"]
    C --> C2["bitmap 位运算<br/>前提: ID 稠密整数"]
    F --> F2["特征平台<br/>→ 第 20 篇"]

    R2 --> RU(["分析师 / 管理层"])
    A2 --> AU(["业务系统 / C 端接口"])
    C2 --> CU(["营销 / 增长"])
    F2 --> FU(["推荐 / 搜索 / 广告模型"])

看这张图的方式:四条路的起点是同一份数据,但从第二列开始就完全分岔了。分岔的原因不是业务偏好,是"延迟 × 并发 × 查询模式"这三个参数的组合不同——下面逐条看。

报表 / BI(人在看)

特征:并发低(几十)、延迟宽松(几秒可接受)、查询发散(分析师会临时改维度、改筛选条件、下钻)。

技术选择:仓,或湖上直查。因为查询发散,很难为每种查询预建索引,所以引擎的通用扫描与聚合能力比索引更重要。

主要工程问题:慢查询治理。分析师写的 SQL 质量参差不齐,一个笛卡尔积就能拖垮集群。对策是查询超时、扫描量上限、资源组隔离。

数据 API(系统在调)

特征并发极高(万级 QPS)、延迟极严(P99 要求几十毫秒)、但查询模式高度固定(永远是"给一个 user_id,返回他的偏好标签"这一种)。

这里有一条必须讲清的工程铁律:

⚠️ 绝不能让线上业务系统直连数仓。

为什么?四条理由,每条都足以致命:

  1. 稳定性耦合。数仓是给分析用的,随时可能因为某个大查询、某次集群升级、某个 compaction 任务(13 篇 §7)而变慢。线上服务的 SLA 不能依赖一个为分析设计的系统。
  2. 并发模型不匹配。OLAP 引擎为"少量大查询"设计,每个查询会起多个执行线程、申请较大内存。几万 QPS 的小点查会让它的调度开销和内存管理成为瓶颈——不是它不够快,是它不是干这个的
  3. 故障爆炸半径。数仓挂了,本来只影响报表(可以等);如果线上接口也依赖它,就变成了 C 端故障。
  4. 成本模型不匹配。为了扛住 C 端流量峰值而扩容整个数仓,是用最贵的资源做最便宜的事。

正确做法:把结果预先推到线上存储。

数仓/湖(离线或实时计算)  ──推送──▶  线上 KV 存储(Redis/HBase/自研)  ──▶  数据 API  ──▶  业务系统
      ↑                                    ↑                              ↑
  查询发散、延迟宽松                   查询固定、毫秒级                 由服务端团队维护
  为分析优化                          为点查优化                       有独立 SLA

这个动作通常叫数据回流结果推送。它把"计算"和"服务"彻底解耦:计算在数仓里慢慢做,服务在 KV 上快速读。

图里的 HBase 是一个分布式的 KV / 宽列存储(列族模型,按 rowkey 排序存放,擅长按 key 点查和按 key 范围扫描),常用于"数据量大到 Redis 装不下、但访问模式仍然是按 key 点查"的场景。这里只作为线上 KV 存储的一个例子出现,本系列不展开它的原理。

代价:数据有一次额外的搬运和延迟;KV 里的数据是快照,不能做灵活查询;要额外维护一条推送链路和它的监控。

人群包 / 圈选(营销在用)

特征超大基数的集合运算。典型需求是"最近 30 天买过母婴用品、且未购买过奶粉、且在一线城市的用户"——三个亿级用户集合做交集和差集。

用普通 SQL 怎么做

-- 朴素做法:三次大表扫描 + 两次 JOIN
SELECT a.user_id
FROM  (SELECT DISTINCT user_id FROM dws_user_cate_buy_30d WHERE cate_id = 5001) a   -- 买过母婴
JOIN  (SELECT DISTINCT user_id FROM dim_user WHERE city_level = 1)              c ON a.user_id = c.user_id
LEFT JOIN (SELECT DISTINCT user_id FROM dws_user_cate_buy_30d WHERE cate_id = 5008) b ON a.user_id = b.user_id
WHERE b.user_id IS NULL;   -- LEFT JOIN 后取右表为空的行 = 差集,即"未买过奶粉"

模拟输入(下面 bitmap 方案用完全相同的输入,以便对比):母婴用户 3,200 万、一线城市用户 8,500 万、奶粉用户 900 万,全站用户 6 亿。

运行结果

user_id 结果集: 4,138,206 行
耗时: 3 分 12 秒

为什么这么慢:三个子查询各上千万到上亿行,两次 JOIN 都要 shuffle(05 篇讲过 shuffle 为什么贵)——上亿行数据要经过序列化、网络传输、反序列化。运营在圈人平台上每调一次条件就等 3 分钟,这个交互体验不可用。

bitmap 方案:把用户 ID 集合表示成位图——一个很长的二进制串,第 i 位为 1 表示用户 i 在集合中。

为什么快,因果链

用户集合 → 位图
   ↓
集合的交集 = 两个位图按位 AND
集合的并集 = 按位 OR
集合的差集 = AND NOT
   ↓
按位运算是 CPU 原生指令,一条指令处理 64 位(用 SIMD 还能更多)
   ↓
1 亿用户的交集 = 1 亿 / 64 ≈ 156 万次机器指令 → 毫秒级
   ↓
对比 JOIN:要 shuffle 上亿行数据过网络

还有一层压缩RoaringBitmap(一种被广泛使用的压缩位图实现)把整个 ID 空间切成若干块,每块根据其中元素的稀疏程度选择不同的存储方式(稀疏用有序数组,稠密用原始位图)。这样既省空间,又保持了位运算的高效。

-- StarRocks 方言。⚠️ bitmap 函数名各引擎不同(ClickHouse 系用 groupBitmapState / bitmapAnd
--    / bitmapCardinality 等),请以你所用引擎的版本文档为准

-- 步骤 1:预先把每个标签的用户集合物化成一个 bitmap,一行一个标签
CREATE TABLE dws_tag_user_bitmap (
    tag_id   INT,
    user_bm  BITMAP BITMAP_UNION      -- 列的聚合方式声明为"bitmap 并集":同 tag_id 的多行合并时做 OR
) AGGREGATE KEY(tag_id)               -- 聚合模型:按 tag_id 聚合,相同 key 的行自动合并成一行
DISTRIBUTED BY HASH(tag_id);          -- 按 tag_id 哈希分桶决定数据落在哪个节点

INSERT INTO dws_tag_user_bitmap
SELECT cate_id AS tag_id, BITMAP_UNION(TO_BITMAP(user_id))   -- TO_BITMAP:单个 id 变成只有一位的 bitmap
FROM dws_user_cate_buy_30d GROUP BY cate_id;                 -- BITMAP_UNION:组内所有 bitmap 求并集

-- 步骤 2:圈人。整个过程只做位运算,不扫明细
SELECT BITMAP_COUNT(                                  -- 数出结果 bitmap 里有多少个 1
         BITMAP_AND_NOT(                              -- 差集:从左边减去右边(AND NOT)
           BITMAP_AND(                                -- 交集:母婴 ∩ 一线城市(AND)
             (SELECT user_bm FROM dws_tag_user_bitmap WHERE tag_id = 5001),
             (SELECT user_bm FROM dws_tag_city_bitmap  WHERE city_level = 1)
           ),
           (SELECT user_bm FROM dws_tag_user_bitmap WHERE tag_id = 5008)   -- 减掉买过奶粉的
         )
       ) AS crowd_size;

模拟输入:与上面朴素方案完全相同——母婴 3,200 万、一线城市 8,500 万、奶粉 900 万,全站 6 亿。

运行结果

crowd_size
---------
  4,138,206
耗时: 47 毫秒

为什么是这个结果:人数与朴素方案完全一致(两种算法在语义上等价,只是执行方式不同),但耗时从 3 分 12 秒降到 47 毫秒。三个 bitmap 压缩后各约 4 MB~11 MB,两次位运算在单机内存里完成;朴素方案要 shuffle 上亿行过网络。数量级差异来自"位运算 vs 网络传输",不是来自"引擎更快"。

bitmap 的硬前提(必须知道,否则会踩坑)

  1. 用户 ID 必须是稠密的整数。bitmap 的第 i 位对应 ID 为 i 的用户。如果 ID 是 UUID 字符串,或者是雪花 ID(snowflake ID,一种由"时间戳 + 机器号 + 序列号"拼成的 64 位全局唯一 ID,Java 后端常用;它的值域极大且分布稀疏,比如 1752846231120384001),位图会极度稀疏——为了标记 1 亿个用户,可能要覆盖 10¹⁸ 量级的 ID 空间,空间和性能优势全部消失。解决办法是建一张全局字典表,把原始 ID 映射成从 0 开始的连续整数——而这张字典表本身就是一个需要长期维护的工程负担(新用户要分配、不能重复、要能反查、还要在离线和实时两条链上保持一致)。
  2. 只能回答"在不在集合里",不能带明细。想知道"这批人平均消费多少",还得拿 ID 回去 join 明细表。
  3. 标签的组合是预定义的。每个标签要预先物化成一个 bitmap,标签数量多了,构建和存储成本上升。

特征(模型在用)

特征是喂给机器学习模型的输入变量

它的技术要求是四种里最复杂的:既要离线批量产出(训练时要几个月的历史样本),又要在线实时读取(推理时要毫秒级拿到当前用户的最新特征),还要保证两边算出来的值一致

这里只点到为止。第 20 篇会专门处理特征平台、离线在线一致性(training-serving skew)、以及样本拼接这三个问题——它们是数据团队和算法团队之间最主要的接缝,值得单独一篇。

预聚合 / 物化视图(materialized view):把一个聚合查询的结果预先算好并物化存储;后续相同或可由它推导的查询,直接读这份结果,不再扫描明细。

术语说明:物化视图强调"把视图的结果存下来"(对比普通视图只是一段 SQL 的别名,每次查询都要重算);预聚合强调"按维度组合预先做 GROUP BY"。有些引擎里叫 rollup(上卷表)。三者在本篇语境下指同一类技术。

为什么快,因果链

明细表 dwd_item_click_di: 一天 120 亿行
    ↓ 预先按 (dt, cate_id) 聚合
物化视图 mv_cate_click_1d: 一天 3,000 行(类目数)
    ↓
查询"昨天各类目点击量"时,扫 3,000 行而不是 120 亿行
    ↓
扫描量下降 400 万倍 → 从几十秒变成几毫秒

注意这里的收益来源:不是引擎变快了,而是要处理的数据量少了几个数量级。这是所有预计算类优化的共同本质。

问题在于——预聚合到哪个维度组合?

假设有 6 个常用分析维度:日期、类目、城市、渠道、设备类型、用户等级。分析师可能按任意子集来看。

可能的维度组合数 = 2⁶ = 64 种(每个维度选或不选)。

推广到 n 个维度就是 2ⁿ

维度数 n 组合数 2ⁿ
5 32
10 1,024
15 32,768
20 1,048,576

全部预聚合是不可能的。所以实际做法是只对高频组合做预聚合,其余回落到明细。这带来了下面的性能悬崖。

查询命中预聚合   →  扫 3,000 行   →   8 毫秒     
查询未命中       →  扫 120 亿行   →   45 秒      

同样是"看一眼报表",性能差 5000 倍,而且用户完全无法预知自己会落在哪一边。分析师加了一个筛选维度,页面就从秒开变成转圈——他不理解为什么,也无从优化。

这是预聚合最难处理的问题:它让性能变得不可预测

缓解手段

  • 引擎侧的自动查询改写(query rewrite):优化器判断一个查询能否由已有的物化视图推导出来,能则自动改写。比如已有 (dt, cate_id, city) 的聚合,查 (dt, cate_id) 时可以在它之上再聚合一次,不必回明细。这能显著扩大命中面,但要求聚合函数可再聚合(SUM/COUNT/MIN/MAX 可以,COUNT(DISTINCT) 不行——这是一个重要限制)。
  • 把常用维度组合做成固定报表,把发散查询引导到明细表并明确告知"这个会慢"。

明细表更新了,物化视图怎么办?

刷新方式 做法 代价
全量重算 定时把整个视图重算一遍 简单可靠,但算力开销大,且重算期间视图数据是旧的
增量刷新 只把新增/变化的部分合并进视图 省算力,但实现复杂:要能识别哪些数据变了;遇到 UPDATE/DELETE(15 篇的电商场景)时,增量合并的正确性很难保证

还有一个隐蔽的一致性风险:物化视图和明细表在不同时刻刷新,可能出现"报表 A(走视图)显示 1000,报表 B(走明细)显示 1030"。两个数都对,只是时点不同——但业务方看到的是"数据不一致"。

所以预聚合的完整代价是三条:存储放大(多存一份甚至多份)、性能不可预测(命中悬崖)、维护复杂度与时点不一致

什么时候不该用预聚合:查询模式还在快速变化的探索期(预聚合建了就废)、维度基数极高(比如按 user_id 聚合,那不叫聚合)、需要 COUNT(DISTINCT) 且无法用近似算法替代时。

数据一旦对外提供服务,就要按服务的标准来运维,而不是按任务的标准。

① SLA 要显式定义。 一份交付物的 SLA 至少包含三项:产出时间(每天几点前一定可用)、查询延迟(P99 多少毫秒)、准确性(口径是什么、允许多大误差)。没有明确 SLA 的交付物,出问题时会陷入"这算不算故障"的扯皮。

② 资源隔离。 报表查询、临时分析、线上 API 三类负载必须隔离到不同资源组(引擎里的一种配额划分机制:给每组查询设定 CPU / 内存 / 并发上限,超出就排队而不是抢占别人的),否则一个失控的分析查询会打穿所有服务。这是 09 篇资源调度思想在交付层的具体应用。

③ 慢查询与熔断。 必须有查询超时、单查询扫描量上限、单用户并发上限。没有这些,一个 SELECT * FROM dwd_xxx(没带分区条件)就能把集群打满。

④ 缓存的两面性。 在数据 API 前加缓存能大幅降低后端压力,但会引入"缓存里的数据是几分钟前的"这一新的口径问题。如果两个接口的缓存过期时间不同,业务方会看到两个接口的数对不上——这类问题排查成本极高,因为两边的代码逻辑都是对的。

小结

  • "存下来"不等于"交付了"。湖满足不了消费方的三个要求:延迟、并发、稳定性隔离。
  • 仓比湖快由五条独立机制构成:存储介质与局部性、精细索引、可控的数据组织、预聚合、深度耦合的引擎优化。总代价是"数据要搬一遍 + 写入变慢 + 存储变贵 + 灵活性下降 + 绑定单一引擎"。
  • 选型用五个问题判断:延迟、并发、查询模式(固定还是发散)、新鲜度、数据量与访问频率。默认答案是留在湖里,通过检验才搬进仓。
  • 四种交付形态的技术要求根本不同:报表要通用扫描能力、API 要预推到 KV、人群包要 bitmap、特征最复杂(留 20 篇)。
  • 绝不能让线上业务系统直连数仓——理由是稳定性耦合、并发模型不匹配、故障爆炸半径、成本模型不匹配。正确做法是把结果推到线上 KV。
  • bitmap 圈选快,是因为集合运算变成了 CPU 位运算,不是因为引擎更快。它的硬前提是用户 ID 必须映射成稠密整数,这张全局字典表本身是长期的工程负担。
  • 预聚合的收益来自数据量下降几个数量级;三个代价是存储放大、性能不可预测(命中悬崖)、维护复杂度与时点不一致。自动查询改写能扩大命中面,但对 COUNT(DISTINCT) 无效。
  • 交付物要按服务运维:显式 SLA、资源隔离、慢查询熔断;注意缓存过期时间不一致会制造"两个接口对不上数"这类高排查成本的问题。
Back to Blog