多维聚合中的数据变形:从坐标系构建到立方体切片 1. 这不是简单的“分组求和”——多维聚合中的数据变形到底在动什么骨头你打开一份销售报表想看“华东地区、2023年Q3、手机品类、华为品牌”的销售额总和系统秒出结果但当你再加一列“同比上季度增长率”或者想把“华东/华南/华北”三个大区横向并排、每个区再按“手机/电脑/配件”纵向展开生成一个带行列小计的交叉表时界面卡顿了两秒导出的Excel里数字对不上——这时候问题大概率不出在数据库连接慢也不在你的SQL写错了而是在多维聚合过程中的数据变形环节被悄悄绕过去了。我做BI系统交付和数据管道调优十多年几乎每个中大型项目都会在Part 20这个节点翻车表面看是“GROUP BY加了几个字段”实际背后是一整套数据形态的强制重塑——从扁平记录流到带层级坐标的立方体切片再到可折叠、可钻取、可计算的语义网格。这里的“Data Manipulation”根本不是pandas.DataFrame.dropna()那种单点操作而是像捏陶土一样在行、列、页、时间、组织、产品等多个维度构成的拓扑空间里对原始数据块进行拉伸、压缩、旋转、投影和重采样。它直接决定你后续所有分析动作的颗粒度是否可控、指标是否可比、下钻路径是否连贯。如果你还在用SUM(sales) GROUP BY region, quarter, category就以为搞定了多维聚合那相当于只学会了用扳手拧螺丝却没搞懂液压扭矩扳手的力矩反馈机制——工具能用但一上高强度场景就打滑。本文聚焦的就是这个常被忽略的“变形阶段”它不产生最终数值却决定了数值能否被正确解读它不暴露在报表界面上却让前端每一次点击都稳如磐石。适合正在搭建OLAP模型、调试Power BI/DAX度量值、优化ClickHouse物化视图或被PivotTable“自动合并重复项”坑过的数据工程师、分析师和BI开发人员。别急着写代码先看清数据在多维空间里是怎么被掰弯又掰直的。2. 多维聚合的本质不是“分组”而是构建坐标系与定义投影规则2.1 为什么传统SQL GROUP BY在多维场景下天然失效我们先拆解一个典型误操作。假设你有一张销售明细表sales_fact含字段sale_id,region,quarter,category,brand,sales_amount,cost_amount。业务方要一个“各区域各季度各品类的销售额毛利率”。新手会写SELECT region, quarter, category, SUM(sales_amount) AS total_sales, SUM(sales_amount - cost_amount) / SUM(sales_amount) AS gross_margin FROM sales_fact GROUP BY region, quarter, category;这段SQL在技术上完全合法也能跑出结果。但它犯了一个根本性错误把多维分析降级成了单层分组统计。问题出在哪缺失层级锚点region大区和quarter季度本应是正交坐标轴但SQL结果集里它们只是并列的字符串字段系统无法识别“华东”是“华东/华南/华北”集合中的一员“Q3”属于“Q1-Q4”序列。这意味着你无法做“所有大区Q3销售额占比”因为没有全局分母也无法做“Q2→Q3环比”因为Q2数据不在同一结果集中。聚合粒度不可控当你要查看“华东地区所有季度的手机品类总销售额”时必须重新执行GROUP BY region, category而不是从已有结果中筛选。每次切换视角都要全表扫描性能雪崩。空值处理失真如果某区域某季度某品类无销售即该组合在源表中无记录SQL结果里直接消失。但业务上你需要的是“0销售额”否则同比计算会因分母缺失而报错。这就像用一张二维平面地图去导航三维山地——你能标出A点和B点的经纬度但永远算不准爬坡需要多少体力。真正的多维聚合第一步不是算数而是定义坐标系。2.2 多维空间的三要素维度、度量、层次结构一个健壮的多维聚合模型必须显式声明以下三要素缺一不可要素定义关键要求实操反例维度Dimension描述数据观察角度的分类体系如region、time、product必须有明确的成员集合如region{华东,华南,华北}和层级关系如time: year → quarter → month把region当成普通字符串字段未预定义其枚举值及父子关系度量Measure可被聚合计算的数值型指标如sales_amount、order_count必须声明聚合函数SUM/COUNT/AVG和可加性additive/non-additive对gross_margin直接SUM而它本质是非可加度量需用SUM(sales)/SUM(cost)重构层次结构Hierarchy维度内成员的上下级组织方式如product_category → product_subcategory → brand必须支持上卷roll-up和下钻drill-down操作且层级间有确定映射time维度只存quarter字段缺失year和month导致无法按年汇总我在给某零售客户重构数据模型时发现他们原来的“商品维度表”只有category_name一列没有任何ID、父类ID或层级编码。结果所有“品类TOP10”报表都只能按字符串模糊匹配当出现“手机”和“智能手机”两个名称时系统认为是两个独立品类。后来我们强制增加category_id和parent_category_id并用闭包表Closure Table预计算所有祖先路径才让“电子数码→手机→华为”这条下钻链稳定下来。维度建模不是填字段而是建路标——没有路标再快的车也开不出高速路。2.3 数据变形的核心动作从“事实表”到“立方体切片”的四步重塑多维聚合中的Data Manipulation本质是将扁平的事实表通过四步坐标映射变形为可交互的立方体切片。这四步不是理论概念而是每个OLAP引擎如Apache Kylin、Doris、甚至Excel数据透视表内部必经的流水线维度对齐Dimension Alignment将事实表中各维度字段强制映射到预定义的维度表主键。例如sales_fact.region_code→dim_region.region_id。这步解决“同义词”问题如源系统用“EC”代表华东维度表用“EastChina”并过滤掉维度表中不存在的脏数据如region_codeXXX。关键技巧务必开启“外键检查”但不要用数据库级ON DELETE CASCADE而应在ETL层用LEFT JOIN IS NULL检测保留问题记录供业务确认。层级展开Hierarchy Expansion根据维度层次结构为每条事实记录生成所有可能的上卷路径。例如一条记录{region_id101, quarter_id202303}若region有“大区→省份→城市”三级quarter有“年→季度”两级则它将同时属于(华东, 2023Q3)(江苏省, 2023Q3)(南京市, 2023Q3)(华东, 2023)(江苏省, 2023)(南京市, 2023)共6个坐标点。这步由引擎自动完成但必须预先配置好层次关系否则只会生成最细粒度组合。空值填充Null Filling对维度组合中缺失的记录注入占位符而非跳过。例如“华北地区2023Q3无手机销售”系统应生成(华北,2023Q3,手机,0,0)而非直接忽略。这是实现“稳定分母”的基础。实测经验在Doris中用UNION ALL拼接SELECT ... FROM fact和SELECT ... FROM (SELECT DISTINCT region,quarter,category FROM dim_region CROSS JOIN dim_time CROSS JOIN dim_product)再GROUP BY比依赖引擎自动补全更可控。坐标投影Coordinate Projection将上述生成的所有坐标点按查询请求的维度组合进行投影。例如用户请求“按regionquarter查看”则所有(region,quarter,category)组合被折叠category维度被上卷SUM所有子类最终输出二维矩阵。这步决定了前端看到的“表格形状”也是性能瓶颈所在——投影维度越多中间结果集越大。提示很多团队把“数据变形”等同于ETL清洗这是巨大误区。清洗解决数据质量变形解决分析语义。前者在数据入仓前完成后者在查询时动态发生或预计算。二者必须解耦否则一个业务需求变更就要重跑全量ETL。3. 核心变形操作详解旋转、折叠、重采样与跨维计算3.1 旋转Pivot把维度从行变列打破“宽表诅咒”传统宽表思维是为每个分析场景建一张表如sales_by_region_quarter、sales_by_category_brand。这导致表数量爆炸且无法应对临时分析。Pivot操作则用一套底层事实表动态生成任意行列布局。原理Pivot本质是维度角色转换。将某个维度如category的离散值作为新列名其对应度量值如sales_amount填充到该列。例如regionquartercategorysales_amount华东2023Q3手机500000华东2023Q3电脑300000华南2023Q3手机450000Pivot后变为regionquarter手机电脑华东2023Q3500000300000华南2023Q34500000关键参数与陷阱聚合函数选择Pivot必须指定如何处理同一坐标下的多条记录。SUM最常用但若原始数据已按天粒度聚合再SUM会导致重复计算。此时应改用MAX或FIRST_VALUE。空值处理Pivot后“华南-电脑”为NULL但业务需要0。必须在Pivot前用COALESCE(sales_amount, 0)填充而非Pivot后NVL——因为Pivot引擎对NULL的处理逻辑各不相同。列名动态性当category新增“智能穿戴”Pivot结果自动增加一列。但某些BI工具如旧版Tableau需手动刷新元数据否则新列不显示。我的解决方案在数据集发布时用Python脚本扫描dim_category表生成包含所有当前类别的Pivot SQL模板并纳入CI/CD流程。3.2 折叠Fold从明细到汇总控制分析颗粒度折叠是多维聚合的“呼吸感”来源。没有折叠所有分析都卡在最细粒度如单笔订单无法看到宏观趋势。折叠的两种模式显式折叠通过GROUP BY或OLAP引擎的ROLLUP关键字实现。例如SELECT region, quarter, SUM(sales_amount) FROM sales_fact GROUP BY region, quarter WITH ROLLUP;生成结果包含(华东,2023Q3)、(华东,NULL)华东合计、(NULL,2023Q3)Q3合计、(NULL,NULL)总计。WITH ROLLUP比多次UNION高效但仅支持相邻维度即GROUP BY顺序必须匹配层次顺序。隐式折叠在BI工具中拖拽维度时自动触发。如Power BI中将time维度拖入“轴”区域选择“季度”层级则自动折叠month和day。风险点若time维度表缺失quarter字段只存date则BI工具会用FORMAT(date,yyyy-QQ)计算导致无法利用索引查询变慢。必须确保维度表提供所有常用层级字段。折叠的黄金法则折叠层级必须可逆能从“华东2023Q3”下钻到“江苏省2023Q3”就不能跳过“省份”层级。折叠后度量必须语义一致SUM(sales)可折叠AVG(price)不可直接折叠需用SUM(sales*price)/SUM(sales)重算。折叠边界需业务确认财务要求“按自然月结算”则quarter必须是2023-07~09而非日历季2023-Q3。我在某项目中因未确认此点导致Q3报表在7月1日就显示“0”实际财务周期从7月5日开始。3.3 重采样Resampling时间维度的变形魔法时间是最特殊、最易出错的维度。重采样解决“不同时间粒度数据如何对齐”的问题。典型场景与方案非对齐周期对比比较“2023年7月1日-8月15日” vs “2022年同期”。直接WHERE date BETWEEN 2023-07-01 AND 2023-08-15会丢失2022年8月16-31日数据。正确做法是-- 步骤1生成日期序列覆盖两个周期 WITH date_series AS ( SELECT generate_series(2022-07-01::date, 2023-08-31::date, 1 day::interval) AS dt ), -- 步骤2左连接事实表按年份分组重采样 sales_by_day AS ( SELECT dt, EXTRACT(YEAR FROM dt) AS yr, SUM(COALESCE(sales_amount,0)) AS daily_sales FROM date_series LEFT JOIN sales_fact ON date_series.dt sales_fact.sale_date GROUP BY dt, yr ) -- 步骤3计算同比需窗口函数 SELECT *, LAG(daily_sales,365) OVER (ORDER BY dt) AS last_year_same_day FROM sales_by_day;这里generate_series是重采样的核心它强制创建连续时间轴避免数据稀疏导致的对比失真。工作日/节假日调整零售业分析常需排除周末。不能简单WHERE EXTRACT(DOW FROM date) NOT IN (0,6)因为这会破坏时间序列连续性。应先用generate_series生成全量日期再标记is_workday字段最后在聚合时用CASE WHEN is_workday THEN sales_amount ELSE 0 END参与计算。注意重采样计算量极大务必在物化视图中预计算。ClickHouse的ReplacingMergeTree引擎配合TTL可自动维护每日重采样结果查询速度提升10倍以上。3.4 跨维计算Cross-Dimensional Calculation让数据自己“对话”这是多维聚合的高阶能力——在一个查询中让不同维度组合的度量值相互运算。例如“华东Q3销售额占全国Q3总额的比例”。实现方式对比方式原理优点缺点适用场景子查询嵌套SELECT r.region, r.q3_sales / (SELECT SUM(q3_sales) FROM regional_q3) AS ratio FROM regional_q3 r逻辑清晰兼容所有SQL引擎性能差子查询重复执行低频、小数据量窗口函数SELECT region, q3_sales, SUM(q3_sales) OVER() AS total_q3, q3_sales/SUM(q3_sales) OVER() AS ratio FROM regional_q3一次扫描性能最优需引擎支持PostgreSQL/Redshift/Doris主流OLAP场景首选DAX/MDX计算成员在Power BI中新建度量值Q3_Share DIVIDE([Q3_Sales], CALCULATE([Q3_Sales], ALL(dim_region)))语义化强可随切片动态变化学习成本高调试复杂Power BI/Tableau高级分析跨维计算的致命陷阱过滤器上下文穿透错误在DAX中CALCULATE([Sales], ALL(dim_region))会清除所有region筛选但如果用户同时筛选了brandAppleALL(dim_region)不会影响brand筛选结果仍是“Apple在各区域的占比”而非“Apple在全国的占比”。必须用ALL(dim_region, dim_brand)或REMOVEFILTERS()显式声明。空值传播当某区域Q3销售额为00/total得0但业务上可能是“未开展业务”应返回BLANK()。DAX中用DIVIDE(numerator, denominator, BLANK())SQL中用NULLIF(denominator,0)。4. 实操全流程从原始数据到可交互多维报表的7个关键步骤4.1 步骤1梳理业务维度与层次耗时最长决定成败这不是技术活而是和业务方一起画地图。我坚持用白板而非文档因为可视化能暴露矛盾。例如某次梳理“客户维度”时销售说“按行业分金融、制造、零售”客服说“按服务等级分VIP、Gold、Silver”财务说“按付款方式分预付、月结、账期”。这其实是三个独立维度但原始系统全塞在customer_type一个字段里。我们当场画出三张维度表草图并约定dim_customer_industryid, industry_name, parent_industry_iddim_customer_tierid, tier_name, min_annual_spenddim_payment_termid, term_name, days_to_pay关键产出维度字典含英文名、中文名、枚举值、层级深度、业务负责人并签字确认。没有这份字典后续所有开发都是空中楼阁。4.2 步骤2构建一致性维度表不是简单JOIN而是主数据治理维度表不是从源系统SELECT *导出的。以dim_time为例我从不用数据库的EXTRACT函数实时计算而是用Python脚本生成10年完整日历import pandas as pd from datetime import datetime, timedelta def generate_calendar(start_date, end_date): dates pd.date_range(startstart_date, endend_date, freqD) df pd.DataFrame({date: dates}) df[year] df[date].dt.year df[quarter] df[date].dt.to_period(Q).dt.strftime(Q%q %Y) # Q3 2023 df[month] df[date].dt.to_period(M).dt.strftime(%Y-%m) # 2023-07 df[is_workday] ~df[date].dt.weekday.isin([5,6]) # 周六日 df[fiscal_quarter] ((df[date].dt.month - 1) // 3 1).map({1:Q1,2:Q2,3:Q3,4:Q4}) df[year].astype(str) return df calendar_df generate_calendar(2020-01-01, 2030-12-31) calendar_df.to_sql(dim_time, conengine, if_existsreplace, indexFalse)为什么必须预生成避免每次查询都计算TO_CHAR(date,YYYY-QQ)消耗CPU支持自定义财年如7月起始、节假日标记春节放假7天保证所有系统使用同一份时间定义消除“BI说Q3ERP说Q3”的扯皮同样dim_region表必须包含region_id,region_name,parent_region_id,region_level1大区,2省份,3城市并用递归CTE预计算path字段如/1/5/12/方便快速查找所有下属区域。4.3 步骤3事实表轻度聚合不是越细越好而是恰到好处事实表设计原则原子性 可加性 稳定性。原子性存储最细业务事件如“一笔订单的某个SKU发货”而非“订单总金额”。可加性所有度量必须支持SUM。discount_rate不行要存discount_amount。稳定性主键维度组合一旦生成永不变更。sale_id不能作为主键因为退货时会更新应改为sale_event_id发货、退货、换货均为独立事件。轻度聚合实践我们不对原始交易流水做全量预聚合而是按“天区域品类”做轻度聚合生成fact_sales_daily-- 每日聚合减少事实表行数90% CREATE TABLE fact_sales_daily AS SELECT DATE(sale_time) AS sale_date, region_id, category_id, COUNT(*) AS order_count, SUM(sales_amount) AS sales_amount, SUM(cost_amount) AS cost_amount, SUM(quantity) AS quantity FROM sales_raw GROUP BY DATE(sale_time), region_id, category_id;这样查询“2023年各季度销售额”时引擎只需扫描fact_sales_daily的365行而非原始表的千万行。注意轻度聚合后不能再下钻到单笔订单所以必须保留原始表sales_raw供审计使用。4.4 步骤4配置OLAP引擎的Cube以Apache Doris为例Doris的Aggregate Key模型是多维聚合的利器。我们为fact_sales_daily创建如下SchemaCREATE TABLE fact_sales_daily ( sale_date DATE COMMENT 销售日期, region_id LARGEINT COMMENT 区域ID, category_id LARGEINT COMMENT 品类ID, order_count BIGINT SUM DEFAULT 0 COMMENT 订单数, sales_amount DECIMAL(18,2) SUM DEFAULT 0 COMMENT 销售额, cost_amount DECIMAL(18,2) SUM DEFAULT 0 COMMENT 成本额, quantity BIGINT SUM DEFAULT 0 COMMENT 销量 ) AGGREGATE KEY(sale_date, region_id, category_id) DISTRIBUTED BY HASH(region_id) BUCKETS 10 PROPERTIES(replication_num 3);关键配置解析AGGREGATE KEY声明哪些字段是维度不可聚合哪些是度量自动SUM。引擎在导入时自动合并相同KEY的记录。DISTRIBUTED BY HASH(region_id)按region_id哈希分片确保同一区域数据在同个BE节点加速WHERE region_id101查询。BUCKETS 10分片数根据region_id基数设置通常region总数*3。导入数据用Stream Load将fact_sales_dailyCSV推送到Doris引擎自动完成聚合。实测10亿行数据导入聚合耗时15分钟。4.5 步骤5编写多维查询SQL不是写GROUP BY而是写坐标指令在Doris中一个典型的多维查询长这样-- 查询各区域各季度销售额及全国占比 SELECT r.region_name, t.quarter, SUM(f.sales_amount) AS q3_sales, -- 跨维计算本区域Q3占全国Q3比例 SUM(f.sales_amount) / SUM(SUM(f.sales_amount)) OVER() AS share_of_nation FROM fact_sales_daily f JOIN dim_region r ON f.region_id r.region_id JOIN dim_time t ON f.sale_date t.date WHERE t.quarter Q3 2023 GROUP BY r.region_name, t.quarter ORDER BY q3_sales DESC;为什么用SUM(SUM()) OVER()内层SUM(f.sales_amount)是按regionquarter分组聚合外层SUM() OVER()是对所有分组结果再求和得到全国Q3总额OVER()不改变行数完美实现“一行一区域同时显示本区域值和全局值”性能优化技巧在dim_time表的quarter字段上建Bitmap索引ALTER TABLE dim_time ADD INDEX idx_quarter (quarter) USING BITMAP;对高频查询的region_id和sale_date组合创建Rollup表ALTER TABLE fact_sales_daily ADD ROLLUP rollup_region_quarter (region_id, quarter, sales_amount);Rollup表只存region_idquarter粒度体积缩小80%查询提速5倍。4.6 步骤6在BI工具中构建语义层Power BI DAX实战Power BI中我们不直接连Doris而是用DAX建模赋予数据业务灵魂// 度量值Q3销售额自动响应切片器 Q3_Sales CALCULATE( SUM(fact_sales_daily[sales_amount]), dim_time[quarter] Q3 2023 ) // 度量值华东Q3占比跨维计算 EastChina_Q3_Share VAR east_china_sales CALCULATE( [Q3_Sales], dim_region[region_name] 华东 ) VAR nation_q3_sales CALCULATE( [Q3_Sales], ALL(dim_region) // 清除region筛选 ) RETURN DIVIDE(east_china_sales, nation_q3_sales, BLANK())DAX避坑指南ALL()只清除指定表的筛选ALL(dim_region, dim_time)才能清除两个维度。CALCULATE内的筛选条件是“覆盖”而非“追加”dim_time[quarter]Q3 2023会替换用户在时间切片器中的选择。如需保留用户选择改用KEEPFILTERS()。测试时务必开启“性能分析器”查看每个度量值的查询计划避免COUNTROWS(ALL(...))这类全表扫描操作。4.7 步骤7上线验证与持续监控90%的问题出在这里上线不是终点而是监控起点。我们建立三层验证机制层级验证点工具频率不合格处理数据层各维度表记录数是否与上游一致dim_region中region_level1的记录数是否等于大区总数SQL脚本Prometheus告警每日自动暂停下游任务邮件通知负责人聚合层fact_sales_daily中SUM(sales_amount)是否等于sales_raw中对应日期的SUM(sales_amount)Python校验脚本抽样10%日期每周生成差异报告定位ETL逻辑缺陷应用层BI报表中“华东Q3销售额”与财务系统导出的Excel是否一致人工比对自动化截图比对Playwright上线首周每日回滚至前一版本启动根因分析真实案例某次上线后BI报表显示“华东Q3销售额”比财务系统少2.3%。排查发现dim_region表中“江苏省”被错误归类到parent_region_id0根节点而非parent_region_id101华东导致JOIN时丢失了江苏数据。修复后用校验脚本扫描所有parent_region_id0的记录发现还有3个省份存在同样问题。教训维度表的主数据质量比事实表的聚合逻辑更重要。5. 常见问题与独家排查技巧实录5.1 问题1报表数字“看起来对但一算就错”——空值与零值混淆现象用户反馈“华北地区Q3销售额为0”但财务说华北Q3实际有50万销售。查数据库发现fact_sales_daily中确实没有华北Q3的记录。根因分析ETL流程中LEFT JOIN dim_region后未过滤region_id IS NULL的记录导致华北数据因region_code不匹配而被丢弃。更深层原因dim_region表未及时同步新区域上游系统新增“华北战区”但维度表仍用旧版。排查技巧先查维度表完整性-- 查找事实表中有、维度表中无的region_id SELECT DISTINCT region_id FROM fact_sales_daily EXCEPT SELECT region_id FROM dim_region;再查空值分布-- 统计各维度字段的NULL率 SELECT COUNT(*) FILTER (WHERE region_id IS NULL) *100.0/COUNT(*) AS region_null_pct, COUNT(*) FILTER (WHERE category_id IS NULL) *100.0/COUNT(*) AS category_null_pct FROM fact_sales_daily;提示NULL率0.1%必须告警。我们设定阈值为0.05%超过即触发自动修复流程。终极方案在ETL中加入“维度健康检查”步骤用INSERT ... SELECT ... WHERE NOT EXISTS补全缺失的维度记录region_name华北战区parent_region_id0用UPDATE fact_sales_daily SET region_id (SELECT region_id FROM dim_region WHERE region_code HB) WHERE region_id IS NULL修复事实表5.2 问题2下钻时数据“凭空消失”——层次结构断裂现象在Power BI中从“大区”下钻到“省份”华东下钻后只显示“上海”“浙江”缺失“江苏”“安徽”。根因分析dim_region表中“江苏省”的parent_region_id指向region_id999一个已删除的大区而非region_id101华东。或者dim_region未启用“闭包表”导致PATH字段未更新GET_DESCENDANTS()函数无法找到江苏。排查技巧可视化层次树用Python的anytree库生成区域树状图from anytree import Node, RenderTree # 从dim_region读取所有节点构建树 root Node(All Regions) for row in dim_region_data: parent find_node(root, row[parent_region_id]) or root Node(row[region_name], parentparent) print(RenderTree(root)) # 直观看到江苏是否挂在华东下SQL递归验证-- 查找所有“华东”下属区域含间接 WITH RECURSIVE region_tree AS ( SELECT region_id, region_name, parent_region_id FROM dim_region WHERE region_id 101 UNION ALL SELECT d.region_id, d.region_name, d.parent_region_id FROM dim_region d INNER JOIN region_tree r ON d.parent_region_id r.region_id ) SELECT * FROM region_tree;如果结果中没有江苏说明层次断裂。修复方案短期用UPDATE dim_region SET parent_region_id 101 WHERE region_name 江苏省;长期在dim_region表增加lft/rgt字段采用Modified Preorder Tree TraversalMPTT算法保证层次查询O(1)。5.3 问题3同比计算“分母错乱”——时间维度对齐失败现象2023年Q3同比显示“增长120%”但2022年Q3实际销售额更高。根因分析时间维度表dim_time中“2022-Q3”的日期范围是2022-07-01至2022-09-30但“2023-Q3”因财务周期调整为2023-07-05至2023-10-04导致天数不等92天 vs 91天且起始日不同。查询时未用dim_time的quarter_id关联而是用EXTRACT(QUARTER FROM sale_date)