【问题标题】:Can SQLite handle 90 million records?SQLite 可以处理 9000 万条记录吗?
【发布时间】:2011-03-10 19:30:42
【问题描述】:

或者我应该使用不同的锤子来解决这个问题。

我有一个用于存储数据的非常简单的用例,实际上是一个稀疏矩阵,我尝试将其存储在 SQLite 数据库中。我创建了一个表:

create TABLE data ( id1 INTEGER KEY, timet INTEGER KEY, value REAL )

一年中的大部分时间,我都会在其中插入大量数据(每 10 分钟 800 个元素,每天 45 次)。 (id1,timet) 的元组总是唯一的。

timet 值是自纪元以来的秒数,并且会一直增加。出于所有实际目的,id1 是一个随机整数。不过,可能只有 20000 个唯一 ID。

然后我想访问 id1==someid 的所有值或访问 timet==sometime 的所有元素。在我通过 Linux 上的 C 接口使用最新 SQLite 的测试中,查找其中一个(或此查找的任何变体)大约需要 30 秒,这对于我的用例来说不够快。

我尝试为数据库定义一个索引,但这将插入速度减慢到完全不可行的速度(虽然我可能做错了......)

上表导致对任何数据的访问都非常缓慢。我的问题是:

  • SQLite 是否完全是错误的工具?
  • 我可以定义索引来显着加快速度吗?
  • 我是否应该为此使用 HDF5 而不是 SQL?

请原谅我对 SQL 的基本了解!

谢谢

我包含一个代码示例,该示例显示了在使用索引时插入速度如何减慢到爬行。使用“创建索引”语句,代码需要 19 分钟才能完成。没有它,它会在 18 秒内运行。


#include <iostream>
#include <sqlite3.h>

void checkdbres( int res, int expected, const std::string msg ) 
{
  if (res != expected) { std::cerr << msg << std::endl; exit(1); } 
}

int main(int argc, char **argv)
{
  const size_t nRecords = 800*45*30;

  sqlite3      *dbhandle = NULL;
  sqlite3_stmt *pStmt = NULL;
  char statement[512];

  checkdbres( sqlite3_open("/tmp/junk.db", &dbhandle ), SQLITE_OK, "Failed to open db");

  checkdbres( sqlite3_prepare_v2( dbhandle, "create table if not exists data ( issueid INTEGER KEY, time INTEGER KEY, value REAL);", -1, & pStmt, NULL ), SQLITE_OK, "Failed to build create statement");
  checkdbres( sqlite3_step( pStmt ), SQLITE_DONE, "Failed to execute insert statement" );
  checkdbres( sqlite3_finalize( pStmt ), SQLITE_OK, "Failed to finalize insert");
  checkdbres( sqlite3_prepare_v2( dbhandle, "create index issueidindex on data (issueid );", -1, & pStmt, NULL ), SQLITE_OK, "Failed to build create statement");
  checkdbres( sqlite3_step( pStmt ), SQLITE_DONE, "Failed to execute insert statement" );
  checkdbres( sqlite3_finalize( pStmt ), SQLITE_OK, "Failed to finalize insert");
  checkdbres( sqlite3_prepare_v2( dbhandle, "create index timeindex on data (time);", -1, & pStmt, NULL ), SQLITE_OK, "Failed to build create statement");
  checkdbres( sqlite3_step( pStmt ), SQLITE_DONE, "Failed to execute insert statement" );
  checkdbres( sqlite3_finalize( pStmt ), SQLITE_OK, "Failed to finalize insert");

  for ( size_t idx=0; idx < nRecords; ++idx)
  {
    if (idx%800==0)
    {
      checkdbres( sqlite3_prepare_v2( dbhandle, "BEGIN TRANSACTION", -1, & pStmt, NULL ), SQLITE_OK, "Failed to begin transaction");
      checkdbres( sqlite3_step( pStmt ), SQLITE_DONE, "Failed to execute begin transaction" );
      checkdbres( sqlite3_finalize( pStmt ), SQLITE_OK, "Failed to finalize begin transaction");
      std::cout << "idx " << idx << " of " << nRecords << std::endl;
    }

    const size_t time = idx/800;
    const size_t issueid = idx % 800;
    const float value = static_cast<float>(rand()) / RAND_MAX;
    sprintf( statement, "insert into data values (%d,%d,%f);", issueid, (int)time, value );
    checkdbres( sqlite3_prepare_v2( dbhandle, statement, -1, &pStmt, NULL ), SQLITE_OK, "Failed to build statement");
    checkdbres( sqlite3_step( pStmt ), SQLITE_DONE, "Failed to execute insert statement" );
    checkdbres( sqlite3_finalize( pStmt ), SQLITE_OK, "Failed to finalize insert");

    if (idx%800==799)
    {
      checkdbres( sqlite3_prepare_v2( dbhandle, "END TRANSACTION", -1, & pStmt, NULL ), SQLITE_OK, "Failed to end transaction");
      checkdbres( sqlite3_step( pStmt ), SQLITE_DONE, "Failed to execute end transaction" );
      checkdbres( sqlite3_finalize( pStmt ), SQLITE_OK, "Failed to finalize end transaction");
    }
  }

  checkdbres( sqlite3_close( dbhandle ), SQLITE_OK, "Failed to close db" ); 
}

