SQL para análise de dados · Guias · 30 de outubro de 2026 · 11 min de leitura

Funções de texto no SQL: CONCAT, SUBSTRING, REPLACE, UPPER, TRIM e mais

Etiquetas de bagagem de papel com nomes de passageiros sendo cortadas, unidas e reetiquetadas numa mesa de triagem

As funções de texto (ou funções de string) servem para limpar e transformar texto dentro da própria consulta, sem exportar nada para uma planilha. Um punhado delas resolve quase todo o trabalho real: || e CONCAT juntam textos, SUBSTRING, LEFT, RIGHT e SPLIT_PART recortam pedaços, REPLACE e REGEXP_REPLACE trocam caracteres, UPPER, LOWER e TRIM padronizam valores para que eles batam, e LENGTH e POSITION medem e procuram. Este guia passa por cada uma em dados reais e mostra onde PostgreSQL, DuckDB e outros bancos de dados divergem.

Todas as consultas rodam no banco de dados da companhia aérea do curso de SQL da Nomto, na sintaxe do PostgreSQL sobre o DuckDB (o motor por trás da sala de SQL da Nomto). As tabelas de resultado são a saída real. As colunas de texto que vamos usar:

Tabela Coluna Exemplo de valor
airports airport_code, airport_name, city SVO, Sheremetyevo, Moscow
aircrafts model Sukhoi Superjet-100
flights flight_no PG0405
seats seat_no 10A
tickets passenger_name, contact_data (JSON) VALERIY TIKHONOV, {"phone": "+70127117011"}

Concatenação: || e CONCAT

No PostgreSQL e no DuckDB, o jeito padrão de juntar textos é o operador ||. A função CONCAT() faz o mesmo trabalho:

SELECT airport_code,
       city,
       airport_name,
       city || ' (' || airport_code || ')' AS label,
       CONCAT(city, ' - ', airport_name) AS full_name
FROM airports
WHERE city IN ('Moscow', 'Sochi')
ORDER BY airport_code;
airport_code city airport_name label full_name
AER Sochi Sochi Sochi (AER) Sochi - Sochi
DME Moscow Domodedovo Moscow (DME) Moscow - Domodedovo
SVO Moscow Sheremetyevo Moscow (SVO) Moscow - Sheremetyevo
VKO Moscow Vnukovo Moscow (VKO) Moscow - Vnukovo

Os dois não são exatamente iguais. Eles tratam o NULL de forma diferente:

SELECT 'PG' || NULL AS pipes,
       CONCAT('PG', NULL) AS concat_fn,
       concat_ws(' / ', 'SVO', NULL, 'LED') AS ws;
pipes concat_fn ws
NULL PG SVO / LED

Com ||, um único NULL transforma o resultado inteiro em NULL. O CONCAT ignora os NULL. O concat_ws (concatenar com separador) coloca o separador entre os valores e também ignora os NULL, o que ajuda a montar endereços ou rótulos de rota a partir de partes opcionais. Se você monta um rótulo com || e algumas linhas saem vazias, o motivo de sempre é uma coluna NULL; envolva-a em COALESCE(col, '') ou troque para CONCAT.

Outros bancos. || e CONCAT funcionam no PostgreSQL, DuckDB, Oracle e SQLite. O MySQL usa CONCAT (lá, || significa OU lógico, a menos que um modo específico esteja ativado), e o SQL Server usa + ou CONCAT.

UPPER, LOWER e nomes com inicial maiúscula

UPPER e LOWER mudam a caixa das letras. Os nomes dos passageiros estão gravados em maiúsculas:

SELECT passenger_name,
       lower(passenger_name) AS lower_name,
       upper(left(split_part(passenger_name, ' ', 1), 1))
         || lower(substring(split_part(passenger_name, ' ', 1), 2)) AS first_name,
       length(passenger_name) AS len
FROM tickets
LIMIT 3;
passenger_name lower_name first_name len
VALERIY TIKHONOV valeriy tikhonov Valeriy 16
EVGENIYA ALEKSEEVA evgeniya alekseeva Evgeniya 18
ARTUR GERASIMOV artur gerasimov Artur 15

O PostgreSQL tem initcap(passenger_name), que coloca a inicial de cada palavra em maiúscula de uma vez (Valeriy Tikhonov). O DuckDB não tem initcap, por isso a consulta acima faz isso na mão: primeira letra maiúscula, o resto minúsculo.

O uso mais importante do LOWER é na comparação. A comparação de textos diferencia maiúsculas de minúsculas: WHERE city = 'moscow' não encontra nada. Compare lower(city) = 'moscow' ou use ILIKE, que é um LIKE sem distinção de caixa:

SELECT COUNT(*) FILTER (WHERE airport_name LIKE '%ovo') AS like_ovo,
       COUNT(*) FILTER (WHERE city ILIKE 'novo%')       AS ilike_novo
FROM airports;
like_ovo ilike_novo
11 2

