Consultas como SELECT * FROM t ORDER BY case when _parameter='a' then column_a end, case when _parameter='b' then column_b end
são possíveis, mas: Esta é uma boa prática?
É comum usar parâmetros na parte WHERE das consultas e ter algumas colunas computadas na parte SELECT, mas não tão comum para parametrizar a cláusula ORDER BY.
Digamos que temos um aplicativo que lista carros usados (à la CraigsList). A lista de carros pode ser classificada por preço ou cor. temos uma função que, dada uma certa quantidade de parâmetros (digamos, faixa de preço, cor e critérios de classificação, por exemplo) retorna um conjunto de registros com os resultados.
Para torná-lo concreto, vamos supor que cars
estão todos na tabela a seguir:
CREATE TABLE cars
(
car_id serial NOT NULL PRIMARY KEY, /* arbitrary anonymous key */
make text NOT NULL, /* unnormalized, for the sake of simplicity */
model text NOT NULL, /* unnormalized, for the sake of simplicity */
year integer, /* may be null, meaning unknown */
euro_price numeric(12,2), /* may be null, meaning seller did not disclose */
colour text /* may be null, meaning unknown */
) ;
A tabela teria índices para a maioria das colunas...
CREATE INDEX cars_colour_idx
ON cars (colour);
CREATE INDEX cars_price_idx
ON cars (price);
/* etc. */
E tenha algumas enumerações de commodities:
CREATE TYPE car_sorting_criteria AS ENUM
('price',
'colour');
... e alguns dados de exemplo
INSERT INTO cars.cars (make, model, year, euro_price, colour)
VALUES
('Ford', 'Mondeo', 1990, 2000.00, 'green'),
('Audi', 'A3', 2005, 2500.00, 'golden magenta'),
('Seat', 'Ibiza', 2012, 12500.00, 'dark blue'),
('Fiat', 'Punto', 2014, NULL, 'yellow'),
('Fiat', '500', 2010, 7500.00, 'blueish'),
('Toyota', 'Avensis', NULL, 9500.00, 'brown'),
('Lexus', 'CT200h', 2012, 12500.00, 'dark whitish'),
('Lexus', 'NX300h', 2013, 22500.00, NULL) ;
O tipo de consultas que vamos fazer são do estilo:
SELECT
make, model, year, euro_price, colour
FROM
cars.cars
WHERE
euro_price between 7500 and 9500
ORDER BY
colour ;
Gostaríamos de ter consultas desse estilo em uma função:
CREATE or REPLACE FUNCTION get_car_list
(IN _colour text,
IN _min_price numeric,
IN _max_price numeric,
IN _sorting_criterium car_sorting_criteria)
RETURNS record AS
$BODY$
SELECT
make, model, year, euro_price, colour
FROM
cars
WHERE
euro_price between _min_price and _max_price
AND colour = _colour
ORDER BY
CASE WHEN _sorting_criterium = 'colour' THEN
colour
END,
CASE WHEN _sorting_criterium = 'price' THEN
euro_price
END
$BODY$
LANGUAGE SQL ;
Em vez dessa abordagem, o SQL nessa função poderia ser gerado dinamicamente (em PL/pgSQL) como uma string e depois EXECUTADO.
Podemos sentir algumas limitações, vantagens e desvantagens com qualquer uma das abordagens:
- Dentro de uma função, descobrir qual é o plano de consulta para uma determinada instrução é difícil (se possível). No entanto, tendemos a usar funções principalmente quando vamos usar algo com bastante frequência.
- Erros em SQL estático serão capturados (principalmente) quando a função for compilada ou quando for chamada pela primeira vez.
- Erros em SQL dinâmico só serão detectados (principalmente) depois que a função for compilada e todos os caminhos de execução forem verificados (ou seja: o número de testes a serem executados na função pode ser muito alto).
- Uma consulta paramétrica como a exposta provavelmente será menos eficiente do que uma gerada dinamicamente; no entanto, o executor terá um trabalho mais difícil de analisar / criar a árvore de consulta / decidir todas as vezes (o que pode afetar a eficiência na direção oposta).
Pergunta:
Como "obter o melhor dos dois mundos" (se possível)? [Eficiência + Verificações do compilador + Depuração fácil + Otimização fácil]
NOTA: isso deve ser executado no PostgreSQL 9.6.
Três pontos que eu levantaria,
VIEW
para isso. Faça com que seus usuários personalizem asWHERE
condições usando o arquivoVIEW
. As funções são caixas pretas para o planejador de consulta. É horrível usá-los dentro de outras funções, só queSQL
é embutido. E as funções dinâmicas não obtêm planos em cache.RETURNS QUERY
(ouRETURNS QUERY EXECUTE
) notRETURNS SETOF
. Não há razão para usarRETURNS SETOF
com uma classificação. Tem que ser armazenado em buffer de qualquer maneira, afaik. Você encontrará problemas com qualquer um deles se o conjunto de resultados for maior quework_mem
.Avançando, eu até consideraria agrupar um serviço como o PostgREST, que lida com pedidos completamente arbitrários,
Resposta geral
Primeiro, quero abordar a ambigüidade na premissa:
As colunas calculadas na
SELECT
parte quase nunca são relevantes para o plano de consulta ou desempenho. Mas "naWHERE
parte" é ambíguo.É comum parametrizar valores na
WHERE
cláusula, o que funciona para instruções preparadas. (E o PL/pgSQL trabalha com instruções preparadas internamente.) Um plano de consulta genérico geralmente faz sentido independentemente dos valores fornecidos . Ou seja, a menos que as tabelas tenham uma distribuição de dados muito desigual, mas como o Postgres 9.2 PL/pgSQL replaneja as consultas algumas vezes para testar se o plano genérico parece bom o suficiente:Mas não é tão comum parametrizar predicados inteiros (incluindo identificadores ) na
WHERE
cláusula, o que é impossível com instruções preparadas para começar. Você precisa de SQL dinâmico comEXECUTE
, ou monta as strings de consulta no cliente.As expressões dinâmicas
ORDER BY
estão em algum lugar entre as duas. Você pode fazer isso com umaCASE
expressão, mas isso é muito difícil de otimizar em geral. O Postgres pode usar índices com um simplesORDER BY
, mas não comCASE
expressões que escondam a eventual ordem de classificação. O planejador é inteligente, mas não uma IA. Dependendo do restante da consulta (ORDER BY
pode ser relevante para o plano ou não - é relevante no seu exemplo), você pode acabar com um plano de consulta abaixo do ideal o tempo todo .Além disso, você adiciona o menor custo da
CASE
(s) expressão(ões). E no seu exemplo particular também para váriasORDER BY
colunas inúteis .Normalmente, SQL dinâmico com
EXECUTE
é mais rápido ou muito mais rápido para isso.A capacidade de manutenção não deve ser um problema se você mantiver um formato de código claro e legível no corpo da função.
Corrigir função de demonstração
A função na pergunta está quebrada . O tipo de retorno é definido para retornar um registro anônimo:
Mas a consulta na verdade retorna um conjunto de registros, teria que ser:
Mas isso ainda é inútil. Você teria que fornecer uma lista de definição de coluna com cada chamada. Sua consulta retorna colunas de tipo conhecido. Declare o tipo de retorno de acordo! Estou supondo aqui, use tipos de dados reais de colunas/expressões retornadas:
Eu uso os mesmos nomes de coluna por conveniência. As colunas na
RETURNS TABLE
cláusula são efetivamenteOUT
parâmetros, visíveis em todas as instruções SQL no corpo (mas não dentroEXECUTE
de ). Portanto, qualifique colunas em consultas no corpo da função para evitar possíveis conflitos de nomenclatura. A função de demonstração funcionaria assim:Não confunda a
RETURNS
palavra-chave na declaração da função com oRETURN
comando plpgsql como Evan fez em sua resposta . Detalhes:Dificuldade geral da consulta de exemplo
Predicado em algumas colunas (ainda pior: predicados de intervalo ), outras colunas em
ORDER BY
, isso já é difícil de otimizar. Mas você mencionou em um comentário :Portanto, você adicionará
LIMIT
eOFFSET
a essas consultas, retornando as n "melhores" correspondências primeiro. Ou alguma técnica de paginação mais inteligente:Você precisa de um índice correspondente para tornar isso rápido. Não vejo como isso poderia funcionar com
CASE
expressões emORDER BY
.Considerar:
Não faz sentido ter um caso na ordem de uma função no caso descrito
Os índices não serão usados na cláusula order by dessa forma. O mecanismo de banco de dados terá que calcular a expressão de caso para cada linha e depois classificar. E essa técnica não é extensível para junções e filtros dinâmicos.
Eu iria com uma geração dinâmica de SQL...
Gerando SQL dinâmico em uma função PostgreSQL
Você pode fazer algo assim:
Não costumo fazer SQL dinâmico para algo tão específico da camada de apresentação dentro do PL/pgSQL. Normalmente prefiro deixar essa construção SQL em PHP ou Java. Mas para muitas outras coisas eu faço muito SQL dinâmico dentro do PL/pgSQL. Principalmente para particionamento, manutenção de banco de dados, implementação de fluxo de trabalho e controles de consistência de dados.
Eu descobri que esta política deixa o código mais limpo, tem melhor uso de índices e é utilizável em SQL dinâmico mais complicado. O uso do índice é crítico no meu mundo porque trabalho com análises em bancos de dados com vários bilhões de registros e preciso de respostas rápidas.
Notas adicionais sobre por que é mais comum gerar SQL dinâmico fora do banco de dados (a seguir comentários)
Isso acontece porque a maioria do SQL dinâmico só é necessário quando um nome de tabela ou um nome de coluna é dinâmico. Para todo o resto, as consultas parametrizadas funcionarão bem. O SQL dinâmico é necessário para a maioria dos relatórios. Não apenas porque você precisará escolher a coluna de ordem de classificação, mas na maioria das vezes você precisará incluir filtros e colunas dinamicamente. Muitos desenvolvedores continuam relatando SQL nos programas de relatório na camada de apresentação ou nos lotes que os geram, caso em que o SQL dinâmico também será gerado fora do banco de dados.
Eu usaria SQL dinâmico neste caso.
A consulta dinâmica é menos complexa e pode render um melhor plano de execução.
Pela minha experiência, qualquer ganho com o armazenamento em cache do plano ou tempo de análise mais curto é insignificante perto disso.
Eu costumava seguir a regra de "Usar SQL dinâmico somente se o SQL estático não funcionar". Hoje, como regra geral, costumo escolhê-lo de acordo com a cláusula da consulta SQL em que preciso de flexibilidade:
É verdade que é um pouco mais difícil de depurar, mas o uso de algumas práticas recomendadas deve ajudar a reduzir a lacuna.
Por exemplo, em plpgsql você pode usar uma notação $ + REPLACE + RAISE NOTICE:
Você deve ser capaz de interceptar quaisquer erros de sintaxe nas primeiras execuções.
Erros lógicos serão tão fáceis/difíceis de encontrar quanto com SQL estático.