 | Level: Introductory Krishnakumar PoolothInfosys Technologies Limited
01 Apr 2002 Typical decision support systems load and insert large volumes of data periodically. Large scale Decision Support Systems are commonly deployed on partitioned environments. Buffered Inserts is a programming technique offered by DB2 to achieve optimal performance for insert intensive workloads on partitioned environments. This article looks at how to leverage this feature from Java. It will also study the performance gains using this technique under different configurations like varying row lengths and commit frequencies.
Introduction
Typical decision support systems (DSSs) load and insert large volumes of data periodically. Large scale DSSs are commonly deployed on partitioned environments. Because of the popularity of the JavaTM programming language, this article describes how you can leverage the buffered insert option of DB2® Universal DatabaseTM from Java to achieve optimal performance for insert-intensive workloads on partitioned environments.
This article assumes that you have some familiarity with Java, SQLJ and JDBC. Note that the concept explained in this article will only work with DB2 UDB
EEE, the version of UDB that supports database partitioning.
Comparing JDBC and SQLJ
JDBC technology is an API that lets you access virtually any tabular data source from the Java programming language. It provides cross-DBMS connectivity to a wide range of SQL databases. The JDBC API allows developers to take advantage of the Java platform's "Write Once, Run Anywhere" capabilities for industrial strength, cross-platform applications that require access to enterprise data.
SQLJ is an ANSI-standard way of coding SQL access in Java. SQLJ is more concise and thus easier to write than JDBC, and provides compile-time schema validation and syntax checking for easier debugging. In this approach, SQL statements are embedded within Java program code and the SQLJ pre-compiler is used to convert the embedded SQL statements into Java method calls. However, the code generated by the SQLJ pre-compiler may not always be optimal. For more information about SQLJ, see the SQLJ tutorial.
The power of partitioned database
Because data is divided across database partitions, it is possible to harness the power of multiple processors on multiple physical nodes to satisfy user requests. Data retrieval and update requests are split automatically into sub-requests, and executed in parallel among the applicable database partitions. The fact that a database is partitioned into multiple database partitions is transparent to applications issuing SQL statements.
Application interaction occurs through one database partition, known as the coordinator node for that application. The coordinator runs on the same database partition as the application, or in the case of a remote application, the database partition to which that application is connected. Any database partition can act as a coordinator node. For more information about partitioned databases, see Clustering for Scalability.
DB2 offers some special programming features to achieve optimal performance in partitioned environments. Buffered inserts is an example of a technique that can give substantial performance gains for insert-intensive workloads typical of decision support systems.
In this article, we will:
- Take a detailed look at buffered inserts.
- Write a SQLJ based table interface class. For simplicity, this class will only support the interface for inserting data into a table.
- Explain RTStatement object reuse as a technique to improve performance.
- Benchmark the performance of JDBC, basic SQLJ and SQLJ with RTStatement reuse for varying commit frequencies. We will also study the effect of row length on buffered inserts.
Overview of buffered inserts
On partitioned environments, DB2 UDB can take advantage of table queues to buffer the rows being inserted. In insert-intensive applications like DSSs, this feature can give substantial performance improvement. To use buffered inserts, an application must be prepared or bound with the INSERT BUF option enabled. The differences between a simple insert and a buffered insert are discussed below.
Simple inserts in partitioned databases
The flow of events for a simple database insert (without buffering) in a partitioned environment is shown in Figure 1.
Figure 1. Simple insert processing in a partitioned environment
- The coordinator agent receives the row from the client application.
- The coordinator passes this to the database manager at that node.
- The database manager applies the hashing logic on the partitioning key values and determines the target node.
- The target node receives the row and inserts it locally.
- The target node sends a response back to the coordinator agent.
- The coordinator passes the response back to the client.
Buffered inserts in partitioned databases
As shown in Figure 2, the flow of events is slightly different if the application is bound with the INSERT BUF option enabled.
Figure 2. Buffered inserts in a partitioned environment
- The database manager opens a 4 KB page for each node on which this table is present.
- The database manager, after receiving the row from the coordinator agent, applies the hashing logic and determines the target node.
- The row is then placed into the corresponding page.
- The database manager returns control to the application.
- If this connection is used only to insert rows into the same table, the buffer would be flushed when it is full or at a commit or rollback statement. Typically, any statement other than insert statement into this same table would cause the buffer to be flushed. Examples could be insert into another table, delete from table, update table, etc.
Refer to DB2 UDB 7.2 Application Development Guide -- Chapter 18: Programming Considerations in a Partitioned Environment to get a full list of statements that would flush the buffer.
Buffered inserts give better performance by:
- Reducing communications overhead between nodes by sending a single message from the target node to the coordinator node for each buffer received by the target.
- Allowing the coordinator node to receive new rows while insertions are being done across other nodes.
To take advantage of buffered inserts you must bind the application with the INSERT BUF option enabled. In JDBC, the SQL statements are dynamically bound at runtime. Because of this limitation, code written based on JDBC cannot use buffered inserts. However, it is possible to use this feature in SQLJ- based database code.
SQLJ and buffered inserts-- an example
Let us take, as an example, a Java program that inserts rows into a table defined as follows:
create table testtable (
id integer not null,
name char(20) not null,
age smallint not null,
address char(74) not null )
partitioning key (id);
alter table testtable add primary key(id);
|
The column named id is both the primary key and partitioning key for the table.
We will implement the code to insert data into this table using two classes:
- TestTableRow.java, which represents a single row in the table, and is shown in Listing 1.
Listing 1. TestTableRow.java class
// TestTableRow.java
/**
* Represents a row of data in TestTable
*/
public class TestTableRow {
public int id;
public String name;
public short age;
public String address;
}
|
In Listing 1, we have kept the member variables public for simplicity.
- TestTableInserter.sqlj, which provides the interface for inserting a row into the table.
The class shown in Listing 2 implements the insert operation for testtable.
Listing 2. TestTableInserter.sqlj for insert operations
// TestTableInserter.sqlj
import java.sql.*;
/**
* Implements the method to insert a row into testtable
*/
public class TestTableInserter {
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ref.DefaultContext _defaultContext;
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ExecutionContext _executionContext ;
/**
* Sole Constructor. Sets the protected DefaultContext variable and initializes the
* ExecutionContext to be used for the insert statement.
* @param connCtx DefaultContext object used for SQLJ operations
*/
public TestTableInserter( sqlj.runtime.ref.DefaultContext connCtx) {
_defaultContext = connCtx;
// This instance of ExecutionContext will be used in the insert operation
_executionContext = new sqlj.runtime.ExecutionContext( );
}
/**
* Inserts a single row into the testtable.
* @param row TestTableRow object containing data to be inserted
* @throws SQLException when database error occurs
*/
public void insertRow(TestTableRow row) throws SQLException {
int id = row.id;
String name = row.name;
short age = row.age;
String address = row.address;
#sql [_defaultContext,_executionContext] { insert into testtable values
(:id, :name, :age , :address) };
}
}
|
Run the SQLJ translator (sqllib\bin\sqlj.exe) on TestTableInserter.sqlj. This replaces the SQL statements in the SQLJ program with the Java source statements and generates a serialized profile containing information about the SQL statements in the SQLJ source file.
The code generated by the translator is shown in Appendix A.
By default, apart from creating this java file, the translator also performs the following steps:
- Compiles TestTableInserter.java to generate two class files: TestTableInserter.class and TestTableInserter_SJProfileKeys.class.
- Generates TestTableInserter_SJProfile0.ser, which contains profile information about the SQL statements embedded in the original source.
For an overview of SQL processing, see John Campbell's Meet the Experts topic on this subject.
Now, run the db2profc command to install the DB2 SQLJ customizers on generated profiles and to create the DB2 packages in the target DB2 database.
db2profc -user=dbuser -password=dbpwd -url=jdbc:db2:dbname -prepoptions=
"bindfile using TestTableInserter.bnd package using TTblIns insert buf"
TestTableInserter_SJProfile0.ser
|
Note: It is the insert buf option specified as part of prepoptions property for the db2profc command that makes this insert statement a candidate for buffered inserts. The default value of this option, insert def, disables buffered inserts.
Optimizing by reusing RTStatement object
The code generated by the translator replaces the embedded insert statement with the following code fragment shown in Listing 3.
Listing 3. Code generated by SQLJ translator
1 {
2 sqlj.runtime.ConnectionContext __sJT_connCtx = _defaultContext;
3 if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
4 sqlj.runtime.ExecutionContext __sJT_execCtx = _executionContext;
5 if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
6 int __sJT_1 = id;
7 String __sJT_2 = name;
8 short __sJT_3 = age;
9 String __sJT_4 = address;
10 synchronized (__sJT_execCtx) {
11 sqlj.runtime.profile.RTStatement __sJT_stmt = __sJT_execCtx.registerStatement
12 (__sJT_connCtx, TestTableInserter_SJProfileKeys.getKey(0), 0);
13 try
14 {
15 __sJT_stmt.setInt(1, __sJT_1);
16 __sJT_stmt.setString(2, __sJT_2);
17 __sJT_stmt.setShort(3, __sJT_3);
18 __sJT_stmt.setString(4, __sJT_4);
19 __sJT_execCtx.executeUpdate();
20 }
21 finally
22 {
23 __sJT_execCtx.releaseStatement();
24 }
25 }
26 }
|
It is evident from Listing 3 that the generated code creates (line number: 11) and destroys (line number: 23) an instance of RTStatement class for each call to the insertRow function. This is overhead. As shown in the SQLJStmtCachedTestTableInserter class in Appendix B, I modified the code to remove this overhead, as follows:
- I added a protected variable, _insertStatement, to store the instance of RTStatement.
- I added throws SQLException clause to the constructor, because the call to registerStatement can throw an SQLException.
- I moved the registerStatement call in the insertRow method to the constructor of TestTableInserter.
- The insertRow method now uses the protected instance of RTStatement. Instead of creating new RTStatement object each time insertRow is called, the object is now created only once in the constructor and is used for any number of insertRow calls.
- The insertRow method allows SQLException to be thrown. In addition, since _insertStatement is an instance variable of this class, there is no need to destroy the instance of RTStatement class after each call. Hence, I removed the try and finally blocks within the insertRow method.
- I added a finalize method to free the RTStatement object during garbage collection.
Benchmark results
Performance gains using buffered inserts were studied for the following scenarios.
- Effects of row length on buffered inserts.
- Effects of commit frequency on buffered inserts.
The deployment configuration shown in Figure 3 was used to perform the different test cases.
Figure 3. Benchmark configuration
In all test cases scenarios, the client application was connecting to Node 1, which is the catalog node of the test database. In addition, the test data used in the study was such that it was almost equally divided between the two nodes.
The configurations of the various machines involved are shown in Table 1.
|
Table 1: Machine configurations
| | System | OS | CPU | Memory | Software | | Node 1 - Catalog | Win NT 4.0 (SP5) | Pentium III 647 | 196 MB | IBM DB2 UDB EEE 7.2 | | Node 2 | Win NT 4.0 (SP5) | Pentium III 647 MHz | 196 MB | IBM DB2 UDB EEE 7.2 | | Client | Win 2000 Professional | Pentium III 800 MHz | 128 MB | IBM DB2 UDB EEE 7.2 Client |
SQLJStmtCachedTestTableInserter (RTStatement reused) and TestTableInserter classes were used to study the effects of row lengths on buffered inserts. A client application was used to insert 10000 rows into testtable. The row size was varied from 0.1 KB to 4 KB by changing the data type of the address column. A single commit at the end of the test program was used to minimize the effect of commit on buffered inserts. The test was conducted by both enabling and disabling buffered inserts. Insert performance using plain JDBC code was also measured for different row lengths. The results were as shown in Figure 4.
Figure 4. Effect of row size on buffered inserts
The results show the performance advantage using buffered inserts. For smaller row lengths, the number of inserts per second with buffered insert was many times higher compared to simple inserts. As the row length increases, the performance gains due to buffered inserts diminishes rapidly. This is because the number of rows the buffer can hold before it is flushed becomes smaller as the row size increases.
As discussed before, a commit statement (among others) would result in flushing of buffers used in buffered inserts. Hence as commit frequency increases, the performance gains due to buffering diminishes. Figure 5 compares the effect of commit frequency when using:
The listing in Appendix D shows the test program used to test both the SQLJ variants.
Figure 5. Effect of commit frequency on buffered inserts
The results show that the performance advantage of using buffered inserts decreases as commit frequency increases and as row sizes become larger.
Both the tests were conducted on a two-node configuration. Note that the performance would improve near-linearly as we increase the number of database nodes.
Limitations
While designing applications using buffered inserts, the following limitations of this feature must be given due consideration.
- Buffering inserts is a technique useful only for optimizing frequent inserts into a table. It is not advisable to use this technique for typical OLTP workloads that involve a mixture of inserts, updates, and deletes on a set of tables.
- An error detected during the insertion of a group of rows will cause all the rows of that group to be backed out. A group of rows is defined as all the rows inserted using a buffered insert statement:
- From the beginning of the unit of work.
- Since the statement was prepared (if it is dynamic), or
- Since the previous execution of another statement that closes or flushes a buffered insert.
- Because of their asynchronous nature, buffered inserts show certain behaviors that affect application programs. Certain error conditions may not get reported as part of the insert itself. They may get reported on any statement that closes the buffered insert statement. For example, you could get an SQL error code of 803 (unique key violation) while issuing a commit statement.
- Rows inserted will not be immediately visible through a SELECT statement using a cursor. An application should not depend on cursor-selected rows if it is using buffered inserts.
Conclusion
On partitioned environments, it is possible to achieve very high insert performance using buffered inserts. To take advantage of this feature with Java, the code must be based on SQLJ. It is useful for applications performing large number of inserts into the same table. The performance gains are substantial for tables with small row lengths.
Supporting files
| Description | File type | File size | Download method | | bufferedInsertSource.zip | zip | 10 KB | HTTP
Download
|
Acknowledgments
The author wishes to acknowledge and thank Srinivas Thonse at Infosys for the thorough technical
review of the initial draft of this article and the encouragement; Cass Squire and Glen Sheffield at IBM for
several thoughtful remarks and guidance on getting this work published; and
Kathy Zeidenstein for her thorough editorial review.
Appendix A
The code generated by the sqlj translator is as listed below:
/*@lineinfo:filename=TestTableInserter*//*@lineinfo:user-code*//*@lineinfo:1^1*///
TestTableInserter.sqlj
import java.sql.*;
/**
* Implements the method to insert a row into testtable
*/
public class TestTableInserter {
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ref.DefaultContext _defaultContext;
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ExecutionContext _executionContext ;
/**
* Sole Constructor. Sets the protected DefaultContext variable and initializes the
* ExecutionContext to be used for the insert statement.
* @param connCtx DefaultContext object used for SQLJ operations
*/
public TestTableInserter( sqlj.runtime.ref.DefaultContext connCtx) {
_defaultContext = connCtx;
// This instance of ExecutionContext will be used in the insert operation
_executionContext = new sqlj.runtime.ExecutionContext( );
}
/**
* Inserts a single row into the testtable.
* @param row TestTableRow object containing data to be inserted
* @throws SQLException when database error occurs
*/
public void insertRow(TestTableRow row) throws SQLException {
int id = row.id;
String name = row.name;
short age = row.age;
String address = row.address;
/*@lineinfo:generated-code*//*@lineinfo:40^4*/
// ************************************************************
// #sql [_defaultContext,_executionContext] { insert into testtable values (:id, :name,
:age , :address) };
// ************************************************************
{
sqlj.runtime.ConnectionContext __sJT_connCtx = _defaultContext;
if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
sqlj.runtime.ExecutionContext __sJT_execCtx = _executionContext;
if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
int __sJT_1 = id;
String __sJT_2 = name;
short __sJT_3 = age;
String __sJT_4 = address;
synchronized (__sJT_execCtx) {
sqlj.runtime.profile.RTStatement __sJT_stmt = __sJT_execCtx.registerStatement
(__sJT_connCtx, TestTableInserter_SJProfileKeys.getKey(0), 0);
try
{
__sJT_stmt.setInt(1, __sJT_1);
__sJT_stmt.setString(2, __sJT_2);
__sJT_stmt.setShort(3, __sJT_3);
__sJT_stmt.setString(4, __sJT_4);
__sJT_execCtx.executeUpdate();
}
finally
{
__sJT_execCtx.releaseStatement();
}
}
}
// ************************************************************
/*@lineinfo:user-code*//*@lineinfo:40^106*/
}
}/*@lineinfo:generated-code*/class TestTableInserter_SJProfileKeys
{
private static TestTableInserter_SJProfileKeys inst = null;
public static java.lang.Object getKey(int keyNum)
throws java.sql.SQLException
{
if (inst == null)
{
inst = new TestTableInserter_SJProfileKeys();
}
return inst.keys[keyNum];
}
private final sqlj.runtime.profile.Loader loader = sqlj.runtime.RuntimeContext.getRuntime()
.getLoaderForClass(getClass());
private java.lang.Object[] keys;
private TestTableInserter_SJProfileKeys()
throws java.sql.SQLException
{
keys = new java.lang.Object[1];
keys[0] = sqlj.runtime.ref.DefaultContext.getProfileKey(loader, "TestTableInserter_SJProfile0");
}
}
|
 |
