Ten artykuł jest obecnie po rosyjsku — trwa tłumaczenie na angielski.
format() в PostgreSQL собирает строку по шаблону, как printf в Си: вы пишете шаблон со спецификаторами, а функция подставляет на их места аргументы. Берите её, когда нужно склеить приветствие, путь к таблице или целый текст запроса для динамического SQL — то, что через оператор || быстро превращается в нечитаемую кашу из кавычек.
Главное преимущество format() перед конкатенацией — спецификаторы %I и %L, которые сами закавычивают идентификаторы и экранируют литералы. Благодаря им функция отделяет шаблон от данных и даёт базе правильно обработать опасные части, поэтому именно format() в PostgreSQL — штатный способ безопасно собирать динамический SQL внутри EXECUTE и функций на PL/pgSQL.
Базовый синтаксис и %s
format(template, args...) берёт строку-шаблон и подставляет аргументы вместо спецификаторов. Самый простой из них — %s, который вставляет значение как текст:
SELECT format('Hi %s, id=%s', name, id) AS greeting
FROM users
WHERE country = 'US';
Что важно знать про %s:
- Любой аргумент приводится к тексту через свой
::text, поэтому числа, даты и булевы значения работают сразу.
NULL превращается в пустую строку, а не в слово NULL — это частый сюрприз.
- Чтобы вставить буквальный знак процента, удваивайте его:
%%.
SELECT format('name=[%s]', NULL) AS demo;
SELECT format('100%% done') AS pct;
%I и %L: безопасный динамический SQL
Главная причина любить format() — это спецификаторы %I и %L. %I оформляет аргумент как идентификатор (имя таблицы или колонки), %L — как строковый литерал, со всем экранированием кавычек.
SELECT format('SELECT * FROM %I WHERE email = %L', 'users', 'a@b.com');
Это и есть защита от SQL-инъекций в динамических запросах. Сравните с наивной конкатенацией внутри функции:
CREATE FUNCTION count_by_status(tbl text, st text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE
n bigint;
BEGIN
EXECUTE format('SELECT count(*) FROM %I WHERE status = %L', tbl, st)
INTO n;
RETURN n;
END;
$$;
SELECT count_by_status('orders', 'paid');
Если бы здесь стояло '... WHERE status = ''' || st || '''', то значение st вида x'' OR ''1''=''1 сломало бы запрос. %L экранирует апострофы автоматически, а %I корректно закавычит имя weird table или зарезервированное слово вроде order.
Ловушка: для имени схемы с таблицей не пишите %I на всю строку public.orders — иначе получится один идентификатор "public.orders". Передавайте части по отдельности: format('%I.%I', 'public', 'orders').
Позиционные спецификаторы
Когда один аргумент нужен несколько раз, удобны позиционные ссылки вида %n$. Цифра — это номер аргумента, начиная с единицы:
SELECT format('%1$s <%2$s> aka %1$s', name, email)
FROM users
LIMIT 3;
Так шаблон становится короче, а перечислять аргумент дважды не нужно. Позиционную форму можно смешивать с %I и %L:
SELECT format(
'INSERT INTO %1$I (email) VALUES (%2$L) -- into %1$I',
'users', 'new@b.com'
);
Тот же результат можно собрать оператором ||, но цена — читаемость и безопасность. Сравните две версии письма-уведомления:
SELECT 'Order ' || o.id || ' for ' || u.name
|| ': ' || o.amount || ' (' || o.status || ')'
FROM orders o JOIN users u ON u.id = o.user_id;
SELECT format('Order %s for %s: %s (%s)', o.id, u.name, o.amount, o.status)
FROM orders o JOIN users u ON u.id = o.user_id;
Почему format() обычно выигрывает:
- Шаблон видно целиком, без визуального шума из кавычек и
||.
NULL не отравляет всю строку: при конкатенации 'a' || NULL даёт NULL, а в format() это просто пустая подстановка.
- Для динамического SQL
%I/%L дают защиту, которой у || нет в принципе.
MySQL и ClickHouse
Важный нюанс переносимости: в MySQL есть функция с тем же именем FORMAT, но она про другое — форматирует число с разделителями разрядов, а не собирает строку по шаблону:
SELECT FORMAT(1234567.891, 2);
Аналог шаблонной сборки в MySQL — это CONCAT, CONCAT_WS (с разделителем) и printf-подобная MAKE_SET/ELT для частных случаев. Прямого эквивалента %I/%L нет; для безопасного динамического SQL используйте подготовленные выражения с плейсхолдерами ?. ClickHouse предлагает format() с фигурными скобками {0}, {1} как у Python:
SELECT format('Hi {0}, id={1}', name, toString(id)) FROM users;
Эти различия легко проглядеть при переносе кода: одно и то же имя FORMAT в MySQL ждёт число и точность, в PostgreSQL — шаблон со спецификаторами %s, а ClickHouse использует фигурные скобки {0}. Поэтому шаблонную сборку строк нельзя копировать между движками вслепую: проверяйте, какую из трёх несовместимых функций вы на самом деле вызываете, и помните, что аналога %I/%L за пределами PostgreSQL нет — там безопасность динамического SQL обеспечивают подготовленные выражения с плейсхолдерами ?.
Отдельно стоит проверить поведение format() на краевых данных, ведь именно %s молча превращает NULL в пустую строку, а %L обязан корректно экранировать апострофы и Unicode внутри литералов. Прогоните шаблон на значениях с NULL, пустой строкой, кавычкой и зарезервированным именем таблицы: так вы убедитесь, что %I и %L действительно закрывают инъекцию там, где наивная конкатенация || ломается.
Вывод: в PostgreSQL format() — это и удобный printf для отчётов, и единственно правильный способ строить динамический SQL благодаря %I и %L. В MySQL и ClickHouse одноимённые функции делают совсем другое, проверяйте документацию.
format()в PostgreSQL собирает строку по шаблону, какprintfв Си: вы пишете шаблон со спецификаторами, а функция подставляет на их места аргументы. Берите её, когда нужно склеить приветствие, путь к таблице или целый текст запроса для динамического SQL — то, что через оператор||быстро превращается в нечитаемую кашу из кавычек.Главное преимущество
format()перед конкатенацией — спецификаторы%Iи%L, которые сами закавычивают идентификаторы и экранируют литералы. Благодаря им функция отделяет шаблон от данных и даёт базе правильно обработать опасные части, поэтому именноformat()в PostgreSQL — штатный способ безопасно собирать динамический SQL внутриEXECUTEи функций на PL/pgSQL.Базовый синтаксис и %s
format(template, args...)берёт строку-шаблон и подставляет аргументы вместо спецификаторов. Самый простой из них —%s, который вставляет значение как текст:SELECT format('Hi %s, id=%s', name, id) AS greeting FROM users WHERE country = 'US';Что важно знать про
%s:::text, поэтому числа, даты и булевы значения работают сразу.NULLпревращается в пустую строку, а не в словоNULL— это частый сюрприз.%%.-- NULL becomes an empty string, not the text 'NULL' SELECT format('name=[%s]', NULL) AS demo; -- name=[] SELECT format('100%% done') AS pct; -- 100% done%I и %L: безопасный динамический SQL
Главная причина любить
format()— это спецификаторы%Iи%L.%Iоформляет аргумент как идентификатор (имя таблицы или колонки),%L— как строковый литерал, со всем экранированием кавычек.-- %I quotes an identifier, %L quotes a literal SELECT format('SELECT * FROM %I WHERE email = %L', 'users', 'a@b.com'); -- SELECT * FROM users WHERE email = 'a@b.com'Это и есть защита от SQL-инъекций в динамических запросах. Сравните с наивной конкатенацией внутри функции:
CREATE FUNCTION count_by_status(tbl text, st text) RETURNS bigint LANGUAGE plpgsql AS $$ DECLARE n bigint; BEGIN -- Safe: %I and %L handle quoting and escaping for us EXECUTE format('SELECT count(*) FROM %I WHERE status = %L', tbl, st) INTO n; RETURN n; END; $$; SELECT count_by_status('orders', 'paid');Если бы здесь стояло
'... WHERE status = ''' || st || '''', то значениеstвидаx'' OR ''1''=''1сломало бы запрос.%Lэкранирует апострофы автоматически, а%Iкорректно закавычит имяweird tableили зарезервированное слово вродеorder.Ловушка: для имени схемы с таблицей не пишите
%Iна всю строкуpublic.orders— иначе получится один идентификатор"public.orders". Передавайте части по отдельности:format('%I.%I', 'public', 'orders').Позиционные спецификаторы
Когда один аргумент нужен несколько раз, удобны позиционные ссылки вида
%n$. Цифра — это номер аргумента, начиная с единицы:-- %1$ refers to the first argument, reused twice SELECT format('%1$s <%2$s> aka %1$s', name, email) FROM users LIMIT 3;Так шаблон становится короче, а перечислять аргумент дважды не нужно. Позиционную форму можно смешивать с
%Iи%L:SELECT format( 'INSERT INTO %1$I (email) VALUES (%2$L) -- into %1$I', 'users', 'new@b.com' );FORMAT против конкатенации
Тот же результат можно собрать оператором
||, но цена — читаемость и безопасность. Сравните две версии письма-уведомления:-- Concatenation: hard to read, easy to misplace a quote SELECT 'Order ' || o.id || ' for ' || u.name || ': ' || o.amount || ' (' || o.status || ')' FROM orders o JOIN users u ON u.id = o.user_id; -- format(): the template reads like the output SELECT format('Order %s for %s: %s (%s)', o.id, u.name, o.amount, o.status) FROM orders o JOIN users u ON u.id = o.user_id;Почему
format()обычно выигрывает:||.NULLне отравляет всю строку: при конкатенации'a' || NULLдаётNULL, а вformat()это просто пустая подстановка.%I/%Lдают защиту, которой у||нет в принципе.MySQL и ClickHouse
Важный нюанс переносимости: в MySQL есть функция с тем же именем
FORMAT, но она про другое — форматирует число с разделителями разрядов, а не собирает строку по шаблону:-- MySQL: FORMAT formats a NUMBER, not a template SELECT FORMAT(1234567.891, 2); -- 1,234,567.89Аналог шаблонной сборки в MySQL — это
CONCAT,CONCAT_WS(с разделителем) иprintf-подобнаяMAKE_SET/ELTдля частных случаев. Прямого эквивалента%I/%Lнет; для безопасного динамического SQL используйте подготовленные выражения с плейсхолдерами?. ClickHouse предлагаетformat()с фигурными скобками{0},{1}как у Python:-- ClickHouse: positional braces, not percent specifiers SELECT format('Hi {0}, id={1}', name, toString(id)) FROM users;Эти различия легко проглядеть при переносе кода: одно и то же имя
FORMATв MySQL ждёт число и точность, в PostgreSQL — шаблон со спецификаторами%s, а ClickHouse использует фигурные скобки{0}. Поэтому шаблонную сборку строк нельзя копировать между движками вслепую: проверяйте, какую из трёх несовместимых функций вы на самом деле вызываете, и помните, что аналога%I/%Lза пределами PostgreSQL нет — там безопасность динамического SQL обеспечивают подготовленные выражения с плейсхолдерами?.Отдельно стоит проверить поведение
format()на краевых данных, ведь именно%sмолча превращаетNULLв пустую строку, а%Lобязан корректно экранировать апострофы и Unicode внутри литералов. Прогоните шаблон на значениях сNULL, пустой строкой, кавычкой и зарезервированным именем таблицы: так вы убедитесь, что%Iи%Lдействительно закрывают инъекцию там, где наивная конкатенация||ломается.Вывод: в PostgreSQL
format()— это и удобныйprintfдля отчётов, и единственно правильный способ строить динамический SQL благодаря%Iи%L. В MySQL и ClickHouse одноимённые функции делают совсем другое, проверяйте документацию.