由于行估计非常不准确,全文搜索速度缓慢

Jam*_*nah 10 postgresql performance full-text-search execution-plan postgresql-9.1 query-performance

针对此数据库的全文查询(存储 RT(请求跟踪器)票证)似乎需要很长时间才能执行。附件表(包含全文数据)大约为 15GB。

数据库模式如下,大约有 200 万行:

rt4=# \d+ 附件
                                                    表“public.attachments”
     专栏 | 类型 | 修饰符 | 存储 | 描述
-----------------+------------------------------------------+-- -------------------------------------------------- -------+----------+-------------
 身份证 | 整数 | not null default nextval('attachments_id_seq'::regclass) | 平原 |
 交易ID | 整数 | 不为空| 平原 |
 家长 | 整数 | 非空默认值 0 | 平原 |
 消息ID | 字符变化(160) | | 扩展 |
 主题 | 字符变化(255) | | 扩展 |
 文件名 | 字符变化(255) | | 扩展 |
 内容类型 | 字符变化(80) | | 扩展 |
 内容编码 | 字符变化(80) | | 扩展 |
 内容 | 文字 | | 扩展 |
 标题| 文字 | | 扩展 |
 创造者 | 整数 | 非空默认值 0 | 平原 |
 创建 | 没有时区的时间戳 | | 平原 |
 内容索引 | 向量 | | 扩展 |
索引:
    “attachments_pkey”主键,btree (id)
    “attachments1”btree(父)
    "attachments2" btree (transactionid)
    “attachments3”btree(父,transactionid)
    “contentindex_idx”杜松子酒(内容索引)
有 OID:否

我可以使用以下查询非常快速(<1s)查询自己的数据库:

select objectid
from attachments
join transactions on attachments.transactionid = transactions.id
where contentindex @@ to_tsquery('frobnicate');
Run Code Online (Sandbox Code Playgroud)

但是,当 RT 运行一个应该对同一个表执行全文索引搜索的查询时,通常需要数百秒才能完成。查询分析输出如下:

询问

SELECT COUNT(DISTINCT main.id)
FROM Tickets main
JOIN Transactions Transactions_1 ON ( Transactions_1.ObjectType = 'RT::Ticket' )
                                AND ( Transactions_1.ObjectId = main.id )
JOIN Attachments Attachments_2 ON ( Attachments_2.TransactionId = Transactions_1.id )
WHERE (main.Status != 'deleted')
AND ( ( ( Attachments_2.ContentIndex @@ plainto_tsquery('frobnicate') ) ) )
AND (main.Type = 'ticket')
AND (main.EffectiveId = main.id);
Run Code Online (Sandbox Code Playgroud)

EXPLAIN ANALYZE 输出

                                                                             查询计划 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 合计(cost=51210.60..51210.61 rows=1 width=4)(实际时间=477778.806..477778.806 rows=1 loops=1)
   -> Nested Loop (cost=0.00..51210.57 rows=15 width=4) (实际时间=17943.986..477775.174 rows=4197 loops=1)
         -> Nested Loop (cost=0.00..40643.08 rows=6507 width=8) (实际时间=8.526..20610.380 rows=1714818 loops=1)
               -> Seq Scan on ticket main (cost=0.00..9818.37 rows=598 width=8) (实际时间=0.008..256.042 rows=96990 loops=1)
                     过滤器:(((status)::text 'deleted'::text) AND (id = Effectiveid) AND ((type)::text = 'ticket'::text))
               -> 使用transactions1 对事务transactions_1 进行索引扫描(成本=0.00..51.36 行=15 宽度=8)(实际时间=0.102..0.202 行=18 次循环=96990)
                     索引条件:(((objecttype)::text = 'RT::Ticket'::text) AND (objectid = main.id))
         -> 在附件attachments_2上使用attachments2进行索引扫描(成本=0.00..1.61行=1宽度=4)(实际时间=0.266..0.266行=0循环=1714818)
               索引条件:(transactionid = transactions_1.id)
               过滤器:(contentindex @@plainto_tsquery('frobnicate'::text))
 总运行时间:477778.883 毫秒

据我所知,问题似乎是它没有使用在contentindex字段 ( contentindex_idx)上创建的索引,而是对附件表中的大量匹配行进行过滤。解释输出中的行数似乎也非常不准确,即使在最近的ANALYZE:估计行数=6507 实际行数=1714818 之后。

我不太确定接下来要做什么。

Erw*_*ter 5

这可以通过一千零一种方式改进,那么它应该是几毫秒的事情。

更好的查询

这只是您使用别名重新格式化的查询,并删除了一些噪音以清除迷雾:

SELECT count(DISTINCT t.id)
FROM   tickets      t
JOIN   transactions tr ON tr.objectid = t.id
JOIN   attachments  a  ON a.transactionid = tr.id
WHERE  t.status <> 'deleted'
AND    t.type = 'ticket'
AND    t.effectiveid = t.id
AND    tr.objecttype = 'RT::Ticket'
AND    a.contentindex @@ plainto_tsquery('frobnicate');
Run Code Online (Sandbox Code Playgroud)

