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

Mais SQL, menos código, com PostgreSQL


Com apenas alguns ajustes e melhorias em suas consultas SQL do Postgres, você pode reduzir a quantidade de código de aplicativo repetitivo e propenso a erros necessário para interagir com seu banco de dados. Na maioria das vezes, essa alteração também melhora o desempenho do código do aplicativo.

Aqui estão algumas dicas e truques que podem ajudar o código do seu aplicativo a terceirizar mais trabalho para o PostgreSQL e tornar seu aplicativo mais simples e rápido.

Upsert


Desde o Postgres v9.5, é possível especificar o que deve acontecer quando uma inserção falha devido a um “conflito”. O conflito pode ser uma violação de um índice exclusivo (incluindo uma chave primária) ou qualquer restrição (criada anteriormente usando CREATE CONSTRAINT).

Esse recurso pode ser usado para simplificar a lógica do aplicativo de inserção ou atualização em uma única instrução SQL. Por exemplo, dada uma tabela kv com chave e valor colunas, a instrução abaixo irá inserir uma nova linha (se a tabela não tiver uma linha com key='host') ou atualizar o valor (se a tabela tiver uma linha com key='host'):
CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT);

INSERT INTO kv (key, value)
VALUES ('host', '10.0.10.1')
    ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value;

Observe que a coluna key é a chave primária de coluna única da tabela e é especificada como a cláusula de conflito. Se você tiver uma chave primária com várias colunas, especifique o nome do índice da chave primária aqui.

Para exemplos avançados, incluindo a especificação de índices e restrições parciais, consulte os documentos do Postgres.

Inserir .. return


A instrução INSERT também pode retornar uma ou mais linhas, como uma instrução SELECT. Ele pode retornar valores gerados por funções, palavras-chave como current_timestamp e série colunas /sequência/identidade.

Por exemplo, aqui está uma tabela com uma coluna de identidade gerada automaticamente e uma coluna que contém o carimbo de data/hora da criação da linha:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(>                  at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(>                  foo text);

Podemos usar a instrução INSERT .. RETURNING para especificar apenas o valor da coluna foo , e deixe o Postgres retornar os valores gerados para o id e em colunas:
db=> INSERT INTO t1 (foo) VALUES ('first'), ('second') RETURNING id, at, foo;
 id |                at                |  foo
----+----------------------------------+--------
  1 | 2022-01-14 11:52:09.816787+01:00 | first
  2 | 2022-01-14 11:52:09.816787+01:00 | second
(2 rows)

INSERT 0 2

No código do aplicativo, use os mesmos padrões/APIs que você usaria para executar instruções SELECT e ler valores (como executeQuery() em JDBC ou db.Query() em Go).

Aqui está outro exemplo, este tem um UUID gerado automaticamente:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);

INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;

Semelhante a INSERT, as instruções UPDATE e DELETE também podem conter cláusulas RETURNING no Postgres. A cláusula RETURNING é uma extensão do Postgres e não faz parte do padrão SQL.

Qualquer um em um conjunto


A partir do código do aplicativo, como você criaria uma cláusula WHERE que precisa corresponder o valor de uma coluna a um conjunto de valores aceitáveis? Quando o número de valores é conhecido de antemão, o SQL é estático:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);

Mas e se o número de chaves não for 2, mas puder ser qualquer número? Você construiria a instrução SQL dinamicamente? Uma opção mais fácil é usar arrays Postgres:
SELECT key, value FROM kv WHERE key = ANY(?)

O operador ANY acima recebe um array como argumento. A cláusula chave =QUALQUER(?) seleciona todas as linhas onde o valor de chave é um dos elementos da matriz fornecida. Com isso, o código do aplicativo pode ser simplificado para:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);

Essa abordagem é viável para um número limitado de valores; se você tiver muitos valores para corresponder, considere outras opções, como unir-se a tabelas (temporárias) ou visualizações materializadas.

Movendo linhas entre tabelas


