Lernprogramm: Ausführen mehrerer EVALUATE-Anweisungen mit PowerShell

In diesem Lernprogramm verwenden Sie PowerShell, um eine einzelne EXECUTE DAX Queries REST-API-Anforderung zu senden, die mehrere EVALUATE Anweisungen enthält, und analysieren sie dann die Multi-Resultset Apache Arrow-Antwort. Mit diesem Muster können Sie in einem PowerShell-Automatisierungsskript mehrere zusammengehörige Ergebnismengen in einem einzigen Roundtrip abrufen.

Diagramm mit einer HTTP POST-Anforderung, die drei EVALUATE-Anweisungen im Abfragetext enthält, und der Pfeil-IPC-Antwort, die drei Resultsets in derselben Reihenfolge enthält.

Warum mehrere EVALUATE-Anweisungen in einer Anforderung übermittelt werden

Die EXECUTE DAX-Abfragen-API akzeptiert eine einzelne query Zeichenfolge, die mehrere EVALUATE Anweisungen enthalten kann. Jede Anweisung gibt ihre eigene Ergebnismenge zurück, und der Antworttextkörper ist die Verkettung jeweils eines Arrow-IPC-Datenstroms für jede EVALUATE-Anweisung in Deklarationsreihenfolge. Das Zusammensenden verwandter Abfragen vermeidet den Aufwand pro Anforderung getrennter HTTP-Aufrufe, einschließlich zusätzlicher Microsoft Entra Tokenüberprüfung und DAX-Modulinitialisierung. Das Senden mehrerer EVALUATE Anweisungen in einer Anforderung kann auch dazu beitragen, die Auswirkungen der Anforderungseinschränkung zu verringern. Power BI beschränkt Aufrufer auf 120 Abfrageanforderungen pro Minute pro Benutzer für Semantikmodell-Abfragevorgänge.

Was Sie erstellen

In einem PowerShell-Skript führen Sie Folgendes aus:

  1. Abrufen eines Microsoft Entra Zugriffstokens.
  2. Erstellen Sie einen Anforderungstext, dessen query drei EVALUATE Anweisungen enthält.
  3. Senden Sie die Anfrage und erfassen Sie den rohen Arrow-IPC-Antwortdatenstrom.
  4. Parsen Sie die Antwort in eine Ergebnismenge pro EVALUATE-Anweisung.
  5. Zeigt jedes Resultset als PowerShell-Objekte an.

Voraussetzungen

  • PowerShell 7.4 oder höher. Windows PowerShell 5.1 wird nicht unterstützt, da das Apache.Arrow in diesem Lernprogramm verwendete Paket mit der System.Memory in PowerShell 5.1 enthaltenen Assembly in Konflikt steht.
  • Ein Power-BI-Arbeitsbereich in Premium- oder Fabric-Kapazität mit mindestens einem Semantikmodell.
  • Erstellen und Lesen von Berechtigungen für das semantische Modell.
  • Das MicrosoftPowerBIMgmt-Modul für die Authentifizierung. Die Cmdlets verwenden die Erstanbieter-Power BI-Client-App von Microsoft, sodass Sie Ihre eigene App nicht in Microsoft Entra registrieren müssen.
  • Die Apache.Arrow- und Apache.Arrow.Compression-.NET-Bibliotheken zum Deserialisieren der Antwort. Die REST-API zum Ausführen von DAX-Abfragen komprimiert Arrow-Puffer mit LZ4-Frame-Komprimierung, sodass Apache.Arrow.Compression und seine Abhängigkeiten (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port) erforderlich sind. Im nächsten Schritt wird gezeigt, wie sie heruntergeladen werden.
  • Die folgenden Mandanteneinstellungen sind im Power BI Admin-Portal aktiviert:
    • Rest-API für Datasetausführungsabfragen (unter Entwicklereinstellungen).
    • Ermöglichen von XMLA-Endpunkten sowie der Analyse in Excel mit lokalen semantischen Modellen (unter Integrationseinstellungen).

Installieren Sie PowerShell 7.4 oder höher mithilfe von Winget:

winget install --id Microsoft.PowerShell --source winget

Starten Sie nach der Installation die neue Shell mit pwsh. Führen Sie die restlichen Befehle in diesem Tutorial in dieser Sitzung aus.

Installieren Sie das MicrosoftPowerBIMgmt-Modul. Der -Force Schalter bestätigt die Aufforderung zu einem nicht vertrauenswürdigen Repository in der PowerShell-Katalog.

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

Laden Sie die erforderlichen NuGet-Pakete herunter, und extrahieren Sie ihre Assemblys in C:\Tools\Apache.Arrow\. Eine .nupkg-Datei ist ein ZIP-Archiv, daher funktioniert Expand-Archive direkt damit. Die Schleife wählt den höchsten netX.0 Zielordner in jedem Paket aus, sodass die Assemblys kompatibel bleiben, wenn die Pakete neuere Ziele veröffentlichen.

$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 – Authentifizieren

