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.