2012-01-24 39 views
25

我在SQLite中有一個大表上的SELECT COUNT(*)出現性能問題。SQLite:COUNT在大桌子上慢

由於我還沒有收到可用的答案,我做了一些進一步的測試,我編輯了我的問題,以納入我的新發現。

我有2個表:

CREATE TABLE Table1 (
Key INTEGER NOT NULL, 
... several other fields ..., 
Status CHAR(1) NOT NULL, 
Selection VARCHAR NULL, 
CONSTRAINT PK_Table1 PRIMARY KEY (Key ASC)) 

CREATE Table2 (
Key INTEGER NOT NULL, 
Key2 INTEGER NOT NULL, 
... a few other fields ..., 
CONSTRAINT PK_Table2 PRIMARY KEY (Key ASC, Key2 ASC)) 

表1有大約800萬條記錄和表2具有約51萬條記錄,而databasefile超過5GB。

表1有2個指標:

CREATE INDEX IDX_Table1_Status ON Table1 (Status ASC, Key ASC) 
CREATE INDEX IDX_Table1_Selection ON Table1 (Selection ASC, Key ASC) 

「狀態」是必填字段,但只有6個,「選擇」不要求不同的值,且只有大約150萬值從零,只有各地不同600k不同的值。

我在兩個表上做了一些測試,你可以看到下面的時間表,並且我爲每個請求(QP)添加了「解釋查詢計劃」。我將數據庫文件放置在USB存儲棒上,以便在每次測試後將其刪除,並獲得可靠的結果,而不受磁盤緩存的干擾。 USB上的一些請求速度更快(我想由於缺少seektime),但有些速度較慢(表掃描)。正如你所看到的,計數非常緩慢,但正常的選擇很快(除了第二個,這需要16秒)。

這同樣適用於表2:

SELECT COUNT(*) FROM Table2 
    Time: 528 sec 
    QP: SCAN TABLE Table2 USING COVERING INDEX sqlite_autoindex_Table2_1(~1000000 rows) 
SELECT COUNT(Key) FROM Table2 
    Time: 249 sec 
    QP: SCAN TABLE Table2 (~1000000 rows) 
SELECT * FROM Table2 WHERE Key = 5123456 AND Key2 = 0 
    Time: 7 ms 
    QP: SEARCH TABLE Table2 USING INDEX sqlite_autoindex_Table2_1 (Key=? AND Key2=?) (~1 rows) 

爲什麼不使用自動創建的索引上表1的主鍵的SQLite? 爲什麼當他在Table2上使用自動索引時,它仍然需要很多時間?

我在SQL Server 2008 R2上創建了具有相同內容和索引的相同表,並且計數幾乎是瞬時的。

下面的一條評論建議在數據庫上執行ANALYZE。我做了,花了11分鐘才完成。 在那之後,我又跑了一些測試:

SELECT COUNT(*) FROM Table1 
    Time: 104 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~7848023 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 151 sec 
    QP: SCAN TABLE Table1 (~7848023 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 5 ms 
    QP: SEARCH TABLE Table1 USING INTEGER PRIMARY KEY (rowid>?) (~196200 rows) 
SELECT COUNT(*) FROM Table2 
    Time: 529 sec 
    QP: SCAN TABLE Table2 USING COVERING INDEX sqlite_autoindex_Table2_1(~51152542 rows) 
SELECT COUNT(Key) FROM Table2 
    Time: 249 sec 
    QP: SCAN TABLE Table2 (~51152542 rows) 

正如你所看到的,查詢了同樣的時間(除了查詢計劃現在顯示行的實數),僅較慢選擇是現在也快。

接下來,我在Table1的Key字段上創建一個額外的索引,它應該對應於自動索引。我在原始數據庫上做了這個,沒有ANALYZE數據。創建此索引需要23分鐘以上(請記住,這是在USB棒上)。

CREATE INDEX IDX_Table1_Key ON Table1 (Key ASC) 

然後我又跑測試:

SELECT COUNT(*) FROM Table1 
    Time: 4 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Key(~1000000 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 167 sec 
    QP: SCAN TABLE Table2 (~1000000 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 17 sec 
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows) 

正如你可以看到,指數幫助與COUNT(*),但與數(密鑰)。

CREATE TABLE Table1 (
Key INTEGER PRIMARY KEY ASC NOT NULL, 
... several other fields ..., 
Status CHAR(1) NOT NULL, 
Selection VARCHAR NULL) 

然後我又跑測試:

SELECT COUNT(*) FROM Table1 
    Time: 6 sec 
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~1000000 rows) 
SELECT COUNT(Key) FROM Table1 
    Time: 28 sec 
    QP: SCAN TABLE Table1 (~1000000 rows) 
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1 
    Time: 10 sec 
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows) 

儘管查詢計劃相同,則

Finaly,我使​​用列約束而不是表約束創建的表時間好得多。爲什麼是這樣 ?

問題是,ALTER TABLE不允許轉換現有的表,我有很多現有的數據庫,我不能轉換爲這種形式。另外,使用列約束而不是表約束對Table2不起作用。

