MySQL Server-Side Cursor及OceanBase是否支持此功能
背景与目的
从过往经验,从Informix到Oracle到DB2,应用使用这些数据库时,都会被强调:在语句中尽量使用变量参数,然后Prepare后可多次使用,减少数据库Server端语句硬解析带来的性能损耗,减少数据库Server端语句Cache空间的占用。
另外,不管是显式声明还是隐式产生,数据库Server端都会为Select语句(或者所有DML语句)一个Cursor(游标),通过Fetch这个Cursor来获取结果集。如果结果集小,一次就可全部取得整个结果集;如果结果集大,就需要多次交互,每次由Server端从库中读取一部分数据返回。每次获取的记录数,可通过参数控制,以在减少交互提高性能与客户端内存资源耗用间权衡。比如Oracle,JDBC是通过fetchsize、OCI是通过OCIStmtFetch函数参数nrows、Pro*C是通过宿主变量数组大小,来控制每次从Server端获取多少条记录回来。
由于上面提到的Prepare与Cursor功能,都是在数据库Server端实现的,所以可以被叫做Server-Side Prepared Statement与Server-Side Cursor。
最近因为某些原因,需要了解下OceanBase分布式数据库,OB对外是兼容MySQL数据库协议,也就看了下MySQL JDBC对Server-Side Prepared Statement和Server-Side Cursor的支持程度,以及OceanBase数据库Server是否支持这两个功能。
MySQL JDBC的实现
这部分内容的主要参考内容是MySQL Connector/J(JDBC) Reference及JDBC源码、MySQL Server源码。
Server-Side Prepared Statement
MySQL JDBC实现了两种Prepared Statement:
Client-Side Prepared Statement
此种方式是由JDBC在客户端模拟对Prepared Statment的功能,实际发往数据库Server端的是已经将变量参数值代入后拼装出来的SQL语句,变量参数值不同就是不同的SQL语句。
这也是MySQL JDBC的默认使用的方式,官方解释的原因是:
default because early MySQL versions did not support the prepared statement feature or had problems with its implementation
Server-Side Prepared Statement
为了使用这种方式,需要设置属性useServerPrepStmts的值为true,通过:
- com.mysql.jdbc.jdbc2.optional.MysqlDataSource 或 com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource 的 set 方法
- 传给DriverManager.getConnection() 或 Driver.connect() 的 Properties 键值对
- JDBC URL连接串
让我们来大致看一下JDBC源代码中对Server-Side Prepared Statement的支持实现。
首先,在java.sql.Connection实现类com.mysql.jdbc.ConnectionImpl中方法prepareStatement():
public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
synchronized (getConnectionMutex()) {
....
if (this.useServerPreparedStmts && canServerPrepare) {
if (this.getCachePreparedStatements()) {
synchronized (this.serverSideStatementCache) {
....
if (pStmt == null) {
try {
pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
resultSetConcurrency);
...
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException sqlEx) {
....
}
} else {
try {
pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException sqlEx) {
...
}
} else {
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
}
return pStmt;
}
}
从上面代码可以看出,如果属性useServerPreparedStmts为true,且canServerPrepare也为真,就会使用ServerPreparedStatement类来构造Statement供后续执行SQL使用。属性useServerPreparedStmts是在哪里设置的呢? 是在ConnectionImpl类的initializePropsFromServer()方法中:
if (getUseServerPreparedStmts() && versionMeetsMinimum(4, 1, 0)) {
this.useServerPreparedStmts = true;
if (versionMeetsMinimum(5, 0, 0) && !versionMeetsMinimum(5, 0, 3)) {
this.useServerPreparedStmts = false; // 4.1.2+ style prepared
// statements
// don't work on these versions
}
}
而getUseServerPreparedStmts()方法读取了连接属性中的detectServerPreparedStmts属性值:
public boolean getUseServerPreparedStmts() {
return this.detectServerPreparedStmts.getValueAsBoolean();
}
属性detectServerPreparedStmts在这里被定义:
// Think really long and hard about changing the default for this many, many applications have come to be acustomed to the latency profile of preparing
// stuff client-side, rather than prepare (round-trip), execute (round-trip), close (round-trip).
private Boolean