恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot + MyBatis 集成 ClickHouse:配置、插入查询与避坑指南
首页
资讯中心
/
SpringBoot + MyBatis 集成 ClickHouse:配置、插入查询与避坑指南
SpringBoot + MyBatis 集成 ClickHouse:配置、插入查询与避坑指南
发布时间:2026/10/15 2:25:06
1. 为什么要在 SpringBoot 里接 ClickHouse 而不是继续用 MySQL做过数据类项目的朋友大概率都遇到过这个场景业务表数据量一过千万MySQL 的查询就开始肉眼可见地变慢加索引、分库分表、读写分离一套组合拳打下来维护成本高得离谱而业务方要的只是按时间维度做个聚合统计这种看似简单的需求。这时候 ClickHouse 就该登场了。它是一个列式存储的 OLAP 数据库天生为海量数据的聚合分析而生单表几十亿行做 count、sum、group by 这类操作响应时间经常能压到毫秒级这是行式存储的 MySQL 很难做到的。但问题来了ClickHouse 虽然查询猛可它毕竟不是给业务系统当主库用的我们的业务代码还是跑在 SpringBoot 里数据访问层习惯用 MyBatis。于是SpringBoot MyBatis 配置 ClickHouse实现插入和查询就成了一个非常典型的落地需求。这篇内容就是围绕这个场景展开的我会把从依赖引入、数据源配置、Mapper 编写到实际插入查询的完整链路讲清楚同时把我在实操中踩过的坑一并交代出来。适合已经有 SpringBoot 和 MyBatis 基础、正准备把 ClickHouse 接进项目的开发者参考纯新手也能跟着走通因为关键的地方我都会解释清楚为什么这么做。先说一个很多人会犯的认知错误以为 ClickHouse 可以像 MySQL 一样直接替换。实际上 ClickHouse 对标准 SQL 的支持是够用但不完整的它不支持完整的事务不支持高频的单行 update/delete早期版本基本没有后来有了也是异步的、代价很大JOIN 的性能也远不如单表聚合。所以正确的姿势是把它当成一个分析型副库业务写入走 MySQL通过同步或者异步的方式把数据灌到 ClickHouse查询分析走 ClickHouse。理解了这层定位后面的配置和代码才不会走偏。2. 依赖选型JDBC 驱动到底该用官方版还是社区版2.1 两种驱动的本质区别接 ClickHouse 第一步就是选驱动。市面上主要有两个选择一个是 ClickHouse 官方提供的clickhouse-jdbc另一个是早期社区常用的ru.yandex.clickhouse那个老包。这里有个非常容易踩的坑——包名不一样类名也不一样混用会直接报 ClassNotFound。官方驱动现在的坐标是com.clickhouse:clickhouse-jdbc它同时支持 HTTP 和 TCP 两种协议推荐用 HTTP 协议默认 8123 端口因为兼容性最好穿透各种网络环境也更省心。老版本的ru.yandex.clickhouse:clickhouse-jdbc已经停止维护了新项目千万别再用。在pom.xml里引入的时候我一般会带上allclassifier 或者配合clickhouse-http-client这样能把依赖的 httpclient 一起拉进来省得后面手动补包dependency groupIdcom.clickhouse/groupId artifactIdclickhouse-jdbc/artifactId version0.6.0/version classifierall/classifier /dependency注意classifier用all会把 shaded 过的依赖打进来好处是省心坏处是可能和你项目里已有的 httpclient 版本冲突。如果你的项目本身对 httpclient 版本敏感那就去掉 classifier改成单独引clickhouse-http-client自己控制版本。2.2 版本兼容性这件事必须提前确认ClickHouse 服务端的版本和 JDBC 驱动的版本之间是有兼容关系的。我遇到过最典型的问题就是服务端是比较新的版本驱动用的是很老的 0.3.x结果连接能建立但一执行某些带新类型比如Nullable、LowCardinality的查询就报解析异常。所以选驱动版本时尽量和服务端版本保持接近或者至少用官方文档里标注兼容的版本区间。另外 SpringBoot 的版本也要留意。SpringBoot 2.x 和 3.x 在数据源自动配置上有差异3.x 默认用的是 HikariCP而且对 JDBC URL 的解析更严格。如果你是从 2.x 升级上来的项目配置项名字可能要对一遍。2.3 为什么不用 MyBatis-Plus 直接接有朋友会问既然用了 MyBatis能不能直接上 MyBatis-Plus我的建议是可以但要谨慎。MyBatis-Plus 的很多便利功能比如自动填充、逻辑删除、分页插件都是围绕 MySQL 这类支持事务和标准 SQL 的数据库设计的套到 ClickHouse 上不一定好使。比如它的分页插件生成的LIMIT ?, ?语法 ClickHouse 是支持的但一些批量操作生成的 SQL 可能就不兼容。所以接 ClickHouse 我更倾向于用原生 MyBatisSQL 自己写可控性最强出问题也好排查。3. 数据源配置一个项目里同时挂 MySQL 和 ClickHouse 怎么搞3.1 单数据源配置的写法如果你的项目里只有 ClickHouse 一个数据源那配置相对简单。在application.yml里这样写spring: datasource: driver-class-name: com.clickhouse.jdbc.ClickHouseDriver url: jdbc:clickhouse://127.0.0.1:8123/default username: default password: hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000这里的 URL 格式是jdbc:clickhouse://host:port/database默认端口 8123 是 HTTP 协议。用户名密码按你服务端实际配置来很多测试环境默认就是default空密码。连接池这块我要多说一句。ClickHouse 的查询往往比较重单个查询可能占用较长时间所以连接池的maximum-pool-size不要设太大否则容易把服务端的连接数打满。我一般控制在 10 到 20 之间配合合理的connection-timeout避免请求堆积。3.2 多数据源共存才是真实场景实际项目里更常见的是MySQL 作为业务主库ClickHouse 作为分析库两个都要用。这时候就得配多数据源。核心思路是手动定义两个DataSourceBean分别绑定不同的Mapper包路径然后用Configuration类区分开。先定义 ClickHouse 的数据源配置类Configuration MapperScan(basePackages com.example.mapper.ck, sqlSessionFactoryRef ckSqlSessionFactory) public class ClickHouseDataSourceConfig { Bean(name ckDataSource) ConfigurationProperties(prefix spring.datasource.clickhouse) public DataSource ckDataSource() { return DataSourceBuilder.create().build(); } Bean(name ckSqlSessionFactory) public SqlSessionFactory ckSqlSessionFactory( Qualifier(ckDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/ck/*.xml)); return bean.getObject(); } }MySQL 那边同理只是包路径和前缀换成对应的。这里的关键点是MapperScan的basePackages一定要分开sqlSessionFactoryRef也要指向各自的工厂否则 MyBatis 会不知道某个 Mapper 该用哪个数据源启动就报错。提示多数据源场景下Transactional注解要特别小心。它默认只对主数据源生效如果你在操作 ClickHouse 的方法上加了事务注解要么不生效要么报错。ClickHouse 本身也不支持传统事务所以涉及 ClickHouse 的操作就别加事务了。3.3 配置文件里的前缀要对应上多数据源时application.yml里要写两组配置前缀和上面ConfigurationProperties里的一致spring: datasource: mysql: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/biz?useSSLfalse username: root password: 123456 clickhouse: driver-class-name: com.clickhouse.jdbc.ClickHouseDriver url: jdbc:clickhouse://127.0.0.1:8123/analytics username: default password: 这样两个数据源互不干扰各管各的 Mapper逻辑清晰。4. 建表与实体映射ClickHouse 的表设计有几个反直觉的点4.1 引擎选择决定了这张表能干什么ClickHouse 建表必须指定引擎这是和 MySQL 最大的不同之一。最常用的就是MergeTree家族。一个典型的日志分析表长这样CREATE TABLE user_event ( event_date Date, user_id UInt64, event_type String, amount Float64, create_time DateTime ) ENGINE MergeTree() PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, user_id);这里有几个点要解释。PARTITION BY决定数据按什么分区一般按时间月或者天来分方便过期数据整体删除。ORDER BY是 ClickHouse 的排序键它直接决定了主键索引的构建方式查询时如果 where 条件命中了排序键的前缀性能会非常好。所以ORDER BY的字段顺序要按查询频率来排最常用的过滤字段放前面。注意ClickHouse 的主键和 MySQL 的主键概念不一样。它没有唯一约束你插入两条完全一样的记录它不会拦你。所以去重逻辑得在业务层或者用ReplacingMergeTree引擎自己处理。4.2 数据类型映射要一一对应Java 类型和 ClickHouse 类型的映射关系如果对不上插入时就会报类型转换异常。常见的对应关系我整理成表格方便对照ClickHouse 类型Java 类型说明UInt8/16/32/64Integer / Long无符号整数注意范围Int8/16/32/64Integer / Long有符号整数Float32/64Float / Double浮点数StringString变长字符串Datejava.sql.Date / LocalDate日期DateTimejava.sql.Timestamp / LocalDateTime日期时间Nullable(T)包装类型允许为 null基础类型不行最容易踩的坑是Nullable。ClickHouse 里如果一个字段声明成Nullable(String)那 Java 侧必须用包装类型或者 StringString 本身可为 null但如果是Nullable(Int32)Java 侧就得用Integer而不是int否则插入 null 会直接抛异常。4.3 实体类怎么写才不出错实体类本身没什么特别的就是普通的 POJO字段名和表字段对应即可。但有个细节ClickHouse 的字段名是大小写敏感的而 MyBatis 默认的驼峰转下划线映射在 ClickHouse 上不一定按你预期工作。我的做法是老老实实在 XML 里写resultMap把列名和属性名显式对应上别偷懒靠自动映射。public class UserEvent { private LocalDate eventDate; private Long userId; private String eventType; private Double amount; private LocalDateTime createTime; // getter setter 省略 }5. Mapper 层实操插入和查询的 SQL 到底怎么写5.1 插入批量插入才是正确姿势ClickHouse 最忌讳的就是一条一条插。它的设计是面向批量写入的单条插入的开销极大几千条单插能把性能拖垮。所以插入一定要用批量。MyBatis 里用foreach拼批量 valuesinsert idbatchInsert parameterTypejava.util.List INSERT INTO user_event (event_date, user_id, event_type, amount, create_time) VALUES foreach collectionlist itemitem separator, (#{item.eventDate}, #{item.userId}, #{item.eventType}, #{item.amount}, #{item.createTime}) /foreach /insert对应的 Mapper 接口public interface UserEventMapper { int batchInsert(Param(list) ListUserEvent list); }这里有个经验值单批次插入的记录数控制在 1000 到 10000 条之间比较合适。太小了网络往返开销占比高太大了单条 SQL 太长可能触发服务端的max_query_size限制。我一般用 5000 一批实测下来很稳。提示如果数据量特别大比如一次要灌几十万条建议在 Service 层做分批每 5000 条调一次batchInsert而不是拼一个巨大的 SQL。这样内存占用可控失败也容易重试。5.2 查询聚合统计是 ClickHouse 的主场查询这块ClickHouse 的强项是聚合。比如统计某天各事件类型的总金额select idsumByEventType resultTypecom.example.dto.EventStat SELECT event_type AS eventType, sum(amount) AS totalAmount, count() AS cnt FROM user_event WHERE event_date #{date} GROUP BY event_type ORDER BY totalAmount DESC /select注意count()这个写法ClickHouse 里count()不带参数就是统计行数比count(*)更地道性能也更好。另外 ClickHouse 有很多 MySQL 没有的聚合函数比如uniq()做去重计数、quantile()算分位数这些在数据分析场景里非常有用值得花时间了解一下。5.3 分页查询的坑ClickHouse 的分页和 MySQL 一样用LIMIT offset, size但深分页offset 很大在 ClickHouse 上性能会急剧下降因为它还是要扫描前面的数据。如果业务上确实需要深分页更好的做法是用排序键做游标分页比如WHERE event_date #{lastDate} ORDER BY event_date LIMIT 100每次带上上一页的最后一条记录的排序键值。这个思路和很多大数据场景的翻页优化是一致的。6. 实测中那些文档不会告诉你的坑6.1 时间类型和时区问题ClickHouse 的DateTime默认用的是服务端的时区而 Java 侧LocalDateTime是没有时区概念的。如果两边时区不一致插入和查询出来的时间就会差几个小时。我踩过一次数据插进去看着是对的查出来全偏了 8 小时。解决办法有两个一是保证服务端和应用的时区配置一致二是在 JDBC URL 里显式指定时区参数。我倾向于后者明确比默认靠谱。6.2 连接池空闲连接被服务端断开ClickHouse 服务端有个keep_alive_timeout默认比较短。如果连接池里的空闲连接长时间不用服务端会主动断开而连接池这边还以为连接是好的下次取出来用就报连接已关闭。解决办法是在 HikariCP 里配置max-lifetime小于服务端的超时时间并且开启连接有效性检测hikari: max-lifetime: 600000 connection-test-query: SELECT 1 validation-timeout: 3000SELECT 1这个检测语句 ClickHouse 是支持的可以放心用。6.3 大批量插入时的内存溢出前面说了批量插入但如果你的实体对象本身很大一次攒几千条在内存里再加上 MyBatis 拼 SQL 时的字符串拼接很容易 OOM。我的做法是控制单批大小并且在 Service 层用流式处理边读边写不要一次性把所有数据加载到 List 里。如果数据源本身是文件或者另一个库用游标分批读读一批写一批。6.4 查询结果字段名大小写ClickHouse 返回的列名默认是原样返回的如果你 SQL 里写了别名AS eventType那结果集里就是eventType。但如果你没写别名直接用event_type那 MyBatis 映射到 Java 属性eventType时自动映射可能匹配不上取决于你的mapUnderscoreToCamelCase配置。所以我在 ClickHouse 的查询里一律显式写别名避免这种模糊地带。7. 一个完整的插入查询链路串起来把上面的东西串一遍一个典型的 Service 方法大概长这样Service public class EventService { Autowired private UserEventMapper userEventMapper; public void saveEvents(ListUserEvent events) { int batchSize 5000; for (int i 0; i events.size(); i batchSize) { int end Math.min(i batchSize, events.size()); ListUserEvent sub events.subList(i, end); userEventMapper.batchInsert(sub); } } public ListEventStat statByDate(LocalDate date) { return userEventMapper.sumByEventType(date); } }这段代码没什么花哨的但每一步都有讲究分批是为了控制内存和 SQL 长度查询直接走聚合是为了发挥 ClickHouse 的优势。实际项目里你可能还要加上异常处理、日志记录、慢查询监控但核心链路就是这样。8. 关于性能调优我个人的几条经验ClickHouse 的性能调优是个大话题这里只讲和 SpringBoot 集成相关的几条。第一查询尽量带上分区键的过滤条件比如WHERE event_date ?这样 ClickHouse 能直接跳过不相关的分区扫描量能降一个数量级。第二避免SELECT *列式存储的优势就是只读需要的列你SELECT *等于把这个优势扔了。第三聚合查询如果结果集很大考虑在 ClickHouse 侧用LIMIT或者物化视图预聚合别把几百万行结果拉回应用层再处理。还有一点ClickHouse 的JOIN性能确实一般尤其是大表 JOIN 大表。如果业务上需要关联尽量把小表放到右边或者干脆在写入时就把数据打宽做成一张大宽表查询时就不用 JOIN 了。这是 OLAP 场景很常见的思路用存储换查询性能。我在实际使用中发现很多性能问题其实不是 ClickHouse 本身的问题而是用法不对。比如把 ClickHouse 当 MySQL 用频繁单条写入、频繁 update、大量 JOIN这些都会让它很难受。把它放在它擅长的位置上它就能给你惊喜。踩过几次坑之后我的体会是理解一个工具的边界比学会它的语法更重要。