sql >> Base de Datos >  >> RDS >> PostgreSQL

Comprender las restricciones de verificación en PostgreSQL

La gestión de datos es un gran desafío. A medida que nuestro mundo gira, los datos continúan siendo generalizados, abundantes e intensivos. Por lo tanto, debemos tomar medidas para manejar la afluencia.

Validación de cada pieza de datos 'a mano ' durante todo el día es simplemente poco práctico. Que fantástico sueño. Pero, después de todo, es sólo eso. Un sueño. Los datos malos son datos malos. No importa cómo lo cortes o cortes en dados (juego de palabras). Es un problema desde el principio, lo que genera aún más problemas.

Las bases de datos modernas manejan gran parte del trabajo pesado para nosotros. Muchos proporcionan soluciones integradas para ayudar a administrar esta área particular de datos.

Una forma segura de controlar los datos ingresados ​​en la columna de una tabla es con un tipo de datos. ¿Necesita una columna con números decimales, con un recuento total de dígitos de 4, con 2 de ellos después del decimal?

¡Cosa segura! No hay problema en absoluto.

NUMERIC(4,2), una opción viable, está protegiendo esa columna como un perro guardián. ¿Pueden deslizarse valores de texto de caracteres allí? Ni una bola de nieve.

PostgreSQL ofrece una multitud de tipos de datos. Lo más probable es que ya exista uno para satisfacer sus necesidades. Si no, puedes crear el tuyo propio. (Ver:PostgreSQL CREAR TIPO)

Sin embargo, los tipos de datos por sí solos no son suficientes. No puede asegurar que los requisitos más específicos estén cubiertos y se ajusten a una estructuración tan amplia. Por lo general, se requieren reglas de cumplimiento y algún tipo de "estándar" al diseñar un esquema.

Suponga que en esa misma columna NUMERIC(4,2), solo desea valores mayores que 25,25 pero menores que 74,33. En caso de que se almacene el valor 88.22, el tipo de datos no tiene la culpa. Al permitir 4 dígitos en total, con 2 como máximo después del decimal, está haciendo su trabajo. Echa la culpa a otra parte.

¿Cómo ganamos en este frente cuando se trata de controlar los datos permitidos en nuestra base de datos? La consistencia de los datos es de máxima prioridad y es parte integral de cualquier solución de datos sólida. En caso de que haya controlado los datos recopilados desde el inicio de su fuente de origen, es probable que la consistencia sea un problema menor.

Pero, un mundo perfecto solo existe (tal vez) en una de esas muchas novelas de fantasía que me encanta leer.

Desafortunadamente, los datos incompletos, inconsistentes y 'sucios' son características y realidades muy comunes presentes en un campo centrado en bases de datos.

Sin embargo, no todo se pierde en el pesimismo, ya que tenemos restricciones de verificación para mitigar estos problemas. Para esas reglas específicas, debemos implementar, por necesidad, que garantice que manejamos y almacenamos solo datos consistentes. Al exigir esas especificaciones en la base de datos, podemos minimizar el impacto que tienen los datos inconsistentes en nuestros objetivos comerciales y soluciones que se llevan adelante.

¿Qué es una restricción? - Una definición de alto nivel

En este contexto, una restricción es un tipo de regla o restricción colocada en una columna de la tabla de la base de datos. Esta especificidad requiere que los datos que ingresan cumplan con los requisitos establecidos antes de ser almacenados. Dicho(s) requisito(s) tiende(n) a ser acuñado(s) 'profesionalmente' (y a menudo lo es) como reglas de negocio . Esto se reduce a una prueba booleana de validación de la verdad. Si los datos pasan (verdadero), se almacenan. Si no, no hay entrada (falso).

Restricciones disponibles en PostgreSQL

Al momento de escribir, la documentación de PostgreSQL enumera 6 categorías de restricciones.

Ellos son:

  • Comprobar restricciones
  • Restricciones no nulas
  • Restricciones únicas
  • Claves primarias
  • Claves foráneas
  • Restricciones de exclusión

Comprobar restricciones

Un ejemplo simple para una columna INTEGER sería no permitir valores mayores que, por ejemplo, 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).

Como se vio anteriormente, los intentos de INSERTAR cualquier valor que viole la restricción de verificación fallan.

Las restricciones de verificación no solo monitorean las columnas durante INSERTAR, sino que incluso las declaraciones de ACTUALIZACIÓN (y otras, por ejemplo, \copiar y COPIAR) también deben cumplir con las restricciones.

