ARCHININGEN TECH BLOG
← 記事一覧へ戻る

Power BI executeQueries でデータを取り出す — API の制約とページング設計

CONTENTS

executeQueries で無音のデータ欠落が起きる 3 パターンと対策を示した図。CASE 1 は上限超過でも HTTP 200 が返り、応答 body の中に error が埋まったまま途中の行までしか書き込まれない切り詰め — 対策は error を top-level / results / tables の 3 箇所で検査すること。CASE 2 は 100k 行のページが続く途中で 62k 行の端数ページが返り、それを終端と誤認して以降のページを破棄してしまう — 対策は終端判定を 0 行のみとし、offset を実際に取得できた行数で進めること。CASE 3 はページ 1 に A・B・C の 3 列があるのにページ 2 では null 落ちで B 列が消え、列集合がページ間でぶれる — 対策は includeNulls: true を明示すること。図の下部に「欠落を防げない仕様なら、必ず検出できるループにする」というキャプションがある。

前回の記事では、Managed Identity によるシークレットレス構成で Power BI のレポートをアプリに埋め込むところまでを組みました。末尾で予告した「REST API でセマンティックモデルから生データを取り出す話」の回収が本記事です。埋め込みが「見せる」ための API だとすれば、今回は「取り出す」ための API の話になります。

レポートを埋め込むと、次に来る要件はだいたい決まっています。「この画面の元データを CSV でください」です。Power BI REST API の executeQueries は、セマンティックモデルに DAX クエリを POST して結果を JSON で受け取れるエンドポイントで、この要件に対する最小依存の答えになります。

ただしこの API、制約がかなり多く、しかも最大の罠は失敗が失敗の顔をしていないことです。取得上限を超えると、エラーステータスではなく HTTP 200 が返り、結果は無言で切り詰められますres.ok だけを見る素朴な実装は、欠損したデータを「正常」として出力します。データエクスポートという用途でこれは致命的です。

本記事では、筆者が実環境で executeQueries を運用して確認した制約を公式仕様と突き合わせ、データ欠落を仕組みで検出できるページング設計を Node.js で組みます。認証は前回の Managed Identity 構成をそのまま流用します。取り出した行を CSV/ZIP としてストリーミング配信する実装 (backpressure や archiver の地雷) は次回に回し、RLS 付きモデルも対象外とします — そもそもサービスプリンシパルでは RLS 付きモデルに executeQueries を使えません (制約の章で触れます)。

executeQueries とは — 埋め込みの隣にある生データ経路

executeQueriesPOST /v1.0/myorg/groups/{workspaceId}/datasets/{datasetId}/executeQueries に DAX クエリを送ると、結果の行が JSON で返る API です。まず動かして感触を掴みます。

BASH
# 疎通確認: テーブルの先頭 3 行を DAX で取得する
TOKEN=$(az account get-access-token \
  --resource https://analysis.windows.net/powerbi/api \
  --query accessToken --output tsv)
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"queries\":[{\"query\":\"EVALUATE TOPN(3, '受注明細')\"}]}" \
  "https://api.powerbi.com/v1.0/myorg/groups/<workspaceId>/datasets/<datasetId>/executeQueries"

results[0].tables[0].rows に行が返ってくれば疎通は成功です。前提は 3 つあります。

前提 内容
テナント設定 「Dataset Execute Queries REST API」の有効化 (前回のサービスプリンシパル許可とは別の設定。管理ポータルの検索で探すのが早いです)
データセット権限 Read に加えて Build 権限が必要
サービスプリンシパル利用時 前回のテナント設定 + ワークスペースメンバーに加え、RLS 付き・SSO 有効のモデルでは利用不可

生データを取り出す手段は他にもあるので、位置づけを整理しておきます。

手段 形式 前提 向き
executeQueries JSON Build 権限のみ 依存最小の汎用エクスポート
executeDaxQueries (Arrow 版) Arrow IPC Premium/Fabric 容量 + XMLA 有効化 + Arrow ライブラリ。プレビュー 大規模・高精度
XMLA エンドポイント TOM/ADOMD Premium/Fabric 容量 + 専用クライアントライブラリ 管理・大規模分析
レポートのエクスポート API PDF/PPTX 等 容量必須 見た目の再現

筆者が executeQueries を採用したのは、HTTP + JSON だけで完結し、追加の容量要件も専用ライブラリも不要だからです。Node.js のアプリからは fetch 1 本で呼べます。その代わり、次章の制約と正面から付き合うことになります。

制約 — 公式仕様を逐語で確認する

executeQueries の制限は公式リファレンスに明記されています。まず数字から。

制限
行数 1 クエリあたり最大 100,000 行
値の数 1 クエリあたり最大 1,000,000 値 (行数と先に達した方)
応答サイズ 1 クエリあたり最大 15MB
クエリ構造 1 コール 1 クエリ、1 クエリの結果は 1 テーブル
レート ユーザーあたり 120 リクエスト/分

「値 (values)」の上限は行数より先に効くことがあります。列数を N とすると実効行数の目安は 1,000,000 ÷ N です。

列数 実効的な最大行数
5 列 100,000 行 (行上限が先)
10 列 100,000 行
20 列 50,000 行
100 列 10,000 行

上限超過は「HTTP 200 + body のエラー」で返る

ここが本記事で一番伝えたい仕様です。公式リファレンスには、15MB を超えた場合「処理中の行は書き切られるが、それ以降の行は書き込まれない」こと、そしてその際「成功の HTTP ステータスコード (200) と、応答内のエラーが返る」ことが明記されています。切り詰めはバグではなく仕様です。

さらに応答スキーマを見ると、error フィールドは top-level・results[i]results[i].tables[i] の 3 箇所に定義されています。どこに現れるかはケースによるため、1 箇所だけ見る実装ではすり抜けます。

INFO 関数が使えない — テーブル一覧を API で列挙できない

executeQueries がサポートするのは DAX クエリのみで、INFO 関数・MDX・DMV クエリは非サポートです。INFO.TABLES() を投げると 400 が返り、テナント設定でも緩和できません。つまり「モデルにどんなテーブルがあるか」をこの API 自身に聞く手段がありません。

これは設計に波及します。筆者はエクスポート対象のテーブル名を管理画面で手入力する (1 行 1 テーブル) 設計に落としました。自動列挙にこだわると、代替は 2 つとも前提が重くなります。

代替 前提 見送った理由
executeDaxQueries (Arrow 版) は INFO 対応 Premium/Fabric 容量 + XMLA + Arrow 依存。プレビュー 依存とプレビュー status
Scanner API (メタデータスキャン) テナント管理者権限 アプリに管理者級の権限を持たせたくない

テーブル名の手入力は一見泥臭いですが、「エクスポートしてよいテーブルを明示的に宣言する」という許可リストとしても機能します。

ページングは公式に「Not supported」

100,000 行を超えるテーブルは複数回に分けて取るしかありませんが、公式ドキュメントの新旧 API 比較表には Pagination: Not supported と明記されています。DAX の TOPNSKIP 関数を使えば「動く」ものの、TOPNSKIP は行の順序を保証しません。順序が保証されないままページを切ると、原理的には重複や欠落が起こりえます。

つまりこの API で大きなテーブルを取り出すことは、公式には支えられていない領域に足を踏み入れることです。それでも使うなら、「欠落が起きたら必ず検出できる」ことをループの不変条件として組み込むべきです。次章がその設計です。

設計 — 「無音のデータ欠落」を仕組みで塞ぐページング

冒頭の図に挙げたとおり、無音の欠落は 3 つの経路で起きます: (1) 200 + body error の切り詰め、(2) 端数ページの終端誤認、(3) null 落ちによる列集合のぶれ。以下の設計はこの 3 つをすべて検出可能にします。

DAX は TOPNSKIP で切り出す

CODE
EVALUATE TOPNSKIP(100000, <offset>, '受注明細')

第 1 引数が取得行数、第 2 引数が読み飛ばし行数です。テーブル名はシングルクォートで囲むため、日本語のテーブル名・列名もそのまま通ります (実環境で確認済み)。

