PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Una panoramica delle colonne generate per PostgreSQL

PostgreSQL 12 include una nuova fantastica funzionalità, Generated Columns. La funzionalità non è esattamente una novità, ma la standardizzazione, la facilità d'uso, l'accessibilità e le prestazioni sono state migliorate in questa nuova versione.

Una colonna generata è una colonna speciale in una tabella che contiene dati generati automaticamente da altri dati all'interno della riga. Il contenuto della colonna generata viene popolato e aggiornato automaticamente ogni volta che i dati di origine, come qualsiasi altra colonna nella riga, vengono modificati.

Colonne generate in PostgreSQL 12+

Nelle versioni recenti di PostgreSQL, le colonne generate sono una funzionalità incorporata che consente alle istruzioni CREATE TABLE o ALTER TABLE di aggiungere una colonna in cui il contenuto viene "generato" automaticamente come risultato di un'espressione. Queste espressioni potrebbero essere semplici operazioni matematiche da altre colonne o una funzione immutabile più avanzata. Alcuni vantaggi dell'implementazione di una colonna generata nella progettazione di un database includono:

  • La possibilità di aggiungere una colonna a una tabella contenente dati calcolati senza la necessità di aggiornare il codice dell'applicazione per generare i dati per poi includerli nelle operazioni INSERT e UPDATE.
  • Ridurre il tempo di elaborazione su istruzioni SELECT estremamente frequenti che elaboreranno i dati al volo. Poiché l'elaborazione dei dati viene eseguita al momento di INSERT o UPDATE, i dati vengono generati una volta e le istruzioni SELECT devono solo recuperare i dati. In ambienti di lettura intensiva, questo può essere preferibile, a condizione che lo spazio di archiviazione dati aggiuntivo utilizzato sia accettabile.
  • Poiché le colonne generate vengono aggiornate automaticamente quando vengono aggiornati i dati di origine stessi, l'aggiunta di una colonna generata aggiungerà una presunta garanzia che i dati nella colonna generata siano sempre corretti.

In PostgreSQL 12, è disponibile solo il tipo "STORED" della colonna generata. In altri sistemi di database è disponibile una colonna generata con un tipo "VIRTUAL", che agisce più come una vista in cui il risultato viene calcolato al volo quando i dati vengono recuperati. Poiché la funzionalità è così simile alle viste e semplicemente scrivendo l'operazione in un'istruzione select, la funzionalità non è così vantaggiosa come la funzionalità "MEMORIZZATA" discussa qui, ma è possibile che le versioni future includano la funzionalità.

La creazione di una tabella con una colonna generata viene eseguita quando si definisce la colonna stessa. In questo esempio, la colonna generata è "profitto" e viene generata automaticamente sottraendo il prezzo_acquisto dalle colonne prezzo_vendita, quindi moltiplicato per la colonna quantità_venduta.

CREATE TABLE public.transactions (

    transactions_sid serial primary key,

    transaction_date timestamp with time zone DEFAULT now() NOT NULL,

    product_name character varying NOT NULL,

    purchase_price double precision NOT NULL,

    sale_price double precision NOT NULL,

    quantity_sold integer NOT NULL,

    profit double precision NOT NULL GENERATED ALWAYS AS  ((sale_price - purchase_price) * quantity_sold) STORED

);

In questo esempio, viene creata una tabella "transazioni" per tenere traccia di alcune transazioni di base e dei profitti di un bar immaginario. L'inserimento di dati in questa tabella mostrerà alcuni risultati immediati.

​severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('House Blend Coffee', 5, 11.99, 1);

severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('French Roast Coffee', 6, 12.99, 4);

severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('BULK: House Blend Coffee, 10LB', 40, 100, 6);



severalnines=# SELECT * FROM public.transactions;

 transactions_sid |       transaction_date |          product_name | purchase_price | sale_price | quantity_sold | profit

------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------

                1 | 2020-02-28 04:50:06.626371+00 | House Blend Coffee             | 5 | 11.99 | 1 | 6.99

                2 | 2020-02-28 04:50:53.313572+00 | French Roast Coffee            | 6 | 12.99 | 4 | 27.96

                3 | 2020-02-28 04:51:08.531875+00 | BULK: House Blend Coffee, 10LB |             40 | 100 | 6 | 360

Quando si aggiorna la riga, la colonna generata si aggiornerà automaticamente:

​severalnines=# UPDATE public.transactions SET sale_price = 95 WHERE transactions_sid = 3;

UPDATE 1



severalnines=# SELECT * FROM public.transactions WHERE transactions_sid = 3;

 transactions_sid |       transaction_date |          product_name | purchase_price | sale_price | quantity_sold | profit

------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------

                3 | 2020-02-28 05:55:11.233077+00 | BULK: House Blend Coffee, 10LB |             40 | 95 | 6 | 330

Ciò assicurerà che la colonna generata sia sempre corretta, senza necessità di logica aggiuntiva sul lato applicazione.