Supongamos que la tabla no_go tiene este valor:

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

También falla una ACTUALIZACIÓN en el valor de la columna de identificación a uno que no se ajusta a la restricción de verificación:

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).

Las restricciones de comprobación deben "tener sentido" para el tipo de datos de la columna de destino. No es válido intentar y restringir una columna INTEGER para prohibir el almacenamiento de valores de texto, ya que el tipo de datos en sí no lo permitirá.

Vea este ejemplo donde trato de imponer ese tipo de restricción de verificación durante la creación de la tabla:

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

Vida sin restricciones de control

Un viejo dicho que he escuchado y que resuena en mí es:"No echas de menos el agua hasta que el pozo se seca . "

Sin restricciones de verificación, seguramente podemos relacionarnos porque su notable beneficio es más apreciado cuando tiene que arreglárselas sin ellas.

Toma este ejemplo...

Para empezar, tenemos esta tabla y datos que representan los materiales de la superficie del sendero:

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)

Y esta tabla con nombres de senderos y su propia superficie_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)

Queremos asegurarnos de que los registros de la tabla solo contengan los ID_superficie para los valores correspondientes en la tabla material_superficie.

Si si lo se. Me estás gritando.

"¿No se puede solucionar esto con un ¿¡¿LLAVE EXTRANJERA?!?"

Sí puede. Pero lo estoy usando para demostrar un uso genérico, junto con una trampa para saber (mencionada más adelante en la publicación).

Sin las restricciones de verificación, puede recurrir a un TRIGGER y evitar que se almacenen valores inconsistentes.

Aquí hay un ejemplo crudo (pero funcional):

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();

Intenta INSERTAR un valor que no tiene un Surface_id correspondiente en los registros de la tabla, falla:

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

Los resultados de la consulta a continuación confirman el 'ofensivo ' valor no fue almacenado:

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)

Seguro que es mucho trabajo prohibir valores no deseados.

Volvamos a implementar este requisito con una restricción de verificación.

Dado que no puede usar una subconsulta (esta es la razón por la que usé el ejemplo anterior) en la definición real de la restricción de verificación, los valores deben estar codificados .

Para una tabla pequeña o un ejemplo trivial como el que se presenta aquí, está bien. En otros escenarios, al incorporar más valores, es mejor que busque una solución alternativa.

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

Aquí he llamado a la restricción de verificación t_check en lugar de dejar que el sistema la nombre.

(Nota:El definido anteriormente check_me() FUNCIÓN y acompañante TRIGGER se descartaron (no se muestran) antes de ejecutar lo siguiente INSERTAR)

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).

¡Mira lo fácil que fue! No se necesita TRIGGER ni FUNCIÓN.

Las restricciones de verificación hacen que este tipo de trabajo sea fácil.

¿Quieres ser astuto en la definición de la restricción Check?

Tú puedes.

Suponga que necesita una tabla con una lista de senderos que sean un poco más amables con las personas con tobillos y rodillas sensibles. Aquí no se desean superficies duras.

Desea asegurarse de que cualquier ruta de senderismo o pista enumerada en la tabla nice_trail tenga un material de superficie de 'Grava' o 'Tierra'.

Esta restricción Check maneja ese requisito sin problema:

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

Eso absolutamente funciona bien.

Pero, ¿qué tal una FUNCIÓN que devuelve las dos identificaciones requeridas para que la verificación funcione? ¿Se permite una FUNCIÓN en la definición de restricción de comprobación?

Sí, se puede incorporar uno.

Aquí hay un ejemplo de trabajo.

Primero, el cuerpo y la definición de la función:

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;

Observe que en esta declaración CREATE TABLE, defino la restricción Check en la 'tabla ' nivel mientras que anteriormente solo he proporcionado ejemplos en la 'columna ' nivel.

Compruebe que las restricciones definidas a nivel de tabla son perfectamente válidas:

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

Estas inserciones son todas buenas:

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

Ahora viene un INSERT para un rastro que no cumple con la restricción en la columna 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).

Nuestra llamada FUNCTION en la definición de la restricción Check funciona según lo diseñado, restringiendo los valores de columna no deseados.

¿Humo y espejos?

¿Es todo lo que parece con las restricciones de verificación? ¿Todo en blanco y negro? ¿Sin fachada al frente?

Un ejemplo digno de mención.

