Come posso ottenere in modo efficiente "la riga corrispondente più recente"?


53

Ho un modello di query che deve essere molto comune, ma non so come scrivere una query efficiente per questo. Voglio cercare le righe di una tabella che corrispondono alla "data più recente non successiva" alle righe di un'altra tabella.

Ho una tabella, per inventoryesempio, che rappresenta l'inventario che conservo in un determinato giorno.

date       | good | quantity
------------------------------
2013-08-09 | egg  | 5
2013-08-09 | pear | 7
2013-08-02 | egg  | 1
2013-08-02 | pear | 2

e una tabella, ad esempio "prezzo", che contiene il prezzo di un bene in un determinato giorno

date       | good | price
--------------------------
2013-08-07 | egg  | 120
2013-08-06 | pear | 200
2013-08-01 | egg  | 110
2013-07-30 | pear | 220

Come posso ottenere in modo efficiente il prezzo "più recente" per ogni riga della tabella di inventario, ad es

date       | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07   | egg  | 5        | 120
2013-08-09 | 2013-08-06   | pear | 7        | 200
2013-08-02 | 2013-08-01   | egg  | 1        | 110
2013-08-02 | 2013-07-30   | pear | 2        | 220

Conosco un modo per farlo:

select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good

e quindi unire nuovamente questa query all'inventario. Per tabelle di grandi dimensioni, anche facendo la prima query (senza entrare ancora una volta di un magazzino) è molto lento. Tuttavia, lo stesso problema viene rapidamente risolto se utilizzo semplicemente il mio linguaggio di programmazione per inviare una max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1query per ciascuno date_of_interestdalla tabella di inventario, quindi so che non esiste alcun impedimento computazionale. Preferirei comunque risolvere l'intero problema con una singola query SQL, poiché mi consentirebbe di eseguire ulteriori elaborazioni SQL sul risultato della query.

Esiste un modo standard per farlo in modo efficiente? Sembra che debba venire fuori spesso e che ci dovrebbe essere un modo per scrivere una query veloce per questo.

Sto usando Postgres, ma una risposta generica di SQL sarebbe apprezzata.


3
Votato per essere migrato su DBA.SE in quanto è una domanda di efficienza. Potremmo scrivere la query in diversi modi, ma ciò non lo renderà molto più veloce.
ypercubeᵀᴹ

5
Hai davvero bisogno di tutte le merci per tutti i giorni da una singola query? Sembra un requisito improbabile? Più comunemente si recupererebbero i prezzi per una data specifica o i prezzi per un bene specifico (a una data specifica). Quelle query alternative potrebbero beneficiare molto più facilmente di indici (appropriati). Dobbiamo anche sapere: cardinalità (quante righe in ogni tabella?), La definizione di tabella completa incl. tipi di dati, vincoli, indici, ... (uso \d tblin psql), la tua versione di Postgres e min. / max. numero di prezzi per bene.
Erwin Brandstetter,

@ErwinBrandstetter Mi stai chiedendo di accettare una risposta? Non sono veramente qualificato per sapere qual è il migliore, anche se il tuo ha il maggior numero di voti, sono felice di accettarlo.
Tom Ellis,

Accetta solo se risponde alla tua domanda o funziona per te. Potresti anche lasciare un commento su come hai proceduto se ciò potesse aiutare i casi correlati. Se ritieni che la tua domanda non abbia ricevuto risposta, faccelo sapere.
Erwin Brandstetter,

1
Devo poi scusarmi, perché anche se ho ricevuto quelle che sembrano risposte eccellenti, non sto più lavorando al problema che ha provocato la domanda, quindi non sono in grado di giudicare quale sia la risposta migliore, o se davvero una di esse sono davvero adatti al mio caso d'uso (com'era). Se c'è qualche etichetta DBA.Stackexchange che dovrei seguire in questo caso, per favore fatemelo sapere.
Tom Ellis,

Risposte:


42

Dipende molto dalle circostanze e dai requisiti esatti. Considera il mio commento alla domanda .

Soluzione semplice

Con DISTINCT ONin Postgres:

SELECT DISTINCT ON (i.good, i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good, i.the_date, p.the_date DESC;

Risultato ordinato.

O con NOT EXISTSSQL standard (funziona con ogni RDBMS che conosco):

SELECT i.the_date, p.the_date AS pricing_date, i.good, i.quantity, p.price
FROM   inventory  i
LEFT   JOIN price p ON p.good = i.good AND p.the_date <= i.the_date
WHERE  NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good = p.good
   AND p1.the_date <= i.the_date
   AND p1.the_date >  p.the_date
   );

Stesso risultato, ma con ordinamento arbitrario, a meno che non venga aggiunto ORDER BY.
A seconda della distribuzione dei dati, dei requisiti e degli indici esatti, uno di questi può essere più veloce.
Generalmente, DISTINCT ONè il vincitore e ottieni un risultato ordinato su di esso. Ma per alcuni casi altre tecniche di query sono (molto) più veloci, ancora. Vedi sotto.

Le soluzioni con sottoquery per calcolare i valori max / min sono generalmente più lente. Le varianti con CTE sono generalmente più lente, ancora.

Le viste semplici (come quelle proposte da un'altra risposta) non aiutano affatto le prestazioni in Postgres.

SQL Fiddle.


Soluzione corretta

Stringhe e regole di confronto

Prima di tutto, soffri di un layout di tabella non ottimale. Può sembrare banale, ma la normalizzazione del tuo schema può fare molto.

Ricerca per tipi di carattere ( text, varchar, ...) deve essere fatto in base alle impostazioni internazionali - il COLLATION in particolare. Molto probabilmente il tuo DB usa alcune regole locali (come nel mio caso:) de_AT.UTF-8. Scoprilo con:

SHOW lc_collate;

Ciò rende più lenti l' ordinamento e l'indicizzazione delle ricerche . Più lunghe sono le stringhe (nomi dei prodotti), peggio. Se in realtà non ti interessano le regole di confronto nell'output (o l'ordinamento), questo può essere più veloce se aggiungi COLLATE "C":

SELECT DISTINCT ON (i.good COLLATE "C", i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good COLLATE "C", i.the_date, p.the_date DESC;

Nota come ho aggiunto le regole di confronto in due punti.
Due volte più veloce nel mio test con 20k righe ciascuno e nomi molto semplici ('good123').

Indice

Se si suppone che la query utilizzi un indice, le colonne con i dati dei caratteri devono utilizzare un confronto corrispondente ( goodnell'esempio):

CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);

Assicurati di leggere gli ultimi due capitoli di questa risposta correlata su SO:

Puoi anche avere più indici con regole di confronto diverse sulle stesse colonne, se hai anche bisogno di merci ordinate in base a un'altra (o impostazione predefinita) di confronto in altre query.

Normalizzare

Le stringhe ridondanti (nome del bene) gonfiano anche le tabelle e gli indici, il che rende tutto ancora più lento. Con un corretto layout della tabella potresti evitare la maggior parte del problema. Potrebbe apparire così:

CREATE TABLE good (
  good_id serial PRIMARY KEY
, good    text   NOT NULL
);

CREATE TABLE inventory (
  good_id  int  REFERENCES good (good_id)
, the_date date NOT NULL
, quantity int  NOT NULL
, PRIMARY KEY(good_id, the_date)
);

CREATE TABLE price (
  good_id  int     REFERENCES good (good_id)
, the_date date    NOT NULL
, price    numeric NOT NULL
, PRIMARY KEY(good_id, the_date));

Le chiavi primarie forniscono automaticamente (quasi) tutti gli indici di cui abbiamo bisogno.
A seconda dei dettagli mancanti, un indice a più pricecolonne attivo con ordine decrescente nella seconda colonna può migliorare le prestazioni:

CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);

Ancora una volta, le regole di confronto devono corrispondere alla tua query (vedi sopra).

In Postgres 9.2 o versioni successive gli "indici di copertura" per le scansioni solo indice potrebbero essere di aiuto, soprattutto se le tue tabelle contengono colonne aggiuntive, rendendo la tabella sostanzialmente più grande dell'indice di copertura.

Queste query risultanti sono molto più veloci:

NON ESISTE

SELECT i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
AND    NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good_id = p.good_id
   AND    p1.the_date <= i.the_date
   AND    p1.the_date >  p.the_date
   );

DISTINCT ON

SELECT DISTINCT ON (i.the_date)
       i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
