Tutorial: Ejecución de varias instrucciones EVALUATE con PowerShell

En este tutorial, usará PowerShell para enviar una única solicitud a la API REST Execute DAX Queries que contiene varias EVALUATE sentencias y, a continuación, analizar la respuesta Apache Arrow de múltiples conjuntos de resultados. Este patrón le permite recuperar varios conjuntos de resultados relacionados en un recorrido de ida y vuelta desde un script de automatización de PowerShell.

Diagrama que muestra una solicitud HTTP POST que contiene tres instrucciones EVALUATE en su cuerpo de consulta y la respuesta IPC de flecha que contiene tres conjuntos de resultados en el mismo orden.

¿Por qué enviar varias sentencias EVALUATE en una sola solicitud?

La API Execute DAX Queries acepta una sola query cadena que puede contener varias EVALUATE instrucciones. Cada sentencia devuelve su propio conjunto de resultados, y el cuerpo de la respuesta es la concatenación de un flujo Arrow IPC por cada sentencia EVALUATE, según el orden de declaración. Enviar consultas relacionadas de forma conjunta evita la sobrecarga asociada a cada solicitud de realizar llamadas HTTP separadas, incluida la validación adicional de tokens de Microsoft Entra y la inicialización del motor DAX. Enviar varias EVALUATE sentencias en una sola solicitud también puede ayudar a mitigar el impacto de la limitación del número de solicitudes. Power BI limita las llamadas a 120 solicitudes de consulta por minuto por usuario para las operaciones de consulta de modelo semántico.

Lo que construyes

En un script de PowerShell, puede:

  1. Adquiera un token de acceso de Microsoft Entra.
  2. Cree un cuerpo de solicitud cuyo query contenga tres sentencias EVALUATE.
  3. Envíe la solicitud y capture el flujo en bruto de respuesta IPC de Arrow.
  4. Analice la respuesta en un conjunto de resultados por cada instrucción EVALUATE.
  5. Muestra cada conjunto de resultados como objetos de PowerShell.

Prerequisites

  • PowerShell 7.4 o posterior. Windows PowerShell 5.1 no se admite porque el paquete Apache.Arrow usado en este tutorial entra en conflicto con el ensamblado System.Memory incluido en PowerShell 5.1.
  • Un área de trabajo de Power BI en la capacidad Premium o Fabric con al menos un modelo semántico.
  • Permisos de compilación y lectura en el modelo semántico.
  • El módulo MicrosoftPowerBIMgmt para la autenticación. Los cmdlets usan la aplicación cliente de Power BI propia de Microsoft, por lo que no necesita registrar una aplicación propia en Microsoft Entra.
  • Las bibliotecas Apache.Arrow y Apache.Arrow.Compression .NET para deserializar la respuesta. La API REST de Execute DAX Queries comprime los búferes de Arrow con compresión de tramas LZ4, por lo que Apache.Arrow.Compression y sus dependencias (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port) son necesarios. En el paso siguiente se muestra cómo descargarlos.
  • Las siguientes opciones de configuración del inquilino habilitadas en el portal de administración de Power BI:
    • API REST para ejecutar consultas de conjuntos de datos (en Configuración del desarrollador).
    • Permitir endpoints XMLA y Analizar en Excel con modelos semánticos locales (dentro de configuración de integración).

Instale PowerShell 7.4 o posterior mediante winget:

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

Después de la instalación, inicie el nuevo shell con pwsh. Ejecute los comandos restantes de este tutorial desde esa sesión.

Instale el módulo MicrosoftPowerBIMgmt. El indicador -Force acepta el aviso de repositorio no confiable de Galería de PowerShell.

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

Descargue los paquetes NuGet necesarios y extraiga sus ensamblados en C:\Tools\Apache.Arrow\. Un .nupkg archivo es un archivo ZIP, por lo que Expand-Archive funciona directamente en él. El bucle selecciona la carpeta de destino más alta netX.0 de cada paquete para que los ensamblados permanezcan compatibles a medida que los paquetes publiquen destinos más recientes.