您的查询的大部分问题在于前两个表ticketstransactions,它们在问题中缺失。我正在填写有根据的猜测。

  • t.statust.objecttype并且tr.objecttype可能不应该是text,但enum也可能是引用查找表的一些非常小的值。

EXISTS 半连接

假设tickets.id是主键,这种重写的形式应该便宜得多:

SELECT count(*)
FROM   tickets t
WHERE  status <> 'deleted'
AND    type = 'ticket'
AND    effectiveid = id
AND    EXISTS (
   SELECT 1
   FROM   transactions tr
   JOIN   attachments  a ON a.transactionid = tr.id
   WHERE  tr.objectid = t.id
   AND    tr.objecttype = 'RT::Ticket'
   AND    a.contentindex @@ plainto_tsquery('frobnicate')
   );
Run Code Online (Sandbox Code Playgroud)

不是将行与两个 1:n 连接相乘,只是在最后折叠多个匹配项count(DISTINCT id),而使用EXISTS半连接,一旦找到第一个匹配项,它就可以停止进一步查找,同时废弃最后DISTINCT一步。根据文档:

子查询通常只会执行足够长的时间来确定是否至少返回一行,而不是一直执行到完成。

有效性取决于每张票和每笔交易的附件有多少交易。

确定连接顺序 join_collapse_limit

如果您知道您的搜索词 forattachments.contentindex非常有选择性的- 比查询中的其他条件更具选择性('frobnicate' 可能是这种情况,但不是 'problem' 的情况),您可以强制连接序列。查询规划器很难判断特定词的选择性,除了最常见的词。根据文档:

join_collapse_limit( integer)

[...]
因为查询规划器并不总是选择最佳连接顺序,高级用户可以选择暂时将此变量设置为 1,然后明确指定他们想要的连接顺序。

使用SET LOCAL为宗旨,以仅设置为当前事务。

BEGIN;
SET LOCAL join_collapse_limit = 1;

SELECT count(DISTINCT t.id)
FROM   attachments  a                              -- 1st
JOIN   transactions tr ON tr.id = a.transactionid  -- 2nd
JOIN   tickets      t  ON t.id = tr.objectid       -- 3rd
WHERE  t.status <> 'deleted'
AND    t.type = 'ticket'
AND    t.effectiveid = t.id
AND    tr.objecttype = 'RT::Ticket'
AND    a.contentindex @@ plainto_tsquery('frobnicate');

ROLLBACK; -- or COMMIT;
Run Code Online (Sandbox Code Playgroud)

WHERE条件的顺序总是无关紧要的。这里只有连接的顺序是相关的。

或者使用像@jjanes 在“选项 2”中解释的 CTE 为了类似的效果。

索引

B树索引

采取的所有条件tickets与其中的大多数查询使用方法相同,并创建一个部分索引tickets

CREATE INDEX tickets_partial_idx
ON tickets(id)
WHERE  status <> 'deleted'
AND    type = 'ticket'
AND    effectiveid = id;
Run Code Online (Sandbox Code Playgroud)

如果其中一个条件是可变的,请将其从WHERE条件中删除,并将该列作为索引列添加到前面。

另一个关于transactions

CREATE INDEX transactions_partial_idx
ON transactions(objecttype, objectid, id)
Run Code Online (Sandbox Code Playgroud)

第三列只是启用仅索引扫描。

此外,由于您有这个带有两个整数列的复合索引attachments

"attachments3" btree (parent, transactionid)
Run Code Online (Sandbox Code Playgroud)

这个额外的索引完全是浪费,删除它:

"attachments1" btree (parent)
Run Code Online (Sandbox Code Playgroud)

细节:

GIN指数

添加transactionid到您的 GIN 索引以使其更有效。这可能是另一个灵丹妙药,因为它可能允许仅索引扫描,完全消除对表的访问。
您需要由附加模块提供的附加运算符类btree_gin。详细说明:

"contentindex_idx" gin (transactionid, contentindex)
Run Code Online (Sandbox Code Playgroud)

integer列中的4 个字节不会使索引变大。另外,幸运的是,GIN 索引在一个关键方面与 B 树索引不同。根据文档:

多列 GIN 索引可以与涉及索引列的任何子集的查询条件一起使用 。与 B-tree 或 GiST 不同,无论查询条件使用哪个索引列,索引搜索的有效性都是相同的

大胆强调我的。所以你只需要一个(大而且有点贵)GIN 索引。

表定义

移动integer not null columns到前面。这对存储和性能有一些小的积极影响。在这种情况下,每行可节省 4 - 8 个字节。

                      Table "public.attachments"
         Column      |            Type             |         Modifiers
    -----------------+-----------------------------+------------------------------
     id              | integer                     | not null default nextval('...
     transactionid   | integer                     | not null
     parent          | integer                     | not null default 0
     creator         | integer                     | not null default 0  -- !
     created         | timestamp                   |                     -- !
     messageid       | character varying(160)      |
     subject         | character varying(255)      |
     filename        | character varying(255)      |
     contenttype     | character varying(80)       |
     contentencoding | character varying(80)       |
     content         | text                        |
     headers         | text                        |
     contentindex    | tsvector                    |
Run Code Online (Sandbox Code Playgroud)