PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Entendendo as restrições de verificação no PostgreSQL


Gerenciar dados é um grande desafio. À medida que nosso mundo gira, os dados continuam a ser difundidos, abundantes e intensivos. Portanto, devemos tomar medidas para lidar com o influxo.

Validando todos os dados 'manualmente ' 24 horas por dia é simplesmente impraticável. Que sonho fantástico. Mas, afinal, é apenas isso. Um sonho. Dados ruins são dados ruins. Não importa como você o corta ou corta (trocadilho intencional). É um problema desde o início, levando a ainda mais problemas.

Bancos de dados modernos lidam com grande parte do trabalho pesado para nós. Muitos fornecem soluções integradas para auxiliar no gerenciamento dessa área específica de dados.

Uma maneira segura de controlar os dados inseridos na coluna de uma tabela é com um tipo de dados. Precisa de uma coluna com números decimais, com uma contagem total de dígitos de 4, com 2 deles após o decimal?

Coisa certa! Nenhum problema.

NUMERIC(4,2), uma opção viável, está guardando essa coluna como um watchdog. Os valores do texto dos caracteres podem entrar lá? Não é a chance de uma bola de neve.

O PostgreSQL oferece vários tipos de dados. É provável que já exista um para satisfazer sua(s) necessidade(s). Se não, você pode criar o seu próprio. (Veja:PostgreSQL CREATE TYPE)

No entanto, os tipos de dados por si só não são suficientes. Você não pode garantir que os requisitos mais específicos sejam cobertos e estejam em conformidade com uma estrutura tão ampla. Regras de conformidade e algum tipo de 'padrão' são normalmente necessários ao projetar um esquema.

Suponha que nessa mesma coluna NUMERIC(4,2), você queira apenas valores maiores que 25,25, mas menores que 74,33? Caso o valor 88.22 seja armazenado, o tipo de dados não está com defeito. Ao permitir 4 dígitos no total, com 2 no máximo após o decimal, ele está fazendo seu trabalho. Coloque a culpa em outro lugar.

Como ganhamos nessa frente quando se trata de controlar os dados permitidos em nosso banco de dados? A consistência dos dados é a prioridade máxima e é parte integrante de qualquer solução de dados sólida. Na chance (fora) de você controlar os dados coletados desde o início de sua fonte de origem, a consistência provavelmente seria um problema menor.

Mas, um mundo perfeito só existe (talvez) em um daqueles muitos romances de fantasia que adoro ler.

Infelizmente, dados incompletos, inconsistentes e 'sujos' são características e realidades muito comuns presentes em um campo centrado em banco de dados.

No entanto, nem tudo está perdido em desgraça e melancolia, pois temos restrições de verificação para mitigar esses problemas. Para essas regras específicas, devemos colocar em prática, por necessidade, que garanta que manipulemos e armazenemos apenas dados consistentes. Ao impor essas especificações no banco de dados, podemos minimizar o impacto que dados inconsistentes têm em nossas metas de negócios e soluções futuras.

O que é uma restrição? - Uma definição de alto nível


Nesse contexto, uma restrição é um tipo de regra ou restrição colocada em uma coluna da tabela do banco de dados. Essa especificidade exige que os dados recebidos cumpram o(s) requisito(s) definido(s) antes de serem armazenados. Esses requisitos tendem a ser cunhados "profissionalmente" (e geralmente são) como regras de negócios . Isso se resume a um teste booleano de validação para veracidade. Se os dados passarem (true), eles serão armazenados. Se não, nenhuma entrada (falso).

Restrições disponíveis no PostgreSQL


No momento em que escrevo, a documentação do PostgreSQL lista 6 categorias de restrições.

Eles estão:
  • Verificar restrições
  • Restrições não nulas
  • Restrições exclusivas
  • Chaves primárias
  • Chaves estrangeiras
  • Restrições de exclusão

Verificar restrições


Um exemplo simples para uma coluna INTEGER seria proibir valores maiores que, digamos, 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 visto acima, as tentativas de INSERT quaisquer valores que violem a restrição Check falham.

As restrições de verificação não apenas monitoram as colunas durante o INSERT, mesmo as instruções UPDATE (e outras, por exemplo, \copy e COPY) também devem aderir às restrições.

Suponha que a tabela no_go tenha este valor:
learning=> TABLE no_go;
id 
----
55
(1 row)

Um UPDATE no valor da coluna id para um que não esteja em conformidade com a restrição Check também falha:
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).

As restrições de verificação devem 'fazer sentido' para o tipo de dados da coluna de destino. É inválido tentar restringir uma coluna INTEGER para proibir o armazenamento de valores de texto, pois o próprio tipo de dados não permitirá isso.

