Question

J'ai une table de produits dans laquelle j'insère environ 150 000 enregistrements par jour.La plupart d’entre eux sont redondants, mais je dois les conserver en raison de la nouvelle date d’expiration.Je reçois des flux de produits d'environ 5 fournisseurs sur 30 par jour.Chaque fournisseur propose environ 35 000 produits uniques.Aucun produit ne peut appartenir à plus d’un fournisseur.

CREATE TABLE vendor_prices (
  id serial PRIMARY KEY,
  vendor integer NOT NULL,
  sku character varying(25) NOT NULL,
  category_name character varying(100) NOT NULL,
  price numeric(8,5) NOT NULL,
  effective_date timestamp without time zone,
  expiration_date timestamp without time zone DEFAULT (now() + '1 year'::interval)
);

J'essaie de supprimer les enregistrements non pertinents pour lesquels il n'y a pas eu de changement de prix et ce n'est plus la dernière mise à jour pour ledit produit, par exemple :

  effective_date     price
  '2015-05-01'       $1.99 
  '2015-05-02'       $1.99 supprimer
  '2015-05-03'       $1.59 
  '2015-05-04'       $1.99 
  '2015-05-05'       $1.99 supprimer
  '2015-05-06'       $1.99 conserver jusqu'à la nouvelle date de péremption

Ainsi, après chaque chargement (je pensais que ce serait plus facile pour un fournisseur à la fois), je souhaite effectuer une sorte de suppression.Voici la solution longuement non performante que j'ai proposée.

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

Cette fonction a fonctionné pendant quelques heures, je l'ai donc supprimée.La table contient environ 5 millions d'enregistrements.J'ai essayé toutes sortes d'index différents et d'index combinés, mais rien ne semble aider.Il peut y avoir d'autres insertions et suppressions pendant que j'exécute cette fonction.

Exécution de PostgreSQL 9.3.4 sur Solaris 11.2.
J'ai beaucoup de RAM et d'espace disque.

Était-ce utile?

La solution

La fonctionnalité principale est la fonction de fenêtre lag().
Portez également une attention particulière pour éviter les blocages et les conditions de concurrence avec des suppressions et des insertions simultanées (qui peuvent affecter les lignes à supprimer !) :

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;

Appel:

SELECT remove_vendor_price_dupes(1);

Remarques

  • La version actuelle de la version majeure 9.3 est la 9.3.6. Le projet recommande que ...

    tous les utilisateurs exécutent la dernière version mineure disponible, quelle que soit la version majeure utilisée.

  • UN index multicolonne sur (vendor, sku, effective_date, id) serait parfait pour cela - dans cet ordre particulier.Mais Postgres peut également combiner des index de manière assez efficace.
    Il pourrait payer pour ajouter ce qui autrement ne serait pas pertinent price comme dernier élément de l'index pour en extraire des analyses d'index uniquement.Il faudra tester.

  • Étant donné que vous avez des suppressions simultanées, il peut être judicieux d'exécuter une suppression distincte par fournisseur afin de réduire le risque de conditions de concurrence critique et de blocages.Puisqu’il n’y a que quelques fournisseurs, cela semble être un partitionnement raisonnable.(De nombreux petits appels seraient relativement lents.)

  • Je dirige un séparé SELECT (PERFORM dans plpgsql, puisque nous n'utilisons pas le résultat) car le clause de verrouillage de ligne FOR UPDATE ne peut pas être utilisé avec les fonctions de fenêtre.Ne vous laissez pas induire en erreur par le mot-clé, ce n'est pas uniquement destiné aux mises à jour.Je verrouille toutes les lignes du fournisseur donné, car le résultat dépend de toutes les lignes.Les lectures simultanées ne sont pas altérées, seules les écritures simultanées doivent attendre que nous ayons terminé.C'est une autre raison pour laquelle il est préférable de supprimer des lignes pour un fournisseur à la fois dans une transaction distincte.

  • sku est unique par produit, nous pouvons donc PARTITION BY il.

  • ORDER BY effective_date, id:votre première version de la question incluait du code pour les lignes en double, j'ai donc ajouté l'identifiant à ORDER BY comme bris d’égalité supplémentaire.De cette façon, cela fonctionne pour les doublons sur (sku, effective_date) aussi.

  • Pour conserver la dernière ligne de chaque ensemble : AND (lead(id) OVER w) IS NOT NULL.Réutiliser le même fenêtre pour lead() est bon marché - indépendamment de l'ajout explicite WINDOW clause - c'est juste un raccourci syntaxique pour plus de commodité.

  • Je verrouille les lignes dans le même ordre : ORDER BY sku, effective_date, id.Assurez-vous que les DELETE simultanés fonctionnent dans le même ordre pour éviter les blocages.Si toutes les autres transactions ne suppriment pas plus d'une seule ligne au sein de la même transaction, il ne peut pas y avoir de blocages et vous n'avez pas du tout besoin du verrouillage de ligne.

  • Si des INSERT simultanés pourraient conduire à un résultat différent (rendre différentes lignes obsolètes), vous devez verrouiller toute la table en mode EXCLUSIF au lieu de cela, pour éviter les conditions de concurrence :

    LOCK TABLE vendor_prices IN EXCLUSIVE MODE;
    

    Faites-le seulement si c'est nécessaire.Il bloque tout accès simultané en écriture.

  • Je renvoie le nombre de lignes supprimées, mais c'est totalement facultatif.Autant ne rien renvoyer et déclarer la fonction comme RETURNS void.

Licencié sous: CC-BY-SA avec attribution
Non affilié à dba.stackexchange
scroll top