跳到主要内容

数据中台对外开放数据查询接口文档


目录​

  1. 接入信息
  2. 认证与权限
  3. 接口一:聚合指标查询(推荐首选)
  4. 接口二:自由查询
  5. 接口三:标准 SQL 查询
  6. 接口四:冷查询(异步)
  7. 接口五:异步导出
  8. 接口选择决策树
  9. 安全约束
  10. 限流机制
  11. 错误码汇总
  12. SDK 中台表说明
  13. 各业务数据表
  14. 完整场景示例
  15. 附录

1. 接入信息​

项目值
基础 URLhttp://dc.example.com:17030
认证方式请求头 X-API-Key: {API Key}
认证类型query 类型 API Key
所有接口前缀/api/v1/external
返回格式JSON

API 端点总览​

#方法路径说明响应模式适用场景
1POST/aggregate聚合指标查询同步看板、实时大盘
2POST/free-query自由查询同步明细下钻、探索分析
3POST/sql-query标准 SQL 查询同步复杂分析、SQL 直接写
4POST/query冷查询异步(轮询)批量报表、预定义数据集
5GET/query/{job_id}轮询冷查询结果同步-
6POST/query/{job_id}/cancel取消冷查询同步-
7POST/export异步导出异步(轮询)大数据量导出 CSV/JSON
8GET/export/{task_id}查询导出状态同步-

2. 认证与权限​

2.1 认证方式​

所有外部查询接口均在请求头中传入 X-API-Key:

X-API-Key: <DC_API_KEY>

这不同于 Cube.js 的 Authorization 头。Cube.js 使用 Authorization 头,查询 API 使用 X-API-Key 头。

2.2 Scope 权限模型​

每个 query 类型 API Key 绑定一种 scope:

Scope 类型scope_ref权限范围支持接口
gamegame_id仅限查询该业务全部
projectproject_id可查询该项目下所有业务全部
datasetdataset_name仅限查询指定数据集仅冷查询

2.3 认证失败响应​

// HTTP 401 - Key 无效
{ "detail": "Invalid or inactive query API Key" }

// HTTP 403 - 无权访问
{ "detail": "API Key 无权查询 game_id='xxx'" }

3. 接口一:聚合指标查询​

POST /api/v1/external/aggregate​

最推荐的首选接口。查 DAU、付费金额、付费人数、在线峰值等核心指标。当日或未最终物化日期从 dc_events_* 事件表实时/近实时计算,历史日期自动路由到 GA 物化日表。Redis 当前用于查询结果缓存、限流和异步结果缓存,不作为实时指标源。

请求参数​

{
"game_id": "game_a",
"region": "region_a",
"metrics": ["dau", "revenue", "pay_users", "pay_rate", "arpu", "online_peak"],
"start_date": "2026-05-12",
"end_date": "2026-05-18"
}
字段类型必填说明
game_idstring是业务标识
regionstring否区域,对外统一使用 "region_a",默认 "region_a"
metricslist[string]是指标列表,1-20 个
start_datedate是起始日期 YYYY-MM-DD
end_datedate是结束日期 YYYY-MM-DD

支持的全部指标​

指标名标签类型数据源说明
dauDAUcountEvents / GA日活跃用户数
new_usersNew UserscountGA新增用户数
revenueRevenuesumEvents / GA充值总金额
pay_usersPay UserscountGA付费用户数
online_peakOnline PeakcountEvents / GA在线峰值
pay_ratePay Ratecomputed计算付费率 = pay_users / dau
arpuARPUcomputed计算人均收入 = revenue / dau
arppuARPPUcomputed计算付费人均收入 = revenue / pay_users
retention_d1D1 RetentioncountGA次日留存率
retention_d7D7 RetentioncountGA7 日留存率
ltv_d30LTV D30computed计算30 天 LTV = arpu x 30

数据源自动路由​

end_date >= 今天或物化未最终完成 -> dc_events_* 事件表实时/近实时计算
end_date < 今天且 GA 物化完成 -> GA TimescaleDB 物化日表

计算指标(pay_rate, arpu, arppu, ltv_d30)由引擎先查依赖指标再计算,无需手动处理。

响应示例​

{
"game_id": "game_a",
"region": "region_a",
"start_date": "2026-05-12",
"end_date": "2026-05-18",
"metrics": [
{ "metric": "dau", "value": 85230.0, "label": "DAU", "source": "ga" },
{ "metric": "revenue", "value": 1245600.5, "label": "Revenue", "source": "ga" },
{ "metric": "pay_users", "value": 3420.0, "label": "Pay Users", "source": "ga" },
{ "metric": "pay_rate", "value": 0.0401, "label": "Pay Rate", "source": "computed" },
{ "metric": "arpu", "value": 14.62, "label": "ARPU", "source": "computed" },
{ "metric": "online_peak", "value": 12100.0, "label": "Online Peak", "source": "ga" }
]
}

