【问题标题】:SQLite: COUNT slow on big tablesSQLite:大表上的 COUNT 慢
【发布时间】:2012-02-17 19:44:34
【问题描述】:

我在 SQLite 中遇到性能问题,在大型表上使用 SELECT 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))

Table1 大约有 800 万条记录,Table2 大约有 5100 万条记录,数据库文件超过 5GB。

Table1 还有 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 万个与 null 不同的值,只有大约 60 万个不同的值。

我对这两个表都做了一些测试,您可以在下面看到时间安排,并且我为每个请求 (QP) 添加了“解释查询计划”。我将数据库文件放在 USB 记忆棒上,这样我就可以在每次测试后将其删除并获得可靠的结果,而不会干扰磁盘缓存。有些请求在 USB 上更快(我想是因为缺少搜索时间),但有些请求较慢(表扫描)。

SELECT COUNT(*) FROM Table1
    Time: 105 sec
    QP: SCAN TABLE Table1 USING COVERING INDEX IDX_Table1_Selection(~1000000 rows)
SELECT COUNT(Key) FROM Table1
    Time: 153 sec
    QP: SCAN TABLE Table1 (~1000000 rows)
SELECT * FROM Table1 WHERE Key = 5123456
    Time: 5 ms
    QP: SEARCH TABLE Table1 USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
SELECT * FROM Table1 WHERE Status = 73 AND Key > 5123456 LIMIT 1
    Time: 16 sec
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Status (Status=?) (~3 rows)
SELECT * FROM Table1 WHERE Selection = 'SomeValue' AND Key > 5123456 LIMIT 1
    Time: 9 ms
    QP: SEARCH TABLE Table1 USING INDEX IDX_Table1_Selection (Selection=?) (~3 rows)

如您所见,计数非常慢,但正常选择速度很快(第二次选择除外,需要 16 秒)。

Table2 也是如此:

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)

为什么 SQLite 没有在 Table1 的主键上使用自动创建的索引? 为什么,当他在 Table2 上使用自动索引时,仍然需要很多时间?

我在 SQL Server 2008 R2 上创建了具有相同内容和索引的相同表,并且计数几乎是瞬时的。

下面的其中一个 cmets 建议对数据库执行 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 字段上创建一个额外的索引,它应该对应于自动索引。我在原始数据库上做了这个,没有分析数据。创建此索引花费了 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(*) 有帮助,但对 count(Key) 没有帮助。

最后,我使用列约束而不是表约束来创建表:

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)

虽然查询计划相同,但时间要好很多。这是为什么呢?

问题是 ALTER TABLE 不允许转换现有表,并且我有很多现有数据库无法转换为这种形式。此外,使用列约束代替表约束不适用于 Table2。

有谁知道我做错了什么以及如何解决这个问题?

我使用 System.Data.SQLite 版本 1.0.74.0 创建表并运行我使用 SQLiteSpy 1.9.1 的测试。

谢谢,

马克

【问题讨论】:

  • 如果您在使用 SQLite 时遇到性能问题,解决方案通常是升级到更大的数据库服务器(我推荐 Postgres 而不是 MS SQL)。
  • 我没有任何其他性能问题,所有其他选择都很快(并使用正确的索引),插入和更新很快,只是计数让我感到困扰。
  • @Borealid:Postgres 还对没有 WHERE 子句的 COUNT 查询使用全表扫描,因此切换到 Postgres 不会为您带来该查询的任何性能优势。
  • @limscoder 但是您可以执行触发器来维护信息。或者通过让它们在 RAM 中的热计数来节省您在正常查询上的时间。此外,InnoDB 在这方面并没有快多少,而 MyISAM 没有事务......
  • @borealis SQLite 也支持触发器,因此跟踪计数的机制是相同的。

标签: sql database performance sqlite


【解决方案1】:

如果您还没有DELETEd 任何记录,请执行以下操作:

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

将避免全表扫描。

注意_ROWID_ is a SQLite identifier。

【讨论】:

  • 应该是最好的答案。立即返回并给出一个很好的近似值(通常是你想要的)
  • 确认,这会在几毫秒内为我的数据库返回一个值,其中表中有 1.15 亿条记录。执行完整的 COUNT(*) 从未真正完成(我在 4 小时后放弃了等待)。
  • 这很好,但请记住 Alix 已经说过的话 - 即使您删除了此表中的一条记录 - 永远,您会得到不正确的结果(因为 ROWID i> 是一个不断递增的记录 ID,“删除”不会导致 ROWID 递减)。
  • 还应该注意的是,MAX(_ROWID_) 的使用只有在您有代理键并且如前所述没有删除任何行的情况下才有意义。这两个限制意味着无论多快,这都不是获取行数的可靠方法。
  • 与COUNT(column_name) 或COUNT(*) 的约5.3 亿条记录相比,给了我一个即时结果。我的是一个静态表,所以只有在没有删除行的情况下才有效的警告不适用于我。
