dv-query について
dv-queryは、Dataverseのレコードを読み取り分析するスキルです。レコードの検索、集計、エクスポートが必要なユーザー向けに、読み取りの形に応じてMCP、Dataverse CLI、Python SDKを使い分け、サーバー側での処理を重視します。
主な機能
- 01CLI、MCP、SDKでレコードを読み取り
- 02SQL、OData、FetchXMLでクエリ
- 03フィルター、結合、集計に対応
- 04クエリ結果をDataFrameに読み込み
- 05大規模な結果をページ単位で取得
活用シーン
- 01Dataverseのレコードを絞り込んで確認
- 02レコード数やフィールド値を集計
- 03関連テーブルを結合して分析
- 04レコードを一括抽出して表形式で分析
MCP 対応
小規模な対話型読み取りの選択肢としてMCPを挙げ、read_queryに言及しています。MCPサーバーを提供するとは記載されていません。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ microsoft/ dataverse-skills --skill dv-queryインストール後、Claude Code で/dv-queryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/erp-reads.mdjupyter-setup.mdquerybuilder.mdweb-api-advanced.md詳細
スキル:クエリ — Dataverse レコードの読み取りと分析
このスキルでは Python と Dataverse CLI を使用します。 Dataverse のスクリプト作成には Node.js、JavaScript、その他の言語を使用しないでください。概要スキルの「厳守事項」を参照してください。
読み取り:管理された手段を優先し、データの形状に応じて選択する
シンプルな読み取りの最短手順: dataverse auth who でアクティブなプロファイルが表示された場合は、ワークスペースのセットアップを省略し、以下の CLI の例を使って直接クエリしてください。データ読み取りに .env、auth.py、pip install、PAC は必要ありません。
読み取りの形状に応じて、MCP、Dataverse CLI、SDK のいずれかを選択してください。いずれも認証と再試行を処理します(以下のルーティング表と、概要のツールの機能/厳守事項 2 を参照)。MCP は対話形式の小規模な読み取りに、CLI はヘッドレスのワンライナー(OData、SQL、件数)に、SDK は一括処理や分析に適しています。$apply による集計や N:N の $expand には、client.query.fetchxml()(集計+link-entity)または管理された dataverse api のエスケープハッチを優先してください。手書きの urllib/get_token() を使うのは、Python のタイトなループ内で処理を完結させる必要がある場合に限ります(例:クライアント側の処理を伴う数千行のページング。web-api-advanced.md を参照)。
Dataverse CLI の注意点(カスタムテーブル+Windows)
dataverse CLI を直接使用する場合(ヘッドレスの読み取り/CRUD。CLI には .NET とキーリングが必要なため、ChatGPT web/Codex cloud では使用できません。そちらでは SDK を使用してください)、実際に確認されている落とし穴が 2 つあります。
- カスタムテーブルの SQL 複数形化。 SQL モードの
dataverse data queryはテーブル名を自動的に複数形化します。不規則な複数形の場合、解決先が誤りになります。FROM im_categoryはエンティティセットim_categorysを検索し、「テーブルが見つかりません」という内容の 404 を返します。テーブルが存在しないわけではありません。明示的なエンティティセットを指定した OData モードに切り替えてください:dataverse data query --table im_categories --select im_name。不明な場合はEntityDefinitionsから実際のEntitySetNameを確認してください。この 404 を根拠にテーブルが存在しないと判断しないでください。 - Windows シェルでの引用符。
cmd.exe/PowerShell が&をコマンド区切りとして扱わないように、--pathの値全体を二重引用符で囲んでください。&はそのままにしてください。OData のクエリオプションを区切る文字であり、%26にエンコードすると複数のオプションが結合され、クエリが壊れます。エンコードするのは$->%24のみです(PowerShell では、裸の$selectは変数として読み取られます)。引用符で囲まれていない&によってコマンドが分割されると、API が有効な JSON を返していてもラッパーがゼロ以外の終了コードを返す場合があります。引用符で囲めば防止できます。(他のスキルのdataverse api requestの例でパスを引用符で囲み、%24を使い、&はそのままにしているのはこのためです。)
Dataverse CLI のクエリ例(コピーしてすぐ実行できます)
すべての dataverse コマンドは、スキルの帰属を示すために --context(グローバルフラグ)を受け取ります。
# OData filtered read (--table takes the EntitySet name, e.g. accounts not account)
dataverse data query --table accounts --select "name,accountid" --filter "name eq 'john'" --top 10 --json --context "app=dataverse-skills/<ver>;skill=dv-query;agent=<agent>"
# Count records
dataverse data count --table accounts --context "app=dataverse-skills/<ver>;skill=dv-query;agent=<agent>"
# SQL mode (uses the logical name, e.g. account not accounts)
dataverse data query --sql "SELECT name, accountid FROM account WHERE name LIKE '%john%'" --json --context "app=dataverse-skills/<ver>;skill=dv-query;agent=<agent>"
# Get single record by ID
dataverse data get --table accounts --id <guid> --json --context "app=dataverse-skills/<ver>;skill=dv-query;agent=<agent>"
# Raw API escape hatch
dataverse api request --target dataverse --path "/api/data/v9.2/accounts?%24select=name&%24top=5" --context "app=dataverse-skills/<ver>;skill=dv-query;agent=<agent>"
ERP の対象データには別の経路を使用します。 Dataverse 環境にリンクされた ERP(Finance and Operations)は Python SDK を経由しません。references/erp-reads.md を参照してください。
データに関する質問への回答方法
ユーザーからデータについて質問された場合は、知っている API ではなく、質問の内容に応じて方法を選択してください。
| ユーザーの質問... | 方法 | 理由 |
|---|---|---|
| 「未解決のチケットを表示して」/単純なフィルター | MCP read_query、CLI dataverse data query --table ... --filter ...、または client.records.list(table, filter=...) | 結果が少なく、集計もありません |
| 「X はいくつある?」/単純な件数 | CLI dataverse data count --table ...、MCP read_query、または client.query.sql("SELECT COUNT(*) ...") | サーバー側で件数を数えるため(行をダウンロードしません) |
| 単一テーブルの集計(最頻値/合計/平均/上位 N 件) | $apply(生データ)または client.query.sql() の GROUP BY | どちらもサーバー側で実行され、グループ化された結果のみを返します |
| 複数テーブルの集計 | client.query.sql("...INNER JOIN...GROUP BY...") または client.query.fetchxml(...)(サーバー側)。それ以外の場合は builder から DataFrame に変換し、pd.merge() を使用 | sql() は INNER/LEFT JOIN と GROUP BY をサポートします。SQL で表現できない形状には pandas merge を使用します |
| 「関連する Y と一緒に X を表示して」/ルックアップの解決 | client.records.list(table, expand=...) または QueryBuilder | ルックアップを解決します |
| 「このデータをエクスポートして」/一括抽出 | client.query.builder(t).select(...).execute().to_dataframe() | DataFrame に直接変換して CSV にできます |
| 「ノートブックに読み込んで」/対話型分析 | client.query.builder(t).select(...).execute().to_dataframe() | pandas をネイティブに利用できます |
| 「重複を見つけて」/複雑なフィルター | client.records.list(table, filter=...) または QueryBuilder | SDK がページネーションを処理します |
| 単純なフィルター付き読み取り(5K 行未満) | CLI dataverse data query --sql "SELECT ..." または client.query.sql() | 軽量で、1 回の呼び出しで済みます |
重要な原則: サーバーに処理させてください。単一テーブルの集計には $apply(生データ)または client.query.sql() の GROUP BY を使用してください。どちらもサーバー側で実行され、グループ化された結果のみを返します。複数テーブルに関する質問では、サーバー側の sql() JOIN(INNER/LEFT)または fetchxml() の link-entity を優先してください。SQL で表現できない場合は、各テーブルを client.query.builder(t).select(...).execute().to_dataframe() で取得して pd.merge() を使用します。マージは 1 秒未満で完了します。ボトルネックはネットワーク転送であり、select によって最小限に抑えられます。
SQL クエリ — client.query.sql()
client.query.sql() は Dataverse Web API の ?sql= パラメーターを使用します。これは T-SQL のサブセットです。SELECT/SELECT DISTINCT/SELECT TOP N(0~5000)、INNER JOIN/LEFT JOIN、WHERE、GROUP BY、ORDER BY、OFFSET/FETCH、および COUNT/SUM/AVG/MIN/MAX をサポートします。SELECT *、サブクエリ、CTE、HAVING、UNION、RIGHT/FULL/CROSS JOIN、CASE、文字列/日付/数学関数はサポートしていません。結果は約 5,000 行に制限されます。
使用する場面: 5K 行未満のテーブルから、条件で絞り込んだデータを高速に読み取る場合です。この用途では、HTTP 呼び出しが 1 回で済むため、ページ単位の反復処理や DataFrame よりも大幅に高速です(約 2~6 秒)。
# 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}")
使用しない場面: 5K 行を超えるテーブル(結果が通知なく切り捨てられます)、SELECT *、サブクエリ/CTE、HAVING、UNION、RIGHT/FULL/CROSS JOIN、または関数です。INNER/LEFT JOIN と GROUP BY はサポートされています。5K 行未満の結果に対するサーバー側の結合/集計にはこれらを使用してください。より大きなデータやサポートされていない形状には、fetchxml() または $apply を使用してください。
FetchXML — サーバー側の結合と集計
SQL-JOIN が必要な場合や、OData ビルダーでは表現できない集計には FetchXML を使用してください。client.query.fetchxml(xml) は実行されていないクエリオブジェクトを返します。.execute()(すべてのページを一度に取得)または .execute_pages()(一度に 1 ページずつ遅延取得)を呼び出すまで、HTTP リクエストは送信されません。どちらも .to_dataframe() を備えた QueryResult ページを返します。
query = client.query.fetchxml("""
<fetch top="50">
<entity name="account">
<attribute name="name" />
<link-entity name="contact" from="parentcustomerid" to="accountid" alias="c" link-type="inner">
<attribute name="fullname" />
</link-entity>
</entity>
</fetch>
""")
result = query.execute() # collect all pages
df = result.to_dataframe()
# Or stream one page at a time for large results:
for page in query.execute_pages():
print(page.to_dataframe().shape)
クエリ可能な列を確認する — client.query.sql_columns()
SQL または $select を使った読み取りを行う前に、SQL エンドポイントで実際にクエリできる列を一覧表示します。仮想列と、計算された検索表示列は除外されます。各エントリには name、type、is_pk、is_name、label があります。
for c in client.query.sql_columns("account"):
print(f"{c['name']:30s} {c['type']:20s} PK={c['is_pk']}")
より詳しいスキーマ情報(列の全メタデータとテーブルのリレーションシップ)を確認するには、dv-metadata
(client.tables.list_columns()、client.tables.list_relationships()、
client.tables.list_table_relationships())を使用します。
スキルの適用範囲
| 必要な操作 | 代わりに使用 |
|---|---|
| レコードの作成、更新、削除(Dataverse) | dv-data |
| レコードのクエリ、作成、更新、削除(ERP) | references/erp-reads.md と erp-writes を参照 |
| テーブル、列、リレーションシップの作成 | dv-metadata |
| ソリューションのエクスポートまたはデプロイ | dv-solution |
セットアップ
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) は認証、環境 URL、プラグインのアトリビューション(User-Agent タグ付け)を処理します。scripts/auth.py を参照してください。最後まで実行するスクリプトでは、接続を自動的にクリーンアップできるよう、返されたクライアントを with 文で囲んでください。ERP では、ERP MCP または Dataverse CLI の --target erp パスを使用します。詳細は references/erp-reads.md を参照してください。
フィールド名の大文字・小文字の規則
この規則に従わないと、400 エラーが発生します。
| プロパティの種類 | 規則 | 例 | 使用時 |
|---|---|---|---|
| 構造(列) | LogicalName — 常に小文字 | new_name、new_priority | $select、$filter、$orderby |
| ナビゲーション(検索列) | Navigation Property Name — 大文字・小文字を区別し、$metadata と一致 | new_AccountId | $expand |
- システムテーブルのナビゲーションプロパティ(例:
parentaccountid、ownerid):小文字 - カスタム検索列のナビゲーションプロパティ:大文字・小文字を区別し、
$metadataの SchemaName と一致(例:new_AccountId)
レコードのクエリ
client.records.list() は、GA SDK の主要な読み取りメソッドです。すべてのページを収集し、直接イテレートできるフラットな QueryResult(ページではなくレコード)を返します。非常に大きな結果セットの場合は、client.records.list_pages() を使うと、HTTP ページごとに 1 つの QueryResult がストリーミングされます。列を絞り込むため、必ず select= を使用してください。
# list() -- flat QueryResult, iterate records directly
result = client.records.list(
"new_ticket",
select=["new_name", "new_priority", "new_status"],
filter="new_status eq 100000000",
orderby=["new_name asc"],
top=50,
)
for r in result:
print(r["new_name"], r["new_priority"])
print(f"{len(result)} tickets") # QueryResult supports len(), indexing, .first(), .to_dataframe()
すべての行を一度にメモリに読み込みたくない大きなテーブルでは、ページをストリーミングします。
for page in client.records.list_pages("new_ticket", select=["new_name"], page_size=200):
for r in page: # each page is a QueryResult
print(r["new_name"])
各レコードは、辞書のようにアクセスできる Record オブジェクトです。r["column"]、r.get("column")、r.keys() を使用できます。r.data.get() は使用せず、r.get() を直接使用してください。
records.get()からの移行:records.get()は GA SDK で非推奨です。for page in client.records.get(...): for r in page:はfor r in client.records.list(...):(フラット)に置き換えるか、list_pages(...)を使ってページループを維持してください。GUID を指定するrecords.get(table, guid)はrecords.retrieve(table, guid)に置き換えてください(見つからない場合はNoneを返します)。
ID で単一のレコードを取得する
client.records.retrieve() はレコードを返します。該当する GUID の行がない場合は None を返します(404 でも例外は発生しません)。
record = client.records.retrieve("new_ticket", "<record-guid>",
select=["new_name", "new_priority", "new_status"])
if record is not None:
print(record["new_name"])
else:
print("Ticket not found")
検索列を含む $select(GUID を使わない表示)
GUID の代わりに表示名を表示するには、include_annotations で書式設定値のアノテーションをリクエストします。
for r in client.records.list("opportunity",
select=["name", "estimatedvalue", "_parentaccountid_value"],
include_annotations="OData.Community.Display.V1.FormattedValue",
):
account_name = r.get("[email protected]")
print(f"{r['name']} — {account_name}")
必ず include_annotations を渡してください。渡さないと、Prefer: odata.include-annotations ヘッダーが送信されず、書式設定値がレスポンスに含まれません。すべてのアノテーションには "*" を、特定のアノテーションには上記の名前を使用してください。
書式設定値は、検索列、選択肢、ステータス、所有者のフィールドで利用できます。
$expand — 検索列から関連レコード全体を解決する
for r in client.records.list("opportunity",
select=["name", "estimatedvalue"],
expand=["parentaccountid($select=name)"], # nested $select avoids fetching all account columns
):
account = r.get("parentaccountid") or {}
print(f"{r['name']} — {account.get('name', 'Unknown')}")
$expand の中では、必ずネストした $select を使用してください。使用しない場合、Dataverse は関連エンティティのすべての列を返すため、帯域幅とメモリが無駄になります。
複数のカスタム検索列を使った $expand
for r in client.records.list(
"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
):
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では、小文字の論理名(new_customerid)ではなく、Navigation Property Name(new_CustomerId)を使用します。小文字を使うと 400 エラーになります。
高度なクエリパターン(生の Web API)
生の OData $apply 変換は、SDK メソッドが用意されていない唯一のパターンです。SDK では集計/結合に対応しています。client.query.sql()(INNER/LEFT JOIN、GROUP BY、COUNT/SUM/AVG)と client.query.fetchxml()(集計 + link-entity)が利用でき、N:N の $expand 読み取りには client.records.list(<table>, expand=["<n:n_nav>"]) または client.query.builder(<table>).expand(...) を使用できます。生の Web API を使うのは、$apply 変換の場合に限ってください。コードサンプルの全容については、references/web-api-advanced.mdを参照してください。
クイックリファレンス:
- N:N リレーションシップの
$expand: SDK のclient.records.list(<table>, expand=["<n:n_nav>"])/builder().expand()を優先してください。生の形式:GET /<entitySet>?$expand=<n:n_nav>($select=...)— 1 ページのみです。結果が5,000件を超える場合は@odata.nextLinkをたどってください。 - 集計用の
$apply: サーバー側で実行され、グループ化した結果を1回の呼び出しで返します。パターン:groupby((col),aggregate(metric with sum as total))、aggregate($count as count)、aggregate(amount with average as avg)。ソースレコード数の上限は50Kです。 - テーブルをまたぐ集計:
$applyは1つのエンティティセット内でのみ動作します。client.query.sql()(INNER/LEFT JOIN + GROUP BY)またはfetchxml()の link-entity を優先してください。それ以外の場合は、各テーブルをclient.query.builder(t).select(...).execute().to_dataframe()で取得し、pd.merge()→groupby()を実行してください。必ずselectを指定してください。指定しないと、データ転送量が10~20倍になります。
QueryBuilder — 流暢なクエリ API
単一の OData URL や FetchXML 文字列では扱いにくい複雑なクエリを、メソッドチェーンで構築するビルダーです。詳細なリファレンスと使用例については、references/querybuilder.mdを参照してください。
Jupyter Notebook のセットアップ
ノートブックで対話的にクエリを実行する方法(認証 + DataverseClient + DataFrame の表示)については、references/jupyter-setup.mdを参照してください。
ERP データのクエリ
ERP にリンクされた環境では、ERP の読み取りに DataverseClient は使用しません。ERP MCP、または dataverse data query/get/count --target erp を使用してください。references/erp-reads.mdを参照してください。
よくあるクエリエラー
| ステータス | 原因 | 対処方法 |
|---|---|---|
| 400 | $select/$filter のフィールド名の大文字/小文字が誤っている(小文字の LogicalName が必要)、または $expand の名前が誤っている(大文字/小文字を区別する Navigation Property Name が必要) | EntityDefinitions(LogicalName='...')/Attributes で名前を確認してください |
| 400 | サポートされていない SQL — MCP の read_query は DISTINCT/HAVING/サブクエリ/OFFSET/UNION/CAST/CONVERT/CASE/日付関数を拒否します(ただし JOIN + GROUP BY は許可されます)。client.query.sql() は SELECT */サブクエリ/CTE/HAVING/UNION/RIGHT/FULL/CROSS JOIN/関数を拒否します(ただし INNER/LEFT JOIN、GROUP BY、DISTINCT は許可されます) | sql() で表現できない形式には fetchxml()/$apply を使用するか、テーブル間の処理には pandas を使用してください |
| 404 | テーブルの論理名が見つかりません | スペルを確認してください。client.tables.get("<name>") を使って確認できます |
| 429 | レート制限に達しました | SDK が自動的に再試行します。ページサイズを小さくするか、ページ間に遅延を追加してください |
SDK スクリプトでの HttpError の処理については、dv-data のエラー処理パターンを参照してください。
Windows スクリプトに関する注意事項
.pyファイルでは ASCII 文字のみを使用してください。曲がった引用符やエムダッシュは Windows でSyntaxErrorの原因になります。- 複数行のコードに
python -cを使用しないでください。代わりに.pyファイルを作成してください。 - スクリプト内で GUID を生成してください。シェルのバッククォート置換ではなく、
str(uuid.uuid4())を使用します。