jjzjj

performance - 这个 SQLite 查询可以更快吗?

coder 2023-07-19 原文

我有一个代表安全摄像头 NVR 元数据的数据库。有一个 26 字节的 recording每 1 分钟的视频片段行。 (如果您很好奇,设计文档正在进行中 here 。)我的设计限制是 8 个摄像头,1 年(约 400 万行,每个摄像头 50 万个)。我伪造了一些数据来测试性能。这个查询比我预期的要慢:

select
  recording.start_time_90k,
  recording.duration_90k,
  recording.video_samples,
  recording.sample_file_bytes,
  recording.video_sample_entry_id
from
  recording
where
  camera_id = ?
order by
  recording.start_time_90k;

这只是扫描相机的所有数据,使用索引过滤掉其他相机并进行排序。索引看起来像这样:
create index recording_camera_start on recording (camera_id, start_time_90k);
explain query plan看起来像预期的那样:
0|0|0|SEARCH TABLE recording USING INDEX recording_camera_start (camera_id=?)

行很小。
$ sqlite3_analyzer duplicated.db
...

*** Table RECORDING w/o any indices *******************************************

Percentage of total database......................  66.3%
Number of entries................................. 4225560
Bytes of storage consumed......................... 143418368
Bytes of payload.................................. 109333605   76.2%
B-tree depth...................................... 4
Average payload per entry......................... 25.87
Average unused bytes per entry.................... 0.99
Average fanout.................................... 94.00
Non-sequential pages.............................. 1            0.0%
Maximum payload per entry......................... 26
Entries that use overflow......................... 0            0.0%
Index pages used.................................. 1488
Primary pages used................................ 138569
Overflow pages used............................... 0
Total pages used.................................. 140057
Unused bytes on index pages....................... 188317      12.4%
Unused bytes on primary pages..................... 3987216      2.8%
Unused bytes on overflow pages.................... 0
Unused bytes on all pages......................... 4175533      2.9%

*** Index RECORDING_CAMERA_START of table RECORDING ***************************

Percentage of total database......................  33.7%
Number of entries................................. 4155718
Bytes of storage consumed......................... 73003008
Bytes of payload.................................. 58596767    80.3%
B-tree depth...................................... 4
Average payload per entry......................... 14.10
Average unused bytes per entry.................... 0.21
Average fanout.................................... 49.00
Non-sequential pages.............................. 1            0.001%
Maximum payload per entry......................... 14
Entries that use overflow......................... 0            0.0%
Index pages used.................................. 1449
Primary pages used................................ 69843
Overflow pages used............................... 0
Total pages used.................................. 71292
Unused bytes on index pages....................... 8463         0.57%
Unused bytes on primary pages..................... 865598       1.2%
Unused bytes on overflow pages.................... 0
Unused bytes on all pages......................... 874061       1.2%

...

我希望每次点击特定网页时都运行这样的东西(一次可能只运行一个月,而不是一整年),所以我希望它非常快。但是在我的笔记本电脑上,它需要大部分时间,而在我想要支持的 Raspberry Pi 2 上,它太慢了。下面的时间(以秒为单位);它受 CPU 限制(用户 + 系统时间 ~= 实时):
laptop$ time ./bench-profiled
trial 0: time 0.633 sec
trial 1: time 0.636 sec
trial 2: time 0.639 sec
trial 3: time 0.679 sec
trial 4: time 0.649 sec
trial 5: time 0.642 sec
trial 6: time 0.609 sec
trial 7: time 0.640 sec
trial 8: time 0.666 sec
trial 9: time 0.715 sec
...
PROFILE: interrupts/evictions/bytes = 1974/489/72648

real    0m20.546s
user    0m16.564s
sys     0m3.976s
(This is Ubuntu 15.10, SQLITE_VERSION says "3.8.11.1")

raspberrypi2$ time ./bench-profiled
trial 0: time 6.334 sec
trial 1: time 6.216 sec
trial 2: time 6.364 sec
trial 3: time 6.412 sec
trial 4: time 6.398 sec
trial 5: time 6.389 sec
trial 6: time 6.395 sec
trial 7: time 6.424 sec
trial 8: time 6.391 sec
trial 9: time 6.396 sec
...
PROFILE: interrupts/evictions/bytes = 19066/2585/43124