source 字段说明数据来自哪里:"events" 表示事件事实层实时/近实时计算,"ga" 表示历史物化表,"computed" 表示引擎计算,"unavailable" 表示当前指标不可用。

cURL 示例​

# 查询最近 7 天核心指标
curl -X POST 'http://dc.example.com:17030/api/v1/external/aggregate' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_a",
"metrics": ["dau", "revenue", "pay_users", "pay_rate", "arpu"],
"start_date": "2026-05-12",
"end_date": "2026-05-18"
}'

# 查询今日实时 DAU + 在线峰值
curl -X POST 'http://dc.example.com:17030/api/v1/external/aggregate' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_b",
"metrics": ["dau", "online_peak", "revenue"],
"start_date": "2026-05-19",
"end_date": "2026-05-19"
}'

Python 示例​

import requests

BASE = "http://dc.example.com:17030/api/v1/external"
HEADERS = {"X-API-Key": "<DC_API_KEY>", "Content-Type": "application/json"}


def get_core_kpi(game_id: str, start: str, end: str) -> dict[str, float]:
"""获取核心 KPI 指标"""
resp = requests.post(
f"{BASE}/aggregate",
headers=HEADERS,
json={
"game_id": game_id,
"metrics": ["dau", "revenue", "pay_users", "pay_rate", "arpu", "arppu"],
"start_date": start,
"end_date": end,
},
)
resp.raise_for_status()
data = resp.json()
return {m["metric"]: m["value"] for m in data["metrics"]}


# 使用
kpis = get_core_kpi("game_a", "2026-05-12", "2026-05-18")
print(f"DAU: {kpis['dau']:.0f}, 收入: {kpis['revenue']:.2f}, 付费率: {kpis['pay_rate']:.2%}")

4. 接口二:自由查询​

POST /api/v1/external/free-query​

自由查询支持指标模式、公开 GA 表模式和原始表模式。指标模式走统一查询引擎,公开 GA 表模式走 GA table executor,原始表模式才会通过 DW SparkSQL 查询外部表。同步返回。

4.1 明细查询​

返回原始数据行,适合查具体用户记录、道具流水、充值明细等。

请求结构:

{
"game_id": "game_a",
"table": "t_l_log_player_login",
"columns": ["recorddate", "openid", "roleid", "areaid", "level", "ip"],
"filters": {
"record_date_start": "2026-05-18",
"record_date_end": "2026-05-19",
"areaid": "1"
},
"limit": 500
}
字段类型必填默认值说明
game_idstring是-业务标识
tablestring是-表名
columnslist[string]否-返回列。省略 = SELECT *
filtersdict是-必须包含 record_date_start 和 record_date_end
limitint否10001-10000
cache_ttlint否3600缓存秒数,0=不缓存

filters 说明:

  • record_date_start:必填,起始日期 YYYY-MM-DD
  • record_date_end:必填,结束日期(不包含,即 < 'end_date')
  • 其他 key:自定义维度过滤,均按 = 精确匹配

响应:

{
"data": [
{ "recorddate": "2026-05-18", "openid": "oXXX...", "roleid": "123456", "areaid": "1", "level": "45", "ip": "example.com" },
{ "recorddate": "2026-05-18", "openid": "oYYY...", "roleid": "789012", "areaid": "1", "level": "32", "ip": "example.com" }
],
"row_count": 2,
"cached": false,
"sql": null
}

4.2 聚合查询​

指定 group_by + aggregates,进行 SUM/COUNT/AVG/MAX/MIN 聚合。

请求结构:

{
"game_id": "game_a",
"table": "t_l_log_player_login",
"filters": {
"record_date_start": "2026-05-12",
"record_date_end": "2026-05-19"
},
"group_by": ["count_date"],
"aggregates": {
"dau": "COUNT(DISTINCT openid)",
"login_times": "COUNT(*)",
"avg_level": "AVG(level)"
},
"limit": 30
}
字段类型必填说明
group_bylist[string]是(聚合模式)分组字段,支持 DATE(col)/HOUR(col) 等函数
aggregatesdict是(聚合模式)key=别名, value=SUM(col)/COUNT(col)/AVG(col)/MAX(col)/MIN(col)/COUNT(*)/COUNT(DISTINCT col)

group_by 支持的函数包装:

  • DATE(col) - 按天分组
  • HOUR(col) - 按小时分组
  • MONTH(col) - 按月分组
  • YEAR(col) - 按年分组
  • DAY(col) - 按日分组

响应:

{
"data": [
{ "count_date": "2026-05-12", "dau": "82300", "login_times": "215000", "avg_level": "38.5" },
{ "count_date": "2026-05-13", "dau": "84500", "login_times": "221000", "avg_level": "38.7" }
],
"row_count": 7,
"cached": false,
"sql": null
}

聚合查询返回的数值都是 string 类型(DW CSV 解析特性),调用方需自行 float() / int() 转换。

4.3 SDK 中台表自动隔离​

