ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Power BI度量值从入门到实战:DAX、CALCULATE与时间智能

Power BI度量值从入门到实战:DAX、CALCULATE与时间智能 1. 先别急着写公式度量值到底在解决什么问题我刚开始用 Power BI 的时候最困惑的不是怎么连数据源也不是怎么拖图表而是那排空荡荡的度量值——右键新建一个弹出一个空框让我在里面写一行公式我完全不知道自己该写什么。那时候我把 Excel 里的做法原封不动搬了过来把销售额字段拖到卡片图里把数量字段拖到表格里看着数字跳出来觉得这不挺好用吗。直到业务方问我一句能不能给我看一下华东区今年比去年涨了多少个点我当场卡住了——字段拖拽给我的是全量求和它不会替我判断华东今年去年更不会替我做那个除法。后来我才慢慢想明白Power BI 里字段和度量值的分工有点像厨房里的食材和菜谱。字段是你从数据源搬进来的原料销售额、数量、成本、日期、门店名称这些原料本身没有业务含义它们只是一列一列的值。而度量值是把原料按业务规则加工出来的成品毛利率、客单价、同比增速、目标达成率、库存周转天数。你在报表上看到的每一个会随着切片器跳动的数字背后站着的基本都是一个度量值。所以这篇东西想聊清楚一件事度量值Measure到底是什么它凭什么能随筛选变化写它的正确姿势是什么以及我在实战里踩过的那些坑。如果你是从 Excel 数据透视表转过来的人或者已经在用 Power BI 做报表但只会拖字段、不敢碰 DAX下面的内容应该对你有用。我不打算把它写成函数手册而是按为什么这么设计 — 怎么写才对 — 出错怎么查的顺序往下捋。1.1 一张事实表和一张报表之间隔着一层业务语言很多人以为把数据导进来、把关系建好报表就自然成立了。其实中间还缺一层东西业务规则的固化。字段销售额做求和得到的是数据库里那列的算术总和可业务口径上的销售额往往要剔除退货、剔除赠品、剔除内部调拨单、只算已确认收入的那部分。这些规则如果每次都在脑子里过一遍报表人一多、表一多口径必然打架。度量值的价值就在这里它是一个被命名的、可复用的业务口径。总销售额净销售额有效订单数这几个名字一旦定下来全公司所有页面、所有图表调用的都是同一个定义。改口径的时候只改一处整份报表跟着变。这一点是字段拖拽永远做不到的——你把字段拖到十张表里就等于把同一个隐含假设复制了十遍任何一处需要调整你都得靠人肉去翻。再往下说度量值还是性能上的省油灯。它不占内存里的存储空间不会在数据刷新时被一行一行算出来存进去而是等你把它放到某个视觉对象里、那一刻的筛选条件确定之后才现算一次。你的模型里可以放两三百个度量值模型体积几乎不变但如果同样逻辑做成计算列那就是给事实表的每一行都加了一列几百万行乘上去文件立刻胖一圈。1.2 度量值、计算列、自定义列三者的边界到底在哪这三个东西新手最容易混。我的经验是别去背概念直接记住一句话计算列是给每一行贴标签度量值是给一组行算总账。贴标签的结果会存下来能拿去当切片器、当行标题、当关系键算总账的结果只在当次查询里存在天生就是动态的。具体差别我整理成一张表你对着看就清楚了对比维度度量值Measure计算列Calculated Column自定义列Power Query计算时机查询时随筛选实时算刷新时一次性算完存下刷新时在加载阶段算是否占存储几乎不占占内存行数越多越明显占内存同样随行数膨胀计算环境筛选上下文行上下文表格行逐行处理能否放进切片器不能可以可以能否做关系两端的键不能可以可以典型用途同比、占比、达成率、排名分档、标记、拼接键清洗、拆分、合并、改类型我见过太多人拿计算列去做占比——新建一列[销售额]/SUM([销售额])结果整列全是 1 或者全是几十怎么看怎么不对。原因就在这张表的第三行计算列活在行上下文里它看到的是这一行不是这一批行。想算占比必须用度量值因为只有度量值能被筛选上下文包住。2. 拆开看本质度量值是一段随上下文变化的计算脚本如果只让我用一句话解释度量值我会说它不是一个数字而是一段被调用的计算逻辑调用时把当时的筛选条件当作输入参数。这句话有两个关键词一个是被调用一个是当时的筛选条件。理解了这两个DAX 里那些看起来玄乎的函数尤其是 CALCULATE就都不神秘了。你可以把度量值想成自动售货机里的那套出货程序。同一个按钮你投的币不同、选的品类不同出来的东西就不一样但程序本身没变。度量值也是[总销售额]这个名字在任何页面上都指向同一段代码只不过在华东 2024 年 品类 A这个格子里它算出来的是这个小格子的和换到华南 2025 年同一段代码算出来的是另一个数。2.1 筛选上下文度量值真正的工作台筛选上下文Filter Context是 DAX 里最核心也最容易被忽略的概念。它指的是当前这次计算哪些行被留下来参与了运算。它从哪来三个来源视觉对象里的行、列、图例、小多图带来的分组切片器、筛选器窗格带来的条件以及 DAX 里用 CALCULATE 手动加上的条件。举个很日常的例子。你在表格视觉对象里放了两列行是大区值是[总销售额]。当 Power BI 渲染华北大区这一格的时候它临时把事实表筛成只剩华北的记录然后才去执行SUM(销售明细[销售额])。渲染华南那一格时重新筛一次重新算一遍。这不是算了一次然后拆开而是每一格都在独立地跑一遍你的公式。所以一个 20 行 × 5 列的矩阵背后可能是 100 次以上的计算过程这也是为什么度量值写得不讲究时页面会明显变慢。注意表格或矩阵里的小计行、总计行也是独立的计算它们用的是去掉该维度筛选之后的上下文而不是把明细行加起来。这就是为什么有时候总计对不上明细之和——不是 Bug是你的公式在总计行里按另一套逻辑跑了一遍。2.2 行上下文只活在计算列和迭代函数里和筛选上下文相对的是行上下文Row Context。它只有两个地方会出现一是在计算列里二是在带 X 结尾的迭代函数里SUMX、AVERAGEX、FILTER、ADDCOLUMNS 这一族。行上下文的含义是我正在处理这一行它不负责筛选只负责当前是谁。这个区别我用一个真实的翻车现场来说明。有同事要算每条订单行贡献了多少销售额占比他写了计算列[金额]/SUM(明细[金额])。结果每行都是 100%。因为SUM(明细[金额])在行上下文里并不会被限制成这一行它算的是整列的总和而[金额]是这一行的值占比自然变成了这一行占总和的比——等一下那不该是小数值吗问题出在计算列里的SUM没有收到任何筛选整列求和而分母也是整列求和比值恒等于 1 的幂次……总之结果毫无意义。正确写法是改成度量值DIVIDE(SUM(明细[金额]), CALCULATE(SUM(明细[金额]), ALL(明细)))。行上下文还有一个经常被忽略的细节它不会自动传递到关联表。你在明细表的一行上写RELATED(产品[品类])能取值是因为有关系链可以走但如果你在行上下文里直接引用产品[品类]而不加 RELATED就会报错或者表现得莫名其妙。这也是新手调试计算列时最常见的困惑之一。2.3 上下文转换CALCULATE 那一下隐身衣DAX 里最美妙也最反直觉的设计是上下文转换。简单说当你把一个度量值放进 CALCULATE 的参数里或者放进 SUMX/FILTER 这类迭代函数里时DAX 会悄悄地把当前行转换成对这张表的筛选。原本的行上下文一瞬间变成了筛选上下文。这解释了为什么下面这段代码能正常工作高价值订单数 COUNTROWS ( FILTER ( 订单, [总销售额] 10000 ) )FILTER 在订单表上逐行迭代形成行上下文而内层的[总销售额]是个度量值它需要筛选上下文才能算。DAX 在每一行上都做了一次上下文转换把当前这一行订单变成筛选这张订单表只留这一行于是[总销售额]得到的是这条订单自己的金额。整个过程没有任何显式的 CALCULATE但 CALCULATE 的机制一直在后台工作。理解了这一层你在写筛选出满足条件的客户计算每个门店的排名做动态分组这类需求时就不会再被为什么这里能用度量值那里不能用绕进去了。判断标准很简单迭代函数内部要用度量值就必然发生上下文转换要用列的原始值就可能需要 RELATED 或加 ALL 去掉筛选。3. 动手实操从原始数据到一套能用的度量值理论说够了我们上真东西。下面这套流程是我在大多数项目里都会走的标准路线数据用一份典型的销售明细订单号、日期、门店、品类、数量、单价、金额你可以拿自己手头的任何明细表套进去。3.1 数据接入与轻量建模顺带聊聊 MySQL 数据源那点事先用获取数据把明细拉进来。如果你的库是 MySQL就在列表里选 MySQL 数据库连接器填服务器和数据库名。这里有几个我在现场踩过的点值得单独拎出来说。第一是驱动的位数。MySQL 官方那个 .NET 提供程序很多人习惯叫 Connector/NET分 32 位和 64 位你装的桌面端如果是 64 位版本驱动也必须装 64 位。位数不匹配时连接界面能打开、参数也能填但一点确定就报找不到提供程序或者类似提示新手很容易以为是账号密码的问题在那反复输密码。我一般的做法是先确认位数再谈其他。第二是导入还是 DirectQuery。明细表行数在几十万以内、刷新频率是每天一次我一律选导入速度快、DAX 全功能可用。只有当数据量特别大、业务要求准实时、或者数据合规上不允许落地副本时才考虑直连模式。直连模式下不少时间智能函数和部分 DAX 能力会受限这一点要在建模前就跟业务说清楚别做到一半才发现同比算不出来。第三是类型和列名。MySQL 那边如果日期存的是字符串进到 Power BI 里默认就是文本后面做时间智能会直接报错。老老实实在 Power Query 里转成日期类型顺便把明显为空的列去掉、把中文列名统一。这一步花十分钟能省掉后面两小时的排查。接下来是建模也就是把扁平明细拆成星型结构。日期表我会单独用 DAX 建一张这是时间智能的硬性前提日期表 ADDCOLUMNS ( CALENDAR ( DATE ( 2022, 1, 1 ), DATE ( 2025, 12, 31 ) ), 年份, YEAR ( [Date] ), 月份, FORMAT ( [Date], MM ), 年月, FORMAT ( [Date], yyyy-MM ), 季度, Q QUARTER ( [Date] ), 星期, FORMAT ( [Date], aaaa ) )建完之后一定要做两件事把日期列标记为日期表右键列 → 标记为日期表并且和事实表的日期列建立一对多、单向的关系。漏掉标记这一步SAMEPERIODLASTYEAR、DATESINPERIOD这类函数会直接给你报错或返回空值而且报错信息很不友好只说函数需要日期表。提示日期表一定要连续、无重复、覆盖事实表全部日期范围。我见过因为事实表里有 2021 年的老单据而日期表从 2022 年开始导致去年永远算不出来排查了半天。3.2 第一组基础度量值先把地基打牢基础度量值要少而精。我的习惯是先建三到五个原子度量值后面所有复杂指标都由它们拼出来。原子度量值只做最纯粹的聚合不带任何条件总销售额 SUM ( 销售明细[金额] ) 总数量 SUM ( 销售明细[数量] ) 订单数 DISTINCTCOUNT ( 销售明细[订单号] ) 客单价 DIVIDE ( [总销售额], [订单数] )这里有两处细节值得展开。一是为什么用 DIVIDE 而不是斜杠。DIVIDE在分母为零或空时返回空值或者你指定的备用值不会抛错直接用/在分母为零时会报错在总计行里尤其容易翻车。这是个习惯问题一旦养成能省掉大量调试时间。二是订单数为什么用 DISTINCTCOUNT。明细表是按商品行展开的一个订单可能有多行COUNT会把行数当成订单数COUNTROWS也一样。要去重数订单号才准。我见过报表上客单价只有几十块钱看着挺正常实际上是订单数被放大了五六倍实际客单价应该是一两百——这种错误最危险因为它不报错数据看着也合理。原子度量值建好后衍生指标就都是拼装毛利 [总销售额] - [总成本] 毛利率 DIVIDE ( [毛利], [总销售额] ) 单件均价 DIVIDE ( [总销售额], [总数量] )注意[总成本]这类也要提前建好原子度量值别在每个衍生指标里重复写SUM(销售明细[成本])。原因有两层一是维护成本口径变了要改十处二是性能重复写会让引擎多做几次聚合。更隐蔽的是第三层——你写重复了某天只改了其中几处报表就会出现同一个指标在不同页面数字不一样的诡异现象。格式设置也别偷懒。金额设成货币保留两位比率设成百分比保留一位订单数设成整数不带千分位。这件事看着琐碎但它是报表专业度的分水岭。我在评审会上见过因为毛利率显示成0.24而不是24.0%被业务方质疑这个数据是不是错的其实只是格式没设。3.3 CALCULATE让度量值学会自己改条件前面说度量值是被筛选上下文驱动的而 CALCULATE 就是那个能改写筛选上下文的开关。它是 DAX 里最重要的函数没有之一。基本形状是CALCULATE ( 表达式, 筛选条件1, 筛选条件2, ... )第一个参数是要算的度量值或表达式后面跟的是一串筛选条件。这些条件的威力在于它们会覆盖外部的同类筛选。举几个我天天在用的写法。固定口径的指标华北销售额 CALCULATE ( [总销售额], 门店[大区] 华北 )这个度量值不管切片器选了哪个大区永远返回华北的数字。适合做重点区域卡片。占比类指标品类内占比 DIVIDE ( [总销售额], CALCULATE ( [总销售额], ALL ( 产品[品类] ) ) )ALL(产品[品类])的作用是把品类这一列的筛选清掉分母变成所有品类的总和于是分子除以分母就是该品类在整体里的占比。这里千万别用 ALL(产品) 整表清除那样会把品牌、系列等其他列的筛选也一起清掉在更复杂的层级里会算错。达成率类指标目标达成率 VAR 实际 [总销售额] VAR 目标 SUM ( 目标表[目标金额] ) RETURN DIVIDE ( 实际, 目标 )这里出现了 VAR。我强烈建议你从第一天写 DAX 就用 VAR理由有三个性能上变量只算一次后面的引用是复用结果可读性上实际和目标比嵌套三层括号清楚得多调试上你可以先 RETURN 中间变量看看值对不对。养成这个习惯后你的 DAX 代码质量会立刻上一个台阶。TOP N 或排名类通常需要配合迭代函数门店销售排名 VAR 当前销售额 [总销售额] VAR 全部门店表 ADDCOLUMNS ( ALLSELECTED ( 门店[门店名称] ), 销售额, [总销售额] ) RETURN COUNTROWS ( FILTER ( 全部门店表, [销售额] 当前销售额 ) ) 1这段代码值得细看。ALLSELECTED(门店[门店名称])会保留切片器里选中的门店范围但去掉视觉对象行带来的筛选——这正是排名该有的语义在用户当前关心的这批门店里排。ADDCOLUMNS为每个门店算一遍销售额最后数一数比自己高的有几个加一就是名次。注意过滤条件用的是而不是这样并列第一会同时显示 1而不是 1、2 交替。3.4 时间智能同比、环比、滚动十二个月时间智能是我认为学完立刻见效的一块。前提只有一个有一张标记好的日期表并且日期关系已经建好。先把基期算出来去年同期销售额 CALCULATE ( [总销售额], SAMEPERIODLASTYEAR ( 日期表[日期] ) ) 上个月销售额 CALCULATE ( [总销售额], DATEADD ( 日期表[日期], -1, MONTH ) )然后在基期上做增速同比增速 VAR 本期 [总销售额] VAR 同期 [去年同期销售额] RETURN DIVIDE ( 本期 - 同期, 同期 )一定要用 DIVIDE 包住否则去年同期为零新开门店、新品时会直接报错页面上一片红。这个坑我在一个零售项目里遇到过——新开的三家店去年同期没有数据整页同比卡片全挂着错误提示非常难看。滚动十二个月也类似滚动12个月销售额 CALCULATE ( [总销售额], DATESINPERIOD ( 日期表[日期], MAX ( 日期表[日期] ), -12, MONTH ) )MAX(日期表[日期])取的是当前上下文里最新的一天作为滚动窗口的终点往前推十二个月。这个度量值是做趋势分析的神器因为它能把季节性抹平让趋势线更好看。我一般会把它和当月销售额放在同一张折线图里对比业务方一眼就能看出这个月下滑是正常季节波动还是真出问题了。需要提醒的是时间智能必须用在日期列上不能用在文本型的年月列上。有同事为了省事把日期表里的年月文本列拿来给 DATEADD 用结果报错。如果确实需要按年月做滚动就得换 DATESINPERIOD 之类的窗口函数或者干脆在日期表里保留完整的日期列专门给时间智能用。3.5 把度量值放回视觉对象里验证度量值写完了不算完一定要放回去验证。我的验证清单是四条一是明细核对把某一天的明细在 Excel 或者数据库里手工加一遍和卡片图对二是边界测试把切片器切到只有一个门店、只有一天、空品类这些极端情况看会不会报错或者出现异常值三是总计核对看表格的总计行和明细行的关系是否符合预期如果是占比类明细应该加起来 100%四是性能体感在人最多的页面上拖一遍看交互是否卡顿。这四步里第二步最容易被跳过也最容易出事。空值、零分母、日期边界、单值切片——这四个场景基本能覆盖 80% 的线上事故。我在交付前一定会专门留半小时做这轮边界测试比事后被业务方追着改要舒服得多。4. 踩坑实录度量值最常见的十类问题与排查套路度量值这东西写对了不吭声写错了要么报错、要么给你一个看着挺像那么回事的错数字。后者更可怕。下面这些是我这些年攒下来的典型症状配着原因和处理办法你可以当速查表用。4.1 现象 → 原因 → 处理一张表说清现象常见原因处理办法总计行和明细之和不一致总计是独立计算非明细相加确认指标语义占比类属正常求和类需检查筛选传递每行数据都一样不随行变化度量值里用了 ALL 清了行筛选检查 ALL / ALLSELECTED 的作用列范围同比显示为空日期表未标记或日期不连续标记日期表补齐日期范围同比报错一片红分母为零或空用 DIVIDE 并给备用值占比加起来不到 100%分母用了 ALL 全表而非单列把 ALL 的作用范围收窄到具体列换页面数字变了存在重复定义的同名口径统一到一个原子度量值上刷新时计算列慢用了 SUM 之类聚合在计算列里改成度量值或改用迭代函数数据源连接失败驱动位数与桌面端不匹配32/64 位对齐重装驱动小数显示成整数未设置格式单独设置格式比率用百分比排名出现并列跳号过滤用了 或排序不稳用 或加入次级排序键4.2 性能问题慢在哪里怎么定位度量值本身不慢慢的是不合理的写法。最常见的三个性能杀手一是在度量值里对整张表做 ALL等于每次都把几百万行的维度展开一次二是嵌套迭代SUMX 里面套 FILTER 里面再套 SUMX复杂度是乘法级增长三是滥用计算列做中介几十个计算列堆在事实表上刷新时间和内存都吃不消。排查方法上我一般会先用性能分析器看每个视觉对象耗时把超过两秒的挑出来再逐个精简公式优先把能提取成 VAR 的部分提出来把不必要的 ALL 收窄到具体列。实测下来仅仅是该用 VAR 的地方用 VAR、该收窄 ALL 的地方收窄这两条通常就能把页面响应压下来三成以上。还有一个隐蔽的性能坑在视觉对象里放了用不上的字段。比如为了排序或者提示塞了一个高基数的订单号进去虽然不显示但引擎照样要为每个订单号算一遍度量值。这种看不见的行造成的卡顿最容易被忽略。4.3 命名与维护三个月后的自己会感谢现在的你关于命名我有两条近乎偏执的原则。第一原子度量值和衍生指标分开命名。原子就叫总销售额总成本订单数衍生就叫毛利率同比增速目标达成率。看到名字就知道它是哪一层改的时候不会误伤。第二绝不使用无意义缩写。Sale_Amt_v2_final这种名字三个月后连你自己都要点进去看公式才知道是什么。中文命名在中文团队里其实非常好用上个月销售额比LM_Sales直观太多了。另外给度量值加描述是个低成本高回报的动作。在模型视图里选中度量值填上业务口径、计算方式、更新责任人。报表交接的时候这份描述比任何文档都管用。我经历过一次人员变动交接靠的就是每个度量值下面那句描述接手的人一上午就把八十多个指标摸清了。注意不要在同一个模型里保留旧的但还有人用的度量值。要么改要么明确标记为废弃。我见过一个模型里躺着销售额销售额_新销售额_最终版三个同义度量值报表不同页面各用各的最后对不上账全组排查了两天。5. 再往前走一步度量值的组织与进阶玩法基础打好之后剩下的是怎么让几百个度量值不变成灾难。5.1 度量值表和显示文件夹模型里的度量值默认散落在各张表下面几十个还好上百个就找不着了。标准做法是建一张度量值表新建一个表贴一行表 {BLANK()}之类的构造语句得到一个空表把它隐藏起来或者改个_度量值的名字再把所有度量值都放到它下面。这样做的好处是维度表区域干干净净所有指标集中在一处找起来一目了然。再配上一层显示文件夹按销售库存财务时间对比分类嵌套两层就够了别做太深。我的经验是文件夹层级超过两层点击成本就开始大于收益。同时把用不到的度量值隐藏起来只把业务真正会用的那二三十个露在外面交给业务方自助分析时他们会感激你。5.2 VAR、迭代函数与错误处理三件套VAR 前面说过这里再补一个细节VAR 的求值时机是定义时。也就是说变量一旦赋值后面即使上下文发生变化它也不会重新算。这在某些场景下会带来惊喜比如你想把当前筛选下的销售额固定下来备用在另一些场景下会带来惊吓比如你以为它会跟着行变化结果一直返回同一个值。判断方法很简单把变量拆开、直接在 RETURN 里写表达式如果结果变了那说明你对它的求值时机理解有偏差。错误处理上除了 DIVIDE还有两个常用手段。一是IFERROR包一层适合处理不可预期的异常二是ISBLANK判断适合处理空值导致的格式错乱。但我要说句实话宁可让它报错也不要悄悄返回 0。返回 0 的问题在于你会在报表上看到一条平平的零线以为是真实业务数据直到有人问这个月销量真的是零吗才发现是公式吃掉了异常。让错误显性地暴露出来比掩盖它安全得多。5.3 计算组把格式和时间对比一次做对如果你的模型里已经有一批度量值忽然接到需求所有指标都要能看同比和环比逐个改公式会累死。这时候可以用计算组Calculation Group在 Tabular Editor 这类外部工具里建一个计算组表定义好同比环比累计这几个计算项一次配置应用到所有度量值上。报表里只要把这个计算组的列拖进去所有指标自动多出这几列不用动一行 DAX。计算组的另一个常见用途是统一格式比如金额类都加千分位、比率类都转百分比通过格式表达式自动匹配省掉一个个手点格式的时间。这一块属于建模进阶内容需要外部工具支持我建议先把基础度量值写扎实了再碰否则容易在调试计算组的时候被多层上下文绕晕。如果你还在犹豫要不要投入时间学度量值我的看法是字段拖拽能做出能看的报表度量值才能做出能用的报表。前者是给人看的后者是要拿去支撑决策的。我在实际项目里的体会是一个模型有没有认真设计度量值业务方的提问方式就能看出来——用字段堆出来的报表业务问的是这个数是什么意思用度量值搭起来的报表业务问的是为什么这个月掉了。后一种提问才是一个 BI 从业者真正想要面对的。
返回列表