我有一个表(在 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)
鉴于我们需要每第 100 行,并且大量计算是关于计算点积并按它们排序,为什么顺序扫描不比使用索引更好?
点积计算仅在检索记录后进行(无论用于检索记录的计划如何)。在我的 Google Cloud 实例上,计算 10k 对向量的点积并按它们排序需要 46 毫秒,因此这并不是增加最大开销的原因。
顺序扫描需要评估表中的每一条记录,并确保它符合您的
WHERE
条件。
索引扫描会首先遍历你的索引(比堆短得多)并且只检索需要检索的堆页面。这会产生在循环中进行堆查找的额外成本(如果两条记录最终位于同一页上,则可能每页两次或更多次),这可能会也可能不会因为所需的堆页读取较少而被抵消。
索引扫描的位图变体尝试更智能地处理检索堆页面的顺序(它首先从索引检索所有指针,将它们存储在位图中,然后按堆顺序检索所有页面,跳过那些它不需要的)。额外的成本是维护位图;优点是每个堆表只需要检索一次。
您的第二个查询也在执行冗余
HashAggregate
(以确保来自GENERATE_SERIES
的值是唯一的),但这不是什么大问题。