Melden Sie sich interaktiv beim Power BI-Dienst an, und extrahieren Sie dann ein Zugriffstoken. Das Connect-PowerBIServiceAccount Cmdlet erfordert nicht, dass Sie Ihre eigene App in Microsoft Entra registrieren.

Connect-PowerBIServiceAccount -WarningAction SilentlyContinue
$accessToken = (Get-PowerBIAccessToken).Authorization -replace '^Bearer\s+',''

2 – Erstellen einer Anforderung mit mehreren EVALUATE-Anweisungen

Definieren Sie die Arbeitsbereichs- und Semantikmodellziele. Erstellen Sie dann den Request-Body. Die query-Eigenschaft ist eine einzelne Zeichenfolge, die drei durch Leerzeilen getrennte EVALUATE-Anweisungen enthält.

$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 – Senden der Anforderung und Erfassen des rohen Antwortdatenstroms

Senden Sie die POST-Anforderung, und lesen Sie den Antworttext als binären Datenstrom. Verwenden Sie HttpWebRequest anstelle von Invoke-RestMethod, Invoke-PowerBIRestMethod oder Invoke-WebRequest. Die Antwort ist ein binärer Arrow-IPC-Datenstrom. Die PowerShell-Cmdlets auf höherer Ebene interpretieren Antworttexte als Text, wodurch binäre Inhalte beschädigt werden. HttpWebRequest gibt den unmodifizierten Datenstrom zurück.

$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 – Analysieren der Multi-Result-Set-Antwort

Der Antworttext ist die Verkettung eines Apache Arrow IPC-Datenstroms pro EVALUATE Anweisung. PowerShell enthält keinen Arrow-Parser, daher lädt dieser Schritt die .NET-Bibliothek über einen kleinen Inline-C#-Helper, der mit Add-Type hinzugefügt wird, durch Apache.Arrow. Das Beibehalten der Stream-Loop-Logik in C# hält die Aufrufstelle kurz und gibt eine Liste von Ergebnismengen zurück, die Ihr PowerShell-Skript iterieren kann. Die Hilfsfunktion öffnet nach jeder End-of-Stream-Markierung eine neue ArrowStreamReader, sodass dieselbe Schleife eine beliebige Anzahl von Ergebnismengen in der Antwort verarbeiten kann.

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 – Arbeiten mit jedem Resultset

Wandeln Sie jede Ergebnismenge in PSCustomObject Zeilen um. Jetzt können Sie die Zeilen über Where-Object, Group-Object, , Export-Csvoder ein beliebiges anderes PowerShell-Cmdlet weiterleiten.

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

Jede Variable enthält die Zeilen aus der entsprechenden EVALUATE Anweisung, in der Reihenfolge, in der die Anweisungen in der Anforderung angezeigt werden.

Problembehandlung

  • 401 Nicht autorisiert – Das zwischengespeicherte Token ist abgelaufen. Führen Sie Connect-PowerBIServiceAccount erneut aus, um die Anzeige zu aktualisieren, und lesen Sie $accessToken dann erneut von Get-PowerBIAccessToken.
  • MSAL-Warnungen während Connect-PowerBIServiceAccountMicrosoftPowerBIMgmt enthält eine ältere Version von MSAL.NET, die interne Tracemeldungen (zum Beispiel SetAuthorityUri, TryNormalizeRealm, MsaDeviceOperationProvider is not available) mit dem Schweregrad „Warnung“ ausgibt. Sie sind sicher zu ignorieren, solange das Cmdlet den Environment / TenantId / UserName Block druckt. Um sie zu unterdrücken, übergeben Sie -WarningAction SilentlyContinue.
  • HTTP 200 mit einem Fehlerergebnissatz – Die HTTP-Anforderung war erfolgreich, der Arrow-Datenstrom weist jedoch einen Fehler auf. Überprüfen Sie die Schemametadaten für IsError=true, und lesen Sie FaultCode und FaultString. Ausführliche Informationen finden Sie unter Bewährte Methoden für die REST-API "DAX-Abfragen ausführen".
  • Invoke-RestMethod gibt verstümmelten Text zurück — Verwenden Sie Invoke-RestMethod, Invoke-PowerBIRestMethod oder Invoke-WebRequest nicht mit dieser API. Die Antwort ist binär; verwenden Sie HttpWebRequest, wie in Schritt 3 gezeigt.
  • Add-Type kann nicht geladen werden Apache.Arrow.dll — Unter Windows PowerShell 5.1 steht das Paket Apache.Arrow in Konflikt mit der systemseitig enthaltenen System.Memory-Assembly. Verwenden Sie PowerShell 7.4 oder höher.
  • Keine oder weniger Ergebnismengen zurückgegeben als EVALUATE-Anweisungen – Vergewissern Sie sich, dass jede EVALUATE-Anweisung für sich genommen syntaktisch gültig ist. Ein einzelnes ungültiges EVALUATE führt dazu, dass die API einen Fehler statt einer teilweisen Antwort mit mehreren Ergebnismengen zurückgibt.