ORDER  BY i.the_date, p.the_date DESC;

SQL Fiddle.


Soluzioni più veloci

Se ciò non è ancora abbastanza veloce, potrebbero esserci soluzioni più veloci.

Sottoquery JOIN LATERALcorrelata CTE ricorsiva /

Soprattutto per le distribuzioni di dati con molti prezzi per bene :

Vista materializzata

Se devi eseguire questo spesso e velocemente, ti suggerisco di creare una vista materializzata. Penso che sia sicuro presumere che i prezzi e gli inventari per le date passate raramente cambino. Calcola il risultato una volta e archivia un'istantanea come vista materializzata.

Postgres 9.3+ ha un supporto automatizzato per viste materializzate. Puoi facilmente implementare una versione base nelle versioni precedenti.


3
L' price_good_date_desc_idxindice che raccomandi ha notevolmente migliorato le prestazioni per una mia query simile. Il mio piano di query è passato da un costo di 42374.01..42374.86fino a 0.00..37.12!
cimmanon,

@cimmanon: Nice! Qual è la tua funzione di query principale? NON ESISTE? DISTINCT ON? RAGGRUPPARE PER?
Erwin Brandstetter,

Usando DISTINCT ON
cimmanon

6

Cordiali saluti, ho usato mssql 2008, quindi Postgres non avrà l'indice "include". Tuttavia, l'utilizzo dell'indicizzazione di base mostrata di seguito cambierà dai join hash per unire i join in Postgres: http://explain.depesz.com/s/eF6 (nessun indice) http://explain.depesz.com/s/j9x ( con indice sui criteri di join)

Propongo di suddividere la tua richiesta in due parti. Innanzitutto, una vista (non destinata a migliorare le prestazioni) che può essere utilizzata in una varietà di altri contesti che rappresenta la relazione tra le date di inventario e le date di determinazione del prezzo.

create view mostrecent_pricing_dates_per_good as
select i.good,i.date i_date,max(p.date)p_date
  from inventory i
  join price p on i.good = p.good and i.date >= p.date
 group by i.good,i.date;

Quindi la tua query può diventare più semplice e più facile da manipolare per altri tipi se l'inchiesta (come l'uso dei join di sinistra per trovare l'inventario senza date di prezzo recenti):

select i.good
       ,i.date inventory_date
       ,i.quantity
       ,p.date pricing_date
       ,p.price       
  from inventory i
  join price p on i.good = p.good
  join mostrecent_pricing_dates_per_good x 
    on i.good = x.good 
   and p.date = x.p_date
   and i.date = x.i_date

Ciò produce il seguente piano di esecuzione: http://sqlfiddle.com/#!3/24f23/1 nessuna indicizzazione

... Tutte le scansioni con un ordinamento completo. Si noti che il costo delle prestazioni delle partite di hash occupa gran parte del costo totale ... e sappiamo che le scansioni e l'ordinamento delle tabelle sono lente (rispetto all'obiettivo: ricerche di indice).

Ora, aggiungi gli indici di base per aiutare i criteri utilizzati nel tuo join (non pretendo che si tratti di indici ottimali, ma illustrano il punto): http://sqlfiddle.com/#!3/5ec75/1 con indicizzazione di base

Questo mostra un miglioramento. Le operazioni di ciclo nidificato (join interno) non occupano più alcun costo totale rilevante per la query. Il resto del costo è ora ripartito tra le ricerche di indice (una scansione per inventario perché stiamo tirando ogni riga di inventario). Ma possiamo fare ancora meglio perché la query tira quantità e prezzo. Per ottenere tali dati, dopo aver valutato i criteri di join, è necessario eseguire ricerche.

L'iterazione finale utilizza "include" sugli indici per facilitare il passaggio del piano e ottenere i dati aggiuntivi richiesti direttamente dall'indice stesso. Quindi le ricerche sono sparite: http://sqlfiddle.com/#!3/5f143/1 inserisci qui la descrizione dell'immagine

