1.10.1. 概述

时序库统一操作 API 提供了一套 跨时序数据库的统一访问与写入抽象, 用于屏蔽不同后端在连接方式、写入模型、SQL 使用方式以及查询结果处理上的差异。

当前支持的后端包括:

  • InfluxDB 1.x(jdbc:influxdb:

  • ClickHouse(jdbc:clickhouse:

同时支持通过统一 SQL(Unisql)机制, 在查询与写入阶段使用一套统一的 SQL 语法, 由框架在运行时自动适配并下发至目标时序数据库。

该 API 的目标是提供一套 稳定、统一、接近 JDBC 使用体验的时序操作接口, 重点解决跨时序库使用成本高、SQL 与结果处理不一致的问题。

1.10.2. 快速开始

本节通过一个完整示例,展示典型使用流程:

  • 创建数据源

  • 获取客户端(类似 JDBC Connection)

  • 使用预编译 SQL 写入数据(支持绑定变量)

  • 查询并处理统一结果集

1.10.2.1. Maven 依赖

1.10.2.1.1. 基础依赖

统一操作 API 由 com.hundsun.lightdb:tsdb 提供, 并依赖统一 SQL 转换运行时组件:

<dependency>
    <groupId>com.hundsun.lightdb</groupId>
    <artifactId>tsdb</artifactId>
    <version>${unisql.version}</version>
</dependency>
<dependency>
    <groupId>com.hundsun.lightdb</groupId>
    <artifactId>sql-convert-runtime</artifactId>
    <version>${unisql.version}</version>
</dependency>

1.10.2.1.2. 原生时序库依赖(必需)

统一操作 API 不会内置具体时序库的原生客户端。 使用方必须根据实际后端 显式引入对应的原生 API 依赖, 否则在运行期将无法正常使用。

1.10.2.1.2.1. InfluxDB 1.x

<dependency>
    <groupId>org.influxdb</groupId>
    <artifactId>influxdb-java</artifactId>
    <version>2.25</version>
</dependency>

1.10.2.1.2.2. ClickHouse

<dependency>
    <groupId>com.clickhouse</groupId>
    <artifactId>clickhouse-jdbc</artifactId>
    <version>0.9.6</version>
</dependency>

1.10.2.2. 创建数据源

TimeSeriesDataSource 采用 接口抽象 的方式定义, 具体实现可以支持:

  • 连接池化

  • 统一配置管理

  • 与 JDBC DataSource 类似的生命周期管理

当需要启用统一 SQL 时, 通过 jdbc:unisql: 前缀并结合 targetDialect 参数指定目标时序库。

TimeSeriesDataSource dataSource = new TimeSeriesManagerDataSource();
dataSource.setUrl(
    "jdbc:unisql:influxdb:127.0.0.1:8086?targetDialect=influxdb1"
);
dataSource.setDatabase("example_db");
dataSource.setUsername("admin");
dataSource.setPassword("admin");

1.10.2.3. 获取客户端

获取 TimeSeriesClient 的方式与 JDBC 获取 Connection 类似, 使用完成后必须关闭,以释放底层资源。

try (TimeSeriesClient client = dataSource.getClient()) {
    // 与 JDBC Connection 类似,使用完成后需要关闭
}

1.10.2.4. 使用预编译 SQL 写入时序数据

时序数据的写入通过 标准 SQL + 预编译语句 完成。

SQL 中可以使用 ? 占位符, 通过 TimeSeriesPreparedStatement 进行参数绑定。

示例:写入一条时序数据。

String insertSql =
    "INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather(time, region, temperature, humidity) " +
    "VALUES (?, ?, ?, ?)";

try (TimeSeriesClient client = dataSource.getClient();
     TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {

    ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    ps.setString(2, "east");
    ps.setDouble(3, 23.5);
    ps.setInt(4, 60);

    ps.executeUpdate();
}

说明:

  • SQL 使用统一形式描述写入语义

  • 时间戳通过 setTimestamp 显式指定时间单位

  • 参数索引从 1 开始,行为与 JDBC PreparedStatement 一致

  • INSERT 语句必须有配套的 HINT ,用于指定列类型 INSERT SQL 规则

  • 所有 ``setXxx()`` 方法均不允许传入 ``null`` 值,否则抛出 ``IllegalArgumentException``

ClickHouse 自动建表机制

ClickHouse 后端支持自动建表和添加列功能,简化用户的使用流程:

  • 自动创建表:当表不存在时,根据 INSERT 语句和 hint 中的 ENGINE/PARTITION/ORDER_BY 信息自动创建表

  • 自动添加列:当表已存在但缺少某些列时,通过 ALTER TABLE 自动添加缺失的列

  • 列类型推断:根据 setXxx() 调用的参数类型自动推断列类型(如 setInt() 推断为 Int64)

  • NULLable 处理:TAG 和 TIMESTAMP 列自动声明为非 NULLable,FIELD 列自动声明为 NULLable

自动建表示例

// 第一次写入,表不存在,自动创建
String insertSql =
    "INSERT /*+ TAGS(region) FIELDS(temperature) TIMESTAMP(time)
               ENGINE(MergeTree())
               PARTITION(toDate(time))
               ORDER_BY(time) */ INTO sensor_data
    (time, region, temperature) VALUES (?, ?, ?)";

try (TimeSeriesClient client = dataSource.getClient();
     TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {

    ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    ps.setString(2, "east");
    ps.setDouble(3, 25.3);

    ps.executeUpdate();  // 自动创建表并写入数据
}

添加新列示例

// 表已存在,但新增了 humidity 字段
String insertSql =
    "INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time)
               ENGINE(MergeTree())
               PARTITION(toDate(time))
               ORDER_BY(time) */ INTO sensor_data
    (time, region, temperature, humidity) VALUES (?, ?, ?, ?)";

try (TimeSeriesClient client = dataSource.getClient();
     TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {

    ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    ps.setString(2, "east");
    ps.setDouble(3, 25.3);
    ps.setInt(4, 60);  // humidity 是新增列

    ps.executeUpdate();  // 自动添加 humidity 列并写入数据
}

注意事项

  • 自动建表和添加列功能仅对 ClickHouse 后端生效

  • ALTER TABLE 添加列可能因类型不兼容而失败,此时会抛出异常

  • 多线程并发插入同一表时,使用表级锁保证操作的线程安全

  • 元数据缓存的轻微不一致是可以接受的,仅影响自动 alter 的性能**

参数绑定限制示例

// 错误示例:绑定 null 会抛出异常
ps.setString(1, null);  // 抛出 IllegalArgumentException

// 正确做法:如果字段不传,直接在 INSERT 语句中不包含该列
String insertSql = "INSERT /*+ TAGS(region) FIELDS(value) TIMESTAMP(time) */ INTO table (region, value, time) VALUES (?, ?, ?)";

1.10.2.5. 时间戳处理方式

时序库统一操作 API 中的时间戳处理遵循以下规则:

统一 UTC 时间戳

  • 所有时间戳参数(setTimestamp)和时间戳返回值(getTimestampNanos)均使用 UTC 时区 的时间戳

  • 时间戳以纳秒(nanoseconds)为单位,从 1970-01-01T00:00:00Z 开始计算

  • 不同后端的时区差异由框架内部自动转换,用户无需关心

写入时的时间戳

写入时使用 setTimestamp(int parameterIndex, long timestamp, TimeUnit unit) 方法:

// 使用毫秒时间戳写入
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);

说明:

  • parameterIndex:参数索引,从 1 开始

  • timestamp:时间戳值

  • unit:时间单位(如 TimeUnit.MILLISECONDS、TimeUnit.SECONDS)

  • 框架会根据传入的 timeUnit 自动转换为纳秒存储

查询时的时间戳

查询时使用 getTimestampNanos(String columnName) 方法:

try (TimeSeriesResultSet rs = ps.executeQuery()) {
    while (rs.next()) {
        long timestampNanos = rs.getTimestampNanos("time");

        // 根据需要转换为毫秒或其他单位
        long timestampMillis = timestampNanos / 1_000_000;

        // 转换为 LocalDateTime(UTC 时区)
        Instant instant = Instant.ofEpochSecond(
            timestampNanos / 1_000_000_000,
            timestampNanos % 1_000_000_000
        );
        LocalDateTime utcDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
    }
}

说明:

  • 返回值为纳秒级 UTC 时间戳

  • 用户可根据需要进行单位转换或格式化

  • 不同后端返回的时间戳格式由框架统一处理

时区处理建议

  • 建议始终使用 UTC 时区进行时间戳的存储和查询

  • 如需显示其他时区时间,可在应用层进行转换

  • 避免在不同时区之间进行隐式转换,以免引起混淆

示例:完整的时区转换

// 写入:将本地时间转换为 UTC 时间戳后写入
LocalDateTime localTime = LocalDateTime.now();
Instant utcInstant = localTime.atZone(ZoneId.systemDefault()).toInstant();
long timestampNanos = utcInstant.getEpochSecond() * 1_000_000_000 + utcInstant.getNano();

ps.setTimestamp(1, timestampNanos, TimeUnit.NANOSECONDS);

// 查询:读取 UTC 时间戳后转换为本地时区显示
long readTimestampNanos = rs.getTimestampNanos("time");
Instant readInstant = Instant.ofEpochSecond(
    readTimestampNanos / 1_000_000_000,
    readTimestampNanos % 1_000_000_000
);
LocalDateTime localDisplayTime = LocalDateTime.ofInstant(readInstant, ZoneId.systemDefault());

1.10.2.6. 批量操作支持

时序数据库预编译语句支持批量插入操作,仅限 INSERT 语句使用。 通过 addBatch() executeBatch()clearBatch() 方法实现批量操作。

示例:批量写入多条时序数据。

String insertSql =
    "INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather(time, region, temperature, humidity) "
    "VALUES (?, ?, ?, ?)";

try (TimeSeriesClient client = dataSource.getClient();
     TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {

    // 第一条记录
    ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    ps.setString(2, "east");
    ps.setDouble(3, 23.5);
    ps.setInt(4, 60);
    ps.addBatch();  // 添加到批量缓存

    // 第二条记录
    ps.setTimestamp(1, System.currentTimeMillis() + 1000, TimeUnit.MILLISECONDS);
    ps.setString(2, "west");
    ps.setDouble(3, 24.1);
    ps.setInt(4, 55);
    ps.addBatch();  // 添加到批量缓存

    // 执行批量操作
    int[] results = ps.executeBatch();
}

说明:

  • 仅 INSERT 语句支持批量操作

  • 使用 addBatch() 将当前参数加入批量缓存

  • 使用 executeBatch() 执行所有批量语句

  • 使用 clearBatch() 可清空批量缓存

  • 返回结果为每条语句的影响行数数组

1.10.2.7. 查询与结果处理

查询同样通过 prepareStatement 执行, 查询结果以 统一结果集 TimeSeriesResultSet 返回。

示例如下:

String querySql =
    "SELECT time, temperature, humidity " +
    "FROM weather WHERE region = ?";

try (TimeSeriesClient client = dataSource.getClient();
     TimeSeriesPreparedStatement ps = client.prepareStatement(querySql)) {

    ps.setString(1, "east");

    try (TimeSeriesResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            long time = rs.getTimestampNanos("time");
            double temperature = rs.getDouble("temperature");
            int humidity = rs.getInt("humidity");

            // 业务处理
        }
    }
}

结果说明:

  • 返回标准的 TimeSeriesResultSet 接口,接近 JDBC 规范

  • 使用 next() 遍历结果集

  • 通过列名或列索引获取字段值

  • 使用完成后必须关闭 TimeSeriesResultSet

1.10.2.8. 完整案例

下面给出一个 完整、可运行的示例,演示:

  • 使用统一 SQL 创建 InfluxDB 1.x 数据源

  • 写入一条时序数据

  • 查询并解析统一结果集

TimeSeriesDataSource dataSource = new TimeSeriesManagerDataSource();
dataSource.setUrl("jdbc:unisql:influxdb:127.0.0.1:8086?targetDialect=influxdb1");
dataSource.setDatabase("example_db");
dataSource.setUsername("admin");
dataSource.setPassword("admin");

try (TimeSeriesClient client = dataSource.getClient()) {

    // =========================
    // 1. 写入时序数据
    // =========================

    String insertSql =
        "INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather(time, region, temperature, humidity) " +
        "VALUES (?, ?, ?, ?)";

    try (TimeSeriesPreparedStatement ps =
             client.prepareStatement(insertSql)) {

        ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        ps.setString(2, "east");
        ps.setDouble(3, 25.3);
        ps.setInt(4, 58);

        ps.executeUpdate();
    }

    // =========================
    // 2. 查询时序数据
    // =========================

    String querySql =
        "SELECT time, temperature, humidity FROM weather WHERE region = ?";

    try (TimeSeriesPreparedStatement ps =
             client.prepareStatement(querySql)) {

        ps.setString(1, "east");

        try (TimeSeriesResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                long time = rs.getTimestampNanos("time");
                double temperature = rs.getDouble("temperature");
                int humidity = rs.getInt("humidity");

                // 业务处理
            }
        }
    }
}

1.10.3. 统一 SQL(Unisql)使用说明

统一 SQL(Unisql)用于在 SQL 层面 屏蔽不同后端时序库的语法差异, 使应用能够使用一套统一的 SQL 描述完成写入与查询操作。

1.10.3.1. 启用方式

统一 SQL 通过 JDBC URL 前缀 启用:

  • 使用 jdbc:unisql: 作为 URL 前缀

  • 通过 targetDialect 参数指定目标时序库类型

示例:

  • InfluxDB 1.x

    jdbc:unisql:influxdb:host:port?targetDialect=influxdb1

  • ClickHouse

    jdbc:unisql:clickhouse:host:port?targetDialect=clickhouse

1.10.3.2. 使用限制与注意事项

  • 统一 SQL 对 prepareStatementSELECT 语句生效,其余语句不转换

  • SQL 在执行前会根据 targetDialect 自动转换

  • 返回结果统一为 TimeSeriesResultSet

  • 并非所有底层特性都具备完全等价的统一表达形式

  • 运行期切换 database 或 retentionPolicy 的机制并不是所有后端都支持

建议将连接级别的上下文配置放置在 DataSource 初始化阶段完成。

1.10.4. 数据类型说明

本节说明 Java API 中 TimeSeriesPreparedStatementTimeSeriesResultSet 支持的数据类型, 以及各后端数据库支持的具体数据类型。

1.10.4.1. Java API 方法支持的数据类型

TimeSeriesPreparedStatement setXxx() 方法

方法

InfluxDB 数据类型

ClickHouse 数据类型

setString(int, String)

所有字符串类型:String, tag/field 字符串

String

setInt(int, int)

整数(在 Influx 中为 numeric field)

Int64

setLong(int, long)

64 位整数或时间戳表示

Int64

setShort(int, short)

小范围整数

Int64

setFloat(int, float)

浮点数(单精度)

Float64

setDouble(int, double)

浮点数(双精度)

Float64

setBoolean(int, boolean)

布尔或数值表示的布尔字段

Bool (底层为 UInt8)

setTimestamp(int, long, TimeUnit)

时间戳(以纳秒/毫秒等传入,框架转换为纳秒)

DateTime, DateTime64

TimeSeriesResultSet getXxx() 方法

方法

InfluxDB 数据类型

ClickHouse 支持的数据类型

getString(String columnName)

所有类型(通用方法,返回字符串表示)

所有类型(通用方法,返回字符串表示)

getInt(String columnName)

整数类型(在 Influx 中 field integer)

Int8, Int16, Int32, UInt8, UInt16, UInt32

getLong(String columnName)

64 位整数类型

Int64, UInt64

getShort(String columnName)

小范围整数

Int8, Int16

getFloat(String columnName)

单精度浮点

Float32

getDouble(String columnName)

双精度浮点

Float64

getBoolean(String columnName)

布尔或整数字段(0/1)

Bool 或数字(0=false, 非0=true)

getTimestampNanos(String columnName)

时间戳(以纳秒返回)

DateTime, DateTime64

重要说明

  • getString() 是最通用的方法,可用于所有类型的数据读取

  • 整数类型(Int/UInt)只能使用对应的数值类型 getXxx() 方法读取

  • 大整数类型(Int128/Int256, UInt128/UInt256)和 Decimal 类型必须使用 getString() 读写

  • 时间戳类型(DateTime, DateTime64 等)在写入时使用 setTimestamp(),读取时使用 getTimestampNanos()

1.10.4.2. 各后端数据库支持的数据类型

1.10.4.2.1. InfluxDB 1.x

InfluxDB 1.x 支持的数据类型相对简单:

类型

元素类型

描述

Float

Field 值

默认数值类型。IEEE-754 64 位浮点数(不支持 NaN 或 +/- Inf)。示例:1, 1.0, 1.e+78, 1.E+78

Integer

Field 值

有符号 64 位整数(-9223372036854775808 到 9223372036854775807)。在数字后加 i 指定整数。示例:1i

String

measurement, tag key/value, field key/value

字符串类型,长度限制 64KB

Boolean

Field 值

存储 TRUE 或 FALSE 值

Timestamp

时间戳

Unix 纳秒时间戳。可通过 InfluxDB API 指定其他精度。最小有效时间戳:-9223372036854775806(1677-09-21T00:12:43.145224194Z),最大有效时间戳:9223372036854775806(2262-04-11T23:47:16.854775806Z)

类型映射

  • Float → setDouble() / getDouble()

  • Integer → setLong() / getLong()

  • String → setString() / getString()

  • Boolean → setBoolean() / getBoolean()

  • Timestamp → setTimestamp() / getTimestampNanos()

1.10.4.2.2. ClickHouse

在本项目的统一 API 中,ClickHouse 端仅保证并推荐使用下列常用字段类型(示例建表):

CREATE TABLE example (
    id Int32,
    int8_val Int8,
    int16_val Int16,
    int32_val Int32,
    int64_val Int64,
    uint8_val UInt8,
    uint16_val UInt16,
    uint32_val UInt32,
    uint64_val UInt64,
    float32_val Float32,
    float64_val Float64,
    string_val String,
    fixedstring_val FixedString(20),
    bool_val Bool,
    datetime_val DateTime,
    datetime64_val DateTime64(3),
    enum8_val Enum8('ready'=1,'running'=2,'finished'=3),
    enum16_val Enum16('ready'=1,'running'=2,'finished'=3),
    uuid_val UUID,
    ipv4_val IPv4,
    ipv6_val IPv6
);

说明:

  • 上述类型为统一 API 在 ClickHouse 端经常使用并保证支持的集合;其他 ClickHouse 特殊类型未列入清单的不保证兼容。

  • Date 类型暂不推荐使用(未列入受支持清单)。

  • Time 类型不受支持,请使用 DateTime / DateTime64 表示时间戳类字段。

1.10.5. INSERT 自动建表机制

目前 ClickHouse 后端支持自动建表和添加列功能,简化用户的使用流程。(InfluxDB 原生支持自动建表) 本节详细说明 hint 的传递方式、生成的 CREATE TABLE 语句结构,以及列类型的绑定推断机制。

Hint 传递方式

在 INSERT 语句的注释中添加 ClickHouse 专用 hint 子句:

INSERT /*+ TAGS(deviceId, region) FIELDS(temperature, humidity) TIMESTAMP(time)
          ENGINE(MergeTree())
          PARTITION(toDate(time))
          ORDER_BY(time, deviceId)
          PRIMARY_KEY(deviceId) */
INTO sensor_data
(time, deviceId, region, temperature, humidity)
VALUES (?, ?, ?, ?, ?);

可用的 hint 子句

  • ENGINE(engine_type) - 指定 ClickHouse 表引擎,如 MergeTree()ReplicatedMergeTree()

  • PARTITION(partition_expr) - 指定分区表达式,如 toDate(time)toYYYYMM(time)time, deviceId

  • ORDER_BY(column_list) - 指定排序键(必需),支持单个列或多个列,如 time(time, deviceId)

  • PRIMARY_KEY(column_list) - 指定主键(可选),如 deviceId

生成的 CREATE TABLE 语句

根据上面的 INSERT 语句,框架会自动生成如下 CREATE TABLE 语句:

CREATE TABLE IF NOT EXISTS `sensor_data` (
    `time` DateTime(9),
    `deviceId` String,
    `region` Nullable(String),
    `temperature` Nullable(Float64),
    `humidity` Nullable(Float64)
)
ENGINE = MergeTree()
PARTITION BY toDate(`time`)
ORDER BY (`time`, `deviceId`)
PRIMARY KEY `deviceId`

生成的规则说明

  • 列定义顺序与 INSERT 语句中列的顺序一致

  • TAG 列(deviceIdregion)和 TIMESTAMP 列(time)声明为非 NULLable

  • FIELD 列(temperaturehumidity)声明为 Nullable

  • 如果未指定 ENGINE,默认使用 MergeTree()

  • 如果未指定 ORDER_BY,默认使用第一个列作为排序键

  • PARTITION 支持函数表达式(如 toDate(time))或列名

  • PRIMARY_KEY 可选,未指定时默认与 ORDER_BY 相同

列类型映射机制

框架根据 setXxx() 方法的调用自动推断列类型:

setXxx() 方法

ClickHouse 列类型(非 NULLable)

ClickHouse 列类型(NULLable)

setString(int, String)

String

Nullable(String)

setInt(int, int)

Int64

Nullable(Int64)

setLong(int, long)

Int64

Nullable(Int64)

setShort(int, short)

Int64

Nullable(Int64)

setFloat(int, float)

Float64

Nullable(Float64)

setDouble(int, double)

Float64

Nullable(Float64)

setBoolean(int, boolean)

UInt8

Nullable(UInt8)

setTimestamp(int, long, TimeUnit)

DateTime(9)

Nullable(DateTime(9))

类型推断示例

String insertSql =
    "INSERT /*+ TAGS(deviceId) FIELDS(value) TIMESTAMP(time) */ INTO sensor_data
    (time, deviceId, value) VALUES (?, ?, ?)";

try (TimeSeriesClient client = dataSource.getClient();
     TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {

    ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    // -> time 列类型:DateTime(9)(TIMESTAMP 列,非 NULLable)

    ps.setString(2, "device-001");
    // -> deviceId 列类型:String(TAG 列,非 NULLable)

    ps.setInt(3, 123);
    // -> value 列类型:Nullable(Int64)(FIELD 列,NULLable)

    ps.executeUpdate();
}

注意事项

  • 自动建表和添加列功能目前仅对 ClickHouse 后端生效(InfluxDB 本身自动创建)

  • ALTER TABLE 添加列可能因类型不兼容而失败,此时会抛出异常

  • 多线程并发插入同一表时,使用表级锁保证操作的线程安全

  • 元数据缓存的轻微不一致是可以接受的,仅影响自动 alter 的性能

1.10.6. 异库兼容性说明

本节总结跨不同时序数据库使用时需要注意的兼容性问题。

重要提示:由于底层时序库的实现差异,某些行为在不同数据库间可能不一致。 建议在设计阶段就避免依赖这些差异,确保业务逻辑的跨库一致性。

1.10.6.1. 连接 URL

  • InfluxDB 1.x:jdbc:influxdb:host:port

  • ClickHouse:jdbc:clickhouse:host:port

通过 URL scheme 区分后端实现。

1.10.6.2. 数据类型兼容性

不要依赖 getMetaData 中的 ColumnType

由于不同底层时序库的数据类型存储方式存在差异,getMetaData() 返回的 ColumnType 可能不一致。 尤其是布尔类型:ClickHouse 底层使用数字(1/0)存储布尔值,metadata 中只能获取到数值类型而非布尔类型。

示例

TimeSeriesResultSetMetaData meta = rs.getMetaData();
int type = meta.getColumnType(1);  // 跨库场景下,此值可能不一致

// 错误做法:依赖 ColumnType 判断字段类型
if (type == Types.BOOLEAN) { ... }  // ClickHouse 中此条件可能不成立

// 正确做法:根据业务语义处理字段值,而非依赖 metadata 的类型
Object value = rs.getObject(1);
if (value instanceof Boolean) {
    boolean boolValue = (Boolean) value;
} else if (value instanceof Number) {
    // 兼容 ClickHouse 使用数字表示布尔值的场景
    boolean boolValue = ((Number) value).intValue() == 1;
}

1.10.6.3. NULL 值行为差异

由于不同时序数据库对 NULL 值的默认行为不一致,框架无法在底层完全抹平这些差异。 强烈建议在应用层设计时避免出现 NULL 值与空字符串混合使用的场景,确保业务逻辑的一致性。

设计建议

  • 所有字段尽量显式设置值,避免依赖数据库的默认填充行为

  • 字符串类型:统一使用非空字符串,不要混用空字符串和 null

  • 数值类型:如有需要表示”无值”的语义,使用特殊的标记值而非依赖 null

  • 如果必须使用 null,在 ClickHouse 中应将字段声明为 Nullable 类型

getXxx() 方法的 NULL 返回行为

当查询结果中某列为 SQL NULL 时,各 getXxx() 方法的返回值:

方法

返回值

getString()

null

getInt()

0

getLong()

0

getShort()

0

getFloat()

0.0f

getDouble()

0.0

getBoolean()

false

getTimestampNanos()

0

不同数据库的 NULL 行为差异

建议在使用时序数据库的时候,在业务设计之初就不要存在空串与 null 。

由于底层时序库的默认行为不同,对于 INSERT 时不传字段的数据,查询结果存在差异:

场景

InfluxDB

ClickHouse

TAG 不传

返回 nullwasNull() 返回 true

自动填充空串 ""wasNull() 返回 false

TAG 传入空字符串

返回 nullwasNull() 返回 true

自动填充空串 ""wasNull() 返回 false

FIELD 不传(非 Nullable)

返回 nullwasNull() 返回 true

自动填充默认值(字符串空串、数值 0、布尔 false),wasNull() 返回 false

FIELD 不传(Nullable)

返回 nullwasNull() 返回 true

返回 nullwasNull() 返回 true

FIELD 传入空字符串

返回空串 ""wasNull() 返回 false

返回空串 ""wasNull() 返回 false

1.10.6.4. API 兼容性说明

由于后端能力差异,部分 API 在不同后端的支持程度不同:

  • InfluxDB 1.x - 支持运行期切换 database - 支持 retentionPolicy

  • ClickHouse - 支持运行期切换 database - 不支持 retentionPolicy

建议根据目标后端选择合适的使用方式。

1.10.7. 接口说明

1.10.7.1. TimeSeriesDataSource

方法

说明

getClient()

获取时序客户端实例(需显式关闭)

setUrl(String)

设置连接 URL

setDatabase(String)

设置默认数据库

setUsername(String)

设置用户名

setPassword(String)

设置密码

1.10.7.2. TimeSeriesClient

方法

说明

prepareStatement(String)

创建预编译 SQL 语句

setConsistency(ConsistencyLevel)

设置一致性级别(部分后端支持)

setRetentionPolicy(String)

设置保留策略(部分后端支持)

setDatabase(String)

设置当前数据库(部分后端不支持)

close()

关闭客户端并释放资源

1.10.7.3. TimeSeriesPreparedStatement

方法

说明

setString(int, String)

绑定字符串参数(不允许传入 null,传入空字符串需注意各库行为差异)

setInt(int, int)

绑定整数参数(索引从 1 开始)

setLong(int, long)

绑定长整数参数

setShort(int, short)

绑定短整数参数

setFloat(int, float)

绑定浮点数参数

setDouble(int, double)

绑定双精度浮点数参数

setBoolean(int, boolean)

绑定布尔参数

setTimestamp(int, long, TimeUnit)

绑定时间戳参数(按 UTC 时区处理)

executeUpdate()

执行写入类 SQL

executeQuery()

执行查询并返回统一结果集

addBatch()

将当前参数加入批量缓存(仅 INSERT 语句支持)

executeBatch()

执行批量缓存中的所有语句,返回每条语句的影响行数数组(为1)

clearBatch()

清空批量缓存

close()

关闭预编译语句并释放资源

重要注意事项

  1. setString 参数限制: - 不允许传入 null 值,否则抛出 IllegalArgumentException - 传入空字符串需注意:InfluxDB 查询结果为 null,ClickHouse 查询结果为空字符串 - 详见 异库兼容性说明

  2. 参数绑定完整性: - SQL 中的每个占位符(?)都必须进行参数绑定 - 缺少绑定会在执行时抛出 IllegalStateException

1.10.7.4. TimeSeriesResultSet

方法

说明

next()

将光标移动到下一行

getString(String columnName)

以字符串形式获取指定列的值

getInt(String columnName)

以整数形式获取指定列的值

getLong(String columnName)

以长整数形式获取指定列的值

getShort(String columnName)

以短整数形式获取指定列的值

getFloat(String columnName)

以浮点数形式获取指定列的值

getDouble(String columnName)

以双精度浮点数形式获取指定列的值

getBoolean(String columnName)

以布尔值形式获取指定列的值

getTimestampNanos(String columnName)

以纳秒时间戳形式获取指定列的值(按 UTC 时区返回)

getMetaData()

获取结果集的元数据信息

wasNull()

上一个 get 方法返回的列值是否为 null

close()

关闭结果集并释放相关资源

1.10.7.5. TimeSeriesResultSetMetaData

方法

说明

getColumnCount()

获取结果集中的列数

getColumnType(int column)

获取指定列的 SQL 类型(对应 java.sql.Types 中的常量)

getColumnTypeName(int column)

获取指定列的数据库特定类型名称

getColumnName(int column)

获取指定列的名称

1.10.8. INSERT SQL 规则

  • 所有写入操作必须使用 INSERT SQL 语句

  • SQL 中必须包含 hint 注释,用于声明 tag / field 元信息

  • hint 必须位于 INSERT 与 INTO 之间,采用 MySQL 类似的注释风格:

INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather
(time, region, temperature, humidity) VALUES (?, ?, ?, ?);
  • hint 用于指导框架正确地写入时序数据库

  • tag / field 声明内容必须与 TimeSeriesPreparedStatement 中绑定的列保持一致

  • 未包含正确 hint 的 INSERT 将被视为非法,框架会抛出异常

ClickHouse 专用 hint 子句

对于 ClickHouse 后端,hint 中还支持以下子句,用于指定表结构和分区策略:

子句

说明

ENGINE(engine_type)

指定 ClickHouse 的表引擎类型,如 MergeTree()ReplicatedMergeTree()

PARTITION(partition_expr)

指定表分区表达式,如 toDate(timestamp)toYYYYMM(timestamp)

ORDER_BY(column_list)

指定排序键(必需),支持单个列或多个列,如 timestamp(timestamp, deviceId)

PRIMARY_KEY(column_list)

指定主键(可选),如 deviceId

示例:完整的 ClickHouse INSERT SQL

INSERT /*+ TAGS(deviceId, region) FIELDS(temperature, humidity) TIMESTAMP(time)
          ENGINE(MergeTree())
          PARTITION(toDate(time))
          ORDER_BY(time, deviceId)
          PRIMARY_KEY(deviceId) */
INTO sensor_data
(time, deviceId, region, temperature, humidity)
VALUES (?, ?, ?, ?, ?, ?);

说明

  • ENGINE、PARTITION、ORDER_BY、PRIMARY_KEY 子句在 InfluxDB 后端会被忽略

  • ORDER_BY 是 ClickHouse 必需的子句,如未指定,默认使用时间戳列

  • PARTITION 建议设置为 toDate(timestamp) 或类似函数,按天分区

  • PRIMARY_KEY 可选,未指定时默认与 ORDER_BY 相同