一秒钟一句话生成 PowerBI 数据字典并与同事分享

2022-07-07 15:36:27 浏览数 (2)

很多伙伴问罗叔是否可以给小白直接直接操作的技巧,例如:直接点一个按钮,直接写一个公式,直接解决一个问题的。

我是小白我怕谁

可以的。

尽管讲解一个按钮,公式,问题是很直接的。但不排除一句话可以解决大问题的神技,小白不需要理解为什么,只需要用,只需要欣赏,只需要赞叹,不仅仅帮助小白解决问题,同时让小白可以增加兴趣,来体会 Power BI 和数据分析中的美和艺术。

如何提取数据模型的信息

有很多方法提取数据模型的信息,但是对小白来说,我们需要:

一秒钟一句话生成 Power BI 数据字典并与同事分享。

这看似是一个不可能完成的任务。

的确,有很多方法和工具可以从 Power BI Desktop 的数据模型中提取信息,但是对于小白来说,怎么可以快速实现呢?

小白的标准操作在于:

第一步,复制粘贴 “度量值” 内容。

第二步,复制粘贴 “结果” 即可。

本文就是这样的案例,我们从正统的思路开始做事,让大家知道其来龙去脉,然后构建实用的解决方案,然后重构,然后优化,然后再优化,然后再反思再优化,然后再封装,然后适配小白的思想,拿来就用。

请大家一起来欣赏吧。

DAX 新函数

DAX 引擎还在进化,每一次的进化都是在主体框架下的一些小补充。但每次的小补充可能带来新的可能。今天要和大家介绍的是:

DAX 出了一个新的函数:COLUMNSTATISTICS。它可以直接返回当前数据模型中所有表和列的信息。

打开 DAX Studio,直接输入:

代码语言:javascript复制
EVALUATE 
COLUMNSTATISTICS()

便可以得到:

分别是:

  • 表名
  • 列名
  • 最小值
  • 最大值
  • 非重复元素数
  • 最大长度(如果是文本类型)

模型数据字典

如果你正考虑输出一个模型数据字典来告知相关人员,数据模型的信息,可以快速用这个函数实现。

这样就可以获得一套当前模型中所有表和列的字典。

无法用于计算表

不难想到可以用计算表来计算以上的字典并放入当前数据模型,可惜是不行的,例如在 Power BI Desktop 中,创建计算表,并写入:

这就出现了一个循环依赖的错误。道理很简单:

正在创建的计算表也是该 DAX 函数统计的对象;而该表还没创建完;要创建该表就要计算完该 DAX 函数;而要计算完该 DAX 函数,该表就要计算完;导致循环依赖。

好可惜啊,有没有。

我们希望这个很实用的函数可以使用。

度量值实现

既然该 DAX 函数仅仅依赖表和列,但并不会依赖度量值,所以,可以通过度量值来获取信息。在写度量值前,还注意到一点,有些系统生成的表,我们并不需要,因此,可以过滤掉,写出度量值的示例,如下:

可以看出:

  • 的确可以运行成功。
  • 编辑器的智能提示出错,说明 Power BI 的公式编辑器并没有支持对该函数的解析。但的确存在该函数。

显示信息

用度量值显示一个数值不是我们想要的,毕竟我们想要的是信息,而不是有多少条数据。

根据上述实验,我们可知有这样的限制:

  • 我们想要表,但却不能用计算表;
  • 可以用度量值,但度量值却不能返回表。

这导致一个矛盾。先考虑在度量值中用字符串来实现,如下:

这的确显示了信息,但不是特别紧凑,以及有的表里面没有列,也可以不必显示,因此,可以对这个度量值再做优化,得到:

这个效果的确是我们想要的了。

其优化的度量值内容为:

代码语言:javascript复制
Model.Info.Text = 
// 设置要排除的表,默认留空
VAR vFilterOutTables = { "" }
// 设置要排除的辅助表,如:参数,度量值容器
VAR vFilterOutTables_OneColumn = { BLANK() , "列" , "列 1" , "Value" , "Column" , "Column 1" }

