Con solo un poco de ajuste y mejora de sus consultas SQL de Postgres, puede reducir la cantidad de código de aplicación repetitivo y propenso a errores que se requiere para interactuar con su base de datos. La mayoría de las veces, dicho cambio también mejora el rendimiento del código de la aplicación.
Aquí hay algunos consejos y trucos que pueden ayudar a que el código de su aplicación subcontrate más trabajo a PostgreSQL y haga que su aplicación sea más delgada y rápida.
Upsert
Desde Postgres v9.5, es posible especificar qué debe suceder cuando una inserción falla debido a un "conflicto". El conflicto puede ser una violación de un índice único (incluida una clave principal) o cualquier restricción (creada anteriormente mediante CREATE CONSTRAINT).
Esta función se puede utilizar para simplificar la lógica de la aplicación de inserción o actualización en una sola instrucción SQL. Por ejemplo, dada una tabla kv con clave y valor columnas, la siguiente instrucción insertará una nueva fila (si la tabla no tiene una fila con clave='host') o actualizará el valor (si la tabla tiene una fila con clave='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;
Tenga en cuenta que la columna key
es la clave principal de una sola columna de la tabla y se especifica como la cláusula de conflicto. Si tiene una clave principal con varias columnas, especifique aquí el nombre del índice de la clave principal.
Para obtener ejemplos avanzados, incluida la especificación de índices y restricciones parciales, consulte los documentos de Postgres.
Insertar...returning
La sentencia INSERT también puede volver una o más filas, como una instrucción SELECT. Puede devolver valores generados por funciones, palabras clave como current_timestamp y serie /secuencia/columnas de identidad.
Por ejemplo, aquí hay una tabla con una columna de identidad generada automáticamente y una columna que contiene la marca de tiempo de la creación de la fila:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
Podemos usar la instrucción INSERT .. RETURNING para especificar solo el valor de la columna foo y dejar que Postgres devuelva los valores que generó para el id y en columnas:
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
Desde el código de la aplicación, use los mismos patrones/API que usaría para ejecutar sentencias SELECT y leer valores (como executeQuery() en JDBC o db.Query() en Ir).
Aquí hay otro ejemplo, este tiene un UUID generado automáticamente:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
Al igual que INSERT, las declaraciones UPDATE y DELETE también pueden contener cláusulas RETURNING en Postgres. La cláusula RETURNING es una extensión de Postgres y no forma parte del estándar SQL.
Cualquiera en un conjunto
A partir del código de la aplicación, ¿cómo crearía una cláusula WHERE que deba hacer coincidir el valor de una columna con un conjunto de valores aceptables? Cuando el número de valores se conoce de antemano, el SQL es estático:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
Pero, ¿y si el número de llaves no es 2 pero puede ser cualquier número? ¿Construiría la declaración SQL dinámicamente? Una opción más fácil es usar matrices de Postgres:
SELECT key, value FROM kv WHERE key = ANY(?)
El operador ANY anterior toma una matriz como argumento. La cláusula key =ANY(?) selecciona todas las filas donde el valor de key es uno de los elementos de la matriz proporcionada. Con esto, el código de la aplicación se puede simplificar a:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
Este enfoque es factible para un número limitado de valores, si tiene muchos valores con los que hacer coincidir, considere otras opciones como unirse con tablas (temporales) o vistas materializadas.
Mover filas entre tablas
¡Sí, puede eliminar filas de una tabla e insertarlas en otra con una sola instrucción SQL! Una declaración INSERT principal puede extraer las filas para insertar usando un CTE, que envuelve un DELETE.
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
Hacer el equivalente en el código de la aplicación puede ser muy detallado, lo que implica almacenar el resultado completo de la eliminación en la memoria y usarlo para hacer múltiples INSERCIONES. Por supuesto, mover filas tal vez no sea un caso de uso común, pero si la lógica comercial lo requiere, el ahorro de memoria de la aplicación y los viajes de ida y vuelta de la base de datos presentados por este enfoque lo convierten en la solución ideal.
El conjunto de columnas en las tablas de origen y destino no tiene que ser idéntico, por supuesto, puede reordenar, reorganizar y usar funciones para manipular los valores en las listas de selección/retorno.
Coalesce
La entrega de valores NULL en el código de la aplicación suele requerir pasos adicionales. En Go, por ejemplo, necesitaría usar tipos como sql.NullString; en Java/JDBC, funciones como resultSet.wasNull() . Estos son engorrosos y propensos a errores.
Si es posible manejar, digamos NULL como cadenas vacías o enteros NULL como 0, en el contexto de una consulta específica, puede usar la función COALESCE. La función COALESCE puede convertir valores NULL en cualquier valor específico. Por ejemplo, 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 obtiene los números de factura y las direcciones de envío de las facturas generadas en enero de 2022. Presumiblemente, shipping_address es NULL si los bienes no tienen que enviarse físicamente. Si el código de la aplicación simplemente quiere mostrar una cadena vacía en algún lugar en tales casos, por ejemplo, es más simple usar COALESCE y eliminar el código de manejo NULL en la aplicación.
También puede usar otras cadenas en lugar de una cadena vacía:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
Incluso puede obtener el primer valor que no sea NULL de una lista, o usar la cadena especificada en su lugar. Por ejemplo, para usar la dirección de facturación o la dirección de envío, puede usar:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
Caso
CASE es otra construcción útil para manejar datos imperfectos de la vida real. Digamos que en lugar de tener valores NULL en shipping_address para artículos que no se pueden enviar, nuestro software de creación de facturas no tan perfecto ha puesto "NO ESPECIFICADO". Le gustaría asignar esto a un NULL o una cadena vacía cuando lea los datos. Puedes usar CASO:
-- 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 tiene una sintaxis poco elegante, pero es funcionalmente similar a las declaraciones de cambio de mayúsculas y minúsculas en lenguajes similares a C. Aquí hay otro ejemplo:
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;
Seleccionar .. union
Los datos de dos (o más) declaraciones SELECT separadas se pueden combinar usando UNION. Por ejemplo, si tiene dos tablas, una con usuarios actuales y otra eliminada, a continuación se indica cómo consultarlas a ambas al mismo tiempo:
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 = ?
Las dos consultas deben tener la misma lista de selección, es decir, deben devolver el mismo número y tipo de columnas.
UNION también elimina los duplicados. Solo se devuelven filas únicas. Si prefiere conservar las filas duplicadas, use "UNION ALL" en lugar de UNION.
Como complemento de UNION, también hay INTERSECT y EXCEPT, consulte los documentos de PostgreSQL para obtener más información.
Seleccione...distinct on
Las filas duplicadas devueltas por SELECT se pueden combinar (es decir, solo se devuelven filas únicas) agregando la palabra clave DISTINCT después de SELECT. Si bien esto es SQL estándar, Postgres proporciona una extensión, "DISTINCT ON". Es un poco complicado de usar, pero en la práctica suele ser la forma más concisa de obtener los resultados que necesita.
Considere un clientes tabla con una fila por cliente y compras tabla con una fila por compras realizadas por (algunos) clientes. La siguiente consulta devuelve todos los clientes, junto con cada una de sus 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 fila de cliente se repite para cada compra que ha realizado. ¿Qué pasa si queremos devolver solo la primera compra de un cliente? Básicamente queremos ordenar las filas por cliente, agrupar las filas por cliente, dentro de cada grupo ordenar las filas por tiempo de compra y finalmente devolver solo la primera fila de cada grupo. En realidad, es más corto escribir eso en SQL con 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;
La cláusula "DISTINCT ON (C.id)" agregada hace exactamente lo que se describió anteriormente. ¡Eso es mucho trabajo con solo unas pocas letras adicionales!
Usando números en orden por cláusula
Considere obtener una lista de nombres de clientes y el código de área de sus números de teléfono de una tabla. Asumiremos que los números de teléfono de EE. UU. están almacenados con el formato (123) 456-7890
. Para otros países, solo diremos "NO DE EE. UU." 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;
Eso está bien, y también tenemos la construcción CASE, pero ¿y si necesitamos ordenarlo por el código de área ahora?
Esto 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;
Pero ¡uf! Repetir la cláusula del caso es feo y propenso a errores. Podríamos escribir una función almacenada que tome el código de país y el teléfono y devuelva el código de área, pero en realidad hay una mejor opción:
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;
¡El "ORDEN POR 3" dice orden por el tercer campo! Debe recordar actualizar el número cuando reorganiza la lista de selección, pero generalmente vale la pena.