O ILIKE funciona no PostgreSQL e no DuckDB, mas não no MySQL nem no SQL Server, onde a distinção de caixa depende da collation da coluna.

SUBSTRING, LEFT, RIGHT e SPLIT_PART: recortar texto

substring(texto, início, tamanho) devolve parte de uma string. As posições começam em 1, e se você omitir o tamanho recebe tudo até o fim. Os números de voo são o código da companhia PG mais quatro dígitos:

SELECT flight_no,
       substring(flight_no, 3) AS num,
       CAST(substring(flight_no, 3) AS INTEGER) AS num_int,
       right(flight_no, 4) AS r
FROM flights
WHERE flight_id IN (1, 2);
flight_no num num_int r
PG0405 0405 405 0405
PG0404 0404 404 0404

num continua sendo texto ('0405'); converta com CAST se precisar ordenar ou comparar como número, senão '1000' vem antes de '999'. LEFT(texto, n) e RIGHT(texto, n) são atalhos para os primeiros ou os últimos n caracteres. O PostgreSQL também aceita a forma padrão substring(texto FROM 3 FOR 4); MySQL e SQL Server também usam SUBSTRING(texto, 3, 4), e o Oracle chama a função de SUBSTR.

Quando o texto tem um delimitador, split_part(texto, delimitador, n) é mais fácil do que contar posições. Separando o nome do passageiro em nome e sobrenome, e a mesma coisa feita com POSITION:

SELECT passenger_name,
       split_part(passenger_name, ' ', 1) AS first_name,
       split_part(passenger_name, ' ', 2) AS last_name,
       position(' ' IN passenger_name) AS space_at,
       substring(passenger_name FROM 1 FOR position(' ' IN passenger_name) - 1) AS first2,
       left(passenger_name, 1) AS initial
FROM tickets
LIMIT 3;
passenger_name first_name last_name space_at first2 initial
VALERIY TIKHONOV VALERIY TIKHONOV 8 VALERIY V
EVGENIYA ALEKSEEVA EVGENIYA ALEKSEEVA 9 EVGENIYA E
ARTUR GERASIMOV ARTUR GERASIMOV 6 ARTUR A

position(' ' IN x) e strpos(x, ' ') são a mesma coisa no PostgreSQL e no DuckDB; as duas devolvem 0 quando o trecho não existe. split_part existe no PostgreSQL, DuckDB e Snowflake; o MySQL tem SUBSTRING_INDEX no lugar, e o SQL Server não tem nenhuma das duas.

Depois de extrair um pedaço, dá para agrupar por ele. Os primeiros nomes mais comuns nos bilhetes:

SELECT split_part(passenger_name, ' ', 1) AS first_name,
       COUNT(*) AS tickets
FROM tickets
GROUP BY 1
ORDER BY 2 DESC
LIMIT 5;
first_name tickets
ALEKSANDR 20328
SERGEY 15133
VLADIMIR 12806
TATYANA 12058
ELENA 11291

REPLACE e REGEXP_REPLACE

replace(texto, de, para) troca todas as ocorrências de um trecho:

SELECT model,
       replace(model, 'Sukhoi ', '') AS short,
       replace(model, '-', ' ') AS no_dash
FROM aircrafts
WHERE model LIKE 'S%' OR model LIKE 'Bombardier%';
model short no_dash
Bombardier CRJ-200 Bombardier CRJ-200 Bombardier CRJ 200
Sukhoi Superjet-100 Superjet-100 Sukhoi Superjet 100

Se o trecho não existe, o valor volta sem mudança. Trocar por '' apaga.

Quando o que você quer trocar é um padrão e não um texto fixo, use uma expressão regular. Os números de assento combinam fileira e letra (10A); separe os dois removendo os dígitos ou as letras:

SELECT seat_no,
       regexp_replace(seat_no, '[0-9]', '', 'g') AS letter,
       CAST(regexp_replace(seat_no, '[A-Z]', '', 'g') AS INTEGER) AS row_no
FROM seats
WHERE aircraft_code = '319'
ORDER BY row_no, letter
LIMIT 4;
seat_no letter row_no
1A A 1
1C C 1
1D D 1
1F F 1

A flag 'g' significa “global”: trocar todas as ocorrências, não só a primeira. Sem ela, regexp_replace('12A', '[0-9]', '') dá 2A. Isso vale igualmente para PostgreSQL e DuckDB. Para extrair o trecho encontrado em vez de trocá-lo, o PostgreSQL usa substring(texto FROM 'padrão') ou regexp_substr (versão 15 em diante), e o DuckDB usa regexp_extract(texto, 'padrão').

TRIM, LTRIM, RTRIM e LPAD

trim remove espaços no começo e no fim, o motivo mais comum para dois valores que parecem iguais não baterem num JOIN:

SELECT '|' || trim('  SVO  ')  || '|' AS t,
       '|' || ltrim('  SVO  ') || '|' AS l,
       '|' || rtrim('  SVO  ') || '|' AS r,
       trim(BOTH '0' FROM '0405') AS z,
       ltrim('0405', '0') AS lz;