Appendix B
The modified code, which was used in the performance studies, is listed below:
/*@lineinfo:filename=SQLJStmtCachedTestTableInserter*//*@lineinfo:user-code*//*@
lineinfo:1^1*/// TestTableInserter.sqlj
import java.sql.*;
/**
* Implements the method to insert a row into testtable
*/
public class SQLJStmtCachedTestTableInserter {
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ref.DefaultContext _defaultContext;
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ExecutionContext _executionContext ;
/**
* Stores the registered insert statement
*/
protected sqlj.runtime.profile.RTStatement _insertStatement ;
/**
* Releases the RTStatement object during garbage collection
* @throws SQLException if releaseStatement call fails
*/
protected void finalize() throws SQLException {
_executionContext.releaseStatement();
}
/**
* Sole Constructor. Sets the protected DefaultContext variable and initializes the
* ExecutionContext to be used for the insert statement.
* @param connCtx DefaultContext object used for SQLJ operations
* @throws SQLException if registerStatement call fails
*/
public SQLJStmtCachedTestTableInserter( sqlj.runtime.ref.DefaultContext connCtx)
throws SQLException {
_defaultContext = connCtx;
// This instance of ExecutionContext will be used in the insert operation
_executionContext = new sqlj.runtime.ExecutionContext( );
_insertStatement = _executionContext.registerStatement(_defaultContext,
TestTableInserter_SJProfileKeys.getKey(0), 0);
}
/**
* Inserts a single row into the testtable.
* @param row TestTableRow object containing data to be inserted
* @throws SQLException when database error occurs
*/
public void insertRow(TestTableRow row) throws SQLException {
int id = row.id;
String name = row.name;
short age = row.age;
String address = row.address;
/*@lineinfo:generated-code*//*@lineinfo:40^4*/
// ************************************************************
// #sql [_defaultContext,_executionContext] { insert into testtable values
(:id, :name, :age , :address) };
// ************************************************************
{
sqlj.runtime.ConnectionContext __sJT_connCtx = _defaultContext;
if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
sqlj.runtime.ExecutionContext __sJT_execCtx = _executionContext;
if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
int __sJT_1 = id;
String __sJT_2 = name;
short __sJT_3 = age;
String __sJT_4 = address;
synchronized (__sJT_execCtx) {
_insertStatement.setInt(1, __sJT_1);
_insertStatement.setString(2, __sJT_2);
_insertStatement.setShort(3, __sJT_3);
_insertStatement.setString(4, __sJT_4);
__sJT_execCtx.executeUpdate();
}
}
// ************************************************************
/*@lineinfo:user-code*//*@lineinfo:40^106*/
}
}/*@lineinfo:generated-code*/class TestTableInserter_SJProfileKeys
{
private static TestTableInserter_SJProfileKeys inst = null;
public static java.lang.Object getKey(int keyNum)
throws java.sql.SQLException
{
if (inst == null)
{
inst = new TestTableInserter_SJProfileKeys();
}
return inst.keys[keyNum];
}
private final sqlj.runtime.profile.Loader loader = sqlj.runtime.RuntimeContext.getRuntime().
getLoaderForClass(getClass());
private java.lang.Object[] keys;
private TestTableInserter_SJProfileKeys()
throws java.sql.SQLException
{
keys = new java.lang.Object[1];
keys[0] = sqlj.runtime.ref.DefaultContext.getProfileKey(loader,
"TestTableInserter_SJProfile0");
}
}
|
 |
