当需要每一百行时,使用索引比顺序扫描更好,但仅限于明确的值列表

问题描述 投票:0回答:1

我有一个表(在 RDS Postgres v.15.4 实例下

db.m7g.large
):

CREATE TABLE MyTable (
    content_id integer, 
    part integer, 
    vector "char"[]
);

content_id
上有一个B树索引。我的数据由 100M 行组成。有 1M (0 .. 10^6-1) 个不同的
content_id
值。对于每个
content_id
值,都有 100 (0..99) 个
part
值。如果
vector
可以被 100 整除而没有余数,则
content_id
列包含 384 个字节大小的数字的数组。否则就是
NULL

我构建了这个人工数据来测试从 Python 脚本提交的以下查询的性能(稍后就会清楚为什么我将其留在 Python 中来解决这个问题):

query = f"""
WITH 
    Vars(key) as (
        VALUES (array_fill(1, ARRAY[{384}])::vector)
    ),
    Projection as (
        SELECT *
        FROM MyTable P
        WHERE P.content_id in ({str(list(range(0, 999999, 100)))[1:-1]})
    )
SELECT P.content_id, P.part
FROM Projection P, Vars
ORDER BY P.vector::int[]::vector <#> key
LIMIT {10};
"""

<#>
pgvector
扩展的点积运算符,
vector
是该扩展定义的类型,据我理解,它类似于
real[]

请注意,

WHERE
子句指定了 10K 个
content_id
值的显式列表(对应于 1M 行,即表中的每一百行)。由于这个庞大的显式列表,我必须将查询留在 Python 中并且无法运行
EXPLAIN ANALYZE

执行上述查询大约需要 6 秒。

但是,当我在该查询前面添加

SET enable_seqscan = off;
时,查询只需要约 3 秒。

问题 1:鉴于我们需要每第 100 行,并且大量计算是关于计算点积并按它们排序,为什么顺序扫描比使用索引更好? (更重要的是,我无法理解使用索引如何能够将性能提高 2 倍。)

问题 2: 如果我更改

generate_series
的显式值列表,如下所示,为什么这种改进会消失?

WHERE content_id IN (SELECT generate_series(0, 999999, 100))

现在,对于后一个查询,我有

EXPLAIN ANALYZE
:

的输出
 Limit  (cost=1591694.63..1591695.46 rows=10 width=24) (actual time=6169.118..6169.125 rows=10 loops=1)
   ->  Result  (cost=1591694.63..2731827.31 rows=13819790 width=24) (actual time=6169.117..6169.122 rows=10 loops=1)
         ->  Sort  (cost=1591694.63..1626244.11 rows=13819790 width=424) (actual time=6169.114..6169.117 rows=10 loops=1)
               Sort Key: ((((p.vector)::integer[])::vector <#> '[1,1,...,1]'::vector))
               Sort Method: top-N heapsort  Memory: 34kB
               ->  Nested Loop  (cost=194.30..1293053.94 rows=13819790 width=424) (actual time=2.629..6025.693 rows=1000000 loops=1)
                     ->  HashAggregate  (cost=175.02..177.02 rows=200 width=4) (actual time=2.588..5.321 rows=10000 loops=1)
                           Group Key: generate_series(0, 999999, 100)
                           Batches: 1  Memory Usage: 929kB
                           ->  ProjectSet  (cost=0.00..50.02 rows=10000 width=4) (actual time=0.002..0.674 rows=10000 loops=1)
                                 ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.000..0.001 rows=1 loops=1)
                     ->  Bitmap Heap Scan on mytable p  (cost=19.28..4204.85 rows=1382 width=416) (actual time=0.007..0.020 rows=100 loops=10000)
                           Recheck Cond: (content_id = (generate_series(0, 999999, 100)))
                           Heap Blocks: exact=64444
                           ->  Bitmap Index Scan on idx_content_on_mytable  (cost=0.00..18.93 rows=1382 width=0) (actual time=0.005..0.005 rows=100 loops=10000)
                                 Index Cond: (content_id = (generate_series(0, 999999, 100)))
 Planning Time: 0.213 ms
 Execution Time: 6169.260 ms
(18 rows)
python postgresql query-optimization amazon-rds database-indexes
1个回答
0
投票

鉴于我们需要每第 100 行,并且大量计算是关于计算点积并按它们排序,为什么顺序扫描不比使用索引更好?

点积计算仅在检索记录后进行(无论用于检索记录的计划如何)。在我的 Google Cloud 实例上,计算 10k 对向量的点积并按它们排序需要 46 毫秒,因此这并不是增加最大开销的原因。

顺序扫描需要评估表中的每一条记录,并确保它符合您的

WHERE
条件。

索引扫描会首先遍历你的索引(比堆短得多)并且只检索需要检索的堆页面。这会产生在循环中进行堆查找的额外成本(如果两条记录最终位于同一页上,则可能每页两次或更多次),这可能会也可能不会因为所需的堆页读取较少而被抵消。

索引扫描的位图变体尝试更智能地处理检索堆页面的顺序(它首先从索引检索所有指针,将它们存储在位图中,然后按堆顺序检索所有页面,跳过那些它不需要的)。额外的成本是维护位图;优点是每个堆表只需要检索一次。

您的第二个查询也在执行冗余

HashAggregate
(以确保来自
GENERATE_SERIES
的值是唯一的),但这不是什么大问题。

© www.soinside.com 2019 - 2024. All rights reserved.