real    3m20.083s
user    2m47.120s
sys 0m30.620s
(This is Raspbian Jessie; SQLITE_VERSION says "3.8.7.1")

我可能最终会处理某种非规范化数据,但首先我想看看我是否可以让这个简单的查询尽可能好地执行。我的基准测试非常简单;它提前准备好语句,然后循环执行:
void Trial(sqlite3_stmt *stmt) {
  int ret;
  while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) ;
  if (ret != SQLITE_DONE) {
    errx(1, "sqlite3_step: %d (%s)", ret, sqlite3_errstr(ret));
  }
  ret = sqlite3_reset(stmt);
  if (ret != SQLITE_OK) {
    errx(1, "sqlite3_reset: %d (%s)", ret, sqlite3_errstr(ret));
  }
}

我用 gperftools 创建了一个 CPU 配置文件.图片:


$ google-pprof bench-profiled timing.pprof
Using local file bench-profiled.
Using local file timing.pprof.
Welcome to pprof!  For help, type 'help'.
(pprof) top 10
Total: 593 samples
     154  26.0%  26.0%      377  63.6% sqlite3_randomness
     134  22.6%  48.6%      557  93.9% sqlite3_reset
      83  14.0%  62.6%       83  14.0% __read_nocancel
      61  10.3%  72.8%       61  10.3% sqlite3_strnicmp
      41   6.9%  79.8%       46   7.8% sqlite3_free_table
      26   4.4%  84.1%       26   4.4% sqlite3_uri_parameter
      25   4.2%  88.4%       25   4.2% llseek
      13   2.2%  90.6%      121  20.4% sqlite3_db_config
      12   2.0%  92.6%       12   2.0% __pthread_mutex_unlock_usercnt (inline)
      10   1.7%  94.3%       10   1.7% __GI___pthread_mutex_lock