終端は「0 行」だけ、offset は実取得行数で進める

ページングループの本体です。不変条件は 2 つ — 終端判定は「0 行が返ったとき」だけ、offset は「実際に取得できた行数」でだけ進める。

JAVASCRIPT
// 終端は 0 行のみ。offset は実際に取得できた行数でだけ進める
let offset = 0;
const all = [];
while (true) {
  const rows = await runQuery(
    `EVALUATE TOPNSKIP(${PAGE_SIZE}, ${offset}, '${tableName}')`);
  if (rows.length === 0) break;
  all.push(...rows);
  offset += rows.length;
}

直感的には rows.length < PAGE_SIZE を終端とみなしたくなりますが、これが図の CASE 2 です。15MB 制限による切り詰めはページの途中で端数の行数を返すため、端数 = テーブルの終わりと誤認すると、残りの全ページを黙って捨てることになります。同じ理由で offset += PAGE_SIZE も危険です。切り詰めで 62,000 行しか返っていないのに offset を 100,000 進めれば、差分の 38,000 行が欠落します。実取得行数で進めていれば、切り詰められたページの続きは次のループが自然に回収します。

エラーは 3 箇所すべて検査する

JAVASCRIPT
// 200 応答でも error は 3 箇所に現れうる — すべて辿る
function pickError(body) {
  return body?.error
    ?? body?.results?.[0]?.error
    ?? body?.results?.[0]?.tables?.[0]?.error
    ?? null;
}

runQuery の中でこの検査を挟み、error があり行が 0 行なら実行時エラーとして throwerror があっても行が返っていれば「切り詰め」として warn ログを出して行を使います。切り詰め自体は前節の offset 設計が回収してくれるので処理は続行できますが、warn が出続けるならページサイズか列数を見直すサインです (運用の章で触れます)。

includeNulls: true を明示する

JSON
{
  "queries": [{ "query": "EVALUATE TOPNSKIP(100000, 0, '受注明細')" }],
  "serializerSettings": { "includeNulls": true }
}

includeNulls の既定値は false で、このとき null のセルはキーごと応答から消えます。あるページでは全行 null だった列が、次のページでは値を持って現れる — 図の CASE 3 のとおり、ページ間で列集合がぶれるということです。CSV 化するときにヘッダを 1 ページ目で確定できなくなり、列ズレの原因になります。リクエスト body で必ず true を明示します。

リトライは一時的な失敗だけ、1 回だけ

失敗の種類によって、再送に意味があるかは明確に分かれます。

ステータス 扱い
429 / 5xx / ネットワークエラー / タイムアウト 1 回だけ再送
400 (DAX 構文エラー等) / 401 / 403 / 404 即 throw

400 系は再送しても同じ結果が返るだけで、ユーザーあたり 120 リクエスト/分の枠を無駄に消費します。また再送回数を 1 回に留めているのは、この処理の先がストリーミング応答だからです。応答を返し始めた後のリトライは途中まで送ったデータの掃除ができず、粘るほど応答時間が延びるだけ、というのが実環境での結論です。

躓きポイント

タイムアウトは body を読み切るまで張り続ける

executeQueries の応答は最大 15MB あります。fetch にタイムアウトを付けるとき、「ヘッダが到着したらタイマー解除」という素朴な実装だと、本文の読み出しが無防備になります。接続が本文の途中でストールすると res.json() が永久に pending し、インフラ側の request timeout までソケットを掴み続けます。

JAVASCRIPT
// AbortSignal は body の読み出し完了まで張り続ける
async function fetchWithTimeout(url, init, timeoutMs, consume) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, { ...init, signal: controller.signal });
    return await consume(res); // body の読み出しも同じタイマーの内側
  } finally {
    clearTimeout(timer);
  }
}

ポイントは、呼び出し側から consume コールバック (例: (res) => res.json()) を受け取り、本文の読み出しまで同じタイマーの内側で行うことです。筆者はメタ情報の取得に 15 秒、executeQueries に 60 秒を割り当てています。

