Självstudie: Köra flera EVALUATE-instruktioner med PowerShell

I den här självstudien använder du PowerShell för att skicka en enda REST API-begäran till Execute DAX Queries som innehåller flera EVALUATE satser och sedan tolka Apache Arrow-svaret med flera resultatuppsättningar. Med det här mönstret kan du hämta flera relaterade resultatuppsättningar i en tur och retur från ett PowerShell-automatiseringsskript.

Diagram som visar en HTTP POST-begäran som innehåller tre EVALUATE-instruktioner i frågetexten och Pil-IPC-svaret som innehåller tre resultatuppsättningar i samma ordning.

Varför skicka in flera EVALUATE-satser i en och samma begäran

API:et för att köra DAX-frågor accepterar en enda query-sträng som kan innehålla flera EVALUATE-satser. Varje sats returnerar sin egen resultatuppsättning, och svarsinnehållet är en sammanfogning av en Arrow IPC-ström per EVALUATE sats i deklarationsordning. Om du skickar relaterade frågor tillsammans undviker du kostnaden per begäran för separata HTTP-anrop, inklusive extra Microsoft Entra tokenverifiering och DAX-motorinitiering. Att skicka flera EVALUATE instruktioner i en begäran kan också bidra till att minska effekten av begränsning av begäranden. Power BI begränsar anropare till 120 frågebegäranden per minut per användare för semantiska modellfrågeåtgärder.

Det här skapar du

I ett PowerShell-skript:

  1. Hämta en Microsoft Entra åtkomsttoken.
  2. Skapa en begärandetext vars query innehåller tre EVALUATE instruktioner.
  3. Skicka begäran och fånga den råa Arrow IPC-svarsströmmen.
  4. Tolka svaret i en resultatuppsättning per EVALUATE-sats.
  5. Visa varje resultatuppsättning som PowerShell-objekt.

Förutsättningar

  • PowerShell 7.4 eller senare. Windows PowerShell 5.1 stöds inte eftersom Apache.Arrow paketet som används i den här självstudien står i konflikt med System.Memory sammansättningen som ingår i PowerShell 5.1.
  • En Power BI-arbetsyta i Premium- eller Fabric-kapacitet med minst en semantisk modell.
  • Skapa och läsa behörigheter för semantikmodellen.
  • MicrosoftPowerBIMgmt-modulen för autentisering. Cmdletarna använder Microsofts förstaparts Power BI-klientapp, så du behöver inte registrera din egen app i Microsoft Entra.
  • Apache.Arrow- och Apache.Arrow.Compression .NET-biblioteken för att deserialisera svaret. REST-API:et för att köra DAX-frågor komprimerar Arrow-buffertar med LZ4-framekomprimering, så Apache.Arrow.Compression och dess beroenden (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port) krävs. Nästa steg visar hur du laddar ned dem.
  • Följande klientinställningar aktiverade i administratörsportalen för Power BI:
    • REST-API för att köra frågor mot datauppsättningar (under Utvecklarinställningar).
    • Tillåt XMLA-slutpunkter och Analysera i Excel med lokala semantiska modeller (under Integreringsinställningar).

Installera PowerShell 7.4 eller senare med winget:

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

Efter installationen startar du det nya gränssnittet med pwsh. Kör de återstående kommandona i den här självstudien från den sessionen.

Installera modulen MicrosoftPowerBIMgmt. Flaggan -Force godtar frågan om ej betrodd lagringsplats i PowerShell Gallery.

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

Ladda ned nödvändiga NuGet-paket och extrahera deras sammansättningar till C:\Tools\Apache.Arrow\. En .nupkg fil är ett ZIP-arkiv, så Expand-Archive fungerar direkt på den. Loopen väljer den högsta netX.0 målmappen i varje paket så att sammansättningarna förblir kompatibla när paketen publicerar nyare mål.

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

Logga in på služba Power BI interaktivt och extrahera sedan en åtkomsttoken. Cmdleten Connect-PowerBIServiceAccount kräver inte att du registrerar din egen app i Microsoft Entra.

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

2 – Skapa en begäran med flera EVALUATE-satser

Definiera mål för arbetsyta och semantisk modell. Skapa sedan begärandetexten. Egenskapen query är en enskild sträng som innehåller tre EVALUATE instruktioner avgränsade med tomma rader.

$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 – Skicka begäran och samla in råsvarsströmmen

Skicka POST-begäran och läs svarstexten som en binär dataström. Använd HttpWebRequest i stället Invoke-RestMethodför , Invoke-PowerBIRestMethodeller Invoke-WebRequest. Svaret är en binär Arrow IPC-ström. PowerShell-cmdletarna på högre nivå tolkar svarskroppar som text, vilket skadar binärt innehåll. HttpWebRequest returnerar råströmmen oförändrad.

$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 – Parsa svaret för flera resultatuppsättningar

Svarstexten är sammanlänkningen av en Apache Arrow IPC-ström per EVALUATE instruktion. PowerShell innehåller inte någon Arrow-parser, så i det här steget laddas .NET-biblioteket Apache.Arrow in via en liten infogad C#-hjälpklass som lagts till med Add-Type. Om du behåller stream-loop-logiken i C# blir anropswebbplatsen kort och returnerar en lista över resultatuppsättningar som PowerShell-skriptet kan iterera. Hjälpfunktionen öppnar en ny ArrowStreamReader efter varje markör för slut på strömmen, så samma loop hanterar hur många resultatmängder som helst i svaret.

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 – Arbeta med varje resultatuppsättning

Konvertera varje resultatuppsättning till PSCustomObject rader. Nu kan du skicka raderna via Where-Object, Group-Object, Export-Csveller någon annan 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

Varje variabel innehåller raderna från motsvarande EVALUATE instruktion, i den ordning som uttrycken visas i begäran.

Troubleshooting

  • 401 Obehörig – den cachelagrade token har upphört att gälla. Kör Connect-PowerBIServiceAccount igen för att uppdatera den och läs $accessToken sedan igen från Get-PowerBIAccessToken.
  • MSAL-varningar under Connect-PowerBIServiceAccountMicrosoftPowerBIMgmt innehåller en äldre version av MSAL.NET som avger interna spårningsmeddelanden (till exempel SetAuthorityUri, TryNormalizeRealm, MsaDeviceOperationProvider is not available) på varningsnivå. De är säkra att ignorera så länge cmdleten skriver ut Environment / TenantId / UserName blocket. Om du vill förhindra dem skickar du -WarningAction SilentlyContinue.
  • HTTP 200 med en felresultatuppsättning – HTTP-begäran lyckades, men Pilströmmen har ett fel. Granska schemametadata för IsError=trueoch läs FaultCode och FaultString. Mer information finns i Bästa praxis för REST-API:t Köra DAX-frågor.
  • Invoke-RestMethod returnerar förvrängd text – Använd Invoke-RestMethodinte , Invoke-PowerBIRestMethodeller Invoke-WebRequest med det här API:et. Svaret är binärt. använd HttpWebRequest som du ser i steg 3.
  • Add-Type kan inte laddas Apache.Arrow.dll — I Windows PowerShell 5.1 står Apache.Arrow-paketet i konflikt med den inbyggda System.Memory-sammansättningen. Använd PowerShell 7.4 eller senare.
  • Inga eller färre resultatuppsättningar returnerade än EVALUATE -instruktioner – Bekräfta att varje EVALUATE instruktion är syntaktiskt giltig på egen hand. En enda ogiltig EVALUATE gör att API:et returnerar ett fel i stället för ett partiellt svar med flera resultatuppsättningar.