Имя таблицы как параметр функции PostgreSQL

Я хочу передать имя таблицы в качестве параметра функции Postgres. Я пробовал этот код:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

И у меня получилось вот это:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

И вот ошибка, которую я получил при переходе на этот select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Вероятно, quote_ident($1) работает, потому что без части where quote_ident($1).id=1 я получаю 1, что означает, что что-то выбрано. Почему первый quote_ident($1) может работать, а второй - нет одновременно? И как это можно было решить?


person John Doe    schedule 22.05.2012    source источник
comment
Я знаю, что это старый вопрос, но я нашел его, когда искал ответ на другой вопрос. Не могла ли ваша функция просто запросить информационную_схему? Я имею в виду, что это своего рода то, для чего он нужен - чтобы вы могли запрашивать и видеть, какие объекты существуют в базе данных. Просто идея.   -  person David S    schedule 26.09.2012
comment
@DavidS Спасибо за комментарий, я попробую.   -  person John Doe    schedule 26.09.2012


Ответы (8)


Это можно еще больше упростить и улучшить:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

Вызов с именем, указанным в схеме (см. Ниже):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

Or:

SELECT some_f('"my very uncommon table name"');

Основные моменты

Вот почему вы всегда правильно избегаете ввода пользователя для динамического SQL:

db ‹› fiddle здесь, демонстрирующий внедрение SQL
< sub> Старый sqlfiddle

person Erwin Brandstetter    schedule 22.05.2012
comment
@suhprano: Конечно. Попробуйте: DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$; - person Erwin Brandstetter; 23.02.2015
comment
почему% s, а не% L? - person Lotus; 02.06.2017
comment
@Lotus: Объяснение кроется в ответе. regclass значения экранируются автоматически при выводе в виде текста. %L в этом случае будет неправильным. - person Erwin Brandstetter; 02.06.2017
comment
CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; создать функцию подсчета строк таблицы, select table_rows('nf_part1'); - person Ferris; 22.05.2019
comment
как мы можем получить все столбцы? - person Ashish; 22.04.2020

Если возможно, не делайте этого.

Это ответ - это антипаттерн. Если клиент знает таблицу, из которой ему нужны данные, то SELECT FROM ThatTable. Если база данных спроектирована таким образом, чтобы это требовалось, кажется, что она спроектирована неоптимально. Если уровень доступа к данным должен знать, существует ли значение в таблице, легко составить SQL в этом коде, и вставлять этот код в базу данных нецелесообразно.

Для меня это похоже на установку устройства внутри лифта, где можно ввести номер желаемого этажа. После нажатия кнопки Go он перемещает механическую руку к нужной кнопке для нужного этажа и нажимает ее. Это создает множество потенциальных проблем.

Обратите внимание: здесь нет намерения насмехаться. Мой глупый пример с лифтом был * самым лучшим устройством, которое я мог представить * для лаконичного указания на проблемы с этой техникой. Он добавляет бесполезный уровень косвенного обращения, перемещая выбор имени таблицы из пространства вызывающего абонента (с использованием надежного и хорошо понятного DSL, SQL) в гибрид с использованием непонятного / причудливого кода SQL на стороне сервера.

Такое разделение ответственности за счет перемещения логики построения запроса в динамический SQL затрудняет понимание кода. Это нарушает стандартное и надежное соглашение (как SQL-запрос выбирает, что выбрать) в названии настраиваемого кода чревато ошибкой.

