web-dev-qa-db-ja.com

なぜSpringのjdbcTemplate.batchUpdate()がそんなに遅いのですか?

バッチ挿入を行うより高速な方法を見つけようとしています。

jdbcTemplate.update(String sql)で複数のバッチを挿入しようとしました。SQLはStringBuilderによってビルドされ、次のようになります。

INSERT INTO TABLE(x, y, i) VALUES(1,2,3), (1,2,3), ... , (1,2,3)

バッチサイズは正確に1000でした。100個近くのバッチを挿入しました。 StopWatchを使用して時間をチェックし、挿入時間を見つけました。

min[38ms], avg[50ms], max[190ms] per batch

嬉しかったのですが、コードを改善したかったのです。

その後、次のようにjdbcTemplate.batchUpdateを使用しようとしました。

    jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
                       // ...
        }
        @Override
        public int getBatchSize() {
            return 1000;
        }
    });

sQLは次のように見えました

INSERT INTO TABLE(x, y, i) VALUES(1,2,3);

がっかりしました! jdbcTemplateは、1000行のバッチを挿入するたびに個別に実行しました。私はmysql_logを検索し、1000件の挿入を見つけました。 StopWatchを使用して時間をチェックし、挿入時間を見つけました。

バッチごとの最小[900ms]、平均[1100ms]、最大[2000ms]

だから、誰も私に説明できますか、なぜこのメソッドでjdbcTemplateが別々の挿入を行うのですか?メソッドの名前がbatchUpdateなのはなぜですか?または、この方法を間違った方法で使用している可能性がありますか?

18
user2602807

JDBC接続URLのこれらのパラメーターは、バッチステートメントの速度に大きな違いをもたらす可能性があります。

?useServerPrepStmts = false&rewriteBatchedStatements = true

参照: JDBCバッチ挿入パフォーマンス

12
teu

Spring JDBCテンプレートでも同じ問題に直面しました。おそらく、Spring Batchでは、すべての挿入またはチャンクでステートメントが実行およびコミットされたため、処理が遅くなりました。

JdbcTemplate.batchUpdate()コードを元のJDBCバッチ挿入コードに置き換えたところ、主要なパフォーマンスの改善が見つかりました。

DataSource ds = jdbcTemplate.getDataSource();
Connection connection = ds.getConnection();
connection.setAutoCommit(false);
String sql = "insert into employee (name, city, phone) values (?, ?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
final int batchSize = 1000;
int count = 0;

for (Employee employee: employees) {

    ps.setString(1, employee.getName());
    ps.setString(2, employee.getCity());
    ps.setString(3, employee.getPhone());
    ps.addBatch();

    ++count;

    if(count % batchSize == 0 || count == employees.size()) {
        ps.executeBatch();
        ps.clearBatch(); 
    }
}

connection.commit();
ps.close();

このリンクも確認してください JDBCバッチ挿入パフォーマンス

10
Rakesh Soni

単にトランザクションを使用します。メソッドに@Transactionalを追加します。

複数のデータソース@Transactional( "dsTxManager")を使用する場合は、正しいTXマネージャーを必ず宣言してください。 60000レコードを挿入する場合があります。約15秒かかります。他の微調整なし:

@Transactional("myDataSourceTxManager")
public void save(...) {
...
    jdbcTemplate.batchUpdate(query, new BatchPreparedStatementSetter() {

            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ...

            }

            @Override
            public int getBatchSize() {
                if(data == null){
                    return 0;
                }
                return data.size();
            }
        });
    }
7
Mike

SQL挿入をINSERT INTO TABLE(x, y, i) VALUES(1,2,3)に変更します。フレームワークはループを作成します。例えば:

_public void insertBatch(final List<Customer> customers){

  String sql = "INSERT INTO CUSTOMER " +
    "(CUST_ID, NAME, AGE) VALUES (?, ?, ?)";

  getJdbcTemplate().batchUpdate(sql, new BatchPreparedStatementSetter() {

    @Override
    public void setValues(PreparedStatement ps, int i) throws SQLException {
        Customer customer = customers.get(i);
        ps.setLong(1, customer.getCustId());
        ps.setString(2, customer.getName());
        ps.setInt(3, customer.getAge() );
    }

    @Override
    public int getBatchSize() {
        return customers.size();
    }
  });
}
_

このようなものがある場合。 Springは次のようなことをします。

_for(int i = 0; i < getBatchSize(); i++){
   execute the prepared statement with the parameters for the current iteration
}
_

フレームワークは最初にクエリ(sql変数)からPreparedStatementを作成し、次にsetValuesメソッドが呼び出され、ステートメントが実行されます。これは、getBatchSize()メソッドで指定した回数だけ繰り返されます。したがって、挿入ステートメントを記述する正しい方法は、値句を1つだけ使用することです。 http://docs.spring.io/spring/docs/3.0.x/reference/jdbc.html をご覧ください

6
Evgeni Dimitrov

主要な改善呼び出しでargTypes配列を設定していることがわかりました。

私の場合、Spring 4.1.4とOracle 12cで、35フィールドの5000行を挿入するには:

jdbcTemplate.batchUpdate(insert, parameters); // Take 7 seconds