这看起来很奇怪,让我希望它能得到改进。也许我在做一些愚蠢的事情。我特别怀疑 sqlite3_randomnesssqlite3_strnicmp操作:
  • 文档说 sqlite3_randomness在某些情况下用于插入 rowids,但我只是在做一个选择查询。为什么它现在会使用它?从略读 sqlite3 源代码中,我看到它用于选择 sqlite3ColumnsFromExprList但这似乎是在准备声明时会发生的事情。我正在这样做一次,而不是在进行基准测试的部分。
  • strnicmp用于不区分大小写的字符串比较。但是这个表中的每个字段都是一个整数。为什么要使用这个功能?它在比较什么?
  • 总的来说,我不知道为什么 sqlite3_reset会很贵或者为什么会从 sqlite3_step 调用它.

  • 架构:
    -- Each row represents a single recorded segment of video.
    -- Segments are typically ~60 seconds; never more than 5 minutes.
    -- Each row should have a matching recording_detail row.
    create table recording (
      id integer primary key,
      camera_id integer references camera (id) not null,
    
      sample_file_bytes integer not null check (sample_file_bytes > 0),
    
      -- The starting time of the recording, in 90 kHz units since
      -- 1970-01-01 00:00:00 UTC.
      start_time_90k integer not null check (start_time_90k >= 0),
    
      -- The duration of the recording, in 90 kHz units.
      duration_90k integer not null
          check (duration_90k >= 0 and duration_90k < 5*60*90000),
    
      video_samples integer not null check (video_samples > 0),
      video_sync_samples integer not null check (video_samples > 0),
      video_sample_entry_id integer references video_sample_entry (id)
    );
    

    我已经把我的测试数据 + 测试程序涂上了 tar ;您可以下载here .

    编辑 1:

    啊,通过 SQLite 代码,我看到了一个线索:
    int sqlite3_step(sqlite3_stmt *pStmt){
      int rc = SQLITE_OK;      /* Result from sqlite3Step() */
      int rc2 = SQLITE_OK;     /* Result from sqlite3Reprepare() */
      Vdbe *v = (Vdbe*)pStmt;  /* the prepared statement */
      int cnt = 0;             /* Counter to prevent infinite loop of reprepares */
      sqlite3 *db;             /* The database connection */
    
      if( vdbeSafetyNotNull(v) ){
        return SQLITE_MISUSE_BKPT;
      }
      db = v->db;
      sqlite3_mutex_enter(db->mutex);
      v->doingRerun = 0;
      while( (rc = sqlite3Step(v))==SQLITE_SCHEMA
             && cnt++ < SQLITE_MAX_SCHEMA_RETRY ){
        int savedPc = v->pc;
        rc2 = rc = sqlite3Reprepare(v);
        if( rc!=SQLITE_OK) break;
        sqlite3_reset(pStmt);
        if( savedPc>=0 ) v->doingRerun = 1;
        assert( v->expired==0 );
      }
    

    它看起来像 sqlite3_step电话sqlite3_reset关于架构更改。 ( FAQ entry ) 我不知道为什么会有架构更改,因为我的声明已经准备好了......

    编辑 2:

    我下载了 SQLite 3.10.1“合并”并使用调试符号对其进行编译。我现在得到了一个完全不同的配置文件,看起来不那么奇怪,但它并没有更快。也许我之前看到的奇怪结果是由于相同的代码折叠或其他原因。



    编辑 3:

    尝试下面 Ben 的聚集索引解决方案,速度提高了大约 3.6 倍。我认为这是我要对这个查询做的最好的事情。 SQLite 的 CPU 性能在我的笔记本电脑上约为 700 MB/s。如果没有重写它以将 JIT 编译器用于其虚拟机或类似的东西,我不会做得更好。特别是,我认为我在第一个个人资料中看到的奇怪电话实际上并没有发生;由于优化或其他原因,gcc 必须编写了误导性的调试信息。

    即使 CPU 性能会提高,吞吐量也超过了我的存储现在在冷读取时所能做的,我认为在 Pi 上也是如此(它具有用于 SD 卡的有限 USB 2.0 总线)。
    $ time ./bench
    sqlite3 version: 3.10.1
    trial 0: realtime 0.172 sec cputime 0.172 sec
    trial 1: realtime 0.172 sec cputime 0.172 sec
    trial 2: realtime 0.175 sec cputime 0.175 sec
    trial 3: realtime 0.173 sec cputime 0.173 sec
    trial 4: realtime 0.182 sec cputime 0.182 sec
    trial 5: realtime 0.187 sec cputime 0.187 sec
    trial 6: realtime 0.173 sec cputime 0.173 sec
    trial 7: realtime 0.185 sec cputime 0.185 sec
    trial 8: realtime 0.190 sec cputime 0.190 sec
    trial 9: realtime 0.192 sec cputime 0.192 sec
    trial 10: realtime 0.191 sec cputime 0.191 sec
    trial 11: realtime 0.188 sec cputime 0.188 sec
    trial 12: realtime 0.186 sec cputime 0.186 sec
    trial 13: realtime 0.179 sec cputime 0.179 sec
    trial 14: realtime 0.179 sec cputime 0.179 sec
    trial 15: realtime 0.188 sec cputime 0.188 sec
    trial 16: realtime 0.178 sec cputime 0.178 sec
    trial 17: realtime 0.175 sec cputime 0.175 sec
    trial 18: realtime 0.182 sec cputime 0.182 sec
    trial 19: realtime 0.178 sec cputime 0.178 sec
    trial 20: realtime 0.189 sec cputime 0.189 sec
    trial 21: realtime 0.191 sec cputime 0.191 sec
    trial 22: realtime 0.179 sec cputime 0.179 sec
    trial 23: realtime 0.185 sec cputime 0.185 sec
    trial 24: realtime 0.190 sec cputime 0.190 sec
    trial 25: realtime 0.189 sec cputime 0.189 sec
    trial 26: realtime 0.182 sec cputime 0.182 sec
    trial 27: realtime 0.176 sec cputime 0.176 sec
    trial 28: realtime 0.173 sec cputime 0.173 sec
    trial 29: realtime 0.181 sec cputime 0.181 sec
    PROFILE: interrupts/evictions/bytes = 547/178/24592
    
    real    0m5.651s
    user    0m5.292s
    sys     0m0.356s
    

    我可能需要保留一些非规范化数据。幸运的是,我认为我可以将它保存在我的应用程序的 RAM 中,因为它不会太大,启动不必非常快,并且只有一个进程会写入数据库。

    最佳答案

    您需要一个聚集索引,或者如果您使用的是不支持的 SQLite 版本,则需要一个覆盖索引。

    Sqlite 3.8.2 及以上

    在 SQLite 3.8.2 及更高版本中使用它:

    create table recording (
      camera_id integer references camera (id) not null,
    
      sample_file_bytes integer not null check (sample_file_bytes > 0),
    
      -- The starting time of the recording, in 90 kHz units since
      -- 1970-01-01 00:00:00 UTC.
      start_time_90k integer not null check (start_time_90k >= 0),
    
      -- The duration of the recording, in 90 kHz units.
      duration_90k integer not null
          check (duration_90k >= 0 and duration_90k < 5*60*90000),
    
      video_samples integer not null check (video_samples > 0),
      video_sync_samples integer not null check (video_samples > 0),
      video_sample_entry_id integer references video_sample_entry (id),
    
      --- here is the magic
      primary key (camera_id, start_time_90k)
    ) WITHOUT ROWID;
    

    早期版本

    在 SQLite 的早期版本中,您可以使用这种东西来创建覆盖索引。这应该允许 SQLite 从索引中提取数据值,避免为每一行获取单独的页面:
    create index recording_camera_start on recording (
         camera_id, start_time_90k,
         sample_file_bytes, duration_90k, video_samples, video_sync_samples, video_sample_entry_id
     );
    

    讨论

    成本可能是 IO(不管你说它不是),因为回想一下 IO 需要 CPU,因为数据必须复制到总线和从总线复制。

    如果没有聚集索引,行插入时带有一个 rowid,并且可能没有任何合理的顺序。这意味着对于您请求的每个 26 字节行,系统可能必须从 SD 卡中获取一个 4KB 的页面——这是一个很大的开销。

    限制为 8 个摄像头,一个简单的聚集索引 id通过确保获取的页面包含接下来需要的 10-20 行,以确保它们以插入的顺序出现在磁盘上,速度可能会提高约 10 倍。

    相机和时间上的聚集索引应确保获取的每个页面包含 100 行或更多行。

    关于performance - 这个 SQLite 查询可以更快吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34846464/

    有关performance - 这个 SQLite 查询可以更快吗?的更多相关文章

    1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    2. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

      我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

    3. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

      使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

    4. ruby - 我可以使用 Ruby 从 CSV 中删除列吗? - 2

      查看Ruby的CSV库的文档,我非常确定这是可能且简单的。我只需要使用Ruby删除CSV文件的前三列,但我没有成功运行它。 最佳答案 csv_table=CSV.read(file_path_in,:headers=>true)csv_table.delete("header_name")csv_table.to_csv#=>ThenewCSVinstringformat检查CSV::Table文档:http://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV/Table.html

    5. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

      我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

    6. ruby-on-rails - 在 Rails 和 ActiveRecord 中查询时忽略某些字段 - 2

      我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr

    7. ruby - 有人可以帮助解释类创建的 post_initialize 回调吗 (Sandi Metz) - 2

      我正在阅读SandiMetz的POODR,并且遇到了一个我不太了解的编码原则。这是代码:classBicycleattr_reader:size,:chain,:tire_sizedefinitialize(args={})@size=args[:size]||1@chain=args[:chain]||2@tire_size=args[:tire_size]||3post_initialize(args)endendclassMountainBike此代码将为其各自的属性输出1,2,3,4,5。我不明白的是查找方法。当一辆山地自行车被实例化时,因为它没有自己的initialize方法

    8. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

      我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

    9. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

      我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

    10. ruby - 一个 YAML 对象可以引用另一个吗? - 2

      我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

    随机推荐