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


10

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

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

rt4 =#\ d +附件
                                                    表“ public.attachments”
     专栏 类型 修饰符| 储存| 描述
----------------- + ----------------------------- +- -------------------------------------------------- ------ + ---------- + -------------
 id | 整数| 不是null默认nextval('attachments_id_seq':: regclass)| 普通
 transactionid | 整数| 不为空| 普通
 父| 整数| 不为null默认值0 | 普通
 messageid | 角色变化(160)| | 扩展|
 主题| 字符变化(255)| | 扩展|
 文件名| 字符变化(255)| | 扩展|
 内容类型| 角色变化(80)| | 扩展|
 contentencoding | 角色变化(80)| | 扩展|
 内容| 文字| | 扩展|
 标头| 文字| | 扩展|
 创作者| 整数| 不为null默认值0 | 普通
 创建| 没有时区的时间戳| | 普通
 contentindex | tsvector | | 扩展|
索引:
    “ attachments_pkey”主键,btree(id)
    “ attachments1” btree(父级)
    “ attachments2” btree(transactionid)
    “ attachments3” btree(父级,transactionid)
    杜松子酒(contentindex_idx)杜松子酒(contentindex)
有OID:否

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

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

但是,当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);

EXPLAIN ANALYZE 输出

                                                                             查询计划 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 总计(费用= 51210.60..51210.61行= 1宽度= 4)(实际时间= 477778.806..477778.806行= 1循环= 1)
   ->嵌套循环(成本= 0.00..51210.57行= 15宽度= 4)(实际时间= 17943.986..477775.174行= 4197循环= 1)
         ->嵌套循环(成本= 0.00..40643.08行= 6507宽度= 8)(实际时间= 8.526..20610.380行= 1714818循环= 1)
               ->在票证主上进行Seq扫描(成本= 0.00..9818.37行= 598宽度= 8)(实际时间= 0.008..256.042行= 96990循环= 1)
                     过滤器:((((status)::: text'deleted':: text)AND(id = validid)AND(((type):: text ='ticket':: text)))
               ->使用事务1上的transaction1进行索引扫描(成本= 0.00..51.36行= 15宽度= 8)(实际时间= 0.102..0.202行= 18循环= 96990)
                     索引条件:((((objecttype):: text ='RT :: Ticket':: text)AND(objectid = main.id))
         ->使用附件_2上的附件2进行索引扫描(成本= 0.00..1.61行= 1宽度= 4)(实际时间= 0.266..0.266行= 0循环= 1714818)
               索引条件:(transactionid = transaction_1.id)
               过滤器:(contentindex @@ plainto_tsquery('frobnicate':: text))
 总运行时间:477778.883毫秒

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

我不太确定下一步该怎么做。


升级将带来更多好处。除了许多常规改进之外,尤其是:9.2允许仅索引扫描和对可伸缩性的改进。即将发布的9.4将为GIN索引带来重大改进。
Erwin Brandstetter 2014年

Answers:


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');

您查询的大部分问题都位于前两个表tickets和中transactions,而这两个表是该问题所缺少的。我充满了有根据的猜测。

  • 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')
   );

而不是使用两个1:n联接来对行进行乘法运算,而只是count(DISTINCT id)使用结尾折叠多个匹配项,而使用EXISTS半联接,它可以在找到第一个匹配项后立即停止进一步查找,同时使最后DISTINCT一步过时。每个文档:

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

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

确定连接顺序 join_collapse_limit

如果你知道,对于你的搜索词attachments.contentindex非常有选择性的 -比查询的其他条件更有选择性的(这可能是,但不是“问题”的“frobnicate”的情况下),可以强制加入的顺序。除了最常见的单词外,查询计划人员几乎无法判断特定单词的选择性。每个文档:

join_collapse_limitinteger

[...]
因为查询计划者并非总是选择最佳的联接顺序,所以高级用户可以选择将该变量临时设置为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;

WHERE条件的顺序始终无关紧要。此处仅连接顺序相关。

或使用CTE,如“选项2”中的@jjanes所述。具有类似的效果。

指标

B树索引

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

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

如果条件之一是变量,则将其从WHERE条件中删除,并将该列作为索引列放在前面。

另一个transactions

CREATE INDEX transactions_partial_idx
ON transactions(objecttype, objectid, id)

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

另外,由于您具有此复合索引,其中包含两个整数列attachments

"attachments3" btree (parent, transactionid)

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

"attachments1" btree (parent)

细节:

GIN指数

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

"contentindex_idx" gin (transactionid, contentindex)

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

多列GIN索引可与涉及索引列的任何子集的查询条件一起使用 。与B树或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                    |

3

选项1

计划者无法了解EffectiveId和id之间关系的真实性质,因此可能认为以下子句:

main.EffectiveId = main.id

将会比实际更具选择性。如果这就是我的想法,EffectiveID几乎总是等于main.id,但是计划者并不知道。

存储这种类型的关系的一种可能更好的方法通常是将EffectiveID的NULL值定义为“与id有效地相同”,并仅在存在差异的情况下在其中存储某些内容。

假设您不想重新组织架构,可以尝试通过以下方式重写该子句来解决它:

main.EffectiveId+0 between main.id+0 and main.id+0

计划者可能认为与between选择性相比,选择的选择性较小,这足以使它摆脱当前的陷阱。

选项2

另一种方法是使用CTE:

WITH attach as (
    SELECT * from Attachments 
        where ContentIndex @@ plainto_tsquery('frobnicate') 
)
<rest of query goes here, with 'attach' used in place of 'Attachments'>

这迫使计划者将ContentIndex用作选择性来源。一旦被迫这样做,Tickets表上的误导性列关联将不再具有吸引力。当然,如果有人搜索“问题”而不是“轻信”,那可能适得其反。

选项3

为了进一步研究不良行估计,您应该在注释掉的不同AND子句的所有2 ^ 3 = 8个排列中运行以下查询。这将有助于找出错误估计的来源。

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.