【问题标题】:Update query too slow on Postgres 9.1Postgres 9.1 上的更新查询太慢
【发布时间】:2012-07-08 01:48:59
【问题描述】:

我的问题是我对一个有 1400 万行的表的更新查询非常慢。我尝试了不同的方法来调整我的服务器,这带来了良好的性能,但不适用于更新查询。

我有两张桌子:

  • T1 上有 4 列和 3 个索引(530 行)
  • T2 上有 15 列和 3 个索引(1400 万行)
  • 我想通过在文本字段 stxt 上连接两个表,将 T2 中的字段 vid(整数类型)更新为 T1 中相同的 vid 值。

这是我的查询及其输出:

explain analyse 
update T2 
  set vid=T1.vid 
from T1 
where stxt2 ~ stxt1 and T2.vid = 0;
T2 更新(成本=0.00..9037530.59 行=2814247 宽度=131)(实际时间=25141785.741..25141785.741 行=0 循环=1) -> 嵌套循环(成本=0.00..9037530.59 行=2814247 宽度=131)(实际时间=32.636..25035782.995 行=679354 循环=1) 加入过滤器:((T2.stxt2)::text ~ (T1.stxt1)::text) -> T2 上的 Seq Scan(成本=0.00..594772.96 行=1061980 宽度=121)(实际时间=0.067..5402.614 行=1037809 循环=1) 过滤器:(视频= 1) -> 实现(成本=0.00..17.95 行=530 宽度=34)(实际时间=0.000..0.069 行=530 循环=1037809) -> T1 上的 Seq Scan(成本=0.00..15.30 行=530 宽度=34)(实际时间=0.019..0.397 行=530 循环=1) 总运行时间:25141785.904 毫秒

如您所见,查询耗时约 25141 秒(约 7 小时)。 f 我理解得很好,planner 估计执行时间为 9037 秒(~ 2.5 小时)。我在这里遗漏了什么吗?

以下是有关我的服务器配置的信息:

  • CentOS 5.8,20GB 内存
  • shared_buffers = 12GB
  • work_mem = 64MB
  • maintenance_work_mem = 64MB
  • bgwriter_lru_maxpages = 500
  • checkpoint_segments = 64
  • checkpoint_completion_target = 0.9
  • 有效缓存大小 = 10GB

我已经在 T2 表上运行了 Vacuum full 并分析了几次,但这仍然没有改善这种情况。

PS:如果我将 full_page_writes 设置为关闭,这会大大改善更新查询,但我不想冒数据丢失的风险。请问您有什么建议吗?

【问题讨论】:

  • 尝试改用 MERGE。它可以更快地链接表格。
  • 你真的需要 ~ 操作符吗? stxt1、stxt2 字段中有什么,它们的类型是什么?
  • @wildplasser ~ 运算符几乎等同于stxt2 like '%'||stxt1||'%'。两个字段 stxt 都是不同的字符。 @radashk 我试过这个 link 但 Postgres 一直告诉我 ERROR: syntax error at or near "MERGE"。我应该如何尝试“合并”?
  • 1) 我知道 ~ 是什么意思,但您似乎在 table1 中存储了 500 个正则表达式。 2) postgresql 中没有合并(在这种情况下也无济于事)
  • @wildplasser 我不知道是否有另一种方法可以连接这两个表,这就是我使用 ~ 的原因。如果 stxt2 包含 stxt1,那么我希望 T2.vid 等于 T1.vid,否则什么都不会发生。感谢您的帮助。

标签: regex performance postgresql


【解决方案1】:

这不是解决方案,而是数据建模解决方法

  • 将 url 分解为 {protocol,hostname,pathname} 组件。
  • 现在您可以使用完全匹配来加入主机名部分,避免正则表达式匹配中的前导 %。
  • 该视图旨在证明可以根据需要重建 full_url。

更新可能需要几分钟时间。

SET search_path='tmp';

DROP TABLE urls CASCADE;
CREATE TABLE urls
        ( id SERIAL NOT NULL PRIMARY KEY
        , full_url varchar
        , proto varchar
        , hostname varchar
        , pathname varchar
        );

INSERT INTO urls(full_url) VALUES
 ( 'ftp://www.myhost.com/secret.tgz' )
,( 'http://www.myhost.com/robots.txt' )
,( 'http://www.myhost.com/index.php' )
,( 'https://www.myhost.com/index.php' )
,( 'http://www.myhost.com/subdir/index.php' )
,( 'https://www.myhost.com/subdir/index.php' )
,( 'http://www.hishost.com/index.php' )
,( 'https://www.hishost.com/index.php' )
,( 'http://www.herhost.com/index.php' )
,( 'https://www.herhost.com/index.php' )
        ;

