PostgreSQL 中高效的全文搜索,在另一列上排序

jak*_*e n 4 postgresql full-text-search query-optimization postgresql-13 postgresql-14

在 PostgreSQL 中,如何有效地对一列进行全文搜索,对另一列进行排序?

假设我有一个tbl包含列abc、 ... 和许多(> 一百万)行的表。我想对列进行全文搜索,a并按其他列对结果进行排序。

所以我va从 column创建一个 tsvector a

ALTER TABLE tbl
ADD COLUMN va tsvector GENERATED ALWAYS AS (to_tsvector('english', a)) STORED;
Run Code Online (Sandbox Code Playgroud)

iva为其创建一个索引,

CREATE INDEX iva ON tbl USING GIN (va);
Run Code Online (Sandbox Code Playgroud)

ib和列的索引b

CREATE INDEX ib ON tbl (b);
Run Code Online (Sandbox Code Playgroud)

然后我查询像

SELECT * FROM tbl WHERE va @@ to_tsquery('english', 'test') ORDER BY b LIMIT 100
Run Code Online (Sandbox Code Playgroud)

现在 Postgres 明显的执行策略是:

  1. 对于频繁出现的单词,使用 进行索引扫描ib,过滤va @@ 'test'::tsquery,并在 100 个匹配后停止,

  2. ivawhile 对于罕见单词,使用with 条件 进行(位图)索引扫描,然后手动va @@ 'test'::tsquery排序b

然而,Postgres 的查询规划器似乎没有考虑词频:

  • 对于低LIMIT(例如100),它总是使用策略 1(正如我用 检查的那样EXPLAIN),并且在我的情况下,对于罕见(或未出现)的单词需要花费一分钟多的时间。然而,如果我通过设置一个大的(或没有)来欺骗它使用策略 2 LIMIT,它会在一毫秒内返回!

  • 相反,对于较大的LIMIT(例如200),它总是使用策略 2,该策略对于稀有单词效果很好,但对于频繁单词则非常慢

那么如何让 Postgres 在每种情况下都使用良好的查询计划呢?

由于目前似乎没有办法让Postgres自动选择正确的计划,

  • 如何获取包含特定词位的行数,以便我可以决定最佳策略?

    SELECT COUNT(*) FROM tbl WHERE va @@ to_tsquery('english', 'test')速度非常慢(10000 行中出现的词素大约需要 1 秒),ts_stat除了构建我自己的词频列表之外,似乎也没有帮助)

  • 那么我如何告诉 Postgres 使用这个策略呢?


这是一个具体的例子

我有一个items包含 150 万行的表,其中有一个v3用于进行文本搜索的 tsvector 列,以及一个rating用于排序的列。在这种情况下,我确定如果 LIMIT 为 135 或更少,则查询规划器始终选择策略 1,否则选择策略 2

这里是罕见词“aberdeen”(出现在 132 行中)的 EXPLAIN ANALYZE,LIMIT 135:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'aberdeen')
  ORDER BY rating DESC NULLS LAST LIMIT 135

Limit  (cost=0.43..26412.78 rows=135 width=28) (actual time=5915.455..499917.390 rows=132 loops=1)
  Buffers: shared hit=4444267 read=2219412
  I/O Timings: read=485517.381
  ->  Index Scan using ir on items  (cost=0.43..1429202.13 rows=7305 width=28) (actual time=5915.453..499917.242 rows=132 loops=1)
        Filter: (v3 @@ '''aberdeen'''::tsquery)"
        Rows Removed by Filter: 1460845
        Buffers: shared hit=4444267 read=2219412
        I/O Timings: read=485517.381
Planning:
  Buffers: shared hit=253
Planning Time: 1.270 ms
Execution Time: 499919.196 ms
Run Code Online (Sandbox Code Playgroud)

LIMIT 136:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'aberdeen')
  ORDER BY rating DESC NULLS LAST LIMIT 136

Limit  (cost=26245.53..26245.87 rows=136 width=28) (actual time=29.870..29.889 rows=132 loops=1)
  Buffers: shared hit=57 read=83
  I/O Timings: read=29.085
  ->  Sort  (cost=26245.53..26263.79 rows=7305 width=28) (actual time=29.868..29.876 rows=132 loops=1)
        Sort Key: rating DESC NULLS LAST
        Sort Method: quicksort  Memory: 34kB
        Buffers: shared hit=57 read=83
        I/O Timings: read=29.085
        ->  Bitmap Heap Scan on items  (cost=88.61..25950.14 rows=7305 width=28) (actual time=1.361..29.792 rows=132 loops=1)
              Recheck Cond: (v3 @@ '''aberdeen'''::tsquery)"
              Heap Blocks: exact=132
              Buffers: shared hit=54 read=83
              I/O Timings: read=29.085
              ->  Bitmap Index Scan on iv3  (cost=0.00..86.79 rows=7305 width=0) (actual time=1.345..1.345 rows=132 loops=1)
                    Index Cond: (v3 @@ '''aberdeen'''::tsquery)"
                    Buffers: shared hit=3 read=2
                    I/O Timings: read=1.299
