删除重复的记录,中间没有变化

Mos*_*aic 5 postgresql performance delete greatest-n-per-group postgresql-performance

我有一个产品表,我每天在其中插入大约 150,000 条记录。它们中的大多数都是多余的,但由于新的到期日期,我需要保留它们。我每天从 30 个供应商中的大约 5 个那里获得产品提要。每个供应商都有大约 35,000 种独特的产品。任何产品都不能属于多个供应商。

创建表 vendor_prices (
  id 序列主键,
  供应商整数非空,
  sku 字符变化 (25) NOT NULL,
  category_name 字符变化(100)非空,
  价格数字(8,5)非空,
  没有时区的有效日期时间戳,
  不带时区 DEFAULT 的 expire_date 时间戳(now() + '1 year'::interval)
);

我正在尝试删除没有价格变化且不再是该产品的最后更新的不相关记录,例如:

  生效日期价格
  '2015-05-01' $1.99 
  '2015-05-02' $1.99删除
  '2015-05-03' $1.59 
  '2015-05-04' $1.99 
  '2015-05-05' $1.99删除
  '2015-05-06' $1.99保留新的到期日期

所以每次加载后(我认为一次对一个供应商来说会更容易)我想做某种删除。这是我想出的长期无效的解决方案。

CREATE OR REPLACE FUNCTION remove_vendor_price_dupes(_vendor integer)
  RETURNS integer AS
$BODY$
BEGIN
    -- Delete Redundant prices
    delete from vendor_prices
    where id in (
      select id from (
        select vp1.id, vp1.vendor, vp1.sku, vp1.price, vp1.effective_date, vp1.expiration_date
          from vendor_prices vp1 
          inner join (
              select vendor, sku, price from vendor_prices
                where vendor = _vendor
                group by vendor, sku, price 
          ) vp2
          on vp1.vendor = vp2.vendor and vp1.sku = vp2.sku and vp1.price = vp2.price
          where vp1.vendor = _vendor
      ) dupe

      -- fetch the irrelevant record
      WHERE (select a.effective_date from vendor_prices a
        where vendor = _vendor   
        and a.price = dupe.price and a.sku = dupe.sku and dupe.effective_date > a.effective_date

        -- but make sure there's no price change in-between(
        and (select b.effective_date from vendor_prices b 
          where vendor = _vendor     
          and b.sku = dupe.sku and b.effective_date < dupe.effective_date and b.effective_date > a.effective_date limit 1) IS NULL
          limit 1
      ) IS NOT NULL

      -- and that this is not the last update on said product, otherwise we'll keep it for expiration_date
      and ( select c.effective_date from vendor_prices c 
              where vendor = _vendor
              and c.sku = dupe.sku      
              and c.effective_date > dupe.effective_date limit 1
          ) IS NOT NULL
    );    
 return 0;
END;
$BODY$
LANGUAGE plpgsql
Run Code Online (Sandbox Code Playgroud)

这个函数运行了几个小时所以我杀了它。该表有大约 500 万条记录。我尝试了各种不同的索引和组合索引,但似乎没有任何帮助。在我运行此功能时,可能还有其他插入和删除操作。

在 Solaris 11.2 上运行 PostgreSQL 9.3.4。
我有足够的内存和磁盘空间。

Erw*_*ter 3

核心功能是窗函数lag()
还要特别注意避免并发删除和插入的死锁和竞争条件(这可能会影响要删除的行!):

CREATE OR REPLACE FUNCTION remove_vendor_price_dupes(_vendor int)
  RETURNS integer AS
$func$
DECLARE
   del_ct int;
BEGIN
   -- this may or may not be necessary:
   -- lock rows to avoid race conditions with concurrent deletes
   PERFORM 1
   FROM   vendor_prices
   WHERE  vendor = _vendor
   ORDER  BY sku, effective_date, id  -- guarantee row locks in consistent order
   FOR    UPDATE;

   -- delete redundant prices
   DELETE FROM vendor_prices v
   USING (
      SELECT id
           , price = lag(price) OVER w  -- same as last row
             AND (lead(id) OVER w) IS NOT NULL AS del  -- not last row
      FROM   vendor_prices
      WHERE  vendor = _vendor
      WINDOW w AS (PARTITION BY sku ORDER BY effective_date, id)
      ) d
   WHERE v.id = d.id
   AND   d.del;

   GET DIAGNOSTICS del_ct = ROW_COUNT;  -- optional:
   RETURN del_ct;  -- return number of deleted rows
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

称呼:

SELECT remove_vendor_price_dupes(1);
Run Code Online (Sandbox Code Playgroud)

笔记

  • 9.3 主要版本的当前版本是 9.3.6。该项目建议...

    所有用户对于正在使用的任何主要版本都运行最新的可用次要版本。

  • 列索引(vendor, sku, effective_date, id)是完美的 - 按照这个特定的顺序。但 Postgres 也可以相当有效地组合索引。将不相关的项添加为索引的最后一项以从中获得仅索引扫描
    可能是值得的。price你必须进行测试。

  • 由于您有并发删除,因此最好为每个供应商运行单独的删除,以减少竞争条件和死锁的可能性。由于只有少数供应商,这似乎是一个合理的划分。(许多微小的调用会相对较慢。)

  • 我正在运行一个单独的SELECTPERFORM在 plpgsql 中,因为我们不使用结果),因为行锁定子句FOR UPDATE不能与窗口函数一起使用。不要让关键字误导您,这不仅仅是为了更新。我锁定给定供应商的所有行,因为结果取决于所有行。并发读取不会受到影响,只有并发写入必须等待完成。这就是为什么在单独的事务中一次删除一个供应商的行应该是最好的另一个原因。

  • sku每个产品都是独一无二的,所以我们可以PARTITION BY做到。

  • ORDER BY effective_date, id:问题的第一个版本包含重复行的代码,因此我将 id 添加为ORDER BY附加的平局断路器。(sku, effective_date)这样它也适用于重复项。

  • 要保留每组的最后一行:AND (lead(id) OVER w) IS NOT NULL. 重用相同的窗口lead()很便宜的 - 独立于添加的显式WINDOW子句 - 这只是为了方便起见的语法简写。

  • 我以相同的顺序锁定行:ORDER BY sku, effective_date, id。确保并发 DELETE 按相同顺序操作以避免死锁。如果所有其他事务在同一事务中删除的行不超过一行,则不会出现死锁,并且您根本不需要行锁定。

  • 如果并发 INSERT 可能会导致不同的结果(使不同的行过时),则必须以 EXCLUSIVE 模式锁定整个表以避免竞争条件:

    LOCK TABLE vendor_prices IN EXCLUSIVE MODE;
    
    Run Code Online (Sandbox Code Playgroud)

    仅在必要时才这样做。它阻止所有并发写访问。

  • 我正在返回删除的行数,但这完全是可选的。您也可以不返回任何内容并将函数声明为RETURNS void.