// 以下内容无需修改

VAR vDictionary = 
    FILTER( COLUMNSTATISTICS() , 
        // 清除掉系统生成的内容
        NOT CONTAINSSTRING( [Table Name] , "DateTableTemplate" ) &&
        NOT CONTAINSSTRING( [Column Name] , "RowNumber" )
    )
VAR vDictionaryFiltered = FILTER( vDictionary , NOT [Table Name] IN vFilterOutTables )
VAR vTableInfoFilterd =
FILTER(
    ADDCOLUMNS(
        SUMMARIZE( vDictionaryFiltered , [Table Name] ) , 
        "@ColumnContent" , 
            CONCATENATEX( FILTER( vDictionaryFiltered ,  [Table Name] = EARLIER( [Table Name] ) ) , [Column Name] , ", " )
    ) ,
    NOT [@ColumnContent] IN vFilterOutTables_OneColumn
)
RETURN 
    CONCATENATEX( vTableInfoFilterd , 
        "表【" & [Table Name] & "】" & UNICHAR( 10 ) & "包括列:" & [@ColumnContent]
         , UNICHAR( 10 )
    )

只需要复制上述内容就可以立即提取自己的模型信息。还可以复制给工作伙伴,直接复制粘贴到微信与别人沟通。

然后粘贴到微信吧,如下:

至此,主体已经完成。已经可以拿来就用了。

但这不是我们的调性,接下来我们一起进入思想时刻。

重构

什么是” 重构” 呢?

在我们写的每篇文章以及给出的每个案例中,几乎都有 “重构” 的影子。

重构,顾名思义,就是:重新构建,说白了,就是重新做一遍。

为什么要重新做一遍呢?

重新做一遍的底层逻辑就是:超越上一个版本的自己。

因此,重构是一种重要的思想。

重构,是一种反思,它总是提醒我们进行反思,一件事情是不是可以做得更好。

大家还记得爱因斯坦用纸做小板凳的故事,做了三个版本,拿出了最好的一个。

这里的重构,除了超过上一个版本的意思,还有一层更重要的底层逻辑是:

怎么才算 “更好” 呢?

一般在我们的这个领域来形容更好,有两个方面:

  • 情感方面,你是不是有一种 “哇” 的感觉,获得了一种喜悦,超过了自己。
  • 理性方面,是不是对内容本身有意义。例如:更高更快更通用。

很明显,这两者是常常伴随而来的。

在我们的这个领域,重构,往往意味着去实现:复用,健壮性。

什么意思?

例如:对于正在看本文的小白来说,也许你对度量值一无所知,但你知道如何创建一个度量值,那么只需要复制粘贴就可以解决本文所叙述的目标下的所有问题,那么就说,这个方案是:1)通用的;2)健壮的。

下面,就让我们一起进入重构的过程,对于小白来说,可以欣赏这个过程;对于高手来说,可以参考这个过程。

第一次重构:解除名称硬编码

任何用 "" 写出的文本都存在不够通用的问题,因此,需要将硬编码的部分提取,以便未来需求变化时,在一个位置维护变化的内容。

可以对度量值再做优化,得到:

代码语言:javascript复制
Model.Info.Text = 
// 设置要排除的表,默认留空
VAR vFilterOutTables = { "日期" , "ModelInfo" }
// 设置要排除的辅助表,如:参数,度量值容器
VAR vFilterOutTables_OneColumn = { BLANK() , "列" , "列 1" , "Value" , "Column" , "Column 1" }

// 设置表前缀等信息,默认如下,可不修改
VAR vText_Table_Prefix = "表【" // 设置表前缀
VAR vText_Table_Suffix = "】" // 设置表后缀
VAR vText_Table_IncludingColumns = "包括列:" // 设置包括列
VAR vText_Column_Splitter = ", " // 设置列分隔符

