解析 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