이 자습서에서는 PowerShell을 사용하여 여러 문이 포함된 EVALUATE 요청을 제출한 다음, 다중 결과 집합 Apache Arrow 응답을 구문 분석합니다. 이 패턴을 사용하면 PowerShell 자동화 스크립트에서 한 번의 왕복으로 여러 관련 결과 집합을 검색할 수 있습니다.
하나의 요청으로 여러 EVALUATE 문을 제출하는 이유
DAX 쿼리 실행 API는 여러 query 문을 포함할 수 있는 단일 EVALUATE 문자열을 허용합니다. 각 문은 자체 결과 집합을 반환하며, 응답 본문은 선언 순서에 따라 각 EVALUATE 문마다 하나씩 있는 Arrow IPC 스트림을 연결한 것입니다. 관련 쿼리를 함께 제출하면 추가 Microsoft Entra 토큰 유효성 검사 및 DAX 엔진 초기화를 포함하여 별도의 HTTP 호출의 요청당 오버헤드를 방지할 수 있습니다. 한 요청에서 여러 EVALUATE 문을 보내면 요청 제한의 영향을 완화하는 데도 도움이 될 수 있습니다. Power BI 의미 체계 모델 쿼리 작업을 위해 호출자를 사용자당 분당 120개의 쿼리 요청으로 제한합니다.
당신이 구축한 것
하나의 PowerShell 스크립트에서 다음을 수행합니다.
- Microsoft Entra 액세스 토큰을 획득합니다.
-
query에 세 개의EVALUATE구문이 포함된 요청 본문을 빌드합니다. - 요청을 보내고 원시 화살표 IPC 응답 스트림을 캡처합니다.
- 응답을 각
EVALUATE문마다 하나의 결과 집합이 되도록 구문 분석합니다. - 각 결과 집합을 PowerShell 개체로 표시합니다.
사전 요구 사항
- PowerShell 7.4 이상. 이 자습서에서 사용하는
Apache.Arrow패키지가 PowerShell 5.1에 포함된System.Memory어셈블리와 충돌하므로 Windows PowerShell 5.1은 지원되지 않습니다. - 하나 이상의 의미 체계 모델이 있는 프리미엄 또는 Fabric 용량의 Power BI 작업 영역입니다.
- 의미 체계 모델에 대한 빌드 및 읽기 권한입니다.
- 인증을 위한 MicrosoftPowerBIMgmt 모듈입니다. cmdlet은 Microsoft 자사 Power BI 클라이언트 앱을 사용하므로 Microsoft Entra 자체 앱을 등록할 필요가 없습니다.
- 응답을 역직렬화하는 데 사용하는 Apache.Arrow 및 Apache.Arrow.Compression .NET 라이브러리. DAX 쿼리 실행 REST API는 Apache Arrow 버퍼를 LZ4 프레임 압축으로 압축하므로
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을 시작합니다. 해당 세션에서 이 자습서의 나머지 명령을 실행합니다.
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. 응답은 이진 Apache Arrow IPC 스트림입니다. 상위 수준 PowerShell cmdlet은 응답 본문을 텍스트로 해석하여 이진 콘텐츠를 손상합니다.
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 문의 행을 요청에 나타나는 순서대로 보유합니다.
문제 해결
-
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 요청이 성공했지만 화살표 스트림에 오류가 발생합니다.
IsError=true에 대한 스키마 메타데이터를 검사하고FaultCode및FaultString를 읽습니다. 자세한 내용은 DAX 쿼리 실행 REST API에 대한 모범 사례를 참조하세요. -
Invoke-RestMethod는 깨진 텍스트를 반환합니다 — 이 API와 함께Invoke-RestMethod,Invoke-PowerBIRestMethod또는Invoke-WebRequest를 사용하지 마세요. 응답은 이진입니다. 3단계와 같이 사용합니다HttpWebRequest. -
Add-Type을(를) 로드하지 못함Apache.Arrow.dll— Windows PowerShell 5.1에서는Apache.Arrow패키지가 기본 제공System.Memory어셈블리와 충돌합니다. PowerShell 7.4 이상을 사용합니다. -
문보다
EVALUATE반환되는 결과 집합이 없거나 더 적 습니다. 각EVALUATE문이 자체적으로 구문적으로 유효한지 확인합니다. 단일 잘못된EVALUATE경우 API가 부분 다중 결과 집합 응답이 아닌 오류를 반환합니다.
관련 콘텐츠
- DAX 쿼리 실행 API 이해
- DAX 쿼리 실행 REST API 시작
Tutorial: DAX 쿼리 실행 REST API - 자습서: Fabric Notebook에서 대용량 Python 추출
- DAX 쿼리 실행 REST API에 대한 모범 사례
- DAX 쿼리 REST API 참조 실행