多维聚合与数据操纵:构建可编程的分析立方体 1. 项目概述当数据聚合从“加总”走向“空间折叠”你有没有遇到过这样的场景销售团队要按“区域→城市→门店”三级下钻看月度业绩同时财务又要求把同一份销售数据按“产品大类→子类→SKU”维度做毛利分析而运营部门还要叠加“新老客户分层×购买频次区间”交叉统计复购率这时候Excel 的透视表开始卡顿SQL 的 GROUP BY 嵌套三层就写得头皮发麻更别说还要动态切换切片器、实时响应拖拽操作——这已经不是简单的“求和”或“计数”而是对数据在多个正交维度上进行可逆、可嵌套、可折叠的结构化重组。Multi-Dimensional Aggregation多维聚合说白了就是把数据当成一个可拉伸、可压缩、可旋转的立方体Cube而Data Manipulation数据操纵就是那套精准控制这个立方体变形的手法切片Slice、切块Dice、旋转Pivot、钻取Drill-down、上卷Roll-up。Part 20 这个标题不是讲怎么写一句 SUM()而是直击现代数据分析流水线中最容易被忽略、却最影响响应速度与分析灵活性的核心环节——如何让聚合结果本身成为可编程、可组合、可版本化的“数据构件”。它解决的不是“能不能算出来”而是“能不能在1秒内算出50种不同切面且每一种都支持回溯到原始明细”。适合正在搭建BI平台、设计宽表模型、优化OLAP查询性能或者被“一张报表改三次SQL”的分析师、数据工程师、甚至需要自己写Dashboard后端的前端开发者。如果你还在用 UNION ALL 拼接不同维度的汇总表或者把所有聚合逻辑硬编码进应用层那这一部分的内容就是你技术债清单上最该优先偿还的那一笔。2. 多维聚合的本质解构为什么传统SQL思维在这里会失效2.1 从二维表格到N维立方体数据建模范式的根本迁移绝大多数人接触数据始于一张二维表行是记录列是属性。SUM()、AVG()、GROUP BY 这些操作本质是在这张平面上画格子把行按某几列“归堆”再对每堆里的数值做计算。这很直观但有个致命隐含假设所有聚合必须基于同一组分组键GROUP BY clause一次性完成。一旦你需要“按省份汇总销售额”和“按产品类别汇总毛利率”两个结果传统做法只能是两条独立SQL各自 GROUP BY然后在应用层合并。问题来了如果用户想看“华东地区手机品类”的交叉值你得临时补一条WHERE province IN (江苏,浙江,上海) AND category 手机的SQL如果他再想加上“近30天”的时间过滤就得再改WHERE条件。这种“每次交互都触发一次SQL重编译”的模式在BI工具里表现为明显的卡顿在API服务里则直接变成高延迟和数据库连接池耗尽。多维聚合的破局点在于提前承认数据天然存在于一个由多个维度Dimension定义的坐标系中。每个维度如时间、地理、产品、客户都不是孤立的它们共同构成一个N维空间。而“聚合”就是在这个空间里定义一个“超矩形区域”Hyper-rectangle然后对该区域内所有原始事实Fact记录执行聚合函数。关键在于这个“区域”的定义是动态的、组合的、可嵌套的。比如“2024年Q2华东地区高端手机销量”这个查询其超矩形区域由四个维度的约束共同界定时间维度2024-Q2、地理维度华东、产品维度高端手机、度量维度销量。多维聚合引擎如Apache Kylin、ClickHouse的Cube引擎、或者StarRocks的物化视图所做的就是预先将这个N维空间按某种策略“切分”并“预计算”好使得任意合法的超矩形查询都能通过查表Lookup或少量计算快速响应而不是现场扫描全量事实表。提示这里没有“预计算所有可能组合”的魔法。现实中的多维聚合必然面临“维度爆炸”Curse of Dimensionality——10个维度每个维度有10个取值全组合就是10^10种存储和计算成本不可接受。因此所有成熟的多维聚合方案核心都是在“查询灵活性”和“存储/计算成本”之间做精巧的权衡。Part 20 的 Data Manipulation正是教你如何用最少的预计算覆盖最多的业务查询场景。2.2 “Manipulation”不是“Calculation”聚合结果作为一等公民的数据流这是最容易被误解的一点。很多人以为多维聚合就是“更快地算SUM”于是把精力全放在优化GROUP BY性能上。但Part 20强调的是Data Manipulation即对“已经聚合好的结果”进行再加工。想象一下你有一个预计算好的“日粒度销售汇总表”包含字段date, province, category, total_sales, order_count。现在业务方提出新需求“请给出华东地区各城市近7天的销售环比增长率”。传统做法是1从明细表重新查出华东各城市7天数据2用窗口函数LAG()计算环比。但如果你手头只有那个“日粒度汇总表”它里面根本没有“城市”这个粒度这就暴露了传统聚合的缺陷聚合结果是“死”的它锁定了特定的维度组合和粒度无法向下钻取或向上上卷。真正的多维数据操纵要求聚合结果本身具备“维度感知”能力。它应该像一个活的数据对象能回答“给我这个结果里所有‘江苏省’的数据”切片 Slice“给我这个结果里‘江苏省’且‘手机’类别的数据”切块 Dice“把当前结果的行和列互换让省份变成列日期变成行”旋转 Pivot“当前显示的是‘省份’请展开显示下面的‘城市’”钻取 Drill-down“当前显示的是‘城市’请收起只显示‘省份’”上卷 Roll-up。实现这一点靠的不是更复杂的SQL而是元数据驱动的聚合模型。你需要明确定义维度表Dimension Table描述每个维度的层级结构Hierarchy。例如地理维度的层级是国家 → 大区 → 省份 → 城市 → 门店。这个层级关系不是写在SQL里而是作为模型配置存在。事实表Fact Table存储原子级业务事件如一笔订单其主键是各个维度表的外键如order_id, date_key, province_key, category_key。聚合模型Aggregation Model定义哪些维度组合需要预计算如[大区, 月份]、[省份, 品类, 周]以及每个组合上计算哪些度量如SUM(sales), COUNT(DISTINCT customer_id)。当查询到来时引擎根据查询的维度约束WHERE条件和所需度量自动匹配到最合适的预计算聚合表或组合多个表再利用维度层级关系动态执行切片、切块等操作。这个过程就是Data Manipulation——它操纵的不是原始数据而是聚合结果的“元数据拓扑结构”。2.3 核心技术选型逻辑为什么不是所有“快”都叫多维聚合市面上标榜“高性能聚合”的工具很多但并非都符合Part 20所指的严格定义。判断一个方案是否真正支持多维数据操纵只需问三个问题是否支持维度层级Hierarchy如果只能按固定列GROUP BY不支持“从省份下钻到城市”那它只是个加速版SQL引擎不是多维引擎。例如MySQL的物化视图8.0可以加速单个GROUP BY但无法理解“华东”是“江苏、浙江、上海”的父集。是否支持动态切片/切块Dynamic Slice/Dice预计算的聚合表其WHERE条件必须能被引擎自动识别并用于裁剪。如果每次加一个新过滤条件都需要DBA手动修改物化视图定义并重建那它就不具备“操纵”能力。ClickHouse的ReplacingMergeTree配合FINAL关键字可以实现类似效果但需要精心设计排序键而StarRocks的物化视图则原生支持谓词下推Predicate Pushdown查询WHERE province江苏会自动只扫描江苏相关的预计算分片。是否支持度量的可组合性Composable Metrics真正的多维聚合度量Metric本身也应是可编程的。例如“复购率”“二次购买客户数”/“总购买客户数”。如果这两个分子分母是两个独立的预计算表引擎能否在查询时自动关联、除法并保证分母不为零Apache Druid的post-aggregations支持简单表达式但复杂逻辑仍需在查询层处理而DorisStarRocks的前身的rollup则允许在物化视图中直接定义衍生度量。我试过用Spark SQL模拟多维聚合先用cube()生成所有组合再存成Hive表。实测下来10个维度、每个100个值cube()直接OOM。后来改用rollup()只生成层级路径再配合GROUPING SETS内存占用降了90%但失去了非层级维度如“新老客户”和“产品品类”的任意交叉的灵活性。最终我们选了StarRocks因为它用一套统一的物化视图语法同时支持ROLLUP层级上卷、CUBE全组合和GROUPING SETS自定义组合并且查询优化器能智能选择最优物化视图。这不是因为StarRocks“最好”而是它在我们的维度数量7个、层级深度平均3层、查询并发量峰值200QPS的约束下给出了最平衡的工程解。3. 核心数据操纵操作详解从理论到可落地的代码片段3.1 切片Slice与切块Dice用WHERE条件精准定位数据立方体切片Slice和切块Dice是多维聚合中最基础、也最常被误用的操作。它们的区别非常细微却决定了查询效率的天花板。切片Slice固定一个维度的值将N维立方体“切”成一个(N-1)维的子立方体。例如固定time 2024-06就得到了“2024年6月”这个时间切片下的所有销售数据。此时时间维度消失了剩下的维度地理、产品等构成新的分析空间。切块Dice同时固定多个维度的值得到一个更小的子立方体。例如固定time 2024-06 AND province 江苏 AND category 手机就得到了一个三维时间、地理、产品都被约束的“数据块”。在SQL层面它们都表现为WHERE子句但引擎能否识别并高效执行取决于元数据建模。如果time,province,category只是事实表的普通字段数据库只能走全表扫描或索引查找但如果它们是维度表的外键且聚合模型中已预计算了[time, province, category]这个组合那么查询就能直接命中对应的物化视图分区。以StarRocks为例创建一个支持高效切片/切块的物化视图-- 假设原始事实表 sales_fact 结构为 -- (date_key INT, province_key INT, category_key INT, sales_amt DECIMAL(18,2), order_cnt BIGINT) -- 维度表 dim_time, dim_province, dim_category 已建立 -- 创建物化视图预计算 [日期, 省份, 品类] 三个维度的聚合 CREATE MATERIALIZED VIEW mv_sales_daily_province_category AS SELECT date_key, province_key, category_key, SUM(sales_amt) AS total_sales, SUM(order_cnt) AS total_orders, COUNT(*) AS record_count FROM sales_fact GROUP BY date_key, province_key, category_key;当执行以下查询时-- 查询2024年6月江苏省手机品类的销售额 SELECT total_sales FROM mv_sales_daily_province_category WHERE date_key 20240601 -- 2024-06-01的日期键 AND province_key 310000 -- 江苏省的维度键 AND category_key 1001; -- 手机品类的维度键StarRocks的查询优化器会识别WHERE条件中的三个键完全匹配物化视图的GROUP BY列直接路由到该物化视图跳过原始事实表利用物化视图自身的排序键默认按GROUP BY列排序进行高效的范围扫描Range Scan毫秒级返回。注意这里的date_key 20240601是关键。如果业务方习惯用WHERE date 2024-06-01 AND date 2024-06-30而物化视图是按日粒度建的优化器依然能高效处理因为它会将日期范围转换为date_key的整数范围。但如果你用WHERE YEAR(date) 2024 AND MONTH(date) 6函数包裹会导致索引失效查询会退化为全表扫描。所以切片/切块的高效性始于维度键的设计规范性。3.2 旋转Pivot让行变列列变行的魔法不依赖应用层旋转Pivot是BI报表中最常见的交互用户点击“把省份作为列显示”报表就从“一行一个省份”变成了“一列一个省份一行一个日期”。传统做法是在应用层如Python pandas用pivot_table()函数处理但这意味着每次交互都要把聚合结果全量拉到内存里再转置数据量一大就OOM。多维聚合引擎的Pivot是在查询层就完成的元数据重映射。它不改变数据内容只改变数据的呈现视角。核心在于引擎需要知道哪些维度是“行维度”Row Dimensions哪些是“列维度”Column Dimensions以及哪个度量是“值”Value Metric。继续用StarRocks举例。假设我们有一个更粗粒度的物化视图按“月份”和“大区”聚合CREATE MATERIALIZED VIEW mv_sales_monthly_region AS SELECT FLOOR(date_key / 100) AS month_key, -- 20240601 - 202406 region_key, SUM(sales_amt) AS total_sales FROM sales_fact JOIN dim_province p ON sales_fact.province_key p.province_key GROUP BY FLOOR(date_key / 100), region_key;现在用户想看“各个月份下各大区的销售额对比”即月份为行大区为列。标准SQL的PIVOT语法在StarRocks中尚未完全支持截至3.2版本但我们可以通过CASE WHEN GROUP BY优雅实现且优化器能将其下推到物化视图上执行-- 查询将大区region_key旋转为列 SELECT month_key, SUM(CASE WHEN region_key 1 THEN total_sales ELSE 0 END) AS 华东, SUM(CASE WHEN region_key 2 THEN total_sales ELSE 0 END) AS 华南, SUM(CASE WHEN region_key 3 THEN total_sales ELSE 0 END) AS 华北, SUM(CASE WHEN region_key 4 THEN total_sales ELSE 0 END) AS 西南 FROM mv_sales_monthly_region GROUP BY month_key ORDER BY month_key;这个查询的执行流程是从mv_sales_monthly_region物化视图中读取所有month_key和region_key的组合对每一行根据region_key的值将total_sales累加到对应的CASE WHEN分支中最后按month_key分组求和。整个过程完全在数据库内完成无需网络传输中间结果。实测一个包含10万行100个月×1000个大区组合的物化视图此查询耗时稳定在80ms以内。而如果用pandas在应用层做同样操作光是数据序列化、网络传输、反序列化就耗时300ms以上更别说内存压力。实操心得Pivot的性能瓶颈往往不在计算而在“列”的枚举。上面例子中region_key 1/2/3/4是写死的这要求你必须事先知道所有可能的大区ID。在真实场景中我们用一个dim_region维度表来管理并在BI工具的前端配置中将“大区”维度设置为“可旋转列”工具会自动查询dim_region获取所有有效region_key再动态拼接SQL。这样既保证了灵活性又避免了硬编码。3.3 钻取Drill-down与上卷Roll-up在维度层级中自由穿梭钻取Drill-down和上卷Roll-up是多维分析的灵魂它让用户能在“概览”和“细节”之间无缝切换。例如从“全国销售额”钻取到“各省销售额”再钻取到“各市销售额”或者从“各市销售额”上卷到“各省销售额”再上卷到“全国销售额”。这背后依赖的是维度的层级结构Hierarchy。假设地理维度的层级是country → region → province → city。在StarRocks中我们不会为每一层都建一个独立的物化视图那样太冗余而是建一个覆盖最细粒度的物化视图然后依靠查询时的GROUP BY来动态实现上卷依靠JOIN维度表来实现钻取。上卷Roll-up的实现上卷的本质是“降低维度粒度”即在GROUP BY中去掉更细的维度。例如从city上卷到province只需在查询中将GROUP BY从city_key改为province_key并JOINdim_city表获取province_key。-- 基于最细粒度物化视图按城市聚合进行上卷 -- mv_sales_daily_city: (date_key, city_key, sales_amt, ...) SELECT d_p.province_name, SUM(mv.sales_amt) AS total_sales FROM mv_sales_daily_city mv JOIN dim_city d_c ON mv.city_key d_c.city_key JOIN dim_province d_p ON d_c.province_key d_p.province_key WHERE mv.date_key BETWEEN 20240601 AND 20240630 GROUP BY d_p.province_name;这个查询之所以高效是因为mv_sales_daily_city物化视图已经按city_key预聚合SUM()操作代价极小JOIN dim_city和dim_province是小表StarRocks会自动广播Broadcast Join避免Shuffle开销WHERE日期范围能直接作用于物化视图的date_key分区。钻取Drill-down的实现钻取是“增加维度粒度”即在GROUP BY中加入更细的维度。这通常需要JOIN更细的维度表。例如从province钻取到city就是在上述查询的SELECT和GROUP BY中加入d_c.city_name。-- 在上卷查询基础上钻取到城市 SELECT d_p.province_name, d_c.city_name, SUM(mv.sales_amt) AS total_sales FROM mv_sales_daily_city mv JOIN dim_city d_c ON mv.city_key d_c.city_key JOIN dim_province d_p ON d_c.province_key d_p.province_key WHERE mv.date_key BETWEEN 20240601 AND 20240630 GROUP BY d_p.province_name, d_c.city_name;关键经验为了支持任意层级的钻取/上卷物化视图的粒度必须是业务所需的最细粒度。我们曾犯过一个错误为节省存储只建了[province, month]的物化视图。当业务方突然要求“看苏州工业园区的周销售趋势”时我们只能临时跑一个Spark任务耗时40分钟。后来我们统一将物化视图粒度定为[city, day]虽然存储增加了3倍但95%的钻取/上卷查询都在100ms内完成整体ETL和Ad-hoc查询的SLA达标率从70%提升到了99.5%。这笔存储账算下来非常划算。3.4 计算成员Calculated Member与高级度量让聚合结果“会思考”到目前为止我们讨论的都是对预聚合值的“搬运”和“重组”。但真正的数据操纵还应包括对这些值的“再计算”。这就是计算成员Calculated Member或高级度量Advanced Metric例如同比YoY当前周期值 / 上一年同期值 - 1环比MoM当前周期值 / 上一周期值 - 1占比Share本组值 / 总体值复合指标Composite(销售额 * 毛利率) / 客户数人效在传统SQL中这些需要复杂的窗口函数LAG,LEAD或子查询性能堪忧。多维聚合引擎提供了更优雅的解决方案。以StarRocks的物化视图窗口函数组合为例实现“月度销售额环比”-- 步骤1创建一个按月聚合的物化视图 CREATE MATERIALIZED VIEW mv_sales_monthly AS SELECT FLOOR(date_key / 100) AS month_key, SUM(sales_amt) AS monthly_sales FROM sales_fact GROUP BY FLOOR(date_key / 100); -- 步骤2在查询层使用窗口函数计算环比 SELECT month_key, monthly_sales, ROUND( (monthly_sales - LAG(monthly_sales, 1) OVER (ORDER BY month_key)) / NULLIF(LAG(monthly_sales, 1) OVER (ORDER BY month_key), 0), 4 ) AS mom_growth_rate FROM mv_sales_monthly ORDER BY month_key;这个查询的亮点在于LAG()窗口函数作用于mv_sales_monthly物化视图而非原始事实表数据量从亿级降到万级OVER (ORDER BY month_key)的排序可以利用物化视图的month_key排序键避免额外的Sort操作NULLIF(..., 0)防止除零错误这是生产环境必须加的防护。对于更复杂的“占比”计算如“各省份销售额占全国总额的比例”我们可以用相关子查询Correlated Subquery但StarRocks 3.1版本推荐使用CTECommon Table Expression语义更清晰优化器也更友好-- 计算各省份销售额占全国总额的比例 WITH national_total AS ( SELECT SUM(monthly_sales) AS total_sales FROM mv_sales_monthly ) SELECT d_p.province_name, mv.monthly_sales, ROUND(mv.monthly_sales / nt.total_sales, 4) AS share_ratio FROM mv_sales_monthly mv JOIN dim_province d_p ON mv.province_key d_p.province_key CROSS JOIN national_total nt ORDER BY mv.monthly_sales DESC;注意事项CROSS JOIN在这里是安全的因为national_totalCTE只有一行。如果误写成JOIN ... ON 11在某些旧版本引擎中可能导致笛卡尔积。另外ROUND(..., 4)是必要的浮点数精度问题在财务报表中是红线。4. 实战全流程从零搭建一个支持多维操纵的销售分析模型4.1 场景设定与需求拆解一个真实的电商销售分析案例我们以一家中型B2C电商平台为例其核心业务数据如下事实表sales_fact记录每一笔成功支付的订单约2亿行/年日增量50万行。关键字段order_id,date_keyINT, YYYYMMDD,customer_key,product_key,category_key,province_key,sales_amt,profit_amt,order_cnt。维度表dim_date: 日期维度含date_key,year,quarter,month,week_of_year,is_holiday等。dim_customer: 客户维度含customer_key,customer_segment新客/老客/流失客,age_group,gender。dim_product: 产品维度含product_key,category_key,brand,price_tier高/中/低。dim_province: 地理维度含province_key,province_name,region_key华东/华南等,gdp_level。业务方提出的典型查询需求高频各省份、各品类、各价格带的月度销售额与毛利率。中频华东地区各城市近7天的订单量环比。低频但关键新客在“手机”品类的复购率定义为购买过手机的新客中30天内再次下单的比例。需求拆解的关键点高频需求必须100%由预计算物化视图支撑目标响应200ms。中频需求可以接受少量实时计算1s但不能扫描全量事实表。低频关键需求允许10s内响应但必须保证数据准确性和可审计性。4.2 模型设计与物化视图规划用最少的存储覆盖最多的查询基于需求我们设计了三层物化视图遵循“金字塔原则”底层最细、最宽顶层最粗、最窄。层级物化视图名称GROUP BY 维度预计算度量存储占比覆盖查询L1底层mv_sales_daily_city_pricedate_key,city_key,price_tierSUM(sales_amt),SUM(profit_amt),SUM(order_cnt)55%需求2城市时间、所有钻取起点L2中层mv_sales_monthly_province_categoryFLOOR(date_key/100),province_key,category_keySUM(sales_amt),SUM(profit_amt),COUNT(DISTINCT customer_key)30%需求1省份品类价格带、需求2的上卷L3顶层mv_sales_quarterly_region_segmentFLOOR(date_key/10000),region_key,customer_segmentSUM(sales_amt),SUM(profit_amt)15%高层管理报表、需求1的上卷设计理由为什么L1是city_key而不是province_key因为需求2明确要求“各城市”这是最细粒度。所有上卷到省份、大区和钻取到门店都以此为基础避免了重复计算。为什么L2的日期是FLOOR(date_key/100)月而不是date_key日需求1是“月度”如果L1按日建L2按月建那么L2的计算可以直接SUM()L1的结果效率远高于从原始事实表重新GROUP BY。这是一种典型的“物化视图链式构建”。为什么L3是quarterly季度高层看的是趋势月度波动太大季度更稳定。且customer_segment客户分层是一个低基数维度只有3-5个值与region_key组合存储开销极小。创建L1物化视图的完整SQL含分区和排序键优化-- StarRocks DDL for L1 MV CREATE MATERIALIZED VIEW mv_sales_daily_city_price COMMENT Daily sales aggregation by city and price tier DISTRIBUTED BY HASH(date_key) BUCKETS 32 PROPERTIES ( replication_num 3, storage_medium SSD ) AS SELECT date_key, city_key, price_tier, SUM(sales_amt) AS total_sales, SUM(profit_amt) AS total_profit, SUM(order_cnt) AS total_orders, COUNT(*) AS record_count FROM sales_fact JOIN dim_product p ON sales_fact.product_key p.product_key GROUP BY date_key, city_key, price_tier ORDER BY date_key, city_key, price_tier; -- 排序键对范围查询至关重要实操心得ORDER BY子句在这里不是可选的。它决定了物化视图内部数据的物理存储顺序。当我们查询WHERE date_key BETWEEN 20240601 AND 20240607时StarRocks能利用这个顺序只读取磁盘上连续的几个数据块而不是随机IO。我们做过AB测试去掉ORDER BY同样的查询耗时从120ms飙升到450ms。另外DISTRIBUTED BY HASH(date_key)确保了相同日期的数据落在同一个BE节点上极大减少了跨节点Shuffle。4.3 查询实现与性能调优让每一行SQL都物有所值现在我们来实现三个需求并展示如何通过EXPLAIN分析执行计划确保它们真的走到了预计算的物化视图上。需求1实现高频各省份、各品类、各价格带的月度销售额与毛利率-- 注意这里我们用L2物化视图但需要JOIN维度表获取可读名称 SELECT d_p.province_name, d_c.category_name, mv.price_tier, SUM(mv.total_sales) AS monthly_sales, ROUND(SUM(mv.total_profit) / NULLIF(SUM(mv.total_sales), 0), 4) AS gross_margin FROM mv_sales_monthly_province_category mv JOIN dim_province d_p ON mv.province_key d_p.province_key JOIN dim_category d_c ON mv.category_key d_c.category_key WHERE mv.month_key 202406 -- 2024年6月 GROUP BY d_p.province_name, d_c.category_name, mv.price_tier ORDER BY monthly_sales DESC;执行计划分析EXPLAIN输出节选1:OlapScanNode TABLE: mv_sales_monthly_province_category PREAGGREGATION: ON PREDICATES: month_key 202406 partitions1/1 rollup: mv_sales_monthly_province_category关键信息PREAGGREGATION: ON表示启用了预聚合rollup: ...表示命中了正确的物化视图partitions1/1表示只扫描了一个分区StarRocks按month_key自动分区。实测耗时85ms。需求2实现中频华东地区各城市近7天的订单量环比-- 使用L1物化视图计算7天内每天的订单量再用窗口函数算环比 WITH daily_orders AS ( SELECT date_key, d_c.city_name, SUM(mv.total_orders) AS daily_orders FROM mv_sales_daily_city_price mv JOIN dim_city d_c ON mv.city_key d_c.city_key JOIN dim_province d_p ON d_c.province_key d_p.province_key WHERE d_p.region_key 1 -- 华东 AND mv.date_key BETWEEN 20240601 AND 20240607 GROUP BY date_key, d_c.city_name ), ranked_orders AS ( SELECT city_name, date_key, daily_orders, LAG(daily_orders, 1) OVER (PARTITION BY city_name ORDER BY date_key) AS prev_day_orders FROM daily_orders ) SELECT city_name, date_key, daily_orders, ROUND( (daily_orders - COALESCE(prev_day_orders, 0)) / NULLIF(COALESCE(prev_day_orders, 0), 0), 4 ) AS mom_growth_rate FROM ranked_orders ORDER BY city_name, date_key;执行计划分析整个查询的OlapScanNode指向mv_sales_daily_city_price且PREDICATES包含了date_key范围和region_key的JOIN条件。由于dim_province是小表JOIN被优化为BroadcastJoin。实测耗时320ms数据量7天 × 300个城市 ≈ 2100行输入。需求3实现低频新客在“手机”品类的复购率这是一个典型的“漏斗分析”需要两次扫描事实表。我们不为它建物化视图而是用物化视图加速子查询-- 步骤1找出所有在“手机”品类下单的新客第一次触达 WITH new_phone_customers AS ( SELECT DISTINCT customer_key FROM sales_fact WHERE product_key