$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 - Autenticación

Inicie sesión en el servicio Power BI de forma interactiva y, a continuación, extraiga un token de acceso. El Connect-PowerBIServiceAccount cmdlet no requiere que registre su propia aplicación en Microsoft Entra.

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

2 - Crear una solicitud con varias sentencias EVALUATE

Defina el área de trabajo y los destinos del modelo semántico. A continuación, compile el cuerpo de la solicitud. La query propiedad es una sola cadena que contiene tres EVALUATE instrucciones separadas por líneas en blanco.

$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 - Enviar la solicitud y capturar el flujo de respuesta sin procesar

Envíe la solicitud POST y lea el cuerpo de la respuesta como una secuencia binaria. Use HttpWebRequest en lugar de Invoke-RestMethod, Invoke-PowerBIRestMethodo Invoke-WebRequest. La respuesta es una secuencia IPC de Arrow binaria. Los cmdlets de PowerShell de nivel superior interpretan los cuerpos de respuesta como texto, lo que daña el contenido binario. HttpWebRequest devuelve el flujo en bruto sin modificar.

$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 - Análisis de la respuesta de varios conjuntos de resultados

El cuerpo de la respuesta es la concatenación de un flujo IPC de Apache Arrow por cada sentencia EVALUATE. PowerShell no incluye un analizador de Arrow, por lo que este paso carga la biblioteca .NET Apache.Arrow a través de una pequeña utilidad en línea en C# añadida con Add-Type. Mantener la lógica de bucle de flujo en C# mantiene el sitio de llamada corto y devuelve una lista de conjuntos de resultados que el script de PowerShell puede iterar. El asistente abre un nuevo ArrowStreamReader después de cada marcador de fin de flujo, por lo que el mismo bucle controla cualquier número de conjuntos de resultados en la respuesta.

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 - Trabajar con cada conjunto de resultados

Convierta cada conjunto de resultados en PSCustomObject filas. Ahora puede canalizar las filas mediante Where-Object, Group-Object, Export-Csv o cualquier otro cmdlet de 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

Cada variable contiene las filas de la instrucción correspondiente EVALUATE , en el orden en que las instrucciones aparecen en la solicitud.

Solución de problemas

  • 401 No autorizado : el token almacenado en caché expiró. Vuelva a ejecutar Connect-PowerBIServiceAccount para actualizarlo y, a continuación, vuelva a leer $accessToken desde Get-PowerBIAccessToken.
  • Advertencias de MSAL durante Connect-PowerBIServiceAccountMicrosoftPowerBIMgmt incluye una versión anterior de MSAL.NET que emite mensajes internos de seguimiento (por ejemplo, SetAuthorityUri, TryNormalizeRealm, MsaDeviceOperationProvider is not available) con nivel de advertencia. Se pueden omitir sin problema siempre que el cmdlet imprima el bloque Environment / TenantId / UserName. Para suprimirlos, pase -WarningAction SilentlyContinue.
  • HTTP 200 con un conjunto de resultados de error — La solicitud HTTP se completó correctamente, pero el flujo Arrow contiene un error. Inspeccione los metadatos del esquema para IsError=truey lea FaultCode y FaultString. Para más información, consulte Prácticas recomendadas para la API REST Execute DAX Queries.
  • Invoke-RestMethod devuelve texto desagrabado : no use Invoke-RestMethod, Invoke-PowerBIRestMethodo Invoke-WebRequest con esta API. La respuesta es binaria; use HttpWebRequest como se muestra en el paso 3.
  • Add-Type no se carga Apache.Arrow.dll — En Windows PowerShell 5.1, el paquete Apache.Arrow entra en conflicto con el ensamblado System.Memory incluido. Use PowerShell 7.4 o una versión posterior.
  • Se devuelven menos conjuntos de resultados de los correspondientes a las EVALUATE instrucciones — Confirme que cada EVALUATE instrucción es sintácticamente válida por sí sola. Un único EVALUATE no válido hace que la API devuelva un error en lugar de una respuesta parcial con varios conjuntos de resultados.