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

Comprensione dei vincoli di controllo in PostgreSQL

La gestione dei dati è una grande sfida. Mentre il nostro mondo gira, i dati continuano a essere diffusi, abbondanti e intensivi. Pertanto, dobbiamo adottare misure per gestire l'afflusso.

Convalida di ogni singolo dato 'a mano ' 24 ore su 24 è semplicemente impraticabile. Che sogno fantastico. Ma, in fondo, è proprio questo. Un sogno. I dati errati sono dati errati. Non importa come lo tagli o lo tagli (gioco di parole). È un problema fin dall'inizio, che porta a ancora più problemi.

I database moderni gestiscono gran parte del sollevamento pesante per noi. Molti forniscono soluzioni integrate per assistere nella gestione di questa particolare area di dati.

Un modo sicuro per controllare i dati inseriti nella colonna di una tabella è con un tipo di dati. Hai bisogno di una colonna con numeri decimali, con un conteggio totale di 4 cifre, con 2 di quelli dopo il decimale?

Cosa certa! Nessun problema.

NUMERIC(4,2), un'opzione praticabile, sta proteggendo quella colonna come un watchdog. I valori del testo dei caratteri possono scivolare lì dentro? Non una palla di neve.

PostgreSQL offre una moltitudine di tipi di dati. È probabile che ne esista già uno per soddisfare le tue esigenze. In caso contrario, puoi crearne uno tuo. (Vedi:PostgreSQL CREATE TYPE)

Tuttavia, i tipi di dati da soli non sono sufficienti. Non si può garantire che i requisiti più specifici siano coperti e siano conformi a una strutturazione così ampia. Durante la progettazione di uno schema sono generalmente richieste regole di conformità e una sorta di "standard".

Supponiamo che nella stessa colonna NUMERIC(4,2) desideri solo valori maggiori di 25,25 ma inferiori a 74,33? Se viene memorizzato il valore 88.22, il tipo di dati non è errato. Consentendo 4 cifre totali, con 2 al massimo dopo il decimale, sta facendo il suo lavoro. Dai la colpa altrove.

Come si vince su questo fronte quando si tratta di controllare i dati consentiti nel nostro database? La coerenza dei dati è della massima priorità ed è parte integrante di qualsiasi soluzione di dati audio. Nella (non) possibilità che tu abbia controllato i dati raccolti dall'inizio della loro origine, la coerenza sarebbe probabilmente un problema minore.

Ma un mondo perfetto esiste (forse) solo in uno di quei tanti romanzi fantasy che amo leggere.

Purtroppo i dati incompleti, incoerenti e "sporchi" sono caratteristiche e realtà troppo comuni presenti in un campo incentrato sul database.

Tuttavia, non tutto è perduto nel destino, perché abbiamo vincoli Check per mitigare questi problemi. Per queste regole specifiche, dobbiamo mettere in atto, per necessità, che ci assicuri di gestire e archiviare solo dati coerenti. Inserendo tali specifiche nel database, possiamo ridurre al minimo l'impatto che i dati incoerenti hanno sui nostri obiettivi aziendali e sulle soluzioni che portano avanti.

Cos'è un vincolo? - Una definizione di alto livello

In questo contesto, un vincolo è un tipo di regola o restrizione posta su una colonna di una tabella del database. Questa specificità richiede che i dati in entrata debbano soddisfare i requisiti impostati prima di essere archiviati. Tali requisiti tendono ad essere coniati "professionalmente" (e spesso lo sono) come regole commerciali . Questo si riduce a un test booleano di convalida per la verità. Se i dati passano (vero), vengono archiviati. In caso contrario, nessuna voce (falso).

Vincoli disponibili in PostgreSQL

Al momento della scrittura, la documentazione di PostgreSQL elenca 6 categorie di vincoli.

Sono:

  • Verifica vincoli
  • Vincoli non nulli
  • Vincoli unici
  • Chiavi primarie
  • Chiavi estere
  • Vincoli di esclusione

Verifica vincoli

Un semplice esempio per una colonna INTEGER sarebbe non consentire valori maggiori di, diciamo, 100.

learning=> CREATE TABLE no_go(id INTEGER CHECK (id < 100));
CREATE TABLE
learning=> INSERT INTO no_go(id) VALUES(101);
ERROR: new row for relation "no_go" violates check constraint "no_go_id_check"
DETAIL: Failing row contains (101).

Come visto sopra, i tentativi di INSERT di qualsiasi valore che violano il vincolo Check hanno esito negativo.

Controlla i vincoli non solo monitorano le colonne durante INSERT, anche le istruzioni UPDATE (e altre, ad esempio \copy e COPY) devono rispettare le restrizioni.