【解决方案2】:

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

SQLite 总是对 count(*) 进行全表扫描。它 不会在表上保留元信息以加快速度 处理。

不保留元信息是经过深思熟虑的设计 决定。如果每个表都存储一个计数(或者更好的是,每个 B树的节点存储了一个计数)然后更多更新 必须在每个 INSERT 或 DELETE 上发生。这 会减慢 INSERT 和 DELETE,即使在常见的情况下 count(*) 速度不重要的情况。

如果你真的需要一个快速的 COUNT,那么你可以创建 更新正在运行的 INSERT 和 DELETE 触发器 在单独的表中计数,然后查询该单独的表 表来查找最新计数。

当然,如果您保留完整的行数,则不值得 需要依赖于 WHERE 子句的 COUNT(即 WHERE field1 > 0 和 field2

【讨论】:

  • 抱歉回复晚了,我病了几天了。我已经在我的一个 cmets 中发布了指向同一帖子的链接。我认为添加触发器对大量插入和删除来说太不利了。我认为最好在每次插入和/或删除事务结束时跟踪表计数,因此计数器只更新一次,而不是在每次插入/删除时更新。
  • 另外,COUNT(1) 应该比COUNT(*) 甚至COUNT("id") 更快。
  • @AlixAxel 在我所有的测试中 COUNT() 和 COUNT(*) 是最快的,COUNT(1) 是两倍,COUNT(ROWID) 是三倍。
  • @springy76,这似乎不合逻辑。您能否分享您的表的架构和您进行基准测试的行数?
  • @AlixAxel 该表很简单CREATE TABLE [Elements] ([ElementId] integer PRIMARY KEY NOT NULL,[ParentId] integer REFERENCES [Elements] ([ElementId]), [Name] nvarchar, ....,包含超过 200 万行。 EXPLAIN QUERY PLAN 始终相同(使用索引之一作为 COVERING INDEX),但 COUNT() 在 ~200ms 内执行,COUNT(1) 在 ~400ms 内执行,COUNT(rowid) 在 ~600ms 内执行。 MAX(rowid) 在 0 毫秒内执行。
【解决方案3】:

这可能没有多大帮助,但您可以运行ANALYZE 命令来重建有关您的数据库的统计信息。尝试运行“ANALYZE;”以重建有关整个数据库的统计信息,然后再次运行查询,看看它是否更快。

【讨论】:

  • 我执行了ANALYZE命令,花了很长时间才完成,但是并没有改变结果,计数还是很慢。
  • ANALYZE 修复了我的数据库在执行LEFT JOIN时的问题
【解决方案4】:

不要数星星,数记录!或者用其他语言,永远不要发出

从表名中选择计数(*);

使用

SELECT COUNT(ROWID) FROM 表名;

调用 EXPLAIN QUERY PLAN 以查看差异。确保您有一个包含 WHERE 子句中提到的所有列的索引。

【讨论】:

  • 对我来说似乎没有什么不同。
  • @Fidel 取决于您的数据库模型和设置。在我的实验中,当与 ROWID 一起使用以获取完整表计数时,SQLite 对星号搜索而不是索引搜索进行了完整扫描。也许我也忽略了其他一些东西,我并不声称自己是完美的。但是,我仍然推荐使用explain query plan!只需强制数据库在 PK 上使用索引而不是完全扫描,并注意下面的 Arnaud cmets 的操作系统缓存效果。愿您的查询总是很快!
  • 在大多数情况下这是错误的。至少在我所有按 rowid 搜索的表中需要 22 秒,而计数为 4 秒
【解决方案5】:

关于列约束,SQLite 将声明为 INTEGER PRIMARY KEY 的列映射到内部行 id(这反过来又允许一些内部优化)。理论上,它可以对单独声明的主键约束做同样的事情,但在实践中似乎不会这样做,至少在使用 SQLite 的版本时是这样。 (System.Data.SQLite 1.0.74.0 对应于核心 SQLite 3.7.7.1。您可能想尝试使用 1.0.79.0 重新检查您的数字;您不需要更改数据库即可,只需更改库即可。)

【讨论】:

  • 我用最新版本的 System.Data.SQlite (1.0.79.0) 尝试了两个查询(count(*) 和 count(key)),得到的结果和以前一样。
  • 因为我必须编写一个小测试程序(因为 SQLIteSpy 使用的是旧版本的 SQLite,3.7.8),所以我在 32 位和 64 位上都进行了尝试,但我得到了相同的结果。
【解决方案6】:

快速查询的输出均以文本“QP: SEARCH”开头。而那些慢查询以文本“QP: SCAN”开头,这表明 sqlite 正在执行整个表的扫描以生成计数。

谷歌搜索“sqlite table scan count”会找到the following,这表明使用全表扫描来检索计数正是 sqlite 的工作方式,因此可能是不可避免的。

作为一种解决方法,鉴于状态只有八个值,我想知道您是否可以使用如下查询快速获得计数?

选择 1,其中状态=1 联盟 选择 1 其中状态 = 2 ...

然后计算结果中的行数。这显然很难看,但如果它说服 sqlite 将查询作为搜索而不是扫描运行,它可能会起作用。每次返回“1”的想法是避免返回真实数据的开销。

【讨论】:

  • 我已经从 SQLite 的作者那里找到了这个post,所以我已经放弃了希望,因为添加触发器会对插入和删除造成太大的惩罚。但我尝试了你的建议。我第一次尝试SELECT COUNT (*) FROM table1 where Status in (1,2,3,4,5,6),它在 86 秒内执行(快一点),QP:SEARCH TABLE Table1 USING COVERING INDEX IDX_Table1_Status (Status=?) (~60 rows); EXECUTE LIST SUBQUERY 1。更好但还不够好。
  • 我试过你的工会建议。 SELECT COUNT(*) FROM (SELECT 1 FROM Table1 WHERE Status = 1 UNION SELECT 1 FROM Table1 WHERE Status = 2 UNION...) 返回 1,与 SUM(*) 相同,我猜是由于联合的特性。 SELECT COUNT(*) FROM (SELECT 1 FROM Table1 WHERE Status = 1 UNION SELECT 2 FROM Table1 WHERE Status = 2 UNION...) 返回 6。所以最后我尝试了 SELECT COUNT(*) FROM (SELECT Key FROM Table1 WHERE Status = 1 UNION SELECT Key FROM Table1 WHERE Status = 2 UNION...),它返回了正确的结果,但速度很慢(116 秒)。不过感谢您的建议。
  • 我的第一次尝试 (SELECT COUNT (*) FROM table1 where Status in (1,2,3,4,5,6)) 更好一点,但不适用于我的其他表 (Table2)。
【解决方案7】:

这是提高查询性能的潜在解决方法。从上下文来看,您的查询听起来需要大约一分半钟才能运行。

假设您有一个 date_created 列(或可以添加一个),每天午夜(例如上午 00:05)在后台运行一个查询,并将该值与计算的 last_updated 日期一起保存在某处(我会过会儿再说)。

然后,针对您的 date_created 列(带有索引)运行,您可以通过执行 SELECT COUNT(*) FROM TABLE WHERE date_updated > "[TODAY] 00:00:05" 之类的查询来避免全表扫描。

将该查询中的计数值添加到您的持久值中,您就有了一个相当快的计数,通常是准确的。

唯一的问题是,从上午 12:05 到上午 12:07(总计数查询运行的持续时间),您有一个竞争条件,您可以检查全表扫描 count() 的 last_updated 值。如果它超过 24 小时,那么您的增量计数查询需要提取一整天的计数加上今天经过的时间。如果它小于 24 小时,那么您的增量计数查询需要提取部分天数(只是今天过去的时间)。

【讨论】:

  • 抱歉回复晚了,我病了几天。 SQLite 不是 SQL 服务器,它是一个独立的数据库引擎。因此,您无法安排任务,除非您使用 Windows(或其他操作系统)调度程序。无论如何,一次只允许一个与数据库的连接,因此当您的计划计数正在运行时,该数据库将被阻止所有其他访问。这不是一个适合我的解决方案。
【解决方案8】:

我遇到了同样的问题,在我的情况下 VACUUM 命令有帮助。在数据库 COUNT(*) 上执行后,速度提高了近 100 倍。但是,命令本身在我的数据库中需要几分钟(2000 万条记录)。当我的软件在主窗口销毁后退出时,我通过运行 VACUUM 解决了这个问题,因此延迟不会给用户带来问题。

【讨论】:

  • VACUUM 将强制读取和写入整个文件,因此它将磁盘内容填充到内存缓存中。这就是为什么它更快。如果你重新启动你的电脑,你会发现它又变慢了,我想。
猜你喜欢
  • 1970-01-01
  • 2021-08-29
  • 1970-01-01
  • 1970-01-01
  • 2021-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多