<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>性能调优 on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98/</link><description>Recent content in 性能调优 on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 22 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98/index.xml" rel="self" type="application/rss+xml"/><item><title>第七话｜从建表到圈选：Doris 在 CDP 场景的实战</title><link>https://zh.renxinblog.cn/post/gyd-c07-doris-practice-in-cdp/</link><pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/gyd-c07-doris-practice-in-cdp/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/gyd-c07-doris-practice-in-cdp-cover.png" alt="Featured image of post 第七话｜从建表到圈选：Doris 在 CDP 场景的实战" /&gt;&lt;p&gt;上一章讲机制：数据在磁盘上怎么摆、摆好的数据怎么算。这一章讲决定。同样是「建一张会员画像表」，模型选哪个、排序键放谁、分桶怎么分，官方文档给的都是原则和区间；原则怎么变成你手上那个数字，只有在真实负载形状上跑一遍才看得出来。&lt;/p&gt;
&lt;p&gt;本章和后面几章的场景是同一个：&lt;strong&gt;一个 SaaS 会员运营平台&lt;/strong&gt;，商户在页面上勾标签组合圈人群、看预估人数、把人群包推到投放渠道，人群包每天凌晨重算一遍。会员总量 2 亿，分布在 500 家商户下，标签列上百个且每家启用的子集不同（本文示例表建 40 个标签列，判据同构）。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;数据与口径声明&lt;/strong&gt;：场景参数（会员数、商户分布、标签数、日回流量）是&lt;strong&gt;示例设定&lt;/strong&gt;，用来复现这类负载的形状，不是某个生产系统的真实数据。所有耗时、体积、扫描量数字来自作者本机一套 &lt;strong&gt;单 FE 单 BE 的 Doris 2.1.11 集群&lt;/strong&gt;（4 核、24 GB），用同构数据实测。耗时一律取 FE 审计日志的 &lt;code&gt;Time(ms)&lt;/code&gt;（服务端口径），并区分&lt;strong&gt;冷跑&lt;/strong&gt;与&lt;strong&gt;预热后中位数&lt;/strong&gt;——这两个数字差着一个数量级，不标口径的耗时没有意义。机制解释沿用上一章的口径，出自 2.1 官方文档。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一把这张画像表立起来"&gt;一、把这张画像表立起来
&lt;/h2&gt;&lt;h3 id="先看四类动作对表的要求"&gt;先看四类动作对表的要求
&lt;/h3&gt;&lt;p&gt;圈选这类业务，落到数据上是四个动作，每个动作对存储形态提一个要求：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;动作&lt;/th&gt;
					&lt;th&gt;负载形状&lt;/th&gt;
					&lt;th&gt;对表的要求&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;圈选&lt;/td&gt;
					&lt;td&gt;任意标签列组合进 &lt;code&gt;WHERE&lt;/code&gt;，命中从几百人到千万人&lt;/td&gt;
					&lt;td&gt;谓词要能被定位能力接住，接不住就是扫描&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;预估人数&lt;/td&gt;
					&lt;td&gt;同一套条件的 &lt;code&gt;count(*)&lt;/code&gt;，运营每改一个标签就点一次&lt;/td&gt;
					&lt;td&gt;秒级返回，且并发承受得住&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;定时重算&lt;/td&gt;
					&lt;td&gt;每天全量刷新会员标签&lt;/td&gt;
					&lt;td&gt;写入形态决定这件事的代价&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;导出投放&lt;/td&gt;
					&lt;td&gt;千万级 &lt;code&gt;user_id&lt;/code&gt; 分页拉走&lt;/td&gt;
					&lt;td&gt;有序、可游标翻页&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;会员画像表存的是「会员当前的样子」——等级、活跃度、累计消费、最近下单时间。这句话直接决定了第一个选择。&lt;/p&gt;
&lt;h3 id="决定一模型选主键但要知道主键贵在哪"&gt;决定一：模型选主键，但要知道主键贵在哪
&lt;/h3&gt;&lt;p&gt;上一章讲过三种模型的契约：明细模型原样保留每一行，聚合模型落盘即合并，主键模型每个键只留最新一行。「当前状态」这个语义就是主键模型的定义，没有第二个候选。真正要算的账是：&lt;strong&gt;和「明细模型 + 每天全量重灌」这条替代路比，主键模型贵在哪、省在哪&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;贵的那部分是存储。同一份 2 亿行 × 42 列的数据，本机建两张表各灌一遍：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;明细模型 member_profile_dup 6.886 GB
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;主键 MoW member_profile 7.746 GB 多 11%
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;多出来的 11% 是主键模型的两份内部结构：删除位图（记录哪些键的旧版本作废）和持久化索引（写入时定位同键旧行要用）。上一章说 MoW「写入时就地合并」，这句话的成本就在这儿——它得先能找到那个键，才能合并。&lt;/p&gt;
&lt;p&gt;省的那部分是写入。业务上的「标签回流」不是重灌全表，而是每天有一部分会员的若干标签变了。假设一天要更新 3000 万会员的 3 个标签列，两条路的写法完全不同：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 主键模型 + MoW：部分列更新，只写这 3 列，其余列保持原值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enable_unique_key_partial_update&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rfm_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_order_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;新等级&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;新&lt;/span&gt;&lt;span class="n"&gt;RFM&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;新下单时间&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;上游算好的回流结果&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本机实跑的就是这个形态，只是把三个新值表达式换成了从原表算出来的替身（&lt;code&gt;6 - level&lt;/code&gt; 之类），写入行数与列数与真实回流一致。&lt;/p&gt;
&lt;p&gt;明细模型没有「按键更新几列」这个动作——它的契约就是原样保留，改一行只能把整行重新写一遍，或者重灌全表。本机实测这两笔账：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;写法&lt;/th&gt;
					&lt;th&gt;写入量&lt;/th&gt;
					&lt;th&gt;耗时（本机实测）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;主键 MoW 部分列更新&lt;/td&gt;
					&lt;td&gt;3000 万行 × 3 列&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;27.31 秒&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;明细模型全量重灌&lt;/td&gt;
					&lt;td&gt;2 亿行 × 42 列&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;3 分 02 秒&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;写入量差了约 93 倍，耗时只差 6.7 倍——这个不对称本身就是结论：主键模型省下的不是那两分钟，而是**「每天必须把 2 亿行全量重算并搬一遍」这件事**。上游标签计算的输出量、导入窗口占用的资源、重灌期间的版本压力，都随全量这两个字放大。&lt;/p&gt;