Вот подробные сведения о некоторых потенциальных проблемах, связанных с этим подходом:

  • Динамический SQL предлагает возможность SQL-инъекции, которую трудно распознать в коде внешнего интерфейса или только в коде серверной части (чтобы увидеть это, нужно изучить их вместе).

  • Сохраненные процедуры и функции могут получать доступ к ресурсам, на которые владелец SP / функции имеет права, а вызывающий - нет. Насколько я понимаю, без особой осторожности, то по умолчанию, когда вы используете код, который производит динамический SQL и запускает его, база данных выполняет динамический SQL под правами вызывающей стороны. Это означает, что вы либо вообще не сможете использовать привилегированные объекты, либо вам придется открыть их для всех клиентов, увеличивая поверхность потенциальной атаки на привилегированные данные. Установка SP / функции во время создания на постоянный запуск от имени конкретного пользователя (в SQL Server EXECUTE AS) может решить эту проблему, но усложняет задачу. Это усугубляет риск SQL-инъекции, упомянутый в предыдущем пункте, делая динамический SQL очень заманчивым вектором атаки.

  • Когда разработчик должен понять, что делает код приложения, чтобы изменить его или исправить ошибку, ему будет очень трудно получить точный выполняемый SQL-запрос. Профилировщик SQL можно использовать, но это требует особых привилегий и может отрицательно сказаться на производительности производственных систем. Выполненный запрос может регистрироваться SP, но это увеличивает сложность с сомнительной выгодой (требует размещения новых таблиц, очистки старых данных и т. Д.) И совершенно неочевидно. Фактически, некоторые приложения спроектированы таким образом, что разработчик не имеет учетных данных базы данных, поэтому для него становится практически невозможным фактически увидеть отправляемый запрос.

  • При возникновении ошибки, например, при попытке выбрать несуществующую таблицу, вы получите сообщение в строке «недопустимое имя объекта» из базы данных. Это произойдет точно так же, независимо от того, составляете ли вы SQL в серверной части или в базе данных, но разница в том, что какой-то плохой разработчик, пытающийся устранить неполадки в системе, должен углубиться на один уровень глубже в еще одну пещеру ниже той, где находится Проблема существует, чтобы покопаться в чудо-процедуре, которая делает все, чтобы попытаться выяснить, в чем проблема. В журналах не будет отображаться «Ошибка в GetWidget», будет отображаться «Ошибка в OneProcedureToRuleThemAllRunner». Эта абстракция обычно делает систему хуже.

Пример на псевдо-C # переключения имен таблиц на основе параметра:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

Хотя это не устраняет все возможные проблемы, которые можно вообразить, недостатки, которые я обозначил с помощью другой техники, отсутствуют в этом примере.

