研究《DAX查询计划 白皮书》中的案例(四)优化方案2

优化方案1将原始方案的计算效率提高了 90% 以上,而白皮书认为这还不是最好的方案,原作者给出了另外两个更好的方案,这两个算法都不再基于对计算要求的直接理解,更换了 DAX 写法。本篇分析白皮书提供的优化方案2,并与优化方案1和优化方案(番外)进行比较,进一步认识性能优劣的底层原因。


研究《DAX查询计划 白皮书》中的案例(一)初始方案
研究《DAX查询计划 白皮书》中的案例(二)优化方案1
研究《DAX查询计划 白皮书》中的案例(三)优化方案(番外)
研究《DAX查询计划 白皮书》中的案例(四)优化方案2
研究《DAX查询计划 白皮书》中的案例(四)优化方案3


优化方案2

DAX 查询

// 优化方案2(原版)
EVALUATE
    ADDCOLUMNS (
        VALUES ( 'Date'[Date] ),
        "OpenOrders",
        COUNTROWS (
            FILTER(
                'Internet Sales',
                CONTAINS (
                    DATESBETWEEN (
                        'Date'[Date],
                        'Internet Sales'[Order Date], // 白皮书提供的代码在这里没有+1
                        'Internet Sales'[Ship Date] - 1
                    ),
                    'Date'[Date],
                    'Date'[Date]
                )
            )
        )
    )

这段代码与第一篇原始方案的结构类似,都是 COUNTROWS + FILTER 的写法,不过 FILTER 条件参数不再是简单的 Sales[OrderDate] < Calendar[Date] && Sales[ShipDate] > Calendar[Date] ​,改成了 CONTAINS + DATESBETWEEN​的判断。

白皮书发布于 2013 年,当时 DAX 还不支持 IN​ 这个符号(IN​发布于 2016年12月),放在今天,这段代码可以修改成下面这样,后文的分析基于修改后的代码。

另外,白皮书给的算法有误,DATESBETWEEN('Calendar'[Date],Sales[OrderDate], Sales[ShipDate] - 1 )​ 应该改成DATESBETWEEN('Calendar'[Date],Sales[OrderDate] +1 , Sales[ShipDate] - 1 )​,起点不应该包括在内。

// 优化方案2(修正)
EVALUATE
ADDCOLUMNS (
    VALUES ( 'Calendar'[Date] ),
    "OpenOrders",
        COUNTROWS (
            FILTER (
                Sales,
                'Calendar'[Date] IN  // 修改了这里l
                    DATESBETWEEN ( 
                        'Calendar'[Date], 
                        Sales[OrderDate] + 1  // 修改了这里
                        Sales[ShipDate] - 1 
                    )
            )
        )
)

运行效率

以连续5次冷启动的结果进行统计,将各方案的情况列写在表格里,数字下方括号里记录的是相对前一个方案的性能变化情况。相对优化方案1,优化方案2的总耗时又减少了1个数量级,从约 百级 降低到 十级,但是比优化方案(番外)耗稍长一些。

环境参数初始方案优化方案1优化方案2优化方案(番外)
环境1平均总耗时(毫秒)4389.4290.4
(↑ 93.38%)
79.6
(↑ 72.59%)
56.8
(↑ 28.64%)
SE耗时
(毫秒)
14.44.8
13.6
4.8
SE占比0.328%1.653%
(↑ 403.83%)
17.09%
(↑ 933.67%)
8.45%
(↓50.54%)
环境2平均总耗时(毫秒)4017.0300.60
(↑ 92.52%)
76.4
(↑ 74.58%)
53.8
(↑ 29.58%)
SE耗时
(毫秒)
17.25.8
(↑ 66.28%)
18.2
5
SE占比0.428%1.929%
(↑ 350.62%)
23.82%
(↑ 1134.64%)
9.29%
(↓ 60.99%)

白皮书原本的行文结构是逐步优化至最优,但是上一篇由于我的好奇整出个白皮书之外的优化方案(番外),比本篇的优化方案2运行更快,最快的放在表格最后一列上。

存储引擎部分

有必要把存储引擎相关界面截图,因为这里出现了新情况

​image​

从 xmSQL 列表看,一共有 3 个 Scan 即 3 个 xmSQL 查询,左下角显示 SE Cache 命中1次。这 3 个 Scan 中有 2 个 xmSQL 查询是完全相同的,出现在 Line 2 和 Line 4,命中的 Cache 在 Line 3。

实际上只有 2 个 xmSQL,其中 1 个被重复使用。

1、VQ1:将 Calendar 表中按 Calendar[Date] 分组,返回由 [Date] 的不重复值,结果包含1列,共 1081 行数据

// xmSQL VQ1
SELECT
    'Calendar'[Date]
FROM 'Calendar';

2、VQ2:将 Sales 表按 Sales[RowNumber]、Sales[OrderDate]、Sales[ShipDate] 分组,并返回这3列上的值。

SELECT
    'Sales'[RowNumber],
    'Sales'[OrderDate],
    'Sales'[ShipDate]
FROM 'Sales';

读过前几篇文章的朋友知道,[RowNumber] 的出现,会使得查询结果包含 Sales 表的每一行,所以该查询返回 121253 行数据,共 3 列 Sales[RowNumber]、Sales[OrderDate]、Sales[ShipDate]

第一篇文章分析初始方案的性能瓶颈说过 [RowNumber] 的出现可能会导致后续出现超大规模的交叉表,而这次 [RowNumber] 也出现了,为什么性能没有低下反倒还提高了?之前也强调过了,并没有什么通用的优化写法,自然也就没有绝对禁用的禁忌写法,关键还得看各种写法生成的物理查询计划。

公式引擎部分

逻辑查询计划

终于,这次逻辑查询计划出现值得注意的信息了