Veja este exemplo onde tento impor esse tipo de restrição Check durante a criação da tabela:
learning=> CREATE TABLE num_try(id INTEGER CHECK(id IN ('Bubble', 'YoYo', 'Jack-In-The-Box')));
ERROR: invalid input syntax for integer: "Bubble"

Vida sem restrições de cheques


Um velho ditado que ouvi que ressoa comigo é:"Você não sente falta da água até o poço secar . "

Sem restrições de verificação, certamente podemos nos relacionar, pois seu notável benefício é mais apreciado quando você precisa se virar sem elas.

Veja este exemplo…

Para começar temos esta tabela e dados que representam os materiais da superfície da trilha:
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 esta tabela com nomes de trilhas e seu próprio 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)

Queremos garantir que as trilhas da tabela contenham apenas surface_id's para valores correspondentes na tabela surface_material.

Sim, sim, eu sei. Você está gritando comigo.

"Isso não pode ser resolvido com um CHAVE ESTRANGEIRA?!?"

Sim pode. Mas estou usando para demonstrar um uso genérico, junto com uma armadilha a ser conhecida (mencionada mais adiante no post).

Sem restrições Check, você pode recorrer a um TRIGGER e evitar que valores inconsistentes sejam armazenados.

Aqui está um exemplo bruto (mas 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();

Tentativas de INSERT um valor que não tem um surface_id correspondente nas trilhas da tabela, falham:
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

Os resultados da consulta abaixo confirmam a 'ofensa ' valor não foi armazenado:
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)

Isso com certeza dá muito trabalho para proibir valores indesejados.

Vamos reimplementar esse requisito com uma restrição Check.

Como você não pode usar uma subconsulta (aqui está o motivo pelo qual usei o exemplo acima) na definição real da restrição Check, os valores devem ser codificados .

Para uma pequena tabela, ou um exemplo trivial como o apresentado aqui, tudo bem. Em outros cenários, incorporando mais valores, você pode estar mais bem servido para buscar uma solução alternativa.
learning=> ALTER TABLE trails ADD CONSTRAINT t_check CHECK (surface_id IN (101, 202, 303, 404, 505, 606, 707, 808));
ALTER TABLE

Aqui eu nomeei a restrição Check t_check versus deixar o sistema nomeá-la.

(Observação:o definido anteriormente check_me() FUNÇÃO e acompanha TRIGGER foram descartados (não mostrados) antes de executar o abaixo INSERIR.)
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).

Veja como foi fácil! Não é necessário TRIGGER e FUNCTION.

As restrições de verificação facilitam esse tipo de trabalho.

Quer ficar esperto na definição da restrição Check?

Você pode.

Suponha que você precise de uma tabela listando trilhas que sejam um pouco mais gentis para aqueles com tornozelos e joelhos sensíveis. Nenhuma superfície dura desejada aqui.

Você quer garantir que qualquer trilha ou trilha listada na tabela nice_trail tenha um material de superfície de 'Gravel' ou 'Dirt'.

Esta restrição de verificação lida com esse requisito sem problemas:
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

Isso absolutamente funciona muito bem.

Mas, que tal uma FUNCTION que retorne os dois id's necessários para fazer o Check funcionar? Uma FUNCTION é permitida na definição da restrição Check?

Sim, um pode ser incorporado.

Aqui está um exemplo de trabalho.

Primeiro, o corpo e a definição da função:
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 nesta instrução CREATE TABLE, eu defino a restrição Check na 'tabela ', enquanto anteriormente eu apenas forneci exemplos na 'coluna ' nível.

As restrições de verificação definidas no nível da tabela são perfeitamente 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

Essas inserções são todas boas:
learning=> INSERT INTO nice_trail(name, mat_surface_id)
learning-> VALUES ('Smooth Rock Loop', 101), ('High Water Bluff', 303);
INSERT 0 2

Agora vem um INSERT para uma trilha que não atende a restrição na coluna 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).

Nossa chamada FUNCTION na definição de restrição Check funciona conforme projetado, restringindo os valores de coluna indesejados.

Fumaça e espelhos?


Está tudo como parece com as restrições Check? Tudo preto e branco? Sem fachada na frente?

Um exemplo digno de nota.

Temos uma tabela simples na qual queremos que o valor DEFAULT seja 10 para a coluna INTEGER solitária presente:
learning=> CREATE TABLE surprise(id INTEGER DEFAULT 10, CHECK (id <> 10));
CREATE TABLE

