Domanda

Ho una tabella dei prodotti in cui inserisco circa 150.000 record al giorno.La maggior parte di loro sono ridondanti, ma ho bisogno di tenerli a causa della nuova data di scadenza.Ricevo feed di prodotti da circa 5 su 30 venditori al giorno.Ogni fornitore ha circa 35.000 prodotti unici.Nessun prodotto può appartenere a più di un fornitore.

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)
);

Sto cercando di eliminare i record irrilevanti in cui non c'è stata alcuna variazione di prezzo e non è più l'ultimo aggiornamento per detto prodotto, ad es.:

  effective_date     price
  '2015-05-01'       $1.99 
  '2015-05-02'       $1.99 eliminare
  '2015-05-03'       $1.59 
  '2015-05-04'       $1.99 
  '2015-05-05'       $1.99 eliminare
  '2015-05-06'       $1.99 conservare per nuova data di scadenza

Quindi dopo ogni caricamento (ho pensato che sarebbe stato più facile per un fornitore alla volta) voglio fare una sorta di eliminazione.Ecco la lunga soluzione non performante che mi è venuta in mente.

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

Questa funzione ha funzionato per alcune ore, quindi l'ho uccisa.La tabella ha circa 5 milioni di record.Ho provato tutti i tipi di indici diversi e indici combinati, ma nulla sembra aiutare.Potrebbero esserci altri inserti ed eliminazioni mentre sto eseguendo questa funzione.

Eseguire PostgreSQL 9.3.4 su Solaris 11.2.
Ho un sacco di RAM e spazio su disco.

È stato utile?

Soluzione

Caratteristica principale è il funzione finestra lag().
Prestare particolare attenzione anche per evitare deadlock e condizioni di gara con eliminazioni e inserimenti simultanei (che possono influenzare le righe da eliminare!):

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;

Chiamare:

SELECT remove_vendor_price_dupes(1);

Nota

  • La versione corrente della major release 9.3 è la 9.3.6. Il progetto raccomanda quello ...

    tutti gli utenti eseguono l'ultima versione minore disponibile per qualsiasi versione principale sia in uso.

  • A indice multicolore su (vendor, sku, effective_date, id) sarebbe perfetto per questo - in questo ordine particolare.Ma Postgres può combinare gli indici in modo piuttosto efficiente.
    Esso potrebbe pagare per aggiungere il altrimenti irrilevante price come ultimo elemento dell'indice per ottenere scansioni solo indice da questo.Dovrai fare un test.

  • Poiché si dispone di eliminazioni simultanee, potrebbe essere una buona idea eseguire un'eliminazione separata per fornitore per ridurre il potenziale di condizioni di gara e deadlock.Dal momento che ci sono solo pochi fornitori, questo sembra un partizionamento ragionevole.(Molte chiamate minuscole sarebbero relativamente lente.)

  • Sto gestendo un separato SELECT (PERFORM in plpgsql, dal momento che non usiamo il risultato) perché il clausola di blocco riga FOR UPDATE non può essere utilizzato insieme alle funzioni della finestra.Non lasciare che la parola chiave ti inganni, questo non è solo per gli aggiornamenti.Sto bloccando tutte le righe per il fornitore dato, poiché il risultato dipende da tutte le righe.Le letture simultanee non sono compromesse, solo le scritture simultanee devono aspettare fino a quando non abbiamo finito.Questo è un altro motivo per cui l'eliminazione di righe per un fornitore alla volta in una transazione separata dovrebbe essere la migliore.

  • sku è unico per prodotto, in modo da possiamo PARTITION BY esso.

  • ORDER BY effective_date, id:la tua prima versione della domanda includeva il codice per le righe duplicate, quindi ho aggiunto id a ORDER BY come ulteriore tie breaker.In questo modo funziona per i duplicati su (sku, effective_date) così.

  • Per conservare l'ultima riga per ogni set: AND (lead(id) OVER w) IS NOT NULL.Riutilizzare lo stesso finestra per lead() è economico-indipendente dall'aggiunta esplicita WINDOW clausola-questa è solo una abbreviazione della sintassi per comodità.

  • Sto bloccando le righe nello stesso ordine: ORDER BY sku, effective_date, id.Assicurarsi che le eliminazioni simultanee operino nello stesso ordine per evitare deadlock.Se tutte le altre transazioni non eliminano più di una singola riga all'interno della stessa transazione, non possono esserci deadlock e non è necessario il blocco della riga.

  • Se gli inserti simultanei potrebbero portare a un risultato diverso (rendere obsolete righe diverse), è necessario blocca l'intero tavolo in modalità ESCLUSIVA invece per evitare condizioni di gara:

    LOCK TABLE vendor_prices IN EXCLUSIVE MODE;
    

    Fallo solo se è necessario.Blocca tutti gli accessi in scrittura simultanei.

  • Sto restituendo il numero di righe cancellate, ma è totalmente facoltativo.Si potrebbe anche restituire nulla e dichiarare la funzione come RETURNS void.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a dba.stackexchange
scroll top