// 以下内容无需修改
VAR vDictionary = 
    FILTER( COLUMNSTATISTICS() , 
        // 清除掉系统生成的内容
        NOT CONTAINSSTRING( [Table Name] , "DateTableTemplate" ) &&
        NOT CONTAINSSTRING( [Column Name] , "RowNumber" )
    )
VAR vDictionaryFiltered = FILTER( vDictionary , NOT [Table Name] IN vFilterOutTables )
VAR vTableInfoFilterd =
FILTER(
    ADDCOLUMNS(
        SUMMARIZE( vDictionaryFiltered , [Table Name] ) , 
        "@ColumnContent" , 
            CONCATENATEX( 
                FILTER( vDictionaryFiltered ,  [Table Name] = EARLIER( [Table Name] ) ) , 
                [Column Name] , vText_Column_Splitter 
            )
    ) ,
    NOT [@ColumnContent] IN vFilterOutTables_OneColumn
)
RETURN 
    CONCATENATEX( vTableInfoFilterd , 
        vText_Table_Prefix & [Table Name] & vText_Table_Suffix & 
        UNICHAR( 10 ) & vText_Table_IncludingColumns & [@ColumnContent]
        , UNICHAR( 10 )
    )

这就完成了,效果上没有区别。

第二次重构:应对复杂工程

我们刚刚的截图非常简单,如下:

现在的问题是,如果面对的是一个大型的复杂工程,还可以吗?

第一步:先从业务逻辑上想想,有没有这个需求?

思考如下:

同事会不会问我们,数据模型中有哪些表和列的信息呢?然后要快速给出,并进行沟通。

想了一会儿,发现:的确很可能。而且还发现了另一种可能,那就是:

我们也会自己不断从数据库或文件中提取信息,但提取的信息是不是太多了,我们也不知道,尤其是表很多的时候,那么就说明这个需求是有意义的。

第二步:在实际大型工程中,试一试如下:

不难看出,非常实用,一下子就全部提取了。

一个特别实用的动作是,可以在记事本里分析和反查这些列是否合理。如下:

这可以非常快地帮助我们发现问题。

但问题来了,我们发现有的表有很多列,是否可以直观的写下有多少列呢?

因此,进行优化,效果如下:

这的确帮了大忙,我们快速地知道哪些表的列数,以便有针对性的研究下。

第三次重构:修复问题

现在就可以不断使用这个技能了。

直到发现它的问题:

只要报表界面上有任何筛选器,都会导致这个错误。仔细阅读错误信息:

COLUMNSTATISTICS () 不能与筛选上下文一起使用。

仔细思考一下原因,由于 COLUMNSTATISTICS 是用来获得模型信息的,并不是用来进行计算的,因此,DAX 引擎将其隔绝在筛选上下文之外是有道理的。

如何进行修复呢?

既然错误是:不能与筛选上下文一起使用。那么可以清除掉所有的筛选上下文即可。

最后得到了带有这种保护的版本。如下:

代码语言:javascript复制
Model.Info.Text = 

// 进行设置:

    // 设置要排除的表,默认留空
    VAR vFilterOutTables = { "" } // 如果要排除某表,如日期表,可以将表名放入。
    // 设置要排除的辅助表,如:参数,度量值容器
    VAR vFilterOutTables_OneColumn = { BLANK() , "列" , "列 1" , "Value" , "Column" , "Column 1" , "Column1" }

    // 设置表前缀等信息,默认如下,可不修改
    VAR vText_Table_Prefix = "表【" // 设置表前缀
    VAR vText_Table_Suffix = "】" // 设置表后缀
    VAR vText_Table_IncludingColumns = "包括列:" // 设置包括列
    VAR vText_Table_IncludingColumns_Prefix = "(共 " // 设置 共 x 列
    VAR vText_Table_IncludingColumns_Suffix = " )" // 设置 共 x 列

    VAR vText_Column_Splitter = ", " // 设置列分隔符

