Quando voce precisa montar uma string a partir de pedacos -- uma saudacao, um caminho de tabela ou uma consulta inteira para SQL dinamico -- a concatenacao com || rapidamente vira uma sopa ilegivel de aspas. O PostgreSQL oferece format(), que trabalha a partir de um template como o printf do C e sabe interpolar identificadores e literais de forma segura.
Sintaxe basica e %s
format(template, args...) recebe uma string de template e substitui os argumentos pelos especificadores. O mais simples e %s, que insere um valor como texto:
SELECT format('Hi %s, id=%s', name, id) AS greeting
FROM users
WHERE country = 'US';
O que vale saber sobre %s:
- Qualquer argumento e convertido em texto pelo seu proprio
::text, entao numeros, datas e booleanos funcionam de imediato.
NULL vira uma string vazia, nao a palavra NULL -- uma surpresa comum.
- Para inserir um sinal de porcentagem literal, duplique-o:
%%.
SELECT format('name=[%s]', NULL) AS demo;
SELECT format('100%% done') AS pct;
%I e %L: SQL dinamico seguro
O principal motivo para amar format() sao os especificadores %I e %L. %I representa um argumento como identificador (um nome de tabela ou coluna), e %L o representa como literal de string, com todo o escape de aspas resolvido.
SELECT format('SELECT * FROM %I WHERE email = %L', 'users', 'a@b.com');
Essa e a sua protecao contra injecao de SQL em consultas dinamicas. Compare com a concatenacao ingenua dentro de uma funcao:
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');
Se isso usasse '... WHERE status = ''' || st || '''' no lugar, um valor de st como x'' OR ''1''=''1 quebraria a consulta. %L escapa os apostrofos automaticamente, e %I coloca aspas corretamente num nome como weird table ou numa palavra reservada como order.
Gotcha: nao passe um public.orders qualificado por schema para um unico %I -- voce obteria um unico identificador "public.orders". Passe as partes separadamente: format('%I.%I', 'public', 'orders').
Especificadores posicionais
Quando um argumento e necessario varias vezes, a forma posicional %n$ e pratica. O digito e o numero do argumento, comecando em um:
SELECT format('%1$s <%2$s> aka %1$s', name, email)
FROM users
LIMIT 3;
O template fica curto e voce evita listar um argumento duas vezes. A forma posicional combina sem problema com %I e %L:
SELECT format(
'INSERT INTO %1$I (email) VALUES (%2$L) -- into %1$I',
'users', 'new@b.com'
);
Voce pode montar o mesmo resultado com ||, mas o preco e a legibilidade e a seguranca. Compare duas versoes de uma linha de notificacao:
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;
Por que format() costuma vencer:
- O template e visto como um todo, sem o ruido visual de aspas e
||.
NULL nao envenena a string inteira: na concatenacao 'a' || NULL resulta em NULL, enquanto em format() e apenas uma substituicao vazia.
- Para SQL dinamico,
%I/%L te dao uma protecao que || nao pode oferecer de jeito nenhum.
MySQL e ClickHouse
Uma nota importante de portabilidade: o MySQL tem uma funcao com o mesmo nome FORMAT, mas ela faz outra coisa -- formata um numero com separadores de milhar, nao monta uma string a partir de um template:
SELECT FORMAT(1234567.891, 2);
O equivalente de template no MySQL sao CONCAT, CONCAT_WS (com separador) e as funcoes tipo printf MAKE_SET/ELT para casos pontuais. Nao ha equivalente direto de %I/%L; para SQL dinamico seguro use prepared statements com marcadores ?. O ClickHouse oferece um format() com chaves no estilo Python {0}, {1}:
SELECT format('Hi {0}, id={1}', name, toString(id)) FROM users;
Resumo: no PostgreSQL format() e ao mesmo tempo um pratico printf para relatorios e a unica forma correta de montar SQL dinamico, gracas a %I e %L. No MySQL e no ClickHouse as funcoes de mesmo nome fazem algo totalmente diferente, consulte a documentacao.
Quando voce precisa montar uma string a partir de pedacos -- uma saudacao, um caminho de tabela ou uma consulta inteira para SQL dinamico -- a concatenacao com
||rapidamente vira uma sopa ilegivel de aspas. O PostgreSQL ofereceformat(), que trabalha a partir de um template como oprintfdo C e sabe interpolar identificadores e literais de forma segura.Sintaxe basica e %s
format(template, args...)recebe uma string de template e substitui os argumentos pelos especificadores. O mais simples e%s, que insere um valor como texto:SELECT format('Hi %s, id=%s', name, id) AS greeting FROM users WHERE country = 'US';O que vale saber sobre
%s:::text, entao numeros, datas e booleanos funcionam de imediato.NULLvira uma string vazia, nao a palavraNULL-- uma surpresa comum.%%.-- 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 e %L: SQL dinamico seguro
O principal motivo para amar
format()sao os especificadores%Ie%L.%Irepresenta um argumento como identificador (um nome de tabela ou coluna), e%Lo representa como literal de string, com todo o escape de aspas resolvido.-- %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'Essa e a sua protecao contra injecao de SQL em consultas dinamicas. Compare com a concatenacao ingenua dentro de uma funcao:
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');Se isso usasse
'... WHERE status = ''' || st || ''''no lugar, um valor destcomox'' OR ''1''=''1quebraria a consulta.%Lescapa os apostrofos automaticamente, e%Icoloca aspas corretamente num nome comoweird tableou numa palavra reservada comoorder.Gotcha: nao passe um
public.ordersqualificado por schema para um unico%I-- voce obteria um unico identificador"public.orders". Passe as partes separadamente:format('%I.%I', 'public', 'orders').Especificadores posicionais
Quando um argumento e necessario varias vezes, a forma posicional
%n$e pratica. O digito e o numero do argumento, comecando em um:-- %1$ refers to the first argument, reused twice SELECT format('%1$s <%2$s> aka %1$s', name, email) FROM users LIMIT 3;O template fica curto e voce evita listar um argumento duas vezes. A forma posicional combina sem problema com
%Ie%L:SELECT format( 'INSERT INTO %1$I (email) VALUES (%2$L) -- into %1$I', 'users', 'new@b.com' );FORMAT versus concatenacao
Voce pode montar o mesmo resultado com
||, mas o preco e a legibilidade e a seguranca. Compare duas versoes de uma linha de notificacao:-- 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;Por que
format()costuma vencer:||.NULLnao envenena a string inteira: na concatenacao'a' || NULLresulta emNULL, enquanto emformat()e apenas uma substituicao vazia.%I/%Lte dao uma protecao que||nao pode oferecer de jeito nenhum.MySQL e ClickHouse
Uma nota importante de portabilidade: o MySQL tem uma funcao com o mesmo nome
FORMAT, mas ela faz outra coisa -- formata um numero com separadores de milhar, nao monta uma string a partir de um template:-- MySQL: FORMAT formats a NUMBER, not a template SELECT FORMAT(1234567.891, 2); -- 1,234,567.89O equivalente de template no MySQL sao
CONCAT,CONCAT_WS(com separador) e as funcoes tipoprintfMAKE_SET/ELTpara casos pontuais. Nao ha equivalente direto de%I/%L; para SQL dinamico seguro use prepared statements com marcadores?. O ClickHouse oferece umformat()com chaves no estilo Python{0},{1}:-- ClickHouse: positional braces, not percent specifiers SELECT format('Hi {0}, id={1}', name, toString(id)) FROM users;Resumo: no PostgreSQL
format()e ao mesmo tempo um praticoprintfpara relatorios e a unica forma correta de montar SQL dinamico, gracas a%Ie%L. No MySQL e no ClickHouse as funcoes de mesmo nome fazem algo totalmente diferente, consulte a documentacao.