mcpField
D
dv-query

dv-query

github.com/microsoftデータ・データベース248215 回インストール今日更新
リポジトリ

dv-query について

dv-queryは、Dataverseのレコードを読み取り分析するスキルです。レコードの検索、集計、エクスポートが必要なユーザー向けに、読み取りの形に応じてMCP、Dataverse CLI、Python SDKを使い分け、サーバー側での処理を重視します。

主な機能

  1. 01CLI、MCP、SDKでレコードを読み取り
  2. 02SQL、OData、FetchXMLでクエリ
  3. 03フィルター、結合、集計に対応
  4. 04クエリ結果をDataFrameに読み込み
  5. 05大規模な結果をページ単位で取得

活用シーン

  1. 01Dataverseのレコードを絞り込んで確認
  2. 02レコード数やフィールド値を集計
  3. 03関連テーブルを結合して分析
  4. 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.md
jupyter-setup.md
querybuilder.md
web-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=...) または QueryBuilderSDK がページネーションを処理します
単純なフィルター付き読み取り(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()) を使用します。

コメント