// 以下内容无需修改

RETURN

CALCULATE(
    
    VAR vDictionary = 
        FILTER( COLUMNSTATISTICS() , 
            // 清除掉系统生成的内容
            NOT CONTAINSSTRING( [Table Name] , "DateTableTemplate" ) && //系统的日期表
            NOT CONTAINSSTRING( [Table Name] , "LocalDateTable" ) && //表列对应的日期表列
            NOT CONTAINSSTRING( [Column Name] , "RowNumber" ) // 系统生成的表索引
        )
    VAR vDictionaryFiltered = FILTER( vDictionary , NOT [Table Name] IN vFilterOutTables )
    VAR vTableInfoFilterd =
    FILTER(
        ADDCOLUMNS(
            SUMMARIZE( vDictionaryFiltered , [Table Name] ) , 
            "@ColumnCount" , COUNTROWS( FILTER( vDictionaryFiltered ,  [Table Name] = EARLIER( [Table Name] ) ) ) ,
            "@ColumnContent" , 
                CONCATENATEX( 
                    FILTER( vDictionaryFiltered ,  [Table Name] = EARLIER( [Table Name] ) ) , 
                    [Column Name] , vText_Column_Splitter 
                )
        ) ,
        NOT [@ColumnContent] IN vFilterOutTables_OneColumn
    )
    RETURN
        CONCATENATEX( vTableInfoFilterd , 
            vText_Table_Prefix & [Table Name] & vText_Table_Suffix & 
            vText_Table_IncludingColumns_Prefix  & [@ColumnCount] & vText_Table_IncludingColumns_Suffix &
            
            UNICHAR( 10 ) & vText_Table_IncludingColumns & [@ColumnContent]
            , UNICHAR( 10 )
        )
    // 清除筛选上下文
    , REMOVEFILTERS( )

)

// by BI佐罗

用 CALCULATE 配合 REMOVEFILTERS 来保护 COLUMNSTATISTICS 的执行。

于是,此时就得到了一个无懈可击的重构版本,它具备这样的特点:

  • 可以直接复制粘贴使用,无任何依赖。
  • 可以设置各种配置。
  • 针对问题给出保护,没有了复杂度。

小白时刻

作为小白,如果你整个文章都没有读懂,完全没有关系,只需要看以下三句话即可。

要获得数据模型的数据字典信息并与同事分享交流,只需要两步:

第一步,新建度量值,复制粘贴上述 “Model.Info.Text” 内容。

第二步,拖入 Power BI Desktop 报表,复制后去微信粘贴即可。

高手时刻

如果你正在学习 DAX,那你可以看到 DAX 的一个综合运用了,可以体会其中每一步的 DAX 用法。

如果你可以正确地 Thinking In Table,那么用 DAX 就可以帮助我们构建灵巧的解决方案。

总结

快去复制粘贴到你的项目中试试吧。

扩展思考

在此强调,如果仅仅是为了解决一个目标:快速提取数据模型的信息,那仅仅复制粘贴以上度量值即可。

但这里怀着再进一步重构的想法,我们观察到:

在整套解决方案的逻辑链条中,有这样的前提假设:

  • 我们想要表,但 COLUMNSTATISTICS 却不能用于计算表;
  • 可以用度量值,但度量值却不能返回表。

再加上:

  • 一个度量值提取信息的先入为主的设计初衷。

导致:

我们得到了现在的解决方案。

但是,

如果我们真的想得到一个表怎么办呢?

当我们第一次这样尝试的时候,会触发一个错误:

不能与筛选上下文一起使用。

但是,我们已经发现了这个问题的弥补方法,那就是:用 CALCULATE 配合 REMOVEFILTERS 来保护 COLUMNSTATISTICS 的执行。

既然如此,我们思考:

是不是可以构建一套表格方式的解决方案呢?

也就是:

这个问题就留给大家思考吧。

0 人点赞