t l r z lz
|SVO| |SVO | | SVO| 405 405

As barras verticais deixam os espaços visíveis. O trim também remove outros caracteres, como zeros à esquerda. length(' SVO ') dá 6, enquanto length(trim(' SVO ')) dá 3, o que é um jeito rápido de achar valores sujos: WHERE length(code) <> length(trim(code)).

lpad(texto, tamanho, preenchimento) faz o contrário e completa à esquerda. O próximo número de voo depois de PG0405, mantendo quatro dígitos:

SELECT flight_no,
       lpad(CAST(CAST(substring(flight_no, 3) AS INTEGER) + 1 AS VARCHAR), 4, '0') AS next_no
FROM flights
WHERE flight_id = 1;
flight_no next_no
PG0405 0406

Texto dentro de JSON: ->>

Os dados de contato ficam numa coluna JSON. O operador ->> devolve um campo como texto, e a partir daí todas as funções de texto se aplicam:

SELECT ticket_no,
       contact_data ->> 'phone' AS phone,
       contact_data ->> 'email' AS email,
       right(contact_data ->> 'phone', 4) AS last4
FROM tickets
LIMIT 3;
ticket_no phone email last4
0005432000987 +70127117011 NULL 7011
0005432000988 +70378089255 NULL 9255
0005432000989 +70760429203 NULL 9203

Uma chave ausente devolve NULL, não uma string vazia. 199.083 dos 366.733 bilhetes têm e-mail, e você contaria isso com COUNT(*) FILTER (WHERE contact_data ->> 'email' IS NOT NULL). O ->> funciona em json e jsonb no PostgreSQL e em JSON no DuckDB.

STRING_AGG: várias linhas numa só string

O contrário de separar: juntar os valores de um grupo numa única célula separada por vírgulas. Cidades com mais de um aeroporto:

SELECT city,
       string_agg(airport_code, ', ' ORDER BY airport_code) AS airports,
       COUNT(*) AS n
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY city;
city airports n
Moscow DME, SVO, VKO 3
Ulyanovsk ULV, ULY 2

string_agg funciona no PostgreSQL, DuckDB e SQL Server (2017 em diante). O MySQL chama de GROUP_CONCAT; Oracle e Snowflake, de LISTAGG. O HAVING é explicado em GROUP BY e HAVING.

Resumo rápido: PostgreSQL x DuckDB x outros

Tarefa PostgreSQL DuckDB MySQL SQL Server
Concatenar ||, CONCAT ||, CONCAT CONCAT +, CONCAT
Tamanho length, char_length length CHAR_LENGTH LEN
Achar posição position(x IN s), strpos position, strpos LOCATE, INSTR CHARINDEX
Separar por delimitador split_part split_part SUBSTRING_INDEX STRING_SPLIT (devolve uma tabela)
Inicial maiúscula em cada palavra initcap não existe não existe não existe
Extrair com regex substring(s FROM re), regexp_substr regexp_extract REGEXP_SUBSTR não nativo
Juntar linhas numa string string_agg string_agg GROUP_CONCAT STRING_AGG
LIKE sem distinção de caixa ILIKE ILIKE depende da collation depende da collation

Erros comuns

Datas têm um conjunto próprio de funções, explicado em funções de data em SQL. Para praticar com exercícios no estilo de entrevista, veja os exercícios de SQL com respostas.

Perguntas frequentes

Como concatenar strings no SQL?

No PostgreSQL e no DuckDB, use first || ' ' || last ou CONCAT(first, ' ', last). O CONCAT ignora valores NULL; o || devolve NULL se qualquer parte for NULL. No MySQL, use CONCAT; no SQL Server, + ou CONCAT.

Qual a diferença entre SUBSTRING e SPLIT_PART?

O SUBSTRING recorta por posição (do caractere 3 ao 6). O SPLIT_PART recorta por delimitador (a segunda palavra depois de separar pelos espaços). Use SPLIT_PART quando os pedaços têm tamanhos diferentes, mas são separados por um caractere conhecido.

Como remover espaços de uma string no SQL?

trim(x) remove os espaços das duas pontas; ltrim e rtrim, só de um lado. Para remover todos os espaços, inclusive os do meio, use replace(x, ' ', '').

Como fazer uma comparação no SQL sem diferenciar maiúsculas e minúsculas?

Compare os valores em minúsculas, lower(city) = 'moscow', ou use ILIKE no PostgreSQL e no DuckDB. No MySQL e no SQL Server, as comparações muitas vezes já ignoram a caixa, dependendo da collation.

O PostgreSQL tem a função LEN?

Não. Use length(x) ou char_length(x). LEN é do SQL Server; repare que o LEN do SQL Server ignora os espaços no fim, enquanto o length do PostgreSQL sobre text conta esses espaços.

Outros idiomas: English, Español, Tiếng Việt, 简体中文