Effiziente Möglichkeit, Batch-INSERTS mit JDBC durchzuführen

71

In meiner App muss ich viele INSERTS machen. Es ist eine Java-App und ich verwende einfaches JDBC, um die Abfragen auszuführen. Die Datenbank ist Oracle. Ich habe jedoch das Batching aktiviert, sodass mir Netzwerklatenzen beim Ausführen von Abfragen erspart werden. Die Abfragen werden jedoch seriell als separate INSERTs ausgeführt:

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)

Ich habe mich gefragt, ob die folgende Form von INSERT effizienter sein könnte:

insert into some_table (col1, col2) values (val1, val2), (val3, val4), (val5, val6)

dh mehrere INSERTs zu einem zusammenfassen.

Gibt es noch andere Tipps, um Batch-INSERTs schneller zu machen?

Aayush Puri
quelle
2
BEEINDRUCKEND! Ich habe beim Einfügen in SQL Server Ihre "Mehrere Einfügungen zu einer zusammenfassen" getestet und bin von 107 Zeilen / Sekunde auf 3333 Zeilen pro Sekunde gestiegen!
Wouter
1
Das ist eine erstaunliche 31-fache Steigerung.
Gaurav

Antworten:

148

Dies ist eine Mischung aus den beiden vorherigen Antworten:

  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();
Tusc
quelle
3
Dies ist eine perfekte Lösung, da die Anweisung nur einmal erstellt (analysiert) wird.
Ashish Patil
42
Das ps.clearParameters();ist in diesem speziellen Fall nicht notwendig.
BalusC
1
Achten Sie darauf, es zu messen. Abhängig von der Implementierung des JDBC-Treibers ist dies möglicherweise die erwartete Hin- und Rückfahrt pro Stapel, kann aber auch eine Hin- und Rückfahrt pro Anweisung sein.
Stracktracer
prepareStatement / setXXX - so sollte es sein!
msciwoj
3
Für MySQL fügen Sie der URL auch Folgendes hinzu: "& useServerPrepStmts = false & rewriteBatchedStatements = true"
Ant Kutschera
38

Obwohl die Frage lautet, wie man mithilfe von JDBC effizient in Oracle einfügt , spiele ich derzeit mit DB2 (auf IBM Mainframe). Das konzeptionelle Einfügen wäre ähnlich, sodass es hilfreich sein könnte, meine Metriken dazwischen zu sehen

  • Einfügen von jeweils einem Datensatz

  • Einfügen eines Datensatzes (sehr effizient)

Hier gehen die Metriken

1) Jeweils einen Datensatz einfügen

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();
        }
    }
}

Die Metriken für 100 Transaktionen:

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

Die erste Transaktion wird ausgeführt, 120-150msdie für die Analyse der Abfrage und die anschließende Ausführung vorgesehen ist. Die nachfolgenden Transaktionen werden nur ausgeführt 50ms. (Was immer noch hoch ist, aber meine Datenbank befindet sich auf einem anderen Server (ich muss das Netzwerk beheben))

2) Mit Einfügen in eine Charge (effiziente) - erreicht durchpreparedStatement.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");
    }
}

Die Metriken für einen Stapel von 100 Transaktionen sind

total time taken to insert the batch = 127 ms

und für 1000 Transaktionen

total time taken to insert the batch = 341 ms

Daher werden 100 Transaktionen ~5000ms(mit jeweils einem Trxn) auf ~150ms(mit einem Stapel von 100 Datensätzen) reduziert .

HINWEIS - Ignoriere mein Netzwerk, das sehr langsam ist, aber die Metrikwerte wären relativ.

betenagupd
quelle
1
Hallo. Spielt die Länge des Datensatzes eine Rolle in der Zeit zum Einfügen? Ich habe 3 Varchar-Spalten mit URIs als Werten und das Einfügen von 8555 als Stapel dauert noch ca. 3,5 Minuten !!
Prathamesh Dhanawade
Nach meinem Verständnis kann die Datensatzgröße während der Datenübertragung von Ihrem Anwendungsserver zum Datenbankserver von Bedeutung sein, aber die Einfügezeit hat keinen großen Einfluss. Ich habe es in einer lokalen Oracle-Datenbank mit 3 Spalten mit einer Größe von 125 Bytes versucht und benötige ungefähr (145 bis 300) ms für einen Stapel von 10.000 Datensätzen. Code hier . Während mehrere Transaktionen für 10.000 Datensätze 20 Sekunden dauern .
Prayagupd
9