Supponiamo che la tabella no_go abbia questo valore:

learning=> TABLE no_go;
id 
----
55
(1 row)

Anche un UPDATE sul valore della colonna id su uno che non è conforme al vincolo Check ha esito negativo:

learning=> UPDATE no_go SET id = 155
learning-> WHERE id = 55;
ERROR: new row for relation "no_go" violates check constraint "no_go_id_check"
DETAIL: Failing row contains (155).

I vincoli di controllo devono avere un senso per il tipo di dati della colonna di destinazione. Non è valido tentare e vincolare una colonna INTEGER per vietare la memorizzazione di valori di testo poiché il tipo di dati stesso non lo consentirà.

Guarda questo esempio in cui provo a imporre quel tipo di vincolo Check durante la creazione della tabella:

learning=> CREATE TABLE num_try(id INTEGER CHECK(id IN ('Bubble', 'YoYo', 'Jack-In-The-Box')));
ERROR: invalid input syntax for integer: "Bubble"

Vita senza vincoli di controllo

Un vecchio detto che ho sentito che risuona con me è:"Non ti manca l'acqua finché il pozzo non si asciuga . "

Senza i vincoli di Check, possiamo sicuramente relazionarci perché il loro notevole vantaggio è più apprezzato quando devi farne a meno.

Prendi questo esempio...

Per iniziare abbiamo questa tabella e dati che rappresentano i materiali della superficie del sentiero:

learning=> SELECT * FROM surface_material;
surface_id | material 
------------+--------------
101 | Gravel
202 | Grass
303 | Dirt
404 | Turf
505 | Concrete
606 | Asphalt
707 | Clay
808 | Polyurethane
(8 rows)

E questa tabella con i nomi delle tracce e il proprio surface_id:

learning=> SELECT * FROM trails;
id | name | surface_id 
----+-----------------+------------
1 | Dusty Storm | 303
2 | Runners Trip | 808
3 | Pea Gravel Pass | 101
4 | Back 40 Loop | 404
(4 rows)

Vogliamo assicurarci che le tracce della tabella contengano solo surface_id per i valori corrispondenti nella tabella surface_material.

Si si lo so. Stai urlando contro di me.

"Non è possibile risolvere questo problema con un CHIAVE STRANIERA?!?"

Sì, può. Ma lo sto usando per dimostrare un uso generico, insieme a una trappola da sapere (menzionata più avanti nel post).

Senza i vincoli Check, puoi ricorrere a un TRIGGER e impedire la memorizzazione di valori incoerenti.

Ecco un esempio grezzo (ma funzionante):

CREATE OR REPLACE FUNCTION check_me()
RETURNS TRIGGER AS
$$
BEGIN
IF NEW.surface_id NOT IN (SELECT surface_id FROM surface_material)
THEN Raise Exception '% is not allowed for surface id', NEW.surface_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE PLpgSQL;
CREATE TRIGGER check_me BEFORE INSERT OR UPDATE ON trails
FOR EACH ROW EXECUTE PROCEDURE check_me();

I tentativi di INSERT di un valore che non ha un surface_id corrispondente nelle tracce della tabella, falliscono:

learning=> INSERT INTO trails(name, surface_id)
learning-> VALUES ('Tennis Walker', 110);
ERROR: 110 is not allowed for surface id
CONTEXT: PL/pgSQL function check_me() line 4 at RAISE

I risultati della query di seguito confermano l'"offesa ' il valore non è stato memorizzato:

learning=> SELECT * FROM trails;
id | name | surface_id 
----+-----------------+------------
1 | Dusty Storm | 303
2 | Runners Trip | 808
3 | Pea Gravel Pass | 101
4 | Back 40 Loop | 404
(4 rows)

Questo è sicuramente molto lavoro per proibire valori indesiderati.

Implementiamo nuovamente questo requisito con un vincolo Check.