Appendix C
import java.sql.*;
import java.util.*;
class JDBCTableInserterTester
{
static
{
try
{
Class.forName("COM.ibm.db2.jdbc.app.DB2Driver").
newInstance();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String args[]) throws Exception
{
String url = "jdbc:db2:testdb";
Properties prop = new Properties();
prop.put("user", "user");
prop.put("password", "pwd");
Connection con = null;
int commitFrequency ;
if (args.length ==0 )
commitFrequency = 10000;
else
commitFrequency = Integer.parseInt(args[0]);
try
{
// connect with user id/password
con = DriverManager.getConnection(url, prop);
con.setAutoCommit(false);
}
catch (SQLException e)
{
System.out.println("Error: could not open connection to database");
System.err.println(e) ;
System.exit(1);
}
System.out.println("Starting to insert : " + new java.util.Date() ) ;
PreparedStatement inserter = con.prepareStatement("insert into
testtable values(?,?,?,?)");;
String name = "myname";
short age = 27;
char[] arr = new char[74];
Arrays.fill(arr, 'a');
String address = new String(arr);
for ( int id = 0; id <10000; id++ ) {
inserter.setInt(1, id);
inserter.setString(2,name);
inserter.setShort(3,age);
inserter.setString(4, address);
inserter.executeUpdate();
if ((id + 1) % commitFrequency == 0) con.commit();
}
con.commit();
System.out.println("Finished inserting : " + new java.util.Date() ) ;
}
}
|
 |
Appendix D
import java.sql.*;
import java.util.*;
import sqlj.runtime.*;
import sqlj.runtime.ref.*;
class SQLJTableInserterTester
{
static
{
try
{
Class.forName("COM.ibm.db2.jdbc.app.DB2Driver").newInstance();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String args[]) throws Exception
{
String url = "jdbc:db2:testdb";
Properties prop = new Properties();
prop.put("user", "user");
prop.put("password", "pwd");
int commitFrequency ;
if (args.length ==0 )
commitFrequency = 10000;
else
commitFrequency = Integer.parseInt(args[0]);
Connection con = null;
DefaultContext ctx = null;
try
{
// connect with default id/password
con = DriverManager.getConnection(url, prop);
con.setAutoCommit(false);
ctx = new DefaultContext(con);
}
catch (SQLException e)
{
System.out.println("Error: could not get a default context");
System.err.println(e) ;
System.exit(1);
}
System.out.println("Starting to insert : " + new java.util.Date() ) ;
TestTableRow row = new TestTableRow();
TestTableInserter inserter = new TestTableInserter(ctx);
row.name = "myname";
row.age = 27;
char[] arr = new char[74];
Arrays.fill(arr, 'a');
row.address = new String(arr);
for ( int id = 0; id <10000; id++ ) {
row.id = id;
inserter.insertRow(row);
if ( (id + 1) % commitFrequency == 0 ) con.commit();
}
con.commit();
System.out.println("Finished inserting : " + new java.util.Date() ) ;
}
}
|
Resources
- Adamache, Blair. Clustering for Scalability published on the DB2 Developer Domain.
- DB2 UDB 7.2 Application Development Guide - Chapter 18: Programming Considerations in a Partitioned Environment available at http://www-4.ibm.com/cgi-bin/db2www/data/db2/udb/winos2unix/support/v7pubs.d2w/en_main.
- SQLJ Tutorial available at http://www.sqlj.org.
About the author  | 
|  |
Kishnakumar Pooloth is a Technical Specialist with Infosys Technologies Limited (http://www.infy.com). His areas of expertise include object design, component technology, database optimization and development of enterprise
class applications in J2EE. He is an IBM Certified Solutions Expert - DB2 UDB Application Development. He holds a Bachelor's Degree in Electronics and Communication from Calicut University, India. You can contact him at krishnakumarp@infy.com.
|
Rate this page
|  |