PostgreSQL параметризованный Order By/Limit в табличной функции

У меня есть функция sql, которая выполняет простой оператор выбора sql:

CREATE OR REPLACE FUNCTION getStuff(param character varying)
  RETURNS SETOF stuff AS
$BODY$
    select *
    from stuff
    where col = $1
$BODY$
  LANGUAGE sql;

На данный момент я вызываю эту функцию следующим образом:

select * from getStuff('hello');

Какие у меня есть варианты, если мне нужно упорядочить и ограничить результаты пунктами order by и limit?

Я предполагаю, что такой запрос:

select * from getStuff('hello') order by col2 limit 100;

будет не очень эффективно, потому что все строки из таблицы stuff будут возвращены функцией getStuff и только потом упорядочены и нарезаны по лимиту.

Но даже если я прав, нет простого способа передать порядок по аргументу функции языка sql. Можно передавать только значения, а не части инструкции sql.

Другой вариант — создать функцию на языке plpgsql, где можно построить запрос и выполнить его через EXECUTE. Но это тоже не очень хороший подход.

Итак, есть ли другой способ добиться этого? Или какой вариант выбрать? Заказ/ограничение вне функции или plpgsql?

Я использую postgresql 9.1.

Редактировать

Я изменил оператор CREATE FUNCTION следующим образом:

CREATE OR REPLACE FUNCTION getStuff(param character varying, orderby character varying)
  RETURNS SETOF stuff AS
$BODY$
    select t.*
    from stuff t
    where col = $1
    ORDER BY
        CASE WHEN $2 = 'parent' THEN t.parent END,
        CASE WHEN $2 = 'type' THEN t."type" END, 
        CASE WHEN $2 = 'title' THEN t.title END

$BODY$
  LANGUAGE sql;

Выдает:

ОШИБКА: CASE не может совпадать с переменным символом и целым числом. ŘÁDKA 13: КОГДА $1 = 'parent' THEN t.parent

Таблица stuff выглядит так:

CREATE TABLE stuff
    (
      id integer serial,
      "type" integer NOT NULL,
      parent integer,
      title character varying(100) NOT NULL,
      description text,
      CONSTRAINT "pkId" PRIMARY KEY (id),
    )

Редактировать2

Я плохо прочитал код Демса. Я исправил это на вопрос. Этот код работает для меня.


person JoshuaBoshi    schedule 15.11.2011    source источник
comment
Почему использование PL/pgSQL и EXECUTE не лучший подход? Не должно иметь большого значения с точки зрения производительности, и это единственное решение, о котором я могу думать.   -  person a_horse_with_no_name    schedule 15.11.2011
comment
Хм, в основном из-за производительности, которая, как я думал, будет очень низкой по сравнению с функцией языка sql или, по крайней мере, сравнима с select * from getStuff('hello') order by col2 limit 100;, что мне приятнее писать (с точки зрения всего приложения, которое я создаю)   -  person JoshuaBoshi    schedule 15.11.2011
comment
использование EXECUTE будет немного медленнее (из-за дополнительного синтаксического анализа), но я сомневаюсь, что вы сможете измерить разницу.   -  person a_horse_with_no_name    schedule 15.11.2011
comment
@JoshuaBoshi: Догадки о влиянии на производительность обычно не работают.   -  person Mike Sherrill 'Cat Recall'    schedule 15.11.2011
comment
Хорошо :-) не стесняйтесь писать ответ, и я приму его, если никто не предложит другого решения :-)   -  person JoshuaBoshi    schedule 15.11.2011
comment
@Catcall: конечно, я знаю. Я должен сделать некоторые измерения. Но в этом случае я был совершенно уверен, что функции plpgsql на один класс медленнее, чем sql, поэтому мне не пришло в голову что-то измерять.   -  person JoshuaBoshi    schedule 15.11.2011
comment
EXECUTE имеет накладные расходы на синтаксический анализ и компиляцию, но если postgresql позволяет параметризованный динамический sql, это может быть кэшируемым и, таким образом, просто накладными расходами на поиск хэша. Такие накладные расходы практически незаметны, за исключением очень быстро повторяющихся запросов. Также обратите внимание, что альтернативы имеют разные накладные расходы — единый план подходит для всех. И такой план может быть настолько чрезвычайно неэффективным, что наносит вред производительности.   -  person MatBailie    schedule 15.11.2011
comment
@Dems: Итак, если я правильно вас понял, вы рекомендуете использовать скорее EXECUTE, чем ORDER BY CASE?   -  person JoshuaBoshi    schedule 15.11.2011
comment
@JoshuaBoshi - вам нужно будет проверить это для ваших конкретных случаев. Объем данных, допустимые комбинации полей сортировки, доступные индексы, фрагментация данных и т. д. — все это может иметь значение. Динамический SQL с EXECUTE неизменно дает план, производительность которого равна или выше, чем у одного выражения на основе CASE. Но он кажется более беспорядочным, и поэтому его сложнее поддерживать. Тестирование покажет разницу в производительности. Я часто ставлю техническое обслуживание выше производительности, если различия в производительности не заметны.   -  person MatBailie    schedule 15.11.2011
comment
@Dems - Большое спасибо. Ваши комментарии очень полезны!   -  person JoshuaBoshi    schedule 15.11.2011


