PostgreSQL 9.5: partecipazione delle tabelle esterne all’ereditarietà

Dalla versione 9.1, PostgreSQL supporta il concetto di ereditarietà applicato alle tabelle. Questo era possibile esclusivamente per le tabelle locali. Con la nuova feature le tabelle esterne potranno partecipare come membri all’ereditarietà. PostgreSQL 9.5 offre, inoltre, la possibilità di effettuare lo sharding senza bisogno di estensioni aggiuntive.


tab-esterne-ereditarieta

Ereditarietà in PostgreSQL

Vediamo innanzitutto il significato di ereditarietà all’interno del contesto di un database relazionale.

L’ereditarietà permette a una tabella, detta “figlia”, di acquisire tutte le caratteristiche di una tabella già esistente, detta “padre”. In questo modo la tabella “figlia” possiede la stessa struttura e gli stessi vincoli della tabella “padre” in aggiunta alle colonne proprie della sua struttura. Effettuando una query sulla tabella “padre” otteniamo come risultato i record combinati della tabella “padre” e “figlia”, interrogando la tabella “figlia” non è, però, possibile ottenere i record della tabella da cui eredita. Questo fornisce alcune interessanti opzioni quando si tratta di progettare un database.

Piccolo esempio di ereditarietà

Supponiamo di avere una tabella che contenga dati anagrafici di attori, chiamata “actors”, e una tabella che contenga i dati anagrafici degli attori vincitori di un premio Oscar, chiamata “oscar_actors”. È facile dedurre che tutti gli attori presenti nella seconda tabella appartengono legittimamente anche alla prima, ma non il contrario.

Le due tabelle sono state create con i seguenti statement:

CREATE TABLE actors (
    id serial primary key,
    first_name text not null,
    last_name text not null
);

CREATE TABLE oscar_actors (
    id serial primary key,
    first_name text not null,
    last_name text not null,
    oscar_prize text not null
    );

Nota Bene: Tutti gli esempi presenti in questo articolo usano come utente di riferimento “postgres”. Questa è una scelta effettuata per motivi di semplicità e per non deviare dall’argomento trattato ed è una pessima scelta in fatto di sicurezza del vostro database. Vi suggerisco di utilizzare un utente specifico per la vostra applicazione.

Per semplificare la consultazione delle due tabelle possiamo creare una vista che le metta in correlazione tramite una query che usa l’operatore ‘UNION’. Una soluzione alternativa prevede che le tabelle siano strutturate in maniera diversa, usando l’ereditarietà.

CREATE TABLE actors (
    id serial primary key,
    first_name text not null,
    last_name text not null
);

CREATE TABLE oscar_actors (
    oscar_prize text not nul
    ) INHERITS (actors);

Come spiegato, la tabella “oscar_actors” eredita tutte le colonne della tabella “actors”. La tabella “oscar_actors” possiede inoltre la colonna aggiuntiva “oscar_prize” che mostra quale è il premio oscar vinto dall’attore.

Eseguendo una query sulla tabella “actors”, per esempio cercando tutti gli attori di nome Michael presenti nel nostro set di dati, il risultato è la combinazione dei contenuti delle due tabelle, “padre” e “figlia”.

Interrogando la tabella “oscar_actors”, invece, otterremmo i nomi di tutti i Michael che hanno vinto un premio Oscar.

Ereditarietà e tabelle esterne

È possibile usare l’ereditarietà per “partizionare” una tabella in maniera “orizzontale”, ovvero per suddividere una tabella in sottotabelle più piccole. Ogni sottotabella contiene un insieme di record suddivisi secondo un criterio comune (ad esempio il mese di inserimento nel caso di una tabella storicizzata).

L’inserimento delle tabelle esterne in questo scenario rende tutto ancora più interessante!

Le varie sottotabelle possono risiedere tutte su database separati, rendendo possibile l’implementazione di una tecnica di partizionamento nota come sharding.

Implementazione dello sharding tramite tabelle esterne

Vediamo ora come è possibile attuare lo sharding tramite tabelle esterne. Supponiamo di avere un database di nome “master” 4 database di nome “db1”, “db2”, “db3” e “db4”. Su tutti i database creiamo una tabella con questa struttura:

CREATE TABLE products (
    id serial PRIMARY KEY,
    name TEXT NOT NULL);

Connettiamoci quindi al database “master”, creiamo una tabella “products” e produciamo quindi 4 tabelle esterne che ereditano da questa:

