多维聚合中的数据变形术:维度层级与度量聚合规则实战
2026/7/4 23:39:57 网站建设 项目流程

1. 这不是简单的“GROUP BY”——多维聚合中的数据变形术到底在解决什么问题?

如果你正在处理销售报表、用户行为分析、IoT设备时序汇总,或者哪怕只是整理一份带地区、季度、产品线、渠道四个维度的Excel透视表,那你一定遇到过这种场景:原始数据里每行是一次订单(含城市、月份、品类、促销标识、金额),但老板要的不是“北京7月手机销量”,而是“华东大区Q2高客单价新品的环比增长率”。这时候,光靠SQL里的GROUP BY city, month, category已经不够用了——你得把数据“掰开、揉碎、再捏合”,在多个维度上同时做切片、钻取、滚动计算、跨层对比。这就是标题里“Multi-Dimensional Aggregation”(多维聚合)的真实战场,而“Data Manipulation”(数据变形)绝非锦上添花,它是让聚合结果真正可读、可比、可决策的底层引擎。

我做过6个行业超过30个BI看板项目,发现一个铁律:85%以上的分析需求失败,不是因为模型不准,而是因为聚合前的数据变形没做对。比如把“用户首次下单时间”错误地按“订单日期”聚合,会导致新客数虚高;把“库存周转天数”直接对SKU+仓库求平均,会掩盖滞销品风险;甚至把“促销折扣率”用SUM而不是加权平均,会让营销ROI失真。这些都不是语法错误,而是对“维度语义”和“度量性质”的误判。本篇讲的Part 20,正是我在某零售SaaS平台重构分析引擎时踩坑后沉淀出的一套实操框架——它不依赖特定工具(Pandas/Spark/SQL均可落地),核心是三步逻辑:先锚定维度层级关系,再识别度量聚合类型,最后设计变形链路。适合数据工程师调优ETL、分析师写复杂DAX、甚至业务人员理解为什么报表数字“看起来不对”。下面所有内容,都来自真实生产环境日志、监控告警和回滚记录,没有理论推演,只有能抄作业的细节。

2. 多维聚合的本质:维度不是标签,而是有拓扑结构的坐标系

2.1 维度层级(Hierarchy)与交叉维度(Cross-Dimension)必须严格区分

很多人把“省份-城市-门店”和“年-季度-月-日”都叫“层级维度”,但它们在聚合中的数学行为完全不同。前者是树状包含关系(江苏包含南京,南京包含新街口店),后者是线性时间序列(Q2包含4月、5月、6月,但4月不“属于”Q2,而是被Q2覆盖)。混淆这两者,会导致灾难性错误:

  • 错误做法:对“年+季度+城市”直接GROUP BY,然后计算AVG(sales)
  • 后果:南京2023年Q1销售额100万,Q2 120万,苏州同季80万、90万,简单平均得出102.5万——这既不是南京的均值,也不是华东的均值,更不是时间趋势,纯粹是数学垃圾。

正确解法是先明确维度拓扑:

  • 层级维度(Hierarchical Dimension):必须定义“上卷路径”(Roll-up Path)。例如门店→城市→省份→大区,每个下级节点有且仅有一个上级。聚合时,若需“大区级销售额”,必须从门店明细逐级SUM,不能跳过城市直接从门店到大区(否则丢失中间校验点)。
  • 交叉维度(Cross Dimension):如“产品线×促销类型×用户等级”,它们之间无包含关系,是笛卡尔积组合。聚合时需保留所有交叉粒度,或按业务规则预设“有效组合”(如高端产品线不参与满减促销,该组合应置空而非填0)。

提示:在建模阶段就用图谱工具(如draw.io)画出维度关系图,标出每条边的语义(is-a, part-of, occurs-in)。我曾因漏标“仓库类型”和“配送区域”的part-of关系,导致冷链仓数据被错误合并进常温仓报表,损失3天排查时间。

2.2 度量(Measure)不是数字,而是带聚合规则的“物理量”

看到销售额、用户数、停留时长这些字段,新手常默认“SUM就行”。但多维聚合中,每个度量都有其固有聚合函数(Inherent Aggregation Function),选错等于全盘作废:

度量名称固有聚合函数错误聚合示例后果物理类比
订单金额SUMAVG(order_amount)掩盖大额订单影响总重量 ≠ 平均单件重量
活跃用户数COUNT(DISTINCT user_id)SUM(active_users)用户重复计数,虚高200%+人数 ≠ 各房间人数之和
库存周转天数加权平均(按库存金额)AVG(turnover_days)滞销品拉低整体指标平均车速 ≠ 总路程/总时间
首次下单时间MIN(order_time)MAX(first_order_time)把新客识别成老客第一次发生 ≠ 最后一次

关键洞察:固有聚合函数由业务语义决定,而非技术便利性决定。例如“用户生命周期价值(LTV)”,若按用户维度计算后聚合,必须用SUM(总LTV);若按时间维度看趋势,则需用AVG(人均LTV),但分母必须是当期活跃用户数,而非历史累计用户数——这里就嵌套了两层维度逻辑。

2.3 “变形链路”(Transformation Chain):让数据在聚合前完成语义对齐

多维聚合真正的难点,不在聚合本身,而在聚合前的数据准备。我们称之为“变形链路”,它是一组不可逆的操作序列,确保输入聚合引擎的数据已满足维度-度量契约。以电商GMV分析为例:

原始事实表 → [步骤1:时间对齐] → [步骤2:维度标准化] → [步骤3:度量校准] → 聚合输入表
  • 步骤1:时间对齐:订单时间、支付时间、发货时间可能跨天。业务要求“按支付时间归因”,则必须将所有时间戳统一转换为支付时间所在自然日,并标记时区(避免海外仓数据漂移)。
  • 步骤2:维度标准化:城市名“北京市”“北京”“BJ”需统一为ISO标准码“CN-BJ”;促销类型“满300减50”和“满300减50(限指定品类)”需拆分为两个独立维度值,否则聚合时无法区分效果。
  • 步骤3:度量校准:退款订单的金额需标记为负值并参与SUM;试用订单需过滤(不参与GMV);虚拟商品(如会员权益)需按成本价折算(避免虚增GMV)。

注意:变形链路必须幂等且可追溯。我在某金融项目中要求每步操作生成校验摘要(如步骤2后输出“标准化前后城市编码映射表”),当某天上海数据异常时,5分钟内定位到是第三方API返回了“ShangHai”而非“SH”,而非从头排查整个链路。

3. 核心变形技术详解:从Pandas到Spark的实操实现

3.1 维度层级上卷:用pd.crosstabagg()构建安全聚合路径

假设我们有门店销售明细表sales_df,含字段:store_id,city,province,product_category,sales_amount,order_date。目标是生成“省份-季度-品类”三级聚合。

错误示范(常见陷阱):

# 危险!直接GROUP BY多列,忽略层级关系 wrong_agg = sales_df.groupby(['province', 'quarter', 'product_category'])['sales_amount'].sum()

问题:若quarter字段由order_date.dt.quarter生成,但部分门店数据缺失order_datequarter会变成NaN,导致该门店数据被丢弃——而实际应按门店所属省份的默认季度填充。

正确方案(分步上卷,留痕可查):

# 步骤1:门店级基础聚合(最细粒度) store_level = sales_df.groupby(['store_id', 'product_category']).agg({ 'sales_amount': 'sum', 'order_date': 'min' # 记录最早订单时间,用于后续季度推断 }).reset_index() # 步骤2:关联门店维度表,补全省份/城市信息(确保维度完整性) dim_store = pd.read_csv('dim_store.csv') # 含store_id, city, province, open_date store_with_dim = store_level.merge(dim_store, on='store_id', how='left') # 步骤3:安全生成季度(用open_date兜底,避免NaN) store_with_dim['quarter'] = np.where( store_with_dim['order_date'].notna(), store_with_dim['order_date'].dt.quarter, (pd.to_datetime(store_with_dim['open_date']) + pd.DateOffset(months=2)).dt.quarter ) # 步骤4:按省份上卷(显式声明路径,便于审计) province_agg = store_with_dim.groupby(['province', 'quarter', 'product_category']).agg({ 'sales_amount': 'sum', 'store_id': 'nunique' # 同时统计参与聚合的门店数,监控数据覆盖度 }).rename(columns={'store_id': 'active_stores'}).reset_index() # 输出校验报告 print(f"省份聚合覆盖门店数:{province_agg['active_stores'].sum()} / {dim_store.shape[0]}")

Spark版等效实现(应对亿级数据):