&lt;p&gt;读侧这一项没有差别。同一条圈选查询在两张表上预热后的中位数都是 &lt;strong&gt;84 ms&lt;/strong&gt;（明细表 84 / 主键表 84，各 6 次取中位）——上一章说 MoW 换来「读侧干净」，在这个数据量上它确实是干净的：计划里那个 &lt;code&gt;__DORIS_DELETE_SIGN__ = 0&lt;/code&gt; 的谓词，成本量级可以忽略。&lt;/p&gt;
&lt;p&gt;所以第一个决定的答案是：&lt;strong&gt;选主键模型，付 11% 的存储，换掉全量重灌这条路&lt;/strong&gt;。代价不在读，在写侧的合并与那份位图。&lt;/p&gt;
&lt;h3 id="决定二排序键放-tenant_id-user_id然后接受它的局限"&gt;决定二：排序键放 &lt;code&gt;(tenant_id, user_id)&lt;/code&gt;，然后接受它的局限
&lt;/h3&gt;&lt;p&gt;排序键必须是列定义的前 K 列（上一章的纪律），所以这一步同时决定了列的排列顺序。两个约束把它钉死了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;圈选&lt;strong&gt;永远带租户&lt;/strong&gt;——SaaS 的行级隔离靠 &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt;，它是所有查询的第一个条件，按上一章「最常过滤的定长列排前面」的建议，它当第一列；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user_id&lt;/code&gt; 当第二列，服务另一类动作：按会员主键查详情，以及导出时的有序游标翻页。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 排序键第 1 列：圈选必带的租户
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 排序键第 2 列：点查与游标翻页
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;city_code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ┐
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TINYINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- │ 示例表建了 40 个标签列
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lifecycle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TINYINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- │ 生产常见上百个，这条判断不变：
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order_cnt_90d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- │ 多的是列，少的是能进短 key 的位置
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- │
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_order_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DATETIME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ┘
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 其余标签列，类型同上
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OLAP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DISTRIBUTED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HASH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BUCKETS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PROPERTIES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;replication_num&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;enable_unique_key_merge_on_write&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;短 key 上限 36 字节，&lt;code&gt;tenant_id&lt;/code&gt; 8 + &lt;code&gt;user_id&lt;/code&gt; 8 只占 16 字节，还剩 20 字节的空间——但&lt;strong&gt;没有值得放进来的第三列&lt;/strong&gt;。把某个标签（比如 &lt;code&gt;level&lt;/code&gt;）提上来，它确实能让「租户 + 等级」这一类条件走通前缀索引；代价是 &lt;code&gt;user_id&lt;/code&gt; 被挤出连续前缀，按会员主键点查和导出翻页从此用不上前缀索引。而圈选的条件组合是任意的，任何一个单独标签都只服务其中一类查询——三个位置里，用「覆盖必带条件」和「主键访问路径」占掉两个，比赌某个标签常当首条件更划算。&lt;/p&gt;
&lt;p&gt;于是接受它的局限。这条圈选语句在本机的执行计划与 profile：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SELECT count(*) FROM member_profile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WHERE tenant_id = 1 AND level &amp;gt;= 3 AND order_cnt_90d &amp;gt; 5 AND total_amount &amp;gt; 3000;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;partitions=1/1 -- 只建了一个分区，没有可裁的东西
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tablets=32/32 -- 32 个 tablet 全部进扫描
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RawRowsRead 30,000,000 -- 从存储层读出 3000 万行 = 租户 1 的全部
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RowsRead 1,825,506 -- 过滤后剩 182.6 万
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;前缀索引完成了它该做的那一段：2 亿行按租户收窄到 3000 万。剩下的三个标签条件，一个索引都没吃到，&lt;strong&gt;3000 万行全部读出来逐行判断&lt;/strong&gt;。16 倍的读取放大就藏在这里——这也是为什么这条查询「跑得动，但不对」：84 ms 是 4 核单机把蛮力跑满的结果，不是定位能力的结果。&lt;/p&gt;
&lt;h3 id="决定三分桶列和桶数一个服务均衡一个不服务裁剪"&gt;决定三：分桶列和桶数，一个服务均衡、一个不服务裁剪
&lt;/h3&gt;&lt;p&gt;分桶列选 &lt;code&gt;user_id&lt;/code&gt;。理由很朴素：它是这张表里唯一分布均匀的列，租户 ID 不行（头部一家占 15%）、任何标签列更不行（上一章的高基数纪律）。&lt;/p&gt;
&lt;p&gt;这个选择有一个容易被忽略的后果：&lt;strong&gt;圈选条件里没有 &lt;code&gt;user_id&lt;/code&gt;，所以桶裁剪不会发生&lt;/strong&gt;——上面计划里的 &lt;code&gt;tablets=32/32&lt;/code&gt; 就是这个意思，2 亿行摊在 32 个桶上，任何一个桶都可能藏着目标行，只能全扫。这不是错误，是这个场景的固有形状：按主键点查能裁到 1 个桶，按标签圈选一个桶都裁不掉。&lt;strong&gt;分桶列在圈选场景里服务的是均衡与并行，不服务裁剪&lt;/strong&gt;，指望它像 MySQL 索引那样「命中就少读」是错配。&lt;/p&gt;
&lt;p&gt;桶数按上一章的官方口径估：单个 tablet 保持在 1G–10G。这张表全量约 7.7 GB，照这个区间，&lt;strong&gt;8 个桶（每 tablet 约 970 MB）比 32 个桶（每 tablet 248 MB）更贴近建议值&lt;/strong&gt;。本机把 8 桶版本也建了一遍，两笔可查的账：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;32 桶&lt;/th&gt;
					&lt;th&gt;8 桶&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;每 tablet&lt;/td&gt;
					&lt;td&gt;248 MB / 625 万行&lt;/td&gt;
					&lt;td&gt;970 MB / 2500 万行&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;全表体积&lt;/td&gt;
					&lt;td&gt;7.746 GB&lt;/td&gt;
					&lt;td&gt;7.549 GB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;碎桶多花的 2% 是实测数字，归因是我按机制推的——每个 tablet 各自带一份短 key 索引与 page 级元数据，切得越碎这份固定开销越多；我测到的只有这两个体积数本身。桶数少一半还有另一笔对称的账：副本迁移、Schema Change 这类要重写 tablet 的操作，单次代价就大一倍（上一章讲过）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;桶数对查询耗时有多敏感，这台机器测不出可信答案&lt;/strong&gt;：本机 BE 只有 4 核，&lt;code&gt;parallel_pipeline_task_num&lt;/code&gt; 默认取核数一半，也就是 2 个扫描实例——8 桶版本的同一条圈选预热后中位数 117 ms、32 桶 84 ms，这个差值里混着缓存、Compaction 时机和调度抖动；而生产集群的 BE 核数通常远高于 4，扫描实例数不是一个量级，把这个差值外推过去必然失真。所以这里只留一句判断：&lt;strong&gt;桶数按 tablet 目标大小倒推，不要按「多切几块并行快点」的直觉定&lt;/strong&gt;，后者在 2.1 的 pipeline 引擎下已经不是主要收益来源（上一章的分片内并发读讲过原因）。&lt;/p&gt;
&lt;h3 id="三个决定签完之后"&gt;三个决定签完之后
&lt;/h3&gt;&lt;p&gt;把这一章的账并排放：模型选了主键，付 11% 存储、省掉全量重灌；排序键选了 &lt;code&gt;(租户, 会员)&lt;/code&gt;，换来必带条件能裁到 3000 万，代价是标签条件零索引、16 倍蛮力；分桶选了 &lt;code&gt;user_id&lt;/code&gt;，换来均匀分布，代价是圈选时一个桶都裁不掉。&lt;/p&gt;
&lt;p&gt;三个决定都没有「错」的那个，但每一个都把成本挪了个位置。而圈选这条查询真正的问题——&lt;strong&gt;读 3000 万只为留 182.6 万&lt;/strong&gt;——不在建表里，在标签列的索引形态里。&lt;/p&gt;
&lt;h2 id="二第一处不匹配谓词与落盘顺序"&gt;二、第一处不匹配：谓词与落盘顺序
&lt;/h2&gt;&lt;p&gt;上一章末尾那条圈选读 3000 万行、留 182.6 万行。这一章把它补上，补法有两种——跳数索引和点查索引——但先用实测说清一件事：&lt;strong&gt;在这张表上，第一种基本是白付的&lt;/strong&gt;。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;测量口径&lt;/strong&gt;：这一章的主证据是 query profile 里的 &lt;code&gt;RawRowsRead&lt;/code&gt;（从存储层真实读出的行数），不是耗时。原因是同一台机器上同一条查询冷跑 309 ms、热跑 33 ms，耗时里混着缓存与调度；而 &lt;code&gt;RawRowsRead&lt;/code&gt; 是个确定量，它掉不下来就是没生效。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="先量清楚三条查询各自在蛮力什么"&gt;先量清楚：三条查询各自在蛮力什么
&lt;/h3&gt;&lt;p&gt;2 亿行、无任何用户索引的基线（命中行数是 SQL 的真实返回）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;圈选条件&lt;/th&gt;
					&lt;th&gt;读出&lt;/th&gt;
					&lt;th&gt;留下&lt;/th&gt;
					&lt;th&gt;放大&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND city_code=101&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;3000 万&lt;/td&gt;
					&lt;td&gt;6.0 万&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;499×&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND level&amp;gt;=3 AND order_cnt_90d&amp;gt;5 AND total_amount&amp;gt;3000&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;3000 万&lt;/td&gt;
					&lt;td&gt;182.6 万&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;16×&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND is_vip=1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;3000 万&lt;/td&gt;
					&lt;td&gt;1049.8 万&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;3×&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三条读的都是 3000 万——前缀索引把租户那一段裁完之后，标签条件一律逐行判断。它们的差别只在留下多少：放大 499 倍的那条最该救，放大 3 倍的那条救了也没多少可省。这个「留下多少」可以量化，就是各列的&lt;strong&gt;单值命中占比&lt;/strong&gt;：&lt;code&gt;city_code&lt;/code&gt; 有 500 个取值、最大值的占比 &lt;strong&gt;0.2%&lt;/strong&gt;；&lt;code&gt;is_vip&lt;/code&gt; 只有 2 个取值、最大占比 &lt;strong&gt;65%&lt;/strong&gt;。这两个数是下面所有判断的起点。&lt;/p&gt;