Ответы (4)


Нет ничего плохого в функции plpgsql для чего-то более сложного. Единственная ситуация, когда производительность может пострадать, — это когда функция plpgsql является вложенной, поскольку планировщик запросов не может оптимизировать содержащийся в нем код в контексте внешнего запроса, что может замедлить его работу, а может и не замедлить.
Подробнее об этом ниже. отвечать:

В данном случае это намного проще, чем множество предложений CASE в запросе:

CREATE OR REPLACE FUNCTION get_stuff(_param text, _orderby text, _limit int)
  RETURNS SETOF stuff AS
$func$
BEGIN
   RETURN QUERY EXECUTE '
      SELECT *
      FROM   stuff
      WHERE  col = $1
      ORDER  BY ' || quote_ident(_orderby) || ' ASC
      LIMIT  $2'
   USING _param, _limit;
END
$func$  LANGUAGE plpgsql;

Вызов:

SELECT * FROM get_stuff('hello', 'col2', 100);

Примечания

Используйте RETURN QUERY EXECUTE для возврата результатов. запроса за один раз.

Используйте quote_ident() для идентификаторов для защиты от SQLi.
Или format() для чего-то более сложного. Видеть:

Передайте значения параметров с помощью USING. пункт, чтобы снова избежать кастинга, цитирования и SQLi.

Будьте осторожны, чтобы не создавать конфликты имен между параметрами и именами столбцов. В примере я добавил к именам параметров знак подчеркивания (_). Просто мои личные предпочтения.

Ваша вторая функция после редактирования не может работать, потому что вы возвращаете только parent, а тип возвращаемого значения объявлен SETOF stuff. Вы можете объявить любой тип возвращаемого значения, но фактические возвращаемые значения должны соответствовать объявлению. Для этого вы можете использовать RETURNS TABLE.

person Erwin Brandstetter    schedule 16.11.2011
comment
Брандштеттер: Ух ты, теперь ты немного научил меня plpgsql :-) Я понятия не имел о RETURN QUERY EXECUTE и USING. Это действительно элегантное решение, и теперь я не беспокоюсь о методе plpgsql. Большое тебе спасибо! - person JoshuaBoshi; 16.11.2011
comment
как я могу добавить ASC или DESC с ним? - person Developer; 11.04.2018
comment
@erwin: я хочу сделать это динамически, как это делается для _orderby. Я хочу пройти по возрастанию или по убыванию динамически. - person Developer; 11.04.2018
comment
@Sachin: Задайте новый вопрос с соответствующими подробностями. Комментарии не к месту. Вы всегда можете сослаться на это, если вам нужен контекст. Есть простые и безопасные решения. - person Erwin Brandstetter; 11.04.2018
comment
Пожалуйста, проверьте этот вопрос. stackoverflow .com/questions/49775242/ - person Developer; 11.04.2018

Если ваша функция стабильна (не изменяет базу данных), планировщик запросов обычно встраивает его. Следовательно, выполнение SELECT * FROM getStuff('x') LIMIT 10 приведет к тому же плану запроса, как если бы ограничение было внутри getStuff().

Однако вам нужно сообщить PG, что ваша функция стабильна, объявив ее как таковую:

CREATE OR REPLACE FUNCTION getStuff(param varchar)
RETURNS setof STUFF
LANGUAGE SQL
STABLE
AS $$ ... $$;

Теперь выполнение EXPLAIN SELECT * FROM getStuff('x') LIMIT 1 должно привести к тому же плану запроса, что и запись эквивалентного запроса.

Встраивание также должно работать для предложений ORDER BY вне функции. Но если вы хотите параметризовать функцию для определения порядка, вы можете сделать это так, чтобы также управлять направлением сортировки:

CREATE FUNCTION sort_stuff(sort_col TEXT, sort_dir TEXT DEFAULT 'asc')
RETURNS SETOF stuff
LANGUAGE SQL
STABLE
AS $$
    SELECT *
    FROM stuff
    ORDER BY
      -- Simplified to NULL if not sorting in ascending order.
      CASE WHEN sort_dir = 'asc' THEN
          CASE sort_col
              -- Check for each possible value of sort_col.
              WHEN 'col1' THEN col1
              WHEN 'col2' THEN col2
              WHEN 'col3' THEN col3
              --- etc.
              ELSE NULL
          END
      ELSE
          NULL
      END
      ASC,

      -- Same as before, but for sort_dir = 'desc'
      CASE WHEN sort_dir = 'desc' THEN
          CASE sort_col
              WHEN 'col1' THEN col1
              WHEN 'col2' THEN col2
              WHEN 'col3' THEN col3
              ELSE NULL
          END
      ELSE
          NULL
      END
      DESC