// 使用DataFrame API,避免RDD黑盒操作 val storeLevel = salesDF .groupBy("store_id", "product_category") .agg( sum("sales_amount").as("sales_amount"), min("order_date").as("first_order_date") ) val provinceAgg = storeLevel .join(dimStoreDF, "store_id") // 维度表广播Join .withColumn("quarter", when('first_order_date.isNotNull, date_format('first_order_date, "q").cast("int")) .otherwise( date_format(date_add('open_date, 60), "q").cast("int") // open_date加2个月推季度 ) ) .groupBy("province", "quarter", "product_category") .agg( sum("sales_amount").as("sales_amount"), countDistinct("store_id").as("active_stores") )

实操心得:永远在聚合后保留“参与计算的实体数”(如active_stores)。某次大促期间,我们发现华东大区Q3数据突降,排查发现是新开了50家门店但未同步到维度表,导致join后这些门店被过滤——active_stores从200骤降到150,立刻触发告警。没有这个字段,问题会延迟3天以上。

3.2 交叉维度组合爆炸控制:用itertools.product预生成有效空间

当维度数≥4(如[region, channel, device_type, user_segment]),笛卡尔积可达万级,但90%组合实际无数据(如“iOS设备”在“PC端渠道”不可能存在)。盲目GROUP BY会导致大量NULL行,拖慢查询且污染缓存。

解决方案:动态生成有效组合集(Valid Combination Set)

from itertools import product import pandas as pd # 定义各维度的合法取值(从业务规则提取,非全量枚举) regions = ['North', 'South', 'East', 'West'] channels = ['App', 'Web', 'WeChat', 'Offline'] device_types = {'App': ['iOS', 'Android'], 'Web': ['Desktop', 'Mobile'], 'WeChat': ['MiniProgram']} user_segments = ['New', 'Active', 'Churned'] # 构建有效组合:只生成业务允许的交叉 valid_combos = [] for ch in channels: for dt in device_types.get(ch, []): for reg in regions: for seg in user_segments: # 添加业务规则:Offline渠道无设备类型区分 if ch == 'Offline': valid_combos.append((reg, ch, 'All', seg)) else: valid_combos.append((reg, ch, dt, seg)) # 转为DataFrame,作为聚合后的LEFT JOIN基准 combo_df = pd.DataFrame(valid_combos, columns=['region', 'channel', 'device_type', 'user_segment']) # 主聚合(只计算有数据的组合) main_agg = sales_df.groupby(['region', 'channel', 'device_type', 'user_segment']).agg({ 'revenue': 'sum', 'orders': 'count' }).reset_index() # 补全零值(LEFT JOIN combo_df) full_result = combo_df.merge(main_agg, on=['region', 'channel', 'device_type', 'user_segment'], how='left').fillna(0)

关键优势:

  • 预生成组合集可缓存复用,避免每次查询都计算笛卡尔积
  • 业务规则(如Offline→All)集中管理,修改一处全局生效
  • fillna(0)比SQL中COALESCE更可控,避免NULL参与后续计算

注意:组合集必须版本化管理。我们在Git中维护valid_combinations_v2023Q3.yaml,每次发布新渠道(如抖音小程序)时,更新yaml并触发CI检查,确保ETL任务自动加载新版组合集。

3.3 度量校准:处理“反直觉聚合”的三类典型场景

场景1:比率型度量(如转化率、毛利率)不能直接聚合

业务要“各城市毛利率”,原始表有revenuecost字段。错误做法:

# ❌ 危险!先算城市毛利率再平均 city_gross_margin = sales_df.groupby('city').apply( lambda x: (x['revenue'].sum() - x['cost'].sum()) / x['revenue'].sum() ).mean()

这等于计算“城市毛利率的平均值”,而非“整体毛利率”。正确解法是分子分母分别上卷,最后计算

# ✅ 分子分母分离聚合 city_agg = sales_df.groupby('city').agg({ 'revenue': 'sum', 'cost': 'sum' }).reset_index() city_agg['gross_margin'] = (city_agg['revenue'] - city_agg['cost']) / city_agg['revenue'] # 整体毛利率(所有城市合并计算) overall_margin = (city_agg['revenue'].sum() - city_agg['cost'].sum()) / city_agg['revenue'].sum()
场景2:时序型度量(如留存率、N日复购率)需固定时间锚点

