3

A read a lot of stuff, like here: SQLite queries much slower using JDBC than in Firefox SqliteManager plugin

But I have no clue what's the problem. The case is, I have a SQLite database (from an Android tablet) and a not too large table (~50.000 rows in it) If I run a "select * from table" for example in Sqlite Manager it takes 0.11 sec, correct.

But... if I do it in a Java program (with SQLite JDBC) it takes 20 minutes!!! Not kidding.

Somebody (somewhere) said it depends on the versions. But my question is how?

Because this command: "SELECT sqlite_version()" gives different results on the same .db file in every case:

  • in a very old sqlite manager it gives 3.6.19
  • in Sqlite Studio 3.15
  • and in with the newest .exe from sqlite.org it gives 3.23.1 So it's not a database related thing, I think it's the version of the sqlite3.exe used.

I can change the JDBC driver all day long (I did it a few times), but how would I know which I needed?

Anybody with any thought? I'm totally stucked with it.

EDIT: Okay, so JDBC jars are from here: https://bitbucket.org/xerial/sqlite-jdbc/downloads/

And my code is really basic, at first I just wanted to measure the speed.

        Class.forName("org.sqlite.JDBC");
        Connection c1 = DriverManager.getConnection("jdbc:sqlite:" + "c:\\database.db");

        PreparedStatement stmt1 = c1.prepareStatement("select * from table1;");
        ResultSet rs = stmt1.executeQuery();
        String script = "insert into table1 values ";
        while (rs.next()) {
            script += "(";
            script += rs.getInt(1) + ", '" + rs.getString(2) + "', '" + rs.getString(3) + "'";
            script += "),";
        }
        stmt1.close();
        c1.close();

And the executeQuery() row takes 20 minutes.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Rezmalac
  • 53
  • 1
  • 6
  • 1
    Please add the relevant java code, that executes the query and takes 20 minutes. – Arnaud May 11 '18 at 12:31
  • That kind of difference in performance is definitely not (only) JDBC API related. Could be a specific driver bug, but you haven't mentioned exactly what driver you've been using, nor the execution environment on which you ran the JDBC logic, nor shown any code, which would be needed to reproduce this – Lukas Eder May 11 '18 at 12:35
  • We do not have glass balls... we have no way to know whether the problem is in your code or in the driver/library/sqlite version. – Giacomo Alzetta May 11 '18 at 12:40
  • Well, you are comparing a `Select` done in SQLite manage with your code that insert into a table (with a `String` concatenation). First, use `PreparedStatement.addBatch()` to insert each row (to check if batch is manage in SQLITE). Then, don't concatenate yoru `String`. – AxelH May 11 '18 at 13:27
  • Axel, there is no actual inserts, I just building a String which is not executed. (actually I did it, and it finished in seconds, but the problem is with that part of the code above) – Rezmalac May 11 '18 at 13:31
  • 1
    But as explained, you don't just do a `select`, you build a **** ton of `String` instances. This takes times, especially on an `Android` device probably. – AxelH May 11 '18 at 13:55

2 Answers2

4

