在postgres中有效地标记连续的行子集

时间:2014-12-02 23:48:25

标签: sql postgresql indexing random-sample postgresql-performance

我有一个包含数十亿行的Postgres表,对于机器学习应用程序,需要将其划分为训练集和测试集。

我希望测试行大部分与id列连续,所以想要随机选择 每个1,000个连续行的几个块,并将它们标记为测试行。我在id列上有一个索引,因此选择任意1,000个连续行很快:

UPDATE table SET test=true WHERE id BETWEEN 100000 AND 101000;

效率很高,并且正如您所期望的那样使用索引扫描。不幸的是,只要我随机生成初始id,即

WITH off AS (SELECT ROUND(random()*maxId))
  UPDATE table SET test=true
    WHERE id BETWEEN (SELECT * FROM off LIMIT 1)
                 AND (SELECT * FROM off LIMIT 1)+1000;

查询计划程序现在决定进行全表扫描(慢得多)。

当然如果我只需要这样做一次,我只会手动生成一个随机行,没问题。但是最后我想要一个自动分为测试和训练的功能,如下所示:

CREATE OR REPLACE FUNCTION test_train_divide(chunkSize integer, proportion real)
RETURNS BOOLEAN
AS $$
DECLARE
maxId INTEGER := (SELECT MAX(id) FROM table);
BEGIN
FOR i IN 1 .. round(maxId*proportion/chunkSize) LOOP
  RAISE NOTICE 'Update call %', i;
  WITH off AS (SELECT ROUND(random()*maxId))
  UPDATE table SET test=true
    WHERE id BETWEEN (SELECT * FROM off LIMIT 1)
                 AND (SELECT * FROM off LIMIT 1)+chunkSize;
END LOOP;
return true;
END;
$$ LANGUAGE plpgsql;

SELECT test_train_divide(1000,0.01);

这很有效,但是非常慢!有什么指针吗?

更新

这是架构

    tbl "public.tbl”
  Column   |  Type   | Modifiers 
-----------+---------+-----------
 subid     | integer | 
 id        | bigint  | 
 wordid    | integer | 
 capid     | integer | 
 test      | boolean | 
Indexes:
    “tbl_id_idx" btree (id)

这里有两个不同的查询计划,一个是好的(使用索引),另一个是坏的:

will=# EXPLAIN UPDATE tbl SET test=true WHERE id BETWEEN 1000000 AND 1001000;

                                            QUERY PLAN                                             
---------------------------------------------------------------------------------------------------
 Update on tbl  (cost=0.57..790.45 rows=1079 width=38)
   ->  Index Scan using tbl_id_idx on tbl  (cost=0.57..790.45 rows=1079 width=38)
         Index Cond: ((id >= 1000000) AND (id <= 1001000))
(3 rows)


will=# EXPLAIN WITH start AS (SELECT round(random()*max(id)) FROM tbl) UPDATE tbl c SET test=true WHERE c.id BETWEEN (SELECT * FROM start LIMIT 1) AND (SELECT * FROM start LIMIT 1)+1000;



                                                             QUERY PLAN                                                               
---------------------------------------------------------------------------------------------------------------------------------------
 Update on tbl c  (cost=0.65..14932243.97 rows=1459961 width=38)
   CTE start
     ->  Result  (cost=0.59..0.61 rows=1 width=0)
           InitPlan 1 (returns $0)
             ->  Limit  (cost=0.57..0.59 rows=1 width=8)
                   ->  Index Only Scan Backward using tbl_id_idx on tbl  (cost=0.57..5846291.90 rows=288468819 width=8)
                         Index Cond: (id IS NOT NULL)
   InitPlan 3 (returns $2)
     ->  Limit  (cost=0.00..0.02 rows=1 width=8)
           ->  CTE Scan on start  (cost=0.00..0.02 rows=1 width=8)
   InitPlan 4 (returns $3)
     ->  Limit  (cost=0.00..0.02 rows=1 width=8)
           ->  CTE Scan on start start_1  (cost=0.00..0.02 rows=1 width=8)
   ->  Seq Scan on tbl c  (cost=0.00..14932243.32 rows=1459961 width=38)
         Filter: (((id)::double precision >= $2) AND ((id)::double precision <= ($3 + 1000::double precision)))
(15 rows)

Time: 2.649 ms

1 个答案:

答案 0 :(得分:2)

max_id初始化为max(id) - 1000以留出1000行空间后,应该使用索引:

UPDATE table
SET    test = true
FROM  (SELECT (random() * max_id)::bigint AS lower_bound) t
WHERE  id BETWEEN t.lower_bound AND t.lower_bound + 999;
  • 不需要具有CTE和子查询的复杂结构。使用单个子查询。

  • 原始计算产生numeric(或dp),这可能不适合bigint列上的索引。转为bigint。 (第9.3页不应该是问题。)

  • BETWEEN包括下限和上限。严格来说,你的上限应该是lower + 999

  • random()返回per documentationrandom value in the range 0.0 <= x < 1.0。为了完全公平,您的lower_bound应该像这样计算(假设没有间隙):

    trunc(random() * max_id)::bigint + 1
    

如果您需要真正随机的数字(或者id有差距),请考虑以下相关答案:

也许咨询锁或其他方法可能有用。比较这个相关的,后面的答案: