No estoy de acuerdo con algunos de los consejos en otras respuestas. Esto se puede hacer con PL/pgSQL y creo que es mayormente muy superior para ensamblar consultas en una aplicación cliente. Es más rápido y limpio, y la aplicación solo envía solicitudes mínimas a través del cable. Las instrucciones SQL se guardan dentro de la base de datos, lo que facilita su mantenimiento, a menos que desee recopilar toda la lógica comercial en la aplicación cliente, esto depende de la arquitectura general.
Función PL/pgSQL con SQL dinámico
CREATE OR REPLACE FUNCTION func(
_ad_nr int = NULL
, _ad_nr_extra text = NULL
, _ad_info text = NULL
, _ad_postcode text = NULL
, _sname text = NULL
, _pname text = NULL
, _cname text = NULL)
RETURNS TABLE(id int, match text, score int, nr int, nr_extra text
, info text, postcode text, street text, place text
, country text, the_geom geometry)
LANGUAGE plpgsql AS
$func$
BEGIN
-- RAISE NOTICE '%', -- for debugging
RETURN QUERY EXECUTE concat(
$$SELECT a.id, 'address'::text, 1 AS score, a.ad_nr, a.ad_nr_extra
, a.ad_info, a.ad_postcode$$
, CASE WHEN (_sname, _pname, _cname) IS NULL THEN ', NULL::text' ELSE ', s.name' END -- street
, CASE WHEN (_pname, _cname) IS NULL THEN ', NULL::text' ELSE ', p.name' END -- place
, CASE WHEN _cname IS NULL THEN ', NULL::text' ELSE ', c.name' END -- country
, ', a.wkb_geometry'
, concat_ws('
JOIN '
, '
FROM "Addresses" a'
, CASE WHEN NOT (_sname, _pname, _cname) IS NULL THEN '"Streets" s ON s.id = a.street_id' END
, CASE WHEN NOT (_pname, _cname) IS NULL THEN '"Places" p ON p.id = s.place_id' END
, CASE WHEN _cname IS NOT NULL THEN '"Countries" c ON c.id = p.country_id' END
)
, concat_ws('
AND '
, '
WHERE TRUE'
, CASE WHEN $1 IS NOT NULL THEN 'a.ad_nr = $1' END
, CASE WHEN $2 IS NOT NULL THEN 'a.ad_nr_extra = $2' END
, CASE WHEN $3 IS NOT NULL THEN 'a.ad_info = $3' END
, CASE WHEN $4 IS NOT NULL THEN 'a.ad_postcode = $4' END
, CASE WHEN $5 IS NOT NULL THEN 's.name = $5' END
, CASE WHEN $6 IS NOT NULL THEN 'p.name = $6' END
, CASE WHEN $7 IS NOT NULL THEN 'c.name = $7' END
)
)
USING $1, $2, $3, $4, $5, $6, $7;
END
$func$;
Llamar:
SELECT * FROM func(1, '_ad_nr_extra', '_ad_info', '_ad_postcode', '_sname');
SELECT * FROM func(1, _pname := 'foo');
Dado que todos los parámetros de función tienen valores predeterminados, puede usar posicional notación, nombrado notación o mixto notación a su elección en la llamada de función. Ver:
- Funciones con número variable de parámetros de entrada
Más explicación de los conceptos básicos de SQL dinámico:
- Refactorice una función PL/pgSQL para devolver el resultado de varias consultas SELECT
El concat()
La función es fundamental para construir la cadena. Se introdujo con Postgres 9.1.
El ELSE
rama de un CASE
declaración predeterminada a NULL
cuando no está presente. Simplifica el código.
El USING
cláusula para EXECUTE
hace que la inyección SQL sea imposible ya que los valores se pasan como valores y permite usar valores de parámetros directamente, exactamente como en declaraciones preparadas.
NULL
los valores se utilizan para ignorar los parámetros aquí. En realidad, no se usan para buscar.
No necesita paréntesis alrededor de SELECT
con RETURN QUERY
.
Función SQL sencilla
podrías hágalo con una función SQL simple y evite el SQL dinámico. Para algunos casos esto puede ser más rápido. Pero no lo esperaría en este caso . La planificación de la consulta sin uniones y predicados innecesarios normalmente produce mejores resultados. El costo de planificación para una consulta simple como esta es casi insignificante.
CREATE OR REPLACE FUNCTION func_sql(
_ad_nr int = NULL
, _ad_nr_extra text = NULL
, _ad_info text = NULL
, _ad_postcode text = NULL
, _sname text = NULL
, _pname text = NULL
, _cname text = NULL)
RETURNS TABLE(id int, match text, score int, nr int, nr_extra text
, info text, postcode text, street text, place text
, country text, the_geom geometry)
LANGUAGE sql AS
$func$
SELECT a.id, 'address' AS match, 1 AS score, a.ad_nr, a.ad_nr_extra
, a.ad_info, a.ad_postcode
, s.name AS street, p.name AS place
, c.name AS country, a.wkb_geometry
FROM "Addresses" a
LEFT JOIN "Streets" s ON s.id = a.street_id
LEFT JOIN "Places" p ON p.id = s.place_id
LEFT JOIN "Countries" c ON c.id = p.country_id
WHERE ($1 IS NULL OR a.ad_nr = $1)
AND ($2 IS NULL OR a.ad_nr_extra = $2)
AND ($3 IS NULL OR a.ad_info = $3)
AND ($4 IS NULL OR a.ad_postcode = $4)
AND ($5 IS NULL OR s.name = $5)
AND ($6 IS NULL OR p.name = $6)
AND ($7 IS NULL OR c.name = $7)
$func$;
Llamada idéntica.
Para ignorar efectivamente los parámetros con NULL
valores :
($1 IS NULL OR a.ad_nr = $1)
Para usar realmente valores NULL como parámetros , use esta construcción en su lugar:
($1 IS NULL AND a.ad_nr IS NULL OR a.ad_nr = $1) -- AND binds before OR
Esto también permite índices para ser utilizado.
Para el caso en cuestión, reemplace todas las instancias de LEFT JOIN
con JOIN
.
db<>violín aquí - con demostración sencilla para todas las variantes.
Sqlfiddle antiguo
Aparte
-
No use
name
yid
como nombres de columna. No son descriptivos y cuando te unes a un montón de tablas (como lo haces cona lot
en una base de datos relacional), termina con varias columnas, todas llamadasname
oid
, y tienes que adjuntar alias para solucionar el problema. -
Formatee su SQL correctamente, al menos cuando haga preguntas públicas. Pero hazlo también en privado, por tu propio bien.