Ora abbiamo un piano di query in cui il costo totale della query viene distribuito uniformemente tra operazioni di ricerca indice molto veloci. Questo sarà vicino al meglio. Sicuramente altri esperti possono migliorare ulteriormente questo aspetto, ma la soluzione chiarisce un paio di preoccupazioni importanti:

  1. Crea strutture di dati intelligibili nel database che sono più facili da comporre e riutilizzare in altre aree di un'applicazione.
  2. Tutti gli operatori di query più costosi sono stati presi in considerazione dal piano di query utilizzando alcuni indici di base.

3
Questo va bene (per SQL-Server) ma ottimizza per diversi DBMS mentre ha somiglianze, ha anche differenze gravi.
ypercubeᵀᴹ

@ypercube è vero. Ho aggiunto alcune qualifiche su Postgres. La mia intenzione era che la maggior parte del processo di pensiero illustrato qui si applicasse indipendentemente dalle caratteristiche specifiche del DBMS.
Cocogorilla,

La risposta è molto approfondita, quindi mi ci vorrà del tempo per provarlo. Ti farò sapere come vado avanti.
Tom Ellis,

5

Se ti capita di avere PostgreSQL 9.3 (rilasciato oggi), puoi utilizzare un LATERAL JOIN.

Non ho modo di testarlo e non l'ho mai usato prima, ma da quello che posso dire dalla documentazione la sintassi sarebbe qualcosa di simile:

SELECT  Inventory.Date,
        Inventory.Good,
        Inventory.Quantity,
        Price.Date,
        Price.Price
FROM    Inventory
        LATERAL
        (   SELECT  Date, Price
            FROM    Price
            WHERE   Price.Good = Inventory.Good
            AND     Price.Date <= Inventory.Date
            ORDER BY Price.Date DESC
            LIMIT 1
        ) p;

Questo è fondamentalmente equivalente all'APPLICAZIONE di SQL Server e c'è un esempio funzionante su SQL-Fiddle a scopo dimostrativo.


5

Come hanno notato Erwin e altri, una query efficiente dipende da molte variabili e PostgreSQL si impegna molto per ottimizzare l'esecuzione della query in base a tali variabili. In generale, per prima cosa si desidera scrivere per chiarezza, quindi modificare per le prestazioni dopo aver identificato i colli di bottiglia.

Inoltre PostgreSQL ha molti trucchi che puoi usare per rendere le cose un po 'più efficienti (indici parziali per uno), quindi a seconda del tuo carico di lettura / scrittura, potresti essere in grado di ottimizzare molto lontano esaminando un'indicizzazione attenta.

La prima cosa da provare è solo fare una vista e unirla:

CREATE VIEW most_recent_rows AS
SELECT good, max(date) as max_date
FROM inventory
GROUP BY good;

Questo dovrebbe funzionare bene quando si fa qualcosa del tipo:

SELECT price 
  FROM inventory i
  JOIN goods g ON i.goods = g.description
  JOIN most_recent_rows r ON i.goods = r.goods
 WHERE g.id = 123;

Quindi puoi unirti a quello. La query finirà per unire la vista alla tabella sottostante, ma supponendo che tu abbia un indice univoco su (data, buona in quell'ordine ), dovresti essere pronto (dal momento che questa sarà una semplice ricerca nella cache). Funzionerà molto bene con alcune righe cercate, ma sarà molto inefficiente se stai cercando di digerire milioni di prezzi dei beni.

La seconda cosa che potresti fare è aggiungere alla tabella di inventario una colonna bool most_recent e

create unique index on inventory (good) where most_recent;

Dovresti quindi utilizzare i trigger per impostare most_recent su false quando è stata inserita una nuova riga per un bene. Ciò aggiunge maggiore complessità e maggiori possibilità di bug, ma è utile.

Anche in questo caso molto dipende dal fatto che siano in atto indici appropriati. Per le query sulla data più recenti, probabilmente dovresti avere un indice alla data e possibilmente uno a più colonne che inizia con la data e che include i criteri di partecipazione.

Aggiorna il commento di Per Erwin qui sotto, sembra che abbia frainteso questo. Rileggendo la domanda non sono affatto sicuro di ciò che viene chiesto. Voglio menzionare nell'aggiornamento qual è il potenziale problema che vedo e perché questo non lo chiarisce.

Il design del database offerto non ha alcun reale utilizzo dell'IME con ERP e sistemi di contabilità. Funzionerebbe in un ipotetico modello di prezzo perfetto in cui tutto ciò che viene venduto in un determinato giorno di un determinato prodotto ha lo stesso prezzo. Tuttavia, questo non è sempre il caso. Non è nemmeno il caso di cose come gli scambi di valuta (anche se alcuni modelli fingono che lo faccia). Se questo è un esempio inventato, non è chiaro. Se è un vero esempio, ci sono problemi più grandi con la progettazione a livello di dati. Presumo qui che questo sia un vero esempio.

Non si può presumere che la sola data specifichi il prezzo per un dato bene. I prezzi in qualsiasi azienda possono essere negoziati per controparte e talvolta anche per transazione. Per questo motivo, è necessario archiviare il prezzo nella tabella che gestisce effettivamente l'inventario dentro o fuori (la tabella dell'inventario). In tal caso, la tabella data / merce / prezzo specifica semplicemente un prezzo base che può essere soggetto a modifiche in base alla negoziazione. In tal caso, questo problema va dall'essere un problema di segnalazione a uno che è transazionale e che opera su una riga per ogni tabella alla volta. Ad esempio, è possibile quindi cercare il prezzo predefinito per un determinato prodotto in un determinato giorno come:

 SELECT price 
   FROM prices p
   JOIN goods g ON p.good = g.good
  WHERE g.id = 123 AND p."date" >= '2013-03-01'
  ORDER BY p."date" ASC LIMIT 1;