Sim, você pode excluir linhas de uma tabela e inseri-las em outra com uma única instrução SQL! Uma instrução INSERT principal pode extrair as linhas para inserir usando um CTE, que envolve um DELETE.
WITH items AS (
       DELETE FROM todos_2021
        WHERE NOT done
    RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;

Fazer o equivalente no código do aplicativo pode ser muito detalhado, envolvendo armazenar todo o resultado da exclusão na memória e usar isso para fazer vários INSERTs. É verdade que mover linhas talvez não seja um caso de uso comum, mas se a lógica de negócios exigir isso, a economia de memória do aplicativo e as viagens de ida e volta do banco de dados apresentadas por essa abordagem a tornam a solução ideal.

O conjunto de colunas nas tabelas de origem e destino não precisa ser idêntico, é claro que você pode reordenar, reorganizar e usar funções para manipular os valores nas listas de seleção/retorno.

Aglutinar


A entrega de valores NULL no código do aplicativo geralmente requer etapas extras. Em Go, por exemplo, você precisaria usar tipos como sql.NullString; em Java/JDBC, funções como resultSet.wasNull() . Estes são complicados e propensos a erros.

Se for possível manipular, digamos, NULLs como strings vazias ou inteiros NULL como 0, no contexto de uma consulta específica, você pode usar a função COALESCE. A função COALESCE pode transformar valores NULL em qualquer valor específico. Por exemplo, considere esta consulta:
SELECT invoice_num, COALESCE(shipping_address, '')
  FROM invoices
 WHERE EXTRACT(month FROM raised_on) = 1    AND
       EXTRACT(year  FROM raised_on) = 2022

que obtém os números das faturas e os endereços de entrega das faturas geradas em janeiro de 2022. Presumivelmente, shipping_address é NULL se as mercadorias não precisam ser enviadas fisicamente. Se o código do aplicativo simplesmente deseja exibir uma string vazia em algum lugar nesses casos, digamos, é mais simples usar COALESCE e remover o código de manipulação NULL no aplicativo.

Você também pode usar outras strings em vez de uma string vazia:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...

Você pode até obter o primeiro valor não NULL de uma lista ou usar a string especificada. Por exemplo, para usar o endereço de cobrança ou o endereço de entrega, você pode usar:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...

Caso


CASE é outra construção útil para lidar com dados imperfeitos da vida real. Digamos que em vez de ter NULLs em shipping_address para itens que não podem ser enviados, nosso software de criação de faturas não tão perfeito colocou “NÃO ESPECIFICADO”. Você gostaria de mapear isso para um NULL ou uma string vazia ao ler os dados. Você pode usar CASE:
-- map NOT-SPECIFIED to an empty string
SELECT invoice_num,
       CASE shipping_address
	     WHEN 'NOT-SPECIFIED' THEN ''
		 ELSE shipping_address
		 END
FROM   invoices;

-- same result, different syntax
SELECT invoice_num,
       CASE
	     WHEN shipping_address = 'NOT-SPECIFIED' THEN ''
		 ELSE shipping_address
		 END
FROM   invoices;

CASE tem uma sintaxe desajeitada, mas é funcionalmente semelhante a instruções switch-case em linguagens semelhantes a C. Aqui está outro exemplo:
SELECT invoice_num,
       CASE
	     WHEN shipping_address IS NULL THEN 'NOT SHIPPING'
	     WHEN billing_address = shipping_address THEN 'SHIPPING TO PAYER'
		 ELSE 'SHIPPING TO ' || shipping_address
		 END
FROM   invoices;

Selecione .. união


Dados de duas (ou mais) instruções SELECT separadas podem ser combinadas usando UNION. Por exemplo, se você tiver duas tabelas, uma com usuários atuais e outra excluída, veja como consultar as duas ao mesmo tempo:
SELECT id, name, address, FALSE AS is_deleted 
  FROM users
 WHERE email = ?

UNION

SELECT id, name, address, TRUE AS is_deleted
  FROM deleted_users
 WHERE email = ?

As duas consultas devem ter a mesma lista de seleção, ou seja, devem retornar o mesmo número e tipo de colunas.

UNION também remove duplicatas. Somente linhas exclusivas são retornadas. Se preferir manter as linhas duplicadas, use “UNION ALL” em vez de UNION.

Complementando UNION, há também INTERSECT e EXCEPT, veja os documentos do PostgreSQL para mais informações.

Selecione .. distinto em


Linhas duplicadas retornadas por um SELECT podem ser combinadas (ou seja, somente linhas exclusivas são retornadas) adicionando a palavra-chave DISTINCT após SELECT. Embora este seja o SQL padrão, o Postgres fornece uma extensão, o “DISTINCT ON”. É um pouco complicado de usar, mas na prática geralmente é a maneira mais concisa de obter os resultados que você precisa.

Considere um clientes tabela com uma linha por cliente e um compras tabela com uma linha por compras feitas por (alguns) clientes. A consulta abaixo retorna todos os clientes, juntamente com cada uma de suas compras:
   SELECT C.id, P.at
     FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
 ORDER BY C.id ASC, P.at ASC;

Cada linha de cliente é repetida para cada compra que ele fez. E se quisermos devolver apenas a primeira compra de um cliente? Basicamente, queremos classificar as linhas por cliente, agrupar as linhas por cliente, dentro de cada grupo classificar as linhas por tempo de compra e, finalmente, retornar apenas a primeira linha de cada grupo. Na verdade, é mais curto escrever isso em SQL com DISTINCT ON:
   SELECT DISTINCT ON (C.id) C.id, P.at
     FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
 ORDER BY C.id ASC, P.at ASC;

A cláusula “DISTINCT ON (C.id)” adicionada faz exatamente o que foi descrito acima. Isso é muito trabalho com apenas algumas letras adicionais!

Usando números em ordem por cláusula


Considere buscar uma lista de nomes de clientes e o código de área de seus números de telefone em uma tabela. Vamos supor que os números de telefone dos EUA sejam armazenados formatados como (123) 456-7890 . Para outros países, diremos apenas "NON-US" como código de área.
SELECT last_name, first_name,
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END
FROM   customers;

Tudo bem, e também temos a construção CASE, mas e se precisarmos classificá-la pelo código de área agora?

Isso funciona:
SELECT last_name, first_name,
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END
FROM   customers
ORDER  BY
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END ASC;

Mas ufa! Repetir a cláusula case é feio e propenso a erros. Poderíamos escrever uma função armazenada que pegasse o código do país e o telefone e retornasse o código de área, mas na verdade existe uma opção melhor:
SELECT last_name, first_name,
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END
FROM   customers
ORDER  BY 3 ASC;

O “ORDER BY 3” diz ordem pelo 3º campo! Você deve se lembrar de atualizar o número ao reorganizar a lista de seleção, mas geralmente vale a pena.