【问题讨论】:

  • 在任何给定时间您需要访问多少历史数据?您可以将旧数据存档到另一个表中以保持持久性并节省查询“相关”数据的时间。
  • 需要访问所有历史数据。如果需要,我可以每年将其拆分为一个数据集,但我希望 SQLite 可以让我不必管理这些细节。
  • 能否请您发布使用 sqlite3* 函数的整个代码(省略其他部分)?如果进程“爬到完全停止”,那肯定是不对的。
  • 您需要相同粒度的历史数据吗?如果没有,RRDB 可能会很有趣。
  • 为什么需要查找id1==someid?这是为了让您以后可以选择“随机”数据吗?从您的情况来看,您似乎不需要 someid 是唯一的,因此在 2010-01-01 记录 #3 和在 2010-06-06 记录 #79832759385 可能具有相同的 id1 字段。同样,对于每秒插入的多条记录,您在 HHMMSS 上存在冲突。因此,在情况 1 中,您选择 id==someId 并获得零个、一个或多个稀疏记录;在情况 2 中,您会得到零个、一个或多个相邻记录。如果这些是您的需求,您可能会受益于不同的运行时查询方法。

标签: sql sqlite


【解决方案1】:

您是否一次插入所有 800 个元素?如果是这样,在事务中进行插入将大大加快该过程。

http://www.sqlite.org/faq.html#q19

SQLite 可以处理非常大的数据库。见http://www.sqlite.org/limits.html

【讨论】:

  • 我已经将每组 45*800 插入作为单个事务进行,以加快插入速度。这确实对构建数据库产生了很大的影响,在大约 300 毫秒内为我提供了大约 45*800 个元素的插入。我的问题仍然是查询。
  • 您是否使用了所需的索引?索引会将读取时间从 O(n) 降低到 O(log n)。如何最好地检索数据将取决于您一次检索的数据量。您是在检索整个集合,还是单独检索单个记录?
  • 我通过执行以下操作尝试了索引:CREATE INDEX INDEX1 on data (id1);和 CREATE INDEX INDEX2 on data ( timet );然而,这意味着当我建立 db 时,随后的每组插入语句花费的时间越来越长,并且该过程最终完全停止。这些创建索引语句正确/合理吗?
  • 如果你必须同时查询 id 和 time 的话。我得考虑一下。
  • 将所有 800 个 id 值放入一个按时间索引的记录中是否可行?数据有多稀疏,即您是否总是写入所有 800 个值?
【解决方案2】:

回答我自己的问题,作为一个放置一些细节的地方:

事实证明(正如上面正确建议的那样)索引创建是一个缓慢的步骤,每次我执行另一个插入事务时,索引都会更新,这需要一些时间。我的解决方案是: (一)创建数据表 (B) 插入我所有的历史数据(几年价值) (C) 创建索引

现在所有查找等都非常快,sqlite 做得很好。随后的每日更新现在只需几秒钟即可插入 800 条记录,但这没问题,因为它仅每 10 分钟左右运行一次。

感谢 Robert Harvey 和 maxwellb 提供上述帮助/建议/答案。