Das Statementgibt Ihnen die folgende Option:

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();
Bozho
quelle
7
Während das Endergebnis dasselbe ist, werden bei dieser Methode mehrere Anweisungen analysiert, was für die Masse viel langsamer ist und in der Tat nicht viel effizienter ist, als jede Anweisung einzeln auszuführen. Bitte verwenden Sie auch PreparedStatement, wann immer dies möglich ist, für wiederholte Abfragen, da diese viel besser
Ashish Patil
@AshishPatil: Haben Sie Benchmarks zum Testen mit und ohne PreparedStatement?
Gaurav
Whoa! Nach 8 Jahren. Trotzdem hat @prayagupd in seiner Antwort, die viel jünger ist, detaillierte Statistiken angegeben. stackoverflow.com/a/42756134/372055
Ashish Patil
Vielen Dank dafür. Dies ist sehr hilfreich, wenn Sie Daten dynamisch einfügen und Sie nicht die Zeit haben, den Datentyp eines Parameters zu überprüfen.
Morfinismo
5

Sie müssen natürlich ein Benchmarking durchführen, aber über JDBC hinweg ist das Ausgeben mehrerer Einfügungen viel schneller, wenn Sie ein PreparedStatement anstelle einer Anweisung verwenden.

Burleigh Bär
quelle
0

Wie wäre es mit der INSERT ALL-Anweisung?

INSERT ALL

INTO table_name VALUES ()

INTO table_name VALUES ()

...

SELECT Statement;

Ich erinnere mich, dass die letzte select-Anweisung obligatorisch ist, damit diese Anforderung erfolgreich ist. Ich erinnere mich nicht warum. Sie können stattdessen auch PreparedStatement verwenden. viele Vorteile!

Farid

Farid
quelle
0

In meinem Code habe ich keinen direkten Zugriff auf das 'prepareStatement', daher kann ich keinen Stapel verwenden. Ich übergebe ihm nur die Abfrage und eine Liste von Parametern. Der Trick besteht jedoch darin, eine Einfügeanweisung mit variabler Länge und eine LinkedList von Parametern zu erstellen. Der Effekt ist der gleiche wie im oberen Beispiel mit variabler Parametereingabelänge. Siehe unten (Fehlerprüfung weggelassen). Angenommen, 'myTable' hat 3 aktualisierbare Felder: f1, f2 und 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);

In meiner DBInterface-Klasse habe ich:

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;
}
user3211098
quelle
-5

Die Verwendung von PreparedStatements ist VIEL langsamer als Anweisungen, wenn Sie nur geringe Iterationen haben. Um einen Leistungsvorteil durch die Verwendung eines PrepareStatements gegenüber einer Anweisung zu erzielen, müssen Sie es in einer Schleife verwenden, in der die Iterationen mindestens 50 oder höher sind.

Mickey
quelle
6
Nein, das wird es niemals. Ein normales Statement-Objekt (nicht PrepareStatement) muss ALLE die gleichen Aufgaben ausführen wie ein PreparedStatement und ist in der Tat ein Wrapper um PreparedStatement, der auch den vorbereiteten Teil ausführt. Der Unterschied zwischen beiden besteht darin, dass ein Anweisungsobjekt die Anweisung stillschweigend vorbereitet und jedes Mal validiert, wenn Sie sie ausführen. Als vorbereitete Anweisung wird dies nur einmal ausgeführt und kann dann mehrmals ausgeführt werden, um jedes Element im Stapel zu verarbeiten.
David
Ist diese Antwort überhaupt gültig?
Prayagupd
-13

Stapeleinfügung mit Anweisung

int a= 100;
            try {
                        for (int i = 0; i < 10; i++) {
                            String insert = "insert into usermaster"
                                    + "("
                                    + "userid"
                                    + ")"
                                    + "values("
                                    + "'" + a + "'"
                                    + ");";
                            statement.addBatch(insert);
                            System.out.println(insert);
                            a++;
                        }
                      dbConnection.commit();
                    } catch (SQLException e) {
                        System.out.println(" Insert Failed");
                        System.out.println(e.getMessage());
                    } finally {
            
                        if (statement != null) {
                            statement.close();
                        }
                        if (dbConnection != null) {
                            dbConnection.close();
                        }
                    }
        
PD Shah 5382
quelle
Dynamische Aussagen sind fast immer eine schlechte Idee. Sowohl aus Sicherheitsgründen (obwohl dies in diesem sehr einfachen Beispiel nicht der Fall ist) als auch aus Gründen der Leistung.
frroland