PreparedStatement
是如何大幅度提高性能的
作者:
Billy Newport
本文讲述了如何正确的使用
prepared statements
。为什么它可以让你的应用程序运行的更快,和同样的让数据库操作变的更快。
为什么
Prepared Statements
非常重要?如何正确的使用它?
数据库有着非常艰苦的工作。它们接受来自众多并发的客户端所发出的
SQL
查询,并尽可能快的执行查询并返回结果。处理
statements
是一个开销昂贵的操作,不过现在有了
Prepared Statements
这样的方法,可以将这种开销降到最低。可是这种优化需要开发者来完成。所以本文会为大家展示如何正确的使用
Prepared Statements
才能使数据库操作达到最优化。
数据库是如何执行一个
statement
的?
显然,我不会在这里写出很多的细节,我们只关注最关键的部分。当一个数据库收到一个
statement
后,数据库引擎会先解析
statement
,然后检查其是否有语法错误。一旦
statement
被正确的解析,数据库会选出执行
statement
的最优途径。遗憾的是这个计算开销非常昂贵。数据库会首先检查是否有相关的索引可以对此提供帮助,不管是否会将一个表中的全部行都读出来。数据库对数据进行统计,然后选出最优途径。当决创建查询方案后,数据库引擎会将它执行。
存取方案(
Access Plan
)的生成会占用相当多的
CPU
。理想的情况是,当我们多次发送一个
statement
到数据库,数据库应该对
statement
的存取方案进行重用。如果方案曾经被生成过的话,这将减少
CPU
的使用率。
Statement Caches
数据库已经具有了类似的功能。它们通常会用如下方法对
statement
进行缓存。使用
statement
本身作为
key
并将存取方案存入与
statement
对应的缓存中。这样数据库引擎就可以对曾经执行过的
statements
中的存取方案进行重用。举个例子,如果我们发送一条包含
SELECT a, b FROM t WHERE c = 2
的
statement
到数据库,然后首先会将存取方案进行缓存。当我们再次发送相同的
statement
时,数据库会对先前使用过的存取方案进行重用,这样就降低了
CPU
的开销。
注意,这里使用了整个
statement
为
key
。也就是说,如果我们发送一个包含
SELECT a, b FROM t WHERE c = 3
的
statement
的话,缓存中不会没有与之对应的存取方案。这是因为“
c=3
”与曾经被缓存过的“
c=2
”不同。所以,举个例子:
for (int i = 0; i < 1000; i++) {
PreparedStatement ps = conn.prepareStatement("select a,b from t where c = " + i);
ResultSet rs = Ps.executeQuery();
rs.close();
ps.close();
}
在这里缓存不会被使用,因为每一次迭代都会发送一条包含不同
SQL
语句的
statement
给数据库。并且每一次迭代都会生成一个新的存取方案。现在让我们来看看下一段代码:
PreparedStatement ps = conn.prepareStatement("select a,b from t where c = ?");
for (int i = 0; i < 1000; i++) {
ps.setInt(1, i);
ResultSet rs = ps.executeQuery();
rs.close();
ps.close();
}
这样就具有了更好的效率,这个
statement
发送给数据库的是一条带有参数“?”的
SQL
语句。这样每次迭代会发送相同的
statement
到数据库,只是参数“
c=?
”不同。这种方法允许数据库重用
statement
的存取方案,这样就具有了更好的效率。这可以让你的应用程序速度更快,并且使用更少的
CPU
,这样数据库服务器就可以为更多的人提供服务。
PreparedStatement
与
J2EE
服务器
当我们使用
J2EE
服务器时事情会变的比较复杂。通常,一个
perpared statement
会同一个单独的数据库连接相关联。当数据库连接被关闭时
prepared statement
也会被丢弃。通常,一个胖客户端会获取一个数据库连接并将其一直保持到退出。它会用“饿汉”
(eagerly)
或“懒汉”
(lazily)
方式创建所有的
parepared statements
。“饿汉”方式会在应用启动时创建一切。“懒汉”方式意味着只有在使用的时候才去创建。“饿汉”方式会使应用程序在启动的时候梢有延迟,但一旦启动后就会运行的相当理想。“懒汉”方式使应用程序启动速度非常快(但不会做任何准备工作),当需要使用
prepared statement
的时候再去创建。这样,在创建全部
statement
的过程中,性能是非常不稳定的,但一旦创建了所有
statement
后,它会像“饿汉”式应用程序一样具有很好的运行效果。请根据你的需要来选择最好的方式,是快速启动?还是前后一致的性能。
J2EE
应用的问题是它不会像这样工作,连接只会在请求期间被保持。那意味着必须每一次请求的时候都创建
prepared statement
。这远没有胖客户端那种一直保持
prepared statement
的执行性能好。
J2EE
厂商已经注意到了这个问题,并且提供了连接池
(ConnectionPool)
以避免这种问题。
当
J2EE
服务器提供了一个连接给你的应用程序时,其实它并没有给你真正的数据库连接,你只是获得了一个包装器(
Wrapper
)。你可以去看看你所获得的连接的类名以证实这一点。它并不是一个
JDBC
连接,而是一个由应用服务器创建的类。所有的
JDBC
操作都会被应用服务器的连接池管理器所代理。所有的
JDBC ResultSets
,
statements
,
CallableStatements
,
preparedStatements
等都会被包装并以一个“代理对象”(
Proxy Object
)的形式返回给应用程序。当你关闭了连接,这些对象会被标记为失效,并被垃圾回收器所回收。
通常,如果你对一个数据库连接执行
close
,那这个连接会被
JDBC
驱动程序关闭。但我们需要在
J2EE
服务器执行
close
的时候数据库连接会被返回连接池。我们可以创建一个像真正的连接一样的
JDBC Connection
代理类来解决这个问题。它有一个对真正连接的引用。当我们执行一个连接上的方法时,代理会将操作转给真正的连接。但是,当我们对一个连接执行
close
时,这个连接并不会关闭,而是会送回连接池,并可以被其他请求所使用。一个已被准备过的
prepared statement
也会因此而得到重用。
J2EE PreparedStatement Cache
J2EE
服务器的连接池管理器已经实现了缓存的使用。
J2EE
服务器保持着连接池中每一个连接准备过的
prepared statement
列表。当我们在一个连接上调用
preparedStatement
时,应用服务器会检查这个
statement
是否曾经准备过。如果是,这个
PreparedStatement
会被返回给应用程序。如果否,调用会被转给
JDBC
驱动程序,然后将新生成的
statement
对象存入连接缓存。
每个连接都有一个缓存的原因是因为:
JDBC
驱动程序就是这样工作的。任何
prepared statement
都是由指定的连接所返回的。
如果我们想利用这个缓存的优势,那就如前面所说的,使用参数化的查询语句可以在缓存中找到曾经使用过的
statement
。大部分应用服务器允许你调整
prepared statements
缓存的大小。
摘要
我们绝对应该使用包含参数化的查询语句的
prepared statement
。这样数据库就会重用准备过的存取方案。缓存适用于整个数据库,所以,如果你安排所有的应用程序使用相同的参数化
SQL
语句,然后你的其他应用程序就可以重用被准备过的
prepared statement
。这是应用服务器的一个优势,因为所有的数据库操作都集中在数据库操作层(
Database Access Layer
,包括
O/R
映射,实体
Bean
,
JDBC
等)。
第二,正确的使用
prepared statement
也是利用
prepared statement
的缓存优势的关键。由于应用程序可以重用准备过的
prepared statement
,也就减少了调用
JDBC
驱动程序的次数,从而提高了应用程序的性能。这样就拥有了可以与胖客户端比肩的效率,却又不需要总维持一个连接。
使用参数化的
prepared statement
,你的应用程序会具有更好的性能。