为啥 SQLite 使用 JDBC 这么慢?

Posted

技术标签:

【中文标题】为啥 SQLite 使用 JDBC 这么慢?【英文标题】:Why SQLite is so slow with JDBC?为什么 SQLite 使用 JDBC 这么慢? 【发布时间】:2018-05-11 12:28:13 【问题描述】:

阅读了很多东西,比如这里: SQLite queries much slower using JDBC than in Firefox SqliteManager plugin

但我不知道有什么问题。 情况是,我有一个 SQLite 数据库(来自 android 平板电脑)和一个不太大的表(其中约 50.000 行) 例如,如果我在 Sqlite Manager 中运行“select * from table”,则需要 0.11 秒,正确。

但是...如果我在 Java 程序(使用 SQLite JDBC)中执行此操作,则需要 20 分钟!!!不开玩笑。

有人(某处)说这取决于版本。 但我的问题是怎么做?

因为这个命令:“SELECT sqlite_version()”在每种情况下都会在同一个 .db 文件上给出不同的结果:

在一个非常古老的 sqlite 管理器中,它提供 3.6.19 在 Sqlite Studio 3.15 中 并与来自 sqlite.org 的最新 .exe 一起提供 3.23.1 所以这不是数据库相关的东西,我认为是使用的sqlite3.exe的版本。

我可以整天更改 JDBC 驱动程序(我做过几次),但我怎么知道我需要哪个?

有人有什么想法吗?我完全被它困住了。

编辑: 好的,JDBC jar 来自这里:https://bitbucket.org/xerial/sqlite-jdbc/downloads/

而且我的代码真的很基础,一开始我只是想测量一下速度。

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

executeQuery() 行需要 20 分钟。

【问题讨论】:

请添加相关的java代码,执行查询需要20分钟。 这种性能差异绝对不是(仅)与 JDBC API 相关的。可能是一个特定的驱动程序错误,但您没有确切提到您一直在使用什么驱动程序,也没有提到您运行 JDBC 逻辑的执行环境,也没有显示任何代码,这是重现此问题所需要的 我们没有玻璃球...我们无法知道问题出在您的代码中还是在驱动程序/库/sqlite 版本中。 好吧,您正在比较在 SQLite 管理中完成的 Select 与插入到表中的代码(使用 String 连接)。首先,使用PreparedStatement.addBatch() 插入每一行(检查SQLITE 中是否管理批处理)。然后,不要连接 yoru String Axel,没有实际的插入,我只是构建了一个未执行的字符串。 (其实我做到了,几秒钟就完成了,但问题出在上面那部分代码上) 【参考方案1】:

您正在创建一个有 50k 行的 String,这意味着您正在创建 50k * 5 String(每个连接都会创建一个新的 String 实例。这会影响您的性能。

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

我注意到你没有执行String script,所以如果你只想创建一个String,请使用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();

StringBuilder 防止大量String 实例被创建。

如果您想在此之后插入这些值,请直接使用 PreparedStatement 而不是构建查询:

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

那么如果你想改进这一点,请使用批处理系统发送小块插入。使用Statement.addBatch()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 基准测试

不是官方的,只是一个简单执行的 Duration

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

这需要 15 纳秒

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

这需要 >6 秒

【讨论】:

我明白你的意思,但事实并非如此。我的计划是将这个数据库转移到 mysql 中。如果我生成 50k 插入(即使在事务中)也会非常慢。正如我之前所经历的,插入 MySQL 的最佳方法是一个插入命令,而不是像 (1, 1, "abc"), (2, 3, "abc")... 等所有数据。所以我需要那个形式的那个字符串。 @Rezmalac,我刚刚编辑(在开头)向您展示了如何使用更少的Strings 和StringBuilder 构建String 查询。但这不会改变插入这些值的时间。如果您发现大量插入存在问题,您可以检查配置和使用的代码,因为PreparedStatement 更适合(并且更安全!) @Rezmalac 给你一个想法,连接 100k String 需要 7 秒,因为它使用的是 StringBuilder,在 ms 中甚至无法测量。 F@ck 我,这似乎真的是那个愚蠢的 java 字符串连接。你得到了我的选票。我应该更改问题的标题吗?因为它不再相关...... @Rezmalac 真的是“愚蠢的 java 连接”的错吗? ;-) 永远不要在循环中连接String,这是一个很好的规则。每种语言的实例化都需要时间。 “更改”一个不可变对象意味着创建一个新实例,所以这并不是String java 的错;)嗯,这是 SQLite 查询中的执行,“在 SQLte ResultSet 中构建查询”或类似的东西可能是更正确,但这取决于你。【参考方案2】:

在成功的应用程序中,我们使用sqlite 作为数据库。在我们的应用程序中,我们还使用 JPA,并将数据库定义为持久单元,位于 Java 资源目录中:

<?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>

我们在使用 sqlite 时没有拖延时间的访问问题。

在访问大型数据库表时,一般都知道需要为 sql-query 中使用的每一列定义一个索引,以确保快速的查询响应时间。这也适用于 JPA(通常是“findall”查询)。

【讨论】:

以上是关于为啥 SQLite 使用 JDBC 这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 putImageData 这么慢?

为啥 Moose 代码这么慢?

Node - 为啥我的 gif 在使用 GifEncoder 时这么慢

为啥在 Chrome 上的 for 循环中使用 let 这么慢?

为啥 Swift 编译时间这么慢?

为啥同时使用 numba.cuda 和 CuPy 从 GPU 传输数据这么慢?