ARTICLE DETAIL

资讯详情

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

Power BI 度量值实战:计算列、筛选上下文与 DAX 性能优化

Power BI 度量值实战:计算列、筛选上下文与 DAX 性能优化 上周一个做电商运营的朋友把她重建了三次的 Power BI 报表甩给我问题很具体按品类拆的销售额占比明细行每一行都是对的加起来却冲到 300% 多。她一口咬定是数据源污染了数据我把模型打开看了一眼就笑了——她把占比做成了一个计算列而不是度量值。这个错误几乎是每个从 Excel 转过来的人都会踩的第一脚在 Excel 里一行一个公式、往下拉是刻进肌肉记忆的操作到了 Power BI 里这个习惯会把模型带进沟里。度量值Measure这三个字看着不起眼它是整个 Power BI 从会做图跨到会建模的那道门槛。这篇就把它掰开讲清楚度量值到底是个什么东西、它和计算列的本质差别在哪、筛选上下文这套机制怎么运转、常见的比率和时间智能度量值怎么写、模型变大之后怎么组织、以及数据源和刷新环节那些让人抓狂的连带问题。不管你是刚拖出第一张柱状图的新手还是已经把 DAX 写得挺顺但总被性能教育的老手应该都能捞到点东西。1. 一张占比冲到300%的报表把度量值的概念缺口摊开了1.1 计算列算的是每一行度量值算的是一次筛选结果先把那个 300% 的案子拆开看。她建了一个计算列表达式大概长这样占比 销售[销售额] / SUM(销售[销售额])这个公式在计算列的语境里跑起来是这样的Power BI 在刷新数据的那一刻逐行走过销售表的每一行对每一行算一次我的销售额 ÷ 全部销售额。第一行 5%第二行 3%第三行 8%……每一行都得到一个合理的小数加起来正好 100%。这些结果算完之后被固化存进模型里变成销售表上一个普普通通的数值字段。问题出在她把这个字段拖到矩阵的值区域之后。矩阵的明细行有行身份每一行就取自己那一格已经存好的数看起来完全正常可到了底部的总计行矩阵没有某一行可以取值了它只能做一件最朴素的事——把上面所有行已经存好的占比再相加一次。三行相加是 16%三十行相加就是 100% 往上跑几百个 SKU 堆起来合计自然就冲到 300% 甚至更高。如果换成度量值结局完全不同。度量值不会被提前算好、也不会被存下来它更像一段被注册进模型的查询指令。当矩阵要渲染总计那一行时它会把这段指令重新执行一遍而此时传进来的筛选条件是所有品类分母和分子都在这个新条件下重算得出来的就是 100%。同一段文字在不同单元格里跑出了不同的结果这才是度量值真正有意思的地方。1.2 度量值不是一个值是一段随叫随到的计算逻辑很多人第一次听到度量值这个名字会下意识以为它是一个存在某处的数字。恰恰相反度量值本身在模型里不占存储空间你在模型视图里看到它旁边那个小小的计算器图标就说明它和普通的列不是一类东西。一个完整的度量值由三部分组成一个名字、一段返回标量的 DAX 表达式、一套格式设置。它不能用来做切片器、不能放到行标签或列标签上除非你用计算组那是另一个话题也不能被排序——因为它没有行的概念它只会在某个筛选条件下返回一个数或者返回空。我用一个类比来解释这件事。想象一个 40 人的班级老师手里有一张花名册。计算列相当于老师在开学第一天挨个点名让每个学生把自己的分数算好、写在自己的小卡片上之后无论谁来问都是看那张打印好的卡片。度量值相当于老师手里握着一本规则手册每次有人来问三班女生的平均分是多少老师就翻到对应那页现场按当前的口径重新统计一遍。前一任老师给的答案再准遇到新问题也没用后一种做法慢一点但什么口径都能接得住。这个差别直接决定了选型凡是需要在不同粒度、不同筛选组合下自动重算的东西必须用度量值凡是需要作为一个固定的属性摆在行、列、切片器上的东西才用计算列。1.3 什么时候必须上度量值什么时候老老实实用计算列我把这几年用得最多的判断标准整理成一张表基本可以覆盖九成场景需求该用度量值还是计算列原因求和、平均、计数、去重计数度量值聚合结果依赖当前筛选写死就废了占比、同比、环比、排名、累计度量值全部依赖分母或对比基线随筛选变化需要放到横轴、图例、切片器计算列度量值没有可枚举的取值集合需要按它排序比如月份序号计算列排序需要一个稳定的字段需要跨表做行级判断并物化计算列行上下文里的逐行计算需要复用到五六张视觉对象度量值一处修改全局生效有一类需求特别容易混淆把订单金额按区间分成高/中/低三档然后统计各档的销售额。分档这件事是行级属性应该用计算列或者更好的做法是在 Power Query 里分出这一列统计各档销售额是聚合应该用度量值。经常看到有人把分档也写成度量值结果发现没法放到切片器上回头又改成计算列来回折腾。想清楚这个结果是描述一行的还是描述一批行的答案基本就出来了。2. 筛选上下文度量值真正的输入参数2.1 用点名和点名之后报数区分两种上下文理解度量值绕不开两个词行上下文和筛选上下文。这两个词被讲得很玄其实用班级那个类比一句话就能说清。行上下文是老师指着某一个学生说你算一下。它描述的是当前正在处理哪一行计算列和迭代函数SUMX、FILTER、ADDCOLUMNS 这类都在行上下文里工作。行上下文只认自己所在的那张表你没法从它里面直接看到另一张表的行。筛选上下文是老师说现在只统计三班的女生其他人先别动。它描述的是当前参与计算的数据范围视觉对象上的行、列、切片器、页面筛选器、报表筛选器以及 CALCULATE 里写的条件都在往筛选上下文里塞东西。度量值在求值时感知到的是筛选上下文根本不关心行上下文是不是存在。再回到那个 300% 的案子明细行的占比之所以算得对是因为每一行都往筛选上下文里塞了一个当前品类的条件度量值在这个缩小的范围里算出了正确的分母合计行没有塞条件度量值就在全量范围里重算于是回到 100%。计算的主动权从写公式的人手里转移到了渲染那一刻的筛选条件手里。2.2 CALCULATE唯一能改写筛选上下文的函数如果说度量值是一台机器CALCULATE 就是那根最关键的操纵杆。它是 DAX 里唯一能主动修改筛选上下文的函数也是几乎所有高级指标的地基。手机销售额 CALCULATE( [销售额], 产品[类别] 手机 )这段代码做的事情是在本来已有的筛选上下文之外再叠加一个类别等于手机的条件。这里有个新手最容易犯迷糊的细节——同一列上的筛选会替换不同列上的筛选会叠加。如果你在切片器里已经选了配件而 CALCULATE 里写的是类别 手机那么不是两个条件叠加成一个空集而是切片器的那一层被替换掉最终算的是手机。这个规则搞不清楚做出来的排名和占比就会时对时错。配套的几个拆筛选函数也得记住用法边界ALL(产品)把整张产品表上的筛选全部清掉常用于算全量分母。ALLSELECTED(产品)只清到当前视觉对象外部选择的范围为止切片器选了什么就保留什么。做占比、帕累托图基本都用它。REMOVEFILTERS(产品[类别])语法更清晰的一种写法功能上等价于 ALL 的对应列版本我现在的习惯是能写 REMOVEFILTERS 就不写 ALL读起来更直白。提示占比类指标里ALL 和 ALLSELECTED 用错是数字看着对但一联动就崩的最常见原因。判断方法很简单——把切片器换一个值如果占比的合计从 100% 变成了别的数那八成是用了 ALL。2.3 上下文转换性能问题最常见的高发区上下文转换Context Transition是度量值里最容易被忽略的一个机制但它经常是报表卡顿的元凶。规则只有一句**当度量值出现在行上下文里时当前行的行上下文会被自动转换成等效的筛选上下文。**换句话说你写SUMX(销售, [销售额])的时候引擎会为销售表的每一行做一次把这一行的所有列值作为筛选条件的动作然后再调用一次度量值。行数少还好几十万行上这么干一次公式引擎就忙不过来了。对比一下这两种写法-- 写法A触发行上下文到筛选上下文的转换 SUMX(销售, [销售额]) -- 写法B纯列运算全程留在存储引擎 SUMX(销售, 销售[单价] * 销售[数量])写法 A 里每一行都要走一遍建立筛选上下文 → 调用度量值 → 求值的流程本质上是 N 次独立的度量值计算写法 B 只是一个逐行的乘法加法可以整体交给存储引擎批量处理。两者的结果可能完全一样性能却能差出十倍以上。我的经验是在迭代函数内部能用列运算表达的就不要塞度量值。确实需要复用度量值逻辑的时候优先考虑把逻辑抽成基础列再在上层聚合。3. 三个能直接抄进项目的度量值写法3.1 聚合型SUM、COUNTROWS、DISTINCTCOUNT 的取舍最基础的一层就是聚合但选错函数同样会带来麻烦销售额 SUM(销售[金额]) 订单数 DISTINCTCOUNT(销售[订单号]) 明细行数 COUNTROWS(销售)这三个看起来差不多成本却差着量级。SUM是最便宜的走整列压缩扫描存储引擎一把过COUNTROWS也不贵数行数而已DISTINCTCOUNT需要构建哈希表去重是三者里最贵的在千万级明细上可能直接从毫秒级掉到秒级。所以当你发现某张卡片图比别的都慢先看看是不是用了一个 DISTINCTCOUNT。一个实用技巧如果业务上订单号本身是唯一的而你又只需要一个粗略的订单规模可以考虑用COUNTROWS替代如果必须去重尽量让去重的粒度落在维度表上比如对客户表做去重计数而不是对一张大事实表去重。SUM还有个隐式行为要注意它对空值不敏感NULL 会被跳过但如果整列全是空返回的是 BLANK 而不是 0这个区别在下一节会展开说。3.2 比率型分母为什么要用 DIVIDE 兜底比率类是度量值的高频场景占比、转化率、毛利率、达成率都属于这一类。我现在的标准写法是这样占比 DIVIDE( [销售额], CALCULATE([销售额], ALLSELECTED(产品)) )用DIVIDE而不是直接写除号理由有两个。第一直接写/遇到分母为 0 会直接报错整张视觉对象变成一堆错误提示体验极差DIVIDE会自动返回 BLANK视觉对象上就显示空白干净得多。第二DIVIDE还带第三个参数可以自定义除零时的返回值比如DIVIDE(a, b, 0)就是除零返回 0。分母部分用ALLSELECTED而不是ALL是因为占比这个指标几乎永远要和切片器联动。用户在切片器选了华东他希望看到的是华东内部的品类占比而不是华东占全国的占比。这两个口径没有对错但默认用ALLSELECTED更符合直觉。注意占比的分母和分子一定要在同一个粒度上。我见过有人在分子上用CALCULATE加了一堆条件分母却保持原样结果单看每一行都像那么回事合计的时候怎么都对不上。做比率之前先把分子分母分别放到两张卡片图上验证一遍能省掉大量回头排查的时间。3.3 时间智能同比、环比、累计的前提和写法时间智能是度量值最能体现价值的地方也是翻车最多的地方。先写给法再说前提销售额_去年同期 CALCULATE([销售额], SAMEPERIODLASTYEAR(日期表[日期])) 销售额_上月 CALCULATE([销售额], DATEADD(日期表[日期], -1, MONTH)) 销售额_年累计 TOTALYTD([销售额], 日期表[日期])看起来很简单但它们对模型有三个硬性前提缺一个就不出数或者出错数必须有一张独立的日期表日期连续无缺、覆盖完整年份不能拿事实表里的日期列硬凑。事实表里的日期是有业务发生才有周末和节假日是断的时间智能函数一遇到断裂就乱了。日期表必须标记为日期表。在模型视图里右键日期表选择标记为日期表指定日期列。这个动作听起来像个形式实际会影响引擎对时间相关函数的处理路径和筛选行为。日期表和事实表之间必须有有效的一对多关系方向从日期表指向事实表。违反第一条是最常见的错误。有人在事实表的日期列上直接写SAMEPERIODLASTYEAR(销售[下单日期])本地跑出来数字看着也对但一旦数据里某个日期没有订单偏差就悄悄出现了而且是那种不报错、只是数字偏一点的错最难发现。我的建议是任何一张报表只要涉及时间维度第一件事就是先把日期表建出来不要省这个步骤。4. 度量值写完只是开始组织方式与执行性能4.1 度量值表一张只有一列的空表模型里度量值多了之后第一个乱象是找不到。新建的度量值默认落在你当时选中的那张表下面做一张销售报表度量值可能散布在销售表、产品表、日期表、客户表下面一年后接手的人根本猜不到哪个指标藏在哪。成熟团队的做法是建一张专门的度量值表也有人叫指标表_Measures。做法很简单功能区选输入数据随便填一个空表只留一列然后把所有度量值都归到这张表下面。表本身不参与任何计算只是一个容器。更进一步的做法是在 Tabular Editor 或模型视图里建显示文件夹按基础指标 / 比率指标 / 时间智能 / 辅助指标分类几十个度量值也能一眼找到。我自己的命名习惯是给度量值加个简单前缀比如M_销售额、M_同比这样在编辑 DAX 时自动补全列表里能快速扫到和普通列区分开。这个前缀在最终展示的名称里可以去掉模型里看着整齐就够了。4.2 存储引擎与公式引擎同一个数字为什么有时秒出有时转圈Power BI 的 DAX 引擎其实是两个引擎在配合。存储引擎Storage Engine负责从压缩列存里扫数据、做基础聚合和筛选它是多线程的速度极快公式引擎Formula Engine负责处理那些存储引擎搞不定的复杂逻辑比如逐行判断、上下文转换、自定义函数它是单线程的慢得多。一条 DAX 查询快不快核心就看有多少活儿能下推给存储引擎。简单的SUM、COUNTROWS、带基础筛选的聚合基本全程在存储引擎里跑完千万行也就是几十毫秒而只要掺进迭代函数、上下文转换、复杂的 IF 嵌套公式引擎就得一行一行地算同样是千万行可能要好几秒。想看清这件事装一个 DAX Studio打开 Server Timings 面板跑一次查询它会明确告诉你存储引擎花了多少、公式引擎花了多少、扫描了多少行、返回了多少行。我调试性能问题的第一件事就是看这个面板——如果公式引擎占了绝大部分时间那问题一定出在写法上而不是数据量上。常见的高成本写法有几类迭代函数里套度量值上下文转换、FILTER直接过滤大事实表、在度量值里做字符串拼接、用IF层层嵌套做区间判断。这几类能避则避。4.3 VAR把重复计算压成一次写复杂度量值的时候同一个片段写三四遍是常态这时候就该上VAR。同比 VAR 今年 [销售额] VAR 去年 CALCULATE([销售额], SAMEPERIODLASTYEAR(日期表[日期])) VAR 差值 今年 - 去年 RETURN DIVIDE(差值, 去年)VAR有两个好处。一是可读性中间过程有了名字半年后回头看还能读懂当时在想什么。二是性能变量在定义处求值一次后面在RETURN里被引用多次也不会重复计算。上面这段如果没有 VAR去年那部分要么写两遍引擎解析两遍、要么塞进一个参数里硬凑都不好看。用VAR有几个坑得记住变量的作用域只在当前表达式内不能跨度量值引用想复用就得抽成一个独立的度量值。变量在定义时就会根据当时的筛选上下文求值不是等到被引用才算。所以VAR定义的位置很关键如果你在 CALCULATE 之后定义变量它拿到的是被 CALCULATE 改过之后的上下文。别在 VAR 里塞和主逻辑无关的重活变量不管最后用不用得上都会被执行。我就干过把一堆调试用的中间变量留在正式度量值里忘了删的事白白拖慢了一截。5. 度量值算得对刷新却翻车数据源侧的连带坑5.1 日期、类型和时区最先崩的就是这几处模型层写得再漂亮数据源侧一崩照样白搭而且这类问题往往在本地看不到、上了网关才爆。三个高频点日期类型。从数据库取回来的日期字段如果不是 date/datetime 类型而是被识别成了文本或数字时间智能函数会直接失灵日期表也没法正确建立关系。导入后第一件事是去 Power Query 里确认每一个日期列的类型标记为日期或日期/时间而不是任意类型。时区。数据库里存的往往是 UTC而报表使用者在中国。这中间的差不是简单调个显示格式就能解决的得在 Power Query 阶段就做偏移或者在模型里存一个带时区信息的辅助列。如果不处理晚上八点之后的订单会莫名其妙跑到第二天去日汇总和日明细对不上然后你就会花一整天去怀疑自己的度量值写错了。数值精度。金额字段如果在源端是 DECIMAL 类型导入之后容易出现浮点尾差做汇总对账的时候差几分钱。规避办法是在 Power Query 里显式转换类型而不是依赖自动检测。5.2 从 MySQL 这类库取数时连接器这件事比想象中重要前阵子帮人排查过一个刷新问题本地 Power BI Desktop 一切正常一发布到云端定时刷新就报连接错误折腾了半天根因是网关机器上没装对应的驱动。这类事在用 MySQL 做数据源的时候尤其常见值得单独说一说。Power BI 内置的那个 MySQL 连接器并不是纯原生实现它依赖本机安装的 MySQL Connector/NETMySql.Data驱动。这就带来了一连串版本和环境问题驱动没装或者版本不匹配桌面端可能提示找不到提供程序或者连上了但读取字段异常。安装一个和源库版本相匹配的 Connector/NET 通常能解决不建议无脑装最新版。32 位与 64 位不匹配驱动位数和客户端位数对不上是最隐蔽的一类问题本地怎么试都不行换一个位数的安装包立刻就好。桌面端能连、网关刷新失败这是最典型的场景。本地数据网关所在的机器需要同样安装并配置好这份驱动桌面端装了不代表网关端也装了。我的习惯是本地和网关环境用同一个驱动版本减少变量。运行时差异不同版本的桌面端在底层运行时上有差异某些驱动版本在旧运行时上正常、在新运行时上会报错。遇到莫名其妙的连接异常把桌面端和驱动都更新到较新的稳定组合往往比一行行看日志快得多。还有一个容易被忽视的点查询折叠。MySQL 连接器的折叠能力是有限的你在 Power Query 里加的一些自定义步骤很可能在某一处就断链了后面的筛选只能拉到本地内存里做。数据量小的时候看不出来数据量一大就是刷新超时。我的经验是能在源库里建视图解决的逻辑就尽量放到源库里Power Query 里只做轻量的类型转换和改名别让它承担复杂运算。提示判断折叠有没有断可以在 Power Query 里右键某一步看看查看本机查询是否可用。不可用就说明这一步之后已经拉到本地了。这个小动作能在模型做大之前就发现问题。6. 那些年在度量值上踩的坑和一套能复用的排查顺序6.1 BLANK、0 和空字符串长得像但差得远这三个东西在视觉对象上经常都显示成一片空白但行为完全不同。BLANK是 DAX 里的无值它参与聚合时会被自动忽略。AVERAGE遇到一片 BLANK 会跳过它们SUM也会跳过。如果你为了看起来饱满把所有 BLANK 都换成 0平均值立刻被拉低原本几百的平均数可能掉到几十。这不是显示问题是口径问题。只有一种情况必须转 0视觉对象需要参与算术运算或者需要显示0 单而不是空白。即便如此我也建议把转换放在最后一层比如视觉对象的显示逻辑里而不是污染基础度量值。顺便说个真实案例有人发现某个品类的销售额一直是空的查了半天以为度量值写错了最后发现是产品表和销售表的关系上有一条孤立的产品记录根本没有任何销售明细关联上来。这种空不是计算问题是数据完整性问题用ISBLANK配合明细表反查最快。6.2 循环依赖和在计算列里调用度量值计算列和度量值之间有一条容易越界的线计算列是可以引用度量值的因为计算列在逐行求值时行上下文会自动转换成筛选上下文。技术上能跑通但这个写法有两个代价——一是刷新时会为每一行做一次上下文转换性能极差二是结果被物化存下来之后切片器怎么变它都不会重算。还有一种更直白的错误叫循环依赖A 列引用了 B 列B 列又引用了 A 列模型直接报错所有相关列都失效。修的时候别一条条猜直接看报错提示里点名的那两列把其中一列的逻辑改写掉就行。判断标准其实很简单**如果一个计算是一个属性这一行属于哪个档、哪个分组用计算列如果是一个结果多少、多大、占比多少用度量值。**很多时候所谓的循环依赖本质上是把本该是结果的东西做成了属性。6.3 我常用的五步排查顺序度量值出问题的时候直接盯着代码看是最低效的。我现在的固定动作是这五步先在卡片图上看裸数。把出问题的度量值单独放一张卡片图页面上的所有切片器先全部清空看它返回什么。这一步能快速区分是数字错了还是是展示错了。逐层展开维度。把维度层级从高到低逐级展开看数字在哪一层开始偏离。如果是汇总层对、明细层错问题在粒度反过来问题多半在分母或筛选覆盖。检查有没有被意外覆盖的筛选。搜一遍ALL、ALLSELECTED、REMOVEFILTERS确认每一个的意图。九成的联动之后数字不对都是这里。上 DAX Studio 看执行计划。Server Timings 一看就知道是公式引擎在硬扛还是存储引擎已经很快了。这一步能把优化写法和优化数据量两条路彻底分开。最后才动代码。确认问题定位之后再回去改度量值改完重复第一步验证。这五步的顺序我调整过好几次现在这版是踩坑最多的产物。核心原则就一条**先用最少的变量看清事实再动代码。**跳过验证直接改公式改到最后你会连原来对不对都不确定了。我现在每新写一个度量值都会习惯性地先丢到一张不带任何切片器的卡片图上跑一次确认在无筛选这个基准状态下它是合理的。这个动作只花三秒钟但帮我省掉的返工时间按小时算都不过分。
返回列表