NOTA:le colonne generate non possono essere INSERITE o AGGIORNATE direttamente e qualsiasi tentativo in tal senso restituirà un ERRORE:

severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold, profit) VALUES ('BULK: House Blend Coffee, 10LB', 40, 95, 1, 95);

ERROR:  cannot insert into column "profit"

DETAIL:  Column "profit" is a generated column.



severalnines=# UPDATE public.transactions SET profit = 330 WHERE transactions_sid = 3;

ERROR:  column "profit" can only be updated to DEFAULT

DETAIL:  Column "profit" is a generated column.

Colonne generate su PostgreSQL 11 e precedenti

Anche se le colonne generate integrate sono nuove nella versione 12 di PostgreSQL, la funzionalità può ancora essere raggiunta nelle versioni precedenti, necessita solo di un po' più di configurazione con stored procedure e trigger. Tuttavia, anche con la possibilità di implementarlo su versioni precedenti, oltre alle funzionalità aggiuntive che possono essere vantaggiose, è più difficile ottenere una rigorosa conformità all'input dei dati e dipende dalle funzionalità PL/pgSQL e dall'ingegnosità della programmazione.

BONUS:l'esempio seguente funzionerà anche su PostgreSQL 12+, quindi se la funzionalità aggiunta con una combinazione funzione/trigger è necessaria o desiderata nelle versioni più recenti, questa opzione è un valido fallback e non limitato a solo versioni precedenti alla 12. 

Sebbene questo sia un modo per farlo nelle versioni precedenti di PostgreSQL, ci sono un paio di vantaggi aggiuntivi di questo metodo: 

  • Poiché l'imitazione della colonna generata utilizza una funzione, è possibile utilizzare calcoli più complessi. Le colonne generate nella versione 12 richiedono operazioni IMMUTABLE, ma un'opzione trigger/funzione potrebbe utilizzare un tipo di funzione STABILE o VOLATILE con maggiori possibilità e probabilmente prestazioni minori di conseguenza.
  • L'utilizzo di una funzione che ha l'opzione di essere STABILE o VOLATILE apre anche la possibilità di AGGIORNARE colonne aggiuntive, AGGIORNARE altre tabelle o persino creare nuovi dati tramite INSERTI in altre tabelle. (Tuttavia, sebbene queste opzioni di trigger/funzione siano molto più flessibili, ciò non significa che manchi una vera "colonna generata", poiché fa ciò che è pubblicizzato con prestazioni ed efficienza maggiori.)

In questo esempio, viene impostato un trigger/funzione per simulare la funzionalità di una colonna generata da PostgreSQL 12+, insieme a due parti che sollevano un'eccezione se un INSERT o UPDATE tenta di modificare la colonna generata . Questi possono essere omessi, ma se vengono omessi, le eccezioni non verranno sollevate e i dati effettivi INSERTed o UPDATEd verranno eliminati silenziosamente, il che generalmente non sarebbe consigliato.

Il trigger stesso è impostato per essere eseguito PRIMA, il che significa che l'elaborazione avviene prima dell'inserimento effettivo e richiede il RETURN di NEW, che è il RECORD che viene modificato per contenere il nuovo valore di colonna generato. Questo esempio specifico è stato scritto per essere eseguito su PostgreSQL versione 11.

CREATE TABLE public.transactions (

    transactions_sid serial primary key,

    transaction_date timestamp with time zone DEFAULT now() NOT NULL,

    product_name character varying NOT NULL,

    purchase_price double precision NOT NULL,

    sale_price double precision NOT NULL,

    quantity_sold integer NOT NULL,

    profit double precision NOT NULL

);



CREATE OR REPLACE FUNCTION public.generated_column_function()

 RETURNS trigger

 LANGUAGE plpgsql

 IMMUTABLE

AS $function$

BEGIN



    -- This statement mimics the ERROR on built in generated columns to refuse INSERTS on the column and return an ERROR.

    IF (TG_OP = 'INSERT') THEN

        IF (NEW.profit IS NOT NULL) THEN

            RAISE EXCEPTION 'ERROR:  cannot insert into column "profit"' USING DETAIL = 'Column "profit" is a generated column.';

        END IF;

    END IF;



    -- This statement mimics the ERROR on built in generated columns to refuse UPDATES on the column and return an ERROR.

    IF (TG_OP = 'UPDATE') THEN

        -- Below, IS DISTINCT FROM is used because it treats nulls like an ordinary value. 

        IF (NEW.profit::VARCHAR IS DISTINCT FROM OLD.profit::VARCHAR) THEN

            RAISE EXCEPTION 'ERROR:  cannot update column "profit"' USING DETAIL = 'Column "profit" is a generated column.';

        END IF;

    END IF;



    NEW.profit := ((NEW.sale_price - NEW.purchase_price) * NEW.quantity_sold);

    RETURN NEW;



END;

$function$;




