关于 dv-query
dv-query 是面向 Dataverse 数据读取与分析的技能,供需要查询记录、聚合或导出数据的用户使用。它按读取任务形态选择 MCP、Dataverse CLI 或 Python SDK,并强调让服务器执行筛选和聚合。
主要功能
- 01通过 CLI、MCP 或 SDK 读取记录
- 02使用 SQL、OData 和 FetchXML 查询
- 03支持筛选、联表与聚合
- 04将查询结果导入 DataFrame
- 05分页读取大型结果集
适用场景
- 01筛选并查看 Dataverse 记录
- 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。 请勿使用 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=...) 或 QueryBuilder | SDK 负责分页 |
| 简单筛选读取(少于 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: 优先使用 SDKclient.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 反引号替换。