在這個教學中,你會使用 PowerShell 提交一個包含多個語句的 EVALUATE 請求,然後解析多結果集的 Apache Arrow 回應。 此模式允許您一次從 PowerShell 自動化腳本取得多個相關結果集。
為什麼要在同一次申請中提交多個 EVALUATE 陳述
執行 DAX 查詢 API 接受 query 一個字串,且可包含多個 EVALUATE 語句。 每個語句回傳自己的結果集,回應主體是每個語句中一個 Arrow IPC 串流 EVALUATE 的串接,並依宣告順序排列。 將相關查詢一併提交,可避免分開進行個別 HTTP 呼叫時每次請求都會產生的額外負荷,包括額外的 Microsoft Entra 權杖驗證和 DAX 引擎初始化。 在同一個請求中傳送多個 EVALUATE 語句,也有助於減輕請求限速的影響。 Power BI 針對語意模型查詢作業,將每位使用者的查詢要求限制為每分鐘 120 次。
您建置的什麼
在一個 PowerShell 腳本中,你:
- 取得 Microsoft Entra 存取權杖。
- 建立一個包含
query三個EVALUATE陳述的請求體。 - 發送請求並擷取原始的 Arrow IPC 回應串流。
- 將回應解析成每句
EVALUATE話的一個結果集合。 - 將每個結果集顯示為 PowerShell 物件。
先決條件
- PowerShell 7.4 或更新版本。 Windows PowerShell 5.1 不受支援,因為本教學課程中使用的
Apache.Arrow套件與 PowerShell 5.1 內含的System.Memory組件發生衝突。 - 一個位於 Premium 或 Fabric 容量上的 Power BI 工作區,至少擁有一個語義模型。
- 在語意模型上建立並讀取權限。
- 用於認證的 MicrosoftPowerBIMgmt 模組。 這些 cmdlets 會使用 Microsoft 官方的 Power BI 用戶端應用程式,因此你不需要在 Microsoft Entra 中註冊自己的應用程式。
-
Apache.Arrow 與 Apache.Arrow.Compression .NET 函式庫用於反序列化回應。 執行 DAX 查詢 REST API 會使用 LZ4 框架壓縮格式來壓縮 Arrow 緩衝區,因此需要
Apache.Arrow.Compression及其相依性(K4os.Compression.LZ4、K4os.Compression.LZ4.Streams、K4os.Hash.xxHash、ZstdSharp.Port)。 下一步會說明如何下載它們。 - 下列為在 Power BI 管理入口網站中啟用的租戶設定:
- 資料集執行查詢 REST API (在 開發者設定中)。
- 允許使用 XMLA 端點並在 Excel 中分析本地語意模型(整合設定)。
使用 winget 安裝 PowerShell 7.4 或更新版本:
winget install --id Microsoft.PowerShell --source winget
安裝完成後,使用 pwsh 啟動新的 shell。 執行該場次教學中剩餘的指令。
安裝 MicrosoftPowerBIMgmt 模組。 該-Force旗標接受 PowerShell 資源庫 的不受信任儲存庫提示。
Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force
下載所需的 NuGet 套件並將其組合檔解壓成 C:\Tools\Apache.Arrow\。
.nupkg檔案是 ZIP 壓縮檔,所以Expand-Archive可以直接在壓縮檔上運作。 迴圈會選擇每個套件中最高的 netX.0 目標資料夾,使組件在發佈新目標時保持相容。
$dest = "C:\Tools\Apache.Arrow"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
$packages = @(
"Apache.Arrow",
"Apache.Arrow.Compression",
"K4os.Compression.LZ4",
"K4os.Compression.LZ4.Streams",
"K4os.Hash.xxHash",
"ZstdSharp.Port"
)
foreach ($pkg in $packages) {
$nupkg = Join-Path $env:TEMP "$pkg.nupkg"
$expand = Join-Path $env:TEMP $pkg
if (Test-Path $expand) { Remove-Item $expand -Recurse -Force }
Invoke-WebRequest -Uri "https://www.nuget.org/api/v2/package/$pkg" -OutFile $nupkg
Expand-Archive -Path $nupkg -DestinationPath $expand -Force
$libDirs = Get-ChildItem (Join-Path $expand "lib") -Directory
$best = $libDirs | Where-Object { $_.Name -match "^net\d" } |
Sort-Object Name -Descending | Select-Object -First 1
if (-not $best) {
$best = $libDirs | Sort-Object Name -Descending | Select-Object -First 1
}
Get-ChildItem (Join-Path $best.FullName "*.dll") |
Copy-Item -Destination $dest -Force
}
1 - 認證
以互動方式登入 Power BI 服務,然後擷取存取權杖。 這個 Connect-PowerBIServiceAccount cmdlet 不要求你在 Microsoft Entra 註冊自己的應用程式。
Connect-PowerBIServiceAccount -WarningAction SilentlyContinue
$accessToken = (Get-PowerBIAccessToken).Authorization -replace '^Bearer\s+',''
2 - 建立包含多個 EVALUATE 陳述句的請求
定義工作空間與語意模型目標。 然後建立請求體。 該 query 性質是一個包含三個 EVALUATE 以空行分隔的陳述的單一字串。
$groupId = "YOUR_WORKSPACE_ID"
$datasetId = "YOUR_DATASET_ID"
$query = @"
EVALUATE
ROW("RowCount", COUNTROWS('Sales'))
EVALUATE
TOPN(10, 'Sales', 'Sales'[Amount], DESC)
EVALUATE
SUMMARIZECOLUMNS(
'Date'[Year],
"TotalSales", SUM('Sales'[Amount]))
"@
$body = @{
query = $query
resultsetRowcountLimit = 500000
} | ConvertTo-Json
3 - 傳送請求並擷取原始回應串流
發送 POST 請求,並將回應內容視為二進位串流。 使用 HttpWebRequest 而非 Invoke-RestMethod、 Invoke-PowerBIRestMethod、 或 Invoke-WebRequest。 回應是二進位的 Arrow IPC 串流。 高階的 PowerShell 指令本會將回應內容解讀為文字,導致二進位內容損毀。
HttpWebRequest 傳回未經修改的原始串流。
$url = "https://api.powerbi.com/v1.0/myorg/groups/$groupId" +
"/datasets/$datasetId/executeDaxQueries"
$request = [System.Net.HttpWebRequest]::Create($url)
$request.Method = "POST"
$request.ContentType = "application/json"
$request.Accept = "application/vnd.apache.arrow.stream"
$request.Timeout = 180000 # milliseconds
$request.Headers.Add("Authorization", "Bearer $accessToken")
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)
$requestStream = $request.GetRequestStream()
$requestStream.Write($bodyBytes, 0, $bodyBytes.Length)
$requestStream.Close()
$response = $request.GetResponse()
$responseStream = $response.GetResponseStream()
# Buffer the response into memory so the parser can iterate over multiple Arrow IPC streams.
$memoryStream = New-Object System.IO.MemoryStream
$responseStream.CopyTo($memoryStream)
$responseStream.Close()
$response.Close()
$memoryStream.Position = 0
4 - 解析多結果集回應
回應主體是將每個 EVALUATE 陳述式各自對應的一個 Apache Arrow IPC 串流串接而成。 PowerShell 本身沒有隨附 Arrow 解析器,因此此步驟會透過以 Add-Type 新增的小型內嵌 C# 輔助程式來載入 Apache.Arrow .NET 程式庫。 用 C# 保留串流迴圈邏輯可以縮短呼叫站點,並回傳一份結果集清單,讓你的 PowerShell 腳本可以迭代。 幫助工具在每個串流結束標記後開啟一個新的 ArrowStreamReader ,因此同一迴圈可處理回應中任意數量的結果集。
Add-Type -Path "C:\Tools\Apache.Arrow\Apache.Arrow.dll"
Add-Type -Path "C:\Tools\Apache.Arrow\Apache.Arrow.Compression.dll"
# Reference the full .NET reference set that ships with PowerShell 7 so the
# inline C# below can resolve BCL types such as List<T> and Dictionary<,>.
$refs = Get-ChildItem "$PSHOME\ref\*.dll" | ForEach-Object FullName
$refs += Get-ChildItem "C:\Tools\Apache.Arrow\*.dll" | ForEach-Object FullName
Add-Type -ReferencedAssemblies $refs -IgnoreWarnings -WarningAction SilentlyContinue -TypeDefinition @"
using System;
using System.Collections.Generic;
using System.IO;
using Apache.Arrow;
using Apache.Arrow.Compression;
using Apache.Arrow.Ipc;
public class DaxResultSet
{
public List<string> ColumnNames = new List<string>();
public List<Dictionary<string, object>> Rows =
new List<Dictionary<string, object>>();
}
public static class DaxMultiResultReader
{
public static List<DaxResultSet> ReadAll(Stream stream)
{
var results = new List<DaxResultSet>();
var codecFactory = new CompressionCodecFactory();
while (stream.Position < stream.Length)
{
var rs = new DaxResultSet();
bool gotSchema = false;
using (var reader = new ArrowStreamReader(stream, codecFactory, leaveOpen: true))
{
RecordBatch batch;
while ((batch = reader.ReadNextRecordBatch()) != null)
{
using (batch)
{
if (!gotSchema)
{
foreach (var f in batch.Schema.FieldsList)
rs.ColumnNames.Add(f.Name);
gotSchema = true;
}
for (int r = 0; r < batch.Length; r++)
{
var row = new Dictionary<string, object>();
for (int c = 0; c < batch.ColumnCount; c++)
row[rs.ColumnNames[c]] = GetValue(batch.Column(c), r);
rs.Rows.Add(row);
}
}
}
}
if (gotSchema) results.Add(rs);
}
return results;
}
private static object GetValue(IArrowArray a, int i)
{
if (a == null) return null;
if (a is DictionaryArray da)
{
// Resolve the dictionary index, then look up the value in the dictionary.
int dictIndex;
switch (da.Indices)
{
case Int32Array idx32: if (idx32.IsNull(i)) return null; dictIndex = idx32.GetValue(i).Value; break;
case Int16Array idx16: if (idx16.IsNull(i)) return null; dictIndex = idx16.GetValue(i).Value; break;
case Int8Array idx8: if (idx8.IsNull(i)) return null; dictIndex = idx8.GetValue(i).Value; break;
case Int64Array idx64: if (idx64.IsNull(i)) return null; dictIndex = (int)idx64.GetValue(i).Value; break;
default: return da.Indices.ToString();
}
return GetValue(da.Dictionary, dictIndex);
}
if (a is StringArray sa) return sa.GetString(i);
if (a is BooleanArray ba) return ba.IsNull(i) ? (object)null : ba.GetValue(i);
if (a is Int64Array i64) return i64.IsNull(i) ? (object)null : i64.GetValue(i);
if (a is Int32Array i32) return i32.IsNull(i) ? (object)null : i32.GetValue(i);
if (a is DoubleArray d) return d.IsNull(i) ? (object)null : d.GetValue(i);
if (a is Decimal128Array dec) return dec.GetValue(i);
if (a is Date32Array d32) return d32.GetDateTime(i);
if (a is Date64Array d64) return d64.GetDateTime(i);
if (a is TimestampArray ts) return ts.GetTimestamp(i);
return a.ToString();
}
}
"@
$results = [DaxMultiResultReader]::ReadAll($memoryStream)
Write-Host "Received $($results.Count) result sets."
5 - 處理每個結果集
將每個結果集轉換成 PSCustomObject 列。 現在您可以將資料列透過管線傳遞至 Where-Object、Group-Object、Export-Csv 或任何其他 PowerShell Cmdlet。
function ConvertTo-PSObjectRows {
param([Parameter(Mandatory)] $ResultSet)
foreach ($row in $ResultSet.Rows) {
$obj = [ordered]@{}
foreach ($col in $ResultSet.ColumnNames) { $obj[$col] = $row[$col] }
[PSCustomObject]$obj
}
}
$rowCount = ConvertTo-PSObjectRows -ResultSet $results[0]
$topProducts = ConvertTo-PSObjectRows -ResultSet $results[1]
$yearTotals = ConvertTo-PSObjectRows -ResultSet $results[2]
$rowCount | Format-Table
$topProducts | Format-Table
$yearTotals | Format-Table
每個變數都儲存對應 EVALUATE 陳述式的列,依照陳述句在請求中出現的順序排列。
Troubleshooting
-
401 未授權 — 快取的代幣已過期。 再次執行
Connect-PowerBIServiceAccount以重新整理,然後從Get-PowerBIAccessToken重新讀取$accessToken。 -
Connect-PowerBIServiceAccount期間的 MSAL 警告 —MicrosoftPowerBIMgmt內含較舊版本的 MSAL.NET,會以警告層級發出內部追蹤訊息(例如:SetAuthorityUri、TryNormalizeRealm、MsaDeviceOperationProvider is not available)。 只要 Cmdlet 會輸出Environment/TenantId/UserName區塊,就可以放心忽略它們。 若要抑制它們,請傳入-WarningAction SilentlyContinue。 -
HTTP 200 帶有錯誤結果集 — HTTP 請求成功,但 Arrow 串流帶有錯誤。 檢查
IsError=true的結構描述中繼資料,並讀取FaultCode和FaultString。 詳情請參閱 執行 DAX 查詢 REST API 的最佳實務。 -
Invoke-RestMethod回傳模糊文字 — 請勿使用此Invoke-RestMethodInvoke-PowerBIRestMethodInvoke-WebRequestAPI。 回應是二元的;依步驟3所示使用HttpWebRequest。 -
Add-Type無法載入Apache.Arrow.dll— 在 Windows PowerShell 5.1 上,Apache.Arrow套件與內建的System.Memory組件發生衝突。 請使用 PowerShell 7.4 或更新版本。 -
傳回的結果集數量為零,或少於
EVALUATE陳述式的數量——確認每個EVALUATE陳述式本身的語法皆有效。 單一無效的EVALUATE會使 API 回傳錯誤,而非部分的多結果集回應。