CREATE TRIGGER generated_column_trigger BEFORE INSERT OR UPDATE ON public.transactions FOR EACH ROW EXECUTE PROCEDURE public.generated_column_function();

NOTA:assicurati che la funzione disponga delle autorizzazioni/proprietà corrette per essere eseguita dagli utenti dell'applicazione desiderati.

Come visto nell'esempio precedente, i risultati sono gli stessi nelle versioni precedenti con una soluzione di funzione/trigger:

​severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('House Blend Coffee', 5, 11.99, 1);

severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('French Roast Coffee', 6, 12.99, 4);

severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('BULK: House Blend Coffee, 10LB', 40, 100, 6);



severalnines=# SELECT * FROM public.transactions;

 transactions_sid |       transaction_date |          product_name | purchase_price | sale_price | quantity_sold | profit

------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------

                1 | 2020-02-28 00:35:14.855511-07 | House Blend Coffee             | 5 | 11.99 | 1 | 6.99

                2 | 2020-02-28 00:35:21.764449-07 | French Roast Coffee            | 6 | 12.99 | 4 | 27.96

                3 | 2020-02-28 00:35:27.708761-07 | BULK: House Blend Coffee, 10LB |             40 | 100 | 6 | 360

L'aggiornamento dei dati sarà simile.

​severalnines=# UPDATE public.transactions SET sale_price = 95 WHERE transactions_sid = 3;

UPDATE 1



severalnines=# SELECT * FROM public.transactions WHERE transactions_sid = 3;

 transactions_sid |       transaction_date |          product_name | purchase_price | sale_price | quantity_sold | profit

------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------

                3 | 2020-02-28 00:48:52.464344-07 | BULK: House Blend Coffee, 10LB |             40 | 95 | 6 | 330

Infine, il tentativo di INSERT o AGGIORNAMENTO nella colonna speciale stessa risulterà in un ERRORE:

​severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold, profit) VALUES ('BULK: House Blend Coffee, 10LB', 40, 95, 1, 95);

ERROR:  ERROR: cannot insert into column "profit"

DETAIL:  Column "profit" is a generated column.

CONTEXT:  PL/pgSQL function generated_column_function() line 7 at RAISE



severalnines=# UPDATE public.transactions SET profit = 3030 WHERE transactions_sid = 3;

ERROR:  ERROR: cannot update column "profit"

DETAIL:  Column "profit" is a generated column.

CONTEXT:  PL/pgSQL function generated_column_function() line 15 at RAISE

In questo esempio, agisce in modo diverso rispetto alla prima configurazione della colonna generata in un paio di modi che dovrebbero essere notati:

  • Se si tenta di aggiornare la 'colonna generata' ma non viene trovata alcuna riga da aggiornare, restituirà un successo con un risultato "UPDATE 0", mentre una colonna generata effettiva nella versione 12 continuerà comunque restituisce un ERRORE, anche se non viene trovata alcuna riga in UPDATE.
  • Quando si tenta di aggiornare la colonna del profitto, che "dovrebbe" restituire sempre un ERRORE, se il valore specificato è uguale al valore "generato" correttamente, avrà esito positivo. In definitiva i dati sono corretti, tuttavia, se si desidera restituire un ERROR se la colonna è specificata.

Documentazione e comunità PostgreSQL

La documentazione ufficiale per le colonne generate da PostgreSQL si trova sul sito Web ufficiale di PostgreSQL. Controlla quando vengono rilasciate nuove versioni principali di PostgreSQL per scoprire nuove funzionalità quando vengono visualizzate.

Sebbene le colonne generate in PostgreSQL 12 siano abbastanza semplici, l'implementazione di funzionalità simili nelle versioni precedenti potrebbe diventare molto più complicata. La comunità di PostgreSQL è una comunità molto attiva, massiccia, mondiale e multilingue dedicata ad aiutare le persone di qualsiasi livello di esperienza con PostgreSQL a risolvere problemi e creare nuove soluzioni come questa.

  • IRC :Freenode ha un canale molto attivo chiamato #postgres, dove gli utenti si aiutano a vicenda a capire concetti, correggere errori o trovare altre risorse. Un elenco completo dei canali freenode disponibili per tutto ciò che riguarda PostgreSQL può essere trovato sul sito Web PostgreSQL.org.
  • Mailing List :PostgreSQL ha una manciata di mailing list che possono essere unite. Domande/problemi di forma più lunga possono essere inviati qui e possono raggiungere molte più persone di IRC in qualsiasi momento. Gli elenchi possono essere trovati sul sito Web di PostgreSQL e gli elenchi pgsql-general o pgsql-admin sono buone risorse.
  • Slack :Anche la comunità di PostgreSQL ha prosperato su Slack e può essere raggiunta su postgresteam.slack.com. Proprio come IRC, una comunità attiva è disponibile per rispondere alle domande e impegnarsi in tutto ciò che riguarda PostgreSQL.