Mas, também incluí uma restrição Check que proíbe um valor de 10, definindo id não pode ser igual a esse número.

Qual deles vai ganhar o dia? A restrição DEFAULT ou Check?

Você pode se surpreender ao saber qual é.

Eu era.

Um INSERT arbitrário, funcionando bem:
learning=> INSERT INTO surprise(id) VALUES(17);
INSERT 0 1
learning=> SELECT * FROM surprise;
id 
----
17
(1 row)

E um INSERT com o 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).

Ops...

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

A restrição Check vence o valor DEFAULT.

Exemplo estranho


A restrição Check pode aparecer praticamente em qualquer lugar na definição da tabela durante a criação. Mesmo no nível da coluna, ele pode ser definido em uma coluna não envolvida na verificação.

Segue um exemplo para ilustrar:
learning=> CREATE TABLE mystery(id_1 INTEGER CHECK(id_2 > id_3),
learning(> id_2 INTEGER, id_3 INTEGER);
CREATE TABLE

Um INSERT para testar a restrição:
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 como pretendido.

VALIDAÇÃO e NÃO VÁLIDA


Temos esta tabela e dados simples:
learning=> CREATE TABLE v_check(id INTEGER);
CREATE TABLE
learning=> INSERT INTO v_check SELECT * FROM generate_series(1, 425);
INSERT 0 425

Suponha que agora precisamos implementar uma restrição Check que proíbe quaisquer valores menores que 50.

Imagine que esta é uma grande tabela em produção e não podemos pagar nenhum bloqueio adquirido no momento, resultante de uma instrução ALTER TABLE. Mas, precisamos colocar essa restrição no lugar, seguindo em frente.

ALTER TABLE adquirirá um bloqueio (dependente de cada subformulário diferente). Como mencionado, esta tabela está em produção, portanto, desejamos aguardar até que estejamos fora do 'horário de pico '.

Você pode usar a opção NO VALID ao criar a restrição Check:
learning=> ALTER TABLE v_check ADD CONSTRAINT fifty_chk CHECK(id > 50) NOT VALID; 
ALTER TABLE
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper
Continuando as operações, caso uma tentativa de INSERT ou UPDATE viole a restrição 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).

O valor da coluna 'ofensivo' é proibido.

Então, durante o tempo de inatividade, validamos a restrição Check para aplicá-la em (quaisquer) colunas pré-existentes que possam estar em violação:
learning=> ALTER TABLE v_check VALIDATE CONSTRAINT fifty_chk;
ERROR: check constraint "fifty_chk" is violated by some row

A mensagem é bastante enigmática na minha opinião. Mas, nos informa que existem linhas que não estão em conformidade com a restrição.

Aqui estão alguns pontos-chave que eu queria incluir da documentação ALTER TABLE (Verbiage diretamente dos documentos entre aspas):
  • Sintaxe:ADD table_constraint [ NOT VALID ] - Descrição de acompanhamento (parcial) "Este formulário adiciona uma nova restrição a uma tabela usando a mesma sintaxe de CREATE TABLE, mais a opção NOT VALID, que atualmente só é permitida para chave estrangeira e CHECK restrições. Se a restrição estiver marcada como NÃO VÁLIDA, a verificação inicial potencialmente longa para verificar se todas as linhas da tabela atendem à restrição será ignorada."
  • Sintaxe:VALIDATE CONSTRAINT constraint_name - Descrição de acompanhamento (parcial) "Este formulário valida uma chave estrangeira ou restrição de verificação que foi criada anteriormente como NOT VALID, verificando a tabela para garantir que não haja linhas para as quais a restrição não seja satisfeita. " "A validação adquire apenas um bloqueio SHARE UPDATE EXCLUSIVE na tabela que está sendo alterada."

Como um aparte, dois pontos dignos de nota que aprendi ao longo do caminho. Funções e subconsultas de retorno de conjunto não são permitidas nas definições de restrição de verificação. Tenho certeza de que existem outros e congratulo-me com qualquer feedback sobre eles nos comentários abaixo.

As restrições de verificação são impressionantes. Usar as soluções 'embutidas' fornecidas pelo próprio banco de dados PostgreSQL, para impor qualquer restrição de dados, faz todo o sentido. Tempo e esforço gastos na implementação Verifique as restrições para a(s) coluna(s) necessária(s), supera em muito a não implementação de nenhuma. Assim, economizando tempo a longo prazo. Quanto mais nos apoiarmos no banco de dados para lidar com esses tipos de requisitos, melhor. Permitindo-nos focar e aplicar nossos recursos a outras áreas/aspectos do gerenciamento de banco de dados.

Obrigado por ler.