Руководство. Выполнение нескольких инструкций EVALUATE с помощью PowerShell

В этом руководстве вы используете PowerShell, чтобы отправить один запрос к Execute DAX Queries REST API, содержащий несколько операторов EVALUATE, а затем проанализировать ответ Apache Arrow с несколькими наборами результатов. Этот шаблон позволяет получить несколько связанных результирующих наборов в одном цикле из скрипта автоматизации PowerShell.

Схема, на которой показан один HTTP-запрос POST, содержащий три инструкции EVALUATE в тексте запроса, и ответ стрелки IPC, содержащий три результирующих набора в одном порядке.

Зачем отправлять несколько инструкций EVALUATE в одном запросе

API выполнения запросов DAX принимает одну query строку, которая может содержать несколько EVALUATE инструкций. Каждая инструкция возвращает собственный результирующий набор, а текст отклика — объединение одного потока IPC со стрелками на EVALUATE инструкцию в порядке объявления. Совместная отправка связанных запросов позволяет избежать дополнительных накладных расходов на каждый отдельный HTTP-вызов, включая дополнительную проверку токенов Microsoft Entra и инициализацию механизма DAX. Отправка нескольких EVALUATE инструкций в одном запросе также может помочь снизить влияние регулирования запросов. Power BI ограничивает количество запросов к семантической модели 120 запросами в минуту для каждого пользователя.

Что вы строите

В одном сценарии PowerShell вы:

  1. Получите маркер доступа Microsoft Entra.
  2. Создайте тело запроса, в котором query содержит три инструкции EVALUATE.
  3. Отправьте запрос и сохраните необработанный поток ответа в формате Arrow IPC.
  4. Разберите ответ на отдельные результирующие наборы, по одному для каждого оператора EVALUATE.
  5. Отобразите каждый набор результатов в виде объектов PowerShell.

Необходимые условия

  • PowerShell 7.4 или более поздней версии. Windows PowerShell 5.1 не поддерживается, поскольку пакет Apache.Arrow, используемый в этом руководстве, конфликтует со сборкой System.Memory, входящей в состав PowerShell 5.1.
  • Рабочая область Power BI в емкости Premium или Fabric с как минимум одной семантической моделью.
  • Разрешения на сборку и чтение для семантической модели.
  • Модуль MicrosoftPowerBIMgmt для проверки подлинности. Командлеты используют собственное клиентское приложение Microsoft Power BI, поэтому вам не нужно регистрировать собственное приложение в Microsoft Entra.
  • Библиотеки .NET Apache.Arrow и Apache.Arrow.Compression для десериализации ответа. REST API выполнения запросов DAX сжимает буферы Arrow с использованием сжатия LZ4 в формате frame, поэтому требуются Apache.Arrow.Compression и его зависимости (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port). На следующем шаге показано, как скачать их.
  • В портале администрирования Power BI включены следующие параметры арендатора:
    • REST API выполнения запросов набора данных (в разделе "Параметры разработчика").
    • Разрешить интерфейсы XMLA и анализ в Excel с использованием локальных семантических моделей (в разделе Параметры интеграции).

Установите PowerShell 7.4 или более поздней версии с помощью winget:

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

После установки запустите новую оболочку с помощью pwsh. Выполните остальные команды из этого руководства в том же сеансе.

Установите модуль 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 не требует, чтобы вы регистрировали собственное приложение в 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 - Разбор ответа с несколькими наборами результатов

Тело ответа представляет собой конкатенацию потоков Apache Arrow IPC, по одному потоку на каждый оператор EVALUATE. PowerShell не включает парсер Arrow, поэтому на этом шаге загружается библиотека .NET Apache.Arrow с помощью небольшого встроенного вспомогательного кода C#, добавленного через Add-Type. Сохраняя логику потокового цикла в 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.

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 снова, чтобы обновить его, а затем снова прочитайте $accessToken из Get-PowerBIAccessToken.
  • Предупреждения MSAL во время Connect-PowerBIServiceAccount — MicrosoftPowerBIMgmt объединяет более старый MSAL.NET, который выдает внутренние сообщения трассировки (например, SetAuthorityUri, , TryNormalizeRealmMsaDeviceOperationProvider is not available) при серьезности предупреждений. Их можно игнорировать, если командлет выводит блок Environment / TenantId / UserName. Чтобы отключить их, передайте -WarningAction SilentlyContinue.
  • HTTP 200 с набором результатов, содержащим ошибку — HTTP-запрос выполнен успешно, но поток Arrow содержит ошибку. Проверьте метаданные схемы для IsError=true, а также прочитайте FaultCode и FaultString. Дополнительные сведения см. в рекомендациях по REST API выполнения запросов DAX.
  • Invoke-RestMethod возвращает искажённый текст — Не используйте Invoke-RestMethod, Invoke-PowerBIRestMethod или Invoke-WebRequest с этим API. Ответ является двоичным; используйте HttpWebRequest , как показано на шаге 3.
  • Add-Type не загружается Apache.Arrow.dll — в Windows PowerShell 5.1 пакет Apache.Arrow конфликтует со встроенной сборкой System.Memory. Используйте PowerShell 7.4 или более поздней версии.
  • Возвращено меньше наборов результатов, чем операторов EVALUATE — убедитесь, что каждый оператор EVALUATE синтаксически корректен сам по себе. Одно недопустимое значение EVALUATE приводит к тому, что API возвращает ошибку, а не частичный ответ с несколькими наборами результатов.