2013-03-02 11 views
15

Quali sono i buoni modi per aggiungere un vincolo a PostgreSQL per verificare che esattamente una colonna (da un insieme di colonne) contenga un valore non nullo?Il vincolo Postgres garantisce che una colonna di molti sia presente?

Aggiornamento: E 'probabile che voglio usare un'espressione check come dettagliato nella Create Table e Alter Table.

Aggiornamento: Sto osservando lo functions disponibile.

Aggiornamento: Solo per lo sfondo, qui è la logica di convalida Rails Attualmente sto usando:

validate :multi_column_validation 
def multi_column_validation 
    n = 0 
    n += 1 if column_1 
    n += 1 if column_2 
    n += 1 if column_3 
    unless 1 == n 
    errors.add(:base, "Exactly one column from " + 
     "column_1, column_2, column_3 must be present") 
    end 
end 

Per essere chiari, io sto cercando PSQL, non Ruby, qui. Volevo solo mostrare la logica che sto usando poiché è più compatto di enumerare tutte le possibilità della "tavola della verità".

+0

Queste colonne sono booleani? –

+0

Clodoaldo: Sono interessato a NULL o NOT NULL. (Nel mio caso specifico, ho un mix di colonne UUID e TEXT, ma non penso che questo sia fondamentale per il problema.) –

risposta

14

penso che il più pulito e genericamente soluzione è quella di creare una funzione di conta i valori nulli da alcuni argomenti. Per questo è possibile utilizzare il pseudo-type anyarray e una funzione SQL simile:

CREATE FUNCTION count_not_nulls(p_array anyarray) 
RETURNS BIGINT AS 
$$ 
    SELECT count(x) FROM unnest($1) AS x 
$$ LANGUAGE SQL IMMUTABLE; 

Con questa funzione, è possibile creare il CHECK CONSTRAINT come:

ALTER TABLE your_table 
ADD chk_only_one_is_not_null CHECK(count_not_nulls(array[col1, col2, col3]) = 1); 

Questo funziona solo se le colonne sono dello stesso tipo di dati.Se non è il caso, è possibile lanciare, come testo, per esempio (in quanto è sufficiente cura per il caso null):

ALTER TABLE your_table 
ADD chk_only_one_is_not_null CHECK(count_not_nulls(array[col1::text, col2::text, col3::text]) = 1); 

Come ben ricordato da @muistooshort, è possibile creare la funzione con variadic arguments, il che rende Chiama:

CREATE FUNCTION count_not_nulls(variadic p_array anyarray) 
RETURNS BIGINT AS 
$$ 
    SELECT count(x) FROM unnest($1) AS x 
$$ LANGUAGE SQL IMMUTABLE; 

ALTER TABLE your_table 
ADD chk_only_one_is_not_null CHECK(count_not_nulls(col1, col2, col3) = 1); 
+0

È possibile eseguire il cast all'interno della funzione, rendendola generica per tutti i tipi di colonna (o la maggior parte)? –

+0

@DavidJames, non AFAIK. Ma dovrai eseguire il cast solo se i tipi di dati delle colonne sono diversi, se sono uguali (indipendentemente dai quali) non è necessario eseguire il cast. – MatheusOl

+1

Puoi scrivere [funzioni variadiche] (http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-VARIADIC-FUNCTIONS) in PL/pgSQL se non ti piace il "Artificializzazione artificiale" degli argomenti quando si chiama 'count_not_nulls'. –

3

Un po 'goffo, ma dovrebbe fare il trucco:

create table foo 
(
    col1 integer, 
    col2 integer, 
    col3 integer, 
    constraint one_is_not_null check 
     ( (col1 is not null and col2 is null and col3 is null) 
      or (col1 is null and col2 is not null and col3 is null) 
      or (col1 is null and col2 is null and col3 is not null) 
     ) 
) 
+0

Qualche idea su come compattarlo e/o generalizzarlo su 'k' colonne? –

+0

@DavidJames: Non penso che ci sia una soluzione facile a quello –

27

Qui è una soluzione elegante a due colonne secondo il "constraint -- one or the other column not null" PostgreSQL message board:

ALTER TABLE my_table ADD CONSTRAINT my_constraint CHECK (
    column_1 IS NULL != column_2 IS NULL); 

(Ma l'approccio di cui sopra non è generalizzabile a tre o più colonne.)

Se si dispone di tre o più colonne, è possibile utilizzare l'approccio della tabella di verità illustrato da a_horse_with_no_name. Tuttavia, ritengo che la segue per essere più facile da mantenere, perché non c'è bisogno di digitare le combinazioni logiche:

ALTER TABLE my_table 
ADD CONSTRAINT my_constraint CHECK (
    (CASE WHEN column_1 IS NULL THEN 0 ELSE 1 END) + 
    (CASE WHEN column_2 IS NULL THEN 0 ELSE 1 END) + 
    (CASE WHEN column_3 IS NULL THEN 0 ELSE 1 END) = 1; 

Per compattare questo, sarebbe utile per creare una funzione personalizzata in modo che il testo standard CASE WHEN column_k IS NULL THEN 0 ELSE 1 END potrebbe essere rimosso, lasciando qualcosa come:

(non_null_count(column_1) + 
non_null_count(column_2) + 
non_null_count(column_3)) = 1 

Che può essere il più compatto come PSQL consentirà (?). Detto questo, preferisco arrivare a questo tipo di sintassi, se possibile:

non_null_count(column_1, column_2, column_3) = 1 
+3

C'è un cast 'boolean' per' integer' definito (vedi '\ dC boolean' in' psql') e i risultati in stile C sono gli unici cose ragionevoli da fare dato [possibili valori letterali booleani] (http://www.postgresql.org/docs/current/interactive/datatype-boolean.html) così puoi '(null is null) :: int' a ottieni un '1' o' (null non è null) :: int' per ottenere un '0'. –

+0

MatheusOl ha ripreso da dove avevo interrotto e suggerito una [soluzione più elegante] (http://stackoverflow.com/a/15180124/109618). –

+0

Quel primo vincolo è davvero elegante. – Andrew

13

Come accennato da mu is too short:

alter table t 
add constraint only_one_null check (
    (col1 is not null)::integer + (col2 is not null)::integer = 1 
) 
+1

Voglio esattamente un ** non ** - valore null, quindi: '(col1 NON È NULL) :: int + (col2 NON È NULL) :: int = 1' –

+1

@David Ok. Corretto. –

Problemi correlati