计算“7日复购率”,定义为:在T日下单的用户中,T+7日内再次下单的比例。错误做法:

# ❌ 按用户分组后暴力计算,忽略时间窗口边界 user_orders = orders_df.sort_values(['user_id', 'order_date']) user_orders['next_order_date'] = user_orders.groupby('user_id')['order_date'].shift(-1) user_orders['is_7day_repurchase'] = (user_orders['next_order_date'] - user_orders['order_date']) <= pd.Timedelta(days=7)

问题:若用户在T日下单后,T+3日又下单,T+10日再下单,则T日订单会被标记为复购,但T+3日订单不会——导致同一用户多次被计为“复购”,且窗口不统一。

正确解法(固定锚点+窗口聚合):

# 步骤1:为每个用户生成“首单锚点日” first_order = orders_df.groupby('user_id')['order_date'].min().reset_index(name='first_order_date') # 步骤2:关联所有订单,标记是否在首单后7日内复购 orders_with_anchor = orders_df.merge(first_order, on='user_id') orders_with_anchor['days_since_first'] = (orders_with_anchor['order_date'] - orders_with_anchor['first_order_date']).dt.days orders_with_anchor['is_7day_repurchase'] = (orders_with_anchor['days_since_first'] > 0) & (orders_with_anchor['days_since_first'] <= 7) # 步骤3:按城市聚合(注意:分母是首单用户数,分子是复购用户数) city_repurchase = orders_with_anchor.groupby('city').agg({ 'user_id': lambda x: x[orders_with_anchor['days_since_first'] == 0].nunique(), # 首单用户数 'is_7day_repurchase': 'sum' # 复购事件数(注意:一个用户可有多次复购,但业务通常要“用户维度”复购率) }).rename(columns={'user_id': 'first_order_users', 'is_7day_repurchase': 'repurchase_events'}) # 修正:按用户去重计算复购用户数 repurchase_users = orders_with_anchor[orders_with_anchor['is_7day_repurchase']].groupby(['city', 'user_id']).size().groupby('city').size() city_repurchase['repurchase_users'] = repurchase_users city_repurchase['7day_repurchase_rate'] = city_repurchase['repurchase_users'] / city_repurchase['first_order_users']
场景3:分布型度量(如中位数、分位数)需采样或近似算法

对亿级订单计算“各城市订单金额中位数”,pandas.Series.median()会内存溢出。Spark的approxQuantile是解法,但需理解误差边界:

# Spark中计算95%置信度下的中位数(相对误差<1%) median_approx = salesDF.approxQuantile("sales_amount", [0.5], 0.01) # 返回数组,取索引0即中位数 city_median = salesDF.groupBy("city").agg( expr("approx_percentile(sales_amount, 0.5, 1000)").alias("median_sales") )

参数解读:

  • 0.01:采样误差(绝对误差),即真实中位数与结果差值<1%销售额
  • 1000:分位数精度参数,值越大越准但越慢,1000是Spark推荐平衡值
  • 必须配合EXPLAIN检查执行计划,确认是否触发ApproximatePercentile算子而非全量排序

实测对比:10亿行数据,精确中位数耗时47分钟,approxQuantile耗时2.3分钟,误差0.87%,业务完全接受。但若计算“99.9分位数”,误差会飙升至5%,此时必须改用分桶采样+插值法。

4. 生产环境避坑指南:那些文档里不会写的血泪教训

4.1 时间维度陷阱:时区、日历、业务日的三重幻觉

坑1:数据库服务器时区≠业务时区
某跨境电商项目,MySQL服务器设为UTC,但业务要求“按买家所在地时区统计”。直接DATE(created_at)会把美国西海岸订单(UTC-7)归入前一天。正确解法:

-- 在ETL中转换为买家时区(需维护国家-时区映射表) SELECT CONVERT_TZ(created_at, '+00:00', buyer_timezone) AS local_created_at, DATE(CONVERT_TZ(created_at, '+00:00', buyer_timezone)) AS local_date FROM orders o JOIN dim_country c ON o.country_code = c.country_code

坑2:自然日历≠业务日历
零售业常用“4-4-5周制”(每月4周或5周),财务关账日是每月28日。若用YEARWEEK(created_at),会导致1月29日订单被计入下一年第1周。解法:预生成业务日历表dim_business_calendar,含字段calendar_date,fiscal_year,fiscal_week,is_month_end,聚合时JOIN此表。

