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

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 | 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
- Usar
||com uma coluna que pode ser nula. UmNULLapaga o rótulo inteiro. UseCONCAT,concat_wsouCOALESCE. - Comparar sem padronizar.
'SVO 'com espaço no fim e'svo'em minúsculas nunca vão ser iguais a'SVO'. Apliquetrimelower/upperdos dois lados antes de juntar tabelas por chaves de texto. - Ordenar números guardados como texto. Converta os dígitos extraídos para
INTEGERantes doORDER BYou doMAX. - Erro de uma posição. As strings no SQL começam na posição 1, não 0, e
positiondevolve 0 quando não encontra nada; por issosubstring(x, 1, position(...) - 1)devolve string vazia para valores sem o delimitador. - Esquecer a flag
'g'noregexp_replace, e só a primeira ocorrência muda. - Aspas duplas para texto. No PostgreSQL,
"SVO"é nome de coluna; texto vai entre aspas simples.
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, 简体中文