PostgreSQL параметризира Поръчка по/Ограничение в таблична функция

Имам sql функция, която прави прост sql select оператор:

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: WHEN $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

Лошо прочетох кода на Dems. Коригирах го на въпрос. Този код работи за мен.


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
Brandstetter: Уау, сега ме научихте малко 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
Благодаря ви за отговорите и коментарите. Използвам версията на Dems на CASE, но 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 Съжалявам, прочетох зле кода на dems и не съм направил отделен 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