在J2EE应用中,我们经常通过JDBC访问企业资源。但JDBC用的不好,将会影响系统的性能。本文参照John Goodson的《Performance Tips for the Data Tier(JDBC)》一文,写成此文,希望对我们的开发有所帮助。
本文从以下四个部分加以说明:
l 适当地使用数据库的元数据方法
l 检索需要的数据
l 选择优化性能的功能
l 管理连接和数据更新
1. 适当地使用数据库的元数据方法
1.1. 尽量少用元数据方法
由于元数据方法执行速度比较慢,故要尽量少用元数据方法。由于调用元数据方法产生结果集需要大量的开销,由元数据方法产生的结果集应该缓存起来,而不是多次执行查询,这样可以提供JDBC的性能。例如在应用中你调用了getTypeInfo一次,你就应该将结果集缓存起来,共应用再次使用。
1.2. 避免查询模式
给元数据提供null参数或查询模式将会产生耗时的查询。同时,由于一些不需要的数据通过网络传递,导致网络流量的增大,降低整个系统的性能。由于元数据方法执行比较慢,所以尽可能地给它提供非null参数和高效地调用它。而我们的应用常出现这样的现象:
ResultSet WSrs = WSc.getTables (null, null, "WSTable", null);
应该改成:
ResultSet WSrs = WSc.getTables ("cat1", "johng", "WSTable", "TABLE");
显然,在第一个getTables()调用中,应用可能需要知道WSTable表是否存在。当然, JDBC驱动按字面上的调用与解析请求不同。JDBC是这样解析请求的:返回所有的名称叫“WSTable”的表,视图,系统表,同义词,零时表,或在任何数据库目录中数据库的模式存在的别名。
第二个getTables()的调用更准确地反映了应用需要知道什么。JDBC这样解析这个请求:返回所有名叫“WSTable”存在与当前目录中模式为“johng’的所有表。显然,JDBC驱动处理第二个请求要比处理第一个请求来得更有效。
给元数据方法提供的信息越多,你得到的信息的准确性和性能也越高。
1.3. 使用哑元查询来确定表的特征
避免使用getColumns()确定一个表的特征。用getMedata()哑元查询替换之。考虑一个容许用户选择列的应用。应用应该用getColumns()返回用户列的信息还是准备一个哑元查询并调用getMetadata()呢?
情形1:getColumns方法
ResultSet WSrc = WSc.getColumns (... "UnknownTable" ...);// This call to getColumns() will generate a query to// the system catalogs... possibly a join// which must be prepared, executed, and produce// a result set. . .WSrc.next();string Cname = getString(4);. . .// user must retrieve N rows from the server// N = # result columns of UnknownTable// result column information has now been obtained
情形2:getMetadata方法
// prepare dummy queryPreparedStatement WSps = WSc.prepareStatement ("SELECT * from UnknownTable WHERE 1 = 0");// query is never executed on the server - only preparedResultSetMetaData WSsmd=WSps.getMetaData();int numcols = WSrsmd.getColumnCount();...int ctype = WSrsmd.getColumnType(n)...// result column information has now been obtained
在两个情形中,查询被送到服务器上。但在情形1中,查询必须被准备和执行,结果描述信息必须被简洁地表达,并且结果集必须送到客户端。在情形2中,一个简单的查询必须准备并且仅有结果描述信息被简洁地描述。显然,情形2是更好的性能模式。
这多少有些把这个讨论复杂化了,让我们考虑一个不支持本地准备SQL语句的数据库。情形1的性能没有变,但由于哑元查询必须被求值而不是仅仅准备,因此情形 2的性能稍微有些增加。因为查询语句的Where子句计算结果总是FALSE,因此查询没有产出结果行和不存取表数据的执行。在这个情形下,方法2仍然要比方法1做的好。
总之,总是使用结果集元数据检索表列信息,如列名,列数据类型和列精度和数值范围。当被请求的信息不能从结果记录集(例如,表列默认值)获取的时候,仅仅使用getColumns()方法。
2. 检索需要的数据
2.1. 检索长数据
除非必要,由于检索长数据会造成网络资源紧张而降低性能。通常大多数用户不需要看到长数据,如果用户需要看这些数据,应用再去检索。
我们的代码中长出现这样的代码:select * from <table name> …如果选择的表中有长数据列,那这个查询的性能将会非常糟糕。再说,表中的所有数据项你都需要吗?如果不需要,为什么要让它们在网络上传递,浪费网络资源?
例如,看看下边的JDBC代码:
ResultSet rs = stmt.executeQuery ( "select * from Employees where SSID = '999-99-2222'");rs.next();string name = rs.getString (4);
JDBC不是智能的。当你这样写代码的时候,它根本就不知道你真正需要那些列,它把所有的都返回当然是情理之中的事情了,所以开发的时候就劳烦把需要的列在Select语句中指明。如果Employees表中有照片之类的长数据字段,系统的性能之低就可想而知了。
尽管有方法getClob()和getBlod()支持这种长数据字段的检索,但并不是每个数据库都支持它。所以记住:需要长数据的时候再去读它。
2.2. 减少检索到的数据的大小
有时候,长数据必须被检索。在这种情况下,大多数用户可能不需要在屏幕看到100k(或更多)的正文。 为了减少网络流量和提高性能,你可以通过调用 setMaxRows(),setMaxFieldSize(),以及与驱动相关的setFetchSize()方法把检索到的数据大小减少到可管理的范围之内。另一个减少检索到的数据大小的方法是减少列的数量。如果驱动允许你定义包尺寸,使用最小的包尺寸将会满足你的需要。
记住:注意只返回你需要的行和列。如果你返回了五列而你只需要两列,性能就降低了??特别是不需要的结果中包含了长数据。
2.3. 选择正确的数据类型
检索和送出某种数据的类型的开销是很昂贵的。当设计数据库模式时,选择能最有效处理的数据类型。例如,整型要比浮点数和小数数据要快。浮点数根据数据库特殊的格式定义,通常是压缩格式。为了能被数据库通讯协议处理,这些数据必须被解压后再转换成不同的格式。
2.4. 检索记录集
由于数据库系统对滚动游标的有限支持,大多数JDBC驱动不能实现滚动游标。除非你确定数据库支持滚动记录集(例如,rs),否则不要调用rs.last ()和rs.getRow()去得到记录集有多少行。对模仿滚动游标的JDBC驱动而言,调用rs.last()会导致驱动为了到最后一行而通过网络检索所有的数据。可以替代的方法是你可以通过记录集枚举记录行数,或者通过提交在SELECT语句中一个带有COUNT列的查询得到行数。
一般情况下,不要写依赖于记录集行数的代码,因为为了取得行数,驱动必须读取记录集中的所有的行。
3. 选择优化性能的功能
3.1. 使用参数标记作为存储过程的参数
调用存储过程时,用参数标记做为参数尽量不要用字符做参数。JDBC驱动调用存储过程时要么象执行其他SQL查询一样执行该过程,要么通过RPC直接调用来优化执行过程。如果象SQL查询那样执行存储过程,数据库服务器先解析该语句,验证参数类型,然后把参数转换成正确的数据类型,显然这种调用方式不是最高效的。
SQL语句总是做为一个字符串送到数据库服务器上,例如,
“{call getCustName (12345)}”。
在这种情况下,即使程序员设想给getCustName唯一的参数是整型,事实上参数传进数据库的仍旧是字符串。数据库服务器解析该语句,分离出单个参数值12345,然后在把过程当作SQL语言执行之前,将字符串“12345”转换成整型值。
通过RPC在数据库服务器中调用存储过程,就能避免使用SQL字符串带来的开销。
情形1
在这个例子中,就不能使用服务器端的RPC优化调用存储过程。调用的过程包括解析语句,验证参数类型,在执行过程之前把这些参数转换成正确的类型。
CallableStatement cstmt = conn.prepareCall ( "{call getCustName (12345)}"); ResultSet rs = cstmt.executeQuery ();
情形2
在这个例子中,可以使用服务器端RPC优化调用存储过程。由于应用避免了文字参数传递带来的开销,且JDBC能以RPC方式直接在数据库中调用存储过程来优化执行,所以,执行时间也大大地缩短了。
CallableStatement cstmt =
conn.prepareCall ( "{call getCustName (?)}");cstmt.setLong (1,12345);
ResultSet rs = cstmt.executeQuery();
JDBC 根据不同的用途来优化性能,所以我们需要根据用途在PreparedStatement对象和Statement对象之间做出选择。如果执行一个单独的 SQL语句,就选择Statement对象;如果是执行两次或两次以上就选择PreparedStatement对象。
有时,为了提高性能我们可以使用语句池。当使用语句池时,如果查询被执行一次且可能再也不会被执行,那就使用Statement对象。如果查询很少被执行,但在语句池的生命期内可能再一次被执行,那么使用PreparedSatement。在相同的情形下,如果没有语句池,就使用Statement对象。
3.2. 用批处理而不是用PreparedStatement语句
更新大量的数据通常是准备一个INSERT语句并多次执行该语句,结果产生大量的网络往返。为了减少JDBC调用次数和提高性能,你可以使用 PreparedStatement对象的addBatch()方法一次将多个查询送到数据库里。例如,让我们比较一下下边的例子,情形1和情形2。
情形1:多次执行PreparedStatement语句
PreparedStatement ps = conn.prepareStatement("INSERT into employees values (?, ?, ?)");
for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.executeUpdate();
}
情形2:使用批处理
PreparedStatement ps =
conn.prepareStatement( "INSERT into employees values (?, ?, ?)");
for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.addBatch();
}
ps.executeBatch();
在情形1中,一个PreparedStatement用于多次执行一个INSERT语句。在这个情况下,为了100次插入需要101次网络往返,其中一次用于准备语句,额外100次网络往返用于执行每一个操作。当addBatch()方法的时候,如情形2所述,仅需要两个网络往返,一个准备语句,另一个执行批处理。尽管使用批处理需要更多的数据库CPU运算开销,但性能可由减少的网络往返获得。记住要让JDBC驱动在性能方面有良好的表现,就要减少 JDBC驱动和数据库服务器之间的网络通讯量。
3.3. 选择合适的游标
选择合适的游标能提高应用的灵活性。本节总结了三类游标的性能问题。向前游标对连续读表中所有的行提供了优秀的性能。就检索表数据而言,没有一个检索数据的方法要比向前游标更快。然而,当应用必须处理非连续方式的行时,就不能使用它。
对需要数据库高层次的并发控制和需要结果集向前和向后滚动能力的应用而言,JDBC驱动使用的无感知游标是最为理想选择。对无感知游标的第一次请求是获取所有的行(或者当JDBC使用“懒惰”方式读时,可以读取部分行)并将它们存储在客户端。那么,第一次请求将会非常慢,特别是当长数据被检索到的时候。后续的请求不再需要网络交通(或当驱动采用懒惰方式时,只有有限的网络交通)并处理得很快。由于第一次请求处理缓慢,无感知游标不应该用于一行数据的单个请求。当要返回长数据时,内存很容易被耗尽,所以开发人员也应该避免使用无感知游标。一些无感知游标的实现是把数据缓存在数据库中的零时表中,避免了性能问题,但是,大多数是把信息缓存在应用本地。
无感知游标,有时又叫键集驱动的游标,使用标识符,如已经存在于你数据库中的ROWID。当你通过结果集滚动的时候,适合于标识符的数据会被检索到。由于每个请求都产生网络交通量,所以性能将会非常差。然而,返回非连续行不会更多的影响性能。
为了更进一步说明,我们来看一个通常返回应用1000行数据的应用。在执行时或第一行被请求时,JDBC不会执行由应用提供的SELECT语句。而是 JDBC驱动用键标识符替换查询的SELECT列表,例如,ROWID。这个修改的查询将会被驱动执行,并且所有1000键值将会被从数据库中检索出来并被驱动缓存。每一个来自应用对结果行的请求将转到JDBC驱动,为了返回合适的行,JDBC在它本地缓存中查询键值,构造一个类似于 “WHERE ROWID=?”包含WHERE的优化的语句,执行这个修改了查询,然后从服务器上检索单个结果行。
当应用使用来自缓存中的无感知(Insensitive)游标数据时,有感知(Sensitive)游标在动态情形下就是首选的游标模式。
3.4. 有效地使用get方法
JDBC 提供了很多从结果集中检索数据的方法,例如getInt(),getString(),以及getObject()。getObject()方法是最普通的方法,但在没有说明非默认映射时提供了最差的性能。这是因为为了确定被检索值的类型和产生合适的映射,JDBC驱动必须做额外的处理。所以,总是使用能明确数据类型的方法。
为了更好地提高性能,请提供被检索列的列数字,例如,getString(1),getLong(2),和 getInt(3),而不是列名。如果列数字没有说明,网络流量是不受影响的,但转换和查找的成本上升了。例如,假设你使用getString (“foo”)…驱动可能不得不将列的标识符foo转换成大写(如果必要),并在列列表中用“foo”和所有的列名比较。如果提供了列数字,很大部分的处理都被节省了。
例如,假如你有一个15列100行的结果集,列名没有包括在结果集中。你感兴趣的有三列,EMPLOEEMENT(字符串),EMPLOYEENUMBER(长整型),和SALARY(整型)。如果你说明了getString(“EmployeeName”), getLong(“EmployeeNumber”)和getInt(“Salary”),每列的列名必须转换成和数据库元数据中匹配的大小写,毫无疑问查询将相应的增加。如果你说明getString(1),getLong(2),和getInt(15),性能将会大大地提高。
3.5. 检索自动产生的键
许多数据库已经隐藏了描述表中每行唯一键的列(又叫伪列)。通常,由于伪列描述了数据的物理磁盘地址,故而在查询中使用这种类型的列存取行是最快的方式。在JDBC3.0以前,应用仅能在插入数据之后立即执行SELECT语句检索到伪列的值。
For example:
//insert rowint
rowcount = stmt.executeUpdate ( "insert into LocalGeniusList (name) values ('Karen')");
// now get the disk address - rowid - for the newly inserted row
ResultSet rs = stmt.executeQuery ( "select rowid from LocalGeniusList where name = 'Karen'");
这个检索伪列的方法有两个主要的缺点。第一,检索伪列需要通过网络把一个单独的查询语句发送到服务器上执行。第二,由于表中可能没有主键,查询条件可能不能唯一地确定行。在后边的情形中,多个伪列值被返回,应用或许不能确定哪个是最近插入的行。
JDBC规范一个可选的特性是当行插入表时,能检索到行的自动产生的键信息。
For example:
int rowcount = stmt.executeUpdate ( "insert into LocalGeniusList (name) values ('Karen')",
// insert row AND return
keyStatement.RETURN_GENERATED_KEYS);
ResultSet rs = stmt.getGeneratedKeys ();
// key is automatically available
即便该表没主键,这都给应用提供了一个唯一确定行值的最快方法。当存取数据时,检索伪列键的能力给JDBC开发人员提供了灵活性并创造了性能。
4. 管理连接和数据更新
4.1. 管理连接
连接管理的好坏直接影响到应用的性能。采用一次连接创建多个Statement对象的方式来优化你的应用,而不是执行多次连接。在建立最初的连接之后要避免连接数据源。
一个不好的编码习惯是执行SQL语时连接和断开好几次。一个连接对象可以有多个Statement对象和它关联。由于Statement对象是定义SQL 语句信息的内存存储,它能管理多个SQL语句。此外,你可以使用连接池来显著地提高性能,特别是对那些通过网络连接或通过WWW连接的应用。连接池让你重用连接,关闭连接不是关闭与数据库的物理连接,而是将用完的连接放到连接池中。当一个应用请求一个连接时,一个活动的连接将从连接池中取出重用,这样就避免了创建新连接的而产生的网络I/O。
4.2. 在事务中管理提交
由于磁盘I/O和潜在的网络I/O,提交事务往往要慢。经常使用WSConnection.setAutoCommit(false)来关闭自动提交设置。
提交实际上包括了什么呢?数据库服务器必须刷新包含更新的和新数据的磁盘上的每一个数据页。这通常是一个对日志文件连续写的过程,但也是磁盘I/O。默认情况下,当连接到数据源时,自动提交是打开的,由于提交每个操作需要大量的磁盘I/O,自动提交模式通常削弱了性能。此外,大部分数据库没有提供本地的自动提交模式。对这种类型的服务器,JDBC驱动对每一个操作必须明确地给服务器送出COMMIT语句和一个BEGIN TRANSACTION。
尽管使用事务对应用的性能有帮助,但不要过度地使用。由于为了防止其他用户存取该行而在行上长时间的持有锁将减少吞吐量。短时间内提交事务可以最大化并发量。
4.3. 选择正确的事务模式
许多系统支持分布式事务;也就是说,事务能跨越多个连接。由于记录日志和所有包含在分布式事务中组件(JDBC驱动,事务监视器和数据库系统)之间的网络 I/O,分布式事务要比普通的事务慢四倍。除非需要分布式事务,否则尽量避免使用它们。如果可能就使用本地事务。应该注意的是许多Java应用服务器提供了一个默认的利用分布式事务的事务行为。为了最好的系统性能,把应用设计在运行在单个连接对象之下,除非必要避免分布式事务。
4.4. 使用updateXXX方法
尽管编程的更新不适用于所有类型的应用,但开发人员应该试着使用编程的更新和删除,也就是说,使用ResultSet对象的updateXXX()方法更新数据。这个方法能让开发人员不需要构建复杂的SQL语句就能更新数据。为了更新数据库,在结果集中的行上移动游标之前,必须调用updateRow() 方法。
在下边的代码片断中,结果集对象rs的Age列的值使用getInt()方法检索出来,updateInt()方法用于用整型值25更新那一列。UpdateRow()方法用于在数据库中更新修改了值的行。
int n = rs.getInt("Age");
// n contains value of Age column in the resultset rs...
rs.updateInt("Age", 25);
rs.updateRow();
除了使应用更容易地维护,编程更新通常产生较好的性能。由于指针已经定位在被更新的行上,定位行的所带来的开销就避免了。
4.5. 使用getBestRowIdentifier()
使用getBestRowIdentifier()(请参阅DatabaseMetaData接口说明)确定用在更新语句的Where子句中的最优的列集合。伪列常常提供了对数据的最快的存取,而这些列仅能通过使用getBestRowIdentifier()方法来确定。
一些应用不能被设计成利用位置的更新和删除。一些应用或许通过使用可查询的结果列,如调用getPrimaryKeys()或者调用getIndexInfo()找出可能是唯一索引部分的列,使Where子句简洁化。这些方法通常可以工作,但可能产生相当复杂的查询。看看下边的例子:
ResultSet WSrs = WSs.executeQuery ("SELECT first_name, last_name, ssn, address, city, state, zip FROM emp");
// fetch data...
WSs.executeUpdate ("UPDATE EMP SET ADDRESS = ? WHERE first_name = ? and last_name = ? and ssn = ? and address = ? and city = ? and state = ? and zip = ?");
// fairly complex query
应用应该调用getBestRowIdentifier()检索最优集合的能确定明确的记录的列(可能是伪列)。许多数据库支持特殊的列,它们没有在表中被用户明确地定义,但在每一个表中是“隐藏”的列(例如,ROWID和TID)。由于它们是指向确切记录位置的指针,这些伪列通常给数据提供了最快的存取。由于伪列不是表定义的部分,它们不会从getColumns中返回。为了确定伪列是否存在,调用getBestRowIndentifier()方法。
再看一下前边的例子:
...
ResultSet WSrowid = getBestRowIdentifier(... "emp", ...);
...
WSs.executeUpdate ("UPDATE EMP SET ADDRESS = ? WHERE ROWID = ?";
// fastest access to the data!
如果你的数据源没有包含特殊的伪列,那么getBestRowIdentifier()的结果集由指定表上的唯一索引组成(如果唯一索引存在)。因此,你不需要调用getIndexInfo来找出最小的唯一索引。