我从互联网资源中读到,当偏移量增加时,查询会很慢。但就我而言,我觉得它太慢了。我正在使用postgres 9.3
以下是查询(id
是主键):
select * from test_table offset 3900000 limit 100;
它会在10 seconds
左右返回数据。我觉得它太慢了。我在表格中有大约4 million
个记录。数据库的总大小为23GB
。
机器配置:
RAM: 12 GB
CPU: 2.30 GHz
Core: 10
我更改的postgresql.conf
文件中的值很少,如下所示。其他人是默认的。
shared_buffers = 2048MB
temp_buffers = 512MB
work_mem = 1024MB
maintenance_work_mem = 256MB
dynamic_shared_memory_type = posix
default_statistics_target = 10000
autovacuum = on
enable_seqscan = off ## its not making any effect as I can see from Analyze doing seq-scan
除此之外,我还尝试更改random_page_cost = 2.0
和cpu_index_tuple_cost = 0.0005
的值,结果相同。
Explain (analyze, buffers)
结果如下:
"Limit (cost=10000443876.02..10000443887.40 rows=100 width=1034) (actual time=12793.975..12794.292 rows=100 loops=1)"
" Buffers: shared hit=26820 read=378984"
" -> Seq Scan on test_table (cost=10000000000.00..10000467477.70 rows=4107370 width=1034) (actual time=0.008..9036.776 rows=3900100 loops=1)"
" Buffers: shared hit=26820 read=378984"
"Planning time: 0.136 ms"
"Execution time: 12794.461 ms"
世界各地的人们如何在postgres中与这个问题进行谈判?任何替代解决方案对我也有帮助。
UPDATE :: 添加order by id
(尝试使用其他索引列),这里是解释:
"Limit (cost=506165.06..506178.04 rows=100 width=1034) (actual time=15691.132..15691.494 rows=100 loops=1)"
" Buffers: shared hit=110813 read=415344"
" -> Index Scan using test_table_pkey on test_table (cost=0.43..533078.74 rows=4107370 width=1034) (actual time=38.264..11535.005 rows=3900100 loops=1)"
" Buffers: shared hit=110813 read=415344"
"Planning time: 0.219 ms"
"Execution time: 15691.660 ms"
答案 0 :(得分:37)
它很慢,因为它需要找到前offset
行并扫描下一行。当你处理大量偏移时,没有多少优化会改变。
这是因为您的查询确实指示数据库引擎使用offset 3900000
- 3.9M行访问大量行。加快这种速度的选择并不多。
超高速RAM,SSD等将有所帮助。但是你只会在这样做的过程中获得一个恒定的因素,这意味着它只是在你的道路上踢,直到你达到足够大的偏移量。
确保桌子适合记忆,有更多的余地同样有助于更大的常数因素 - except the first time。但是,对于足够大的表或索引,这可能是不可能的。
确保您进行仅索引扫描将在一定程度上起作用。 (请参阅velis'回答;它有很多优点。)这里的问题是,出于所有实际目的,您可以将索引视为存储磁盘位置和索引字段的表。 (它比这更优化,但它是一个合理的第一近似值。)如果有足够的行,你仍然会遇到更大偏移量的问题。
尝试存储和维护行的精确位置也是一种昂贵的方法。(例如benjist建议。)虽然技术上可行,但它受到类似于使用MPTT和树结构:您在读取时会获得显着的收益,但是当插入,更新或删除节点时,最终会有过多的写入时间,因此需要同时更新大块数据。
正如希望更清楚的那样,当你处理这么大的抵消时,没有任何真正的灵丹妙药。考虑其他方法通常会更好。
如果您根据ID(或日期字段或任何其他可索引字段集)进行分页,则潜在的技巧(例如blogspot使用)将使您的查询以任意方式开始指向索引。
换句话说,而不是:
example.com?page_number=[huge]
做类似的事情:
example.com?page_following=[huge]
这样,你就可以看到你在索引中的位置,并且查询变得非常快,因为它可以直接到达正确的起点,而不需要翻阅大量的行:
select * from foo where ID > [huge] order by ID limit 100
当然,你失去了跳到例如但请给出一些诚实的想法:您最后一次跳到网站上的大页码而不是直接浏览其月度档案或使用其搜索框时是什么时候?
如果您要进行分页但希望以任何方式保持页面偏移,另一种方法是禁止使用更大的页码。这并不愚蠢:谷歌正在利用搜索结果做些什么。在运行搜索查询时,Google会为您提供估算的结果数量(您可以使用explain
获得合理的数字),然后将允许您显示前几千个结果 - 仅此而已。除此之外,他们出于性能原因这样做 - 恰恰是你遇到的那个。
答案 1 :(得分:6)
我赞成了Denis的答案,但我会自己添加一个建议,也许它可以为您的特定用例带来一些性能上的好处:
假设您的实际表不是test_table
,而是一些巨大的复合查询,可能有多个连接。您可以先确定所需的起始ID:
select id from test_table order by id offset 3900000 limit 1
这应该比原始查询快得多,因为它只需要扫描索引与整个表。获得此ID然后打开快速索引搜索选项以进行完全提取:
select * from test_table where id >= (what I got from previous query) order by id limit 100
答案 2 :(得分:3)
您没有说您的数据是主要只读还是经常更新。如果您可以设置一次创建表格,并且只是偶尔更新它(比如每隔几分钟),那么您的问题将很容易解决:
答案 3 :(得分:1)
这样您就可以按半随机顺序获取行。您没有在查询中对结果进行排序,因此,您可以获得存储在文件中的数据。问题是,当您更新行时,它们的顺序可能会发生变化。
要解决此问题,您应该将order by
添加到查询中。这样查询将以相同的顺序返回行。更重要的是,它将能够使用索引来加速查询。
所以有两件事:添加索引,在查询中添加order by
。两者都在同一列。如果要使用id列,则不要添加索引,只需将查询更改为:
select * from test_table order by id offset 3900000 limit 100;
答案 4 :(得分:1)
您可以分两步进行优化
首先获得3900000条记录中的最大ID
XMLHttpRequest
然后使用此最大ID获取下100条记录。
select max(id) (select id from test_table order by id limit 3900000);
这将更快,因为两个查询都将按id进行索引扫描。
答案 5 :(得分:0)
我不知道你的数据的所有细节,但400万行可能有点沉重。如果有一种合理的方法来对表进行分片并将其基本分解为较小的表,那么这可能是有益的。
为了解释这一点,让我举个例子。假设我有一个数据库,我有一个名为survey_answer的表,它变得非常大而且非常慢。现在让我们说这些调查答案都来自一组不同的客户(我也有一个客户表跟踪这些客户)。然后,我可以做的是,我可以做到这样,我有一个名为survey_answer的表,其中没有任何数据,但是是一个父表,它有一堆子表,实际上包含跟随的数据命名格式survey_answer_< clientid>,表示我有子表survey_answer_1,survey_answer_2等,每个客户一个。然后,当我需要为该客户端选择数据时,我会使用该表。如果我需要在所有客户端中选择数据,我可以从父调查表中选择,但速度很慢。但是为了获得个人客户的数据,这是我主要做的事情,那么它会很快。
这是如何分解数据的一个例子,还有很多其他的例子。另一个例子是,如果我的survey_answer表没有被客户轻易分解,而是我知道我通常只能在一年的时间内访问数据,那么我可能会根据年份制作子表,例如survey_answer_2014,survey_answer_2013等。如果我知道我一次不会访问超过一年,我只需要访问我的两个子表来获取我需要的所有数据。
在你的情况下,我所得到的只是个人身份。我们也可以通过它来分解它(虽然可能不那么理想)。假设我们将其分解,以便每个表只有大约1000000行。因此,我们的子表将是test_table_0000001_1000000,test_table_1000001_2000000,test_table_2000001_3000000,test_table_3000001_4000000等。因此,您不需要传递3900000的偏移量,而是首先进行一些数学计算并确定所需的表是test_table_3000001_4000000,偏移量为900000代替。如下所示:
SELECT * FROM test_table_3000001_4000000 ORDER BY id OFFSET 900000 LIMIT 100;
现在,如果对表格进行分片是不可能的,那么您可以使用部分索引来执行类似的操作,但同样,我建议首先进行分片。详细了解部分索引here。
我希望有所帮助。 (另外,我同意Szymon Guz你想要一个ORDER BY。)
编辑:请注意,如果您需要在获得100的结果之前删除行或选择性地排除行,那么按ID进行分片将变得非常难以处理(正如Denis所指出的那样;以及通过id进行分片并不是很好的开始)。但是,如果你的'只是'对数据进行分页,并且你只是插入或编辑(不是常见的事情,但确实发生了;记录了日志),那么可以合理地完成id的分片(尽管我仍然会选择其他的东西)碎片)。
答案 6 :(得分:0)
首先,您必须使用order by子句定义limit和offset,否则您将得到不一致的结果。
要加快查询速度,您可以拥有计算索引,但仅限于以下条件:
以下是您可以做到的事情:
create or replace function id_pos (id) returns bigint
as 'select count(id) from test_table where id <= $1;'
language sql immutable;
create index table_by_pos on test_table using btree(id_pos(id));
以下是您如何称呼它(偏移3900000限制100):
select * from test_table where id_pos(id) >= 3900000 and sales_pos(day) < 3900100;
这样,查询不会计算3900000偏移量数据,但只会计算100个数据,使其更快。
请注意这种方法可以发生的2个条件,或者位置会发生变化。
答案 7 :(得分:0)
如果基于ID进行分页而不是偏移/限制呢?
以下查询将提供ID,该ID将所有记录分成大小为per_page
的块。它不取决于记录是否被删除
SELECT id AS from_id FROM (
SELECT id, (ROW_NUMBER() OVER(ORDER BY id DESC)) AS num FROM test_table
) AS rn
WHERE num % (per_page + 1) = 0;
使用这些from_ID,您可以将链接添加到页面。遍历:from_ids并添加索引,并将以下链接添加到页面:
<a href="/test_records?from_id=:from_id">:from_id_index</a>
当用户访问页面时,检索ID大于请求的记录的:from_id:
SELECT * FROM test_table WHERE ID >= :from_id ORDER BY id DESC LIMIT :per_page
对于具有from_id=0
的首页链接,将起作用
<a href="/test_records?from_id=0">1</a>