SQLite:在大表上计数慢

pxyaymoc  于 2023-08-06  发布在  SQLite
关注(0)|答案(8)|浏览(132)

我在SQLite中遇到了一个性能问题,在一个大表上使用SELECT COUNT(*)。
由于我还没有得到一个有用的答案,我做了一些进一步的测试,我编辑了我的问题,以纳入我的新发现。
我有两张table:

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万个与空值不同的值,并且只有大约60万个不同的值。
我对这两个表都做了一些测试,您可以在下面看到时间,我为每个请求(QP)添加了“解释查询计划”。我把数据库文件放在一个USB记忆棒上,这样我就可以在每次测试后将其删除,并在不干扰磁盘缓存的情况下获得可靠的结果。有些请求在USB上更快(我想是因为缺少seektime),但有些请求更慢(表扫描)。

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秒)。
表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)


为什么SQLite不使用在Table1的主键上自动创建的索引?为什么当他在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)


正如您所看到的,查询花费了相同的时间(除了查询计划现在显示的是真实的的行数),只是较慢的select现在也较快。
接下来,我在Table1的Key字段上创建一个额外的索引,它应该与自动索引相对应。我是在原始数据库上做的,没有ANALYZE数据。创建这个索引花了23分钟(请记住,这是在一个U盘上)。

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)


如您所见,索引有助于计数(*),但对计数(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来创建这些表,并使用SQLiteSpy1.9.1运行测试。
谢谢你,
马克

jljoyd4f

jljoyd4f1#

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

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

字符串
将避免全表扫描。
背景资料:
在SQLite中,表行通常有一个64位有符号整数ROWID,它在同一个表中的所有行中是唯一的。(WITHOUT ROWID表是例外。)
您可以使用特殊列名ROWID_ROWID_OID之一来访问SQLite表的ROWID。除非声明一个普通的表列使用这些特殊名称之一,否则使用该名称将引用声明的列,而不是内部ROWID。
如果表包含INTEGER PRIMARY KEY类型的列,则该列将成为ROWID的别名。然后,您可以使用四个不同名称中的任何一个来访问ROWID,这四个名称是上述的原始三个名称或赋予INTEGER PRIMARY KEY列的名称。[...]
如果没有在插入中指定ROWID,或者指定的ROWID的值为NULL,则会自动创建相应的ROWID。通常的算法是给予新创建的行一个ROWID,该ROWID比插入之前表中最大的ROWID大1。如果表最初为空,则使用ROWID 1。[...]
只要您从不使用最大ROWID值并且从不删除表中具有最大ROWID的条目,上述普通ROWID选择算法将生成单调递增的唯一ROWID。如果您曾经删除过行,或者曾经创建过具有最大可能ROWID的行,则在创建新行时可能会重用以前删除的行中的ROWID,并且新创建的ROWID可能不会严格按升序排列。

yx2lnoni

yx2lnoni2#

引用2005年9月sqlite-users邮件列表中D. Richard Hipp的话:
SQLite总是对count(*)进行全表扫描。它不会在表中保留Meta信息以加快此过程。
不保留Meta信息是一个深思熟虑的设计决策。如果每个表都存储了一个计数(或者更好的是,btree的每个节点都存储了一个计数),那么在每个INSERTDELETE上都必须进行更多的更新。这会降低INSERTDELETE的速度,即使在count(*)速度不重要的常见情况下也是如此。
如果你真的需要一个快速的COUNT,那么你可以在INSERTDELETE上创建一个触发器,它在一个单独的表中更新一个运行计数,然后查询这个单独的表来找到最新的计数。
当然,如果您需要依赖于WHERE子句的COUNT s(即,WHERE field1 > 0 and field2 < 1000000000)。

rta7y2nd

rta7y2nd3#

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

w8f9ii69

w8f9ii694#

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

ddrv8njm

ddrv8njm5#

快速查询的输出都以文本“QP:而用于慢速查询的那些以文本“QP:SCAN”,这表明sqlite正在执行整个表的扫描,以便生成计数。
在谷歌上搜索“sqlite table scan count”会发现the following,这表明使用全表扫描来检索计数正是sqlite的工作方式,因此可能是不可避免的。
作为一种变通方法,考虑到status只有8个值,我想知道您是否可以使用如下查询快速获得计数?
select 1 where status=1 union select 1 where status=2 ...
然后计算结果中的行数。这显然是丑陋的,但如果它说服sqlite将查询作为搜索而不是扫描运行,它可能会起作用。每次返回“1”的想法是为了避免返回真实的数据的开销。

nfg76nw0

nfg76nw06#

这里有一个潜在的解决方法来提高查询性能。从上下文来看,您的查询运行起来大约需要一分半钟。
假设你有一个date_created列(或者可以添加一个),每天午夜(比如00:05am)在后台运行一个查询,并将值与计算的last_updated日期沿着持久化(我稍后会回到这个问题)。
然后,对date_created列(带索引)运行,可以通过执行类似SELECT COUNT(*)FROM TABLE WHERE date_updated >“[TODAY] 00:00:05”的查询来避免全表扫描。
将该查询中的计数值添加到持久化值中,就可以得到一个相当快的计数,而且通常是准确的。
唯一的问题是,从12:05am到12:07am(总计数查询运行的持续时间),您可以检查全表扫描计数()的last_updated值。如果它是> 24小时前的,那么您的增量计数查询需要提取一整天的计数加上今天经过的时间。如果时间小于24小时,则增量计数查询需要提取部分日计数(仅为今天经过的时间)。

bjg7j2ky

bjg7j2ky7#

我有同样的问题,在我的情况下真空命令帮助。在数据库COUNT(*)上执行后,速度提高了近100倍。但是,命令本身需要在我的数据库(2000万条记录)中占用一些时间。我解决了这个问题,运行真空时,我的软件退出后,主窗口销毁,所以延迟不会使问题的用户。

svgewumm

svgewumm8#

不要数星星,数记录!或者换句话说永远不要发布

SELECT COUNT(*) FROM tablename;

字符串
使用了

SELECT COUNT(ROWID) FROM tablename;


调用EXPLAIN QUERY PLAN查看两者的区别。确保您有一个索引,其中包含WHERE子句中提到的所有列。

相关问题