1. 项目概述:当数据不再是一张“平铺直叙”的表格
你有没有遇到过这样的场景:销售部门要按季度、按区域、按产品大类看毛利,同时还要对比去年同期;财务团队需要把成本拆解到“部门-项目-费用类型-发生月份”四个维度,再筛选出超预算的组合;甚至一个简单的用户行为分析,都要交叉统计“新老用户 × 设备类型 × 页面路径深度 × 当日活跃时段”。这时候,Excel 的透视表点到第三层就开始卡顿,SQL 里写个 GROUP BY 加上 CASE WHEN 嵌套三层,自己都快看不懂了——这已经不是“汇总”问题,而是多维聚合(Multi-Dimensional Aggregation)的实战现场。本篇标题中的 “Part 20: Data Manipulation in Multi-Dimensional Aggregation”,绝非教科书里抽象的“高维数组”概念,它直指现代数据分析中一个最硬核、也最容易被低估的环节:如何在保留原始数据颗粒度的前提下,自由、高效、可复现地对多个维度进行任意组合、切片、钻取与比较。核心关键词——多维聚合、数据操作、维度建模、OLAP思维、分组聚合、交叉分析——全部围绕一个现实目标:让数据从“静态报表”变成“可交互的决策仪表盘”。它适合三类人:一是刚从单表 GROUP BY 过渡到业务宽表开发的 SQL 工程师,二是用 Pandas 做分析但总被pivot_table参数绕晕的 Python 数据分析师,三是正在搭建 BI 系统、需要理解底层聚合逻辑的产品或数仓工程师。这不是讲理论,而是拆解我在真实项目中处理过 12TB 日志、支撑 37 个业务方自助分析需求时,反复打磨出的一套“多维数据操作心法”。
2. 多维聚合的本质:为什么不能只靠 GROUP BY 和嵌套子查询?
2.1 传统 SQL 聚合的“三维天花板”
很多人以为,只要把 GROUP BY 写得足够长,就能解决所有多维问题。比如统计“各城市、各年龄段、各会员等级的月均消费额”,写出这样的 SQL:
SELECT city, FLOOR((CURRENT_DATE - birth_date) / 365.25) AS age_group, member_level, AVG(order_amount) AS avg_monthly_spend FROM orders o JOIN users u ON o.user_id = u.id GROUP BY city, FLOOR((CURRENT_DATE - birth_date) / 365.25), member_level;表面看没问题,但它立刻暴露出三个致命缺陷:
第一,维度耦合不可拆分。一旦你只想看“城市 × 会员等级”的聚合结果,就必须重写整个 GROUP BY,无法从已有结果中“降维”提取。现实中,业务方的需求是动态的:“先看全国总览 → 再下钻到华东 → 再聚焦上海的 VIP 用户 → 再对比去年同月”。传统 GROUP BY 是“一次性烘焙”,而多维分析需要的是“可拆卸的乐高积木”。
第二,空值维度丢失严重。如果某城市没有 25–35 岁的钻石会员,这条组合在结果集中就彻底消失。但业务决策恰恰需要知道“哪些组合是空白的”,比如“深圳的 Z 世代用户尚未触达”,这属于关键洞察,而非数据缺失。传统聚合默认“只返回有数据的组合”,而多维分析要求“显式返回所有合法组合,空值填 0 或 NULL”。
第三,计算成本指数级增长。当你需要同时支持 (城市, 月份)、(省份, 产品类目, 季度)、(用户等级, 设备类型) 三套视图时,传统方案要么写三个独立 SQL(维护成本翻三倍),要么用 UNION ALL 拼接(执行计划爆炸)。我曾在一个电商项目中测算过:对一张 8 亿行的订单事实表,分别执行 3 个不同维度组合的 GROUP BY,平均耗时 42 秒;而用预计算的多维立方体(Cube),同一份数据加载后,任意组合的响应时间稳定在 0.8 秒以内——差距不是优化,而是代际。
提示:多维聚合不是“更高级的 GROUP BY”,而是将“维度”本身作为一等公民进行建模。它的输入不是“一张表”,而是“一张事实表 + 一组维度表 + 一份维度关系定义”。
2.2 维度建模:星型模型才是多维操作的真正起点
多维聚合的根基,是Kimball 维度建模理论中的星型模型(Star Schema)。它强制分离“发生了什么”(事实表)和“在什么背景下发生”(维度表)。以零售分析为例:
- 事实表
sales_fct:只存度量(metrics)和外键(foreign keys)sale_id,product_key,store_key,date_key,customer_key,quantity,amount,discount
- 维度表
dim_product:描述产品属性product_key,product_name,category,brand,is_new_launch
- 维度表
dim_store:描述门店属性store_key,city,province,store_type,open_date
- 维度表
dim_date:日期维度(不是简单DATE类型,而是展开的语义化字段)date_key,full_date,year,quarter,month,week_of_year,is_holiday,is_weekend
这个结构看似多此一举,但它带来了三个革命性优势:
- 维度可复用:
dim_date可同时被销售、库存、客服工单等多个事实表引用,避免每个表都重复计算YEAR(date)。 - 属性可扩展:想新增“是否为促销期”,只需在
dim_date表加一列,所有关联的事实表自动获得该维度,无需修改任何聚合 SQL。 - 语义清晰可控:
dim_store.city是经过清洗、标准化的城市名(如“北京市”而非“北京”“京市”“Beijing”),确保“北京”和“北京市”不会被算作两个维度值。
我在一家连锁药店落地时,最初用宽表直接 JOIN 所有维度字段,结果发现“城市”字段在采购系统里叫city_name,在物流系统里叫delivery_city,在 CRM 里又叫region,且大小写、空格、简称全不统一。上线两周后,运营总监指着报表问:“为什么‘上海’和‘上海市’的销售额差了 37%?”——这就是没走维度建模的代价。重构为星型模型后,所有系统接入前必须通过dim_store主键映射,问题一夜消失。
2.3 OLAP 思维:切片(Slice)、切块(Dice)、钻取(Drill-down)、旋转(Pivot)
多维聚合的操作语言,本质上是OLAP(Online Analytical Processing)的四类原子操作。它们不是功能按钮,而是数据操作的底层逻辑:
- 切片(Slice):固定一个维度的值,观察其他维度。例如:“只看 2024 年 Q1 的数据” → 在
dim_date.quarter = '2024-Q1'上切一刀。技术实现上,就是 WHERE 条件过滤。 - 切块(Dice):同时固定多个维度的值。例如:“只看 2024 年 Q1、华东地区、A 类门店的数据”。这是 WHERE 多条件 AND。
- 钻取(Drill-down):沿维度层次向下细化。例如:从“省份”钻取到“城市”,从“年”钻取到“季度”再到“月”。这依赖维度表中的层次结构(如
dim_date表里year → quarter → month的父子关系)。 - 旋转(Pivot):改变维度在报表中的展示方向。例如:把“月份”从行头转为列头,生成月度趋势对比表。这对应 SQL 的
PIVOT或 Pandas 的pivot_table。
关键在于:这些操作必须能无损叠加。比如先“切片”到 2024 年,再“钻取”到月份,再“旋转”成横向对比——每一步都不应丢失原始数据精度。而传统宽表聚合,一旦 GROUP BY 完成,SUM(amount)就是一个标量,再也无法知道其中有多少来自 1 月、多少来自 2 月。多维聚合的威力,正在于它始终保留着“可逆操作”的能力。
3. 核心数据操作技术栈:从 SQL 到 Python,再到现代 OLAP 引擎
3.1 SQL 层:窗口函数 + CUBE/ROLLUP 是多维操作的“手工钢锯”
在无法引入专用 OLAP 引擎的场景(如 legacy 系统、临时分析),纯 SQL 仍是最可靠的武器。但必须超越基础 GROUP BY。
CUBE 与 ROLLUP:自动生成所有组合
-- 生成 city, product_category, month 三个维度的所有可能聚合组合 SELECT city, product_category, month, SUM(sales_amount) AS total_sales, GROUPING_ID(city, product_category, month) AS grp_id FROM sales_fct f JOIN dim_store s ON f.store_key = s.store_key JOIN dim_product p ON f.product_key = p.product_key JOIN dim_date d ON f.date_key = d.date_key GROUP BY CUBE(city, product_category, month);GROUPING_ID返回一个位掩码,标识哪些维度被“合计”(即值为 NULL)。例如grp_id = 1(二进制001)表示month被合计,city和product_category是明细值。这让你能精准识别“城市 × 类目”的小计行,而不是把它和真正的 NULL 月混淆。
窗口函数:在聚合后保留明细上下文
-- 计算每个城市的销售额占全省的比例,同时保留该城市所有月份明细 SELECT city, month, SUM(sales_amount) AS city_monthly_sales, SUM(SUM(sales_amount)) OVER (PARTITION BY province, month) AS province_monthly_total, ROUND( SUM(sales_amount) * 100.0 / SUM(SUM(sales_amount)) OVER (PARTITION BY province, month), 2 ) AS pct_of_province FROM sales_fct f JOIN dim_store s ON f.store_key = s.store_key GROUP BY city, province, month;这里SUM(SUM()) OVER (...)是关键:内层SUM()是 GROUP BY 聚合,外层SUM() OVER是窗口聚合,两者嵌套实现了“分组内占比”计算。没有窗口函数,你只能用子查询或 CTE,代码臃肿且性能差。
实操心得:CUBE 会生成 2^n 个组合(n 为维度数),当 n > 5 时结果集极易爆炸。我的经验是:CUBE 仅用于探索性分析(n ≤ 3),生产报表必须用明确的 GROUP BY + UNION 显式控制组合。曾有个项目盲目用 CUBE(12个维度),单次查询生成 4096 行,其中 92% 是无意义的全 NULL 合计行,DBA 直接 kill 了进程。
3.2 Python/Pandas 层:melt/pivot_table/crosstab的黄金三角
当数据量在内存可承受范围(< 5000 万行),Pandas 是最灵活的多维操作沙盒。但新手常陷在pivot_table的index/columns/values/aggfunc参数迷宫里。真相是:90% 的多维分析,只需掌握melt→groupby→unstack三步流。
Step 1:melt—— 把宽表打回“长格式”事实表
# 原始宽表:每行是一个用户,列是各月消费 df_wide = pd.DataFrame({ 'user_id': [1, 2, 3], 'jan_2024': [120, 80, 150], 'feb_2024': [135, 92, 142], 'mar_2024': [118, 88, 160] }) # melt 成标准事实表:每行是一个 (用户, 月份, 金额) 三元组 df_long = df_wide.melt( id_vars=['user_id'], var_name='month', value_name='spend' ) # 结果: # user_id month spend # 0 1 jan_2024 120 # 1 2 jan_2024 80 # 2 3 jan_2024 150 # 3 1 feb_2024 135 # ...melt是多维操作的“归一化”步骤。它强制将所有维度(月份)和度量(spend)分离,为后续任意分组打下基础。
Step 2:groupby—— 按需聚合,维度自由组合
# 按月份聚合总消费(降维) monthly_total = df_long.groupby('month')['spend'].sum() # 按用户等级(需先 join 用户等级表)和月份聚合(双维度) # 假设 df_users 包含 user_id 和 level 列 df_enriched = df_long.merge(df_users, on='user_id') by_level_month = df_enriched.groupby(['level', 'month'])['spend'].agg(['sum', 'count', 'mean']) # 按月份、并计算环比(需要 shift) by_month = df_long.groupby('month')['spend'].sum().sort_index() by_month['mom_change_pct'] = by_month.pct_change() * 100Step 3:unstack或pivot_table—— 按需旋转成报表
# 将双维度 groupby 结果转为“等级为行,月份为列”的矩阵 report_matrix = by_level_month['sum'].unstack(level='month', fill_value=0) # 结果: # month jan_2024 feb_2024 mar_2024 # level # Gold 120.0 135.0 118.0 # Silver 80.0 92.0 88.0 # 或者用 pivot_table 一步到位(等价) report_pivot = df_enriched.pivot_table( values='spend', index='level', columns='month', aggfunc='sum', fill_value=0 )注意:
unstack是pivot_table的底层实现,但unstack更轻量、更可控。pivot_table会自动处理缺失值、排序,但当维度值过多时(如 1000 个城市),它默认会创建巨大稀疏矩阵,吃光内存。我的做法是:先groupby得到紧凑的 Series/DataFrame,再unstack,全程可控。
3.3 现代 OLAP 引擎:Doris、ClickHouse、Apache Druid 的选型逻辑
当数据量突破亿行、并发查询超 50 QPS、响应要求 < 1 秒时,必须引入专业 OLAP 引擎。这不是“升级”,而是架构范式切换。
| 引擎 | 核心优势 | 典型适用场景 | 我的实测瓶颈点 |
|---|---|---|---|
| StarRocks/Doris | MySQL 协议兼容性极佳,物化视图自动改写,实时导入延迟 < 1s | 需要快速对接 BI 工具(Tableau/Superset)、有强实时性要求的 SaaS 产品 | 高基数字符串维度(如 URL)索引膨胀快,需预处理 |
| ClickHouse | 单表分析性能无敌,向量化执行引擎,压缩率极高 | 日志分析、用户行为宽表、离线报表加速 | JOIN 性能弱,复杂维度关联需冗余宽表 |
| Apache Druid | 原生支持时间序列、高并发低延迟、亚秒级响应 | 实时监控大盘、广告效果归因、IoT 设备指标聚合 | SQL 支持较弱,学习成本高,运维复杂 |
选型决策树(基于真实项目):
- 如果你的数据源主要是 Kafka 实时流 + MySQL 维表,且 BI 工具已用 Tableau,Doris 是唯一选择。我们用 Doris 替换掉旧 Hadoop+Hive 架构后,报表首屏加载从 12 秒降到 0.6 秒,运维工作量减少 70%。
- 如果你有一张 50 亿行的用户点击日志表,主要做“设备类型 × 页面路径 × 时间窗口”的漏斗分析,ClickHouse 是答案。其
ReplacingMergeTree引擎能自动去重,arrayJoin函数完美处理埋点中的数组字段。 - 如果你在做金融风控,需要每秒处理 10 万笔交易事件,并实时计算“过去 5 分钟内同一 IP 的交易频次”,Druid 的时间分区 + Rollup 是刚需。
关键提醒:OLAP 引擎不是“银弹”。我见过团队盲目上 ClickHouse,结果因为没做维度字典编码(dictionary encoding),把 10 亿个 UUID 存成 String,磁盘占用暴涨 4 倍,查询反而变慢。多维聚合的性能,30% 在引擎,70% 在数据建模质量。务必先做好维度表主键设计、事实表分区策略、常用过滤字段的索引。
4. 实战全流程:从原始日志到自助分析报表的七步炼金术
4.1 Step 1:原始数据探查与维度识别(2 小时)
拿到一份 200GB 的 Nginx 日志,第一件事不是写 SQL,而是用head/awk/jq快速扫描:
# 查看前 5 行结构 head -5 access.log # 统计 status 码分布(识别“成功/失败”维度) awk '{print $9}' access.log | sort | uniq -c | sort -nr # 提取 JSON 字段中的 user_id(识别“用户”维度) jq -r '.user_id' events.json | head -10目标:标记出所有潜在维度字段(如status,user_id,url_path,http_referer,device_type)和度量字段(如body_bytes_sent,request_time)。此时要警惕“伪维度”:http_referer里包含大量动态参数(?utm_source=...),直接作为维度会导致基数爆炸。正确做法是用正则提取干净域名:REGEXP_EXTRACT(http_referer, r'https?://([^/]+)')。
4.2 Step 2:构建维度表(1 天)
以dim_device为例,不能简单SELECT DISTINCT device_type FROM raw_log。真实设备类型有数百种(iPhone14,2,SM-G998B,Pixel 7 Pro),但业务只关心“iOS/Android/Desktop”。所以维度表必须包含:
CREATE TABLE dim_device ( device_key BIGINT PRIMARY KEY, device_full_name STRING, -- 原始值 device_os STRING, -- iOS/Android/Web device_brand STRING, -- Apple/Samsung/Google device_category STRING, -- Mobile/Tablet/Desktop is_mobile BOOLEAN, load_ts DATETIME ); -- ETL 逻辑(用正则分类) INSERT INTO dim_device SELECT ROW_NUMBER() OVER() AS device_key, device_full_name, CASE WHEN device_full_name LIKE 'iPhone%' OR device_full_name LIKE 'iPad%' THEN 'iOS' WHEN device_full_name LIKE 'SM-%' OR device_full_name LIKE 'RMX%' THEN 'Android' ELSE 'Web' END AS device_os, ... -- 其他字段 FROM (SELECT DISTINCT device_full_name FROM raw_log) t;经验:维度表必须带load_ts和is_current字段,为未来支持缓慢变化维度(SCD Type 2)留接口。
4.3 Step 3:事实表建模与 ETL(2 天)
事实表设计是成败关键。我们采用事务型事实表(Transactional Fact Table),每行代表一次原始事件:
CREATE TABLE sales_fct ( sale_id BIGINT, product_key INT NOT NULL, store_key INT NOT NULL, date_key INT NOT NULL, customer_key INT NOT NULL, quantity INT, amount DECIMAL(18,2), discount DECIMAL(18,2), -- 代理键(surrogate key)替代原始业务键,保证稳定性 etl_batch_id STRING, load_ts DATETIME ) PARTITION BY RANGE (date_key) ( PARTITION p202401 VALUES LESS THAN (20240201), PARTITION p202402 VALUES LESS THAN (20240301) );ETL 过程中,必须用 LEFT JOIN 维度表,并设置COALESCE(dim.key, -1)处理未匹配的外键。否则,一条脏数据(如store_id='ABC'在dim_store中不存在)会导致整条事实记录丢失。我们约定:所有维度表的-1键对应Unknown或Invalid成员,确保事实表完整性。
4.4 Step 4:预计算聚合层(1 天)
为加速高频查询,建立汇总表(Aggregate Table)。不是盲目预计算所有组合,而是基于 A/B 测试数据:
-- 分析 BI 工具中 Top 10 查询模式,发现 80% 请求是: -- (date, product_category) 和 (date, store_city) -- 因此创建两个物化视图 CREATE MATERIALIZED VIEW mv_daily_category AS SELECT d.year_month AS year_month, p.category AS category, COUNT(*) AS order_cnt, SUM(f.amount) AS total_amount FROM sales_fct f JOIN dim_date d ON f.date_key = d.date_key JOIN dim_product p ON f.product_key = p.product_key GROUP BY d.year_month, p.category; CREATE MATERIALIZED VIEW mv_daily_city AS SELECT d.date_key, s.city, SUM(f.amount) AS daily_city_revenue FROM sales_fct f JOIN dim_date d ON f.date_key = d.date_key JOIN dim_store s ON f.store_key = s.store_key GROUP BY d.date_key, s.city;避坑:物化视图的刷新策略必须与业务 SLA 对齐。我们设定mv_daily_city每 15 分钟增量刷新(基于load_ts),而mv_daily_category每日全量重建(因类目变更极少)。
4.5 Step 5:BI 工具建模(半天)
在 Superset 中,不是直接连sales_fct,而是:
- 数据源:连接
mv_daily_city和mv_daily_category视图 - 数据集(Dataset):为每个视图创建独立 Dataset,并在 UI 中配置:
date_key→ 设置为 “Time Column”,时区设为业务所在地city/category→ 设置为 “Filterable Column”,启用搜索total_amount→ 设置为 “Metric”,格式化为货币
- 探索(Explore):拖拽生成图表时,Superset 自动生成的 SQL 会自动命中物化视图,而非扫描全表。
4.6 Step 6:自助分析权限控制(2 小时)
多维分析最大的风险不是性能,而是数据越权。我们采用行级安全(Row Level Security, RLS):
-- 在 Doris 中创建 RLS 策略 CREATE ROW POLICY sales_rls ON sales_fct AS RESTRICTIVE TO analyst_team USING (store_key IN ( SELECT store_key FROM dim_store WHERE region = 'East' ));这样,华东区分析师登录后,所有查询自动追加WHERE store_key IN (华东门店列表),连SELECT * FROM sales_fct LIMIT 10都看不到其他区域数据。RLS 是多维分析的“安全阀”,没有它,再好的模型都是裸奔。
4.7 Step 7:监控与迭代(持续)
上线不是终点。我们部署了三类监控:
- 数据质量:每日校验
sales_fct行数 vsraw_log行数,偏差 > 0.5% 告警 - 查询性能:采集 Top 20 慢查询,自动分析执行计划,识别缺失索引
- 使用热度:统计各维度组合的查询频次,淘汰连续 30 天零调用的物化视图
上周发现mv_daily_category的查询占比从 45% 降至 12%,而(date, customer_segment)新增至 38%。立刻启动迭代:停用旧 MV,新建mv_daily_segment,整个过程 4 小时完成。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 问题:Pandaspivot_table报错 “Index contains duplicate entries”
现象:df.pivot_table(index='city', columns='month', values='sales')抛出ValueError: Index contains duplicate entries。
根因:city和month的组合在原始数据中不唯一。例如上海 2024-01 有 1000 笔订单,pivot_table默认用np.mean聚合,但如果你没指定aggfunc,它会尝试用np.average,而当存在重复索引时,average无法确定如何合并。
解决方案:
# 方案1:显式指定聚合函数(推荐) df.pivot_table( index='city', columns='month', values='sales', aggfunc='sum', # 或 'count', 'mean' fill_value=0 ) # 方案2:先去重或确认组合唯一性 print(df.duplicated(subset=['city', 'month']).sum()) # 查看重复数 df_clean = df.drop_duplicates(subset=['city', 'month']) # 若业务允许实操心得:永远不要假设维度组合天然唯一。在
pivot_table前,先运行df.groupby(['city', 'month']).size().max(),如果结果 > 1,就必须指定aggfunc。
5.2 问题:SQL 中GROUP BY CUBE结果里,NULL 值到底是“空值”还是“合计”?
现象:SELECT city, month, SUM(sales) FROM t GROUP BY CUBE(city, month)返回一行city=NULL, month=NULL, SUM=1000000,但另一行city='Shanghai', month=NULL, SUM=200000,怎么区分哪个 NULL 是真缺失?
解法:GROUPING()函数
SELECT CASE WHEN GROUPING(city) = 1 THEN 'All Cities' ELSE city END AS city_label, CASE WHEN GROUPING(month) = 1 THEN 'All Months' ELSE month END AS month_label, SUM(sales) AS total FROM t GROUP BY CUBE(city, month);GROUPING(city)返回 1 表示该行中city是由 CUBE 生成的合计值(即“所有城市”),返回 0 表示是真实数据值。这是唯一可靠的方式,比看IS NULL准确一万倍。
5.3 问题:OLAP 引擎查询突然变慢,执行计划显示Using filesort
现象:Doris 查询耗时从 200ms 暴涨到 15s,EXPLAIN 显示Using filesort。
排查路径:
- 检查排序字段是否在排序键(Sort Key)中:Doris 要求
ORDER BY字段必须是建表时SORT KEY的前缀。如果建表用SORT KEY(product_key, date_key),但查询ORDER BY date_key, city,就会触发 filesort。 - 检查数据倾斜:
EXPLAIN中看Fragment的Max Peak Memory是否某节点远高于其他。如果是,说明city维度存在热点(如“上海”数据量是其他城市的 100 倍),需在 ETL 阶段对热点城市做salting(加随机前缀再哈希)。 - 检查物化视图是否失效:
SHOW ALTER TABLE mv_name;确认 MV 状态为FINISHED。
终极技巧:在 Doris 中,对高基数维度(如user_id)做聚合时,永远用COUNT(DISTINCT user_id)而非COUNT(user_id)。后者会统计所有行,前者才真正反映独立用户数,且 Doris 对COUNT(DISTINCT)有专门优化。
5.4 问题:维度表更新后,事实表关联出现大量 NULL
现象:dim_store新增了 50 家门店,但sales_fct中这些门店的city字段全是 NULL。
根因:ETL 作业未重新运行,或dim_store的store_key是自增 ID,而新门店的store_key超出了事实表中store_key的历史范围,导致 JOIN 失败。
标准流程:
- 维度表更新必须走CDC(Change Data Capture):用 Debezium 监听 MySQL binlog,实时同步到
dim_store。 - 事实表 ETL 必须以维度表为驱动:先全量拉取
dim_store的最新store_key列表,再用WHERE store_id IN (...)过滤原始日志,确保事实表只包含有效维度键。 - 建立维度一致性检查脚本:每日跑
SELECT COUNT(*) FROM sales_fct f LEFT JOIN dim_store s ON f.store_key = s.store_key WHERE s.store_key IS NULL,结果 > 0 立即告警。
最后分享一个小技巧:在所有维度表的
name字段后,加一列name_for_display,内容为COALESCE(name, 'Unknown')。这样,前端报表永远不用处理 NULL,用户体验提升 100%。这个细节,是我在 7 个客户项目中,被问得最多的问题之一。