UPDATE urls
SET proto = split_part(full_url, '://' , 1)
        , hostname = split_part(full_url, '://' , 2)
        ;

UPDATE urls
SET pathname = substr(hostname, 1+strpos(hostname, '/' ))
        , hostname = split_part(hostname, '/' , 1)
        ;

        -- the full_url field is now redundant: we can drop it
ALTER TABLE urls
        DROP column full_url
        ;
        -- and we could always reconstruct the full_url from its components.
CREATE VIEW vurls AS (
        SELECT id
        , proto || '://' || hostname || '/' || pathname AS full_url
        , proto
        , hostname
        , pathname
        FROM urls
        );

SELECT * FROM urls;
        ;
SELECT * FROM vurls;
        ;

输出:

INSERT 0 10
UPDATE 10
UPDATE 10
ALTER TABLE
CREATE VIEW
 id | proto |    hostname     |     pathname     
----+-------+-----------------+------------------
  1 | ftp   | www.myhost.com  | secret.tgz
  2 | http  | www.myhost.com  | robots.txt
  3 | http  | www.myhost.com  | index.php
  4 | https | www.myhost.com  | index.php
  5 | http  | www.myhost.com  | subdir/index.php
  6 | https | www.myhost.com  | subdir/index.php
  7 | http  | www.hishost.com | index.php
  8 | https | www.hishost.com | index.php
  9 | http  | www.herhost.com | index.php
 10 | https | www.herhost.com | index.php
(10 rows)

 id |                full_url                 | proto |    hostname     |     pathname     
----+-----------------------------------------+-------+-----------------+------------------
  1 | ftp://www.myhost.com/secret.tgz         | ftp   | www.myhost.com  | secret.tgz
  2 | http://www.myhost.com/robots.txt        | http  | www.myhost.com  | robots.txt
  3 | http://www.myhost.com/index.php         | http  | www.myhost.com  | index.php
  4 | https://www.myhost.com/index.php        | https | www.myhost.com  | index.php
  5 | http://www.myhost.com/subdir/index.php  | http  | www.myhost.com  | subdir/index.php
  6 | https://www.myhost.com/subdir/index.php | https | www.myhost.com  | subdir/index.php
  7 | http://www.hishost.com/index.php        | http  | www.hishost.com | index.php
  8 | https://www.hishost.com/index.php       | https | www.hishost.com | index.php
  9 | http://www.herhost.com/index.php        | http  | www.herhost.com | index.php
 10 | https://www.herhost.com/index.php       | https | www.herhost.com | index.php
(10 rows)

【讨论】:

  • 现在尝试运行两次更新以获取临时表中的 {protocol, hostname, pathname} 组件。暂时不要删除 full_url 字段。
  • 我有一个要发布的答案,但我必须等待 27 分钟 :)。我是新手,没有足够的声誉
  • 顺便说一句:您可以将上述临时表用作连接表;将 full_url 转换为(规范?)主机名。
  • 我认为这是正确的方向。不仅〜昂贵,而且我想知道如果 T1.stx1 有“apple”和“google”的行,而 T2.stx2 有“www.appleiscoolerthangoogle.com”,它是否能满足你的要求。
  • @Andrew Lazarus 你说得对~。但是为了比较,我认为它应该在大多数情况下工作,因为 stxt1 包含一个域而不是一个单词。例如 stxt1=somedomain.com 和 stxt2=sub1.somedomain.com。
【解决方案2】:

这是我之前对功能索引的评论的扩展示例。如果您使用 postgresql 并且不知道函数索引是什么,您可能会因此而受苦。

让我们创建一个测试表,将一些数据放入其中:

smarlowe=# create table test (a text, b text, c int);
smarlowe=# insert into test select 'abc','z',0 from generate_series(1,1000000); -- 1 million rows that don't match
smarlowe=# insert into test select 'abc','a',0 from generate_series(1,10); -- 10 rows that do match
smarlowe=# insert into test select 'abc','z',1 from generate_series(1,1000000); -- another million rows that won't match.

现在我们要对其运行一些查询来测试:

\timing
select * from test where a ~ b and c=0; -- ignore how long this takes
select * from test where a ~ b and c=0; -- run it twice to get a read with cached data.

在我的笔记本电脑上,这需要大约 750 毫秒。 c 上的这个经典索引:

smarlowe=# create index test_c on test(c);
smarlowe=# select * from test where a ~ b and c=0;

在我的笔记本电脑上大约需要 400 毫秒。

这个功能索引tho:

smarlowe=# drop index test_c ;
smarlowe=# create index test_regex on test (c) where (a~b);
smarlowe=# select * from test where a ~ b and c=0;

现在运行时间为 1.3 毫秒。

当然,天下没有免费的午餐,您将在更新/插入期间为此索引付费。

【讨论】:

  • 当然是有代价的,但是像您的示例中那样带有选择性 WHERE 子句的部分索引相比之下非常便宜,并且会导致索引很小。非常有用。
  • @Scott Marlowe 谢谢你的提示。那么功能索引是部分索引吗?我想我已经在官方文档中阅读过它们,但从未使用过它们。我不确定,但我认为它们仅在某些特定情况下有用。
【解决方案3】:

谢谢,这带来了一些帮助。所以这就是我所做的:

  • 我创建了您提到的表格网址
  • 我添加了一个整数类型的 vid 列
  • 我在 T2 的 full_url 列中插入了 1000000 行
  • 我启用了计时,并使用不包含“http”和“www”的 full_url 更新了主机名列 update urls set hostname=full_url where full_url not like '%/%' and full_url not like 'www\.%';

Time: 112435.192 ms

然后我运行这个查询:

    mydb=> explain analyse update urls set vid=vid from T1 where hostname=stxt1; 
             QUERY PLAN                                                          
            -----------------------------------------------------------------------------------------------------------------------------
             Update on urls  (cost=21.93..37758.76 rows=864449 width=124) (actual time=767.793..767.793 rows=0 loops=1)
                 ->  Hash Join  (cost=21.93..37758.76 rows=864449 width=124) (actual time=102.324..430.448 rows=94934 loops=1)
                             Hash Cond: ((urls.hostname)::text = (T1.stxt1)::text)
                             ->  Seq Scan on urls  (cost=0.00..25612.52 rows=927952 width=114) (actual time=0.009..265.962 rows=927952 loops=1)
                             ->  Hash  (cost=15.30..15.30 rows=530 width=34) (actual time=0.444..0.444 rows=530 loops=1)
                                         Buckets: 1024  Batches: 1  Memory Usage: 35kB
                                         ->  Seq Scan on T1  (cost=0.00..15.30 rows=530 width=34) (actual time=0.002..0.181 rows=530 loops=1)
             Total runtime: 767.860 ms                     

我对总运行时间感到非常惊讶!不到 1 秒,这证实了您所说的完全匹配的更新。现在我用这种方式搜索 xtxt1 和 stxt2 之间的完全匹配:

mydb=> select count(*) from T2 where vid is null and exists(select null from T1 where stxt1=stxt2);
 count  
--------
 308486
(1 row)

因此我在 T2 表上尝试了更新,结果如下:

mydb=> explain analyse update T2 set vid = T1.vid from T1 where T2.vid is null and stxt2=stxt1;
                                                                                                                            QUERY PLAN                                                               
---------------------------------------------------------------------------------------------------------------------------------------
 Update on T2  (cost=21.93..492023.13 rows=2106020 width=131) (actual time=252395.118..252395.118 rows=0 loops=1)
     ->  Hash Join  (cost=21.93..492023.13 rows=2106020 width=131) (actual time=1207.897..4739.515 rows=308486 loops=1)
                 Hash Cond: ((T2.stxt2)::text = (T1.stxt1)::text)
                 ->  Seq Scan on T2  (cost=0.00..455452.09 rows=4130377 width=121) (actual time=158.773..3915.379 rows=4103865 loops=1)
                             Filter: (vid IS NULL)
                 ->  Hash  (cost=15.30..15.30 rows=530 width=34) (actual time=0.293..0.293 rows=530 loops=1)
                             Buckets: 1024  Batches: 1  Memory Usage: 35kB
                             ->  Seq Scan on T1  (cost=0.00..15.30 rows=530 width=34) (actual time=0.005..0.121 rows=530 loops=1)
 Total runtime: 252395.204 ms
(9 rows)

Time: 255389.704 ms              

实际上 255 秒似乎是这样一个查询的好时机。我将尝试从所有 url 中提取主机名部分并进行更新。我仍然应该确保快速更新完全匹配的内容,因为我对它的体验很差。

感谢您的支持。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-02
    • 1970-01-01
    • 1970-01-01
    • 2016-07-25
    相关资源
    最近更新 更多