Repository navigation
ZH Pagination and Native Queries
当前版本为 2.1.2;本地及 GitHub CI 完整验证通过,仅通过 GitHub 分发;Maven Central 仍为 2.1.0。 使用这些修改须先从源码安装 2.1.2,见 2.1.2 发布说明。
本页涉及的输入校验、配置快照和初始化失败策略修复纳入 2.1.0;详见 2.1.0 变更记录。
第一页不传 cursorTime:
PageResult<AccrueRecord> firstPage = tgTemplate.query(AccrueRecord.class)
.database("tsdb")
.whereTag("source", "annotation-pojo")
.timeRange(startTime, endTime)
.orderByTimeAsc()
.limit(100)
.page();第二页传上一页返回的 nextCursorTime:
PageResult<AccrueRecord> secondPage = tgTemplate.query(AccrueRecord.class)
.database("tsdb")
.whereTag("source", "annotation-pojo")
.timeRange(startTime, endTime)
.cursorTime(firstPage.getNextCursorTime())
.orderByTimeAsc()
.limit(100)
.page();升序分页底层追加 time > cursorTime,降序分页底层追加 time < cursorTime。如果同一时间戳下有多条记录跨越页边界,下一页会跳过该时间戳剩余的记录。
IoTDB / InfluxDB 3 可使用 strictCursorPage();InfluxDB 1.x 不支持复合游标,应使用 offset 分页或保证时间戳唯一。
模板仅转换有限、精确为整数且处于有符号 long 范围的数值结果时间。例如 1.0 可用,1.9、NaN、无穷及大于 Long.MAX_VALUE 的整数均为无效候选。Instant、Date 和 ISO 时间保留毫秒分辨率。普通时间页保留配置时间列优先,再尝试 time、Time、_time、timestamp 的既有规则;前一个候选无法转换时继续回退。
hasNext=true 时,最后一个返回行必须通过上述既有候选提供有效续页时间;否则抛 QUERY_ERROR,避免返回 nextCursorTime=null 后下一次请求重复第一页。真实空页或普通时间分页的最后一页不需要续页边界。严格复合页保留更严格的规则:包括最后一页在内,每个返回行都必须具备全部物理游标键,物理时间无效时直接报 QUERY_ERROR,不会改用别名。
此处校验 adapter 返回结果的转换,不改变公开 cursorTime(Long) API、后端输入校验、时间戳精度或同时间多行限制。
本节适用于 IoTDB 和 InfluxDB 3;InfluxDB 1.x 调用 strictCursorPage() 会报 UNSUPPORTED_OPERATION。
第一页不传 cursor(...):
PageResult<AccrueRecord> firstPage = tgTemplate.query(AccrueRecord.class)
.database("tsdb")
.whereTag("source", "annotation-pojo")
.timeRange(startTime, endTime)
.orderByTimeAsc()
.limit(100)
.strictCursorPage();第二页传上一页返回的 nextCursor:
PageResult<AccrueRecord> secondPage = tgTemplate.query(AccrueRecord.class)
.database("tsdb")
.whereTag("source", "annotation-pojo")
.timeRange(startTime, endTime)
.cursor(firstPage.getNextCursor())
.orderByTimeAsc()
.limit(100)
.strictCursorPage();strictCursorPage() 默认使用 time + @TGTag 列作为复合游标。注解 POJO 示例的默认游标列是
time/device_code/point_type/source,底层会追加字典序条件,避免同一 time 下剩余 tag 行被下一页跳过。
严格游标也支持混合排序。例如:
PageResult<AccrueRecord> page = tgTemplate.query(AccrueRecord.class)
.orderByTimeDesc()
.thenByFieldAsc("device_code")
.thenByFieldDesc("point_type")
.cursor(previousCursor)
.limit(100)
.strictCursorPage();此时模板还会补齐缺失的 source DESC,下一页条件按每列方向生成字典序比较;只有前面全部排序列相等时才比较 source。
如果同时显式设置 cursorColumns(...),列及顺序必须与补齐前的自定义排序声明完全一致,随后一起补齐。
自动补齐同时作用于 SELECT、ORDER BY 和返回游标,手动指定 cursorColumns 也不能省略这些键。
var page = tgTemplate.query(AccrueRecord.class)
.orderByFieldAsc("value").limit(1).strictCursorPage();
// Break equal-value ties using value, time, device_code, point_type and source.
var next = tgTemplate.query(AccrueRecord.class)
.orderByFieldAsc("value").cursor(page.getNextCursor()).limit(1).strictCursorPage();升级前仅含自定义 FIELD 的旧游标缺少新增键,需从第一页重新获取;缺键会明确报错,不会继续执行可能漏数的分页。
有 TSDB 列注解的查询字段只匹配注解物理列名(兼容大小写),与写入保持一致;物理列为 NULL 时不会改取同名 Java 字段对应的其他列。 查询别名需要另一个承接类型时,使用无注解 DTO,可继续按同名、下划线转驼峰及常见时间别名匹配。
将返回的 nextCursor 原样传回。严格分页遵循当前后端的物理列名规则:InfluxDB 3 保留大小写,value、VALUE、Time、time 可以是不同列;IoTDB 非引号列名通过 Locale.ROOT 规范为小写。核心 SPI 的 normalizeColumnIdentifier 默认保留其他适配器的大小写。
执行 select("VALUE").orderByFieldDesc("value") 时,模板会在内部投影中补齐真实的 value,从该列精确生成游标,不会改取拼写相近列的值。后端结果缺少游标列报 QUERY_ERROR;游标键非法或显式游标列与排序不一致报 ARGUMENT_ERROR。必须包含实际时间键,普通 DTO 的宽松时间别名不能代替严格游标键。SELECT 补齐、稳定排序键、n+1 探测和查询限额继续生效;此规则与上面的普通 DTO 映射分别适用。
IoTDB 游标时间必须是 long 范围内的整数毫秒;允许 1.0,拒绝 1.9、NaN、无穷大与越界整数,并在执行 SQL 前报 ARGUMENT_ERROR。整数字符串与 Instant 保留既有毫秒语义;数字游标序列化推荐 Long,避免上游已经丢失精度。本变更不增加纳秒分页或跨页数据库快照。
严格分页返回的每一行(包括最后一页)都必须包含最终游标各列的非空值;否则模板返回 QUERY_ERROR,不会输出无法可靠续页的结果。InfluxDB 3 保留与 time 独立的真实 _time 和 TIME 字段,包括显式为 null 的 _time;只有键不存在时才补充兼容 _time 别名。输入游标键缺失、多余或规范化后重复均返回 ARGUMENT_ERROR。
null 游标 map 或真正的空 map 表示严格分页的第一页。非空 map 会保留原始条目直到物理键校验:null/空白键、null 值、多余键(包括值为 null 的多余键)、trim 或后端规范化后重名的键,都会在查询前返回 ARGUMENT_ERROR。例如 {time: null} 不会静默重启第一页,{time: 1, " time ": 2} 不会静默选择其中一个边界。InfluxDB 3 仍区分大小写不同的真实物理键,IoTDB 使用小写身份规则;应将返回游标原样传回。合法的键两侧空白在查询校验阶段规范化,不修改调用方查询,也不能隐藏非法条目或重名。直接调用 adapter 时,显式 cursorColumns 定义准确键集合;省略它仍遵循后端原有回退规则,不会自动获得实体元数据。
InfluxDB 3 提供后端级配置 tsdb.influxdb.strict-cursor-sql。默认 or 保持现有的单个字典序 OR 条件;显式 union-all 则把结构化严格游标续页生成一个含互斥分支的 SQL 语句。不传游标的 strictCursorPage() 第一页在两种模式下均使用相同的普通 SELECT。IoTDB 不受影响,InfluxDB 1.x 仍不支持严格复合游标。
以简化的两键 time ASC, device ASC 为例,游标 (2026-06-11T03:10:00Z, b) 后的下一页形态如下。示例带业务过滤条件,页大小为 2,因此外层上限包含一条探测记录:
SELECT "time", "device", "value" FROM (
SELECT "time", "device", "value" FROM "telemetry"
WHERE "source" = 'sensor' AND "time" > timestamp '2026-06-11T03:10:00Z'
UNION ALL
SELECT "time", "device", "value" FROM "telemetry"
WHERE "source" = 'sensor' AND "time" = timestamp '2026-06-11T03:10:00Z'
AND "device" > 'b'
) AS "__tsgate_cursor_rows"
ORDER BY "time" ASC, "device" ASC
LIMIT 3;各分支使用相同的列投影、表、原有业务条件和时间范围。第 i 个分支要求此前所有游标键相等,再按当前键的方向严格比较:ASC 使用 >,DESC 使用 <。例如 value DESC, device ASC, time DESC 的三个分支分别为 value < V、value = V AND device > D、value = V AND device = D AND time < T,每个分支都带上完整原有过滤条件。这同样适用于 FIELD 优先排序,时间键不必排在第一位。
分支互斥,因此 UNION ALL 保留原谓词的结果行,无需额外去重。完整的混合方向排序与分页限制都在合并之后执行;直接通过 TSDBQuery 指定的 offset 也放在合并后的外层,不会分别作用于各分支。能够由游标时间与显式 timeRange / startTime / endTime 边界证明为空的分支会被省略,避免发送互相矛盾的时间条件。原生探针在 Core 3.0.3 和 3.11.5 均复现了这类规划器错误,因此并非仅旧版需要处理。此剪枝不推断任意 QueryFilter 条件,也不改写原生 SQL;所有游标值仍须通过校验。全部分支为空时,仍向服务端发送带显式 FALSE 条件的查询,保留正常的数据库、生命周期与查询错误处理。模板仍会向投影、排序和返回游标补齐缺失的 time/tag 键,因此显式选列仍会包含模板补齐的键;直接使用 adapter 级 TSDBQuery 不会凭空获得实体元数据。对于这种直接查询,显式投影缺少的排序键只在 UNION 内部分支补齐,外层 SQL 仍保留调用方指定的投影,不会额外输出内部补齐的游标键。
每个续页仍只有一次 SQL 请求,但最终 m 个游标键最多生成 m 个分支,可能增加重复扫描和外层排序。建议限制时间范围、提高过滤选择性并使用适当页大小;返回页较小不代表服务端工作量较小。两种策略均保留 max-query-rows、单条下一页探测额度和 max-query-response-bytes,失败不会转为部分成功页。
union-all 模式会对严格游标与聚合的组合报 UNSUPPORTED_OPERATION,包括第一页。该配置不改写普通时间游标、list、计数、聚合、offset 或原生查询,也不是对原生 SQL 中任意 OR 表达式的通用修复。没有自动版本探测、策略切换或 HTTP 500 重试。应按兼容性与验证列出的准确服务端/策略组合选择。
两种策略都要求完整、非空的游标值和稳定的全序。游标需包含全部身份 tag;跨页期间的数据变更不受跨页快照保护。统一时间戳契约仍为毫秒,选择 union-all 不会增加纳秒精度游标支持。
PageResult<AccrueRecord> page = tgTemplate.query(AccrueRecord.class)
.database("tsdb")
.whereTag("source", "annotation-pojo")
.timeRange(startTime, endTime)
.orderByTimeAsc()
.page(pageNum, pageSize);
Long total = page.getTotal(); // Total matching rows
Long totalPages = page.getTotalPages(); // Total pages for the page sizeIoTDB / InfluxDB 3 的普通明细 offset 分页使用直接 COUNT(*),聚合/分组分页使用外层 COUNT(*) 统计分页前的聚合结果集。
InfluxDB 1.x 流式扫描对应查询的结果行计数,受 max-query-response-bytes 约束。时间游标分页和复合
游标分页不会执行总数查询,total、totalPages 均为 null。大数据深翻页建议优先使用游标分页,offset 分页更适合小结果集或
需要展示总数、总页数的管理类页面。
executeQuery(...) 用于原生查询,不接收 database 参数,也不改写业务 SQL 去补库名。
InfluxDB 1.x 的原生查询入口在发出请求前仅允许单条 SELECT、SHOW、EXPLAIN [ANALYZE] SELECT,可带一个末尾分号;拒绝裸 INTO、多语句以及修改/管理命令。引号中的关键字、分号和转义引号会正确保留。为避免词法歧义,该入口不接受注释、反引号或引号外 /(包括正则与除法),这些高级语法需直接使用借用的官方 client 并自行承担其读写语义。InfluxDB 1.x 的 GET /query 本身不会可靠阻止写操作,因此不能用 HTTP 方法代替此检查。
OpenGemini 适配沿用同一单语句 InfluxQL 入口限制。只有明确指定 measurement 的结构化统一 query(...)、count(...) 将精确的 measurement not found 查询错误归为空行或零;原生 executeQuery(...) 的单条语句可能涉及多个 measurement 或子查询,因此保留查询错误。借用兼容 client 的查询保留服务端错误 DTO,并绕过 adapter 限额。稳定的 org.influxdb.InfluxDB 代理仅为 ping() / version() 桥接真实 X-Geminidb-Version 响应头。新 measurement/tag series 在写入确认后的查询可见性是异步的;业务需要时,应设置截止时间并确认具体数据点可见后再分页。adapter 不透明重试空结果。详见 OpenGemini 接入指南。
原生 SQL 与链式查询均有适配层结果上限。原生查询超过 max-query-rows(默认 10000)直接抛 QUERY_ERROR,不会返回截断后的成功结果。
时间游标 / 复合游标分页为判断下一页可多读一条探测记录,普通 list 和 offset 分页没有额外行额度。业务 limit/pageSize 仍限制为 1~10000;实际结果还受所选后端 max-query-rows 约束。
IoTDB 逐行读取,超限会关闭结果句柄并归还 Session。InfluxDB 使用流式 JSON 解析,同时限制解压后的响应体为默认 16 MiB(含空白和语法字符),避免先把整个 HTTP body 载入内存。
失败时不会返回已收集的部分行;错误响应仅读取最多 8 KiB。建议在 SQL 中同时使用时间范围和 LIMIT,客户端保护不等同于数据库执行资源配额。
这些入口最终仍返回受限的 List;没有面向调用者的无限流式导出 API。直接使用官方 client 时由调用者自行控制分页、内存和关闭时机。
IoTDB 示例:
List<AccrueRecord> rows = tgTemplate.executeQuery(
"SELECT * FROM tsdb.ACCRUE "
+ "WHERE source = 'annotation-pojo' ORDER BY time DESC LIMIT 5",
AccrueRecord.class
);InfluxDB 示例:
List<AccrueRecord> rows = tgTemplate.executeQuery(
"SELECT * FROM \"ACCRUE\" WHERE \"source\" = 'annotation-pojo' ORDER BY time DESC LIMIT 5",
AccrueRecord.class
);不指定承接类型时返回 List<Map<String, Object>>:
List<Map<String, Object>> rows = tgTemplate.executeQuery(
"SELECT * FROM tsdb.ACCRUE LIMIT 5"
);常用原生 SQL 写法:
IoTDB 表模型常用写法:
-- List tables
SHOW TABLES FROM tsdb;
-- Describe the table
DESCRIBE tsdb.ACCRUE;
-- Query rows
SELECT time, device_code, point_type, source, value, status
FROM tsdb.ACCRUE
WHERE time >= 1781147400000
AND time <= 1781148600000
AND device_code = 'device_001'
ORDER BY time DESC
LIMIT 20;
-- LIMIT/OFFSET pagination
SELECT *
FROM tsdb.ACCRUE
WHERE source = 'annotation-pojo'
ORDER BY time ASC
LIMIT 100
OFFSET 200;
-- Aggregate fixed-offset time windows
SELECT date_bin(5m, time, 1970-01-01T00:00:00+08:00) AS window_start,
device_code,
AVG(value) AS avg_value,
MAX(value) AS max_value,
COUNT(value) AS sample_count
FROM tsdb.ACCRUE
WHERE time >= 1781147400000
AND time <= 1781148600000
GROUP BY date_bin(5m, time, 1970-01-01T00:00:00+08:00),
device_code
ORDER BY window_start ASC, device_code ASC
LIMIT 100;InfluxDB 3 Core 常用写法:
-- List tables
SHOW TABLES;
-- List columns
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'ACCRUE';
-- Query rows; quote case-sensitive InfluxDB table and column names
SELECT time, "device_code", "point_type", "source", "value", "status"
FROM "ACCRUE"
WHERE time >= timestamp '2026-06-11T03:10:00Z'
AND time <= timestamp '2026-06-11T03:30:00Z'
AND "device_code" = 'device_001'
ORDER BY time DESC
LIMIT 20;
-- LIMIT/OFFSET pagination
SELECT *
FROM "ACCRUE"
WHERE "source" = 'annotation-pojo'
ORDER BY time ASC
LIMIT 100
OFFSET 200;
-- Aggregate fixed-offset time windows
SELECT date_bin(interval '5 minutes', time, timestamp '1970-01-01T00:00:00+08:00') AS window_start,
"device_code",
AVG("value") AS "avg_value",
MAX("value") AS "max_value",
COUNT("value") AS "sample_count"
FROM "ACCRUE"
WHERE time >= timestamp '2026-06-11T03:10:00Z'
AND time <= timestamp '2026-06-11T03:30:00Z'
GROUP BY window_start, "device_code"
ORDER BY window_start ASC, "device_code" ASC
LIMIT 100;TsGate 2.1.0 包含独立的 OpenGemini adapter 与 starter。配置、默认引擎边界及准确版本测试见 OpenGemini 接入指南,发行说明见 2.1.0 发布说明。
TsGate · Wiki home · 文档首页 · Apache-2.0 · NOTICE
Compatibility claims apply only to documented capabilities and verified versions. 兼容性承诺仅适用于已列明的能力和已验证的版本。