Planning:
  Buffers: shared hit=253
Planning Time: 1.296 ms
Execution Time: 29.932 ms
Run Code Online (Sandbox Code Playgroud)

这里是频繁出现的单词“游戏”(出现在 240464 行中),LIMIT 135:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'game')
  ORDER BY rating DESC NULLS LAST LIMIT 135

Limit  (cost=0.43..26412.78 rows=135 width=28) (actual time=3.240..542.252 rows=135 loops=1)
  Buffers: shared hit=2876 read=1930
  I/O Timings: read=529.523
  ->  Index Scan using ir on items  (cost=0.43..1429202.13 rows=7305 width=28) (actual time=3.239..542.216 rows=135 loops=1)
        Filter: (v3 @@ '''game'''::tsquery)
        Rows Removed by Filter: 867
        Buffers: shared hit=2876 read=1930
        I/O Timings: read=529.523
Planning:
  Buffers: shared hit=208 read=45
  I/O Timings: read=15.626
Planning Time: 25.174 ms
Execution Time: 542.306 ms
Run Code Online (Sandbox Code Playgroud)

LIMIT 136:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'game')
  ORDER BY rating DESC NULLS LAST LIMIT 136
  
Limit  (cost=26245.53..26245.87 rows=136 width=28) (actual time=69419.656..69419.675 rows=136 loops=1)
  Buffers: shared hit=1757820 read=457619
  I/O Timings: read=65246.893
  ->  Sort  (cost=26245.53..26263.79 rows=7305 width=28) (actual time=69419.654..69419.662 rows=136 loops=1)
        Sort Key: rating DESC NULLS LAST
        Sort Method: top-N heapsort  Memory: 41kB
        Buffers: shared hit=1757820 read=457619
        I/O Timings: read=65246.893
        ->  Bitmap Heap Scan on items  (cost=88.61..25950.14 rows=7305 width=28) (actual time=110.959..69326.343 rows=240464 loops=1)
              Recheck Cond: (v3 @@ '''game'''::tsquery)
              Rows Removed by Index Recheck: 394527
              Heap Blocks: exact=49894 lossy=132284
              Buffers: shared hit=1757817 read=457619
              I/O Timings: read=65246.893
              ->  Bitmap Index Scan on iv3  (cost=0.00..86.79 rows=7305 width=0) (actual time=100.537..100.538 rows=240464 loops=1)
                    Index Cond: (v3 @@ '''game'''::tsquery)
                    Buffers: shared hit=1 read=60
                    I/O Timings: read=26.870
Planning:
  Buffers: shared hit=253
Planning Time: 1.195 ms
Execution Time: 69420.399 ms
Run Code Online (Sandbox Code Playgroud)

Lau*_*lbe 6

这并不容易解决:全文搜索需要 GIN 索引,但 GIN 索引无法支持ORDER BY. ORDER BY另外,如果您有用于全文搜索的B 树索引和 GIN 索引,则可以使用位图索引扫描将它们组合起来,但位图索引扫描ORDER BY两者都不支持。

\n

如果您创建自己的 \xe2\x80\x9cstop word\xe2\x80\x9d 列表,其中包含数据中的所有常用单词(除了正常的英语停用词),我认为有一定的可能性。然后,您可以定义使用该停用词文件的文本搜索词典以及english_rare使用该词典的文本搜索配置。

\n

然后,您可以使用该配置创建全文索引,并分两步进行查询,如下所示:

\n
    \n
  1. 寻找罕见的单词:

    \n
    SELECT *\nFROM (SELECT *\n      FROM tbl\n      WHERE va @@ to_tsquery(\'english_rare\', \'test\')\n      OFFSET 0) AS q\nORDER BY b LIMIT 100;\n
    Run Code Online (Sandbox Code Playgroud)\n

    子查询 withOFFSET 0将阻止优化器扫描 上的索引b

    \n

    对于罕见的单词,这将很快返回正确的结果。对于频繁出现的单词,这不会返回任何内容,因为to_tsquery将返回空结果。要区分由于该单词未出现而导致的未命中和由于该单词频繁出现而导致的未命中,请注意以下注意事项:

    \n
    SELECT *\nFROM (SELECT *\n      FROM tbl\n      WHERE va @@ to_tsquery(\'english_rare\', \'test\')\n      OFFSET 0) AS q\nORDER BY b LIMIT 100;\n
    Run Code Online (Sandbox Code Playgroud)\n
  2. \n
  3. 查找频繁出现的单词(如果第一个查询给了您通知):

    \n
    SELECT *\nFROM (SELECT *\n      FROM tbl\n      ORDER BY b) AS q\nWHERE va @@ to_tsquery(\'english\', \'test\')\nLIMIT 100;\n
    Run Code Online (Sandbox Code Playgroud)\n

    请注意,我们这里使用正常的英文配置。这将始终扫描索引,b并且对于频繁搜索术语来说速度相当快。

    \n
  4. \n
\n