Je ne suis pas d'accord avec certains des conseils des autres réponses. Cela peut être fait avec PL/pgSQL et je pense que c'est généralement de loin supérieur à l'assemblage de requêtes dans une application cliente. C'est plus rapide et plus propre et l'application n'envoie que le strict minimum sur le fil dans les demandes. Les instructions SQL sont enregistrées dans la base de données, ce qui facilite la maintenance - à moins que vous ne souhaitiez collecter toute la logique métier dans l'application cliente, cela dépend de l'architecture générale.
Fonction PL/pgSQL avec SQL dynamique
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$;
Appel :
SELECT * FROM func(1, '_ad_nr_extra', '_ad_info', '_ad_postcode', '_sname');
SELECT * FROM func(1, _pname := 'foo');
Étant donné que tous les paramètres de fonction ont des valeurs par défaut, vous pouvez utiliser positionnel notation, nommée notation ou mixte notation à votre choix dans l'appel de fonction. Voir :
- Fonctions avec un nombre variable de paramètres d'entrée
Plus d'explications sur les bases du SQL dynamique :
- Refactoriser une fonction PL/pgSQL pour renvoyer la sortie de diverses requêtes SELECT
Le concat()
La fonction est instrumentale pour la construction de la chaîne. Il a été introduit avec Postgres 9.1.
Le ELSE
branche d'un CASE
l'instruction par défaut est NULL
lorsqu'il n'est pas présent. Simplifie le code.
Le USING
clause pour EXECUTE
rend l'injection SQL impossible car les valeurs sont transmises en tant que valeurs et permet d'utiliser directement les valeurs des paramètres, exactement comme dans les instructions préparées.
NULL
les valeurs sont utilisées pour ignorer les paramètres ici. Ils ne sont pas réellement utilisés pour effectuer des recherches.
Vous n'avez pas besoin de parenthèses autour du SELECT
avec RETURN QUERY
.
Fonction SQL simple
Vous pourriez faites-le avec une fonction SQL simple et évitez le SQL dynamique. Dans certains cas, cela peut être plus rapide. Mais je ne m'y attendrais pas dans ce cas . La planification de la requête sans jointures ni prédicats inutiles produit généralement de meilleurs résultats. Le coût de planification d'une requête simple comme celle-ci est presque négligeable.
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$;
Appel identique.
Pour ignorer efficacement les paramètres avec NULL
valeurs :
($1 IS NULL OR a.ad_nr = $1)
Pour utiliser réellement les valeurs NULL comme paramètres , utilisez cette construction à la place :
($1 IS NULL AND a.ad_nr IS NULL OR a.ad_nr = $1) -- AND binds before OR
Cela permet également des index à utiliser.
Dans le cas présent, remplacez toutes les instances de LEFT JOIN
avec JOIN
.
db<>jouez ici - avec démo simple pour toutes les variantes.
Ancien sqlfiddle
En passant
-
N'utilisez pas
name
etid
comme noms de colonne. Ils ne sont pas descriptifs et lorsque vous rejoignez un groupe de tables (comme vous le faites poura lot
dans une base de données relationnelle), vous vous retrouvez avec plusieurs colonnes toutes nomméesname
ouid
, et doivent attacher des alias pour trier le désordre. -
Veuillez formater votre SQL correctement, au moins lorsque vous posez des questions publiques. Mais faites-le aussi en privé, pour votre propre bien.