Mashup engine がクエリを評価する所定の動作だから、Power BI Desktop や Excel の Power Query エディタだけでなんとかしようと拗らせても報われることは少ない。効率良い評価結果を得たいなら、まずは挙動の理解が必要だ。
そもそも、どういうことか
面倒なことに関心がないというなら、
'Query1' を参照するクエリ 'Query2' 'Query3' ごとに 'Query1' は評価される。
という理解をしておけばよい。何をしようとも変わらない挙動だから、これで充分かもね。
ログで確認
ここに記述されている通りなのだけど、Table.Buffer 関数の動作も含めて間違った解釈がされているのかもしれない。なので、ログを見て確認。
- Query1 : 5行のテーブルを出力し、列を追加
- Query2 と Query3 : それぞれ Query1 を参照し、先頭2行を選択
( message, value )=> Diagnostics.Trace(
TraceLevel.Information,
message, value, true
)
let
Source = Table.FromColumns(
{
{
fxTrace( "Query1R1", ()=> 1 ),
fxTrace( "Query1R2", ()=> 2 ),
fxTrace( "Query1R3", ()=> 3 ),
fxTrace( "Query1R4", ()=> 4 ),
fxTrace( "Query1R5", ()=> 5 )
}
},
type table [ Column1 = Int64.Type ]
),
AddedColumn2 = fxTrace(
"AddedColumn2",
()=> Table.AddColumn(
Source,
"Column2",
each fxTrace(
Text.Format( "#[Column1] * 10", _ ),
()=> [Column1] * 10
),
Int64.Type
)
)
in
AddedColumn2
let
Source = fxTrace(
"Query1 → Query2",
()=> Query1
),
KeptFirstRows = fxTrace(
"Query2 KeptFirstRows",
()=> Table.FirstN( Source, 2)
)
in
KeptFirstRows
let
Source = fxTrace(
"Query1 → Query3",
()=> Query1
),
KeptFirstRows = fxTrace(
"Query3 KeptFirstRows",
()=> Table.FirstN( Source, 2)
)
in
KeptFirstRows
( log as table )=>
let
FilteredRows = Table.SelectRows(
log,
each [Category] = "User"
),
RemovedOtherColumns = Table.SelectColumns(
FilteredRows,
{"Id", "Query", "Step", "Start Time", "Additional Info", "Path"}
),
ExpandedAdditionalInfo = Table.ExpandRecordColumn(
RemovedOtherColumns,
"Additional Info", {"Message"}
),
SortedRows = Table.Sort(
ExpandedAdditionalInfo,
{"Start Time", Order.Ascending}
)
in
SortedRows
ね、'Query2' と 'Query3' が評価されるときそれぞれで 'Query1' が評価されてるよね。
- Query1 はテーブルに列を追加するステップだけ
- Query1 を参照する Query2 と Query3 はテーブルの行を選択するステップだけ
列の追加と行の選択はそれぞれ別のクエリに定義されているけれども、"テーブルの行ごと" かつ "必要な行" のみ評価されるということが重要なのだ。クエリの評価は定義されたクエリごとに行われるということではない。そうしないと、ふんだんにメモリを積んでも足りなくなっちゃいますからね。
Table.Buffer 関数 / List.Buffer 関数の使いどころを間違ってはいないだろうか
Table.Buffer 関数を使用してもキャッシュ再利用のような効果を得られない
let
Source = Table.FromColumns(
{
{
fxTrace( "Query1R1", ()=> 1 ),
fxTrace( "Query1R2", ()=> 2 ),
fxTrace( "Query1R3", ()=> 3 ),
fxTrace( "Query1R4", ()=> 4 ),
fxTrace( "Query1R5", ()=> 5 )
}
},
type table [ Column1 = Int64.Type ]
),
AddedColumn2 = fxTrace(
"AddedColumn2",
()=> Table.AddColumn(
Source,
"Column2",
each fxTrace(
Text.Format( "#[Column1] * 10", _ ),
()=> [Column1] * 10
),
Int64.Type
)
),
// 👇ココから
BufferedQuery1 = fxTrace(
"BufferedQuery1",
()=> Table.Buffer( AddedColumn2 )
)
// 👆ココまで
in
BufferedQuery1
- 並列処理が見当たらないことは興味深い。扱うデータ量との兼ね合いがあるとしても、すべての評価が完了するまでの所要時間に影響する可能性がある。
- 必要な行だけ評価できることも確認できたが...
- Table.Buffer もクエリの評価ごとに実施されている。まぁ、無駄にメモリを消費しただけ。
Query2 と Query3 の評価中に Table.Buffer でキャッシュされたデータが共有されることはないと考えてよさそうなログである。DOCSにもそう書いてあるので間違いないでしょう。
Power BI Desktop にはキャッシュされたクエリの評価結果の一部分を再利用する機能があり、限定的な評価環境でのみキャッシュ利用されることがある。とはいえ、評価完了までの所要時間短縮を約束するものでもないし、Power BI service 上でのデータセット更新にはこの機能はない。
Table.Buffer でクエリ評価処理が速くなるという理解は、誤りの始まり
Table.Buffer 関数 / List.Buffer 関数の使いどころを間違ってはいないだろうかと。
このサンプルで Table.Buffer を仕掛けたけれども、結果に必要な行だけ評価できることも確認できた。これは Power Query の評価戦略に則っている。
所定の評価戦略がパフォーマンスに大きく影響していることが明白であるときのみ、Table.Buffer 関数 / List.Buffer 関数 の使用を検討するっていう感じかな。Table.Buffer 関数 / List.Buffer 関数を使用するケースは少ないし、クエリに定義したロジックやフロー、込めた想いの再検討再設計したほうが良い結果を得られるはずだ。
Table.Buffer 関数 / List.Buffer 関数は、Power Query の評価戦略を意図的に中断させるものとして考えておいたほうがよいのでしょう。戦略なく使用すると、その効果のわりにメモリ消費が大きく、結果的にパフォーマンス低下とななりがちね。よーく考えてよーく観察してよね。
思ったこと🙄
この動作が困るのだというなら、別の手段を検討しなければならない。ひとつの手段としては Power BI データフローを使うことだけれども、Power BI Desktop や Excel でこさえたスクリプトをそのままコピペしただけではだめですよ。Power BI データフローならではの動作もあるのだ。Power BI データフローで Computed entities を効果的に使うことが大事。
その他