有沒有人知道我做錯了什麼,以及如何解決這個問題?

我使用System.Data.SQLite版本1.0.74.0創建表並運行我使用SQLiteSpy 1.9.1的測試。

感謝,

馬克

+4

如果你有SQLite的性能問題,該解決方案通常是移動到一個更大的數據庫服務器(我推薦使用MS SQL的Postgres)。 – Borealid

+0

我沒有任何其他性能問題,所有其他選擇都很快(並使用正確的索引),插入和更新速度很快,但這只是讓我困擾的計數。 – Marc

+0

這真的很奇怪,因爲(對於DB2來說,至少)大多數RDBMS可能使用有效緩存的信息 - 如果你要求_all_行的數量(或者受到索引中某些東西的限制),它通常可以讀取那些信息索引本身 - 索引知道條目的數量。這是非常奇怪的,因爲你說所有其他的SELECT都很快 - 他們需要知道記錄數量才能夠正確優化!除非有什麼奇怪的事情發生,並且你正在鎖定表(可重複讀取事務級別,或者其他一些?)... –

回答

1

這可能幫助不大,但可以運行ANALYZE命令重建有關數據庫的統計數據。嘗試運行「ANALYZE;」以重建關於整個數據庫的統計信息,然後再次運行查詢並查看它是否更快。

+0

我執行了ANALYZE命令,需要很長時間才能完成,但沒有改變結果,計數仍然很慢。 – Marc

+0

'ANALYZE'解決了我的數據庫在執行'LEFT JOIN時遇到的問題 –

0

在列約束的問題上,SQLite將聲明爲INTEGER PRIMARY KEY的列映射到內部行ID(這又承認了許多內部優化)。理論上,它可以爲單獨聲明的主鍵約束做同樣的事情,但實踐中似乎並沒有這樣做,至少在使用SQLite的版本時是如此。 (System.Data.SQLite 1.0.74.0對應於核心SQLite 3.7.7.1。您可能想嘗試使用1.0.79.0重新檢查您的數據;您不應該更改數據庫來執行此操作,而只需要該庫。)

+0

