解析 JDBC 的批处理参数 rewriteBatchedStatements
提升SQL性能的一大原则就是降低访问数据库的次数,能合并下发的SQL是很好的手段。也就是将insert into t values();insert into t values();转换成insert into t values(),();。
在JDBC中,这种转换的大概流程是这样的:
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 (,)
for(i = this.statementStartPos; i < this.statementLength; ++i){
char c = sql.charAt(i);
if ((c == '?')) {
endpointList.add(new int[] { lastParmEnd, i });
lastParmEnd = i + 1;
}
}
之后判断是否可以进行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。
if (StringUtils.startsWithIgnoreCaseAndWs(sql, "INSERT", statementStartPos)) {
if (StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) != -1) {
return false;
}
if (isOnDuplicateKeyUpdate) {
int updateClausePos = StringUtils.indexOfIgnoreCase(locationOfOnDuplicateKeyUpdate, sql, " UPDATE ");
if (updateClausePos != -1) {
return StringUtils.indexOfIgnoreCase(updateClausePos, sql, "LAST_INSERT_ID", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) == -1;
}
}
return true;
}
所以这里有一个比较特殊的case,如果SQL中包含select,则rewriteBatchedStatements失效。比如下面这个例子,表结构如下:
mysql> desc t1;
+------------+----------+------+------+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+----------+------+------+---------+-------+
| id | int(11) | YES | | NULL | |
| selectNum | int(11) | YES | | NULL | |
| updateTime | datetime | YES | | NULL | |
+------------+----------+------+------+---------+-------+
3 rows in set (0.01 sec)
使用上面的方式进行批量插入,理想情况到数据库应该是
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。
if (this.canRewriteAsMultiValueInsert && session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()) {
buildRewriteBatchedParams(sql, session, encoding);
}
这里会解析value后的内容,比如insert into t (c1,c2) values (?,?)就是(?,?)。这样SQL和value的parseInfo都有了。
excuteBatch
我们直接看如果符合canRewriteAsMultiValueInsertAtSqlLevel是如何处理的,在executeBatchedInserts。
获取这个insert batch有几个需要做合并的,这里的batchedArgs是程序执行addBatch()时候赋值的。
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(?,?),(?,?)了。
final byte[][] sqlStrings = this.staticSql;
final int sqlStringsLength = sqlStrings.length;
for (int i = 0; i < sqlStringsLength; i++) {
size += sqlStrings[i].length;
size++; // for the '?'
}
StringBuilder buf = new StringBuilder(size);
for (int i = 0; i < sqlStringsLength - 1; i++) {
buf.append(StringUtils.toString(sqlStrings[i], this.charEncoding));
buf.append("?");
}
buf.append(StringUtils.toString(sqlStrings[sqlStringsLength - 1]));
return buf.toString();
这样整个拼接流程就结束了。可以看到,有的时候虽然我们设置了rewriteBatchedStatements=true,但在某些特殊的语句格式下,拼接依然不能生效,在实现上,感觉JDBC实现的比较偷懒,比如不能有select,这个可能处于考虑到insert into ... select这种情况,但简单粗暴的禁止,也会给很多人带来困惑。
转载自:https://juejin.cn/post/7004385021330980901