Con un indice sui prezzi (buono, data) questo funzionerà bene.

Se questo è un esempio inventato, forse qualcosa di più vicino a ciò su cui stai lavorando sarebbe di aiuto.


L' most_recentapproccio dovrebbe funzionare bene per il prezzo più recente in assoluto . Tuttavia, sembra che l'OP abbia bisogno del prezzo più recente relativo a ciascuna data di inventario.
Erwin Brandstetter,

Buon punto. Rileggendo però, riscontro alcune vere carenze pratiche con i dati proposti, ma non riesco a capire se si tratta solo di un esempio inventato. Come esempio inventato, non so dire cosa manca. Forse un aggiornamento per sottolineare questo sarebbe anche in ordine.
Chris Travers,

@ChrisTravers: è un esempio inventato, ma non sono libero di pubblicare lo schema reale con cui sto lavorando. Forse potresti dire qualcosa sulle carenze pratiche che hai notato.
Tom Ellis,

Non penso che debba essere esatto, ma preoccupato per il problema che si perde nell'Allegoria. Qualcosa di un po 'più vicino sarebbe utile. Il problema è che con i prezzi, è probabile che il prezzo in un determinato giorno sia un valore predefinito e di conseguenza non lo utilizzeresti solo per i rapporti come predefinito per l'immissione della transazione, quindi le tue query interessanti sono in genere solo poche righe in un tempo.
Chris Travers,

3

Un altro modo sarebbe quello di utilizzare la funzione di finestra lead()per ottenere l'intervallo di date per ogni riga nel prezzo della tabella e quindi utilizzarlo betweenquando si accede all'inventario. In realtà l'ho usato nella vita reale, ma principalmente perché questa è stata la mia prima idea su come risolverlo.

with cte as (
  select
    good,
    price,
    date,
    coalesce(lead(date) over(partition by good order by date) - 1
            ,Now()::date) as ndate
  from
    price
)

select * from inventory i join cte on
  (i.good = cte.good and i.date between cte.date and cte.ndate)

SqlFiddle


1

Utilizzare un join dall'inventario al prezzo con condizioni di join che limitano le registrazioni dal tabelp del prezzo solo a quelle che si trovano alla data dell'inventario o prima di esso, quindi estrarre la data massima e dove la data è la data più alta da quel sottoinsieme

Quindi per il prezzo dell'inventario:

 Select i.date, p.Date pricingDate,
    i.good, quantity, price        
 from inventory I join price p 
    on p.good = i.good
        And p.Date = 
           (Select Max(Date from price
            where good = i.good
               and date <= i.Date)

Se il prezzo di un determinato bene è cambiato più di una volta nello stesso giorno e in queste colonne sono presenti solo date e nessuna ora, potrebbe essere necessario applicare ulteriori restrizioni sui join per selezionare solo uno dei record di modifica del prezzo.


Purtroppo non sembra accelerare le cose.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.