📊dv-query
- プラグイン
- dataverse
- ソース
- GitHub で見る ↗
説明
Dataverseのデータに対して、Python SDKおよびWeb APIを通じた バルク読み取り・複数ページのイテレーション・アナリティクスを実行します。 次のような場合に使用: ユーザーがレコードの読み取り・一覧表示・フィルタリング・集計・グループ化・結合・分析を行いたい場合(pandasのDataFrameワークフローやノートブックでの探索的分析を含む)。
原文を表示
Bulk reads, multi-page iteration, and analytics over Dataverse data via the Python SDK and Web API. Use when the user wants to read, list, filter, aggregate, group, join, or analyze records — including pandas DataFrame workflows and notebook exploration.
ユースケース
- ✓Dataverseのレコードを読み取りたい
- ✓複数ページのデータをイテレーション処理するとき
- ✓レコードをフィルタリング・集計・グループ化したい
- ✓データを分析・探索的調査を行うとき
- ✓pandasのDataFrameワークフローで処理するとき
本文(日本語訳)
スキル: クエリ — Dataverse レコードの読み取りと分析
このスキルは Python のみを使用します。 Dataverse スクリプトに Node.js、JavaScript、その他の言語は使用しないでください。概要スキルの「Hard Rules(厳守ルール)」を参照してください。
読み取りにおける SDK 優先ルール
すべての読み取り操作は SDK を使用します — urllib、requests、または生の HTTP は使用しません。
これは dv-data の「SDK 優先ルール」と同じルールを、読み取り操作にも適用したものです。
クエリのために urllib.request や get_token() を書こうとしている場合は、STOP — SDK がそれを処理します。
例外は $apply による集計と N:N の $expand のみで、以下で説明します。
データに関する質問への回答方法
ユーザーがデータについて質問した場合、ユーザーが何を聞いているかに基づいてアプローチを選択してください。知っている API を基準に選ばないでください。
| ユーザーの質問 | アプローチ | 理由 |
|---|---|---|
| 「オープンなチケットを表示して」/ 単純なフィルター | MCP read_query(利用可能な場合)または client.records.get() + $filter |
少量の結果、集計なし |
| 「X はいくつある?」/ 単純なカウント | MCP read_query または client.records.get() + count=True |
単一の数値 |
| 単一テーブルの集計(最大値/合計/平均/上位N件) | $apply サーバーサイド集計(生の Web API) |
1回の HTTP 呼び出し、グループ化結果のみを返す |
| クロステーブル集計 | client.dataframe.get() + 最小限の $select + pd.merge() |
サーバーはJOINできないため、最小限の列指定でpandasマージを高速に実行 |
| 「関連するYを含むXを表示して」/ ルックアップ解決 | client.records.get() + $expand、または QueryBuilder(b8+) |
ルックアップ解決 |
| 「このデータをエクスポートして」/ 一括抽出 | client.dataframe.get() + select= |
DataFrame → CSV に直接出力 |
| 「ノートブックに読み込んで」/ インタラクティブ分析 | client.dataframe.get() または QueryBuilder .to_dataframe()(b8+) |
pandas ネイティブ |
| 「重複を見つけて」/ 複雑なフィルター | client.records.get() + $filter、または QueryBuilder(b8+) |
SDK がページネーションを処理 |
| 単純なフィルター読み取り(5,000行未満) | client.query.sql() |
WHERE / ORDER BY / TOP 付きの軽量 SQL SELECT |
基本原則: サーバーに処理させる。
単一テーブルの集計には $apply を使用してください — サーバーサイドで実行され、グループ化された結果のみを返します。
クロステーブルの質問には、各テーブルで client.dataframe.get() + 最小限の $select を使用し、その後 pd.merge() を行ってください — マージ自体はサブ秒で完了し、ボトルネックはネットワーク転送であり、それを $select で最小化します。
常にライブの Dataverse 環境に対してクエリを実行してください。 ユーザーが Dataverse からの結果を期待している場合、ローカルコピー、キャッシュファイル、またはソースデータベースにクエリを実行しないでください。Dataverse のデータが信頼できる唯一の情報源です。
SQL クエリ — client.query.sql()
client.query.sql() は Dataverse Web API の ?sql= パラメーターを使用します — これは 限定的な SQL サブセットです(MCP read_query と同じ制限)。
GROUP BY、JOIN、HAVING、DISTINCT、サブクエリはサポートされていません。結果は最大 約5,000行 に制限されます。
次のような場合に使用: 5,000行未満のテーブルに対する高速なフィルター読み取り。 これらの場合、1回の HTTP 呼び出しで完結するため、ページイテレーションや DataFrame よりも大幅に高速です(約2〜6秒)。
# 小さなテーブル(5,000行未満)への高速フィルター読み取り
results = client.query.sql(
"SELECT TOP 100 name, estimatedvalue "
"FROM opportunity "
"WHERE statecode = 0 "
"ORDER BY estimatedvalue DESC"
)
for r in results:
print(f"{r['name']}: ${r.get('estimatedvalue', 0):,.0f}")
次のような場合は使用しないでください:
5,000行を超えるテーブル(結果がサイレントに切り捨てられます)、集計(GROUP BY なし)、またはクロステーブルクエリ(JOIN なし)。
単一テーブルの集計には $apply を、クロステーブルには client.dataframe.get() + pd.merge() を使用してください。
スキルの境界
| 必要な操作 | 代わりに使用するスキル |
|---|---|
| レコードの作成・更新・削除 | dv-data |
| テーブル・列・リレーションシップの作成 | dv-metadata |
| ソリューションのエクスポートまたはデプロイ | dv-solution |
セットアップ
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_client
# get_client は User-Agent ヘッダーにプラグインのアトリビューションコンテキストを設定します。
# コンテキスト値は変更しないでください — これはサーバーサイドのテレメトリ
#(app/skill/agent)向けのクローズドスキーマです。シークレットや PII を含めないでください。
client = get_client("dv-query")
get_client(skill) は認証、環境 URL、およびプラグインのアトリビューション(User-Agent タグ付け)を処理します。
scripts/auth.py を参照してください。
完了まで実行するスクリプトでは、接続の自動クリーンアップのために返されたクライアントを with 文でラップしてください。
フィールド名の大文字小文字ルール
これを誤ると 400 エラーが発生します。
| プロパティの種類 | 規則 | 例 | 使用場面 |
|---|---|---|---|
| 構造的(列) | LogicalName — 常に小文字 | new_name, new_priority |
$select、$filter、$orderby |
| ナビゲーション(ルックアップ) | Navigation Property Name — 大文字小文字を区別、$metadata に一致 |
new_AccountId |
$expand |
- システムテーブルのナビゲーションプロパティ(例:
parentaccountid、ownerid): 小文字 - カスタムルックアップのナビゲーションプロパティ: 大文字小文字を区別し、
$metadataの SchemaName に一致(例:new_AccountId)
レコードのクエリ(複数ページ)
client.records.get() はプライマリの読み取りメソッドです — すべての SDK バージョン(b6+)で動作します。
複数レコードのクエリに対してはページイテレーターを返し、GUID による取得では単一の Record を返します。
常に select= を使用して列を制限してください。
for page in client.records.get(
"new_ticket",
select=["new_name", "new_priority", "new_status"],
filter="new_status eq 100000000",
orderby=["new_name asc"],
top=50,
):
for r in page:
print(r["new_name"], r["new_priority"])
client.records.get() はページイテレーターを返します — 常にページを反復し、その後各ページ内のレコードを反復してください。
各レコードは dict ライクなアクセスをサポートする Record オブジェクトです: r["column"]、r.get("column")、r.keys()。
r.data.get() は使用せず、r.get() を直接使用してください。
ID による単一レコードの取得
record = client.records.get("new_ticket", "<record-guid>",
select=["new_name", "new_priority", "new_status"])
print(record["new_name"])
ルックアップ列での $select(GUID なしの表示名)
GUID の代わりに表示名を表示するには、include_annotations を使用してフォーマット済み値アノテーションをリクエストします:
for page in client.records.get("opportunity",
select=["name", "estimatedvalue", "_parentaccountid_value"],
include_annotations="OData.Community.Display.V1.FormattedValue",
):
for r in page:
account_name = r.get("_parentaccountid_value@OData.Community.Display.V1.FormattedValue")
print(f"{r['name']} — {account_name}")
include_annotations の指定は必須です — 指定しない場合、Prefer: odata.include-annotations ヘッダーが送信されず、フォーマット済み値がレスポンスに含まれません。
すべてのアノテーションには "*" を使用するか、上記の特定のアノテーション名を指定してください。
フォーマット済み値は、ルックアップ・選択肢・ステータス・オーナーのフィールドで利用できます。
$expand — ルックアップを関連レコード全体に解決する
for page in client.records.get("opportunity",
select=["name", "estimatedvalue"],
expand=["parentaccountid($select=name)"], # ネストされた $select でアカウントの全列取得を回避
):
for r in page:
account = r.get("parentaccountid") or {}
print(f"{r['name']} — {account.get('name', 'Unknown')}")
$expand 内では常にネストされた $select を使用してください — 指定しない場合、Dataverse は関連エンティティのすべての列を返し、帯域幅とメモリを無駄にします。
複数のカスタムルックアップを持つ $expand
for page in client.records.get(
"new_ticket",
select=["new_name", "new_priority", "new_status"],
expand=["new_CustomerId($select=new_name)", "new_AgentId($select=new_name)"], # ネストされた $select + 大文字小文字を区別するナビゲーションプロパティ
):
for r in page:
customer = r.get("new_CustomerId") or {}
agent = r.get("new_AgentId") or {}
print(f"{r['new_name']} | {customer.get('new_name','')} | {agent.get('new_name','')}")
expandには Navigation Property Name(new_CustomerId)を使用し、小文字の LogicalName(new_customerid)は使用しないでください。小文字を使用すると 400 エラーが発生します。
高度なクエリパターン(Web API のみ)
集計および多対多の展開については、SDK が直接サポートしていないため、生の Web API を使用します。
完全なコードサンプルは references/web-api-advanced.md を参照してください。
クイックリファレンス:
-
N:N リレーションシップでの
$expand:GET /<entitySet>?$expand=<n:n_nav>($select=...)— 単一ページのみ。5,000件を超える結果は@odata.nextLinkをたどってください。 -
$applyによる集計: サーバーサイドで実行され、1回の呼び出しでグループ化された結果を返します。 パターン:groupby((col),aggregate(metric with sum as total))、aggregate($count as count)、aggregate(amount with average as avg)。 ソースレコードの上限は 50,000件。 -
クロステーブル集計:
$applyは単一エンティティセット内でのみ動作します。 テーブルごとにclient.dataframe.get(entity, select=[...])を使用し、pd.merge()→groupby()を行ってください。 常にselect=を指定してください — 指定しない場合、データ転送量が10〜20倍になります。
QueryBuilder — フルエント クエリ API(SDK b8+)
PowerPlatform-Dataverse-Client b8+ で利用可能。
単一の OData URL や FetchXML 文字列では扱いにくい複雑なクエリを構築するための、チェーン可能なビルダーです。
完全なリファレンスとサンプルは references/querybuilder.md を参照してください。
Jupyter Notebook のセットアップ
ノートブックで
原文(English)を表示
Skill: Query — Read and Analyze Dataverse Records
This skill uses Python exclusively. Do not use Node.js, JavaScript, or any other language for Dataverse scripting. See the overview skill's Hard Rules.
SDK-First Rule for Reads
All reads use the SDK — not urllib, requests, or raw HTTP. This is the same rule as dv-data's SDK-First Rule, applied to reads. If you find yourself writing urllib.request or get_token() for a query, STOP — the SDK handles it. The only exceptions are $apply aggregation and N:N $expand, documented below.
How to Answer Data Questions
When the user asks a question about their data, pick the approach by what they're asking, not by which API you know:
| User asks... | Approach | Why |
|---|---|---|
| "show me open tickets" / simple filter | MCP read_query (if available) or client.records.get() with $filter |
Small result, no aggregation |
| "how many X" / simple count | MCP read_query or client.records.get() with count=True |
Single number |
| Single-table aggregation (most/sum/avg/top-N) | $apply server-side aggregation (raw Web API) |
One HTTP call, returns only grouped results |
| Cross-table aggregation | client.dataframe.get() with minimal $select + pd.merge() |
Server can't join; pandas merge is fast with minimal columns |
| "show me X with related Y" / resolve lookups | client.records.get() with $expand or QueryBuilder (b8+) |
Lookup resolution |
| "export this data" / bulk extract | client.dataframe.get() with select= |
Direct to DataFrame → CSV |
| "load into notebook" / interactive analysis | client.dataframe.get() or QueryBuilder .to_dataframe() (b8+) |
pandas native |
| "find duplicates" / complex filter | client.records.get() with $filter or QueryBuilder (b8+) |
SDK handles pagination |
| Simple filtered read (<5K rows) | client.query.sql() |
Lightweight SQL SELECT with WHERE, ORDER BY, TOP |
Key principle: Let the server do the work. For single-table aggregation, use $apply — it runs server-side and returns only grouped results. For cross-table questions, use client.dataframe.get() with minimal $select on each table, then pd.merge() — the merge itself is sub-second; the bottleneck is network transfer, which $select minimizes.
Always query the live Dataverse environment. Do not query local copies, cached files, or source databases when the user expects results from Dataverse. The data in Dataverse is the source of truth.
SQL Queries — client.query.sql()
client.query.sql() uses the Dataverse Web API ?sql= parameter — a limited SQL subset (same limitations as MCP read_query). It does NOT support GROUP BY, JOINs, HAVING, DISTINCT, or subqueries. Results are capped at ~5,000 rows.
When to use: Fast filtered reads on tables with <5K rows. For these, it's significantly faster (~2-6s) than page iteration or DataFrames because it's a single HTTP call.
# Fast filtered read on small tables (<5K rows)
results = client.query.sql(
"SELECT TOP 100 name, estimatedvalue "
"FROM opportunity "
"WHERE statecode = 0 "
"ORDER BY estimatedvalue DESC"
)
for r in results:
print(f"{r['name']}: ${r.get('estimatedvalue', 0):,.0f}")
Do NOT use for: Tables >5K rows (results silently truncated), aggregation (no GROUP BY), or cross-table queries (no JOINs). Use $apply for single-table aggregation and client.dataframe.get() + pd.merge() for cross-table.
Skill boundaries
| Need | Use instead |
|---|---|
| Create, update, delete records | dv-data |
| Create tables, columns, relationships | dv-metadata |
| Export or deploy solutions | dv-solution |
Setup
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_client
# get_client sets a plugin attribution context on the User-Agent header.
# Do not modify the context value — it is a closed schema for server-side
# telemetry (app/skill/agent). Never include secrets or PII.
client = get_client("dv-query")
get_client(skill) handles auth, environment URL, and plugin attribution (User-Agent tagging). See scripts/auth.py. For scripts that run to completion, wrap the returned client in a with statement for automatic connection cleanup.
Field Name Casing Rule
Getting this wrong causes 400 errors.
| Property type | Convention | Example | When used |
|---|---|---|---|
| Structural (columns) | LogicalName — always lowercase | new_name, new_priority |
$select, $filter, $orderby |
| Navigation (lookups) | Navigation Property Name — case-sensitive, matches $metadata |
new_AccountId |
$expand |
- System table navigation properties (e.g.,
parentaccountid,ownerid): lowercase - Custom lookup navigation properties: case-sensitive, match
$metadataSchemaName (e.g.,new_AccountId)
Query Records (multi-page)
client.records.get() is the primary read method — works on all SDK versions (b6+). It returns a page iterator for multi-record queries and a single Record for by-GUID fetch. Always use select= to limit columns.
for page in client.records.get(
"new_ticket",
select=["new_name", "new_priority", "new_status"],
filter="new_status eq 100000000",
orderby=["new_name asc"],
top=50,
):
for r in page:
print(r["new_name"], r["new_priority"])
client.records.get() returns a page iterator — always iterate pages and then records within each page. Each record is a Record object that supports dict-like access: r["column"], r.get("column"), r.keys(). Do not use r.data.get() — use r.get() directly.
Fetch a Single Record by ID
record = client.records.get("new_ticket", "<record-guid>",
select=["new_name", "new_priority", "new_status"])
print(record["new_name"])
$select with Lookup Columns (GUID-free display)
To show display names instead of GUIDs, request the formatted value annotation via include_annotations:
for page in client.records.get("opportunity",
select=["name", "estimatedvalue", "_parentaccountid_value"],
include_annotations="OData.Community.Display.V1.FormattedValue",
):
for r in page:
account_name = r.get("_parentaccountid_value@OData.Community.Display.V1.FormattedValue")
print(f"{r['name']} — {account_name}")
You MUST pass include_annotations — without it, the Prefer: odata.include-annotations header is not sent and formatted values are not in the response. Use "*" for all annotations or the specific annotation name above.
Formatted values are available for lookup, choice, status, and owner fields.
$expand — Resolve Lookup to Full Related Record
for page in client.records.get("opportunity",
select=["name", "estimatedvalue"],
expand=["parentaccountid($select=name)"], # nested $select avoids fetching all account columns
):
for r in page:
account = r.get("parentaccountid") or {}
print(f"{r['name']} — {account.get('name', 'Unknown')}")
Always use nested $select inside $expand — without it, Dataverse returns every column on the related entity, which wastes bandwidth and memory.
$expand with multiple custom lookups
for page in client.records.get(
"new_ticket",
select=["new_name", "new_priority", "new_status"],
expand=["new_CustomerId($select=new_name)", "new_AgentId($select=new_name)"], # nested $select + case-sensitive nav props
):
for r in page:
customer = r.get("new_CustomerId") or {}
agent = r.get("new_AgentId") or {}
print(f"{r['new_name']} | {customer.get('new_name','')} | {agent.get('new_name','')}")
expanduses the Navigation Property Name (new_CustomerId), not the lowercase logical name (new_customerid). Using lowercase causes a 400 error.
Advanced query patterns (Web API only)
For aggregations and many-to-many expansion, the SDK doesn't have direct support — use raw Web API. See references/web-api-advanced.md for full code samples.
Quick reference:
$expandon N:N relationships:GET /<entitySet>?$expand=<n:n_nav>($select=...)— single page only; follow@odata.nextLinkfor >5,000 results.$applyfor aggregations: runs server-side, returns grouped results in one call. Patterns:groupby((col),aggregate(metric with sum as total)),aggregate($count as count),aggregate(amount with average as avg). 50K source-record limit.- Cross-table aggregation:
$applyonly works within one entity set. Useclient.dataframe.get(entity, select=[...])per table →pd.merge()→groupby(). Always passselect=; without it transfers 10-20× more data.
QueryBuilder — Fluent Query API (SDK b8+)
Available in PowerPlatform-Dataverse-Client b8+. Chainable builder for complex queries that would be awkward as a single OData URL or FetchXML string. Full reference and examples in references/querybuilder.md.
Jupyter Notebook Setup
For interactive querying in notebooks (auth + DataverseClient + DataFrame display), see references/jupyter-setup.md.
Common Query Errors
| Status | Cause | Fix |
|---|---|---|
| 400 | Wrong field casing in $select/$filter (must be lowercase LogicalName) or $expand (must be case-sensitive Navigation Property Name) |
Verify names via EntityDefinitions(LogicalName='...')/Attributes |
| 400 | Unsupported SQL in MCP read_query or client.query.sql() (DISTINCT, HAVING, subqueries, OFFSET, JOINs, GROUP BY) |
Use $apply for single-table aggregation, or client.dataframe.get() + pandas for cross-table |
| 404 | Table logical name not found | Check spelling — use client.tables.get("<name>") to verify |
| 429 | Rate limited | SDK retries automatically; reduce page size or add delays between pages |
For HttpError handling in SDK scripts, see the error handling pattern in dv-data.
Windows Scripting Notes
- ASCII only in
.pyfiles — curly quotes and em dashes causeSyntaxErroron Windows. - No
python -cfor multiline code — write a.pyfile instead. - Generate GUIDs in scripts:
str(uuid.uuid4()), not shell backtick substitution.
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。