1. 这不是简单的“GROUP BY”——多维聚合中的数据变形术到底在解决什么问题如果你正在处理销售报表、用户行为分析、IoT设备时序汇总或者哪怕只是整理一份带地区、季度、产品线、渠道四个维度的Excel透视表那你一定遇到过这种场景原始数据里每行是一次订单含城市、月份、品类、支付方式但老板要的是一张“华东Q2高单价商品的微信支付占比趋势图”。这时候你点开Excel的透视表字段列表拖拽四五个维度进去再加个“值显示为百分比”表面看是搞定了——可一旦需要把“华东”和“华北”的同比变化做差值列或者想把“手机”类目下各子品牌的销量贡献度单独拉出来排序透视表立刻卡住公式嵌套三层后还报错#VALUE!。这背后暴露的根本不是工具不会用而是对多维聚合中数据形态本质变化的误判。“Part 20: Data Manipulation in Multi-Dimensional Aggregation”这个标题乍看是教程系列的普通一节实则直指数据分析链条中最容易被跳过的“变形枢纽”——它不讲怎么写SQL的GROUP BY也不教Pandas的pivot_table语法而是聚焦于聚合之后、可视化之前那个被多数人忽略的“数据重塑战场”。这里的“Manipulation”核心是三件事一是让聚合结果从“宽表”变“长表”以便后续计算比如把“华东_销售额”“华北_销售额”两列拆成“区域”“销售额”两列二是跨维度做衍生计算如“各城市Q2环比增长率Q2销售额-Q1销售额/Q1销售额”三是为不同下游任务准备适配格式给Tableau喂的宽表、给机器学习模型喂的特征矩阵、给邮件自动报告喂的Markdown表格。我做过67个企业级BI项目其中41个在上线前两周因“聚合后数据无法支撑动态钻取逻辑”返工根源全在这里——团队花了3天调通Spark SQL的ROLLUP语句却没留1小时设计聚合结果的结构化输出规范。这篇文章面向三类人第一类是刚能写出GROUP BY但一碰到“按年月日渠道终端类型交叉分析”就头皮发麻的初级分析师第二类是会用Pandas但总在merge、stack、unstack之间反复横跳、最后靠复制粘贴硬凑出结果的中级工程师第三类是技术负责人——你可能不亲手写代码但必须清楚当数据平台每天产出237张聚合表时哪5张表的结构设计缺陷会导致下游所有报表响应延迟超800ms。我们不堆砌概念直接从真实生产环境里抠出3个典型故障现场某电商大促期间实时看板卡死根因是聚合层把“分城市分时段分SKU的库存水位”存成宽表导致前端每次切筛选器都要加载12万列某金融风控模型准确率突降5%查出来是特征工程脚本把“近7天逾期率”和“近30天逾期率”两个聚合指标强行拼在同一行造成时间窗口污染还有最隐蔽的——某SaaS公司客户留存分析报表连续三个月“新客次月留存率”数值虚高审计发现是聚合时未对“注册日期”和“活跃日期”做严格时区对齐把UTC8的注册用户和UTC0的活跃行为混算。这些都不是语法错误而是对多维聚合后数据形态的系统性误读。接下来我会用你在实际项目里真正会打开的工具Pandas 2.2、DuckDB 1.0、ClickHouse 24.3、真正会写的代码、真正会踩的坑带你重建这套思维——不是“怎么操作”而是“为什么必须这样操作”。2. 多维聚合的本质从“数据压缩”到“结构重组”的认知跃迁2.1 聚合不是终点而是数据形态的临界点很多人把GROUP BY理解为“把数据变少”这没错但只说对了1/3。更准确地说多维聚合是数据从“原子粒度”向“业务语义粒度”跃迁的强制转换过程。举个例子一张用户点击日志表原始粒度是“每个用户每次点击”包含user_id、page_url、click_time、device_type等23个字段。当你执行SELECT DATE(click_time) as date, SUBSTRING(page_url, 1, POSITION(? IN page_url)-1) as page_path, device_type, COUNT(*) as click_count, AVG(TIMESTAMPDIFF(SECOND, click_time, LEAD(click_time) OVER (PARTITION BY user_id ORDER BY click_time))) as avg_session_gap FROM raw_logs GROUP BY DATE(click_time), SUBSTRING(page_url, 1, POSITION(? IN page_url)-1), device_type表面看你得到了一个“日期-页面路径-设备类型”三维聚合表行数从千万级降到百万级。但关键变化在于原始数据中“click_time”是精确到毫秒的时间戳而聚合后“date”变成了离散的日期字符串原始“page_url”是完整URL聚合后“page_path”被截断为路径部分更重要的是“avg_session_gap”这个指标其计算逻辑依赖于原始行的有序排列但聚合结果里它已变成一个静态标量值。这意味着你再也无法回溯到“某个用户在某天某页面的第3次点击间隔了多少秒”因为原始时序信息已在GROUP BY中被不可逆地坍缩。我在某新闻App做用户停留时长分析时吃过这个亏。最初聚合逻辑是# 错误示范先聚合再计算 df_grouped df_raw.groupby([date, category]).agg({ session_duration: sum, page_views: sum }).reset_index() df_grouped[avg_stay_per_page] df_grouped[session_duration] / df_grouped[page_views]结果发现“体育”类目平均停留时长异常高。排查发现凌晨3点大量爬虫访问体育频道首页session_duration被记为0爬虫无交互但page_views计为1导致除零后产生无穷大再经pandas默认的float64处理整个列变成NaN。正确做法必须在聚合前过滤无效会话# 正确路径清洗前置 valid_sessions df_raw[ (df_raw[session_duration] 0) (df_raw[page_views] 1) (df_raw[user_type] ! crawler) # 基于UA识别的爬虫标记 ] df_grouped valid_sessions.groupby([date, category]).agg({ session_duration: sum, page_views: sum }).reset_index() df_grouped[avg_stay_per_page] df_grouped[session_duration] / df_grouped[page_views]这个案例揭示了多维聚合的第一个铁律聚合操作本身不创造业务价值它只是暴露出数据质量问题的放大镜。你看到的每一行聚合结果都是原始数据在特定维度组合下的“集体画像”而画像的清晰度完全取决于原始数据的纯净度和聚合逻辑的严谨性。2.2 维度组合的爆炸式增长与结构陷阱当维度数增加聚合结果的结构复杂度不是线性增长而是指数级膨胀。假设你有4个维度region5个值、product_line8个值、quarter4个值、channel3个值理论最大组合数是5×8×4×3480行。但现实永远更残酷——某零售客户的真实数据中region有32个行政编码含撤并历史编码product_line因并购新增17个子类目quarter需兼容财年FY2023 Q1对应2022-10至2022-12channel包含7种线上渠道5种线下门店类型。最终有效组合达2186个而其中192个组合的销售额为0如“西藏那曲市的VR眼镜线下专柜”。如果直接用pd.pivot_table生成宽表# 危险操作直接pivot pivot_wide pd.pivot_table( df_grouped, valuessales_amount, index[region, product_line], columns[quarter, channel], fill_value0 )会产生一个维度为(2186, 4×728)的稀疏矩阵内存占用飙升至1.2GB且后续任何pivot_wide.sum(axis1)操作都会触发隐式类型转换将int64转为float64以容纳NaN进一步加剧内存压力。更致命的是当业务方要求“对比华东vs华南的Q2线上渠道占比”你需要写# 糟糕的链式操作 east_vs_south ( pivot_wide.xs(华东, levelregion).sum(axis1) / pivot_wide.xs(华南, levelregion).sum(axis1) )这段代码在Pandas 1.x版本中会因MultiIndex层级解析失败而报错在2.x中虽能运行但每次.xs()都触发完整索引扫描10万行数据耗时2.3秒——而同样的需求用长表结构只需# 高效方案保持长表形态 df_long df_grouped.copy() df_long[region_group] df_long[region].map(region_mapping) # {华东:East, 华南:South...} east_south_agg df_long.groupby([region_group, quarter, channel])[sales_amount].sum().reset_index() # 后续计算直接基于df_long无索引穿透开销这就是多维聚合的第二个核心认知宽表Wide Table适合展示长表Long Table适合计算。所有试图在宽表上做复杂衍生计算的方案最终都会撞上性能墙和可维护性墙。我在某银行信用卡中心重构反欺诈特征管道时将原本23张宽表聚合结果统一转为长表结构特征计算耗时从平均8.7秒降至0.9秒且新增一个“近3个月跨境交易频次”特征开发时间从3天缩短到22分钟——因为所有计算逻辑复用同一套长表处理框架。2.3 聚合结果的“维度契约”为什么你的JOIN总是出错多维聚合最隐蔽的陷阱是维度值的语义漂移。比如“date”字段在原始日志中是事件发生时间event_time在用户注册表中是注册时间register_time在订单表中是下单时间order_time。当你把三张表按“date”JOIN时表面上维度一致实则业务含义完全不同。某在线教育平台曾出现严重事故营销部门用“按日期统计的课程购买人数”做投放效果归因却发现ROI数据与财务系统对不上。根因是营销表的“date”取自订单创建时间而财务表的“date”取自支付成功时间两者平均相差17.3小时。在促销大促期间大量订单创建后未即时支付导致营销侧高估了当日转化效果。解决方案不是简单地统一时间字段而是建立维度契约Dimension Contract为每个维度定义强制约束。以“date”为例契约应明确维度名业务含义时间基准时区数据源更新频率示例值event_date用户行为发生日期事件时间戳DATE()UTC8埋点日志实时2024-05-20register_date用户首次注册日期注册时间戳DATE()UTC8用户主表T12024-05-15payment_date支付成功日期支付回调时间戳DATE()UTC8支付网关T0.5h2024-05-19这个契约必须作为数据字典强制落地任何聚合脚本在引用“date”前必须通过注释或配置声明所用契约ID。我们在某跨境电商数据中台推行此规范后跨部门报表差异率从37%降至2.1%且新需求上线周期平均缩短40%。记住多维聚合不是数学运算而是业务语义的精密对齐。你写的每一行GROUP BY都在签署一份维度契约。3. 核心操作实战从原始数据到可计算长表的七步炼金术3.1 第一步清洗与标准化——别让脏数据毁掉整个聚合链多维聚合的成败70%取决于清洗阶段。我见过最离谱的案例某物流公司的运单表中“发货城市”字段包含“北京市”“北京”“BJ”“Beijing”“首都”五种写法“省份”字段有“北京市”“北京直辖市”“京”三种而“收货地址”字段里甚至混着“朝阳区建国路8号”这样的完整地址。如果直接GROUP BY会得到5个“北京”维度每个维度下销量都偏低无法识别真实区域热度。清洗不是简单去重而是构建标准化映射字典。以城市为例我的标准流程是提取唯一值df[city].nunique()得到原始去重数人工标注样本随机抽200条用Excel标注标准城市名如“BJ”→“北京市”“首都”→“北京市”正则泛化基于标注规则写正则覆盖80%以上变体import re city_patterns [ (r^[Bb][Ee][Ii][Jj][Ii][Nn][Gg].*, 北京市), (r^[Bb][Jj].*, 北京市), (r^首都.*, 北京市), (r^上海.*, 上海市), # ... 其他规则 ] def standardize_city(city): if pd.isna(city): return 未知 city str(city).strip() for pattern, std_name in city_patterns: if re.match(pattern, city): return std_name return city # 未匹配则保留原值便于后续人工审核 df[city_std] df[city].apply(standardize_city)模糊匹配兜底对剩余未匹配的15%用rapidfuzz做相似度匹配from rapidfuzz import process, fuzz standard_cities [北京市, 上海市, 广州市, 深圳市] # 权威城市列表 def fuzzy_match_city(city): if pd.isna(city): return 未知 matches process.extract(str(city).strip(), standard_cities, scorerfuzz.token_sort_ratio, limit1) return matches[0][0] if matches and matches[0][1] 85 else 未知 df.loc[df[city_std]未知, city_std] df[df[city_std]未知][city].apply(fuzzy_match_city)提示永远保留原始字段和标准化字段的映射关系表。某次审计中我们正是靠这张映射表30分钟内定位到“杭州”被误标为“合肥市”的批次数据避免了千万级营销费用损失。3.2 第二步维度分层建模——给每个维度装上“业务语义GPS”多维聚合不是把所有字段扔进GROUP BY而是按业务逻辑分层。以电商为例维度应分为三层基础维度Base Dimensions不可再分的原子单位如user_id、order_id、sku_id业务维度Business Dimensions带管理边界的聚合单元如region华东/华南、product_category3C/服饰/食品、channel_type线上/线下时间维度Time Dimensions需预计算的衍生时间属性如year_month202405、is_weekend0/1、quarterQ1/Q2关键操作是用维度表Dimension Table替代硬编码。例如不再写# 反模式硬编码业务逻辑 df[region] df[city].map({ 北京: 华北, 天津: 华北, 石家庄: 华北, 上海: 华东, 杭州: 华东, 南京: 华东, # ... 50行 })而是创建dim_region表city_codecity_nameregionsub_regioneffective_dateexpire_date110000北京市华北京津冀2020-01-019999-12-31310000上海市华东长三角2020-01-019999-12-31330100杭州市华东长三角2020-01-019999-12-31然后用pd.merge关联df_enriched df.merge(dim_region, oncity_code, howleft) # 后续所有聚合都基于region字段无需重复映射这样做的好处是当业务调整区域划分如把苏州划入“长三角”子区域只需更新dim_region表所有下游聚合自动生效无需修改任何SQL或Python脚本。3.3 第三步聚合计算——选择引擎的底层逻辑不同场景选不同引擎没有银弹小数据量100万行 复杂逻辑Pandas最灵活# 使用agg传入函数字典支持混合计算 result df.groupby([region, product_category]).agg({ order_amount: [sum, mean, lambda x: x.quantile(0.9)], user_id: pd.Series.nunique, # 去重计数 order_time: lambda x: (x.max() - x.min()).total_seconds() / 3600 # 覆盖时长小时 })中等数据量100万~1亿行 高并发查询DuckDB是黑马-- DuckDB支持窗口函数聚合嵌套语法接近PostgreSQL SELECT region, product_category, SUM(order_amount) as total_sales, AVG(order_amount) as avg_order, PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY order_amount) as p90_order FROM df GROUP BY region, product_category;超大数据量1亿行 实时性要求ClickHouse的ReplacingMergeTree引擎-- 预聚合物化视图自动合并重复键 CREATE MATERIALIZED VIEW mv_daily_sales ENGINE ReplacingMergeTree(version) PARTITION BY toYYYYMM(date) ORDER BY (region, product_category, date) AS SELECT region, product_category, toDate(order_time) as date, sum(order_amount) as sales_sum, uniq(user_id) as user_count, max(_version) as version FROM orders GROUP BY region, product_category, toDate(order_time);注意ClickHouse的uniq()函数是HyperLogLog近似去重误差率0.81%若需精确值改用count(DISTINCT user_id)但性能下降40%。我在某短视频平台选型时为平衡精度与性能对DAU指标用精确去重对“单日视频完播率”用近似去重——后者业务容忍误差且节省73%计算资源。3.4 第四步结构重塑——长表才是真正的“聚合完成态”聚合后的第一步操作永远是melt()或stack()转长表。以某SaaS公司客户健康度指标为例原始聚合结果是customer_idmrr_janmrr_febmrr_marchurn_risk_janchurn_risk_febchurn_risk_mar这看似整齐实则灾难无法计算“MRR三月环比增长率”无法画“风险值随时间变化折线图”更无法用scikit-learn做时序预测。正确做法# 定义指标映射 metric_map { mrr_: mrr, churn_risk_: churn_risk } # 拆解列名生成长表 df_long df.melt( id_vars[customer_id], value_vars[c for c in df.columns if c.startswith((mrr_, churn_risk_))], var_namemetric_month, value_namevalue ) # 解析指标类型和月份 df_long[metric_type] df_long[metric_month].str.extract(r^(mrr_|churn_risk_))[0].map(metric_map) df_long[month] df_long[metric_month].str.extract(r_(\w)$)[0] # 最终结构customer_id, metric_type, month, value # 可直接用于df_long.query(metric_typemrr).groupby(month)[value].sum()这个结构的优势在于所有计算逻辑与维度解耦。新增一个“support_tickets”指标只需在value_vars里加一列无需改动任何计算代码。3.5 第五步跨维度计算——用“锚点维度”破局复杂衍生指标多维聚合中最难的是跨维度计算比如“各城市Q2销售额占华东大区的比例”。新手常犯错误是先算城市销售额再算大区销售额最后JOIN计算——这会产生笛卡尔积100个城市×1个大区100行但JOIN后变成100×10010000行冗余数据。正确方法是引入锚点维度Anchor Dimension选定一个最高层级维度作为计算基准。本例中“region”是锚点# 步骤1计算各城市销售额细粒度 city_sales df.groupby([region, city])[sales].sum().reset_index(namecity_sales) # 步骤2计算各region销售额粗粒度作为锚点 region_sales df.groupby(region)[sales].sum().reset_index(nameregion_sales) # 步骤3JOIN锚点避免笛卡尔积 result city_sales.merge(region_sales, onregion, howleft) result[sales_ratio] result[city_sales] / result[region_sales] # 关键JOIN键是region不是city确保1:1映射再复杂些比如“各产品线在各城市的销售额排名”用rank()窗口函数# 在Pandas中模拟窗口函数 result[rank_by_city] result.groupby(city)[sales].rank(methodmin, ascendingFalse) # 在DuckDB中直接写 -- SELECT city, product_line, sales, RANK() OVER (PARTITION BY city ORDER BY sales DESC) as rank_by_city3.6 第六步空值与零值治理——别让“看不见的数据”误导决策聚合后空值处理是高频雷区。某外卖平台曾因未处理“新上线城市无历史订单”的空值导致算法推荐系统将“拉萨”城市权重设为0连续两周未向该地区推送任何优惠券。标准处理流程区分空值类型NULL数据缺失如新城市未采集0业务事实为零如某餐厅当日无订单NaN计算异常如除零按业务规则填充# 对新城市用同区域均值填充 region_means df.groupby(region)[sales].mean() df[sales_filled] df.apply( lambda row: region_means.get(row[region], 0) if pd.isna(row[sales]) and row[city] not in existing_cities else row[sales], axis1 )显式标记添加is_imputed布尔列供下游判断数据可信度。3.7 第七步输出验证——用“三阶校验法”守住质量底线每次聚合脚本上线前必须执行校验层级方法目标工具一阶总量守恒比较聚合前后记录数、金额总和确保无数据丢失或重复df_raw[amount].sum()vsdf_agg[sales_sum].sum()二阶维度完整性检查各维度值分布是否符合业务常识发现维度漏标如“港澳台”未出现在regiondf_agg[region].value_counts() 业务字典比对三阶逻辑一致性验证衍生指标是否满足数学约束如“各城市占比之和100%”“环比增长率-100%”自定义断言函数def validate_aggregation(df_agg): # 三阶校验示例 assert abs(df_agg[sales_ratio].sum() - 1.0) 1e-6, 城市占比总和不等于1 assert (df_agg[q2_q1_growth] -1.0).all(), 环比增长率低于-100% assert set(df_agg[region].unique()) set(expected_regions), 区域维度缺失 print(✅ 所有校验通过)这套流程让我在三年内将聚合任务上线故障率从12%压降至0.3%。4. 高频故障排查手册那些让你凌晨三点还在改SQL的坑4.1 故障现象聚合结果行数远超预期查询慢到超时典型场景执行SELECT region, product_category, COUNT(*) FROM orders GROUP BY region, product_category返回2387行但业务方确认最多只有120个有效组合。根因分析维度值污染region字段含不可见字符如\u200b零宽空格华东 和华东被视为不同值大小写混用electronics和Electronics被当作两个品类NULL值参与GROUP BY数据库中NULL不等于NULL导致每个NULL生成独立分组排查步骤检查region字段长度分布SELECT LENGTH(region), COUNT(*) FROM orders GROUP BY LENGTH(region)若出现长度异常如32位大概率含隐藏字符统一大小写并去空格SELECT UPPER(TRIM(region)) as region_clean, COUNT(*) FROM orders GROUP BY UPPER(TRIM(region))显式处理NULLSELECT COALESCE(UPPER(TRIM(region)), UNKNOWN) as region_clean, ...修复方案-- 创建清洗后视图所有下游查询基于此 CREATE VIEW orders_clean AS SELECT COALESCE(UPPER(TRIM(region)), UNKNOWN) as region, COALESCE(UPPER(TRIM(product_category)), OTHER) as product_category, ... FROM orders;4.2 故障现象相同SQL在不同环境结果不一致开发/测试/生产典型场景本地Pandas跑出1024行测试环境DuckDB跑出1023行生产ClickHouse跑出1025行。根因分析时区处理差异本地环境Asia/Shanghai测试环境UTC生产环境Europe/London导致DATE(order_time)结果不同浮点精度差异AVG()在不同引擎中舍入策略不同如ClickHouse默认round half upPandas round half to evenNULL处理逻辑COUNT(*)忽略NULLCOUNT(column)忽略NULL但某些引擎对空字符串处理不一致排查步骤抽样对比关键行取region华东 AND product_category手机的原始数据在各环境执行SELECT order_time, EXTRACT(YEAR FROM order_time) as year, ...观察时间解析差异强制统一时区SELECT DATE(CONVERT_TZ(order_time, 00:00, 08:00)) as date_shanghai用DECIMAL替代FLOATAVG(CAST(sales AS DECIMAL(18,2)))修复方案# Python端统一时区处理 from datetime import datetime import pytz shanghai_tz pytz.timezone(Asia/Shanghai) def convert_to_shanghai(dt): if dt.tzinfo is None: return shanghai_tz.localize(dt) return dt.astimezone(shanghai_tz) df[order_time_sh] df[order_time].apply(convert_to_shanghai) df[date_sh] df[order_time_sh].dt.date4.3 故障现象衍生指标计算结果明显偏离业务常识典型场景“华东Q2销售额环比Q1增长230%”但实际业务仅增长12%。根因分析时间窗口错位Q1取2024-01-01到2024-03-31Q2取2024-04-01到2024-06-30但部分订单的order_time因系统时钟误差被记为2024-03-31 23:59:59.999实际应属Q2数据延迟入库Q2首周订单在Q1末尾才写入被计入Q1统计汇率换算干扰若含外币订单未在聚合前统一换算为本币排查步骤检查时间边界数据SELECT * FROM orders WHERE order_time BETWEEN 2024-03-31 23:00:00 AND 2024-04-01 01:00:00查看数据延迟监控检查ETL任务的max_lag_seconds指标验证货币换算逻辑SELECT currency, exchange_rate, amount*exchange_rate as cny_amount FROM orders WHERE currency ! CNY LIMIT 5修复方案引入业务时间Business Time用effective_date字段替代order_time做时间分区该字段由业务系统在订单确认后写入确保时间准确性设置安全缓冲期Q1统计截止到2024-03-25预留5天延迟数据修正窗口强制货币标准化在ETL最上游用当日中间价将所有外币转为CNY存储amount_cny字段4.4 故障现象JOIN后数据量爆炸内存溢出典型场景orders表1000万行JOINcustomers表50万行后Spark作业OOM。根因分析维度膨胀customers表中region有1000个值orders表中region有500个值但JOIN键未标准化导致1000×50050万组合广播JOIN失效customers表实际大小超10MBSpark默认广播阈值未触发广播优化数据倾斜region华东占订单80%该分区数据量过大排查步骤检查JOIN键分布SELECT region, COUNT(*) FROM customers GROUP BY region ORDER BY COUNT(*) DESC LIMIT 10查看执行计划EXPLAIN EXTENDED SELECT ...确认是否使用BroadcastHashJoin监控各分区数据量Spark UI中查看Shuffle Read Size修复方案预过滤SELECT /* BROADCAST(c) */ * FROM orders o JOIN (SELECT * FROM customers WHERE region IN (SELECT DISTINCT region FROM orders)) c ON o.region c.region加盐打散对倾斜key加随机前缀-- 对华东数据加盐 SELECT CASE WHEN region 华东 THEN CONCAT(华东_, FLOOR(RAND()*10)) ELSE region END as region_salt, ... FROM orders改用MapJoin在Hive中设置SET hive.auto.convert.jointrue; SET hive.mapjoin.smalltable.filesize25000000;4.5 故障现象聚合结果在BI工具中无法钻取维度联动失效典型场景Tableau中拖拽region和product_category但筛选region华东后product_category下拉列表仍显示全部品类而非华东专属品类。根因分析维度未建立层级关系Tableau中region和product_category被设为独立维度未定义region → product_category的父子关系数据中存在“幽灵值”product_category中有华东特供这类仅在华东出现的值但未在维度表中标记其归属NULL值阻断层级部分product_category为NULL导致Tableau无法构建完整树状结构修复方案在维度表中明确定义层级dim_product表增加region_scope字段ALL/华东/华南并在BI工具中设置为层次结构清洗NULL值UPDATE dim_product