Qualcuno potrebbe spiegare comportamenti bizzarri eseguendo milioni di AGGIORNAMENTI?


8

Qualcuno potrebbe spiegarmi questo comportamento? Ho eseguito la seguente query su Postgres 9.3 in modo nativo su OS X. Stavo cercando di simulare un comportamento in cui la dimensione dell'indice poteva aumentare molto più della dimensione della tabella, e invece ho trovato qualcosa di ancora più bizzarro.

CREATE TABLE test(id int);
CREATE INDEX test_idx ON test(id);

CREATE FUNCTION test_index(batch_size integer, total_batches integer) RETURNS void AS $$
DECLARE
  current_id integer := 1;
BEGIN
FOR i IN 1..total_batches LOOP
  INSERT INTO test VALUES (current_id);
  FOR j IN 1..batch_size LOOP
    UPDATE test SET id = current_id + 1 WHERE id = current_id;
    current_id := current_id + 1;
  END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT test_index(500, 10000);

L'ho lasciato funzionare per circa un'ora sul mio computer locale, prima di iniziare a ricevere avvisi sul problema del disco da OS X. Ho notato che Postgres stava succhiando circa 10 MB / s dal mio disco locale e che il database Postgres stava consumando un totale di 30 GB dalla mia macchina. Ho finito per annullare la query. Indipendentemente da ciò, Postgres non mi ha restituito lo spazio su disco e ho richiesto al database le statistiche sull'utilizzo con il seguente risultato:

test=# SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_relation_size(C.oid)) AS "size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  ORDER BY pg_relation_size(C.oid) DESC
  LIMIT 20;

           relation            |    size
-------------------------------+------------
 public.test                   | 17 GB
 public.test_idx               | 14 GB

Tuttavia, la selezione dalla tabella non ha prodotto risultati.

test=# select * from test limit 1;
 id
----
(0 rows)

L'esecuzione di 10000 batch di 500 è di 5.000.000 di righe, il che dovrebbe produrre una tabella / dimensione dell'indice piuttosto piccola (sulla scala di MB). Sospetto che Postgres stia creando una nuova versione della tabella / indice per ogni INSERT / UPDATE che sta accadendo con la funzione, ma questo sembra strano. L'intera funzione viene eseguita in modo transazionale e la tabella era vuota per l'avvio.

Qualche idea sul perché sto vedendo questo comportamento?

In particolare, le due domande che ho sono: perché questo spazio non è stato ancora recuperato dal database e il secondo è perché il database ha richiesto così tanto spazio in primo luogo? 30 GB sembra molto anche quando si contabilizza MVCC

Risposte:


7

Versione breve

Il tuo algoritmo sembra O (n * m) a prima vista, ma aumenta effettivamente O (n * m ^ 2), perché tutte le righe hanno lo stesso ID. Invece di 5 milioni di righe, ottieni> 1,25 righe

Versione lunga

La tua funzione è all'interno di una transazione implicita. Ecco perché non vengono visualizzati dati dopo aver annullato la query e anche perché deve mantenere versioni distinte delle tuple aggiornate / inserite per entrambi i loop.

Inoltre, sospetto che tu abbia un bug nella tua logica o sottovaluti il ​​numero di aggiornamenti effettuati.

Prima iterazione del ciclo esterno: current_id inizia da 1, inserisce 1 riga, quindi il ciclo interno esegue un aggiornamento 10000 volte per la stessa riga, finalizzando con l'unica riga che mostra un ID di 10001 e current_id con un valore di 10001. 10001 le versioni della riga vengono comunque mantenute, poiché la transazione non è terminata.

Seconda iterazione del ciclo esterno: poiché current_id è 10001, viene inserita una nuova riga con ID 10001. Ora hai 2 righe con lo stesso "ID" e 10003 versioni in totale di entrambe le righe (10002 della prima, 1 di il secondo). Quindi il ciclo interno aggiorna 10000 volte ENTRAMBE le righe, creando 20000 nuove versioni, arrivando a 30003 tuple finora ...

Terza iterazione del ciclo esterno: l'ID corrente è 20001, una nuova riga viene inserita con l'ID 20001. Hai 3 righe, tutte con lo stesso "ID" 20001, 30006 righe / tuple finora. Quindi esegui 10000 aggiornamenti di 3 righe, creando 30000 nuove versioni, ora 60006 ...

...

(Se il tuo spazio lo avesse permesso) - 500a iterazione del ciclo esterno, crea 5M aggiornamenti di 500 righe, proprio in questa iterazione

Come vedi, invece degli aggiornamenti 5M previsti, hai ricevuto 1000 + 2000 + 3000 + ... + 4990000 + 5000000 aggiornamenti (più modifiche), che sarebbero 10000 * (1 + 2 + 3 + ... + 499+ 500), oltre 1.25G aggiornamenti. E ovviamente una riga non è solo la dimensione del tuo int, ha bisogno di una struttura aggiuntiva, quindi la tabella e l'indice superano le dimensioni di dieci gigabyte.

Domande e risposte correlate:


5

PostgreSQL restituisce spazio su disco solo dopo VACUUM FULL, non dopo un DELETEo ROLLBACK(come risultato della cancellazione)

