1

我的问题是我对一个有 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 内存
  • 共享缓冲区 = 12GB
  • 工作内存 = 64MB
  • 维护工作内存 = 64MB
  • bgwriter_lru_maxpages = 500
  • checkpoint_segments = 64
  • checkpoint_completion_target = 0.9
  • 有效缓存大小 = 10GB

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

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

4

3 回答 3

3

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

  • 将 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)
于 2012-07-08T14:50:52.937 回答
1

这是我之前对功能索引的评论的扩展示例。如果您使用 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 毫秒。

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

于 2012-07-11T21:01:42.973 回答
0

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

  • 如您所述,我创建了表格网址
  • 我添加了一个整数类型的 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 中提取主机名部分并进行更新。我仍然应该确保快速更新完全匹配的内容,因为我对它的体验很差。

谢谢您的支持。

于 2012-07-08T17:02:20.787 回答