在电商、广告、日志平台等场景中,分析型查询通常会扫描数千万甚至数亿行数据,并执行分组、排序、窗口计算等操作。Spring Boot 作为应用开发框架,本身并不提供 OLAP 引擎,但可以借助 ClickHouse 的 JDBC 驱动和 Spring 的 JdbcTemplate 或 MyBatis 快速实现分析接口。ClickHouse 是典型的列式存储数据库,它按列读取数据、使用向量化执行引擎,特别适合大表聚合。本文会从依赖引入开始,逐步演示如何配置数据源、执行聚合 SQL、管理批量写入,以及排查整合过程中常见的连接和性能问题。

为什么分析型查询更适合 ClickHouse
关系型数据库如 MySQL 面向事务,行式存储,适合按主键查找和少量行更新。但 OLAP 查询往往只取少数几列做聚合,却要扫描大量行。行式存储读取整行,即使只需要一列也会把其他列从磁盘拉出来,I/O 浪费严重。ClickHouse 按列独立存储,查询引擎只读取参与计算的列,数据压缩率也更高,常用的 LZ4、ZSTD 压缩能把日志类数据压到原来的十分之一甚至更小。
ClickHouse 的另一个优势是并行化和向量化执行。单条 GROUP BY 查询可以在多核 CPU 上并行扫描分区,聚合函数针对列式内存布局做了 SIMD 优化,因此几亿行数据在单机也能做到秒级响应。它还支持物化视图、投影、跳数索引等机制,可以进一步加速高频维度统计。Spring Boot 项目里引入它,相当于把分析查询从主业务库剥离出来,避免大查询拖垮在线交易。
依赖选择方面,官方提供 com.clickhouse 的 JDBC 驱动,底层使用 HTTP 协议访问 ClickHouse 的 8123 端口。它兼容 JDBC 4.2 规范,能被 HikariCP、Druid 等连接池管理。Maven 中需要同时引入 clickhouse-jdbc 和 spring-boot-starter-jdbc,代码如下:
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.6.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
如果需要更高性能,可以选用 gRPC 驱动,但官方 JDBC 在稳定性、类型映射和连接池兼容性上更适合大多数 Spring Boot 项目。版本上建议用与 ClickHouse 服务端相同或稍新的驱动,避免出现不支持的聚合函数或设置项。
数据源配置与连接池调优
在 application.yml 中配置 ClickHouse 数据源。Spring Boot 2.x 及 3.x 对多数据源的支持一般使用自定义 DataSource Bean 再交给 JdbcTemplate。核心参数包括 JDBC URL、用户名、密码、驱动类名。URL 形如 jdbc:clickhouse://127.0.0.1:8123/default,如果 ClickHouse 未开启密码认证,用户名可以写 default,密码留空。连接池推荐 HikariCP,因为 ClickHouse 的 HTTP 连接创建成本较低,池子不需要太大,通常 maximumPoolSize 设成 10 到 20 就足够。
spring:
datasource:
clickhouse:
jdbc-url: jdbc:clickhouse://127.0.0.1:8123/default
username: default
password:
driver-class-name: com.clickhouse.jdbc.ClickHouseDriver
maximum-pool-size: 20
minimum-idle: 2
connection-timeout: 30000
注意 jdbc-url 而不是 url,这是 HikariCP 的约定。如果项目中同时有 MySQL 数据源,需要为 ClickHouse 单独创建 DataSource 和 JdbcTemplate,并注明 @Primary 避免注入冲突。
连接池的 idleTimeout 和 maxLifetime 不宜设得过短,因为 ClickHouse 的 HTTP 连接本质是短连接复用,连接池会维护一定数量的 TCP 连接。若 maxLifetime 小于服务端 keep-alive 时间,可能导致连接被服务端关闭后客户端仍使用,出现 Connection reset。建议 maxLifetime 设为 10 分钟,idleTimeout 设为 5 分钟。下文给出的配置类可以清晰管理多数据源。
@Configuration
public class ClickHouseDataSourceConfig {
@Bean(name = "clickHouseDataSource")
@ConfigurationProperties(prefix = "spring.datasource.clickhouse")
public DataSource clickHouseDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "clickHouseJdbcTemplate")
public JdbcTemplate clickHouseJdbcTemplate(@Qualifier("clickHouseDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
这样在 Service 层可以用 @Qualifier("clickHouseJdbcTemplate") 注入专门的分析查询模板,与业务库操作完全隔离。
实现 OLAP 查询:聚合、窗口函数与分页
分析型接口通常不需要复杂的 ORM 映射,直接用 SQL 表达聚合逻辑更高效。假设有一张订单明细表 order_detail,包含 order_id、user_id、amount、status、created_at 等字段,要按天统计订单量和销售额,可以这样写:
SELECT
toDate(created_at) AS day,
count() AS order_count,
sum(amount) AS total_amount,
avg(amount) AS avg_amount
FROM order_detail
WHERE created_at >= toDate('2025-01-01')
AND created_at < toDate('2025-02-01')
GROUP BY day
ORDER BY day DESC;
ClickHouse 提供了大量专用函数,例如 toDate、toStartOfHour、uniqExact、quantile 等,比标准 SQL 更适合分析场景。执行时 JdbcTemplate 的 queryForList 会把每一行转换成 Map,字段名与 SQL 别名一致。
窗口函数在 ClickHouse 中同样支持,可以计算用户留存、排名、累计值等。比如统计每个用户最近一笔订单金额占其历史总金额的比例,可以用 sum(amount) OVER (PARTITION BY user_id) 配合 ROW_NUMBER()。这类查询在关系库里可能因为数据量大而非常慢,但 ClickHouse 会并行计算窗口,返回速度明显更快。
String sql = "SELECT user_id, amount, created_at, " +
"sum(amount) OVER (PARTITION BY user_id) AS total_user_amount " +
"FROM order_detail " +
"WHERE created_at >= ? AND created_at < ? " +
"ORDER BY user_id, created_at DESC";
List<Map<String, Object>> result = clickHouseJdbcTemplate.queryForList(
sql, startDate, endDate);
对于 Web 端需要的分页,ClickHouse 支持 LIMIT offset, size 语法,但深度分页代价较高,尽量用时间范围或游标替代 offset。如果业务必须翻页,建议加上 ORDER BY 和 LIMIT,并限制最大返回条数。
如果聚合结果需要进一步存入 MySQL 或缓存,可以在 Service 方法中直接转换 List<Map> 为 DTO,不要为了几个字段引入 JPA 实体。ClickHouse 是分析库,不适合频繁更新和单行删除,更适合追加写入和定期合并。
批量写入与查询性能优化
OLAP 场景不只包含查询,数据导入同样关键。Spring Boot 应用可以通过 JDBC 批量写入 ClickHouse,但要注意批量大小和提交频率。ClickHouse 的每次插入会生成一个 part,频繁小批量插入会导致后台合并压力增大。建议每批 5000 到 20000 行,使用 PreparedStatement 的 addBatch 和 executeBatch 提交。
public void batchInsert(List<OrderDetail> orders) {
String sql = "INSERT INTO order_detail " +
"(order_id, user_id, amount, status, created_at) " +
"VALUES (?, ?, ?, ?, ?)";
clickHouseJdbcTemplate.getJdbcTemplate().execute((Connection conn) -> {
try (PreparedStatement ps = conn.prepareStatement(sql)) {
int count = 0;
for (OrderDetail order : orders) {
ps.setString(1, order.getOrderId());
ps.setLong(2, order.getUserId());
ps.setBigDecimal(3, order.getAmount());
ps.setString(4, order.getStatus());
ps.setObject(5, order.getCreatedAt());
ps.addBatch();
if (++count % 10000 == 0) {
ps.executeBatch();
}
}
ps.executeBatch();
}
return null;
});
}
代码中 conn 是 JDBC 连接,lambda 里手动管理批处理可以避免 JdbcTemplate 每次单条提交。生产环境建议使用 ClickHouse 的 async_insert 参数,将多次插入合并到后台,进一步提升吞吐。
查询优化方面,首先要避免 SELECT *,只查参与计算的列能显著降低 I/O。其次,表引擎选择很关键,明细表通常用 MergeTree 系列,聚合表可以考虑 SummingMergeTree 或 AggregatingMergeTree,它们会在后台合并相同主键的数据,减少扫描量。分布式表则需要区分本地表和分布式表,写入本地表、查询分布式表,避免在所有分片上重复写入相同数据。
还可以通过 max_execution_time 和 max_memory_usage 设置单条 SQL 的资源上限,防止某个大查询把 ClickHouse 整机打挂。在 JDBC URL 中追加参数 ?max_execution_time=60&max_memory_usage=10000000000 即可。不过在 URL 中使用 & 时 YAML 和 properties 需要正确处理,YAML 中直接写没有歧义,properties 中要写 & 转义。
整合中容易踩到的坑
时区问题是 ClickHouse JDBC 最常见的坑。ClickHouse 的 DateTime 默认使用服务器时区,JDBC 驱动读取时可能转换成 JVM 默认时区,导致时间偏移。解决方法是在连接 URL 中加上 use_server_time_zone=false 并指定 server_time_zone=Asia/Shanghai,或者统一用 DateTime64 并带时区参数 DateTime64(3, 'Asia/Shanghai')。写入和查询两侧时区一致后,问题基本消失。
连接超时和空闲连接断开也常见。如果应用启动后长时间没有查询,连接池里的连接可能被 ClickHouse 服务端关闭,第一次访问报 Connection is not available。这时可以在 HikariCP 中配置 connection-test-query,例如 SELECT 1,让连接池在借出连接前做健康检查。ClickHouse 也支持 SELECT 1,不会产生额外负载。
另一个误区是把 ClickHouse 当关系库用,频繁更新或删除单行。MergeTree 系列引擎不支持行级更新,虽然可以通过 ALTER UPDATE 实现异步修改,但代价很高。正确做法是设计表时以追加和分区为主,定期用 OPTIMIZE TABLE 合并分区。如果需要实时去重,可以使用 ReplacingMergeTree,并接受查询时可能出现短暂重复。理解这些边界后,Spring Boot 应用可以稳定地把 ClickHouse 作为独立的分析服务,为报表和看板提供亚秒级聚合能力。

