离线导出 SQL 说明
本文说明离线导出 SQL 支持的能力与语法,用于指导在配置离线导出类任务(如分析型文本导出、敏感词导出上报)时填写「导出 SQL」。本文只聚焦 SQL 本身,不涉及导出任务如何配置。
1. 概述
离线导出 SQL 是一条类 SQL 的 SELECT 语句:它从 TcaplusDB 表的离线(每日备份)静态数据中筛选记录、投影并转换字段,导出为文本(字段间以 | 分隔)。
基本形态:
SELECT 列1, 列2, ... FROM 表名 [WHERE 过滤条件]
一个最小示例——从 PlayerInfo 表导出 uin、name 两列,仅保留 uin > 1000 的记录:
SELECT uin, name FROM PlayerInfo WHERE uin > 1000
几个要点:
- 导出的是静态备份数据,不是在线实时数据。
- 必须显式列出要导出的列,不支持
SELECT *。 - 字段间以
|分隔输出;字符串中的|和换行符会被替换为空格(设计列时需留意)。
与在线读写 SQL 的区别(重要,请勿混用)
本文的离线导出 SQL,与在线读写请求所使用的条件过滤/更新语法不完全一样(在线语法见《条件过滤和更新功能》《条件过滤和更新语法说明》)。
离线导出 SQL 是一条完整的
SELECT查询(包含字段投影、表达式转换和条件过滤);而在线读写只是请求上携带的condition/operation文本片段,不含SELECT投影。离线导出在 SELECT 列、函数、算术运算、CASE WHEN、数组展开等方面,能力更丰富。两者的差异小结见 §5。
2. 快速上手(高频能力)
2.1 SELECT 列
- 一级字段:直接写字段名,如
uin、name、openid。 - 列别名(
AS):为导出列指定名称,如c_binary AS bin。 - 常量:用类型函数提示常量类型,如
INT(0)、STRING('foo')。常用于补齐目标格式中的固定列。
SELECT uin, name, openid, INT(0) AS area_id, INT(9999) AS plat_id
FROM PlayerInfo
文本导出必须显式列出列(不支持
SELECT *),输出时各列以|分隔。
2.2 嵌套字段访问
TDR 表和 PB 表都支持多级嵌套字段访问,用 . 逐级深入:
- TDR 原生嵌套:
ReasonID.ID、single_struct.x,支持任意深度。 - PB 普通嵌套:
additional.card_brief_info.card_signature。 - PB map 元素:
model_data['basic'].basic_info.declaration,其中model_data是 map 类型、'basic'是 key。 - 数组元素:
all_type_array[1].aname、rstr[0],下标从 0 开始。
-- TDR 表的多级嵌套字段
SELECT UID, MailID, ReasonID.ID FROM tbMail WHERE ReasonID.ID > 0
-- PB 表的嵌套字段与 map 取值
SELECT
additional.card_brief_info.card_signature AS card_signature,
model_data['basic'].basic_info.guild_name AS guild_name
FROM GuildActorData
更多字段路径形态(按 PB 字段编号访问、bytes 字段内嵌 PB、选取整个复杂字段等)见 §3.2。
2.3 WHERE 过滤(常用)
仅导出满足条件的记录。常用:
- 比较:
>、>=、<、<=、=/==、!=/<>。 - 逻辑:
AND、OR、NOT,可用括号控制优先级。 - 模糊匹配:
LIKE、NOT LIKE,与 MySQL 一致,默认忽略大小写,%匹配任意字符串、_匹配单字符。 - 集合:
IN (...)、NOT IN (...)。
SELECT actor_id, guild_name
FROM GuildActorData
WHERE actor_id LIKE 'guild:%' AND actor_id NOT LIKE 'guild:%:%'
2.4 常用函数
| 函数 | 说明 | 示例 |
|---|---|---|
INT / UINT / FLOAT / STRING |
类型提示或转换 | STRING(openid)、UINT(platid) |
LENGTH |
字符串/bytes 的字节长度 | LENGTH(name) |
SUBSTRING |
取子串,SUBSTRING(s, begin[, len]) |
SUBSTRING(name, 0, 5) |
CONCAT |
字符串拼接 | CONCAT(name, '_', UINT(platid)) |
UPPER / LOWER |
转大写/小写 | UPPER(name) |
SIZE |
数组或 map 的元素个数 | SIZE(mailbox) |
SELECT uin, UPPER(name) AS name_upper, LENGTH(name) AS name_len
FROM PlayerInfo
WHERE LENGTH(name) > 0
完整函数清单见 §3.4。
3. 详细能力
3.1 SELECT 列表达式总览
每一个 SELECT 列都可以是下列形态之一,并可用 AS 命名:
- 常量:
INT(0)、STRING('x')。 - 字段路径:一级字段或嵌套字段,见 §3.2。
- 系统字段:如
__last_access_time__,见 §3.3。 - 函数调用:如
STRING(openid)、CONCAT(a, b),见 §3.4。 - 算术表达式:
+、-、*、/,如(base + bonus) * 2、total / 1024。 CASE WHEN表达式:见 §3.7。- 数组展开
EXPAND_ARRAY:见 §3.6。
3.2 字段路径(详解)
字段路径用来定位「取哪个字段」,可用于 SELECT 列和 WHERE 条件。一条路径里可以混用:字段名、. 逐级嵌套、['key']/[index] 容器取值,以及 PB 字段编号(tag)。
3.2.1 按字段名访问
| 形态 | 示例 | 说明 |
|---|---|---|
| 一级字段 | openid |
适用于所有表 |
| TDR 原生多级嵌套 | ReasonID.ID、single_struct.x、a.b.c.d |
任意深度,需要表的 TDR 结构定义可用 |
| PB 多级嵌套 | additional.card_brief_info.card_signature |
message 逐级访问 |
| PB map 元素 | str_map['k10'].aname(字符串 key)、int_map[20].aid(整型 key) |
['key']/[key] 定位 map 元素,可继续嵌套 |
| 数组元素 | all_type_array[1].aname、rstr[0] |
[index] 定位数组元素,下标从 0 开始 |
-- TDR 嵌套 + PB 嵌套 + map + 数组
SELECT
ReasonID.ID,
all_type.aname,
str_map['k10'].aname,
all_type_array[1].aname
FROM tbl
取数组/map 的全部元素并展开成多行,请用
EXPAND_ARRAY,见 §3.6。
3.2.2 按 PB 字段编号(tag)访问
PB 字段可以用其 tag 编号 代替字段名来访问,形如 a.1.2.3:把路径中的字段名换成对应的 PB tag 编号即可逐级深入。
$代表整条记录(根),从根按编号访问顶层字段:$.1、$.2、$.11.10。- 可与字段名、map、数组混用:
all_type.11.2、all_type_array[1].2、int_map[20].1、$.17.102.14['k20'].1、$.17.102.12[0].1。 - 默认按
uint64解析,请用类型函数/类型提示纠正:STRING($.2)、INT($.3)、FLOAT($.6)。 - 不依赖 proto 定义:即使没有提供该 PB 的 proto,也能按编号解析。
-- $ 是整条记录;$.1=id, $.2=name, $.3=i32, $.11.10=all_type.fix64, $.11.13=all_type.rinfo
SELECT $.1, STRING($.2), INT($.3), $.11.10, $.11.13 FROM tbl
应用场景:业务没有提供(完整的)proto 定义,或希望直接按编号取字段(schema-free)。
3.2.3 bytes 字段中内嵌的 PB 数据
很常见的一种情况:某个 bytes/char 字段里实际保存的是一段序列化的 PB 数据(例如 TDR 表用 char 字段存 PB,或 PB 表用 bytes 字段存 PB)。此时可以把它当作嵌套 message,继续按字段名或编号往里取,包括其中的嵌套结构、数组、map:
-- scalar_group.bytes_val 是一个内嵌 PB 的 bytes 字段
SELECT
scalar_group.bytes_val.1,
STRING(scalar_group.bytes_val.14['k10'].aname),
scalar_group.bytes_val.all_type_array[0].aid
FROM tbl
buffer 偏移:若 bytes 字段里真正的 PB 数据前面还有 N 个字节的其它内容(如长度头),用 字段[起始字节 - 结束字节] 跳过这 N 字节再解析,-1 表示末尾。例如 additional[16 - -1].3.5 表示 additional 从第 16 字节解析到末尾、再按编号取 .3.5。仅支持 TDR 表的一级字段。
3.2.4 选取整个复杂字段
可以直接 SELECT 一个结构体/message/数组/map 的整体字段(不指定内部子字段),如 single_struct、all_type、all_type_array、rstr。这类复杂字段会以编码形式输出(默认十六进制,导出时也可按需配置为 JSON 或 Base64)。
SELECT id, name, all_type, all_type_array FROM tbl
3.3 系统字段
下列系统字段不属于表定义,但可在 SELECT 和 WHERE 中直接引用:
| 系统字段 | 含义 | 时间类型 |
|---|---|---|
__last_access_time__ |
记录最后修改时间(秒级)。注意:字段名虽含 access,实际语义是最后修改时间 | 是 |
__ttl__ |
记录 TTL(秒) | 否 |
__version__ |
数据版本号 | 否 |
__index__ |
List 表元素的 index | 否 |
__app_id__ |
业务 ID | 否 |
__zone_id__ |
区 ID | 否 |
__make_data_time__ |
引擎数据构造的时间,即所导出的备份数据的生成时刻 | 是 |
__run_task_time__ |
导出任务开始运行的时刻 | 是 |
-- 导出 app/zone 标识,并按最后修改时间过滤
SELECT __app_id__, __zone_id__, uin, name
FROM PlayerInfo
WHERE __last_access_time__ > '2025-01-01'
应用场景:
- 用
__last_access_time__(最后修改时间)做活跃度过滤(如只导出近期有改动的记录)。 - 用
__version__导出数据版本,用__app_id__/__zone_id__标记数据来源。 - 用
__run_task_time__/__make_data_time__作为时间基准,配合时间函数判断/计算记录相对该时刻的新旧(见 §3.4)。
时间类型的系统字段可与字符串形式的时间比较,如
__last_access_time__ < '2024-01-01'、'2024-01-01 00:00:00'。
3.4 函数参考
函数名不区分大小写。下表按用途分组。
类型转换 / 类型提示
| 函数 | 参数 | 说明 |
|---|---|---|
INT / UINT / FLOAT / DOUBLE / STRING / BYTES |
1 | 转换为目标类型,或为常量/字段标注类型 |
INT8 / INT16 / INT32 / INT64 / UINT8 / UINT16 / UINT32 / UINT64 |
1 | 整型位宽类型提示,常用于按 PB tag 访问或解析原始 bytes 时指明解释方式(默认 uint64) |
字符串函数
| 函数 | 参数 | 说明 |
|---|---|---|
LENGTH |
1 | 字符串/bytes 的字节长度 |
SUBSTRING |
2~3 | SUBSTRING(s, begin[, len]),省略 len 取到末尾 |
CONCAT |
1~100 | 拼接多个值为字符串(任一参数为空值则结果为空值) |
UPPER / UCASE |
1 | 转大写 |
LOWER / LCASE |
1 | 转小写 |
TRIM / LTRIM / RTRIM |
1 | 去除首尾/左侧/右侧空白 |
REVERSE |
1 | 字符串反转 |
STRCMP |
2 | 按字典序比较两个字符串,返回 -1/0/1 |
数学函数与算术
| 函数 / 运算符 | 参数 | 说明 |
|---|---|---|
+ - * / |
— | 算术运算,可用于 SELECT 列和 WHERE 比较中,如 total / 1024 |
ABS |
1 | 绝对值 |
MOD |
2 | 取模,MOD(a, b) |
LEAST |
2 | 取较小值 |
GREATEST |
2 | 取较大值 |
时间函数(较少用)
| 函数 | 参数 | 说明 |
|---|---|---|
NOW / CURRENT_TIMESTAMP |
0 | 当前时间 |
UNIX_TIMESTAMP |
0 | 当前时间的 Unix 秒 |
DATEDIFF |
2 | 两个时间相差的天数 |
TIMEDIFF |
2 | 两个时间相差的秒数 |
时间函数应用场景:配合时间类型字段计算记录的"年龄"。例如以导出任务运行时刻为基准,计算记录距最后修改相差的天数:
DATEDIFF(__run_task_time__, __last_access_time__)。
容器与展开
| 函数 | 参数 | 说明 |
|---|---|---|
SIZE |
1 | 数组或 map 的元素个数,如 SIZE(mailbox) |
EXPAND_ARRAY |
1~2 | 展开数组/map 成多行,见 §3.6 |
函数可以嵌套、可以与字段和算术组合使用:
SELECT
uin,
CONCAT(UPPER(name), '_', STRING(platid)) AS tag,
LENGTH(SUBSTRING(name, 0, 8)) AS prefix_len
FROM PlayerInfo
WHERE ABS(score) > 100 AND MOD(uin, 100) = 0
3.5 WHERE 过滤(完整)
WHERE 支持的运算符:
- 比较:
==/=、!=/<>、<、<=、>、>=。整型与浮点可跨精度比较;字符串按字典序比较;数值与字符串之间不可比较。 - 逻辑:
AND、OR、NOT,可用括号分组。优先级从高到低为比较→NOT→AND→OR。 - 位运算:
&、~、<<、>>。 - 集合:
IN (...)、NOT IN (...)。 - 模糊匹配:
LIKE、NOT LIKE(仅字符串字段,默认忽略大小写,%/_通配)。 - 数组包含:
array CONTAINS (子条件)、array NOT CONTAINS (子条件),子条件中用$引用当前数组元素。 - 算术表达式参与比较:如
c_int32 / 1024 = 0。 - WHERE 中也可使用 §3.4 的全部函数。
-- 位运算:判断 flag 第 4 位(值 8)是否置位
SELECT uin, flag FROM PlayerInfo WHERE flag & 8
-- 数组包含:mailbox 中存在 title='tcaplus' 且未读的元素
SELECT uin FROM PlayerInfo WHERE mailbox CONTAINS (title = 'tcaplus' AND read = 0)
-- 标量数组:gameids 不包含 101
SELECT uin FROM PlayerInfo WHERE gameids NOT CONTAINS ($ = 101)
应用场景:
- 位运算常用于 flag/标志位判断,如
flag & 8判断某个标志位是否置位。 CONTAINS常用于判断数组(结构体数组用字段名,标量数组用$)中是否存在满足条件的元素。
关键字冲突:若字段名与 SQL 关键字(如
key、like、update)相同,需用反引号转义,如`key` > 100。关键字集合参考 MySQL 5.7 Keywords。
3.6 数组展开(EXPAND_ARRAY,高级)
某些场景下,除了导出一级字段,还要把一个数组(PB repeated 字段或 map)里的每个元素都导出,并与一级字段联合成多行——相当于关系数据库中数组表与主表的 JOIN。
语法:EXPAND_ARRAY(数组路径 [, $.子字段])
- 第 1 个参数是数组(或 map)字段路径。
- 第 2 个参数可选,用
$引用当前元素,$.子字段取元素的嵌套字段;省略则元素整体作为一列。 - 一条记录若有 N 个数组元素,展开后生成 N 行。
- 同一条 SQL 中多次调用
EXPAND_ARRAY,展开的必须是同一个数组。 - 数组路径与
$.子字段同样支持按 PB 字段编号访问(见 §3.2.2),如EXPAND_ARRAY($.12, $.1)。
示例:PlayerInfo 表中 section_data_5 是 PB(DBPlayerPetInfo),其 pet_info.pet_data 是 repeated PetData(含 gid、name)。把每个精灵展开为一行,与玩家一级字段联合:
SELECT
EXPAND_ARRAY(section_data_5.pet_info.pet_data, $.name) AS pet_name,
STRING(openid) AS openid,
UINT32(platid) AS platid,
EXPAND_ARRAY(section_data_5.pet_info.pet_data, $.gid) AS gid
FROM PlayerInfo
若某玩家有 N 个精灵,则该玩家展开为 N 行(精灵的 name/gid 与玩家的 openid/platid 联合)。
map 也支持展开,$.1 取 key、$.2 取 value(value 可继续取嵌套字段 $.2.aid):
SELECT
id,
EXPAND_ARRAY(int_map, $.1) AS map_key,
EXPAND_ARRAY(int_map, $.2.aid) AS aid
FROM tbl
EXPAND_ARRAY 也可用于 WHERE,对展开后的元素做过滤:
SELECT id, EXPAND_ARRAY(all_type_array, $.aid) AS aid
FROM tbl
WHERE EXPAND_ARRAY(all_type_array, $.aid) > 150
3.7 CASE WHEN(高级)
支持类似 MySQL 的 CASE WHEN ... THEN ... [ELSE ...] END,用于把字段值映射成另一组值:
SELECT uin, name,
CASE WHEN level > 100 AND level <= 200 THEN 0
WHEN level > 200 AND level <= 500 THEN 1
WHEN level > 500 AND level <= 1000 THEN 2
ELSE 9999 END AS level_group
FROM PlayerInfo WHERE uin > 1000
可与函数结合:
SELECT uin,
CASE WHEN LENGTH(name) > 5 THEN 'long' ELSE 'short' END AS name_len
FROM PlayerInfo
4. 综合示例
示例 1:TDR 内嵌 PB 的多级取值
TDR 表 PlayerInfo 的 additional 字段保存的是 PB(PlayerAdditionalData),要导出其中的 card_brief_info.card_signature,并补齐若干固定列:
SELECT
additional.card_brief_info.card_signature AS card_signature,
STRING(openid) AS openid,
UINT32(platid) AS platid,
INT(0) AS area_id,
INT(9999) AS f5
FROM PlayerInfo
示例 2:map 嵌套取值 + 过滤
SELECT
model_data['basic'].basic_info.declaration AS declaration,
model_data['basic'].basic_info.guild_name AS guild_name,
model_data['basic'].basic_info.guild_id AS guild_id
FROM GuildActorData
WHERE actor_id LIKE 'guild:%' AND actor_id NOT LIKE 'guild:%:%'
示例 3:EXPAND_ARRAY 多行展开
SELECT
EXPAND_ARRAY(section_data_5.pet_info.pet_data, $.name) AS pet_name,
STRING(openid) AS openid,
EXPAND_ARRAY(section_data_5.pet_info.pet_data, $.gid) AS gid
FROM PlayerInfo
示例 4:按 PB 字段编号访问(无需 proto)
未提供 proto 时,用 $(整条记录)+ tag 编号直接取字段,并用类型函数指明类型:
SELECT
$.1 AS id,
STRING($.2) AS name,
$.11.10 AS fix64
FROM tbl
WHERE $.3 > 1000
5. 与在线条件过滤语法的差异
| 维度 | 离线导出 SQL(本文) | 在线条件过滤 / 更新 |
|---|---|---|
| 形态 | 完整 SELECT 查询(投影 + 转换 + 过滤) |
请求上携带的 condition / operation 文本片段 |
| 字段投影(SELECT 列、别名、常量) | 支持 | 不涉及 |
算术运算、CASE WHEN |
支持 | 不支持 |
数组展开 EXPAND_ARRAY |
支持 | 不支持 |
| 函数 | 类型/字符串/数学/时间等丰富函数 | 仅 size/length/substring/类型转换等 |
| 过滤运算符 | 比较、逻辑、位运算、IN、LIKE、CONTAINS、算术比较 |
风格相近,但能力不完全一致 |
| 数据 | 静态备份数据 | 在线实时数据 |
在线语法详见《条件过滤和更新功能》与《条件过滤和更新语法说明》。
6. 注意事项与限制
- 文本导出必须显式列出 SELECT 列,不支持
SELECT *。 - 字段名命中 SQL 关键字(
key、like、update等)时,需用反引号转义,如`key`。 - 输出以
|分隔;字符串中的|与换行符会被替换为空格。 - 解析嵌套字段需要表的结构定义可用:TDR 嵌套需要 TDR 结构定义、PB 嵌套需要对应 proto;若改用 PB 字段编号(tag)访问,则可不依赖 proto(见 §3.2.2)。
- 一条字段路径中的容器下标(数组
[i]/ map['k'])一般只出现一次;要遍历数组的全部元素,请用EXPAND_ARRAY。 - 字符串/数学等函数遵循空值传播:任一参数为空值(NULL)时,结果为空值。
7. 相关文档
- 《条件过滤和更新功能》:在线读写的条件过滤/更新能力。
- 《条件过滤和更新语法说明》:在线条件/更新的类 SQL 语法定义。
- 《Schema Free》:基于 PB map +
FieldSet的 schema-free 实践。 - 《敏感词导出和上报》:离线导出 SQL 的一个典型应用场景。