坑3:夏令时切换导致数据重复或丢失
欧洲某项目在10月最后一个周日凌晨2点时钟拨回1小时,导致2023-10-29 02:15:00时间戳出现两次。GROUP BY DATE(created_at)会将两次记录合并为一行,但实际是不同订单。解法:存储时间戳时强制使用UTC,显示层再转换;或在ETL中添加is_dst_flag字段标记。

4.2 维度漂移(Dimension Drift):昨天的“华东大区”今天已不存在

维度表不是静态快照。当公司重组,原“华东大区”拆分为“长三角大区”和“山东大区”,历史数据如何归属?常见错误:

  • 全量更新维度表:导致历史报表中“华东大区”数据消失
  • 不更新:新报表中“长三角大区”无历史数据,无法同比

正确方案:维度缓慢变化类型2(SCD Type 2)
dim_region表中增加字段:region_id,region_name,valid_from,valid_to,is_current。例如:

region_idregion_namevalid_fromvalid_tois_current
101华东大区2020-01-012023-09-300
102长三角大区2023-10-019999-12-311
103山东大区2023-10-019999-12-311

聚合时关联逻辑:

SELECT r.region_name, SUM(f.sales_amount) FROM fact_sales f JOIN dim_region r ON f.region_id = r.region_id AND f.order_date BETWEEN r.valid_from AND r.valid_to GROUP BY r.region_name

这样,2023年9月订单关联华东大区,10月订单关联长三角大区,历史可比性完美保持。

4.3 资源爆炸预警:当GROUP BY遇上高基数维度

user_id(亿级)、sku_id(千万级)参与GROUP BY,Spark会触发HashAggregate的内存溢出。监控指标spark.sql.adaptive.enabled=true开启自适应查询,但治标不治本。

根治方案:分治聚合(Divide and Conquer Aggregation)

# 步骤1:按user_id哈希分桶(1000桶) sales_df['user_bucket'] = sales_df['user_id'].apply(lambda x: hash(x) % 1000) # 步骤2:桶内聚合(小数据集,内存可控) bucket_agg = sales_df.groupby(['user_bucket', 'product_category']).agg({ 'revenue': 'sum', 'order_count': 'count' }) # 步骤3:桶间合并(聚合结果远小于原始数据) final_agg = bucket_agg.groupby('product_category').agg({ 'revenue': 'sum', 'order_count': 'sum' })

关键参数选择:

  • 桶数 =sqrt(高基数维度唯一值数量),如1亿用户→10000桶
  • 必须保证user_bucket在后续所有JOIN中作为分区键,避免Shuffle

我们在线上集群实测:10亿行用户行为数据,直接GROUP BY user_idOOM,分桶后内存占用下降83%,耗时仅增加12%。

4.4 可解释性黑洞:为什么报表数字每天变?

业务方最常问:“昨天北京销售额是1200万,今天怎么变成1180万?”——往往不是数据错误,而是聚合逻辑的隐式变更。

