sqlpostgresqlformatdynamic-sql

A funcao FORMAT no SQL: templates de string com %s, %I e %L no PostgreSQL

Como montar strings a partir de um template com FORMAT no PostgreSQL: os especificadores %s, %I e %L, SQL dinamico seguro e por que supera a concatenacao.

3 min de leituraReferênciasql · postgresql · format · dynamic-sql · mysql · strings

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: %%.
-- 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 %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.

-- %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 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:

-- %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 %I e %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:

  • 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:

-- MySQL: FORMAT formats a NUMBER, not a template
SELECT FORMAT(1234567.891, 2);   -- 1,234,567.89

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}:

-- 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 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.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador