Eu discordo de alguns dos conselhos em outras respostas. Isso pode ser feito com PL/pgSQL e acho que é principalmente muito superior para montar consultas em um aplicativo cliente. É mais rápido e mais limpo e o aplicativo envia apenas o mínimo necessário em solicitações. As instruções SQL são salvas dentro do banco de dados, o que facilita a manutenção - a menos que você queira coletar toda a lógica de negócios no aplicativo cliente, isso depende da arquitetura geral.
Função PL/pgSQL com 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$;
Ligar:
SELECT * FROM func(1, '_ad_nr_extra', '_ad_info', '_ad_postcode', '_sname');
SELECT * FROM func(1, _pname := 'foo');
Como todos os parâmetros de função têm valores padrão, você pode usar posicional notação, nomeado notação ou misto notação à sua escolha na chamada da função. Ver:
- Funções com número variável de parâmetros de entrada
Mais explicações para noções básicas de SQL dinâmico:
- Refatorar uma função PL/pgSQL para retornar a saída de várias consultas SELECT
O
concat()
função é fundamental para construir a string. Foi introduzido com o Postgres 9.1. O
ELSE
ramificação de um CASE
declaração padrão para NULL
quando não está presente. Simplifica o código. O
USING
cláusula para EXECUTE
impossibilita a injeção de SQL, pois os valores são passados como valores e permite usar valores de parâmetro diretamente, exatamente como em instruções preparadas. NULL
valores são usados para ignorar parâmetros aqui. Eles não são realmente usados para pesquisar. Você não precisa de parênteses ao redor do
SELECT
com RETURN QUERY
. Função SQL simples
Você poderia faça isso com uma função SQL simples e evite SQL dinâmico. Para alguns casos, isso pode ser mais rápido. Mas eu não esperaria isso neste caso . Planejar a consulta sem junções e predicados desnecessários normalmente produz melhores resultados. O custo de planejamento para uma consulta simples como essa é quase 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$;
Chamada idêntica.
Para efetivamente ignorar parâmetros com
NULL
valores :($1 IS NULL OR a.ad_nr = $1)
Para usar valores NULL como parâmetros , use esta construção em vez disso:
($1 IS NULL AND a.ad_nr IS NULL OR a.ad_nr = $1) -- AND binds before OR
Isso também permite índices a ser usado.
Para o caso em questão, substitua todas as instâncias de
LEFT JOIN
com JOIN
. db<>mexa aqui - com demonstração simples para todas as variantes.
Antigo sqlfiddle
Apartes
-
Não usename
eid
como nomes de colunas. Eles não são descritivos e quando você junta um monte de tabelas (como você faz paraa lot
em um banco de dados relacional), você acaba com várias colunas, todas chamadasname
ouid
, e tem que anexar aliases para classificar a bagunça.
-
Por favor, formate seu SQL corretamente, pelo menos ao fazer perguntas públicas. Mas faça isso em particular também, para o seu próprio bem.