&lt;h3 id="第一种补法bloom-filter一次干净的白付"&gt;第一种补法：bloom filter，一次干净的白付
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;city_code&lt;/code&gt; 是等值查询、基数不低，看起来正是上一章说的 bloom filter 的适用场景。加上它：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bloom_filter_columns&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city_code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;命令 0.11 秒返回，后台的 schema change 作业在 2 亿行上跑了&lt;strong&gt;约 80 秒&lt;/strong&gt;才 FINISHED，存储从 8.450 GB 涨到 8.581 GB（+131 MB）。然后重跑那条 &lt;code&gt;city_code=101&lt;/code&gt; 的查询：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RawRowsRead = 30,000,000 ← 一行没少读
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RowsRead = 60,138
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;放大 = 499× ← 与加索引前完全相同
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;一点没变&lt;/strong&gt;。原因在上一章讲过的那条纪律上：bloom filter 是&lt;strong&gt;跳数索引&lt;/strong&gt;，它只对「这一页里有没有这个值」回答「没有」，从而整页跳过。而这张表按 &lt;code&gt;(tenant_id, user_id)&lt;/code&gt; 排序，&lt;code&gt;city_code&lt;/code&gt; 是随用户哈希随机散布的——租户 1 的 3000 万行里，&lt;strong&gt;每一个 page 都装着全部 500 个城市码&lt;/strong&gt;。过滤器于是永远回答「可能有」，一个页也跳不掉。&lt;/p&gt;
&lt;p&gt;所以判据不是「等值 + 高基数就建 bloom filter」，而是：&lt;strong&gt;这一列的取值在页之间是否有偏斜&lt;/strong&gt;。有偏斜的列，是那些与排序键共序、或本身按时间单调写入的列（比如 &lt;code&gt;last_order_time&lt;/code&gt;，同一批回流的数据时间聚在一处）；标签列按用户随机分布时，跳数索引没有饭吃。&lt;/p&gt;
&lt;h3 id="第二种补法倒排索引它不吃分布"&gt;第二种补法：倒排索引，它不吃分布
&lt;/h3&gt;&lt;p&gt;倒排索引走的是另一条路：不给「这页有没有」的答案，直接给&lt;strong&gt;哪些行命中&lt;/strong&gt;的行号。所以列值怎么散布与它无关。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_city&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INVERTED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BUILD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_city&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 事后加的索引要对已有数据补建一次
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;BUILD INDEX&lt;/code&gt; 在 2 亿行上跑了 &lt;strong&gt;804 秒&lt;/strong&gt;，这一列的索引多占 &lt;strong&gt;412 MB&lt;/strong&gt;。重跑同一条查询：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RawRowsRead = 60,138 ← 从 3000 万降到与命中行数相等
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RowsRead = 60,138
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;放大 = 1×
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;499 倍的蛮力一次消掉，热态耗时 33 ms。&lt;strong&gt;但别急着给所有标签列都建&lt;/strong&gt;——换一条命中面大的条件，同一个索引立刻失效：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tenant_id=1 AND city_code BETWEEN 100 AND 275 -- 覆盖 35% 的取值
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RawRowsRead = 30,000,000 ← 又回到全读
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RowsRead = 10,562,749
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;命中面铺到三分之一时，索引没有省下任何读取。按机制推，是因为「按索引逐行取位 + 随机读」在命中行足够多时比不过顺序扫全量（这一句是推演，我测到的只是 &lt;code&gt;RawRowsRead&lt;/code&gt; 没变）。这与上一章那条「倒排的收益来自命中少，不是无条件快」是同一件事的两面：&lt;strong&gt;索引省下的读取量，上限就是你不命中的那部分行数&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="组合条件的木桶第一个没索引的条件决定下限"&gt;组合条件的木桶：第一个没索引的条件决定下限
&lt;/h3&gt;&lt;p&gt;真实圈选从来不只一个条件。把索引列和没索引的列混起来，看谁说了算：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tenant_id=1 AND city_code=101 AND level&amp;gt;=3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RawRowsRead = 60,138 RowsRead = 13,150 放大 5×
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;city_code&lt;/code&gt; 上的索引把读取钉死在 6 万行，&lt;code&gt;level&lt;/code&gt; 没有索引，就在这 6 万行里逐行筛掉 78%——它筛得动，因为基数已经被前一个条件压下去了。反过来，把有索引的那一列拿掉：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tenant_id=1 AND level&amp;gt;=3 AND order_cnt_90d&amp;gt;5 AND total_amount&amp;gt;3000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RawRowsRead = 30,000,000 放大 16× ← 索引一点没帮上
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结论很硬：&lt;strong&gt;一条圈选里，第一个没有索引的条件，决定这条查询的读取量下限&lt;/strong&gt;。索引建在哪个列上不重要，重要的是运营圈人时&lt;strong&gt;最先出现、且命中占比最低&lt;/strong&gt;的那几个列有没有被覆盖到。&lt;/p&gt;
&lt;h3 id="那到底给哪几列建"&gt;那到底给哪几列建
&lt;/h3&gt;&lt;p&gt;三个筛子按顺序过：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;进条件的频率&lt;/strong&gt;。别靠猜——Doris 的 FE 审计日志记录了每一条查询的原文，按周统计一次各标签列出现在 &lt;code&gt;WHERE&lt;/code&gt; 里的次数，就是运营的真实习惯。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单值命中占比&lt;/strong&gt;。上面那两个数（0.2% 与 65%）就是筛子：占比低的列建了才可能省，占比高的列建了也省不动。一条 SQL 就能量出来：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;top_value_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_rows&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;city_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;city_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="3"&gt;
&lt;li&gt;&lt;strong&gt;预算&lt;/strong&gt;。这一项在 2 亿行上是硬的：每列一次 &lt;code&gt;BUILD INDEX&lt;/code&gt; 约 13 分钟、约 412 MB 存储。40 个标签列全建，就是 9 小时窗口和 16 GB 索引——荒谬。所以真实动作是&lt;strong&gt;从高频且低占比的列里挑 5 到 8 个&lt;/strong&gt;，其余列接受「在已筛剩的行上逐行判断」，只要前面那一两个索引把基数压得够低，后面的逐行过滤并不贵。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顺带一句运维量级：这些索引作业都是异步的、且互斥（上一章讲过 &lt;code&gt;SCHEMA_CHANGE&lt;/code&gt; 状态会挡住其他结构变更）。在 2 亿行的表上，「加一列索引」意味着一个 13 分钟的窗口和一次不能并发的变更——这是它和 MySQL 上加个索引完全不同的地方。&lt;/p&gt;
&lt;h2 id="三第二处不匹配写入节奏与合并节奏"&gt;三、第二处不匹配：写入节奏与合并节奏
&lt;/h2&gt;&lt;p&gt;前两章都在动「读」这一侧。但这类系统每天真正压着集群的是写：标签要回流，行为事件要持续进来。上一章建立过一个等式——&lt;strong&gt;一次导入 = 一个不可变的 rowset = 一个版本&lt;/strong&gt;，读的时候要把这些版本叠成一张视图。这个等式在导入频率高的时候会开始收利息。&lt;/p&gt;
&lt;h3 id="先量一遍一次导入一个版本有多贵"&gt;先量一遍「一次导入一个版本」有多贵
&lt;/h3&gt;&lt;p&gt;一张 4 桶的小表，连灌 24 条单行 &lt;code&gt;INSERT&lt;/code&gt;，tablet 的 &lt;code&gt;Version&lt;/code&gt; 从 3500 涨到 3524（重跑一轮是 3553 → 3577）：&lt;strong&gt;精确的 24&lt;/strong&gt;。没有合并、没有优化，一条语句一个版本。&lt;/p&gt;
&lt;p&gt;大表上单笔大写入反而是便宜的——第一章那次 3000 万行的部分列更新，27.31 秒完成，版本只 +1。贵的是&lt;strong&gt;高频小批&lt;/strong&gt;：每次导入都留下一个待合并的 rowset，读的时候就要多叠一层。同一个圈选查询，在这次更新刚落地后的头两次跑出了 502 ms 和 655 ms，随后回落到 81 ms 和 65 ms。多出来的那一段，按机制推是新 rowset 首次读取（缓存未热）加上合并路径变长——我测到的是这四个数本身，归因是推的。&lt;/p&gt;
&lt;h3 id="-235-的真实成因不是导入次数多"&gt;-235 的真实成因不是「导入次数多」
&lt;/h3&gt;&lt;p&gt;版本堆到有上限，导入就会被拒绝，报 -235。但这里有两处口径要先说清，因为它们直接影响你怎么设防线。&lt;/p&gt;
&lt;p&gt;第一，&lt;strong&gt;默认值文档自己不一致&lt;/strong&gt;。官方 FAQ「数据操作问题」页写的是「默认 500」，而 BE 配置项文档页和本机 2.1.11 的实际配置都是 &lt;strong&gt;2000&lt;/strong&gt;（且可动态修改）。以本机为准：&lt;code&gt;max_tablet_version_num = 2000&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第二，也是更要紧的：&lt;strong&gt;这个限制判的不是版本号&lt;/strong&gt;。&lt;code&gt;SHOW TABLETS FROM 表&lt;/code&gt; 里有两个容易混的字段——&lt;code&gt;Version&lt;/code&gt; 是这条 tablet 的时间线走到第几号，只增不减；&lt;code&gt;VersionCount&lt;/code&gt; 是当前还并存的 rowset 份数，会被合并压回去。撞 -235 判的是后者。&lt;/p&gt;
&lt;p&gt;这一点有直接证据。往同一个 tablet 连灌 24 条单行 &lt;code&gt;INSERT&lt;/code&gt;，逐条采样：&lt;code&gt;Version&lt;/code&gt; 从 3553 一路走到 3577（&lt;strong&gt;每条 +1，精确 24&lt;/strong&gt;），而 &lt;code&gt;VersionCount&lt;/code&gt; 自始至终钉在 &lt;strong&gt;5&lt;/strong&gt;——这张小表的合并是毫秒级的，攒不起来。此时 &lt;code&gt;Version&lt;/code&gt; 早已越过 2000 这个阈值 1500 多，导入一次都没被拒。反向实验指向同一处：把阈值临时调到 200、再调到 8，&lt;code&gt;Version&lt;/code&gt; 涨到 3500 仍不报错；要是判的是版本号，调到 8 之后的第二次导入就该失败。&lt;/p&gt;
&lt;p&gt;&lt;img alt="同一个 tablet 上 Version 一路涨、VersionCount 恒为 5，检查比的是后者" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" decoding="async" height="766" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/gyd-c07-doris-practice-in-cdp-version-vs-rowset.svg" width="1023"&gt;&lt;/p&gt;
&lt;p&gt;所以「导入次数多就会撞 -235」是错的。事故条件是&lt;strong&gt;导入速度大于合并速度&lt;/strong&gt;，而这恰恰发生在大表上：一个 248 MB 的 tablet 合并要秒级，此时高并发小批导入就会把 rowset 堆起来。小表反而几乎撞不到——这也是为什么这类事故常在业务增长、tablet 变大之后才第一次出现。&lt;/p&gt;
&lt;p&gt;排查动作按顺序：&lt;code&gt;SHOW TABLETS FROM 表&lt;/code&gt; 看每个 tablet 的 &lt;code&gt;Version&lt;/code&gt;；BE 日志里搜 &lt;code&gt;compaction&lt;/code&gt; 与该 tablet id，看合并是否积压；导入侧看 Stream Load 返回 JSON 里的时间分解（上一章讲过），确认是卡在写还是卡在等版本发布。&lt;/p&gt;
&lt;h3 id="解法与代价攒批的合并率来自并发"&gt;解法与代价：攒批的合并率来自并发
&lt;/h3&gt;&lt;p&gt;Doris 2.1 的 Group Commit 是官方给的答案：把多个小事务在存储侧合并提交。但「合到什么程度」取决于&lt;strong&gt;同一时刻落在同一个 tablet 上的导入数&lt;/strong&gt;，这一点只有实测说得清。四组配置，同一张小表：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;配置&lt;/th&gt;
					&lt;th&gt;并发&lt;/th&gt;
					&lt;th&gt;导入条数&lt;/th&gt;
					&lt;th&gt;版本增量&lt;/th&gt;
					&lt;th&gt;墙钟&lt;/th&gt;
					&lt;th&gt;可见性&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;默认（不开）&lt;/td&gt;
					&lt;td&gt;串行&lt;/td&gt;
					&lt;td&gt;24&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;+24&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;约 0.5 s/条&lt;/td&gt;
					&lt;td&gt;立即&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;async_mode&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;串行&lt;/td&gt;
					&lt;td&gt;30&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;+1&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;秒级返回&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;约 12 秒后可见&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;sync_mode&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;串行&lt;/td&gt;
					&lt;td&gt;20&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;+20&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;202 秒&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;立即&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;sync_mode&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;8 会话并发&lt;/td&gt;
					&lt;td&gt;40&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;+5&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;51 秒&lt;/td&gt;
					&lt;td&gt;立即&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第二行和第三行的对比最值得记：&lt;strong&gt;同样串行，&lt;code&gt;async_mode&lt;/code&gt; 把 30 次导入合成 1 个版本，&lt;code&gt;sync_mode&lt;/code&gt; 一个都没合，还让每条都等了约 10 秒的提交窗口&lt;/strong&gt;。原因是 &lt;code&gt;async_mode&lt;/code&gt; 客户端不等提交就返回，后续导入于是落进同一个窗口；而 &lt;code&gt;sync_mode&lt;/code&gt; 要等本次提交完成才返回，串行时窗口里永远只有自己。第四行验证了另一半：并发一开，&lt;code&gt;sync_mode&lt;/code&gt; 40 条只产生 5 个版本。&lt;/p&gt;
