使用 JDBC 进行批量插入的有效方法
Posted
技术标签:
【中文标题】使用 JDBC 进行批量插入的有效方法【英文标题】:Efficient way to do batch INSERTS with JDBC 【发布时间】:2011-04-16 14:23:33 【问题描述】:在我的应用中,我需要做很多 INSERTS。它是一个 Java 应用程序,我使用普通的 JDBC 来执行查询。数据库是 Oracle。我已经启用了批处理,所以它可以节省我执行查询的网络延迟。但是查询作为单独的 INSERT 串行执行:
insert into some_table (col1, col2) values (val1, val2)
insert into some_table (col1, col2) values (val3, val4)
insert into some_table (col1, col2) values (val5, val6)
我想知道以下形式的 INSERT 是否更有效:
insert into some_table (col1, col2) values (val1, val2), (val3, val4), (val5, val6)
即将多个 INSERT 合并为一个。
还有其他加快批量 INSERT 的技巧吗?
【问题讨论】:
哇!我在插入 SQL Server 时测试了您的“将多个插入合并为一个”,我从每秒 107 行变为每秒 3333 行! 增长了惊人的 31 倍。 【参考方案1】:这是前面两个答案的混合:
PreparedStatement ps = c.prepareStatement("INSERT INTO employees VALUES (?, ?)");
ps.setString(1, "John");
ps.setString(2,"Doe");
ps.addBatch();
ps.clearParameters();
ps.setString(1, "Dave");
ps.setString(2,"Smith");
ps.addBatch();
ps.clearParameters();
int[] results = ps.executeBatch();
【讨论】:
这是完美的解决方案,因为语句只准备(解析)一次。ps.clearParameters();
在这种特殊情况下是不必要的。
一定要量一下。根据 JDBC 驱动程序的实现,这可能是预期的每批一次往返,但也可能最终成为每条语句一次往返。
prepareStatement/setXXX - 应该是这样的!
对于 mysql 还要在 url 中添加以下内容:"&useServerPrepStmts=false&rewriteBatchedStatements=true"【参考方案2】:
虽然问题询问使用 JDBC 有效地插入 Oracle,但我目前正在使用 DB2(在 IBM 大型机上),从概念上讲,插入将是相似的,因此认为查看我的指标可能会有所帮助
一次插入一条记录
插入一批记录(效率很高)
这里是指标
1) 一次插入一条记录
public void writeWithCompileQuery(int records)
PreparedStatement statement;
try
Connection connection = getDatabaseConnection();
connection.setAutoCommit(true);
String compiledQuery = "INSERT INTO TESTDB.EMPLOYEE(EMPNO, EMPNM, DEPT, RANK, USERNAME)" +
" VALUES" + "(?, ?, ?, ?, ?)";
statement = connection.prepareStatement(compiledQuery);
long start = System.currentTimeMillis();
for(int index = 1; index < records; index++)
statement.setInt(1, index);
statement.setString(2, "emp number-"+index);
statement.setInt(3, index);
statement.setInt(4, index);
statement.setString(5, "username");
long startInternal = System.currentTimeMillis();
statement.executeUpdate();
System.out.println("each transaction time taken = " + (System.currentTimeMillis() - startInternal) + " ms");
long end = System.currentTimeMillis();
System.out.println("total time taken = " + (end - start) + " ms");
System.out.println("avg total time taken = " + (end - start)/ records + " ms");
statement.close();
connection.close();
catch (SQLException ex)
System.err.println("SQLException information");
while (ex != null)
System.err.println("Error msg: " + ex.getMessage());
ex = ex.getNextException();
100 笔交易的指标:
each transaction time taken = 123 ms
each transaction time taken = 53 ms
each transaction time taken = 48 ms
each transaction time taken = 48 ms
each transaction time taken = 49 ms
each transaction time taken = 49 ms
...
..
.
each transaction time taken = 49 ms
each transaction time taken = 49 ms
total time taken = 4935 ms
avg total time taken = 49 ms
第一个事务在120-150ms
周围进行,这是the query parse 然后执行,后续事务仅在50ms
周围进行。 (仍然很高,但我的数据库在不同的服务器上(我需要对网络进行故障排除))
2) 批量插入(高效) - 由preparedStatement.executeBatch()
实现
public int[] writeInABatchWithCompiledQuery(int records)
PreparedStatement preparedStatement;
try
Connection connection = getDatabaseConnection();
connection.setAutoCommit(true);
String compiledQuery = "INSERT INTO TESTDB.EMPLOYEE(EMPNO, EMPNM, DEPT, RANK, USERNAME)" +
" VALUES" + "(?, ?, ?, ?, ?)";
preparedStatement = connection.prepareStatement(compiledQuery);
for(int index = 1; index <= records; index++)
preparedStatement.setInt(1, index);
preparedStatement.setString(2, "empo number-"+index);
preparedStatement.setInt(3, index+100);
preparedStatement.setInt(4, index+200);
preparedStatement.setString(5, "usernames");
preparedStatement.addBatch();
long start = System.currentTimeMillis();
int[] inserted = preparedStatement.executeBatch();
long end = System.currentTimeMillis();
System.out.println("total time taken to insert the batch = " + (end - start) + " ms");
System.out.println("total time taken = " + (end - start)/records + " s");
preparedStatement.close();
connection.close();
return inserted;
catch (SQLException ex)
System.err.println("SQLException information");
while (ex != null)
System.err.println("Error msg: " + ex.getMessage());
ex = ex.getNextException();
throw new RuntimeException("Error");
一批 100 笔交易的指标是
total time taken to insert the batch = 127 ms
1000 笔交易
total time taken to insert the batch = 341 ms
因此,在 ~5000ms
中进行 100 次交易(一次只有一个 trxn)减少到 ~150ms
(一批 100 条记录)。
注意 - 忽略我的超级慢的网络,但指标值是相对的。
【讨论】:
嗨。记录的长度是否在插入时间中起作用?我有 3 个 Varchar 列,它们的值是 URI,并且将 8555 作为批处理插入仍然需要 3.5 分钟才能插入!! 据我了解,在将数据从应用程序服务器传输到数据库服务器期间,记录大小可能很重要,但插入时间影响不大。我尝试在本地 oracle 数据库中使用 3 列大小为 125 字节的列,批量处理 10,000 条记录大约需要(145 到 300)毫秒。代码here。而multiple transactions for 10,000 records takes 20seconds. 批处理可以处理的插入数量是否有限制?我有一些分析跟踪代码可以跟踪内存中的分析条目,然后单个线程在常规内部运行并插入记录。 60 秒内可能有数千条记录。 对于任何好奇的人,我在一个具有 1,000、10,000、100,000 和 1,000,000 条记录的 Oracle DB 上测试了这种批处理方法,而且时间非常短。无论批次中的插入总数如何,每个插入的平均插入时间约为 0.2 毫秒。我使用 System.nanoTime() 来获得更准确的时间。【参考方案3】:Statement
为您提供以下选项:
Statement stmt = con.createStatement();
stmt.addBatch("INSERT INTO employees VALUES (1000, 'Joe Jones')");
stmt.addBatch("INSERT INTO departments VALUES (260, 'Shoe')");
stmt.addBatch("INSERT INTO emp_dept VALUES (1000, 260)");
// submit a batch of update commands for execution
int[] updateCounts = stmt.executeBatch();
【讨论】:
虽然最终结果是一样的,但是在这个方法中,会解析多条语句,这对于批量来说速度要慢得多,实际上并没有单独执行每个语句效率高。此外,请尽可能使用 PreparedStatement 进行重复查询,因为它们的性能要好得多.. @AshishPatil:有没有使用 PreparedStatement 和没有 PreparedStatement 的测试基准? 哇! 8年后。尽管如此,@prayagupd 在他最近的回答中给出了详细的统计数据。 ***.com/a/42756134/372055 非常感谢您。这在动态插入数据并且您没有时间检查参数的数据类型时非常有用。 "另外,请尽可能使用 PreparedStatement 进行重复查询,因为它们的性能要好得多。"如果您不想先在循环中解析每个该死的列名和值怎么办?这似乎非常重 Java。如果我将此代码放入单个 DB 类中,则需要 DB 代码了解有关数据的一些信息。非常糟糕的耦合。我宁愿发送一个已经格式化的插入字符串列表以及它们在 HashMap(或其他东西)中的值字符串,然后让 DB 代码插入所有内容。【参考方案4】:显然,您必须进行基准测试,但是如果您使用 PreparedStatement 而不是 Statement,则通过 JDBC 发出多个插入会快得多。
【讨论】:
【参考方案5】:您可以使用此rewriteBatchedStatements
参数使批量插入更快。
您可以在此处阅读有关参数的信息:MySQL and JDBC with rewriteBatchedStatements=true
【讨论】:
【参考方案6】:SQLite:以上答案都是正确的。对于 SQLite,它有点不同。没有什么真正有帮助的,即使将它放在一个批次中(有时)也不会提高性能。在这种情况下,请尝试禁用自动提交并在完成后手动提交(警告!当多个连接同时写入时,您可能会与这些操作发生冲突)
// connect(), yourList and compiledQuery you have to implement/define beforehand
try (Connection conn = connect())
conn.setAutoCommit(false);
preparedStatement pstmt = conn.prepareStatement(compiledQuery);
for(Object o : yourList)
pstmt.setString(o.toString());
pstmt.executeUpdate();
pstmt.getGeneratedKeys(); //if you need the generated keys
pstmt.close();
conn.commit();
【讨论】:
当您使用 try-with-resources 编写代码时,您还应该尝试使用pstmt
,这样您就不会忘记关闭 pstmt
(比如出现异常时)被抛出,例如并发修改)。我也喜欢环绕连接自动提交/提交,但这是因为我有点偏执。 SQLite 有一个很长的记录(自古以来),除非您为它提供事务,否则数据库中的每个更新都会创建自己的事务,但总是一个很好的剩余【参考方案7】:
如何使用 INSERT ALL 语句?
INSERT ALL
INTO table_name VALUES ()
INTO table_name VALUES ()
...
SELECT Statement;
我记得最后一个 select 语句是强制性的,才能使这个请求成功。不过不记得为什么了。 您也可以考虑使用 PreparedStatement。很多优点!
法里德
【讨论】:
【参考方案8】:您可以在 java 中使用 addBatch 和 executeBatch 进行批量插入参见示例:Batch Insert In Java
【讨论】:
【参考方案9】:在我的代码中,我无法直接访问“preparedStatement”,因此无法使用批处理,我只是将查询和参数列表传递给它。然而,诀窍是创建一个可变长度的插入语句和一个 LinkedList 参数。效果和上面的例子一样,可变参数输入长度。见下文(省略错误检查)。 假设“myTable”有 3 个可更新字段:f1、f2 和 f3
String []args="A","B","C", "X","Y","Z" ; // etc, input list of triplets
final String QUERY="INSERT INTO [myTable] (f1,f2,f3) values ";
LinkedList params=new LinkedList();
String comma="";
StringBuilder q=QUERY;
for(int nl=0; nl< args.length; nl+=3 ) // args is a list of triplets values
params.add(args[nl]);
params.add(args[nl+1]);
params.add(args[nl+2]);
q.append(comma+"(?,?,?)");
comma=",";
int nr=insertIntoDB(q, params);
在我的 DBInterface 类中,我有:
int insertIntoDB(String query, LinkedList <String>params)
preparedUPDStmt = connectionSQL.prepareStatement(query);
int n=1;
for(String x:params)
preparedUPDStmt.setString(n++, x);
int updates=preparedUPDStmt.executeUpdate();
return updates;
【讨论】:
【参考方案10】:如果你使用 jdbcTemplate 那么:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
public int[] batchInsert(List<Book> books)
return this.jdbcTemplate.batchUpdate(
"insert into books (name, price) values(?,?)",
new BatchPreparedStatementSetter()
public void setValues(PreparedStatement ps, int i) throws SQLException
ps.setString(1, books.get(i).getName());
ps.setBigDecimal(2, books.get(i).getPrice());
public int getBatchSize()
return books.size();
);
或更高级的配置
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
public int[][] batchInsert(List<Book> books, int batchSize)
int[][] updateCounts = jdbcTemplate.batchUpdate(
"insert into books (name, price) values(?,?)",
books,
batchSize,
new ParameterizedPreparedStatementSetter<Book>()
public void setValues(PreparedStatement ps, Book argument)
throws SQLException
ps.setString(1, argument.getName());
ps.setBigDecimal(2, argument.getPrice());
);
return updateCounts;
链接到source
【讨论】:
【参考方案11】:如果迭代次数少,使用 PreparedStatements 将比 Statements 慢得多。要通过在语句上使用 PrepareStatement 来获得性能优势,您需要在迭代次数至少为 50 次或更高的循环中使用它。
【讨论】:
不,永远不会。一个普通的 Statement(不是 PrepareStatement)对象必须做所有与 PreparedStatement 相同的事情,实际上它是 PreparedStatement 的一个包装器,它实际上也做了准备好的部分。两者的区别在于,Statement 对象静默地准备语句并在每次执行时对其进行验证,而准备好的语句只执行一次,然后可以多次执行以处理批处理中的每个项目。 这个答案是否有效??以上是关于使用 JDBC 进行批量插入的有效方法的主要内容,如果未能解决你的问题,请参考以下文章
JDBC批量插入数据优化,使用addBatch和executeBatch