提升SQL性能的一大原则就是降低访问数据库的次数,能合并下发的SQL是很好的手段。也就是将insert into t values();insert into t values();转换成insert into t values(),();。
在JDBC中,这种转换的大概流程是这样的:
1 | java复制代码PreparedStatement preparedStatement = conn.prepareStatement("insert into t (c1,c2) values(?,?)"); |
判断执行的SQL是否可以做batch重写。这里需要url使用rewriteBatchedStatements=true,会变成insert into t (id) values(1),(2)。这里其实有一些本身应该符合预期的情况却出现rewriteBatched失效的情况。
我们看下JDBC源码:
直接来到 ParseInfo.java,这里是对SQL进行解析进而进行条件判断,JDBC解析SQL的方式比较粗暴,通过字符判断,通过I判断是否是insert语句、通过L判断是否是load data语句、通过on, duplicate, key, update判断是否是on duplicate key update语句,通过?获得preparestatment占位符的位置,通过;判断这个请求包含几条SQL等。
比如占位符的判断,这里会记录下每一个?的位置,用staticSql进行分组,例如insert into t (c1) values (?)会解析成长度为3的数组:
INSERT INTO t (c1,c2) VALUES (,)
1 | java复制代码for(i = this.statementStartPos; i < this.statementLength; ++i){ |
之后判断是否可以进行SQL重写,判断条件有3个,最基本的2个条件:
this.numberOfQueries == 1,必须是一条SQL,即不能是sql;sql的形式。!this.parametersInDuplicateKeyClause,这指的是on duplicate key update后不能有占位符。举个例子,insert into t (c1) values (?) on duplicate key update c1 = ?是不能重写的,insert into t (c1) values (?) on duplicate key update c1 = 123这种可以。(感觉这里JDBC实现的偷懒了)
除了这两个条件必须为true,还有canRewrite()函数是判断条件,我们去看看里面的逻辑:- 如果是
insert语句,如果SQL中存在select,则不能被重写。 on duplicate key update语句不能使用LAST_INSERT_ID。
1 | java复制代码if (StringUtils.startsWithIgnoreCaseAndWs(sql, "INSERT", statementStartPos)) { |
所以这里有一个比较特殊的case,如果SQL中包含select,则rewriteBatchedStatements失效。比如下面这个例子,表结构如下:
1 | sql复制代码mysql> desc t1; |
使用上面的方式进行批量插入,理想情况到数据库应该是insert into t (id, selectNum, updateTime) values(1,11,'2021-01-01 11:11:11'),(1,11,'2021-01-01 11:11:11');插入两条数据。
我们debug一下看看数据库中收到的是什么
可见,数据库收到的还是单SQL,没有整合成一条语句。所以考虑到batch insert的情况,表结构尽量不要带有select字符。
而正常情况,应该会开始构建batch value结构,是在buildRewriteBatchedParams。
1 | java复制代码if (this.canRewriteAsMultiValueInsert && session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()) { |
这里会解析value后的内容,比如insert into t (c1,c2) values (?,?)就是(?,?)。这样SQL和value的parseInfo都有了。
excuteBatch
我们直接看如果符合canRewriteAsMultiValueInsertAtSqlLevel是如何处理的,在executeBatchedInserts。
获取这个insert batch有几个需要做合并的,这里的batchedArgs是程序执行addBatch()时候赋值的。
1 | java复制代码int numBatchedArgs = this.query.getBatchedArgs().size(); |
在prepareBatchedInsertSQL中的((PreparedQuery<?>) this.query).getParseInfo().getParseInfoForBatch(numBatches)对value进行拼接。
JDBC中实现了一个visitor,会将SQL和value的组成merge到一起:
INSERT INTO t (c1,c2) VALUES (,),(,)
结果就是拼出来的是insert into t (c1,c2) values(,),(,)
这里还需要填写占位符,所以程序会来到这里getSqlForBatch,这里用到的sqlStrings就是上面提到的visitor数组,可以看到,循环这个数组,在每个的中间补上?就变成insert into t (c1,c2) values(?,?),(?,?)了。
1 | java复制代码final byte[][] sqlStrings = this.staticSql; |
这样整个拼接流程就结束了。可以看到,有的时候虽然我们设置了rewriteBatchedStatements=true,但在某些特殊的语句格式下,拼接依然不能生效,在实现上,感觉JDBC实现的比较偷懒,比如不能有select,这个可能处于考虑到insert into ... select这种情况,但简单粗暴的禁止,也会给很多人带来困惑。
本文转载自: 掘金