&lt;p&gt;语法上有个坑要标出来：&lt;code&gt;group_commit&lt;/code&gt; 是&lt;strong&gt;会话变量&lt;/strong&gt;，不是表属性——&lt;code&gt;ALTER TABLE ... SET (&amp;quot;group_commit&amp;quot; = &amp;quot;async_mode&amp;quot;)&lt;/code&gt; 会直接报 &lt;code&gt;Unknown table property&lt;/code&gt;，正确写法是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;group_commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;async_mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 会话级；三种取值：off_mode / sync_mode / async_mode
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="圈选场景该开哪一档"&gt;圈选场景该开哪一档
&lt;/h3&gt;&lt;p&gt;有了上面那组数字，选择就变成业务问题了：&lt;strong&gt;能不能接受写入后延迟可见&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预估人数、人群包定时重算&lt;/strong&gt;：晚 10 秒知道结果完全可接受，甚至更好（重算期间不该被读到中间状态）。这一类走 &lt;code&gt;async_mode&lt;/code&gt;，把版本压力直接压掉一个数量级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;投放前最后一次确认、部分列更新后立刻查&lt;/strong&gt;：不可接受。这一类要么保持默认（自己把批量攒大，比如按分片攒到百万级再灌），要么用 &lt;code&gt;sync_mode&lt;/code&gt; &lt;strong&gt;并且真的并发写&lt;/strong&gt;——串行 + &lt;code&gt;sync_mode&lt;/code&gt; 是实测里最坏的组合，既没省版本又赔了时延。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再往回退一步看这件事：合并能力是有预算的，它吃的是 BE 的 CPU 和磁盘 IO。&lt;strong&gt;提高导入频率，本质是借未来的合并算力换现在的低延迟&lt;/strong&gt;。所以「我们的导入能不能更频繁」这个问题，正确的问法是「我们的合并还追不追得上」——&lt;code&gt;SHOW TABLETS&lt;/code&gt; 里的版本号和 BE 日志里的 compaction 记录，就是它的仪表盘。&lt;/p&gt;
&lt;h2 id="四第三处不匹配人群包形态与集合运算"&gt;四、第三处不匹配：人群包形态与集合运算
&lt;/h2&gt;&lt;p&gt;前三章都在处理「一张表怎么被查、怎么写」。这一章换到业务对象上：&lt;strong&gt;圈选跑出来的结果本身要怎么存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;CDP 里人群包（audience）在语义上就是一个用户 ID 集合，但仓库里的第一版实现通常是一张明细结果表——&lt;code&gt;pack_id + user_id&lt;/code&gt;，一行一个成员。它最直观：能数、能 JOIN、能分页拉走。等到要做「两个人群包求交」「在包上再叠一个标签条件」「排除风控名单后再并上某个渠道包」这类操作时，才会发现集合运算在关系表上就是一串 JOIN，而每一个 JOIN 都要把成员重新读出来、重新装进哈希表（哈希表是 Java 侧最熟的结构：把一批值按哈希放进桶里，用 O(1) 判断某个值在不在）。&lt;/p&gt;
&lt;p&gt;Doris 给了另一条路：&lt;strong&gt;BITMAP 聚合列&lt;/strong&gt;——把整个成员集合压进一行的一个二进制列里，集合运算退化成位运算。这条路到底省了什么、又在哪里付出代价，只能用同一批人群包在两种形态上跑同一组操作来回答。&lt;/p&gt;
&lt;p&gt;本章的对比在两张表上做，成员列都是 BIGINT：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 明细结果表：一行一个成员，成员 ID 进排序键
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_member&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OLAP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DUPLICATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DISTRIBUTED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HASH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BUCKETS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PROPERTIES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;replication_num&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 位图聚合表：一个包一行，成员压进一个 BITMAP 列
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BITMAP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BITMAP_UNION&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OLAP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;AGGREGATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DISTRIBUTED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HASH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BUCKETS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PROPERTIES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;replication_num&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第二段是 Doris 聚合模型的写法：列声明时跟在类型后面挂聚合函数，&lt;code&gt;user_bm BITMAP BITMAP_UNION&lt;/code&gt; 表示同一个聚合键下新来的位图和已有的取并。（这里踩过一个语法坑：&lt;code&gt;user_bm BITMAP_UNION&lt;/code&gt;——省掉类型只写聚合函数——在 2.1 直接报语法错，必须写全 &lt;code&gt;BITMAP BITMAP_UNION&lt;/code&gt;。）&lt;/p&gt;
&lt;h3 id="四个包以及它们的真值"&gt;四个包，以及它们的真值
&lt;/h3&gt;&lt;p&gt;四个包都属租户 1（3000 万会员），规模跨两个数量级——这类系统的真实分布就是这样，头部活动圈千万级，长尾条件只圈几万：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;包 ID&lt;/th&gt;
					&lt;th&gt;圈选条件&lt;/th&gt;
					&lt;th&gt;成员数（真值）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;9001&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND is_vip=1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;10,497,584&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;9002&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND active_30d=1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;8,401,829&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;9003&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND level&amp;gt;=4&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;2,097,972&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;9004&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tenant_id=1 AND city_code=101&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;60,138&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;「真值」这一列很重要：它由一次单表扫描直接数出来，后面两种形态算出的任何成员数都必须能对上它。&lt;strong&gt;形态对比的前提是双方都算对&lt;/strong&gt;，否则比的只是谁错得更隐蔽。&lt;/p&gt;