person ErikE    schedule 11.07.2012
comment
Я не совсем согласен с этим. Допустим, вы нажимаете кнопку Go, а затем какой-то механизм проверяет, существует ли пол. Функции могут использоваться в триггерах, которые, в свою очередь, могут проверять некоторые условия. Это решение может быть не самым красивым, но если система уже достаточно велика и вам нужно внести некоторые коррективы в ее логику, что ж, я полагаю, что этот выбор не так драматичен. - person John Doe; 11.07.2012
comment
Но учтите, что действие попытки нажать кнопку, которой не существует, просто вызовет исключение, независимо от того, как вы с этим справитесь. На самом деле вы не можете нажать несуществующую кнопку, поэтому нет никакой пользы от добавления поверх нажатия кнопки слоя для проверки несуществующих чисел, поскольку такая запись числа не существовала до того, как вы создали указанный слой! На мой взгляд, абстракция - самый мощный инструмент программирования. Однако добавление слоя, который просто плохо дублирует существующую абстракцию, неправильно. Сама база данных уже является слоем абстракции, который сопоставляет имена с наборами данных. - person ErikE; 01.01.2014
comment
Пятно на. Вся суть SQL состоит в том, чтобы выразить набор данных, которые вы хотите извлечь. Единственное, что делает эта функция, - инкапсулирует стандартный оператор SQL. Учитывая тот факт, что идентификатор также жестко запрограммирован, все это имеет неприятный запах. - person Nick Hristov; 19.05.2014
comment
Как насчет пакета, который угадывает структуру базы данных, имена таблиц и т. Д., И в какой-то момент он также хочет узнать количество строк в найденных им таблицах. - person arivero; 17.01.2015
comment
@arivero пакет? О чем мы здесь говорим? Не может пакет построить свой запрос, используя известное имя таблицы? Вы не предлагаете ничего другого, чем я уже говорил. - person ErikE; 28.01.2015
comment
У меня есть база данных, предоставленная сторонним приложением. Я хочу найти все таблицы, которые были обновлены с момента отметки времени записи в этой базе данных. Я думаю, что могу сделать это с помощью EXECUTE, но, очевидно, это неправильный способ. Как правильно? - person AixNPanes; 29.04.2015
comment
Пожалуйста, задайте новый вопрос. Комментарии - не лучший способ получить ответы. - person ErikE; 30.04.2015
comment
Это та же самая логика, которая использовалась, чтобы отпугнуть всех от gotos в коде C. В целом это хороший совет, но не всегда плохой. - person three-cups; 29.05.2015
comment
@three До тех пор, пока кто-то не достигнет фазы мастерства (см. модель приобретения навыков Дрейфуса) навыка, он должен просто полностью подчиняться таким правилам, как НЕ передавать имена таблиц в процедуру, которая будет использоваться в динамическом SQL. Даже намек на то, что это не всегда плохо, - это сам по себе плохой совет. Зная это, у новичка возникнет соблазн им воспользоваться! Плохо. Только мастера темы должны нарушать правила, поскольку они единственные, у кого есть опыт, чтобы знать в каждом конкретном случае, действительно ли такое нарушение правил имеет смысл. - person ErikE; 30.05.2015
comment
@ErikE Вы правы, вы должны знать, что делаете, прежде чем использовать нестандартный подход. Я мог бы сосредоточить больше энергии на объяснении, почему это плохая идея, вместо того, чтобы высмеивать рассматриваемую технику. - person three-cups; 02.06.2015
comment
@ three-cups Я обновил намного больше деталей о том, почему это плохая идея. - person ErikE; 09.04.2016
comment
По крайней мере, в SQL Server динамический SQL выполняется с привилегиями вызывающего, а не выполняемого кода. - что вы имеете в виду? Обычно любой оператор, который вы выполняете, выполняется с привилегиями вызывающего. И кроме того: в Postgres (и SQL Server) вы можете определить, с какими привилегиями выполняется функция: либо владелец / создатель функции, либо текущий пользователь (= вызывающий) - person a_horse_with_no_name; 01.10.2016
comment
@a_horse_with_no_name Обновлено с гораздо лучшим описанием проблем с привилегиями, о которых я думал. - person ErikE; 01.10.2016
comment
Что ж, в Postgres динамический SQL запускается с привилегиями, которые были определены для функции. Вы не можете выйти из этого только из-за динамического SQL. Но я до сих пор не понимаю, что вы беспокоитесь об этом. Выполнение кода с привилегиями вызывающего - это то, что обычно ожидает вызывающий. И если вы создаете функцию, которая запускается с повышенными привилегиями (для чего обычно используется определитель безопасности), вы не должны позволять вызывающей стороне передавать SQL для выполнения. - person a_horse_with_no_name; 01.10.2016

Внутри кода plpgsql EXECUTE должен использоваться для запросов, в которых имена таблиц или столбцы берутся из переменных. Также конструкция IF EXISTS (<query>) не разрешена, когда query генерируется динамически.

