mcpField
D
dv-query

dv-query

github.com/microsoft数据与数据库248215 次安装今天更新
代码仓库

关于 dv-query

dv-query 是面向 Dataverse 数据读取与分析的技能,供需要查询记录、聚合或导出数据的用户使用。它按读取任务形态选择 MCP、Dataverse CLI 或 Python SDK,并强调让服务器执行筛选和聚合。

主要功能

  1. 01通过 CLI、MCP 或 SDK 读取记录
  2. 02使用 SQL、OData 和 FetchXML 查询
  3. 03支持筛选、联表与聚合
  4. 04将查询结果导入 DataFrame
  5. 05分页读取大型结果集

适用场景

  1. 01筛选并查看 Dataverse 记录
  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。 请勿使用 Node.js、JavaScript 或任何其他语言编写 Dataverse 脚本。请参阅概览技能中的硬性规则。

读取:优先使用托管接口,根据读取类型选择

简单读取的快捷方式: 如果 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 备用方案;仅在紧凑的 Python 循环中需要进程内执行时,才使用手动编写的 urllib/get_token()(例如,分页读取数千行并在客户端处理 — 请参阅 web-api-advanced.md)。

Dataverse CLI 注意事项(自定义表 + Windows)

直接使用 dataverse CLI(无头读取/CRUD;请注意,CLI 需要 .NET + 密钥环,因此无法在 ChatGPT web / Codex cloud 上使用 — 在这些环境中请使用 SDK)时,有两个经验性陷阱:

  • 自定义表的 SQL 复数化。 dataverse data query 在 SQL 模式下会自动将表名复数化,不规则复数会导致解析错误:FROM im_category 会查找实体集 im_categorys,并返回看起来像是“表缺失”的 404。表并没有缺失 — 请切换到 OData 模式并指定实体集:dataverse data query --table im_categories --select im_name。不确定时,请从 EntityDefinitions 中查找实际的 EntitySetName;绝不要因这个 404 就断定表不存在。
  • Windows shell 引号。 将整个 --path 值用双引号括起来,避免 cmd.exe/PowerShell 将 & 当作命令分隔符。保持 & 原样 — 它用于分隔 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 目标使用单独的路径。 ERP(Finance and Operations)关联到 Dataverse 环境时,不会通过 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
“显示 X 及其关联的 Y”/解析查找字段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()轻量级单次调用

**核心原则:**让服务器完成工作。对于单表聚合,使用 $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 可以减少传输量。

**始终查询实时 Dataverse 环境。**当用户期望获取 Dataverse 中的结果时,不要查询本地副本、缓存文件或源数据库。Dataverse 中的数据才是事实来源。


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 调用,比逐页迭代或使用 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()(每次惰性获取一页)时才会发起 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 页面流式返回一个个 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() 迁移: GA SDK 中的 records.get() 已弃用。请将 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 使用 Navigation Property Name(new_CustomerId),而不是小写的逻辑名称(new_customerid)。使用小写会导致 400 错误。


高级查询模式(原始 Web API)

原始 OData $apply 转换是唯一没有 SDK 方法的模式。SDK 确实支持聚合/联接——client.query.sql()(INNER/LEFT JOIN、GROUP BY、COUNT/SUM/AVG)和 client.query.fetchxml()(aggregate + link-entity)——以及 N:N $expand 读取,可通过 client.records.list(<table>, expand=["<n:n_nav>"]) 或 client.query.builder(<table>).expand(...) 实现。只有 $apply 转换才需要使用原始 Web API。完整代码示例请参阅 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=...)——仅支持单页;结果超过 5,000 条时,需继续跟进 @odata.nextLink。
  • 用于聚合的 $apply: 在服务器端运行,一次调用即可返回分组结果。模式:groupby((col),aggregate(metric with sum as total))、aggregate($count as count)、aggregate(amount with average as avg)。源记录上限为 50K。
  • 跨表聚合: $apply 仅适用于单个实体集。优先使用 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 设置

有关在 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()),不要使用 shell 反引号替换。

评论

支持 Markdown