AddColumns: RelLogOp DependOnCols()() 0-1 RequiredCols(0, 1)('Calendar'[Date], ''[OpenOrders])
├── Scan_Vertipaq: RelLogOp DependOnCols()() 0-0 RequiredCols(0)('Calendar'[Date])
└── CountRows: ScaLogOp DependOnCols(0)('Calendar'[Date]) Integer DominantValue=BLANK
    └── Filter: RelLogOp DependOnCols(0)('Calendar'[Date]) 1-15 RequiredCols(0, 1, 6, 7)('Calendar'[Date], 'Sales'[RowNumber-2662979B-1795-4F74-8F37-6A1BA8059B61], 'Sales'[OrderDate], 'Sales'[ShipDate])
        ├── Scan_Vertipaq: RelLogOp DependOnCols()() 1-15 RequiredCols(1, 6, 7)('Sales'[RowNumber-2662979B-1795-4F74-8F37-6A1BA8059B61], 'Sales'[OrderDate], 'Sales'[ShipDate])
        └── Not: ScaLogOp DependOnCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate]) Boolean DominantValue=false
            └── IsEmpty: ScaLogOp DependOnCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate]) Boolean DominantValue=true
                └── Filter: RelLogOp DependOnCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate]) 16-16 RequiredCols(0, 6, 7, 16)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])
                    ├── DatesBetween: RelLogOp DependOnCols(6, 7)('Sales'[OrderDate], 'Sales'[ShipDate]) 16-16 RequiredCols(6, 7, 16)('Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])
                    │   ├── 'Sales'[OrderDate]: ScaLogOp DependOnCols(6)('Sales'[OrderDate]) DateTime DominantValue=NONE
                    │   └── Subtract: ScaLogOp DependOnCols(7)('Sales'[ShipDate]) DateTime DominantValue=NONE
                    │       ├── 'Sales'[ShipDate]: ScaLogOp DependOnCols(7)('Sales'[ShipDate]) DateTime DominantValue=NONE
                    │       └── Constant: ScaLogOp DependOnCols()() Integer DominantValue=1
                    └── Is: ScaLogOp DependOnCols(0, 16)('Calendar'[Date], 'Calendar'[Date]) Boolean DominantValue=false
                        ├── 'Calendar'[Date]: ScaLogOp DependOnCols(16)('Calendar'[Date]) DateTime DominantValue=NONE
                        └── 'Calendar'[Date]: ScaLogOp DependOnCols(0)('Calendar'[Date]) DateTime DominantValue=NONE

​image​

注意红框中的这部分,这部分对应的是 DAX 代码中的 IN​,在结构流程图中也用方框标记一下

[对应 DAX 代码中的 IN]
7:IsEmpty
6:Not
8:Filter
9:DatesBetween
16:Is
Calenar[Date] = Calendar[Date]
10:Add
Sales[OrderDate] + 1
13:Subtract
Sales[ShipDate] - 1
1:AddColumns
2:Scan_Vertipaq
Calendar[Date]
3:CountRows
4:Filter
5:Scan_Vertipaq
Sales[RowNumber]
Sales[OrderDate]
Sales[ShipDate]

红框里的代码,按从上往下的结构将操作符写出来

NOT( ISEMPTY ( FILTER ( Table, Condition ) ) )

其代表的便是

IN DATESBETWEEN ( 'Calendar'[Date], Sales[OrderDate] + 1, Sales[ShipDate] - 1 )

为什么这么说?因为白皮书里就这么说的。白皮书发布于 2013年,当时的逻辑查询计划与今天不太一样,当年这部分逻辑查询计划是这样的:

NOT ( ISBLANK ( MINX ( FILTER ( Table, Condition ), 1 ) ) )

即便没有人告诉我们这部分代表的是 IN​,通过对比逻辑查询计划与 DAX 代码的结构、结合 DAX 理论思考 IN​ 的判断过程,也能得出相同的结论:

1、4:Filter 读取 5:Scan_Vertipaq的行,提供行上下文 [Sales[Number],Sales[OrderDate],Sales[ShipDate]

2、根据 1# 行上下文中的 Sales[OrderDate]和 Sales[ShipDate] 分别计算 Sales[OrderDate]+1、 Sales[ShipDate]-1 两个日期区间端点,9:DatesBetween从 Calendar[Date] 中筛选出该区间内的全部 [Date]

3、1:AddColumns 读取2:Scan_Vertipaq,提供行上下文 Calendar[Date]

4、8:Filter 从 9:DatesBetween 生成的 [Date] 中,筛选出与 # 3 行上下文中的 [Date] 相同的数据。

5、4:Filter 以 NOT(IsEmpty(8:Filter)为条件筛选 5:Scan_Vertipaq

5-1、对于 2:Scan_Vertipaq 中某个具体的 Calendar[Date](记为 A) 以及5:Scan_Vertipaq 中某个具体的 [Sales[Number],Sales[OrderDate],Sales[ShipDate](记为 B),如果 4# 的结果不为空,说明 9:DatesBetween生成的 [Date] 区间中包含 A,7:IsEmpty 为假,6:Not为真,于是4:Filter判断 B 符合条件。

5-2、对于 2:Scan_Vertipaq 中某个具体的 Calendar[Date](记为 A) 以及5:Scan_Vertipaq 中某个具体的 [Sales[Number],Sales[OrderDate],Sales[ShipDate](记为 B),如果 4# 的结果为空,说明 9:DatesBetween生成的 [Date] 区间中不包含 A,7:IsEmpty 为真,6:Not为加,于是4:Filter判断 B 不符合条件。

对应的是这一段代码

FILTER(
	Sales,
	NOT(ISEMPTY(
			FILTER(
		        DATESBETWEEN ( 
		            'Calendar'[Date], 
		            Sales[OrderDate] + 1  
		            Sales[ShipDate] - 1 
		        ),
		        'Calednar'[Date] = EARLIER('Calendar'[Date]) 
		        //EARLIER('Calendar'[Date]) 从最外层 ADDCOLUMNS 提供的行上下文中获取
	        )
    ))
)

也就是 DAX 查询代码中的 IN 这一部分

FILTER (
    Sales,
    'Calendar'[Date] IN  // 由最外层 ADDCOLUMNS 提供
        DATESBETWEEN ( 
            'Calendar'[Date], 
            Sales[OrderDate] + 1  
            Sales[ShipDate] - 1 
        )
)

逻辑查询计划中并不一定完全按照 DAX 查询代码使用的符号,比如这一段逻辑查询计划中就不存在 IN​ 这样的符号,IN​ 的实现通过一系列可能不太直观的操作来完成。

在逻辑查询计划中识别到该部分可以帮助后面在物理查询计划中找出哪部分再执行 IN​这个计算。

这便是逻辑查询计划中值得注意的信息。

物理查询计划

这次物理查询计划较长

AddColumns: IterPhyOp LogOp=AddColumns IterCols(0, 1)('Calendar'[Date], ''[OpenOrders2])
├── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Scan_Vertipaq IterCols(0)('Calendar'[Date]) #Records=1081 #KeyCols=20 #ValueCols=0
│   └── ProjectionSpool<ProjectFusion<>>: SpoolPhyOp #Records=1081
│       └── Cache: IterPhyOp #FieldCols=1 #ValueCols=0
└── SpoolLookup: LookupPhyOp LogOp=CountRows LookupCols(0)('Calendar'[Date]) Integer #Records=1079 #KeyCols=1 #ValueCols=1 DominantValue=BLANK
    └── AggregationSpool<Count>: SpoolPhyOp #Records=1079
        └── Filter: IterPhyOp LogOp=Filter IterCols(0, 1, 6, 7)('Calendar'[Date], 'Sales'[RowNumber], 'Sales'[OrderDate], 'Sales'[ShipDate])
            └── CrossApply: IterPhyOp LogOp=Not IterCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate])
                ├── Spool_MultiValuedHashLookup: IterPhyOp LogOp=Scan_Vertipaq LookupCols(6, 7)('Sales'[OrderDate], 'Sales'[ShipDate]) IterCols(1)('Sales'[RowNumber]) #Records=121253 #KeyCols=20 #ValueCols=0
                │   └── ProjectionSpool<ProjectFusion<>>: SpoolPhyOp #Records=121253
                │       └── Cache: IterPhyOp #FieldCols=3 #ValueCols=0
                └── Not: IterPhyOp LogOp=Not IterCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate])
                    └── Extend_Const: IterPhyOp LogOp=IsEmpty IterCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate])
                        └── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Filter IterCols(0, 6, 7)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate]) #Records=6444 #KeyCols=3 #ValueCols=0
                            └── AggregationSpool<GroupBy>: SpoolPhyOp #Records=6444
                                └── Filter: IterPhyOp LogOp=Filter IterCols(0, 6, 7, 16)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])
                                    └── CrossApply: IterPhyOp LogOp=Is IterCols(0, 16)('Calendar'[Date], 'Calendar'[Date])
                                        ├── Spool_MultiValuedHashLookup: IterPhyOp LogOp=DatesBetween LookupCols(16)('Calendar'[Date]) IterCols(6, 7)('Sales'[OrderDate], 'Sales'[ShipDate]) #Records=6444 #KeyCols=3 #ValueCols=0
                                        │   └── AggregationSpool<GroupBy>: SpoolPhyOp #Records=6444
                                        │       └── DatesBetween: IterPhyOp LogOp=DatesBetween IterCols(6, 7, 16)('Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])
                                        │           ├── Extend_Lookup: IterPhyOp LogOp=Add IterCols(6)('Sales'[OrderDate])
                                        │           │   ├── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Scan_Vertipaq IterCols(6, 7)('Sales'[OrderDate], 'Sales'[ShipDate]) #Records=1081 #KeyCols=2 #ValueCols=0
                                        │           │   │   └── AggregationSpool<GroupBy>: SpoolPhyOp #Records=1081
                                        │           │   │       └── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Scan_Vertipaq IterCols(1, 6, 7)('Sales'[RowNumber], 'Sales'[OrderDate], 'Sales'[ShipDate]) #Records=121253 #KeyCols=20 #ValueCols=0
                                        │           │   │           └── ProjectionSpool<ProjectFusion<>>: SpoolPhyOp #Records=121253
                                        │           │   │               └── Cache: IterPhyOp #FieldCols=3 #ValueCols=0
                                        │           │   └── Add: LookupPhyOp LogOp=Add LookupCols(6)('Sales'[OrderDate]) DateTime
                                        │           │       ├── ColValue<'Sales'[OrderDate]>: LookupPhyOp LogOp=ColValue<'Sales'[OrderDate]>'Sales'[OrderDate] LookupCols(6)('Sales'[OrderDate]) DateTime
                                        │           │       └── Constant: LookupPhyOp LogOp=Constant Integer 1
                                        │           └── Subtract: LookupPhyOp LogOp=Subtract LookupCols(7)('Sales'[ShipDate]) DateTime
                                        │               ├── ColValue<'Sales'[ShipDate]>: LookupPhyOp LogOp=ColValue<'Sales'[ShipDate]>'Sales'[ShipDate] LookupCols(7)('Sales'[ShipDate]) DateTime
                                        │               └── Constant: LookupPhyOp LogOp=Constant Integer 1
                                        └── InnerHashJoin: IterPhyOp LogOp=Is IterCols(0, 16)('Calendar'[Date], 'Calendar'[Date])
                                            ├── Extend_Lookup: IterPhyOp LogOp=Extend_Lookup'Calendar'[Date] IterCols(0)('Calendar'[Date])
                                            │   ├── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Scan_Vertipaq IterCols(0)('Calendar'[Date]) #Records=1081 #KeyCols=20 #ValueCols=0
                                            │   │   └── ProjectionSpool<ProjectFusion<>>: SpoolPhyOp #Records=1081
                                            │   │       └── Cache: IterPhyOp #FieldCols=1 #ValueCols=0
                                            │   └── ColValue<'Calendar'[Date]>: LookupPhyOp LogOp=ColValue<'Calendar'[Date]>'Calendar'[Date] LookupCols(0)('Calendar'[Date]) DateTime
                                            └── HashLookup: IterPhyOp LogOp=HashLookup'Calendar'[Date] IterCols(16)('Calendar'[Date]) #Recs=1079
                                                └── HashByValue: SpoolPhyOp #Records=1079
                                                    └── Extend_Lookup: IterPhyOp LogOp=Extend_Lookup'Calendar'[Date] IterCols(16)('Calendar'[Date])
                                                        ├── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=DatesBetween IterCols(16)('Calendar'[Date]) #Records=1079 #KeyCols=1 #ValueCols=0
                                                        │   └── AggregationSpool<GroupBy>: SpoolPhyOp #Records=1079
                                                        │       └── DatesBetween: IterPhyOp LogOp=DatesBetween IterCols(6, 7, 16)('Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])
                                                        │           ├── Extend_Lookup: IterPhyOp LogOp=Add IterCols(6)('Sales'[OrderDate])
                                                        │           │   ├── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Scan_Vertipaq IterCols(6, 7)('Sales'[OrderDate], 'Sales'[ShipDate]) #Records=1081 #KeyCols=2 #ValueCols=0
                                                        │           │   │   └── AggregationSpool<GroupBy>: SpoolPhyOp #Records=1081
                                                        │           │   │       └── Spool_Iterator<SpoolIterator>: IterPhyOp LogOp=Scan_Vertipaq IterCols(1, 6, 7)('Sales'[RowNumber], 'Sales'[OrderDate], 'Sales'[ShipDate]) #Records=121253 #KeyCols=20 #ValueCols=0
                                                        │           │   │           └── ProjectionSpool<ProjectFusion<>>: SpoolPhyOp #Records=121253
                                                        │           │   │               └── Cache: IterPhyOp #FieldCols=3 #ValueCols=0
                                                        │           │   └── Add: LookupPhyOp LogOp=Add LookupCols(6)('Sales'[OrderDate]) DateTime
                                                        │           │       ├── ColValue<'Sales'[OrderDate]>: LookupPhyOp LogOp=ColValue<'Sales'[OrderDate]>'Sales'[OrderDate] LookupCols(6)('Sales'[OrderDate]) DateTime
                                                        │           │       └── Constant: LookupPhyOp LogOp=Constant Integer 1
                                                        │           └── Subtract: LookupPhyOp LogOp=Subtract LookupCols(7)('Sales'[ShipDate]) DateTime
                                                        │               ├── ColValue<'Sales'[ShipDate]>: LookupPhyOp LogOp=ColValue<'Sales'[ShipDate]>'Sales'[ShipDate] LookupCols(7)('Sales'[ShipDate]) DateTime
                                                        │               └── Constant: LookupPhyOp LogOp=Constant Integer 1
                                                        └── ColValue<'Calendar'[Date]>: LookupPhyOp LogOp=ColValue<'Calendar'[Date]>'Calendar'[Date] LookupCols(16)('Calendar'[Date]) DateTime

​image​

如果直接画结构流程图太长了,我将一些中间步骤精简一下,留下我觉得需要关注的操作符。找一下刚才在逻辑查询计划的NOT( ISEMPTY ( FILTER ( Table, Condition ) ) )​部分标记出来。

IN .
DatesBetween
21/45:Extend_Lookup
20/44:DatesBetween
30/54:Subtract
Sales[ShipDate] - 1
23/47:GroupBy
'Sales'[OrderDate], 'Sales'[ShipDate]
27/51:Add
Sales[OrderDate]+1
26/50:VQ2
Sales[RowNumber]
Sales[OrderDate]
Sales[ShipDate]
13:Extend_Const
IsEmpty
12:Not
16:Filter
1:AddColumns
4:VQ1
Calendar[Date]
6:Aggregate
Count
7:Filter
8:CrossApply
9:MultiValuedHashLookup
11:VQ2
Sales[RowNumber]
Sales[OrderDate]
Sales[ShipDate]
17:CrossApply
18:MultiValuedHashLookup
19:GroupBy
Calendar[Date], Sales[OrderDate],Sales[ShipDate]
33:InnerHashJoin
37:VQ1
Calendar[Date]
41:Extend_Lookup
43:Aggregation
GroupBy
57:Calendar[Date]

底层计算过程

从下往上,按大块来分析,首先分析 [DatesBetween] 这一块

一

DatesBetween 块

DatesBetween
21/45:Extend_Lookup
20/44:DatesBetween
30/54:Subtract
Sales[ShipDate] - 1
23/47:GroupBy
Sales[OrderDate], Sales[ShipDate]
27/51:Add
Sales[OrderDate]+1
26/50:VQ2
Sales[RowNumber]
Sales[OrderDate]
Sales[ShipDate]

(1) 26/50:VQ2​ 包含 Sales 表中每一行的 [RowNumber]、[OrderDate]、[ShipDate],共 121253 行

(2) 23/47:GroupBy​ 将 (1)# 的结果表按照 Sales[OrderDate]、Sales[ShipDate] 分组,共 1081 行

(3) 21/45:Extend_Lookup​ 与 27/51:Add​、 30/54:Subtract​ ,从(2)# 的结果中逐行计算 Sales[OrderDate]+1​ 和 Sales[ShipDate] -1​,仍是 1081 行

(4) 20/44:DatesBetween​ 以(3)# 中每一行的 Sales[OrderDate]+1​ 和 Sales[ShipDate] -1​ 作为条件,从 Calendar[Date] 筛选出符合 Sales[OrderDate] < Calendar[Date] < Sales[ShipDate] 的数据并与对应的 Sales[OrderDate]、Sales[ShipDate]合并,共 6444 行

可以用下面这段 DAX 查询代码的结果来理解

​image​

二

DatesBetween 往上

16:Filter
17:CrossApply
18:MultiValuedHashLookup
19:GroupBy
Calendar[Date], Sales[OrderDate],Sales[ShipDate]
33:InnerHashJoin
37:VQ1
Calendar[Date]
41:Extend_Lookup
43:GroupBy
Calendar[Date]
57:Calendar[Date]
DatesBetween
Calendar[Date],Sales[OrderDate],Sales[ShipDate]

​43:GroupBY​把 DatesBetween​块产生的 6444 行数据(包含 Calendar[Date], Sales[OrderDate],Sales[Shipdate] 3 列) 按 [Date] 分组,等价于对 [Date] 列去重返回不重复值,共 1079 行

​41:Extend_Lookup​和57:Calendar[Date]​是从43:GroupBy​中取出行上下文中的 [Date] 值

​37:VQ1​从存储引擎获取 VQ1,得到 Calendar[Date] 列中共 1081 个值

​33:InnerHashJoin​从37:VQ1​中逐一取出 [Date] 值,并通过 41:Extend_Lookup​和57:Calendar[Date]​从43:GroupBy​中取出 DatesBetween​ 生成的 [Date] 值逐一比对,保留相同值。也可以简单描述为:37:VQ1​ 与 43:GroupBy​ 做内连接,得到 两列 [Date],[Date]​。共 1079 行

两列都是 [Date] 吗?是的。

为了进一步说明这里在做什么,下面要讲一些让人头晕的东西,我将用 [Date](16)​ 表示 DatesBetween​ 中的 [Date],用 [Date](0)​表示 Calendar 表中的 [Date]:

这个过程有点绕,

​43:GroupBy​ 从 DatesBetween​ 中取出 [Date](16)​(的不重复值),37:VQ1​ 从 Calendar 表中取出[Date](0)​,33:InnerHashJoin​将[Date](16)​和 [Date](0)​内连接,得到两者的交集,含有两列,[Date](16), [Date](0)​。

​19:GroupBy​ 从 DatesBetween​ 中取出[Date](16), [OrderDate],[ShipDate]​,17:CrossApply​ 通过 18:MultiValuedHashLookup​,将 19:GroupBy​ 的[Date](16), [OrderDate],[ShipDate]​ 与 33:InnerHashJoin​ 的 [Date](16), [Date](0)​ 通过该表中的 [Date](16)​匹配并合并,最终得到是一张有 4 列的表,这4列分别是:

​[Date](0)​, [Date](16)​, Sales[OrderDate]​, Sales[ShipDate]​

共 6444 行。

为什么会出现[Date](0)​, [Date](16)​这两个东西,不都是 Calendar[Date] 列吗?

数字 0 和数字 16 可不是我乱编的,而是真实记录在物理查询计划代码中的,比如第一行

​image​

AddColumns: IterPhyOp LogOp=AddColumns IterCols(0, 1)('Calendar'[Date], ''[OpenOrders2])

​IterCols(0,1)('Calendar'[Date], ''[OpenOrders2])​,IterCols​后面第一个括号里的0​,对应了第二个括号中的第一个参数'Calendar'[Date]​。

在后面的物理查询计划中,凡是出现IterCols(..0..)(..'Calendar'[Date]..)​字样,都是 0​ 表示这个[Date]​列跟第一行的[Date]​列来自同一处。

再比如第33行,也就是上文的33:InnerHashJoin​

​image​

InnerHashJoin: IterPhyOp LogOp=Is IterCols(0, 16)('Calendar'[Date], 'Calendar'[Date])

​IterCols(0, 16)('Calendar'[Date], 'Calendar'[Date])​,这里提供的信息告诉我们,两个 Calendar[Date] 一个是 [Date](0)​,另一个是[Data](16)​,这其中的[Date](0)​与前面一段话中说的第一行中的 [Data](0)​ 是同一个。

那[Date](16)​是从哪来的?

在第44行:

​image​

DatesBetween: IterPhyOp LogOp=DatesBetween IterCols(6, 7, 16)('Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])

这就是DatesBetween​块中的 20/44:DatesBetween​,(20/44​是表示在第20行和第44行都是相同的代码)

​image​

​DatesBetween​ 生成的数据中包含的 Calendar[Date]​ 是 [Date](16)​,与[Date](0)​是不同的。

为什么要区分不同的 [Date]​?回想一下 DAX 理论,哪个部分讲过需要对同一列进行区分?

是嵌套行上下文,DAX 理论告诉我们当行上下文存在嵌套时,同一列在不同层次的行上下文中,可以有不同的值,可以使用函数 EARLIER​ 取得某层的行上下文。

在底层的物理查询计划中,虽然名称一样,但来自不同内容的列,用不同的数字标号标记,比如[Date](16)​ 来自 DatesBetween​迭代器,而[Date](0)​来自 AddColumns​迭代器,

也许这样标记一下会更好理解

​image​

总之,本小节描述的这一部分物理查询计划,生成了这样一个结果:

​[Date](0), Sales[OrderDate](6), Sales[ShipDate](7), [Date](16)​

物理查询计划的第16行也对此有提示

​image​

Filter: IterPhyOp LogOp=Filter IterCols(0, 6, 7, 16)('Calendar'[Date], 'Sales'[OrderDate], 'Sales'[ShipDate], 'Calendar'[Date])

​6​和7​分别对应了Sales[OrderDate]​, Sales[ShipDate]​,不过这里不重要,因为全篇没有其他 Sales[OrderDate]​, Sales[ShipDate]​ 需要区分。

非要弄出两个[Date]​来有什么意义吗?有,往后看。

三

继续往上,7:Filter​ 将通过 IN​ 的判断从 Sales 表中筛选出符合条件的数据。

IN .
13:Extend_Const
IsEmpty
12:Not
16:Filter
[Date](0), [Date](16), Sales[OrderDate], Sales[ShipDate]
14:Spool_Iterator
by
[Date](0), [OrderDate], [ShipDate]
7:Filter
8:CrossApply
9:MultiValuedHashLookup
11:VQ2
Sales[RowNumber]
Sales[OrderDate]
Sales[ShipDate]

​7:Filter​ 、 8.CrossApply​ 、9:MultiValuedHashLookup​ 操作符用 11:VQ2​中每一行的 Sales[OrderDate] ​和 Sales[ShipDate]​ 作为匹配列,将 11:VQ2​ 与 14:Spool_Iterator​ 以Sales[OrderDate], Sales[ShipDate]​为匹配列进行查询合并,得到另一张包含4列的结果表,共 714840 行,实际这就是将 Sales 表里每一行的 [OrderDate]、[ShipDate] 期间的 Calendar[Date] 合并了进来。

​[Date](0)​, Sales[RowNumber]​, Sales[OrderDate]​, Sales[ShipDate]​

​Date[16]​不见了,只剩下 [Date](0)​。为什么需要 [Date](0)​而不需要[Date](16)​,继续往下看。

四

最后一步

1:AddColumns
4:VQ1
Calendar[Date](0)
6:Aggregate
Count
7:Filter
[Date](0), Sales[RowNumber], Sales[OrderDate], Sales[ShipDate]

​1:AddColumns​ 以 4:VQ1​为基准,通过 6:Aggregate​对7:Filter​生成的数据表,以 [Date](0)​进行分组并进行计数,得到最终的结果。

注意我在 4:VQ1​块特意标记了 (0)​,因为这个 [Date]​来自AddColumns​。上一节7:Filter​的结果中留下 [Date](0)​丢掉[Date](16)​的原因就在这里,既然最后一步要用到 [Date](0)​来匹配,就需数据表里也包含有 [DATE](0)​,如果7:Filter​里留下的是 [DATE](16)​,那就不能匹配了。

在 DAX 底层,很多操作的作用是为了匹配列,比如本例中DatesBetween​ 中的 [Date](16)​经过步骤三最后生成了匹配的[Date](0)​。

性能分析

优化方案2的性能介于优化方案1和优化方案(番外)之间,这几篇文章一直在介绍中间步骤产生的数据量的大小对性能的影响,那么就把这3个方案放一起再横向比较一下。

​比较3个方案​

从物理查询计划的行数来看,优化方案2的物理查询计划的确是最长的,由于 DatesBetween的存在,它的步骤看上去也确实很多,但大多数步骤处理的数据量相对较小,只有图中标记的第1(2次)、6、7步处理了十万级的数据,共4次。

优化方案1中两个独立的 FILTER 产生了两个 交叉表+筛选,1(2次)、2(2次)、3(2次)、4(2次)、6、7 步需要处理十万级的数据,共10次。

优化方案(番外) 只在 1(2次)、2(2次)两步需要处理十万级的数据,共4次。


优化方案1优化方案2优化方案(番外)
处理10万级数据的步骤次数1044
测试总耗时(毫秒)【环境1】290.479.656.8
测试总耗时(毫秒)【环境2】300.676.453.8

处理10万级以上数据的步骤数量差异、各步骤处理的具体数据数量差异,再加上剩下的其他步骤,导致了3个方案的性能差异。优化方案(番外)耗时更少的原因,应该是它的步骤更少。

瓶颈分析

一、 RowNumber

很显然,RowNumber 的出现增加需要处理的数据量,使 121253 行 Sales 表都需要物化,尽管在整个底层计算过程中只出现了 3 次,分别在第1(2次)、6步,如果能想法将 [RowNumber] 去掉也许会更好。

二、DateBetween 的潜在问题

第一步使用了 DatesBetween 从 Calendar[Date] 中获取 [OrderDate]+1、[ShipDate]-1 区间的日期,其生成数据量取决于 [OrderDate] 与 [ShipDate] 之间的天数,例如用一个简化的数据表作演示,第一步计算过程和结果如下:

在这里插入图片描述

图中用红框标记了 Sales 表里 RowNumber = 3 这行及其对应的结果,这一行的 OrderDate 和 ShipDate 分别是 2021/1/2 和 2021/1/7,生成的结果表便包含了之间的日期 2021/1/3, 2021/1/4, 2021/1/5, 2021/1/6 ,共4行。

如果 OrderDate 与 ShipDate 两个日期相差很大,比如 2021-1-1 到 2022-1-1,那么生成的结果表中将包含近一整年的日期,数据量增加。

DatesBetween块产生的数据量,取决于 Sales 表中各行 OrderDate 与 ShipDate 之间包含的天数。
在这里插入图片描述

在本案例中,Sales 表中除了部分行的 [ShipDate] 记录是空白,有记录的每一行 [OrderDate] 与 [ShipDate] 都是间隔 6 天。1074 个不重复值对 { [OrderDate] , [ShipDate] },每个都间隔6天,于是在这一步产生的数据量是 1074 × 6 = 6444 行。
当更换另一张 Sales 表,如果存在大量数据行中 [OrderDate] 与 [ShipDate] 相差间隔日期很大,那么这一步生成的数据量就不是 6444 ,而可能是上万行甚至更多,计算速度就会变慢。
优化方案2并不具备通用性,它的计算效率受 Sales 表中 [OrderDate] 和 [ShipDate] 两列数据的分布影响。

因此,优化 DAX 计算性能,并不是只需要关心 DAX 代码怎么写,还要关注数据模型的情况。

总结

一

物理查询计划会通过给列加序号来区分,同一个列可以重复出现但序号不同,可以与 DAX 理论中行上下文嵌套的概念参照对应。

二

DAX 底层很多重复的操作块,很多时候是在更换列,比如本例中 DatesBetween 生成的 [Date](16)​,需要在最后一步之前与 AddColumns 生成的 [Date](0)​匹配对应。

三

DAX 底层处理大量数据的步骤越多,执行需要的时间越长,因此优化的方向是:减少步骤 + 尽快减少数据量。比如案例中的最终结果只有 1081 行,比较3个方案,优化方案(番外) 从 1081×1081=1168561 行开始,快速几步就将需要处理的数据从百万级降到千级,所以目前它计算得最快。

四

优化方案可能只对当前数据模型有效,换一个数据模型,同样的写法可能并不能取得优化效果。

模拟每个步骤计算过程

鉴于本篇所讲的案例过程相对复杂,我在 Excel 中用 DAX、PowerQuery、公式 模拟了动态计算过程,用户可以自行更改数据源(需要手动更改日期表),刷新后可自动重算。

希望可以帮助理解本文。

下载资源文件
在这里插入图片描述

BI平台选型实战指南:数据就绪度、用户操作熵与演进韧性 BI平台不是单纯的可视化工具,而是组织数据能力的放大器。其核心在于理解底层原理——数据就绪度决定分析可信边界,用户操作熵影响业务采纳深度,演进韧性保障系统随业务持续生长。技术价值体现在将模糊业务问题(如销售漏斗断点、库存周转预警)转化为可执行的数据路径;典型应用场景覆盖快消品归因分析、制造业实时OEE监控、教培机构学情追踪等复杂环境。本文聚焦真实项目中反复验证的三大能力维度,结合Power BI、Tableau、Superset、Metabase等主流工具的落地约束与适配逻辑,为预算有限、数据杂乱、团队能力 阅读详情

相关推荐

3、配置 Power BI 开发工具

本文详细介绍了配置Power BI开发工具的完整流程,涵盖Power BI的最新更新资源、相较于Excel的优势、安全与数据源隐私设置、本地数据网关和Power BI Publisher for Excel的安装步骤,以及DAX Studio的安装与性能调优功能。同时提供了丰富的学习资源和社区博客推荐,帮助BI专业人员高效构建和管理Power BI解决方案,提升数据分析与可视化能力。

i8j9k0l1的博客 46

各种颠覆想象的PowerBI DAX案例

学习DAX需要先学习原理,用理论来指导实践,否则很容易就会写出错误的公式,而且错了也不知道哪里错,或者公式对了但也不知道哪里对。但你敢保证自己掌握的理论就是正确的吗?下面这些DAX案例,你能解释它们的结果吗?方式二:https://cloud.ximaple.com/s/MNf5。案例文件的获取方式请见文末。另外,新手不建议阅读,容易走火入魔!方式一:QQ群:344353627,可以自行加群,从群文件中获取。

333

48、利用高级版和分析服务进行扩展

本文深入探讨了如何利用 Power BI 高级版和 Analysis Services 进行商业智能项目的扩展。内容涵盖报告与可视化的性能优化策略、高级版容量的估算与配置方法,并详细比较了 Analysis Services 表格模型与 Power BI Desktop 的功能差异。同时,分析了 Azure Analysis Services 与本地 SSAS 在企业部署中的优势,特别是在大规模数据处理、多语言支持和专业开发管理场景下的应用。通过实际操作步骤和架构图示,帮助组织根据业务需求选择合适的分析服务

chair的专栏 42

研究《DAX查询计划 白皮书》中的案例(五)优化方案3

这是本系列的最后一个优化方案,这一篇的优化方案3是所有方案中最优秀的,它解决了优化方案2中产生的[RowNumber] 问题,使整个底层计算过程中全部步骤都没有出现数据量超过万行的情况,使性能获得了极大的提升!

一棵松的博客 1352

11、构建 Power BI 数据模型的全面指南

本文全面介绍了构建 Power BI 数据模型的关键步骤与最佳实践,涵盖基础指标(如净销售额、利润率、计数指标)的创建、二级关系处理(包括角色扮演关系与USERELATIONSHIP函数的应用)、深入分析指标(如定价趋势、几何平均值、交叉销售与客户活跃度分析)以及实际与预算对比模型的实现。同时探讨了指标融合对性能的影响、不同业务场景下的应用,并提供了拓展指标示例与未来发展趋势展望,帮助读者系统掌握 Power BI 高级建模技术,提升数据分析能力。

i8j9k0l1的博客 75

【信息科学与工程学】【产品体系】第三十三篇 DPU/smartinic芯片中的学科知识03

需求实现DMA 与流边界检测优化​分级超时状态机(短/中/长)+ 积压计数 + 批量触发DMA 控制器优化​描述符预取(深度 4)+ 中断合并(阈值 8)Fence 语义正确性验证​RTL 断言 + C 语言序列号检测 + 性能对比 + 硬件计数器所有代码均为可综合 Verilog,已在仿真器中通过基本功能测试。在 FPGA 原型上运行上述 RTL,配合 ChipScope 观察 fence 时序在 BlueField-3 上运行 DOCA DPA 测试程序,对比加/不加 fence 的结果。

weixin_49199313的博客 288

研究《DAX查询计划 白皮书》中的案例(三)优化方案(番外)

‍上一篇给出的优化方案中使用了两个 FILTER 函数,而如果将两个 FILTER 函数合并成一个,可以使计算性能进一步提升,但是白皮书中并没有记录该方案。为了查明原因,比较优劣,本篇先将白皮书的其他优化方案抛开,先研究一下这个番外优化方案的秘诀。

一棵松的博客 1022

研究《DAX查询计划 白皮书》的案例(一)初始方案

SQLBI 网站上提供的[《White paper: Understarnding DAX Query Plans》,发布于2023年7月17日,距今已超过10年,随着DAX 版本不断更新优化,原文中关于查询计划的描述已不再符合当前实际情况。为充分理解、运用该文阐述的分析方法,以当前 DAX 版本运行文中案例,尝试理解查询计划及优化思路。

一棵松的博客 1017

研究《DAX查询计划 白皮书》中的案例(二)优化方案1

上一篇文章中介绍了初始方案的查询计划,找到了性能瓶颈来自 FILTER 的一参使用 Sales 整表,在底层执行时需要计算数据量巨大的交叉表,导致公式引擎部分耗费大量时间,确定了改进方向是想法提高 FILTER 的性能,减少迭代对象的势。不过白皮书给出的第一个方案并没有直接从减少 FILTER 迭代对象的势入手,而是采取了 CALCULATE+FILTER 的写法,这也是许多初学者可能采取的方案。那么本篇文章将继续通过理解查询计划,了解其底层执行过程,分析其优劣势。

一棵松的博客 559

SUMMARIZECOLUMNS 的疑似 bug(1)引言

与 Excel 通过 MDX 创建报表的方式不同,PowerBI 在代码层面主要是通过 SUMMARIZECOLUMNS​ 函数计算报表中各个视觉对象,它实现了 MDX 中的 auto-exist 功能并具有诸多特性,能够在许多场景中代替ADDCOLUMNS​+ SUMMARIZE​的应用。在特定情况下,SUMMARIZECOLUMNS ​的计算结果似乎并不符合 DAX 理论,本篇介绍这些疑似存在 BUG 的现象。

一棵松的博客 1382

AWS原生CDP最小可行架构:从零构建可审计、可伸缩、可计费分摊的智能客户数据平台

智能客户数据平台(CDP)是企业实现统一身份识别、跨渠道归因与实时批处理融合的核心基础设施。其本质并非复杂算法堆砌,而是云原生环境下数据采集、存储、计算与治理的协同工程。基于AWS原生服务构建CDP,可规避Kafka运维负担与Hadoop调参陷阱,依托Kinesis、DynamoDB、Athena/Iceberg、Glue和Lambda等服务,实现高吞吐摄入、毫秒级查询、ACID批处理及自动血缘追踪。技术价值体现在资源可审计、弹性可伸缩、成本可分摊——尤其适合日均亿级事件的零售、金融与SaaS场景。本文聚焦

weixin_29611239的博客 235

Power BI Copilot在Microsoft Fabric中的实战应用与避坑指南

自然语言查询是现代BI工具的核心交互范式,其本质是将业务意图转化为可执行的数据逻辑。Power BI Copilot依托Microsoft Fabric统一元数据、实时权限与弹性算力三大底座,实现DAX生成、报表构建与数据解释三大能力闭环。它并非替代分析思维,而是将传统需数小时完成的建模调试、视觉设计与知识交接压缩至分钟级,显著降低DAX学习门槛并提升协作效率。典型应用场景包括业务分析师的即席洞察、数据工程师的代码辅助及新手的零文档上手。本文聚焦Fabric环境下Copilot的原理边界、实操路径与7大高发

weixin_34348805的博客 428

将一维数据(序列)转化为二维数据(图像)的方法汇总GAFS, MTF, Recurrence plot,STFT

虽然深度学习方法(1D CNN, RNN, LSTM 等)可以直接处理一维数据,但是当前的深度学习方法主要还是处理二维结构数据的,特别是在计算机视觉CV以及自然语言处理NLP领域,各种各样的方法层出不穷。因此,如果能够将一维序列数据转化为二维(图像)数据, 则可以直接结合CV以及NLP领域的方法,是不是很有趣! GAF, GASF, GADF, MTF, Recurrence Plot, STFT, 短时傅里叶变换

MasterQKK 5万+

Power BI星型模型实战:VertiPaq性能优化核心指南

星型模型是BI数据建模的基础范式,其本质源于列式内存引擎(如VertiPaq)对窄事实表与宽维度表的物理适配需求。原理上,它通过整数外键映射、字典压缩和位图索引显著提升存储效率与DAX聚合速度;技术价值体现在模型体积缩减、刷新耗时降低及内存占用可控——实测重构后模型可缩小65%、刷新提速3倍;典型应用场景覆盖零售销售分析、财务多维报表及大规模交易明细建模;而关系基数、单向筛选与键值设计等关键配置,正是决定VertiPaq能否高效运转的底层开关。本文聚焦Power BI生产环境中的星型模型落地,深度融合Con

weixin_34122810的博客 435

unity 腾讯地图sdk移动和缩放代码

unity 腾讯地图sdk 实现移动端 缩放 和 移动

。。。。。 1283

论文分享|ECCV2022,PATnet,Cross-Domain Few-Shot Semantic Segmentation,CDFSS新基准,文末有复现教程

这篇论文提出的跨域小样本语义分割(CD-FSS)任务,旨在将源域学习的分割能力迁移到目标域,仅需少量标注样本。针对传统小样本分割方法在跨域场景性能下降的问题,作者提出了基于金字塔锚点变换的网络(PATNet),通过将域相关特征转换为域无关特征,并引入任务自适应微调推理(TFI)提升目标域适应能力。实验表明,PATNet在1-shot和5-shot设置下分别比现有方法提升8.49%和10.61%的准确率。同时,作者构建了CD-FSS基准并设计了任务难度评估指标。代码和数据集已开源,复现难度中等。

m0_74277350的博客 1095

Power BI 指导性文档

Power BI 指导性文档 巨人之肩的牛顿今天 轻松地连接到数据、对数据进行建模和可视化,从而创建通过 KPI 和品牌进行个性化的令人印象深刻的报表。快速获取针对业务问题的受 AI 支持的答案,即使是使用对话语言提问,也是如此。 通过连接到您的所有数据源,在组织中分析、共享和优化见解,同时保持数据准确性、一致性和安全性,从而充分利用大数据投资。 可以跨常用的 Microsoft Office 应用程序(例如 Microsoft Teams 和 Excel)轻松地协作处理...

computer19810的专栏 422

Win11如何更改用户文件夹名(完整流程- 已验证可行)

把C盘所有文件备份,如果不想备份,则选择创建一个C盘的系统还原点(能在后续把C盘玩崩之后恢复原状)

Y_Aryan的博客 5万+

【Linux】xfs 文件系统的优势与配置

本文深入探讨了XFS文件系统在Linux环境下的核心优势与配置方法。XFS作为高性能日志型文件系统,具有支持超大文件(8EB)、高并发I/O、在线扩展、强大元数据校验等显著特点。文章通过基准测试对比了XFS与ext4、btrfs的性能差异,显示XFS在各类文件操作中均表现优异。详细介绍了XFS的挂载参数调优建议,包括noatime、logbufs等关键配置项。最后提供了Java应用适配XFS的实用示例代码,展示了大文件分块上传的实现方案。本文为开发者与运维人员提供了从理论到实践的完整指导,帮助构建高可靠、高

千淘万漉虽辛苦,吹尽狂沙始到金 2万+

FOC观测器之龙伯格观测器

状态观测器: 理论依据:根据控制理论,如果一个系统能够完全通过其检测到的输出值来重构其系统状态,则认为该系统是可观测的。其作用于无传感器转子的位置和速度的检测反馈,再作用到PARK逆变换和转矩磁链控制上。 Luenberger观察器原理图: Luenberger观测器的离散化: (其观测器的闭环参数为经验验证值) 引入估测电流( iα,iβ )的反馈值并离散化(T 为采样时间),可得...

ShawnChan的博客 2万+

Vision-Language Navigation - Windows环境下编译安装Matterport3D Simulator仿真器(三)

目录1 编译 Matterport3D Simulator 源码1.1 源码下载1.2 编译 1 编译 Matterport3D Simulator 源码   在 Windows 下编译 Matterport3D Simulator 稍微有些不方便,需要对 CMakeLists.txt 进行一些修改,如果有更好的方法欢迎大家评论。 1.1 源码下载   还是直接在 Github 上搜索 Matterport3D Simulator 的 repo,下载源码。如果用 zip 方式,那么直接在下即可。如果使用

weixin_44110393的博客 4845

Power BI电商销售看板实战:从MySQL直连到实时决策

数据可视化看板是现代电商销售运营的核心基础设施,其本质是将数据库中的原始业务数据(如MySQL订单、库存、用户行为)转化为可执行的决策信号。实现这一目标需兼顾三重能力:稳定高效的数据源接入能力、贴合电商业务逻辑的指标建模能力(如净销售额、新客归因、库存周转),以及面向非技术人员的自助分析体验。Power BI作为主流BI工具,其价值不仅在于图表呈现,更在于通过直连MySQL、合理设计DAX度量值、构建动态预警视图,打通‘数据—洞察—行动’闭环。本文聚焦电商场景下Power BI与MySQL协同落地的关键实践

weixin_34341229的博客 164

用 XCO 库一键找出某个 ABAP 类的全部子类

ABAP XCO库子类枚举技术解析 本文介绍了在ABAP项目中如何利用XCO库高效获取类的继承层次结构。传统方法使用CL_ABAP_CLASSDESCR或手动维护映射表存在效率低、易遗漏的问题。新方法通过XCO库的IF_XCO_AO_CLASS接口,可简洁地获取父类的所有子类(包括间接子类)。 核心API链式调用: 获取类句柄:xco_cp_abap=>class() 导航子类集合:subclasses->all 获取结果:get()返回类引用表或get_names()返回类名列表 典型应用场景

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 127

2026大专财务专业学数据分析的价值分析

例如,可以在数据分析、数据科学等领域担任更高级别的职位,拓展自己的职业发展空间。通过 CDA 认证的学习和考试,可以系统地掌握数据分析的知识和技能,提升自己在数据分析方面的能力。CDA 认证可以证明他们具备扎实的数据分析能力和专业素养,增强他们在人工智能、大数据分析、金融等领域的就业竞争力。蒙特卡洛模拟在基建项目评估中的应用:某PPP项目通过5000次迭代测算,将预期IRR波动范围从±3%压缩至±1.2%。某商业银行真实案例:学生参与标注的3万条交易数据,用于训练XGBoost模型,最终召回率达89%。

YangYang9YangYan的博客 577

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

大数据分析师核心能力框架与技术实战指南

数据分析作为数字化转型的核心驱动力,其本质是通过数据工具链、分析方法论和业务洞察的三维融合实现价值转化。在技术层面,SQL的多表连接与窗口函数、Python的pandas数据变形、Hadoop生态的HiveQL等构成了数据处理的基础设施;而统计思维中的A/B测试样本量计算、业务诊断中的决策树模型应用则体现了分析方法的工程实践价值。随着企业数据湖架构和特征存储等技术的普及,分析师需掌握从数据清洗到模型监控的全链路能力。本文以大数据分析师的薪资溢价47%为切入点,详细拆解了数据处理四件套、可视化叙事技巧等热门前

weixin_34087307的博客 332

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

上一篇: 研究《DAX查询计划 白皮书》中的案例(三)优化方案(番外)
下一篇: 研究《DAX查询计划 白皮书》中的案例(五)优化方案3
apinerand
博客等级 码龄9年 201粉丝 11原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值