我用最新版本的System.Data.SQlite(1.0.79.0)和I對兩個查詢(count(*)和count(key)獲得與以前相同的結果。 – Marc

+0

由於我不得不編寫一個測試程序(因爲SQLIteSpy使用舊版本的SQLite,3.7.8),所以我在32位和64位都試過,但是我獲得了相同的結果。 – Marc

0

快速查詢的輸出全部以文本「QP:SEARCH」開始。雖然慢速查詢的開始是文本「QP:SCAN」,這表明sqlite正在執行整個表的掃描以生成計數。

谷歌搜索「sqlite表掃描計數」發現the following,這表明使用全表掃描來檢索計數只是sqlite的工作方式,因此可能是不可避免的。

作爲一種解決方法,考慮到狀態只有八個值,我想知道您是否可以使用類似下面的查詢快速計數?

選擇1,其中狀態= 1個 工會 選擇1,其中狀態= 2 ...

然後計數結果中的行。這顯然是醜陋的,但它可能工作,如果它說服sqlite運行查詢作爲搜索,而不是掃描。每次返回「1」的想法是爲了避免返回真實數據的開銷。

+0

我已經找到了來自SQLite作者的[post](http://www.mail-archive.com/[email protected]/msg10279.html),所以我已經放棄了希望,因爲添加觸發器會對插入和刪除太過分了。但我試過你的建議。我首先嚐試了'SELECT COUNT(*)FROM table1 where(1,2,3,4,5,6)'中的狀態,它在86秒內執行了(更快一點),QP:'SEARCH TABLE Table1 USING COVERING INDEX IDX_Table1_Status(狀態=?)(〜60行);執行列表子查詢1'。更好但不夠好。 – Marc

+0

我試過你的聯盟建議。 'SELECT COUNT(*)FROM(SELECT 1 FROM TABLE1 WHERE Status = 1 UNION SELECT 1 FROM Table1 WHERE Status = 2 UNION ...)'返回1,對於SUM(*)也是一樣的,我想由於特徵a聯盟。 'SELECT COUNT(*)FROM(SELECT 1 FROM TABLE1 WHERE Status = 1 UNION SELECT 2 FROM Table1 WHERE Status = 2 UNION ...)'returned 6.所以finaly我嘗試了'SELECT COUNT(*)FROM(SELECT Key FROM Table1 WHERE Status = 1 UNION SELECT鍵FROM TABLE1 WHERE Status = 2 UNION ...)'返回了正確的結果,但是非常慢(116秒)。 (還是)感謝你的建議。 – Marc

+0

我的第一次嘗試('SELECT COUNT(*)FROM table1 where(1,2,3,4,5,6)')中的狀態稍微好一點,對我的其他表格(Table2)不起作用。 – Marc

0

這是改善查詢性能的潛在解決方法。從上下文來看,這聽起來像是你的查詢需要大約一分半的時間才能運行。

假設你有一個date_created列(或可以添加一列),每天在午夜(例如在上午00:05)在後臺運行一個查詢,並將該值與最後更新的日期一起保存在某處(I'稍微回來一下)。

隨後,針對您的DATE_CREATED列運行(與索引),您可以避免做這樣SELECT COUNT(*)FROM表的查詢WHERE日期date_updated> 「[TODAY] 00:00:05」 全表掃描。

將該查詢的計數值添加到持久值中,並且計算結果合理快速,一般準確。

唯一的問題是,從12:05到12:07 am(您的總計數查詢運行的持續時間),您有一個競爭條件,您可以檢查最後更新的全表掃描計數值()。如果它大於24小時,那麼您的增量計數查詢需要計算一整天的計數加上今天的時間。如果它是24小時前的<,那麼您的增量計數查詢需要拉出部分日計數(僅僅是今天的時間)。

+0

對不起,回覆晚了,我病了幾天。 SQLite不是一個SQL服務器,它是一個獨立的數據庫引擎。因此,除非使用Windows(或其他操作系統)調度程序,否則無法調度任務。無論如何,只允許在數據庫中有一個連接,所以當您的計劃計數正在運行時,數據庫將被阻止用於所有其他訪問。這不是一個適合我的解決方案。 – Marc

18

http://old.nabble.com/count(*)-slow-td869876.html

的SQLite總是這樣的COUNT(*)全表掃描。它
不保留表格上的元信息來加速這個
過程。

不保留元信息是故意設計
的決定。如果每個表存儲一個計數(或更好,btree的每個節點存儲一個計數),那麼更多更新
將不得不在每個INSERT或DELETE上發生。這個
會減慢INSERT和DELETE,即使在count(*)速度不重要的普通
的情況下。

如果你真的需要快速計數,那麼你就可以創建
上INSERT觸發器和DELETE更新單獨的表運行
計數,然後查詢分開
表中找到最新的計數。

當然,這是不值得保持全行數,如果你
需要COUNT秒依賴於WHERE子句(即WHERE字段1> 0,場2 < 1000000000)。

+1

對不起,遲到了,我生病了幾天。我已經在我的一條評論中發佈了同一篇文章的鏈接。我認爲添加觸發器對批量插入和刪除太過分了。我認爲最好在每次插入和/或刪除事務結束時跟蹤表計數,所以計數器只更新一次,而不是每次插入/刪除。 – Marc

+0

另外,'COUNT(1)'應該比'COUNT(*)'甚至'COUNT(「id」)'更快。 –

+0

@AlixAxel在我所有的測試'COUNT()'和'COUNT(*)'是最快的,'COUNT(1)'採取雙人和'COUNT(ROWID)'以三倍的時間。 – springy76

0

我有同樣的問題,在我的情況VACUUM命令幫助。在數據庫上執行COUNT(*)後,速度提高了近100倍。但是,命令本身在我的數據庫中需要幾分鐘(2000萬條記錄)。當我的軟件在主窗口銷燬後退出時,我通過運行VACUUM解決了這個問題,所以延遲不會對用戶造成問題。

+3

真空將強制讀取和寫入整個文件,所以它會填滿磁盤內容到內存中緩存。這就是爲什麼它是速度更快。如果你重新啓動你的電腦,你會發現它再次放緩,我想。 –

18

如果您還沒有DELETE D任何記錄,這樣做的:

SELECT MAX(_ROWID_) FROM "table" LIMIT 1; 

將避免全表掃描。請注意,_ROWID_ is a SQLite identifier

+0

應該是最好的答案。返回瞬間並給出了一個很好的近似(通常是你想要的) – easytiger

+1

確認,這將返回幾毫秒內的值,我的數據庫有1.15億記錄在表格中,做一個完整的COUNT(*)從來沒有真正compl eted(4小時後我放棄了等待)。 –

+1

這是好的,但要記住什麼阿利克斯已經說的 - 即使你已經在這個表中刪除一條記錄 - 曾經,你會得到不正確的結果(因爲_ROWID_是一個不斷遞增的記錄ID,而「刪除」會不會導致_ROWID_遞減)。 – strangetimes

2

不要指望星星,數數記錄!或者在其他語言中,永不發佈

SELECT COUNT(*)FROM tablename;

使用

SELECT COUNT(ROWID)FROM tablename;

調用EXPLAIN QUERY PLAN以查看差異。確保你有一個包含WHERE子句中提到的所有列的索引。

+0

對我來說似乎沒有什麼不同。 – Fidel

+0

@Fidel取決於你的數據庫模型和設置。在我的實驗中,SQLite在使用ROWID進行全表計數時,對星號搜索進行了全面掃描,而不是索引搜索。也許我也忽略了別的東西,我並不認爲它是完美的。不過,我仍然推薦使用**解釋查詢計劃**!只需強制DB在PK上使用索引,而不是全面掃描,並注意下面的Arnaud註釋中的操作系統緩存效果。願你的疑問總是快速! – Thinkeye

+1

這在大多數情況下是錯誤的。至少在我的所有表中,rowid搜索需要22secs,而4s的計數 – easytiger