【讨论】:

    【解决方案3】:

    我查看了您的代码,我认为您使用 preparefinalize 语句可能做得过火了。我绝不是 SQLite 专家,但每次在循环中准备语句时都会有很大的开销。

    引用自 SQLite 网站:

    在准备好的语句之后 通过一个或多个调用评估 sqlite3_step(),可以在里面重置 为了通过电话再次评估 到sqlite3_reset()。使用 sqlite3_reset() 在现有的 准备好的声明,而不是创建一个 新的准备好的语句避免 不必要的电话 sqlite3_prepare()。在许多 SQL 语句,运行所需的时间 sqlite3_prepare() 等于或超过 sqlite3_step() 所需的时间。 所以避免调用 sqlite3_prepare() 可能会导致 显着的性能提升。

    http://www.sqlite.org/cintro.html

    在您的情况下,您可以尝试binding new values to your existing statement,而不是每次都准备新的语句。

    所有这一切,我认为索引可能是真正的罪魁祸首,因为随着您添加更多数据,时间会不断增加。我对此很好奇,我计划在周末进行一些测试。

    【讨论】:

      【解决方案4】:

      由于我们知道在表上没有索引时捕获数据会很快,因此实际可行的是:

      1. 在没有索引的临时表中捕获 800 个值。

      2. 使用采用 SELECT 语句的 INSERT INTO 形式将记录复制到主表(包含索引)。

      3. 从临时表中删除记录。

      该技术基于这样的理论,即采用 SELECT 语句的 INSERT INTO 比执行单个 INSERT 更快。

      步骤 2 可以使用Asynchronous Module 在后台执行,如果它仍然被证明有点慢的话。这利用了捕获之间的停机时间。

      【讨论】:

        【解决方案5】:

        考虑使用一个表用于给定日期的新插入,而不使用索引。然后,在每天结束时,运行一个脚本:

        1. 将 new_table 中的新值插入到 master_table 中
        2. 清除 new_table 以供第二天处理

        如果您可以在 O(log n) 中查找历史数据,并在 O(n) 中查找今天的数据,这应该是一个不错的折衷方案。

        【讨论】:

        • 这是个好主意,只要有一些停机时间可以做到这一点。
        • @Robert,应该有一些停机时间。最初的问题表明他每 10 分钟插入一次行,每天 45 次。这意味着脚本运行应该有超过 16 小时的停机时间。
        • 这就是我提出这个建议的原因。正是因为 10 分钟 x 45。
        • 我也松散地使用术语“脚本”。这当然可以是一个已编译的程序。
        【解决方案6】:

        我无法从您的规范中看出,但如果 ID 字段一直在增加,并且时间字段包含 YYYYMMDD 以表示唯一性并且也一直在增加,并且您正在进行 ID 搜索或时间搜索,那么最简单的非数据库解决方案是简单地将所有记录附加到固定字段文本或二进制文件(因为它们是按“排序”顺序生成的)并使用代码对所需记录进行二进制搜索(例如,找到首先记录 ID 或感兴趣的时间,然后依次遍历所需的范围)。

        【讨论】:

        • 时间值是一个time_t,随着我添加值而增加,每块800个值同时存在。 id1 是(几乎)随机整数。我正在计划使用 hdf5 进行二进制文件存储这样的事情,但希望 sqlite 可能会有所帮助。特别是,该 sqlite 对文件具有事务性写入,因为我将有多个读者偶尔写入访问文件。作为增加的复杂性,该文件保存在 NFS 存储上(但我可以控制访问存储的 linux 内核,并且支持正确的 nfs 锁定)。 sqlite.org/faq.html#q5
        【解决方案7】:

        在构建大型 SQLite 数据库时,请始终在创建索引之前尽可能多地插入数据。这将比在插入数据之前创建索引快很多倍。

        【讨论】:

          【解决方案8】:

          表中的理论最大行数为 2^64(18446744073709551616 或大约 1.8e+19)。由于将首先达到 140 TB 的最大数据库大小,因此无法达到此限制。一个 140 TB 的数据库最多可以容纳大约 1e+13 行,并且只有在没有索引且每行包含非常少数据的情况下。

          【讨论】:

            猜你喜欢
            • 2012-10-21
            • 1970-01-01
            • 1970-01-01
            • 2011-02-19
            • 2020-01-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多