建立聚合指纹(Aggregation Fingerprint)机制:
每次生成聚合表,自动计算并存储:

  • 输入数据版本(如fact_sales_v20231025
  • 变形链路哈希(sha256(步骤1代码+步骤2代码...)
  • 维度表版本(dim_region_v20231020
  • 关键参数(如quarter_calculation_method=payment_date

当数字波动>5%,系统自动比对前后两天的指纹,差异项高亮显示。某次发现波动源于维度表更新,但指纹显示dim_region版本未变,进一步排查发现是payment_date字段新增了NULL值过滤逻辑——这个变更未走发布流程,被指纹系统捕获并拦截。

5. 常见问题速查表:从报错信息直达根因

现象描述典型报错/表现根本原因快速定位命令解决方案
聚合结果为空COUNT(*) = 0,但原始数据有记录维度表JOINON条件不匹配,或WHERE过滤过严SELECT COUNT(*) FROM fact f LEFT JOIN dim d ON f.key=d.key WHERE d.key IS NULL检查JOIN字段类型(如VARCHARvsINT)、空值处理、大小写敏感性
数值明显偏大GMV是预期的2倍同一事实表被多次JOIN维度表(如JOIN city后又JOIN province,导致笛卡尔积)EXPLAIN ANALYZE查看执行计划中BroadcastHashJoin次数改用MAP JOIN或先聚合再JOIN,禁用自动广播
NULL值大量出现SUM(sales)结果为NULL度量字段存在NULL且未COALESCE,或GROUP BY字段有NULL导致分组失效SELECT COUNT(*) FROM table WHERE sales IS NULLSELECT COUNT(*) FROM table WHERE group_field IS NULLCOALESCE(sales, 0)WHERE group_field IS NOT NULL前置过滤
性能断崖下跌查询耗时从10秒升至10分钟高基数维度(如user_id)参与GROUP BY,触发SortAggregate全排序EXPLAIN中查找SortAggregate算子及sort字段改用HashAggregate(Spark配置spark.sql.adaptive.enabled=true)或分桶聚合
同比数据断裂今年Q1有数据,去年Q1为NULL维度表未启用SCD Type 2,历史维度失效SELECT * FROM dim_date WHERE fiscal_year = 2022 AND fiscal_quarter = 1补全历史维度版本,重建聚合表
小数精度丢失AVG(revenue)显示1234567.00,实际应为1234567.89数据库字段类型为FLOAT(精度约7位),非DECIMAL(18,2)DESCRIBE TABLE fact_sales查看字段类型修改表结构,ETL中强制CAST(revenue AS DECIMAL(18,2))

独家避坑技巧:

  • 永远在聚合前加LIMIT 10验证:运行SELECT * FROM (你的聚合SQL) LIMIT 10,确认字段名、数据类型、NULL逻辑符合预期,再删掉LIMIT跑全量。
  • UNION ALL代替OR条件WHERE region='East' OR region='West'会阻止索引使用,拆成SELECT ... WHERE region='East' UNION ALL SELECT ... WHERE region='West',Spark可并行执行。
  • 警惕COUNT(DISTINCT)的内存炸弹:当DISTINCT基数>100万,改用APPROX_COUNT_DISTINCT(误差<2%),或先GROUP BY去重再计数。

6. 从Part 20到Part 21:当多维聚合遇上实时流处理

多维聚合的终极挑战,不是批处理的“算得准”,而是流处理的“算得快且准”。当订单数据以每秒万条速度涌入Kafka,要求“实时计算各城市最近5分钟GMV”,传统批处理模式彻底失效。

核心矛盾:

  • 准确性:需要精确的COUNT(DISTINCT user_id),但流式COUNT DISTINCT内存消耗随用户数线性增长
  • 时效性:5分钟窗口要求毫秒级延迟,但GROUP BY操作天然有状态开销
  • 一致性:Flink的TUMBLING WINDOWHOPPING WINDOW在乱序数据下结果不一致

我们的落地方案(已在生产验证):

  1. 分层聚合架构

    • 实时层(Flink):用HyperLogLog++算法近似COUNT DISTINCT,误差<1.5%,内存恒定2KB/窗口
    • 准实时层(Spark Streaming):每5分钟微批处理,用approx_count_distinct校准实时层偏差
    • 离线层(Hive):T+1全量精确计算,每日凌晨覆盖实时层结果
  2. 维度物化(Dimension Materialization)
    cityproduct_category等维度表预加载到Flink的StateBackend,避免每条消息都查外部DB。使用RocksDB状态后端,支持亿级维度关联。

  3. 水位线(Watermark)策略
    订单事件时间戳可能延迟(如APP离线缓存),设置WATERMARK FOR event_time AS event_time - INTERVAL '30' SECOND,容忍30秒乱序,超时数据进入侧输出流(Side Output)人工核查。

这部分实践已沉淀为内部《流式多维聚合规范v2.1》,核心原则就一条:不追求100%精确的实时,而追求99.9%准确的“足够实时”。当业务能接受±1.5%的用户数误差和30秒延迟时,资源消耗下降70%,这才是工程落地的真相。

我在某次跨部门复盘会上说:“Part 20的价值,不是教会你写一个GROUP BY,而是让你在写出第一行聚合代码前,先问三个问题:这个维度的层级关系是什么?这个度量的固有聚合函数是什么?这次聚合的结果,明天还能和今天对得上吗?”——如果这三个问题没想清楚,后面所有优化都是空中楼阁。现在,你可以打开你的SQL编辑器,或者Jupyter Notebook,把今天读到的任意一个技巧,用在手头那个正让你头疼的报表上。别怕试错,我当年也是从把SUM写成AVG开始的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询