Вот ваша функция с исправленными обеими проблемами:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;
person Daniel Vérité    schedule 22.05.2012
comment
Спасибо, я делал то же самое пару минут назад, когда читал ваш ответ. Единственная разница в том, что мне пришлось удалить quote_ident(), потому что он добавил лишние кавычки, что меня немного удивило, ну, потому что оно используется в большинстве примеров. - person John Doe; 22.05.2012
comment
Эти дополнительные кавычки потребуются, если / когда имя таблицы содержит символы вне [a-z], или если / когда оно конфликтует с зарезервированным идентификатором (пример: группа как имя таблицы) - person Daniel Vérité; 22.05.2012
comment
И, кстати, не могли бы вы предоставить ссылку, которая доказывала бы, что конструкция IF EXISTS <query> не существует? Я почти уверен, что видел что-то подобное как рабочий образец кода. - person John Doe; 23.05.2012
comment
@JohnDoe: IF EXISTS (<query>) THEN ... - вполне допустимая конструкция в plpgsql. Только не с динамическим SQL для <query>. Я часто им пользуюсь. Кроме того, эту функцию можно немного улучшить. Я отправил ответ. - person Erwin Brandstetter; 23.05.2012
comment
Извините, вы правы насчет if exists(<query>), в общем случае это действительно так. Просто проверил и соответствующим образом изменил ответ. - person Daniel Vérité; 23.05.2012
comment
@john 'SELECT 1' внутри предложения EXISTS не создает таблицу с одним столбцом. Пора перестать исправлять профессионалов элементарными и необученными рассуждениями. - person ErikE; 11.07.2012
comment
@ErikE Спасибо, что упомянули об этом (хотя и довольно эмоционально). - person John Doe; 11.07.2012

Первый на самом деле не «работает» в том смысле, в котором вы имеете в виду, он работает только постольку, поскольку он не вызывает ошибки.

Попробуйте SELECT * FROM quote_ident('table_that_does_not_exist');, и вы увидите, почему ваша функция возвращает 1: выборка возвращает таблицу с одним столбцом (с именем quote_ident) с одной строкой (переменная $1 или в данном конкретном случае table_that_does_not_exist).

То, что вы хотите сделать, потребует динамического SQL, который на самом деле является местом, где должны использоваться функции quote_*.

person Matt    schedule 22.05.2012
comment
Большое спасибо, Мэтт, table_that_does_not_exist дал такой же результат, ты прав. - person John Doe; 22.05.2012

Я знаю, что это старый поток, но недавно я столкнулся с ним, пытаясь решить ту же проблему - в моем случае для некоторых довольно сложных сценариев.

Превращение всего сценария в динамический SQL - не лучший вариант. Это утомительная и подверженная ошибкам работа, и вы теряете возможность параметризации: параметры должны быть интерполированы в константы в SQL, что плохо сказывается на производительности и безопасности.

Вот простой трюк, который позволяет сохранить SQL без изменений, если вам нужно только выбрать из таблицы - используйте динамический SQL для создания временного представления:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;
person Nathan Meyers    schedule 18.05.2020
comment
Теперь это даже более старая ветка :). На всякий случай временная требует, чтобы схема тоже была временной. Вы можете опустить это ключевое слово и выполнить очистку по мере необходимости. Помимо ортодоксальных дискуссий здесь, это полезный метод, по крайней мере, для некоторых административных задач. - person full.stack.ex; 14.07.2021

Если вопрос заключался в том, чтобы проверить, пуста ли таблица или нет (id = 1), вот упрощенная версия хранимой процедуры Эрвина:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;
person Julien Feniou    schedule 21.12.2017

Если вы хотите, чтобы имя таблицы, имя столбца и значение динамически передавались для работы в качестве параметра

используйте этот код

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value
person Sandip Debnath    schedule 28.08.2018

У меня PostgreSQL версии 9.4, и я всегда использую этот код:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

А потом:

SELECT add_new_table('my_table_name');

У меня это работает хорошо.

Внимание! Приведенный выше пример является одним из тех, которые показывают «Как этого не сделать, если мы хотим сохранить безопасность при запросе к базе данных»: P

person dm3    schedule 30.04.2015
comment
Создание new таблицы отличается от работы с именем существующей таблицы. В любом случае вам следует избегать текстовых параметров, выполняемых как код, или вы открыты для SQL-инъекции. - person Erwin Brandstetter; 08.05.2015
comment
Ах да, моя ошибка. Тема ввела меня в заблуждение и к тому же я не дочитал ее до конца. Обычно в моем случае. : P Почему код с текстовым параметром подвергается инъекции? - person dm3; 09.05.2015
comment
Ой, это действительно опасно. Спасибо за ответ! - person dm3; 10.05.2015