使用 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 批量插入和查询与使用生成键的单次插入

jdbc-批量插入批量删除批量更新

使用JDBC在MySQL数据库中快速批量插入数据

JDBC批量插入数据优化,使用addBatch和executeBatch

JDBC批量插入数据优化,使用addBatch和executeBatch

JDBC批量插入数据优化,使用addBatch和executeBatch