【问题标题】:postgres large table select optimizationpostgres大表选择优化
【发布时间】:2016-08-08 20:57:50
【问题描述】:

我必须将数据库提取到外部数据库服务器以获取许可软件。 DB 必须是 Postgres,我无法从应用程序更改选择查询(无法更改源代码)。

表(必须是 1 个表)包含大约 6,5M 行,并且在主列(前缀)中具有唯一值。

所有请求都是读取请求,没有插入/更新/删除,每天有约 20 万次选择,峰值为 15 TPS。

选择查询是:

SELECT prefix, changeprefix, deletelast, outgroup, tariff FROM table 
WHERE '00436641997142' LIKE prefix 
AND company = 0  and ((current_time between timefrom and timeto) or (timefrom is null and timeto is null)) and (strpos("Day", cast(to_char(now(), 'ID') as varchar)) > 0  or "Day" is null )  
ORDER BY position('%' in prefix) ASC, char_length(prefix) DESC 
LIMIT 1;

解释分析显示如下

Limit  (cost=406433.75..406433.75 rows=1 width=113) (actual time=1721.360..1721.361 rows=1 loops=1)
  ->  Sort  (cost=406433.75..406436.72 rows=1188 width=113) (actual time=1721.358..1721.358 rows=1 loops=1)
        Sort Key: ("position"((prefix)::text, '%'::text)), (char_length(prefix)) DESC
        Sort Method: quicksort  Memory: 25kB
        ->  Seq Scan on table  (cost=0.00..406427.81 rows=1188 width=113) (actual time=1621.159..1721.345 rows=1 loops=1)
              Filter: ((company = 0) AND ('00381691997142'::text ~~ (prefix)::text) AND ((strpos(("Day")::text, (to_char(now(), 'ID'::text))::text) > 0) OR ("Day" IS NULL)) AND (((('now'::cstring)::time with time zone >= (timefrom)::time with time zone) AN (...)
              Rows Removed by Filter: 6417130
Planning time: 0.165 ms
Execution time: 1721.404 ms`

查询最慢的部分是:

 SELECT prefix, changeprefix, deletelast, outgroup, tariff FROM table 
 WHERE '00436641997142' LIKE prefix 

生成 1,6s(仅测试这部分查询)

单独测试的部分查询:

Seq Scan on table  (cost=0.00..181819.07 rows=32086 width=113) (actual time=1488.359..1580.607 rows=1 loops=1)
  Filter: ('004366491997142'::text ~~ (prefix)::text)
  Rows Removed by Filter: 6417130
Planning time: 0.061 ms
Execution time: 1580.637 ms

关于数据本身: “前缀”列的前几位数字(前 5 位)相同,其余部分是不同的唯一数字。

Postgres 版本是 9.5 我更改了 Postgres 的以下设置:

random-page-cost = 40
effective_cashe_size = 4GB
shared_buffer = 4GB
work_mem = 1GB

我尝试了几种索引类型(unique、gin、gist、hash),但在所有情况下都没有使用索引(如上所述)并且结果速度是相同的。 我也做过,但没有明显的改进:

vacuum analyze verbose table

请推荐数据库和/或索引配置的设置,以加快此查询的执行时间。

当前硬件是 i5、SSD、Win7 上的 16GB RAM,但我可以选择购买更强大的硬件。 据我了解,对于读取(无插入/更新)占主导地位的情况,更快的 CPU 内核比内核数量或磁盘速度更重要 > 请确认。

附加组件 1: 添加9个索引后,索引也没有使用。

附加组件 2: 1)我找到了不使用索引的原因:查询中的词序部分就像是原因。如果查询是:

SELECT prefix, changeprefix, deletelast, outgroup, tariff FROM table WHERE prefix like '00436641997142%'
AND company = 0  and 
((current_time between timefrom and timeto) or (timefrom is null and timeto is null)) and (strpos("Day", cast(to_char(now(), 'ID') as varchar)) > 0  or "Day" is null )
 ORDER BY position('%' in prefix) ASC, char_length(prefix) DESC LIMIT 1

它使用索引。

注意区别:

... WHERE '00436641997142%' like prefix ...

正确使用索引的查询:

... WHERE prefix like '00436641997142%' ...

由于我无法更改查询本身,知道如何克服这个问题吗?我可以更改数据和 Postgres 设置,但不能查询自身。

2) 另外,为了使用并行 seq.scan,我安装了 Postgres 9.6 版本。在这种情况下,仅当查询的最后一部分被省略时才使用并行扫描。所以,查询:

SELECT prefix, changeprefix, deletelast, outgroup, tariff FROM table WHERE '00436641997142' LIKE prefix 
AND company = 0  and 
((current_time between timefrom and timeto) or (timefrom is null and timeto is null))
 ORDER BY position('%' in prefix) ASC, char_length(prefix) DESC LIMIT 1

使用并行模式。

知道如何强制原始查询(我无法更改查询):

SELECT prefix, changeprefix, deletelast, outgroup, tariff FROM erm_table WHERE '00436641997142' LIKE prefix 
AND company = 0  and 
((current_time between timefrom and timeto) or (timefrom is null and timeto is null)) and (strpos("Day", cast(to_char(now(), 'ID') as varchar)) > 0  or "Day" is null )
 ORDER BY position('%' in prefix) ASC, char_length(prefix) DESC LIMIT 1

使用并行序列。扫描?

【问题讨论】:

  • 你有什么理由使用like而不是=
  • 我无法更改 > 它位于我无权访问的源代码中。使用“=”而不是“like”的查询速度提高了 3 倍,但我无法更改它
  • random-page-cost = 40 为什么这么高?在 SSD 上...
  • 默认是4,我增加看看有什么不同。基本上响应时间与 4 或 8 相同
  • 它返回多少行?尝试优化这部分查询。我不知道有什么方法可以优化const LIKE column 之类的条件(不更改查询/数据结构/应用程序逻辑)。

标签: sql postgresql sqlperformance postgresql-performance


【解决方案1】:

strin LIKE pattern 之类的查询创建索引太难了,因为通配符(% 和 _)无处不在。

我可以建议一个有风险的解决方案:

  1. 稍微重新设计表格 - 使其可索引。再添加两列固定宽度的prefix_lowprefix_high - 例如char(32),或任何足以完成任务的任意长度。还要添加一个smallint 列作为前缀长度。用匹配前缀和前缀长度的最低和最高值填充它们。例如:

    select rpad(rtrim('00436641997142%','%'), 32, '0') AS prefix_low, rpad(rtrim('00436641997142%','%'), 32, '9') AS prefix_high, length(rtrim('00436641997142%','%')) AS prefix_length;
    
           prefix_low                 |               prefix_high             |   prefix_length
    ----------------------------------+---------------------------------------+-----
     00436641997142000000000000000000 | 00436641997142999999999999999999      |   14
    
  2. 用这些值创建索引

    CREATE INDEX table_prefix_low_high_idx ON table (prefix_low, prefix_high);
    
  3. 对照表检查修改后的请求:

    SELECT prefix, changeprefix, deletelast, outgroup, tariff 
    FROM table 
    WHERE '00436641997142%' BETWEEN prefix_low AND prefix_high
      AND company = 0  
      AND ((current_time between timefrom and timeto) or (timefrom is null and timeto is null)) and (strpos("Day", cast(to_char(now(), 'ID') as varchar)) > 0  or "Day" is null )
    ORDER BY prefix_length DESC 
    LIMIT 1
    

    检查它与索引的工作情况,尝试调整它 - 添加/删除前缀长度的索引,将其添加到索引之间等等。

  4. 现在您需要重写对数据库的查询。安装 PgBouncer 和PgBouncer-RR patch。它允许您使用简单的 Python 代码即时重写查询,例如:

    import re
    
    def rewrite_query(username, query):
       q1=r"""^SELECT [^']*'(?P<id>\d+)%'[^'] ORDER BY (?P<position>\('%' in prefix\) ASC, char_length\(prefix\) LIMIT """
       if not re.match(q1, query):
          return query  # nothing to do with other queries
       else:
          new_query = # ... rewrite query here
       return new_query
    
  5. 运行 pgBouncer 并将其连接到数据库。尝试像您的应用程序那样发出不同的查询,并检查它们是如何被重写的。因为您要处理文本,所以您必须调整正则表达式以匹配所有必需的查询并正确重写它们。

  6. 当代理准备好并调试好后,将您的应用程序重新连接到 pgBouncer。

专业版:

  • 应用程序没有变化
  • DB 的基本结构没有变化

反对:

  • 额外维护 - 您需要触发器以使所有新列都包含实际数据
  • 额外的支持工具
  • rewrite 使用正则表达式,因此它与您的应用程序发出的特定查询密切相关。您需要运行一段时间并制定稳健的重写规则。

进一步发展: 在 pgsql 本身中劫持解析的查询树 https://wiki.postgresql.org/wiki/Query_Parsing

【讨论】:

  • 非常感谢您的想法和回答,这将对我有很大帮助 > 我将实施并测试它;但基本上想法是完美的;谢谢
  • 您可以为其创建索引,而不是为前缀长度创建额外的列。 CREATE INDEX table_prefix_length_idx ON table (char_length(prefix))
【解决方案2】:

如果我正确理解您的问题,创建重写查询的代理服务器可能是这里的解决方案。

这是example from another question

然后您可以在查询中将“LIKE”更改为“=”,它会运行得更快。

【讨论】:

    【解决方案3】:

    根据documentation,您应该通过添加适当的运算符类来更改索引:

    运算符类 text_pattern_ops、varchar_pattern_ops 和 bpchar_pattern_ops 支持 text、varchar、 和 char 分别。与默认运算符的区别 类是值严格比较的字符 字符而不是根据特定于语言环境的排序规则 规则。这使得这些运算符类适合查询使用 涉及模式匹配表达式(LIKE 或 POSIX 正则 表达式)当数据库不使用标准的“C”语言环境时。 例如,您可以像这样索引一个 varchar 列:

    CREATE INDEX test_index ON test_table (col varchar_pattern_ops);

    【讨论】:

    • 我刚刚添加了:CREATE INDEX test_index ON erm_table (prefix bpchar_pattern_ops);由于前缀字段是字符类型,但查询速度相同 > 1,7s
    • 你放弃了旧的吗?设置 SET enable_seqscan = OFF;用于测试
    • 我在初始帖子中发布了分析器;基本上,结果是一样的,seq.扫描被使用,虽然它被推迟了(奇怪?)
    • 好的,现在我已将整个查询变为红色。问题可能出在其他地方,像这样的表达式: (strpos("Day", cast(to_char(now(), 'ID') as varchar)) > 0 是优化器的黑匣子。也许只是为了验证添加一个where 子句中的部分索引: ((timefrom 和 timeto 之间的 current_time) 或 (timefrom 为 null 且 timeto 为 null)) 和 (strpos("Day", cast(to_char(now(), 'ID') as varchar) ) > 0 或 "Day" 为空)
    • 我完全删除了查询和性能结果相同的那部分(我在除了前缀部分之外的地方放入了没有和其他子句的分析器的初始发布结果)
    猜你喜欢
    • 1970-01-01
    • 2017-12-25
    • 2016-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多