$$;

Пока sort_col и sort_dir постоянны в запросе, планировщик запросов должен иметь возможность упростить многословный запрос до

SELECT *
FROM stuff
ORDER BY <sort_col> <sort_dir>

что вы можете проверить с помощью EXPLAIN.

person dmg    schedule 27.10.2015

Что касается ORDER BY, вы можете попробовать что-то вроде этого:

SELECT
    <column list>
FROM
    Stuff
WHERE
    col1 = $1
ORDER BY
    CASE $2
        WHEN 'col1' THEN col1
        WHEN 'col2' THEN col2
        WHEN 'col3' THEN col3
        ELSE col1  -- Or whatever your default should be
    END

Возможно, вам придется выполнить некоторые преобразования типов данных, чтобы все типы данных в результате CASE совпадали. Просто будьте осторожны при преобразовании чисел в строки — вам нужно будет добавить 0, чтобы они правильно упорядочивались. То же самое касается значений даты/времени. Упорядочить по формату, в котором год следует за месяцем, за которым следует день и т. д.

Я делал это в SQL Server, но никогда в PostgreSQL, и у меня нет копии PostgreSQL на этой машине, так что это не проверено.

person Tom H    schedule 15.11.2011
comment
Чтобы избежать проблемы с типом данных... ORDER BY CASE WHEN $2 = 'a' THEN a END, CASE WHEN $2 = 'b' THEN b END, etc, etc. Но обратите внимание, у этого есть та же проблема оптимизации, о которой я упоминал в ответе Soulcheck. - person MatBailie; 15.11.2011
comment
Это не проблема типа данных, о которой я говорил. Очевидно, что $2 всегда будет одним и тем же типом данных. Если столбец a и столбец b имеют разные типы данных, это может вызвать некоторые проблемы. - person Tom H; 15.11.2011
comment
Пример, который я привел, имеет дело с этим, поскольку каждое поле является отдельным предложением в ORDER BY. Операторы CASE дают ORDER BY null, null, x, null (например) и, таким образом, обеспечивают независимость от типа данных. - person MatBailie; 15.11.2011
comment
Спасибо за ваши ответы и комментарии. Я использую версию CASE от Dems, но postgresql выдает: ERROR: CASE types character varying and integer cannot be matched, когда я пытаюсь создать функцию (в позиции после THEN - странно). Я отредактирую вопрос и добавлю полный источник... - person JoshuaBoshi; 15.11.2011
comment
Как раз та проблема, о которой я говорил. Вы должны убедиться, что вы конвертируете все результаты в один и тот же тип данных. - person Tom H; 15.11.2011
comment
Требует ли postgres оператор ELSE? ORDER BY (CASE WHEN $2 = 'a' THEN table.a ELSE NULL END), (CASE WHEN $2 = 'b' THEN table.b ELSE NULL END), etc, etc? - person MatBailie; 15.11.2011
comment
@TomH Извините, я плохо прочитал код демонстрационной версии и не создавал отдельный CASE для каждой ветки. Теперь это работает, и это имеет смысл :-) - person JoshuaBoshi; 15.11.2011
comment
@Dems Извините, см. мой предыдущий комментарий здесь .-X - person JoshuaBoshi; 15.11.2011

Вы можете без проблем передать предельное значение в качестве аргумента функции. Что касается заказа, вы можете использовать ODER BY в сочетании с оператором CASE. К сожалению, это не сработает для чего-то вроде

ORDER BY CASE condition_variable
WHEN 'asc' THEN column_name ASC
ELSE column_name DESC
END;
person soulcheck    schedule 15.11.2011
comment
1) нужно заменить 1 на a. 2) Это не позволит оптимизатору использовать индексы и т. д. Он действительно дает один запрос для нескольких целей, но это следует делать с помощью динамического sql, чтобы проверить, каковы накладные расходы на производительность. (Для каждого отдельного запроса может быть создан только один план, но разный порядок предложений может требовать эффективности разных планов.) - person MatBailie; 15.11.2011
comment
Это правильно, он упорядочивает по первому столбцу по убыванию или возрастанию в зависимости от того, равно ли a 'asc' или нет. но я все равно отредактирую, чтобы было понятнее. - person soulcheck; 15.11.2011
comment
Это незаконно. Вы не можете использовать ASC или DESC внутри выражения. - person asnyder; 06.03.2014
comment
@asnyder да, это был пример того, что не сработает ;) Очень старый ответ, поэтому качество оставляет желать лучшего. - person soulcheck; 07.03.2014