jdbcTemplate.batchUpdate(insert, parameters, argTypes); // Take 0.08 seconds!!!

ArgTypes paramは、次の方法で各フィールドを設定するint配列です。

int[] argTypes = new int[35];
argTypes[0] = Types.VARCHAR;
argTypes[1] = Types.VARCHAR;
argTypes[2] = Types.VARCHAR;
argTypes[3] = Types.DECIMAL;
argTypes[4] = Types.TIMESTAMP;
.....

Org\springframework\jdbc\core\JdbcTemplate.Javaをデバッグしたところ、ほとんどの時間は各フィールドの性質を知るために費やされており、これは各レコードに対して行われました。

お役に立てれば !

5
Carlos Cuesta

これがあなたのために働くかどうかはわかりませんが、ここで私が使用することになったSpringなしの方法があります。これは、私が試したさまざまなSpringメソッドよりも大幅に高速でした。他の回答で説明されているJDBCテンプレートバッチ更新メソッドを使用しようとしましたが、それでも思ったよりも遅くなりました。私はこの取引が何であるかわからず、インターネットにも多くの答えがありませんでした。コミットの処理方法に関係していると思われます。

このアプローチは、Java.sqlパッケージとPreparedStatementのバッチインターフェイスを使用した単純なJDBCです。これは、MySQL DBに24Mレコードを取得できる最速の方法でした。

多かれ少なかれ「レコード」オブジェクトのコレクションを作成し、すべてのレコードをバッチ挿入するメソッドで以下のコードを呼び出しました。コレクションを構築したループは、バッチサイズの管理を担当しました。

MySQL DBに24Mレコードを挿入しようとしていて、Springバッチを使用して1秒あたり最大200レコードを記録していました。この方法に切り替えると、1秒あたり最大2500レコードになりました。そのため、私の24Mレコードの負荷は、理論的な1.5日から約2.5時間になりました。

最初に接続を作成...

Connection conn = null;
try{
    Class.forName("com.mysql.jdbc.Driver");
    conn = DriverManager.getConnection(connectionUrl, username, password);
}catch(SQLException e){}catch(ClassNotFoundException e){}

次に、準備されたステートメントを作成し、挿入用の値のバッチでロードしてから、単一のバッチ挿入として実行します...

PreparedStatement ps = null;
try{
    conn.setAutoCommit(false);
    ps = conn.prepareStatement(sql); // INSERT INTO TABLE(x, y, i) VALUES(1,2,3)
    for(MyRecord record : records){
        try{
            ps.setString(1, record.getX());
            ps.setString(2, record.getY());
            ps.setString(3, record.getI());

            ps.addBatch();
        } catch (Exception e){
            ps.clearParameters();
            logger.warn("Skipping record...", e);
        }
    }

    ps.executeBatch();
    conn.commit();
} catch (SQLException e){
} finally {
    if(null != ps){
        try {ps.close();} catch (SQLException e){}
    }
}

明らかに、エラー処理を削除し、クエリとRecordオブジェクトは概念的なものであり、その他のものではありません。

Edit:元の質問は、foobar値への挿入の比較(?、?、?)、(?、?、?)...( ?、?、?)Springバッチへのメソッド、これに対するより直接的な応答を次に示します。

元のメソッドは、「LOAD DATA INFILE」アプローチのようなものを使用せずに、MySQLへのバルクデータロードを行う最も速い方法であるようです。 MysQLドキュメントからの引用( http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html ):

同じクライアントから多数の行を同時に挿入する場合は、複数のVALUESリストを含むINSERTステートメントを使用して、一度に複数の行を挿入します。これは、個別の単一行INSERTステートメントを使用するよりもかなり高速です(場合によっては何倍も高速です)。

Spring JDBC TemplateのbatchUpdateメソッドを変更して、 'setValues'呼び出しごとに複数のVALUESを指定して挿入を行うこともできますが、挿入されるもののセットを反復処理するときにインデックス値を手動で追跡する必要があります。そして、挿入されるものの総数が、準備されたステートメントにあるVALUESリストの数の倍数でない場合、最後に厄介なEdgeのケースに遭遇します。

私が概説したアプローチを使用すると、同じことを行うことができます(複数のVALUESリストを持つ準備されたステートメントを使用します)正確な数のVALUESリストを持つ最後のステートメント。それは少しハックですが、ほとんどの最適化されたものです。

4
reblace

@Rakeshによって提供されたソリューションが私にとってはうまくいきました。パフォーマンスの大幅な改善。以前の時間は8分でしたが、このソリューションの所要時間は2分未満でした。

DataSource ds = jdbcTemplate.getDataSource();
Connection connection = ds.getConnection();
connection.setAutoCommit(false);
String sql = "insert into employee (name, city, phone) values (?, ?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
final int batchSize = 1000;
int count = 0;

for (Employee employee: employees) {

    ps.setString(1, employee.getName());
    ps.setString(2, employee.getCity());
    ps.setString(3, employee.getPhone());
    ps.addBatch();

    ++count;

    if(count % batchSize == 0 || count == employees.size()) {
        ps.executeBatch();
        ps.clearBatch(); 
    }
}

connection.commit();
ps.close();
0
Pratidnya