Prestazioni della rara SELECT vs.INSERT frequenti nei dati delle serie temporali
-
29-09-2020 - |
Domanda
Ho una semplice tabella delle serie temporali
movement_history (
data_id serial,
item_id character varying (8),
event_time timestamp without timezone,
location_id character varying (7),
area_id character varying (2)
);
Il mio sviluppatore frontend mi dice che il costo è troppo alto se vuole sapere dove si trova un elemento in un determinato timestamp perché deve ordinare la tabella.Vuole che aggiunga un altro campo timestamp per il prossimo evento in modo da non dover ordinare.Tuttavia, ciò raddoppierà il costo del mio codice per inserire un nuovo movimento poiché dovrò interrogare la voce precedente per l'articolo, aggiornarla e quindi inserire i nuovi dati.
I miei inserti ovviamente superano di gran lunga le sue domande in frequenza.E non ho mai visto una tabella della serie temporale che includesse una voce per l'ora del prossimo evento.Mi sta dicendo che il mio tavolo è rotto perché la sua rara query richiede un ordinamento.Eventuali suggerimenti?
Non so quale query stia usando, ma farei questo:
select * from movement_history
where event_time <= '1-15-2015'::timestamp
and item_id = 'H665AYG3'
order by event_time desc limit 1;
Al momento disponiamo di circa 15.000 articoli che vengono inseriti nel database al massimo una volta al giorno.Tuttavia presto avremo 50.000 articoli con dati dei sensori aggiornati ogni 1-5 minuti.
Non vedo che la sua query venga eseguita molto spesso, ma lo sarà un'altra query per ottenere lo stato corrente dei pallet.
select distinct on (item_id) *
from movement_history
order by item_id, event_time desc;
Su questo server è attualmente in esecuzione la versione 9.3 ma, se necessario, potrebbe essere in esecuzione la versione 9.4.
Soluzione
Creare un indice su (item_id, event_time)
.
Volterà all'elemento specificato, salterà all'event_time specificato per questo elemento_ID, quindi spostarlo.Nessun ordinamento coinvolto.
Altri suggerimenti
Soluzioni contrastanti
Avresti bisogno di un indice a più colonne come fornito da @jjanes.Mentre ci sei, tu Potevo Fare (item_id, event_time)
la chiave primaria per fornire automaticamente l'indice.
Ma questo è in conflitto con le prestazioni di scrittura come @Michael ha spiegato:Raddoppi il costo per 50K of items ... updated every 1 to 5 minutes
produrre occasionale SELECT
query più economiche.Sono circa 1 milione.righe all'ora.
Partizionamento
Se non hai requisiti più contrastanti, il compromesso potrebbe esserlo partizionamento dove il attuale la partizione non ha ancora un indice.In questo modo ottieni prestazioni di scrittura al top e prestazioni di lettura (quasi) al top.
La tabella genitore potrebbe essere movement_history
, la partizione corrente movement_history_current
.Nessun indice, solo un vincolo da consentire esclusione dei vincoli.Potrebbero essere partizioni giornaliere per impostazione predefinita.Ma gli intervalli di tempo possono essere nulla, non deve nemmeno essere regolare.Possiamo lavorare con questo e avviare una nuova partizione ogni volta che ne abbiamo bisogno.
Quando è necessario includere i dati correnti in detta query, procedere come segue:
Per avviare una nuova partizione, in una transazione:
- Rinominare la partizione corrente aggiungendo sth.al nome, come
movement_history_20150110_20150115
(o più specifico) e modificare il vincoloevent_time
. - Crea una nuova partizione con lo stesso nome
movement_history_current
e un vincolo suevent_time
che non si sovrappone all'ultimo e con finale aperto. - A seconda dei tuoi modelli di accesso potresti dover gestire l'accesso in scrittura simultaneo ...
- Rinominare la partizione corrente aggiungendo sth.al nome, come
Aggiungi un PK attivo
(item_id, event_time)
alla nuova partizione storica.Non nella stessa transazione.Creare l'indice in un unico pezzo lo è tanto più economico che aggiungerlo in modo incrementale.2a.Per integrare i consigli per la tua seconda query di seguito:
REFRESH MATERIALIZED VIEW mv_last_movement
Esegui interrogazione.In realtà, puoi eseguire la query Qualunque tempo.Se include la partizione corrente o qualsiasi partizione che non dispone ancora dell'indice, è più lento per quella partizione.
Archivia di tanto in tanto le partizioni più vecchie.Basta eseguire il backup ed eliminare la tabella.Non interferisce molto con le operazioni in corso, questo è il bello del partizionamento.
Leggere prima il manuale.Ci sono avvertenze per eredità E partizionamento.
La tua seconda domanda
La seconda query che hai aggiunto in una modifica è the lontano questione più grande per le prestazioni.Sto parlando di ordini di grandezza:
select distinct on (item_id) * from movement_history order by item_id, event_time desc;
Una volta che inizi a inserire 1 mio.righe all'ora, le prestazioni di questa query peggioreranno rapidamente.Hai a che fare con molti molti righe per articolo, DISTINCT ON
va bene solo per pochi righe per articolo.Spiegazione dettagliata per DISTINCT ON
e alternative più veloci:
- Selezionare la prima riga in ciascun gruppo GRUPPO PER?
- Indice non utilizzato nell'intervallo di query di date
Suggerisco ancora partizionamento come nella mia prima risposta.Ma applica una nuova partizione a intervalli ragionevoli, in modo che la partizione corrente non diventi troppo grande.
Inoltre, crea un file "vista materializzata" che tiene traccia dello stato più recente di ciascun articolo.Non è uno standard MATERIALIZED VIEW
perché la query di definizione ha un autoreferenzialità.Lo nomino mv_last_movement
e ha lo stesso tipo di riga di movement_history
.
Aggiorna ogni volta che viene avviata una nuova partizione (vedi sopra).
Supponendo l'esistenza di un item
tavolo:
CREATE TABLE item (
item_id varchar(8) PRIMARY KEY -- should really be a serial
-- more columns?
);
Se non ne hai uno, crealo.Oppure utilizzare la tecnica CTE ricorsiva alternativa descritta nella risposta collegata sopra.
Dentro mv_last_movement
una volta:
CREATE TABLE mv_last_movement AS
SELECT m.*
FROM item i
, LATERAL (
SELECT *
FROM movement_history_current -- current partition
WHERE item_id = i.item_id -- lateral reference
ORDER BY event_time DESC
LIMIT 1
) m;
ALTER TABLE mv_last_movement ADD PRIMARY KEY (item_id);
Quindi, per aggiornare (in un'unica transazione!):
BEGIN;
CREATE TABLE mv_last_movement2 AS
SELEZIONA m.*
A partire dall'articolo i
, LATERALE (
( -- parentesi obbligatorie
SELEZIONARE*
FROM movement_history_current -- partizione corrente
WHERE item_id = i.item_id -- riferimento laterale
ORDINA PER event_time DESC
LIMITE 1 -- applicato a questo SELECT, non strettamente necessario ma più economico
)
UNION ALL -- se non trovato, torna all'ultimo stato precedente
SELEZIONARE*
DA mv_last_movement -- la tua visione materializzata
WHERE item_id = i.item_id -- riferimento laterale
LIMIT 1 -- applicato all'intera query UNION
) m;
DROP TABLE mv_last_movement;
ALTER TABLE mv_last_movement2 RENAME mv_last_movement;
ALTER TABLE mv_last_movement ADD PRIMARY KEY (item_id);
COMMIT;
O simili.Maggiori dettagli qui:
La stessa query dall'alto (grassetto) sostituisce anche la query originale citata in alto.
In questo modo non è necessario controllare l'intera cronologia per gli elementi senza righe correnti, il che sarebbe estremamente costoso.
Perché UNION ALL ... LIMIT 1
?
Ulteriori consigli
varchar
per le colonne PK/FK è inefficiente, soprattutto per tabelle di grandi dimensioni con 1 milione di righe all'ora.Utilizzointeger
invece le chiavi.Utilizza sempre il formato ISO per i valori letterali di data e ora oppure le tue query dipendono dalle impostazioni locali:
'2015-15-01'
invece di.'1-15-2015'
Aggiungere
NOT NULL
vincoli in cui la colonna non può essere NULL.Ottimizza il layout del tuo tavolo per evitare spazio perso a causa del riempimento
- Configurazione di PostgreSQL per le prestazioni di lettura
(La domanda si intitola "prestazioni di lettura", ma quella parte è la stessa per le prestazioni di scrittura.)
- Configurazione di PostgreSQL per le prestazioni di lettura
Spesso il design del software è un compromesso tra requisiti concorrenti.È importante capire i meriti relativi, sia per il sistema nel suo insieme che per ciascun caso localmente.Ad esempio, tu dici scrive che superano i numerosi recita.Ciò suggerirebbe che il sistema nel suo complesso dovrebbe essere ottimizzato per scritture.Tuttavia, quali sono quelle letture per - impediscono una collisione del veicolo o un arresto cardiaco?Forse quei sistemi dovrebbero essere ottimizzati per leggere.
Hai un indice sulla colonna del tempo?Quindi una query come select top (1) .. where time < parameter .. sorted desc
dovrebbe usare quell'indice.Essenzialmente, pre-ordina i dati per tutte le query.
L'ironia è che ogni scrittura dovrà mantenere questo indice, raddoppiando il costo ogni volta.