Tenemos una tabla simple en la que queremos que el valor PREDETERMINADO sea 10 para la única columna INTEGER presente:

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

Pero, también he incluido una restricción de verificación que prohíbe un valor de 10, al definir id no puede ser igual a ese número.

¿Cuál ganará el día? ¿La restricción DEFAULT o Check?

Te sorprenderá saber cuál es.

yo era.

Un INSERT arbitrario, funcionando bien:

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

Y un INSERT con el valor 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).

Ups...

De nuevo, con una sintaxis alternativa:

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

La restricción Check gana sobre el valor DEFAULT.

Ejemplo de bicho raro

La restricción Check puede aparecer prácticamente en cualquier parte de la definición de la tabla durante la creación. Incluso a nivel de columna, se puede configurar en una columna que no esté involucrada en la verificación.

Aquí hay un ejemplo para ilustrar:

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

Un INSERT para probar la restricción:

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).

Funciona según lo previsto.

VALIDACIÓN y NO VÁLIDA

Tenemos esta tabla simple y datos:

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

Supongamos que ahora necesitamos implementar una restricción de verificación que prohíba cualquier valor inferior a 50.

Imagine que se trata de una tabla grande en producción y en realidad no podemos permitirnos ningún bloqueo adquirido en este momento, como resultado de una instrucción ALTER TABLE. Pero es necesario establecer esta restricción para seguir adelante.

ALTER TABLE adquirirá un bloqueo (dependiendo de cada subformulario diferente). Como se mencionó, esta tabla está en producción, por lo que deseamos esperar hasta que estemos fuera de las 'horas pico '.

Puede usar la opción NO VÁLIDO al crear la restricción Verificar:

learning=> ALTER TABLE v_check ADD CONSTRAINT fifty_chk CHECK(id > 50) NOT VALID; 
ALTER TABLE
Descargue el documento técnico hoy Gestión y automatización de PostgreSQL con ClusterControl Obtenga información sobre lo que necesita saber para implementar, monitorear, administrar y escalar PostgreSQLDescargar el documento técnico

Operaciones continuas, en caso de que se intente INSERTAR o ACTUALIZAR que infrinja la restricción de comprobación:

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).

El valor de la columna 'ofensivo' está prohibido.

Luego, durante el tiempo de inactividad, validamos la restricción Check para aplicarla contra (cualquier) columna preexistente que pueda estar en violación:

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

El mensaje es bastante críptico en mi opinión. Pero sí nos informa que hay filas que no cumplen con la restricción.

Aquí hay algunos puntos clave que quería incluir de la documentación de ALTER TABLE (Verbiage directamente de los documentos entre comillas):

  • Sintaxis:ADD table_constraint [ NOT VALID ] - Descripción adjunta (parcial) "Este formulario agrega una nueva restricción a una tabla usando la misma sintaxis que CREATE TABLE, más la opción NOT VALID, que actualmente solo está permitida para claves foráneas y COMPROBAR restricciones. Si la restricción está marcada como NO VÁLIDA, se omite la verificación inicial potencialmente larga para verificar que todas las filas de la tabla cumplan con la restricción".
  • Sintaxis:VALIDATE CONSTRAINT nombre_restricción - Descripción adjunta (parcial) "Este formulario valida una clave externa o una restricción de verificación que se creó previamente como NO VÁLIDA, escaneando la tabla para asegurarse de que no haya filas para las cuales la restricción no se cumpla. " "La validación adquiere solo un bloqueo COMPARTIR ACTUALIZACIÓN EXCLUSIVA en la tabla que se modifica".

Aparte, dos puntos que vale la pena señalar que aprendí en el camino. Las funciones y subconsultas que devuelven conjuntos no están permitidas en las definiciones de restricciones de comprobación. Estoy seguro de que hay otros y agradezco cualquier comentario sobre ellos en los comentarios a continuación.

Las restricciones de verificación son increíbles. El uso de las soluciones 'integradas' proporcionadas por la propia base de datos PostgreSQL para hacer cumplir cualquier restricción de datos tiene mucho sentido. El tiempo y el esfuerzo dedicados a implementar las restricciones de verificación para las columnas necesarias superan con creces el hecho de no implementar ninguna. Así ahorrando tiempo a largo plazo. Cuanto más nos apoyemos en la base de datos para manejar este tipo de requisitos, mejor. Permitiéndonos enfocar y aplicar nuestros recursos a otras áreas/aspectos de la gestión de bases de datos.

Gracias por leer.