La forma standard di VACUUM rimuove le versioni senza fila nelle tabelle e negli indici e segna lo spazio disponibile per il riutilizzo futuro. Tuttavia, non restituirà lo spazio al sistema operativo, tranne nel caso speciale in cui una o più pagine alla fine di una tabella diventano completamente libere e può essere facilmente ottenuto un blocco esclusivo della tabella. Al contrario, VACUUM FULL compatta attivamente le tabelle scrivendo una nuova versione completa del file della tabella senza spazio morto. Ciò riduce al minimo le dimensioni della tabella, ma può richiedere molto tempo. Richiede inoltre spazio su disco aggiuntivo per la nuova copia della tabella, fino al completamento dell'operazione.

Come nota a margine, tutta la tua funzione sembra discutibile. Non sono sicuro di cosa stai cercando di testare, ma se vuoi creare dati, puoi usarlogenerate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);

Bene, questo spiega perché la tabella era ancora contrassegnata come consumando così tanti dati, ma perché aveva bisogno di tutto quello spazio in primo luogo? Dalla mia comprensione di MVCC, deve mantenere versioni distinte delle tuple aggiornate / inserite per la transazione, ma non dovrebbe avere bisogno di mantenere versioni separate per ogni iterazione del ciclo.
Nikhil N,

1
Ogni iterazione del ciclo sta generando nuove tuple.
Evan Carroll,

2
Giusto, ma la mia impressione è che MVCC non dovrebbe creare tuple per tutte le tuple che è stata modificata nel corso della transazione. Vale a dire, quando il primo INSERT esegue Postgres crea una singola tupla e aggiunge una singola nuova tupla per ogni AGGIORNAMENTO. Poiché gli AGGIORNAMENTI vengono eseguiti per ogni riga 500 volte e vi sono 10000 INSERTI, ciò equivale a 500 * 10000 righe = 5 M tuple al momento in cui la transazione viene eseguita. Ora questa è solo una stima, ma indipendentemente da 5M * dire 50 byte per tracciare ogni tupla ~ = 250 MB, che è MOLTO inferiore a 30 GB. Da dove viene tutto?
Nikhil N,

Anche riguardo a: funzione discutibile, sto provando a testare il comportamento di un indice quando i campi indicizzati si aggiornano molte volte ma in modo monoticamente crescente, producendo così un indice molto scarso, ma che viene sempre aggiunto su disco.
Nikhil N,

Sono confuso su ciò che pensi. Pensi che una riga aggiornata 18 volte in un ciclo sia una tupla o 1e8 tuple?
Evan Carroll,

3

I numeri effettivi dopo aver analizzato la funzione sono molto più grandi perché tutte le righe della tabella ottengono lo stesso valore che viene aggiornato più volte in ogni iterazione.

Quando lo eseguiamo con parametri ne m:

SELECT test_index(n, m);

ci sono minserimenti di riga e n * (m^2 + m) / 2aggiornamenti. Quindi, per n = 500e m = 10000, Postgres dovrà inserire solo 10K righe ma eseguire aggiornamenti di tupla ~ 25G (25 miliardi).

Considerando che una riga in Postgres ha un overhead di circa 24 byte, una tabella con una sola intcolonna avrà bisogno di 28 byte per riga più l'overhead della pagina. Quindi, affinché l'operazione finisca, avremmo bisogno di circa 700 GB più lo spazio per l'indice (che sarebbe anche qualche centinaia di GB).


analisi

Per testare la teoria, abbiamo creato un'altra tabella test_testcon una sola riga.

CREATE TABLE test_test (i int not null) ;
INSERT INTO test_test (i) VALUES (0);

Quindi aggiungiamo un trigger in testmodo che ogni aggiornamento aumenti il ​​contatore di 1. (Codice omesso). Quindi eseguiamo la funzione, con valori più piccoli n = 50e m = 100.

La nostra teoria prevede :

  • 100 inserti di riga,
  • Aggiornamenti tupla 250K (252500 = 50 * 100 * 101/2)
  • almeno 7 MB per la tabella su disco
  • (+ spazio per l'indice)

Test 1 ( testtabella originale , con indice)

    SELECT test_index(50, 100) ;

Dopo il completamento, controlliamo i contenuti della tabella:

x=# SELECT COUNT(*) FROM test ;
 count 
-------
   100
(1 row)

x=# SELECT i FROM test_test ;
   i    
--------
 252500
(1 row)

E utilizzo del disco (query in Dimensioni indice / statistiche di utilizzo in Manutenzione indice ):

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
test      | test_idx  |      100 | 8944 kB    | 5440 kB    | N      |           10001 |      505003
test_test |           |        1 | 8944 kB    |            | N      |                 |            

La testtabella ha utilizzato quasi 9 MB per la tabella e 5 MB per l'indice. Nota che la test_testtabella ha usato altri 9 MB! Questo è previsto poiché ha anche attraversato 250.000 aggiornamenti (il nostro secondo trigger ha aggiornato la singola riga di test_testogni aggiornamento di una riga test).

Nota anche il numero di scan sulla tabella test(10K) e le letture delle tuple (500K).

Test 2 ( testtabella senza indice)

Esattamente come sopra, tranne per il fatto che la tabella non ha indice.

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
 test        |        |      100 | 8944 kB    |            | N      |                 |            
 test_test   |        |        1 | 8944 kB    |            | N      |                 |            

Otteniamo le stesse dimensioni per l'utilizzo del disco della tabella e ovviamente nessun utilizzo del disco per gli indici. Il numero di scansioni sul tavolo testè zero e anche le tuple indicano.

Test 3 (con fattore di riempimento inferiore)

Provato con fillfactor 50 e il più basso possibile, 10. Nessun miglioramento. L'utilizzo del disco era quasi identico ai test precedenti (che utilizzavano il fattore di riempimento predefinito, 100 percento)

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.