查询 sdk_database_new 中的表时,系统自动注入 app_id 过滤,调用方无需传入。详见 第 12 节。

4.4 cURL 示例​

# 明细:查某天某区服登录记录
curl -X POST 'http://dc.example.com:17030/api/v1/external/free-query' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_a",
"table": "t_l_log_player_login",
"columns": ["count_date", "openid", "roleid", "areaid", "level"],
"filters": {
"record_date_start": "2026-05-18",
"record_date_end": "2026-05-19",
"areaid": "1"
},
"limit": 100
}'

# 聚合:每日 DAU 趋势
curl -X POST 'http://dc.example.com:17030/api/v1/external/free-query' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_a",
"table": "t_l_log_player_login",
"filters": {
"record_date_start": "2026-05-01",
"record_date_end": "2026-05-19"
},
"group_by": ["count_date"],
"aggregates": {
"dau": "COUNT(DISTINCT openid)",
"total_logins": "COUNT(*)"
}
}'

# SDK 中台表聚合:每日充值金额
curl -X POST 'http://dc.example.com:17030/api/v1/external/free-query' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_b",
"table": "ads_sdk_charge_total_analyze",
"filters": {
"record_date_start": "2026-05-01",
"record_date_end": "2026-05-19",
"count_type": "1"
},
"group_by": ["count_date"],
"aggregates": {
"charge_amount": "SUM(charge_amount_cny)",
"charge_users": "SUM(charge_account_cnt)"
}
}'

4.5 Python 封装示例​

import requests
from typing import Optional

BASE = "http://dc.example.com:17030/api/v1/external"
HEADERS = {"X-API-Key": "<DC_API_KEY>", "Content-Type": "application/json"}


def free_query(
game_id: str,
table: str,
start: str,
end: str,
*,
columns: Optional[list[str]] = None,
extra_filters: Optional[dict] = None,
group_by: Optional[list[str]] = None,
aggregates: Optional[dict[str, str]] = None,
limit: int = 1000,
cache_ttl: int = 3600,
) -> dict:
"""自由查询 - 明细 & 聚合统一入口"""
filters = {"record_date_start": start, "record_date_end": end}
if extra_filters:
filters.update(extra_filters)

payload: dict = {
"game_id": game_id,
"table": table,
"filters": filters,
"limit": limit,
"cache_ttl": cache_ttl,
}
if columns:
payload["columns"] = columns
if group_by:
payload["group_by"] = group_by
if aggregates:
payload["aggregates"] = aggregates

resp = requests.post(f"{BASE}/free-query", headers=HEADERS, json=payload)
resp.raise_for_status()
return resp.json()


# ---- 用户活跃度示例 ----

# 示例 1: 查询每日 DAU + 新增登录次数(聚合模式)
result = free_query(
"game_a", "t_l_log_player_login",
"2026-05-12", "2026-05-19",
group_by=["count_date"],
aggregates={
"dau": "COUNT(DISTINCT openid)",
"login_times": "COUNT(*)",
},
)
print("=== 每日活跃度 ===")
for row in result["data"]:
print(f" {row['count_date']}: DAU={row['dau']}, 登录次数={row['login_times']}")

# 示例 2: 查某天高等级活跃用户明细(明细模式)
result = free_query(
"game_a", "t_l_log_player_login",
"2026-05-18", "2026-05-19",
columns=["openid", "roleid", "level", "areaid", "ip"],
extra_filters={"areaid": "1"},
limit=10,
)
print("\n=== 1区高活跃用户(最近登录)===")
for row in result["data"]:
print(f" roleid={row['roleid']}, level={row['level']}")

# 示例 3: 分时登录分布
result = free_query(
"game_a", "t_l_log_player_login",
"2026-05-18", "2026-05-19",
group_by=["HOUR(recorddate)"],
aggregates={
"login_count": "COUNT(*)",
"unique_users": "COUNT(DISTINCT openid)",
},
)
print("\n=== 分时登录分布 ===")
for row in result["data"]:
print(f" 时段{row['hour(recorddate)']}: 登录{row['login_count']}次, 独立用户{row['unique_users']}")

# 示例 4: 充值金额趋势
result = free_query(
"game_a", "t_l_log_charge",
"2026-05-01", "2026-05-19",
group_by=["count_date"],
aggregates={
"total_money": "SUM(money)",
"pay_orders": "COUNT(*)",
"pay_users": "COUNT(DISTINCT openid)",
},
)
print("\n=== 每日充值趋势 ===")
for row in result["data"]:
print(f" {row['count_date']}: 金额={row['total_money']}, 订单={row['pay_orders']}, 人数={row['pay_users']}")

5. 接口三:标准 SQL 查询​

POST /api/v1/external/sql-query​

直接写 SELECT 语句查询。SQL 引用指标虚拟表或公开 GA 表时走统一查询引擎/GA table executor;其他白名单外部表经过 7 层安全校验后通过 DW SparkSQL 执行。比 free-query 更灵活,支持 JOIN、子查询、复杂 WHERE 等。