You are create a String with 50k rows, this means you are creating 50k * 5 String (each concatenation create a new String instance. This is what kills your performance.

while (rs.next()) {
    script += "(";
    script += rs.getInt(1) + ", '" + rs.getString(2) + "', '" + rs.getString(3) + "'";
    script += "),";
}

I noticed that you don't excute the String script, so if you just want to create a String, use a StringBuilder

StringBuilder script = new StringBuilder("insert into table1 values ");
    while (rs.next()) {
        script.append("(")
              .append(rs.getInt(1)).append(", '")
              .append(rs.getString(2)).append("', '")
              .append(rs.getString(3)).append("'")
          .append("),");
    }

script.setLength(script.length() - 1); //to remove the last comma.

String query = script.toString();

The StringBuilder prevent the huge number of String instance created for nothing.

If you want to insert those values after that, use a PreparedStatement directly instead of building a query :

PreparedStatement psInsert = c1.prepareStatement("insert into table1 values (?,?,?)");
while (rs.next()) {
    psInsert.setInt(1, rs.getInt(1));
    psInsert.setString(2, rs.getString(2));
    psInsert.setString(2,rs.getString(3));

    psInsert.execute();
}

Then if you want to improve this, use the batch system to send small block of insert. Using Statement.addBatch() and Statement.executeBatch()

 while (rs.next()) {
    psInsert.setInt(1, rs.getInt(1));
    psInsert.setString(2, rs.getString(2));
    psInsert.setString(2,rs.getString(3));

    psInsert.addBatch();
    if(batchSize++ > 100){ //Execute every 100 rows
        psInsert.executeBatch();
        batchSize = 0;
    }
}

if(batchSize > 0){ //execute the remainings data
      psInsert.executeBatch();
}

StringBuilder Benchmark

Not an official one, just a Duration of a simple execution

LocalTime start = LocalTime.now();
StringBuilder sb = new StringBuilder("Foo;");
for(int i = 0; i < 50_000; i++){
    sb.append("Row").append(i).append(";\n");
}
System.out.println(Duration.between(start, LocalTime.now()).toNanos());
String s = sb.toString();
System.out.println(s.substring(0, 50));

This takes 15 nanoseconds

LocalTime start = LocalTime.now();
String s = "Foo;";
for(int i = 0; i < 50_000; i++){
    s += "Row" + i + ";\n";
}
System.out.println(Duration.between(start, LocalTime.now()).toMillis());
System.out.println(s.substring(0, 50));

This takes >6 seconds

AxelH
  • 14,325
  • 2
  • 25
  • 55
  • I see your point, but it's not the case. My plan is transfer this database into MySQL. And if I generate 50k insert (even in a transaction) will be very slow. As I experienced earlier, the best way to insert into MySQL is one insert command, and than all the data like (1, 1, "abc"), (2, 3, "abc")... etc. So I need that String in that form. – Rezmalac May 11 '18 at 13:39
  • @Rezmalac, I have just edited (in the beginning) to show you how to build the `String` query using less `String`s with a `StringBuilder`. But this will not change the time to insert those values. If you notice some problem with huge insertion, you might check the configuration and the code used because `PreparedStatement` is better for that (and safer!) – AxelH May 11 '18 at 13:40
  • @Rezmalac To give you an idea, it took 7seconds to concatenate 100k `String` were using a `StringBuilder` it isn't even mesurable in `ms`. – AxelH May 11 '18 at 13:48
  • F@ck me, it seems to be that stupid java string concatenation really. You got my vote. Should i change the title of the question? Because its not relevant anymore... – Rezmalac May 11 '18 at 13:57
  • @Rezmalac Is it really the fault of the "stupid java concatenation" ? ;-) Never concatenate `String` in a loop, that's a good rule to follow. instanciation take time in every language. "changing" an immutable object means creating a new instance so this isn't really the fault of `String` java ;) Well, this was the execution in the SQLite query, "Build query in SQLte ResultSet" or something like this could be more correct but that up to you. – AxelH May 11 '18 at 14:00
-1

In a successful application, we use sqlite as database. In our application, we also use JPA, and define the database as a persistence unit, within the Java resources directory:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <properties>
            <property name="javax.persistence.jdbc.url" value="jdbc:sqlite:/ourdata/mySqliteDB.db" />
             <property name="javax.persistence.jdbc.driver" value="org.sqlite.JDBC" />
             <property name="eclipselink.logging.level" value="SEVERE"/>
             <property name="eclipselink.jdbc.cache-statements" value="true"/>
             <property name="eclipselink.weaving" value="false"/>
             <property name="eclipselink.weaving.fetch-groups" value="false"/>
             <property name="showSql" value="false"/>
        </properties>
    </persistence-unit>
</persistence>

We have no time-dragging access issues with sqlite.

When accessing large database tables, it is generally known that an index needs to be defined for each column used in an sql-query, in order to ensure fast query response times. This holds also for the JPA (typically the 'findall' query).

Flying Dutchman
  • 135
  • 1
  • 6