2016-01-18 65 views
25

我有一個數據庫表示安全攝像機NVR的元數據。每1分鐘視頻段有一個26字節的recording行。 (如果您好奇,設計文檔正在進行中here。)我的設計限制是8個攝像頭,1年(約400萬行,每個攝像頭50萬)。我僞裝了一些數據來測試性能。此查詢比我預期的要慢:這個SQLite查詢可以做得更快嗎?

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綁定的(用戶+ SYS時間〜=實時):

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

我做了一個CPU配置文件與gperftools。圖片:

CPU profile graph

$ 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用於在某些情況下,插入的rowid,但我只是做一個選擇查詢。爲什麼現在要使用它?從瀏覽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) 
); 

我柏油了我的測試數據+測試程序;你可以下載它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「amalgation 「並通過調試符號進行編譯。現在我看到一個非常不同的配置文件,看起來並不奇怪,但它並沒有更快。也許我之前看到的奇怪的結果是由於相同的代碼摺疊或某事。

enter image description here


編輯3:

下面試圖奔的聚簇索引的解決方案,它是關於3.6X更快。我認爲這是我要用這個查詢做的最好的。 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中,因爲它不會太大,啓動不必非常快,並且只有一個進程寫入數據庫。

+3

感謝您爲您的問題投入瞭如此多的研究工作!你能說出你是CPU限還是IO限?你是否在你的Raspberry Pi上使用[Class 10 SD卡](http://raspberrypi.stackexchange.com/q/12191/27703)? –

+2

謝謝!還有一個我忘記回答的重要問題。它在兩個系統上都是CPU綁定的。我在上面添加了「時間」輸出來顯示。我正在使用Class 10 SD卡:http://www.amazon.com/gp/product/B010Q588D4?psc=1&redirect=true&ref_=od_aui_detailpages00 –

+2

真棒問題!有了這個級別的細節,你應該也可以發佈到sqlite-users ML。 – viraptor

回答

2

您需要一個聚集索引,或者如果您使用不支持一個版本的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倍的速度增加,方法是確保抓取的頁面包含接下來的10-20行需要。

相機和時間上的聚集索引應確保每個抓取的頁面包含100行或更多行。

+0

謝謝!有趣的解決方案,我只是在上面做了基準測試;它快了3倍。 'camera_id,start_time_90k'可能不是唯一的(我希望它是,但是時間跳躍等等,而我的系統可能應該更喜歡記錄某些東西並稍後排除時間偏移)。但是我想我可以把時間稍微調整一下(什麼是1/9萬分之一秒的偏移量),或者只是將「id」作爲主鍵的第三列加上它自己獨特的非空索引。 –

+0

@ScottLamb,我會去找Id。你永遠不會知道鐘錶 - 他們有時會倒退!至少該ID會給你實際插入的訂單,所以不會丟失。 – Ben

相關問題