CREATE TABLE products (id serial PRIMARY KEY, name TEXT NOT NUll);
CREATE EXTENSION postgres_fdw;
CREATE SERVER db1 FOREIGN DATA WRAPPER postgres_fdw OPTIONS( dbname 'db1' );
CREATE SERVER db2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS( dbname 'db2' );
CREATE SERVER db3 FOREIGN DATA WRAPPER postgres_fdw OPTIONS( dbname 'db3' );
CREATE SERVER db4 FOREIGN DATA WRAPPER postgres_fdw OPTIONS( dbname 'db4' );
CREATE USER MAPPING FOR POSTGRES SERVER db1 OPTIONS ( user 'postgres' );
CREATE USER MAPPING FOR POSTGRES SERVER db2 OPTIONS ( user 'postgres' );
CREATE USER MAPPING FOR POSTGRES SERVER db3 OPTIONS ( user 'postgres' );
CREATE USER MAPPING FOR POSTGRES SERVER db4 OPTIONS ( user 'postgres' );
CREATE FOREIGN TABLE products_db1 () INHERITS (products) SERVER db1 OPTIONS ( table_name 'products' );
CREATE FOREIGN TABLE products_db2 () INHERITS (products) SERVER db2 OPTIONS ( table_name 'products' );
CREATE FOREIGN TABLE products_db3 () INHERITS (products) SERVER db3 OPTIONS ( table_name 'products' );
CREATE FOREIGN TABLE products_db4 () INHERITS (products) SERVER db4 OPTIONS ( table_name 'products' );

Creiamo quindi 4 sequenze su “master” per l’assegnazione degli id alle tabelle esterne.

CREATE SEQUENCE products_id_seq_db1 INCREMENT BY 4 RESTART WITH 1;
CREATE SEQUENCE products_id_seq_db2 INCREMENT BY 4 RESTART WITH 2;
CREATE SEQUENCE products_id_seq_db3 INCREMENT BY 4 RESTART WITH 3;
CREATE SEQUENCE products_id_seq_db4 INCREMENT BY 4 RESTART WITH 4;
ALTER FOREIGN TABLE products_db1 ALTER COLUMN id SET DEFAULT nextval('products_id_seq_db1');
ALTER FOREIGN TABLE products_db2 ALTER COLUMN id SET DEFAULT nextval('products_id_seq_db2');
ALTER FOREIGN TABLE products_db3 ALTER COLUMN id SET DEFAULT nextval('products_id_seq_db3');
ALTER FOREIGN TABLE products_db4 ALTER COLUMN id SET DEFAULT nextval('products_id_seq_db4');

Le sequenze degli id delle varie tabelle sono state alterate in modo che abbiano un incremento di 4 e uno sfasamento nel numero di partenza. Gli id generati per le 4 tabelle sono quindi diversi.

Inseriamo, quindi, 4 prodotti direttamente nelle tabelle esterne:

INSERT INTO products_db1 (name) VALUES ('playstation');
INSERT INTO products_db2 (name) VALUES ('xbox');
INSERT INTO products_db3 (name) VALUES ('wii');
INSERT INTO products_db4 (name) VALUES ('saturn');

Facendo una SELECT sulla tabella “products” troviamo tutti i prodotti che abbiamo appena inserito:

master=# SELECT * FROM products;
 id |    name
----+-------------
  1 | playstation
  2 | xbox
  3 | wii
  4 | saturn
(4 rows)

Eseguendo un EXPLAIN ANALIZE possiamo vedere l’esecuzione delle scansioni sulle tabelle esterne:

master=# EXPLAIN ANALYZE SELECT * FROM products;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..603.80 rows=5461 width=36) (actual time=0.693..2.682 rows=4 loops=1)
   ->  Seq Scan on products  (cost=0.00..0.00 rows=1 width=36) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Foreign Scan on products_db1  (cost=100.00..150.95 rows=1365 width=36) (actual time=0.689..0.690 rows=1 loops=1)
   ->  Foreign Scan on products_db2  (cost=100.00..150.95 rows=1365 width=36) (actual time=0.668..0.669 rows=1 loops=1)
   ->  Foreign Scan on products_db3  (cost=100.00..150.95 rows=1365 width=36) (actual time=0.665..0.666 rows=1 loops=1)
   ->  Foreign Scan on products_db4  (cost=100.00..150.95 rows=1365 width=36) (actual time=0.650..0.652 rows=1 loops=1)
 Planning time: 0.316 ms
 Execution time: 4.601 ms
(8 rows)

è possibile modificare anche i record delle tabelle figlie, per esempio con facendo una UPDATE:

master=# EXPLAIN ANALYZE UPDATE products SET name = 'wii-u' WHERE ID = 4 AND name LIKE 'wii';
                                                     QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
 Update on products  (cost=0.00..587.52 rows=5 width=10) (actual time=3.387..3.387 rows=0 loops=1)
   Update on products
   Foreign Update on products_db1
   Foreign Update on products_db2
   Foreign Update on products_db3
   Foreign Update on products_db4
   ->  Seq Scan on products  (cost=0.00..2.91 rows=1 width=10) (actual time=0.018..0.018 rows=0 loops=1)
         Filter: ((name ~~ 'wii'::text) AND (id = 4))
         Rows Removed by Filter: 1
   ->  Foreign Scan on products_db1  (cost=100.00..146.15 rows=1 width=10) (actual time=0.878..0.878 rows=0 loops=1)
   ->  Foreign Scan on products_db2  (cost=100.00..146.15 rows=1 width=10) (actual time=0.861..0.861 rows=0 loops=1)
   ->  Foreign Scan on products_db3  (cost=100.00..146.15 rows=1 width=10) (actual time=0.759..0.759 rows=0 loops=1)
   ->  Foreign Scan on products_db4  (cost=100.00..146.15 rows=1 width=10) (actual time=0.866..0.866 rows=0 loops=1)
 Planning time: 0.786 ms
 Execution time: 5.718 ms
(15 rows)

Lo sharding non è ancora completo. Ogni volta che facciamo una query viene eseguita una scansione di tutte le tabelle, anche quando non sarebbe necessario.

master=# EXPLAIN ANALYZE SELECT * FROM products WHERE name LIKE 'xbox';
                                                     QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..508.81 rows=29 width=36) (actual time=1.478..2.845 rows=1 loops=1)
   ->  Seq Scan on products  (cost=0.00..0.00 rows=1 width=36) (actual time=0.002..0.002 rows=0 loops=1)
         Filter: (name ~~ 'xbox'::text)
   ->  Foreign Scan on products_db1  (cost=100.00..127.20 rows=7 width=36) (actual time=0.740..0.740 rows=0 loops=1)
   ->  Foreign Scan on products_db2  (cost=100.00..127.20 rows=7 width=36) (actual time=0.734..0.736 rows=1 loops=1)
   ->  Foreign Scan on products_db3  (cost=100.00..127.20 rows=7 width=36) (actual time=0.631..0.631 rows=0 loops=1)
   ->  Foreign Scan on products_db4  (cost=100.00..127.20 rows=7 width=36) (actual time=0.733..0.733 rows=0 loops=1)
 Planning time: 0.459 ms
 Execution time: 4.878 ms
(9 rows)

Il problema è risolvibile affidandosi ai meccanismi di partizionamento già presenti in PostgreSQL. Aggiungiamo quindi alle tabelle esterne un constraint di tipo CHECK, definendo così quali siano le chiavi che appartengono a quella partizione

ALTER FOREIGN TABLE products_db1 ADD CHECK ( id % 4 = 1 );
ALTER FOREIGN TABLE products_db2 ADD CHECK ( id % 4 = 2 );
ALTER FOREIGN TABLE products_db3 ADD CHECK ( id % 4 = 3 );
ALTER FOREIGN TABLE products_db4 ADD CHECK ( id % 4 = 0 );

In questo caso abbiamo scelto come discriminante un criterio numerico basato sull’operazione di modulo 4 (perché 4 sono le tabelle) del campo id.

master=# EXPLAIN ANALYZE SELECT * FROM products WHERE id = 4 AND (id % 4) = (4 % 4) ;
                                                     QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..137.13 rows=2 width=36) (actual time=0.913..0.915 rows=1 loops=1)
   ->  Seq Scan on products  (cost=0.00..3.22 rows=1 width=36) (actual time=0.013..0.013 rows=0 loops=1)
         Filter: ((id = 4) AND ((id % 4) = 0))
         Rows Removed by Filter: 1
   ->  Foreign Scan on products_db4  (cost=100.00..133.91 rows=1 width=36) (actual time=0.900..0.901 rows=1 loops=1)
 Planning time: 0.525 ms
 Execution time: 1.591 ms
(7 rows)

PostgreSQL in questo modo identifica immediatamente a quale tabella appartiene l’id che stiamo cercando e evita di fare la scansione delle altre.

Gli inserimenti sono ancora non “automatici” ma basta creare un trigger BEFORE INSERT per la tabella users che applichi gli stessi criteri applicati per i vincoli usati per le tabelle esterne alle linee in inserimento.

Conclusioni

La partecipazione delle tabelle esterne all’ereditarietà è una feature di grande importanza. Finora per poter partizionare orizzontalmente le tabelle era necessario ricorrere a strumenti esterni a PostgreSQL come Pg_shard o PL/Proxy. Dalla 9.5 tutto quello che serve per costruire l’architettura per lo sharding è presente all’interno del core di PostgreSQL.

Sono molteplici i benefici che è possibile trarre da una attenta pianificazione del partizionamento delle tabelle. È possibile migliorare le prestazioni delle query su tabelle di grandi dimensioni, riducendone il set di dati da analizzare. Inoltre, ripartendo il carico delle dimensioni di alcune tabelle molto grandi su diverse istanze di PostgreSQL, si riduce lo spazio occupato sul disco, non solo dal database ma anche dai suoi backup, rendendo più rapide se necessario le operazioni di ripristino dei cluster.

This Post Has 0 Comments

Leave A Reply