順序無保証の残存リスクをどう受容するか

前章の設計で「切り詰め・終端誤認・列ぶれ」は検出できますが、TOPNSKIP の順序無保証だけは技術で塞げません。数百万行を複数リクエストに分割する以上、抽出の途中でセマンティックモデルのリフレッシュが走ったり、容量からモデルが追い出されて再ロードされたりすれば、ページ間で走査順が変わり重複・欠落が起こりえます。

筆者はこれを「抽出中はモデルのリフレッシュを避ける」という運用でカバーすることにし、残存リスクとして明示的に文書化しました。塞げないリスクは、隠すのではなく書き残して受容する — この線引きを設計時に決めておくと、後から「実は欠落しうるのでは」という蒸し返しがなくなります。

200 なのに失敗、400 なのに正常系

この API では HTTP ステータスと成否が一対一に対応しません。200 は「成功」または「切り詰め」であり、400 は多くの場合 DAX の構文エラー、つまりこちらのコードのバグです。監視やログをステータスコードで設計すると実態を見誤るので、「error の有無 + 返った行数」を軸にログを組むのがおすすめです。

運用の勘どころ

スロットリングは目安を計算しておく。 100,000 行/コールでページングする限り、120 リクエスト/分に触れるのは 1 分間に 1,200 万行を取り出す規模です。普通は先に応答サイズや処理時間が問題になります。もし枠に触れるなら、リトライの見直しより先に列数の削減を検討してください。

切り詰め warn を監視に載せる。 前章の「error + 行あり」の warn は、設計上は自動回収されるとはいえ、恒常的に出るならページサイズが列数に対して大きすぎます。列数 N に対して 1,000,000 ÷ N 行を上限の目安にページサイズを下げます。

インフラの request timeout が構造的上限。 同期応答でエクスポートを返す構成では、実行時間の上限は Power BI 側ではなくインフラ側で決まります (筆者の環境の Azure Container Apps は既定 240 秒)。ページ数が増えて恒常的に超えるようになったら、同期応答に固執せず「非同期ジョブ + Blob 配信 + 完了通知」へ構成を切り替える判断が必要です。

抽出ウィンドウとリフレッシュスケジュールをずらす。 順序無保証対策の運用面です。スケジュール更新の時刻が決まっているなら、エクスポートの実行帯をその前後から外すだけでリスクは大きく減らせます。

制約事項 (2026 年 8 月時点)

  • 本記事のページング方式は、公式に「Not supported」とされた領域を検出可能性で補う運用回避策です。Microsoft のサポート対象ではありません
  • 応答が JSON のため、2^53 − 1 を超える整数 ID は JavaScript 側で精度が落ちえます。該当するデータを扱うなら Arrow 版 API の検討が必要です
  • Arrow 版 executeDaxQueries はパブリックプレビューです。INFO 関数対応・固定行数上限なし・ストリーミング応答と利点は大きく、GA すれば本記事の方式からの有力な乗り換え先です
  • サービスプリンシパル (Managed Identity 含む) は、RLS 付き・SSO 有効のセマンティックモデルでは executeQueries を利用できません
  • Azure Analysis Services ホスト / AAS ライブ接続のモデルは非対応です
  • 仕様は更新されていくため、最新の公式ドキュメントを確認してください

まとめ

executeQueries は、HTTP + JSON だけでセマンティックモデルから生データを取り出せる手軽な API ですが、上限超過が HTTP 200 で返り無言で切り詰められるという一点だけは、知らずに使うと欠損データを正常出力する罠になります。守るべき不変条件は 4 つ — 終端判定は 0 行のみ、offset は実取得行数で進める、error は 3 箇所すべて検査する、includeNulls: true を明示する。この 4 つが揃っていれば、欠落は防げないケースでも必ず検出できます。

ここで取り出した行を CSV/ZIP としてストリーミング配信する実装編は、続編「Power BI の生データを CSV/ZIP でストリーミング配信する — Node.js Streams と archiver の地雷処理」として公開しています。

参考リンク

更新履歴

    • executeQueries の制約とページング設計の記事を公開