&lt;p&gt;位图形态有一个硬前提：&lt;strong&gt;成员 ID 必须是整型&lt;/strong&gt;。2.1 官方文档的原话是「Doris 使用 RoaringBitmap 实现精确去重」，且只对 &lt;code&gt;TINYINT&lt;/code&gt;/&lt;code&gt;SMALLINT&lt;/code&gt;/&lt;code&gt;INT&lt;/code&gt;/&lt;code&gt;BIGINT&lt;/code&gt; 生效，其他类型「需要构建全局字典」。这里的 &lt;code&gt;user_id&lt;/code&gt; 恰好是 1 到 2 亿的连续整型，所以两张表都能直接用；真实系统的会员标识经常是 openid、手机号这类字符串，这个前提就得单独处理。&lt;/p&gt;
&lt;h3 id="构建贵的是扫描形态只影响内存"&gt;构建：贵的是扫描，形态只影响内存
&lt;/h3&gt;&lt;p&gt;两种形态都用 &lt;code&gt;INSERT INTO ... SELECT&lt;/code&gt; 从画像表生成同一批包，耗时取审计日志 &lt;code&gt;Time(ms)&lt;/code&gt;（热跑，服务端），内存取同一条日志的 &lt;code&gt;PeakMemoryBytes&lt;/code&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;成员数&lt;/th&gt;
					&lt;th&gt;明细表&lt;/th&gt;
					&lt;th&gt;位图表&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;10,497,584&lt;/td&gt;
					&lt;td&gt;1816 ms / 643 MB&lt;/td&gt;
					&lt;td&gt;1915 ms / 94.6 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;8,401,829&lt;/td&gt;
					&lt;td&gt;913 ms / 611 MB&lt;/td&gt;
					&lt;td&gt;1280 ms / 85.0 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2,097,972&lt;/td&gt;
					&lt;td&gt;262 ms / 134 MB&lt;/td&gt;
					&lt;td&gt;333 ms / 57.3 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;60,138&lt;/td&gt;
					&lt;td&gt;92 ms / 6.7 MB&lt;/td&gt;
					&lt;td&gt;102 ms / 6.3 MB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;位图不但没更快，还慢 5% 到 40%&lt;/strong&gt;。因为构建的成本主体是「扫 2 亿行、命中一千万行」，这一段两种形态完全一样；位图多出来的时间是逐行把整型值塞进压缩容器的开销。所以「换成位图，圈选就快了」在构建这一步不成立——这句话常被写成选型理由，实测不支持。&lt;/p&gt;
&lt;p&gt;不对称的地方在内存：明细侧要把 1050 万行从扫描算子搬到写入算子，峰值 643 MB；位图侧同一个位置只搬一个聚合值，94.6 MB，&lt;strong&gt;差 6.8 倍&lt;/strong&gt;。这笔内存会随导入并发线性叠加（此句是按机制推的，本机没有做并发导入压测），是并发排队时的真实约束。&lt;/p&gt;
&lt;h3 id="存储位图没有比明细省省不省取决于成员-id-在不在排序键里"&gt;存储：位图没有比明细省——省不省取决于成员 ID 在不在排序键里
&lt;/h3&gt;&lt;p&gt;这是本章最反直觉的一个结果，所以做了控制变量的对照。同一批 10,497,584 个成员、单个 tablet，绕开 FE 统计缓存（&lt;code&gt;SHOW DATA&lt;/code&gt; 有约 1 分钟的延迟），直接量 BE 上 tablet 目录的磁盘占用：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;形态（同一批 1050 万成员，1 桶）&lt;/th&gt;
					&lt;th&gt;磁盘占用&lt;/th&gt;
					&lt;th&gt;每成员&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;明细表，成员 ID 在排序键内（自然升序）&lt;/td&gt;
					&lt;td&gt;16,568,475 B = 15.79 MB&lt;/td&gt;
					&lt;td&gt;1.58 B&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;明细表，成员 ID 在排序键内（值被打乱）&lt;/td&gt;
					&lt;td&gt;16,809,834 B = 16.03 MB&lt;/td&gt;
					&lt;td&gt;1.60 B&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;明细表，成员 ID 挪出排序键当值列（打乱）&lt;/td&gt;
					&lt;td&gt;37,401,273 B = 35.67 MB&lt;/td&gt;
					&lt;td&gt;3.56 B&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;位图聚合表&lt;/td&gt;
					&lt;td&gt;21,132,945 B = 20.15 MB&lt;/td&gt;
					&lt;td&gt;2.01 B&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HLL 聚合表&lt;/td&gt;
					&lt;td&gt;101,704 B = 99.3 KB&lt;/td&gt;
					&lt;td&gt;0.01 B&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;成员 ID 进了排序键的那张明细表，比位图还小 21%&lt;/strong&gt;。要理解这件事得看两边各自在压什么。&lt;/p&gt;
&lt;p&gt;位图这边，Doris 的 &lt;code&gt;BITMAP&lt;/code&gt; 列内部存的是 Roaring bitmap。它的通用做法（Roaring 自己的公开设计，不是 Doris 特有）是：把整数按每 65536 个值切成一块，块内值少就用「数组容器」逐个存 2 字节，值多就改用「位图容器」——固定 8 KB、一位代表一个值，切换阈值是 4096。这批包在 2 亿值域上的密度只有 5%，2 亿 / 65536 ≈ 3052 个块，每块摊到约 3440 个成员，正好留在数组容器区间。按这个规则估是 2.0 字节/成员，实测 2.01 字节。&lt;strong&gt;换句话说，这份位图完全没有「压」，它就是把每个成员存成 2 个字节的数组&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;明细表这边，上一章讲过存储层级：同一个 tablet 内数据按排序键有序落盘，官方文档对明细模型的表述是「建表时指定的 Duplicate Key 决定了排序与存储的列」。排好序的整型列相邻差值很小，delta 编码 + bit-packing 能把一个 int64 压到 1~2 字节——实测 1.58 字节/行。&lt;strong&gt;同样是 1050 万个整型 ID，排好序的明细表和数组容器状态的位图，压缩率在同一个量级；前者还略优&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;「排序键里放不放」才是那个变量，这一点单独做了对照实验来证明。把成员 ID 用一个同余映射打乱：&lt;code&gt;(user_id * 7919) % 200000003&lt;/code&gt;（&lt;code&gt;7919&lt;/code&gt; 与 &lt;code&gt;200000003&lt;/code&gt; 互质，且所有 &lt;code&gt;user_id&lt;/code&gt; 都小于这个模数，所以映射在取值域上是单射，成员数不会因碰撞而变——实测打乱后仍是 10,497,584 个不同值）。然后写进两张表：一张仍让 &lt;code&gt;user_id&lt;/code&gt; 参与排序键，另一张把排序键换成一个常量列、让 &lt;code&gt;user_id&lt;/code&gt; 退成普通值列（于是落盘顺序 = 插入顺序，也就是打乱后的顺序）。&lt;/p&gt;
&lt;p&gt;结果是：&lt;strong&gt;同一样一批值，放进排序键 16.03 MB，挪出来 35.67 MB，差 2.2 倍&lt;/strong&gt;。而打乱输入顺序对「放进排序键」那一版几乎没有影响（15.79 → 16.03 MB），因为键内序列一定会被排好——决定体积的是排序这件事本身，不是值长什么样。&lt;/p&gt;
&lt;p&gt;这条不只对人群包表有用：&lt;strong&gt;它解释了「为什么把高频出现在 &lt;code&gt;WHERE&lt;/code&gt; 里的整型列放进排序键，连存储都一起省了」&lt;/strong&gt;——排序键同时买到的是前缀索引 + 有序落盘带来的编码收益。&lt;/p&gt;
&lt;p&gt;两个规模端的补充：60,138 人的小包，明细 149,121 B、位图 145,398 B，打平；四个包合到一张表的整表口径，明细 &lt;code&gt;SHOW DATA&lt;/code&gt; 40.015 MB、位图 40.357 MB，也一样。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果只要「人数」不要「成员」，还有第三档：HLL&lt;/strong&gt;（HyperLogLog，一种只保存基数草图的类型，不管来多少个值，占用的空间基本不变；对应聚合函数 &lt;code&gt;HLL_UNION&lt;/code&gt;）。同一个 1050 万成员的包，HLL 列实测 99.3 KB，&lt;strong&gt;比位图小 208 倍&lt;/strong&gt;，估出来的人数是 10,535,810、误差 &lt;strong&gt;+0.364%&lt;/strong&gt;。千分之一的空间换 0.4% 的人数误差，只用于看板上的规模趋势时它比位图更划算；代价是它只能求并——交集和差集都算不了。&lt;/p&gt;
&lt;h3 id="集合运算这才是真正的分水岭"&gt;集合运算：这才是真正的分水岭
&lt;/h3&gt;&lt;p&gt;四种运算，两种形态，结果全部与真值一致，差别只在代价（耗时与峰值内存同上口径，热跑）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;集合运算&lt;/th&gt;
					&lt;th&gt;真值&lt;/th&gt;
					&lt;th&gt;明细表&lt;/th&gt;
					&lt;th&gt;位图&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;A ∩ B&lt;/td&gt;
					&lt;td&gt;2,941,079&lt;/td&gt;
					&lt;td&gt;493 ms / 189.9 MB&lt;/td&gt;
					&lt;td&gt;273 ms / 0.74 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;A ∪ B&lt;/td&gt;
					&lt;td&gt;15,958,334&lt;/td&gt;
					&lt;td&gt;1400 ms / 1.39 GB&lt;/td&gt;
					&lt;td&gt;219 ms / 0.12 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;A − B&lt;/td&gt;
					&lt;td&gt;7,556,505&lt;/td&gt;
					&lt;td&gt;716 ms / 259.9 MB&lt;/td&gt;
					&lt;td&gt;208 ms / 0.11 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;(A ∪ B) ∩ C − D&lt;/td&gt;
					&lt;td&gt;1,113,829&lt;/td&gt;
					&lt;td&gt;1874 ms / 0.96 GB&lt;/td&gt;
					&lt;td&gt;309 ms / 0.41 MB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;耗时差 1.8 到 6.4 倍，&lt;strong&gt;峰值内存差 256 倍到 1.2 万倍&lt;/strong&gt;——内存才是这一节的分水岭。&lt;/p&gt;
