sql >> Base de Datos >  >> RDS >> PostgreSQL

Prueba de función nula con parámetros variables

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 y id como nombres de columna. No son descriptivos y cuando te unes a un montón de tablas (como lo haces con a lot en una base de datos relacional), termina con varias columnas, todas llamadas name o id , 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.