Poiché non è possibile utilizzare una sottoquery (ecco perché ho utilizzato l'esempio sopra) nella definizione del vincolo Check effettiva, i valori devono essere hardcoded .

Per un tavolino, o un esempio banale come quello presentato qui, va bene. In altri scenari, incorporando più valori, potresti essere più utile per cercare una soluzione alternativa.

learning=> ALTER TABLE trails ADD CONSTRAINT t_check CHECK (surface_id IN (101, 202, 303, 404, 505, 606, 707, 808));
ALTER TABLE

Qui ho chiamato il vincolo Check t_check rispetto a lasciare che il sistema lo denomini.

(Nota:il precedente check_me() FUNZIONE e accompagnamento I TRIGGER sono stati rilasciati (non mostrati) prima di eseguire quanto segue INSER.)

learning=> INSERT INTO trails(name, surface_id)
VALUES('Tennis Walker', 110);
ERROR: new row for relation "trails" violates check constraint "t_check"
DETAIL: Failing row contains (7, Tennis Walker, 110).

Guarderesti com'è stato facile! Nessun TRIGGER e FUNZIONE necessari.

I vincoli di controllo rendono questo tipo di lavoro facile.

Vuoi diventare furbo nella definizione del vincolo di controllo?

Puoi.

Supponiamo di aver bisogno di una tabella che elenchi i percorsi che sono un po' più gentili per chi ha caviglie e ginocchia sensibili. Non sono richieste superfici dure qui.

Vuoi assicurarti che qualsiasi sentiero escursionistico o tracciato elencato nella tabella nice_trail abbia una superficie di materiale di "ghiaia" o "sterrato".

Questo vincolo Check gestisce quel requisito senza problemi:

learning=> CREATE TABLE nice_trail(id SERIAL PRIMARY KEY,
learning(> name TEXT, mat_surface_id INTEGER CONSTRAINT better_surface CHECK(id IN (101, 303))); 
CREATE TABLE

Funziona perfettamente.

Ma che ne dici di una FUNZIONE che restituisce entrambi gli ID necessari per far funzionare il Check? È consentita una FUNZIONE nella definizione del vincolo di controllo?

Sì, uno può essere incorporato.

Ecco un esempio funzionante.

In primo luogo, il corpo della funzione e la definizione:

CREATE OR REPLACE FUNCTION easy_hike(id INTEGER)
RETURNS BOOLEAN AS
$$
BEGIN
IF id IN (SELECT surface_id FROM surface_material WHERE material IN ('Gravel', 'Dirt'))
THEN RETURN true;
ELSE RETURN false;
END IF;
END;
$$ LANGUAGE PLpgSQL;

Nota in questa istruzione CREATE TABLE, definisco il vincolo Check in 'table ' mentre in precedenza ho fornito solo esempi nella 'colonna ' livello.

I vincoli di controllo definiti a livello di tabella sono perfettamente validi:

learning=> CREATE TABLE nice_trail(nt_id SERIAL PRIMARY KEY,
learning(> name TEXT, mat_surface_id INTEGER,
learning(> CONSTRAINT better_surface_check CHECK(easy_hike(mat_surface_id)));
CREATE TABLE

Questi inserti sono tutti buoni:

learning=> INSERT INTO nice_trail(name, mat_surface_id)
learning-> VALUES ('Smooth Rock Loop', 101), ('High Water Bluff', 303);
INSERT 0 2

Ora arriva un INSERT per un sentiero che non soddisfa la restrizione sulla colonna mat_surface_id:

learning=> INSERT INTO nice_trail(name, mat_surface_id)
learning-> VALUES('South Branch Fork', 404);
ERROR: new row for relation "nice_trail" violates check constraint "better_surface_check"
DETAIL: Failing row contains (3, South Branch Fork, 404).

La nostra chiamata FUNCTION nella definizione del vincolo di controllo funziona come previsto, limitando i valori di colonna indesiderati.

Fumo e specchi?

È tutto come sembra con i vincoli Check? Tutto in bianco e nero? Nessuna facciata in primo piano?

Un esempio degno di nota.

Abbiamo una semplice tabella in cui vogliamo che il valore DEFAULT sia 10 per la colonna INTEGER solitaria presente:

learning=> CREATE TABLE surprise(id INTEGER DEFAULT 10, CHECK (id <> 10));
CREATE TABLE

Ma ho anche incluso un vincolo Check che vieta un valore di 10, definendo id non può essere uguale a quel numero.

Quale vincerà la giornata? Il vincolo DEFAULT o Check?

Potresti essere sorpreso di sapere qual è.

lo ero.

Un INSERT arbitrario, che funziona bene:

learning=> INSERT INTO surprise(id) VALUES(17);
INSERT 0 1
learning=> SELECT * FROM surprise;
id 
----
17
(1 row)

E un INSERT con il valore DEFAULT:

learning=> INSERT INTO surprise(id) VALUES(DEFAULT);
ERROR: new row for relation "surprise" violates check constraint "surpise_id_check"
DETAIL: Failing row contains (10).

Ops...

Di nuovo, con una sintassi alternativa:

learning=> INSERT INTO surprise DEFAULT VALUES;
ERROR: new row for relation "surprise" violates check constraint "surpise_id_check"
DETAIL: Failing row contains (10).

Il vincolo Check prevale sul valore DEFAULT.

Esempio strano

Il vincolo Check può apparire praticamente ovunque nella definizione della tabella durante la creazione. Anche a livello di colonna può essere impostato su una colonna non coinvolta in alcun modo nel controllo.

Ecco un esempio per illustrare:

learning=> CREATE TABLE mystery(id_1 INTEGER CHECK(id_2 > id_3),
learning(> id_2 INTEGER, id_3 INTEGER);
CREATE TABLE

Un INSERT per testare il vincolo:

learning=> INSERT INTO mystery(id_1, id_2, id_3) VALUES (1, 2, 3);
ERROR: new row for relation "mystery" violates check constraint "mystery_check"
DETAIL: Failing row contains (1, 2, 3).

Funziona come previsto.

CONVALIDA e NON VALIDA

Abbiamo questa semplice tabella e dati:

learning=> CREATE TABLE v_check(id INTEGER);
CREATE TABLE
learning=> INSERT INTO v_check SELECT * FROM generate_series(1, 425);
INSERT 0 425

Supponiamo ora di dover implementare un vincolo Check che proibisca qualsiasi valore inferiore a 50.

Immagina che questa sia una grande tabella in produzione e che al momento non possiamo permetterci alcun blocco acquisito, risultante da un'istruzione ALTER TABLE. Ma è necessario mettere in atto questo vincolo, andando avanti.

ALTER TABLE acquisirà un blocco (dipendente da ogni diversa sottomaschera). Come accennato, questa tabella è in produzione, quindi desideriamo aspettare fino a quando non saremo fuori dalle "ore di punta '.

Puoi utilizzare l'opzione NO VALID durante la creazione del vincolo Verifica:

learning=> ALTER TABLE v_check ADD CONSTRAINT fifty_chk CHECK(id > 50) NOT VALID; 
ALTER TABLE
Scarica il whitepaper oggi Gestione e automazione di PostgreSQL con ClusterControlScopri cosa devi sapere per distribuire, monitorare, gestire e ridimensionare PostgreSQLScarica il whitepaper

Operazioni in corso, in caso di tentativo di INSERT o UPDATE che viola il vincolo Check:

learning=> INSERT INTO v_check(id) VALUES(22);
ERROR: new row for relation "v_check" violates check constraint "fifty_chk"
DETAIL: Failing row contains (22).

Il valore della colonna "incriminato" è vietato.

Quindi, durante i tempi di inattività, convalidiamo il vincolo Check per applicarlo a (eventuali) colonne preesistenti che potrebbero violare:

learning=> ALTER TABLE v_check VALIDATE CONSTRAINT fifty_chk;
ERROR: check constraint "fifty_chk" is violated by some row

Il messaggio è piuttosto criptico secondo me. Tuttavia, ci informa che ci sono righe non conformi al vincolo.

Ecco alcuni punti chiave che volevo includere dalla documentazione di ALTER TABLE (Verbiage direttamente dai documenti tra virgolette):

  • Sintassi:ADD table_constraint [ NON VALIDO ] - Descrizione di accompagnamento (parziale) "Questo modulo aggiunge un nuovo vincolo a una tabella utilizzando la stessa sintassi di CREATE TABLE, più l'opzione NON VALIDO, che è attualmente consentita solo per la chiave esterna e CHECK vincoli. Se il vincolo è contrassegnato come NON VALIDO, il controllo iniziale potenzialmente lungo per verificare che tutte le righe della tabella soddisfino il vincolo viene ignorato."
  • Sintassi:VALIDATE CONSTRAINT nome_vincolo - Descrizione di accompagnamento (parziale) "Questo modulo convalida una chiave esterna o verifica un vincolo precedentemente creato come NON VALIDO, scansionando la tabella per assicurarsi che non vi siano righe per le quali il vincolo non è soddisfatto. " "La convalida acquisisce solo un blocco SHARE UPDATE EXCLUSIVE sulla tabella che viene modificata."

Per inciso, due punti degni di nota che ho imparato lungo la strada. Le funzioni e le sottoquery di restituzione degli insiemi non sono consentite nelle definizioni dei vincoli di controllo. Sono sicuro che ce ne sono altri e accolgo con favore qualsiasi feedback su di loro nei commenti qui sotto.

I vincoli di controllo sono fantastici. L'uso delle soluzioni "integrate" fornite dal database PostgreSQL stesso, per imporre qualsiasi restrizione sui dati, ha perfettamente senso. Il tempo e gli sforzi spesi per l'implementazione Controllare i vincoli per le colonne necessarie, superano di gran lunga la mancata implementazione. Risparmiando così tempo a lungo termine. Più ci appoggiamo al database per gestire questo tipo di requisiti, meglio è. Ci consente di concentrare e applicare le nostre risorse ad altre aree/aspetti della gestione del database.

Grazie per aver letto.