&lt;p&gt;机制上没有悬念。明细形态做集合运算，要把两侧成员读出来、装进哈希表——&lt;code&gt;count(DISTINCT user_id)&lt;/code&gt; 求并那行为 1596 万个不同值各维护一个条目，1.39 GB 就是这么来的，而且中间结果仍然是行。位图形态两侧各是一个二进制容器，&lt;code&gt;bitmap_and&lt;/code&gt; / &lt;code&gt;bitmap_or&lt;/code&gt; / &lt;code&gt;bitmap_andnot&lt;/code&gt; 在容器上逐段做与或非，中间结果还是位图，&lt;strong&gt;整个查询从头到尾只有一行数据在算子之间流动&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img alt="明细形态把两侧成员装进哈希表，位图形态只在容器上逐段运算" class="gallery-image" data-flex-basis="332px" data-flex-grow="138" decoding="async" height="832" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/gyd-c07-doris-practice-in-cdp-form-execution-diff.svg" width="1153"&gt;&lt;/p&gt;
&lt;p&gt;位图侧那条四层嵌套的表达式长这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap_andnot_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap_and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- A ∪ B
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ∩ C
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- − D
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9002&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9003&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9004&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;明细侧的等价写法是三段 UNION + JOIN + 反连接嵌套，这里不贴——它长不是问题，问题是每多一层就把成员多搬一遍。真实业务里的「排除已购买人群、排除风控名单、再和某个渠道包求交」就是这个深度。&lt;/p&gt;
&lt;p&gt;顺带一个写法层面的实测：同样是 A − B，&lt;code&gt;LEFT JOIN ... IS NULL&lt;/code&gt; 要 716 ms，&lt;code&gt;NOT IN&lt;/code&gt; 子查询 233 ms，差 3.1 倍，内存都是 259.9 MB。差异在中间结果——前者先产出 1050 万行带 NULL 列的宽结果再过滤，后者直接在反连接里计数。用 &lt;code&gt;NOT IN&lt;/code&gt; 的前提是被比较列非空（这里 &lt;code&gt;user_id&lt;/code&gt; 是 NOT NULL）；换成可空列，三值逻辑会让结果整个变空，这个坑在 Doris 上和 MySQL 完全一样。&lt;/p&gt;
&lt;h3 id="回表位图不吃亏"&gt;回表：位图不吃亏
&lt;/h3&gt;&lt;p&gt;集合运算之外，人群包最高频的用法是「拿包去画像表取字段」——投放名单要带渠道、要带最近下单时间。同一个问题（pack 9003 里有多少 VIP，真值 733,742）三种写法：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;写法&lt;/th&gt;
					&lt;th&gt;耗时&lt;/th&gt;
					&lt;th&gt;峰值内存&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;明细包表 JOIN 画像表&lt;/td&gt;
					&lt;td&gt;691 ms&lt;/td&gt;
					&lt;td&gt;243.1 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;user_id IN (子查询)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;914 ms&lt;/td&gt;
					&lt;td&gt;243.1 MB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;位图展开成行后 JOIN 画像表&lt;/td&gt;
					&lt;td&gt;432 ms&lt;/td&gt;
					&lt;td&gt;72.2 MB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;位图这一路把整包先还原成数组再炸开行（&lt;code&gt;LATERAL VIEW explode(bitmap_to_array(...))&lt;/code&gt;，2.1 的 Nereids 优化器不吃 &lt;code&gt;UNNEST(...)&lt;/code&gt; 那套 ANSI 写法，实测会报语法错，只有 Hive 风格的 &lt;code&gt;LATERAL VIEW explode&lt;/code&gt; 能用），省掉了「从一张 210 万行的表里把 ID 一行行解码出来」的开销。所以「位图查详情一定要付额外代价」这个直觉，在本机数据里不成立——它反而是三者最省的。&lt;/p&gt;