请求参数​

{
"game_id": "game_a",
"sql": "SELECT count_date, COUNT(DISTINCT openid) AS dau, COUNT(*) AS login_times FROM game_a_database.t_l_log_player_login WHERE recorddate >= '2026-05-12' AND recorddate < '2026-05-19' GROUP BY count_date ORDER BY count_date LIMIT 30",
"use_cache": true
}
字段类型必填默认值说明
game_idstring是-业务标识
sqlstring是-SELECT 语句,最大 32768 字符
use_cachebool否true是否使用 L2 缓存

7 层安全校验​

层校验内容拦截示例
1SELECT-onlyINSERT/UPDATE/DELETE/DROP 等被拦截
2表名白名单只能查 dc_external_table 中注册的表
3危险关键字EXECUTE/CALL/GRANT/REVOKE 等被拦截
4recorddate 范围 <= 90 天WHERE 条件中日期范围强制限制
5LIMIT 自动注入无 LIMIT 自动补 LIMIT 10000
6SparkSQL 方言兼容适配 DW SparkSQL 特殊语法
7Schema Family 翻译逻辑字段名自动翻译为物理字段名

SQL 编写要点:

  • 表名需写完整路径:{database}.{table_name}(系统会自动校验该表是否属于该 game)
  • 时间过滤必须包含 recorddate >= 'YYYY-MM-DD' AND recorddate < 'YYYY-MM-DD'
  • SQL 必须是纯 SELECT 语句

响应​

{
"query_id": "sql_a1b2c3d4e5f6a7b8",
"game_id": "game_a",
"sanitized_sql": "SELECT count_date, COUNT(DISTINCT openid) AS dau ...",
"rows": [
["2026-05-12", "82300", "215000"],
["2026-05-13", "84500", "221000"]
],
"columns": ["count_date", "dau", "login_times"],
"row_count": 7,
"elapsed_ms": 1234.5,
"source": "dw"
}
字段说明
query_id查询标识(SQL SHA256 前 16 位)
sanitized_sql校验净化后的 SQL
rowslist[list] - 每行是值的列表
columns列名列表,与 rows 顺序对应
row_count结果行数
elapsed_ms执行耗时(毫秒),缓存命中时为 0
source"cache" 或 "dw"

与 free-query 不同,sql-query 返回 rows 是 list[list] 而非 list[dict],需配合 columns 按索引取列。

cURL 示例​

# 查询每日 DAU(SQL 模式)
curl -X POST 'http://dc.example.com:17030/api/v1/external/sql-query' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_a",
"sql": "SELECT count_date, COUNT(DISTINCT openid) AS dau FROM game_a_database.t_l_log_player_login WHERE recorddate >= '\''2026-05-12'\'' AND recorddate < '\''2026-05-19'\'' GROUP BY count_date ORDER BY count_date LIMIT 30",
"use_cache": true
}'

# 查付费用户等级分布
curl -X POST 'http://dc.example.com:17030/api/v1/external/sql-query' \
-H 'X-API-Key: <DC_API_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"game_id": "game_a",
"sql": "SELECT level, COUNT(*) AS user_count, SUM(money) AS total_money FROM game_a_database.t_l_log_charge WHERE recorddate >= '\''2026-05-01'\'' AND recorddate < '\''2026-05-19'\'' GROUP BY level ORDER BY level LIMIT 100",
"use_cache": false
}'

Python 封装示例​

def sql_query(
game_id: str,
sql: str,
use_cache: bool = True,
) -> list[dict]:
"""标准 SQL 查询,返回 list[dict]"""
resp = requests.post(
f"{BASE}/sql-query",
headers=HEADERS,
json={"game_id": game_id, "sql": sql, "use_cache": use_cache},
)
resp.raise_for_status()
result = resp.json()
# 将 list[list] + columns 转换为 list[dict]
return [dict(zip(result["columns"], row)) for row in result["rows"]]


# 示例:查询每日活跃 + 付费交叉分析
sql = """
SELECT
a.count_date,
a.dau,
COALESCE(b.pay_users, 0) AS pay_users,
COALESCE(b.total_revenue, 0) AS total_revenue,
ROUND(COALESCE(b.pay_users, 0) * 100.0 / a.dau, 2) AS pay_rate
FROM (
SELECT count_date, COUNT(DISTINCT openid) AS dau
FROM game_a_database.t_l_log_player_login
WHERE recorddate >= '2026-05-12' AND recorddate < '2026-05-19'
GROUP BY count_date
) a
LEFT JOIN (
SELECT count_date, COUNT(DISTINCT openid) AS pay_users, SUM(money) AS total_revenue
FROM game_a_database.t_l_log_charge
WHERE recorddate >= '2026-05-12' AND recorddate < '2026-05-19'
GROUP BY count_date
) b ON a.count_date = b.count_date
ORDER BY a.count_date
LIMIT 30
"""

