前回の記事では、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 とは — 埋め込みの隣にある生データ経路
executeQueries は POST /v1.0/myorg/groups/{workspaceId}/datasets/{datasetId}/executeQueries に DAX クエリを送ると、結果の行が JSON で返る API です。まず動かして感触を掴みます。
# 疎通確認: テーブルの先頭 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 で切り出す
EVALUATE TOPNSKIP(100000, <offset>, '受注明細')
第 1 引数が取得行数、第 2 引数が読み飛ばし行数です。テーブル名はシングルクォートで囲むため、日本語のテーブル名・列名もそのまま通ります (実環境で確認済み)。
終端は「0 行」だけ、offset は実取得行数で進める
ページングループの本体です。不変条件は 2 つ — 終端判定は「0 行が返ったとき」だけ、offset は「実際に取得できた行数」でだけ進める。
// 終端は 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 箇所すべて検査する
// 200 応答でも error は 3 箇所に現れうる — すべて辿る
function pickError(body) {
return body?.error
?? body?.results?.[0]?.error
?? body?.results?.[0]?.tables?.[0]?.error
?? null;
}
runQuery の中でこの検査を挟み、error があり行が 0 行なら実行時エラーとして throw、error があっても行が返っていれば「切り詰め」として warn ログを出して行を使います。切り詰め自体は前節の offset 設計が回収してくれるので処理は続行できますが、warn が出続けるならページサイズか列数を見直すサインです (運用の章で触れます)。
includeNulls: true を明示する
{
"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 までソケットを掴み続けます。
// 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 の地雷処理」として公開しています。
参考リンク
- Datasets - Execute Queries (Microsoft Learn) — 制限値・切り詰め挙動・error スキーマの一次情報
- Datasets - Execute Dax Queries (Microsoft Learn) — Arrow 版 API のリファレンス
- Mastering the Execute DAX Queries REST API (Microsoft Learn) — 新旧 API 比較表 (Pagination: Not supported の出典)
- TOPNSKIP (DAX Guide) — 順序無保証の解説
- Manage semantic model access permissions (Microsoft Learn) — Build 権限の仕様
- Metadata scanning (Microsoft Learn) — テーブル列挙の代替となる Scanner API
- Embed Power BI content with service principal (Microsoft Learn) — サービスプリンシパルの制約 (RLS/SSO 含む)
- Ingress in Azure Container Apps (Microsoft Learn) — request timeout の仕様