&lt;h3 id="导出唯一稳定吃亏的地方"&gt;导出：唯一稳定吃亏的地方
&lt;/h3&gt;&lt;p&gt;要真正把成员一行一行交出去时，位图开始付出代价。同一份 1050 万成员写进同一张落地表：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;导出路径（同一目标表、同一 1050 万行）&lt;/th&gt;
					&lt;th&gt;耗时&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;从明细包表读&lt;/td&gt;
					&lt;td&gt;1084 ms&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;从位图展开后读&lt;/td&gt;
					&lt;td&gt;1615 ms&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;多出的 49% 就是「反序列化位图 → 构造 ARRAY&amp;lt;BIGINT&amp;gt; → 再炸开」这条链；单独量这一步，把整包 1050 万个成员炸成行要 643 ms、288 MB。方向是固定的：&lt;strong&gt;位图形态在导出上不会比明细便宜，而且规模越大越亏&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;分批游标导出更明显。明细表按 &lt;code&gt;(pack_id, user_id)&lt;/code&gt; 排序，游标翻页是前缀索引直接定位：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_member&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9001&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;上一页末值&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 实测 267 ms，取回 10 万行
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;位图侧只有区间能力，没有「按顺序取第 N 到 M 个」的稳定分页语义。&lt;code&gt;bitmap_subset_in_range&lt;/code&gt; 可以做区间计数——实测 &lt;code&gt;[50000000, 60000000)&lt;/code&gt; 得 524,605，与明细表上等价 &lt;code&gt;WHERE&lt;/code&gt; 的结果一致；边界口径是小位图单独验过的：{1,2,3,4,5} 上 &lt;code&gt;[2,4)&lt;/code&gt; 数出 2 个，即&lt;strong&gt;左闭右开&lt;/strong&gt;。真要交明细，还是得整包炸开再自己分页，那比在明细表上翻页贵得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;导出是明细形态的主场&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="两个会静默出错的坑"&gt;两个会静默出错的坑
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;坑一：字符串 ID 进位图不报错，给你一个空集合&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;CDP 的成员标识经常是 openid、手机号、雪花 ID 字符串，而 &lt;code&gt;to_bitmap()&lt;/code&gt; 只接受整型。传字符串时 Doris 做隐式转换，转不动就返回空位图，&lt;strong&gt;没有任何报错&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_bitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;P3&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_bitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;123&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 1，纯数字字符串能转
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;（第一条是在一个 &lt;code&gt;pref_category&lt;/code&gt; 列取值为 &lt;code&gt;t_11&lt;/code&gt;、&lt;code&gt;t_5&lt;/code&gt; 这类编码的表上验证的：整列 &lt;code&gt;to_bitmap&lt;/code&gt; 之后 &lt;code&gt;bitmap_count&lt;/code&gt; 全是 0。）&lt;/p&gt;
&lt;p&gt;于是整条圈选链路上你会拿到一个「人人都是 0」的人群包，作业状态 &lt;code&gt;OK&lt;/code&gt;。两条出路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按官方口径建那张「全局字典」，把字符串 ID 映射成稠密自增整型。这是位图方案真实的前置成本——字典表要维护增量、要能重放，本文的 &lt;code&gt;user_id&lt;/code&gt; 恰好已经是 1~2 亿的连续整型，所以省掉了这一步。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;bitmap_hash64()&lt;/code&gt; 把字符串直接哈希成 64 位。实测同一个 60,138 成员的集合，&lt;code&gt;bitmap_union_count(bitmap_hash64(CAST(user_id AS STRING)))&lt;/code&gt; 和 &lt;code&gt;bitmap_union_count(to_bitmap(user_id))&lt;/code&gt; 都数出 60,138，64 位哈希在这个量级没有可感知碰撞。代价是哈希值不可逆，导出明细时必须回到原 ID，等于又得留一张映射表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;坑二：重算人群包时不删旧位图，人数静默虚高 7 倍&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BITMAP_UNION&lt;/code&gt; 是聚合函数，只会把新位图并进旧的，不会剔除任何成员。让 pack 9003 重算一次——条件从 &lt;code&gt;level&amp;gt;=4&lt;/code&gt; 收紧到 &lt;code&gt;level&amp;gt;=5&lt;/code&gt;，真值从 2,097,972 降到 298,562：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 直接灌新结果，没有先删
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to_bitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 269 ms，State=OK
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_bm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9003&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 2097972
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因为新集合恰好是旧集合的子集，并完之后就是旧集合本身：&lt;strong&gt;报告人数 2,097,972，是真值的 7.03 倍，全链路没有一处错误&lt;/strong&gt;。明细形态不会有这个问题——它一行一成员，重算就是换一批行，多出来的旧成员一眼能看见。&lt;/p&gt;
&lt;p&gt;位图侧必须「先删干净再灌」：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;audience_bitmap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pack_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9003&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 118 ms
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 再灌新结果 205 ms
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;删完重灌后 &lt;code&gt;bitmap_count&lt;/code&gt; 回到 298,562，与真值一致。明细侧同一次重算是 DELETE 46 ms + INSERT 241 ms。&lt;strong&gt;两侧总耗时几乎一样（323 ms vs 287 ms），差别在漏掉那一步的后果&lt;/strong&gt;：位图侧漏 DELETE 是静默错数，明细侧漏 DELETE 会立刻以重复行的形式暴露出来。所以定时重算的调度里，删除和写入必须绑成同一个作业的两步，而不是只挂一个「跑圈选 SQL」。&lt;/p&gt;
&lt;p&gt;第三章把版本与合并的账算过，这里补一笔删除的账：&lt;code&gt;DELETE&lt;/code&gt; 只写删除标记，不回收物理空间。用 &lt;code&gt;pack_dump&lt;/code&gt; 这张 1050 万行的表做了一次干净对照——删掉一半（&lt;code&gt;WHERE user_id &amp;lt; 100000000&lt;/code&gt;，40 ms），可见行数从 10,497,584 变成 5,249,303，BE 上这个 tablet 的磁盘占用是 &lt;strong&gt;18.84 MB → 18.84 MB，一字节没少&lt;/strong&gt;。物理回收要等合并把带标记的行真正丢掉，本文的观察窗口内没有发生。所以「每天重算一次人群包」对存储的影响是累加的：白天的删除标记会一直占着空间，直到合并追上。这件事在做磁盘容量预算时得单独算一行。&lt;/p&gt;
&lt;h3 id="落地判据谁负责算谁负责交"&gt;落地判据：谁负责算，谁负责交
&lt;/h3&gt;&lt;p&gt;把本章的实测摊平，选择变成四条可判的规则：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;判断依据&lt;/th&gt;
					&lt;th&gt;走向&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;包成员 ID 是不是整型（或愿意为它建字典表）&lt;/td&gt;
					&lt;td&gt;不是整型且不想建映射：&lt;code&gt;bitmap_hash64&lt;/code&gt; 或直接退回明细表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;这批包的典型操作是计数与集合运算，还是导出明细&lt;/td&gt;
					&lt;td&gt;计数/交并差为主：位图，最多省 1.2 万倍内存；导出为主：明细表，省 49%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;有没有「放排序键」的位置可给成员 ID&lt;/td&gt;
					&lt;td&gt;有：明细表存成员本身不比位图贵（实测 1.58 vs 2.01 字节/成员）；没有：位图反超 1.8 倍&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;人数能容忍千分之几误差吗&lt;/td&gt;
					&lt;td&gt;能（只做趋势看板）：HLL，比位图小 208 倍，但只能求并&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我的落地方案是两者并存、各管一段：位图表负责「算」——预估、交并差、多层组合；明细表负责「交」——投放名单导出、快照版本留痕。两张表从同一条圈选 SQL 生成，构建耗时几乎相同（1816 ms vs 1915 ms），多存一份的收益远大于省一份的开销。真正被实测推翻的，是「用位图替掉明细表能省存储、还能提速」这个开工前的预期：&lt;strong&gt;省的是集合运算的内存，不是存储、也不是构建时间&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="五把三处不匹配收成一张表"&gt;五、把三处不匹配收成一张表
&lt;/h2&gt;&lt;p&gt;前四章是分开量下来的，但它们其实是同一件事的三个投影：&lt;strong&gt;Doris 的每一项定位能力，都建立在「事先挑出少数几列、由这几列决定数据物理顺序」之上&lt;/strong&gt;。而圈选这类负载的形状是「任意列组合 × 大值域 × 集合运算」，正好和这个前提错开。三处不匹配分别落在谓词、写入节奏、结果形态上，每一处的解法都不是调参数，而是换一个更能接住这个动作的形态。&lt;/p&gt;
&lt;p&gt;&lt;img alt="写入侧 → 存储侧 → 读取侧 → 结果侧一条链，三处错位标在各自的接缝上" class="gallery-image" data-flex-basis="157px" data-flex-grow="65" decoding="async" height="1016" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://zh.renxinblog.cn/images/gyd-c07-doris-practice-in-cdp-three-mismatches.svg" width="665"&gt;&lt;/p&gt;
&lt;h3 id="落地判断表"&gt;落地判断表
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;要定的事&lt;/th&gt;
					&lt;th&gt;看什么信号&lt;/th&gt;
					&lt;th&gt;付什么代价&lt;/th&gt;
					&lt;th&gt;本章哪一节量过&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;数据模型&lt;/td&gt;
					&lt;td&gt;「当前状态」语义 → 主键；只要历史 → 明细&lt;/td&gt;
					&lt;td&gt;主键比明细多 11% 存储（7.746 vs 6.886 GB）；换来部分列更新 27.31 s，替掉 3 分 02 秒的全量重灌&lt;/td&gt;
					&lt;td&gt;一&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;排序键前三列&lt;/td&gt;
					&lt;td&gt;哪几列&lt;strong&gt;一定&lt;/strong&gt;出现在 &lt;code&gt;WHERE&lt;/code&gt; 前面；有没有主键访问路径要保&lt;/td&gt;
					&lt;td&gt;每个位置都是排他资源：放了标签就挤掉 &lt;code&gt;user_id&lt;/code&gt;，点查与游标翻页失去前缀索引&lt;/td&gt;
					&lt;td&gt;一&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;分桶列与桶数&lt;/td&gt;
					&lt;td&gt;唯一分布均匀的列；tablet 目标大小 1G–10G&lt;/td&gt;
					&lt;td&gt;分桶列放 &lt;code&gt;user_id&lt;/code&gt; 服务均衡，圈选时一个桶都裁不掉（&lt;code&gt;tablets=32/32&lt;/code&gt;）；桶切碎多付 2% 存储&lt;/td&gt;
					&lt;td&gt;一&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;给哪几列建索引&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;进条件频率 × 单值命中占比&lt;/strong&gt;两个维度；取值在页间有无偏斜&lt;/td&gt;
					&lt;td&gt;每列一次 &lt;code&gt;BUILD INDEX&lt;/code&gt; 约 13 分钟、约 412 MB；无偏斜列上 bloom filter 白付 131 MB&lt;/td&gt;
					&lt;td&gt;二&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;导入批大小与提交方式&lt;/td&gt;
					&lt;td&gt;同一 tablet 上的&lt;strong&gt;并发&lt;/strong&gt;导入数；版本增速 vs 合并追得上的速度&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;async_mode&lt;/code&gt; 省 30 倍版本但延迟 12 秒可见；串行 + &lt;code&gt;sync_mode&lt;/code&gt; 是最坏组合&lt;/td&gt;
					&lt;td&gt;三&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;人群包存成什么形态&lt;/td&gt;
					&lt;td&gt;这批包接下来的典型动作是计数还是交付&lt;/td&gt;
					&lt;td&gt;位图集合运算最多省 1.2 万倍内存，但导出多付 49%、构建不省、重算漏删静默虚高 7 倍&lt;/td&gt;
					&lt;td&gt;四&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这张表里唯一跨行通用的判据是「命中占比」那一列。它同时决定了索引值不值得建（第二章：0.2% 的列省 499 倍，35% 的列一分不省）、决定了位图值不值（第四章：密度 5% 时它退化成 2 字节/值的数组容器，压不出东西），也决定了明细表划算不划算（同一个 ID 放进排序键 1.58 字节/行，挪出来 3.56 字节/行）。&lt;strong&gt;先看数据的分布，再挑引擎的特性&lt;/strong&gt;，顺序反过来就是白付。&lt;/p&gt;