rows = sql_query("game_a", sql)
for r in rows:
print(f"{r['count_date']}: DAU={r['dau']}, 付费人数={r['pay_users']}, "
f"收入={r['total_revenue']}, 付费率={r['pay_rate']}%")

6. 接口四:冷查询(异步)​

6.1 POST /api/v1/external/query - 提交冷查询​

基于预定义数据集(dc_dataset_catalog)提交异步查询任务。适用于定期报表场景。

{
"dataset_name": "game_a_login_summary",
"filters": {
"record_date_start": "2026-05-01",
"record_date_end": "2026-05-19"
},
"columns": ["count_date", "dau", "login_times"],
"limit": 1000
}
字段类型必填说明
dataset_namestring是预定义数据集名称(管理员在后台发布)
filtersdict是必须包含 record_date_start 和 record_date_end
columnslist[string]否不传则返回全部字段
limitint否默认 1000

响应 (202 Accepted):

{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"poll_url": "/api/v1/external/query/550e8400-e29b-41d4-a716-446655440000"
}

相同查询参数会命中 query_hash 去重,直接返回 "status": "completed"。

6.2 GET /api/v1/external/query/{job_id} - 轮询结果​

GET /api/v1/external/query/{job_id}

响应:

{
"status": "completed",
"row_count": 7,
"result_bytes": 2048,
"data": [
{ "count_date": "2026-05-12", "dau": 82300, "login_times": 215000 }
],
"cached": true,
"created_at": "2026-05-19T10:30:00",
"heartbeat_at": "2026-05-19T10:30:15",
"error_message": null,
"expires_at": "2026-05-19T11:30:00"
}

状态流转:

pending -> running -> completed / failed
v
cancelled (仅 pending/running 可取消)

6.3 POST /api/v1/external/query/{job_id}/cancel - 取消查询​

取消处于 pending 或 running 的任务。

Python 冷查询封装​

import time


def cold_query_and_wait(
dataset_name: str,
filters: dict,
*,
columns: Optional[list[str]] = None,
limit: int = 1000,
max_wait: float = 120.0,
) -> dict:
"""提交冷查询并等待结果"""
# 1. 提交
payload = {"dataset_name": dataset_name, "filters": filters, "limit": limit}
if columns:
payload["columns"] = columns

resp = requests.post(f"{BASE}/query", headers=HEADERS, json=payload)
resp.raise_for_status()
job = resp.json()

# 2. 轮询
poll_url = f"{BASE}/query/{job['job_id']}"
interval = 1.0
elapsed = 0.0

while elapsed < max_wait:
if job["status"] in ("completed", "failed", "cancelled"):
break

time.sleep(interval)
elapsed += interval
interval = min(interval * 1.5, 5.0)

poll_resp = requests.get(poll_url, headers=HEADERS)
poll_resp.raise_for_status()
job = poll_resp.json()

if job["status"] == "completed":
return job
elif job["status"] == "failed":
raise RuntimeError(f"冷查询失败: {job.get('error_message')}")
else:
raise TimeoutError(f"冷查询超时 ({max_wait}s): status={job['status']}")


# 使用
result = cold_query_and_wait(
"game_a_login_summary",
{"record_date_start": "2026-05-01", "record_date_end": "2026-05-19"},
)
for row in result["data"]:
print(row)

7. 接口五:异步导出​

7.1 POST /api/v1/external/export - 提交导出任务​

大数据量查询结果导出为 CSV/JSON 文件,上传到 对象存储 后返回下载 URL。适用于超过 10000 行的场景。

{
"game_id": "game_a",
"sql": "SELECT * FROM game_a_database.t_l_log_player_login WHERE recorddate >= '2026-05-01' AND recorddate < '2026-05-19'",
"format": "csv",
"filename": "login_20260501_0518"
}
字段类型必填说明
game_idstring是业务标识
sqlstring是SELECT 语句
formatstring否"csv" 或 "json",默认 csv
filenamestring否不含后缀,为空则自动生成

响应:

{
"task_id": "a1b2c3d4e5f6",
"status": "pending",
"message": "导出任务已提交,请通过 GET /api/v1/external/export/a1b2c3d4e5f6 查询进度"
}

7.2 GET /api/v1/external/export/{task_id} - 查询导出状态​

GET /api/v1/external/export/{task_id}

响应(completed):

{
"task_id": "a1b2c3d4e5f6",
"status": "completed",
"download_url": "https://cloud.example.com/exports/login_20260501_0518.csv?sign=...",
"expires_at": "2026-05-19T11:30:00",
"rows_exported": 250000,
"error_message": null,
"created_at": "2026-05-19T10:30:00",
"updated_at": "2026-05-19T10:35:00"
}

状态流程:

pending -> running -> completed (download_url 可获取)
-> failed (error_message 有值)

download_url 是 对象存储 预签名 URL,默认 1 小时有效。

Python 导出封装​

def export_and_download(
game_id: str,
sql: str,
output_path: str,
*,
format: str = "csv",
filename: str = "",
max_wait: float = 600.0,
) -> int:
"""提交导出任务,等待完成,下载到本地。返回导出行数。"""
# 1. 提交
resp = requests.post(
f"{BASE}/export",
headers=HEADERS,
json={"game_id": game_id, "sql": sql, "format": format, "filename": filename},
)
resp.raise_for_status()
task = resp.json()
task_id = task["task_id"]

# 2. 轮询
interval = 3.0
elapsed = 0.0
while elapsed < max_wait:
time.sleep(interval)
elapsed += interval
interval = min(interval * 1.5, 15.0)

resp = requests.get(f"{BASE}/export/{task_id}", headers=HEADERS)
resp.raise_for_status()
status = resp.json()

if status["status"] == "completed":
break
elif status["status"] == "failed":
raise RuntimeError(f"导出失败: {status.get('error_message')}")

# 3. 下载
download_url = status.get("download_url")
if not download_url:
raise RuntimeError("导出完成但无下载 URL")

file_resp = requests.get(download_url)
file_resp.raise_for_status()
with open(output_path, "wb") as f:
f.write(file_resp.content)

print(f"已下载: {output_path} ({len(file_resp.content)} bytes, {status['rows_exported']} 行)")
return status["rows_exported"]


# 导出最近一周所有登录日志
export_and_download(
"game_a",
"SELECT * FROM game_a_database.t_l_log_player_login WHERE recorddate >= '2026-05-12' AND recorddate < '2026-05-19'",
"./login_export.csv",
filename="login_weekly",
)

8. 接口选择决策树​

需要查什么?
│
├─ 查核心指标(DAU/收入/付费率/ARPU/在线峰值/留存)
│ -> POST /aggregate 首选,自动路由实时/历史,计算指标自动算
│
├─ 查明细数据 / 自定义聚合(按表查、灵活组合)
│ ├─ 需求简单(单表、等值过滤、基础聚合)
│ │ -> POST /free-query 简单 JSON 参数,无需写 SQL
│ │
│ └─ 需求复杂(JOIN、子查询、CASE WHEN、多条件组合)
│ -> POST /sql-query 直接写 SQL,7层安全校验
│
├─ 基于预定义数据集定期出报表
│ -> POST /query (冷查询,异步轮询)
│
└─ 导出大量数据(>10000 行)到文件
-> POST /export (异步导出 CSV/JSON,返回下载链接)

各接口对比一览​

维度aggregatefree-querysql-query冷查询export
响应速度<500ms1-10s1-10s分钟后级分钟后级
返回上限20 指标10000 行100000 行10000 行无限制
数据源Events+GAUQE/GA/DWUQE/GA/DWDWDW
缓存Redis L2Redis(可选)Redis L2Redis无
实时数据当日事件表指标模式可支持指标/GA 模式可支持否否
SQL 复杂度无无JOIN/子查询无JOIN/子查询
认证X-API-KeyX-API-KeyX-API-KeyX-API-KeyX-API-Key

9. 安全约束​

9.1 SQL 注入防护​

所有接口在 SQL 到达数据库之前都经过严格的白名单 + 正则校验:

输入类型校验规则
表名/列名^[a-zA-Z_][a-zA-Z0-9_]*$
日期YYYY-MM-DD + strptime 验证
聚合表达式`^(SUM
GROUP BY纯列名 或 `^(DATE
SQL 语句仅 SELECT,表名白名单,危险关键字拦截
字符串值单引号转义为 ''

不合法输入直接返回 400,不会到达 SQL 层。

9.2 通用约束​

约束项值
时间范围上限90 天
free-query limit1 - 10000
sql-query 自动 LIMIT10000(无 LIMIT 时自动注入)
DW 查询超时120 秒
结果缓存上限5 MB
cache_ttl 范围0 - 86400
SQL 最大长度32768 字符

10. 限流机制​

每个 API Key 受 每分钟请求数 限制(默认 10 次),所有对外接口共用限额。

限流失败响应:

HTTP 429 Too Many Requests
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715500800
{ "detail": "查询请求过于频繁,请稍后重试" }

建议: 调用方应在客户端实现重试逻辑,收到 429 后等待 Retry-After 秒再重试。


11. 错误码汇总​

状态码场景典型 detail
400参数验证失败"filters 必须包含 record_date_start 和 record_date_end"
400参数验证失败"时间范围不能超过 90 天"
400参数验证失败"SQL 校验失败: ..."
400不支持的指标"不支持的指标: xxx,可用: dau, revenue, ..."
401API Key 无效"Invalid or inactive query API Key"
403无权访问"API Key 无权查询 game_id='xxx'"
403不支持的操作"dataset scope 的 API Key 不支持自由查询"
404资源不存在"数据集 'xxx' 不存在或未发布"
404任务不存在"查询任务不存在"
404表不存在"表 'xxx' 在该业务绑定的数据库中不存在"
413结果过大"查询结果 xx 字节超过上限"
429限流"查询请求过于频繁,请稍后重试"
502DW 失败"DW 查询失败: ..."

12. SDK 中台表说明​

sdk_database_new 是所有业务共享的 SDK 数据库,通过 app_id 隔离。查询时 系统自动注入 app_id 过滤。

常用 SDK 表​

表名说明常用列
ads_sdk_charge_total_analyze充值汇总count_date, charge_amount_cny, charge_account_cnt, count_type
ads_sdk_register_account_analyze_total注册分析汇总count_date, register_account_cnt
ads_sdk_createrole_account_retention创角留存count_date, day1_retention_rate ~ day180_retention_rate
ods_sdk_login登录原始日志app_id, openid, event_time, ip, channel
ods_sdk_charge_success充值成功日志app_id, openid, order_amount, order_id
dim_sdk_app_timezone时区映射app_id, timezone

13. 各业务数据表​

13.1 业务标识​

业务game_id数据库
game_agame_agame_a_database
game_bgame_bgame_b_database
game_cgame_cgame_c_database
game_dgame_dgame_d_database

13.2 字段风格差异​

字段含义game_a/game_b 系列game_c/game_d
区服IDareaid (int)siteid (int)
设备号devicenodeviceid
充值金额money (double)chargemoney (double)
在线人数online (int)onlineplayer (int)
渠道channelidreg_channel
时间分区recorddate / count_daterecorddate / count_date

13.3 核心表速查​

表名用途game_a/game_bgame_c/game_d
t_l_log_player_login登录日志是是
t_l_log_player_logout登出日志是是
t_l_log_charge充值记录是是
t_l_log_createuser创建账号是-
t_l_log_createrole创建角色是是
t_l_log_player_register注册日志-是
t_l_log_reg_online在线人数是-
t_l_log_regonline在线人数-是
t_l_log_player_level等级变化是-
t_l_log_itemlog道具日志是-
t_l_log_item道具日志-是
t_l_log_money货币日志-是

14. 完整场景示例​

场景 1:运营日报(DAU + 收入 + 付费率 + 留存)​

import requests
from datetime import date, timedelta

BASE = "http://dc.example.com:17030/api/v1/external"
HEADERS = {"X-API-Key": "<DC_API_KEY>", "Content-Type": "application/json"}


def daily_report(game_id: str, days: int = 7):
"""生成运营日报"""
end = date.today()
start = end - timedelta(days=days - 1)

# 一站式获取核心指标
resp = requests.post(
f"{BASE}/aggregate",
headers=HEADERS,
json={
"game_id": game_id,
"metrics": [
"dau", "new_users", "revenue", "pay_users",
"pay_rate", "arpu", "arppu", "online_peak",
"retention_d1", "retention_d7",
],
"start_date": start.isoformat(),
"end_date": end.isoformat(),
},
)
resp.raise_for_status()
report = {m["metric"]: m["value"] for m in resp.json()["metrics"]}

print(f"=== {game_id} 运营日报 ({start} ~ {end}) ===")
print(f"DAU: {report['dau']:>10,.0f}")
print(f"新增用户: {report['new_users']:>10,.0f}")
print(f"收入: {report['revenue']:>10,.2f}")
print(f"付费人数: {report['pay_users']:>10,.0f}")
print(f"付费率: {report['pay_rate']:>10.2%}")
print(f"ARPU: {report['arpu']:>10.2f}")
print(f"ARPPU: {report['arppu']:>10.2f}")
print(f"在线峰值: {report['online_peak']:>10,.0f}")
print(f"次日留存: {report['retention_d1']:>10.2%}")
print(f"7日留存: {report['retention_d7']:>10.2%}")
return report


daily_report("game_a", 7)

场景 2:用户活跃度分析​

def user_activity_analysis(game_id: str, date_str: str):
"""分析某天用户活跃度"""
next_day = (date.fromisoformat(date_str) + timedelta(days=1)).isoformat()

# 1. 分时登录分布(用 free-query 聚合模式)
print("=== 分时登录分布 ===")
resp = requests.post(
f"{BASE}/free-query",
headers=HEADERS,
json={
"game_id": game_id,
"table": "t_l_log_player_login",
"filters": {
"record_date_start": date_str,
"record_date_end": next_day,
},
"group_by": ["HOUR(recorddate)"],
"aggregates": {
"login_count": "COUNT(*)",
"unique_users": "COUNT(DISTINCT openid)",
},
"limit": 24,
},
)
resp.raise_for_status()
for row in resp.json()["data"]:
hour = row["hour(recorddate)"]
bar = "#" * (int(float(row["login_count"])) // 1000)
print(f" {hour:>2}时: {bar} {row['login_count']}次 / {row['unique_users']}人")

# 2. 活跃用户等级分布
print("\n=== 活跃用户等级 Top 10 ===")
resp = requests.post(
f"{BASE}/free-query",
headers=HEADERS,
json={
"game_id": game_id,
"table": "t_l_log_player_login",
"filters": {
"record_date_start": date_str,
"record_date_end": next_day,
},
"group_by": ["level"],
"aggregates": {
"user_count": "COUNT(DISTINCT openid)",
"avg_level": "AVG(level)",
},
"limit": 10,
},
)
resp.raise_for_status()
for row in resp.json()["data"]:
print(f" Lv.{row['level']}: {row['user_count']}人")

# 3. 在线时长分析(登出记录)
print("\n=== 在线时长分布 ===")
resp = requests.post(
f"{BASE}/sql-query",
headers=HEADERS,
json={
"game_id": game_id,
"sql": f"""SELECT
CASE
WHEN online_time < 300 THEN '5分钟以下'
WHEN online_time < 1800 THEN '5-30分钟'
WHEN online_time < 3600 THEN '30分钟-1小时'
WHEN online_time < 7200 THEN '1-2小时'
ELSE '2小时以上'
END AS duration_range,
COUNT(*) AS user_count
FROM {_get_db(game_id)}.t_l_log_player_logout
WHERE recorddate >= '{date_str}' AND recorddate < '{next_day}'
GROUP BY 1 ORDER BY 1
LIMIT 10""",
},
)
resp.raise_for_status()
result = resp.json()
for row in result["rows"]:
print(f" {row[0]}: {row[1]}人")


def _get_db(game_id: str) -> str:
return {"game_a": "game_a_database", "game_b": "game_b_database"}.get(game_id, "")


user_activity_analysis("game_a", "2026-05-18")

场景 3:付费漏斗分析​

def payment_funnel(game_id: str, start: str, end: str):
"""付费漏斗:活跃 -> 付费 -> ARPPU"""
print(f"=== {game_id} 付费漏斗 ({start} ~ {end}) ===")

# 1. 总体指标
resp = requests.post(
f"{BASE}/aggregate",
headers=HEADERS,
json={
"game_id": game_id,
"metrics": ["dau", "pay_users", "pay_rate", "revenue", "arpu", "arppu"],
"start_date": start,
"end_date": end,
},
)
resp.raise_for_status()
kpis = {m["metric"]: m["value"] for m in resp.json()["metrics"]}

print(f"活跃用户: {kpis['dau']:,.0f}")
print(f" v 付费率 {kpis['pay_rate']:.2%}")
print(f"付费用户: {kpis['pay_users']:,.0f}")
print(f" v ARPPU ¥{kpis['arppu']:.2f}")
print(f"总收入: ¥{kpis['revenue']:,.2f}")

# 2. 每日付费趋势
resp = requests.post(
f"{BASE}/free-query",
headers=HEADERS,
json={
"game_id": game_id,
"table": "t_l_log_charge",
"filters": {"record_date_start": start, "record_date_end": end},
"group_by": ["count_date"],
"aggregates": {
"daily_revenue": "SUM(money)",
"pay_users": "COUNT(DISTINCT openid)",
"order_count": "COUNT(*)",
},
},
)
resp.raise_for_status()
print("\n=== 每日付费详情 ===")
for row in resp.json()["data"]:
revenue = float(row["daily_revenue"])
users = int(float(row["pay_users"]))
orders = int(float(row["order_count"]))
print(f" {row['count_date']}: ¥{revenue:>10,.2f} {users:>5}人 {orders:>5}单 "
f"人均¥{revenue/users:.2f}" if users else "N/A")


payment_funnel("game_a", "2026-05-12", "2026-05-18")

15. 附录​

A. 服务端配置参考​

参数默认值说明
RATE_LIMIT_EXTERNAL_QUERY_PER_MINUTE10每分钟每 Key 最大查询次数
EXTERNAL_QUERY_REDIS_DB2查询结果缓存 Redis db
EXTERNAL_QUERY_RESULT_TTL3600结果缓存 TTL(秒)
DW_CONCURRENT_QUERIES5DW 最大并发查询数
DW_QUERY_TIMEOUT60DW 查询超时(秒)

B. 客户端约束汇总​

参数最小值最大值默认值
limit (free-query/冷查询)1100001000
sql-query 自动 LIMIT-10000010000
cache_ttl0864003600
时间范围1 天90 天-
aggregate metrics120-
sql 长度132768-

C. 与 Cube.js 的对比​

特性Cube.jsaggregatefree-querysql-query冷查询
端点:17001/api/v1/load:17030/.../aggregate:17030/.../free-query:17030/.../sql-query:17030/.../query
认证头AuthorizationX-API-KeyX-API-KeyX-API-KeyX-API-Key
响应速度<500ms<500ms1-10s1-10s分钟后级
实时数据-当日事件表指标模式可支持指标/GA 模式可支持-
SQL 能力预定义 Cube无基础聚合完整 SQL预定义数据集
适用场景固定看板实时大盘探索分析复杂分析批量报表
加载评论中...