&lt;h3 id="变慢之后的提问顺序"&gt;变慢之后的提问顺序
&lt;/h3&gt;&lt;p&gt;圈选查询慢下来时，按这个顺序问，每一步都有确定的取证动作，不要从「加索引」开始猜。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：多读了什么&lt;/strong&gt;。先拿扫描量，别拿耗时——同一台机器上同一条查询冷跑 309 ms、热跑 33 ms，耗时不能当证据。扫描量在 query profile 里，2.1 的取数路径三步（&lt;code&gt;SHOW PROFILELIST&lt;/code&gt; 在 2.1.11 直接报语法错，走 HTTP 接口）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enable_profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 跑一次那条圈选，然后从 FE 审计日志里拿它的 QueryId
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tail -5 /opt/apache-doris/fe/log/fe.audit.log &lt;span class="p"&gt;|&lt;/span&gt; grep -o &lt;span class="s1"&gt;&amp;#39;QueryId=[a-f0-9-]*&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -s -u root: &lt;span class="s2"&gt;&amp;#34;http://127.0.0.1:8030/api/profile?query_id=&amp;lt;QueryId&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s1"&gt;&amp;#39;RawRowsRead|RowsRead&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两个数对照着看：&lt;code&gt;RawRowsRead&lt;/code&gt; 是从存储层真实读出的行数，&lt;code&gt;RowsRead&lt;/code&gt; 是过滤后交给上层的行数。两者的比值就是本章反复出现的那个「放大倍数」，第二章那条 499× 就是这么量出来的。审计日志里那个 &lt;code&gt;ScanRows&lt;/code&gt; 别拿来算放大——第二章那条「读 3000 万、留 182.6 万」的组合圈选，审计里 &lt;code&gt;ScanRows&lt;/code&gt; 报的是 &lt;strong&gt;1,825,511&lt;/strong&gt;，和 &lt;code&gt;RowsRead&lt;/code&gt; 几乎相等。它统计的是过滤后交给上层的行数，不是从存储层真实读出的行数；照它算，任何查询都不存在读放大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：多算了什么&lt;/strong&gt;。比值正常（1~3 倍）但耗时高、内存高，问题在算子层。峰值内存看审计日志的 &lt;code&gt;PeakMemoryBytes&lt;/code&gt;：第四章的 &lt;code&gt;count(DISTINCT user_id)&lt;/code&gt; 求并吃了 1.39 GB，同一结果用位图只要 0.12 MB。&lt;strong&gt;OLAP 查询的内存几乎总是「把 N 万个值装进哈希表」这件事&lt;/strong&gt;，Java 侧的直觉在这里完全适用：先看聚合与连接的基数，再看扫描。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：计划为什么这么选&lt;/strong&gt;。前两步定位到列或算子之后，用 &lt;code&gt;EXPLAIN&lt;/code&gt; 确认引擎是不是真的用上了你以为它在用的能力。同一张表上两种查询的计划尾部，对照着读：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXPLAIN SELECT count(*) FROM member_profile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WHERE tenant_id = 1 AND city_code = 101;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; PREDICATES: ((tenant_id[#0] = 1) AND (city_code[#2] = 101))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AND (__DORIS_DELETE_SIGN__[#42] = 0)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; partitions=1/1 (member_profile)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tablets=32/32, tabletList=14134,14136,14138 ... -- 一个桶都没裁掉
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; cardinality=200000015
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXPLAIN SELECT level FROM member_profile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WHERE tenant_id = 1 AND user_id = 12345678;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tablets=1/32 -- 分桶列进条件了，裁到 1 个桶
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;PREDICATES&lt;/code&gt; 那几行告诉你谓词被推到了扫描节点上（以及主键表必然带的 &lt;code&gt;__DORIS_DELETE_SIGN__&lt;/code&gt; 过滤），&lt;code&gt;partitions&lt;/code&gt; 与 &lt;code&gt;tablets&lt;/code&gt; 的 &lt;code&gt;已选/总数&lt;/code&gt; 告诉你裁剪有没有发生，&lt;code&gt;cardinality&lt;/code&gt; 是优化器以为自己扫多少。&lt;strong&gt;这三个数没有任何一个能从耗时里看出来&lt;/strong&gt;，而本章的每一次「明明加了却没生效」都是在这里被抓到的：bloom filter 加了、&lt;code&gt;RawRowsRead&lt;/code&gt; 一行没少；倒排索引建了、命中面一放大又回到全读。&lt;strong&gt;生效与否只能用扫描量和计划文本证明，不能由「DDL 返回 OK」证明&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;写入侧变慢是另一条链，顺序也一样：&lt;code&gt;SHOW TABLETS FROM 表&lt;/code&gt; 看每个 tablet 的 &lt;code&gt;Version&lt;/code&gt; 与 &lt;code&gt;VersionCount&lt;/code&gt; → BE 日志搜 &lt;code&gt;compaction&lt;/code&gt; 与该 tablet id，确认合并追不追得上 → 导入返回 JSON 里的时间分解确认卡在哪一段——本机一次真实导入的返回给了这几个字段：&lt;code&gt;BeginTxnTimeMs&lt;/code&gt; 41、&lt;code&gt;StreamLoadPutTimeMs&lt;/code&gt; 10（FE 建导入计划）、&lt;code&gt;ReceiveDataTimeMs&lt;/code&gt; 54、&lt;code&gt;WriteDataTimeMs&lt;/code&gt; 26、&lt;code&gt;CommitAndPublishTimeMs&lt;/code&gt; 23，总计 &lt;code&gt;LoadTimeMs&lt;/code&gt; 103。&lt;/p&gt;
&lt;p&gt;本章那个 &lt;code&gt;max_tablet_version_num = 2000&lt;/code&gt; 的坑就是在这条链上发现的：&lt;strong&gt;阈值判的是未合并的 rowset 数，不是版本号&lt;/strong&gt;，所以「灌 500 次一定撞 -235」这类推演不成立，别把它写进容量文档。&lt;/p&gt;
&lt;h3 id="三件可以直接带走的事"&gt;三件可以直接带走的事
&lt;/h3&gt;&lt;p&gt;第一，&lt;strong&gt;口径先于数字&lt;/strong&gt;。这篇文章里所有耗时都取 FE 审计日志的 &lt;code&gt;Time(ms)&lt;/code&gt;、所有扫描量都取 query profile、所有存储都在 BE 上直接量目录——因为这三类数字在 Doris 里各有两三个看似可用的来源，而它们互相矛盾（&lt;code&gt;SHOW DATA&lt;/code&gt; 有分钟级延迟、审计 &lt;code&gt;ScanRows&lt;/code&gt; 不可信、shell 墙钟被建连开销吃掉 40 ms）。没有固定口径，前后两次测量就没有可比性，调优会变成随机漫步。&lt;/p&gt;
&lt;p&gt;第二，&lt;strong&gt;任何「加了 X 就会更快」的判断，都要在同一份数据上跑一遍对照&lt;/strong&gt;。本文写到这里，被实测推翻的直觉有四个：位图省存储（不省，明细放排序键更省）、位图让圈选更快（构建慢 5%~40%，省的是集合运算的内存）、bloom filter 适用于高基数等值列（无偏斜时一行都跳不掉）、桶切得多并行更好（tablet 碎到 248 MB，官方建议区间是 1G–10G）。四个都来自「合理但错」的通用经验，共同点是&lt;strong&gt;它们跳过了数据的实际分布&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第三，&lt;strong&gt;形态选择比参数调整更值钱&lt;/strong&gt;。这台机器上没有一个数字是靠调 BE/FE 参数得到的：换模型省了全量重灌、换排序键位置换来了游标翻页、换索引形态把 499 倍降到 1 倍、换人群包形态把 1.39 GB 降到 0.12 MB。参数当然有用，但圈选场景里最大的一笔收益始终在「这一份数据以什么形状落在磁盘上」——这也正是上一章那些机制（有序、版本、列编码、分桶）在使用侧的兑付方式。&lt;/p&gt;</description></item></channel></rss>