SQL · Guías · 30 de octubre de 2026 · 11 min de lectura
Funciones de texto en SQL: CONCAT, SUBSTRING, REPLACE, UPPER, TRIM y más, con ejemplos

Las funciones de texto (o de cadenas) sirven para limpiar y transformar texto dentro de la propia consulta, sin exportar nada a una hoja de cálculo. Con unas pocas se cubre casi todo el trabajo real: || y CONCAT unen texto, SUBSTRING, LEFT, RIGHT y SPLIT_PART recortan trozos, REPLACE y REGEXP_REPLACE sustituyen caracteres, UPPER, LOWER y TRIM normalizan valores para que coincidan, y LENGTH y POSITION miden y buscan. Esta guía repasa cada una sobre datos reales y señala dónde PostgreSQL, DuckDB y otras bases de datos no se ponen de acuerdo.
Todas las consultas usan la base de datos de una aerolínea del curso de SQL de Nomto, con sintaxis de PostgreSQL sobre DuckDB (el motor de la sala de SQL de Nomto). Las tablas de resultados son la salida real (valores en inglés). Estas son las columnas de texto que vamos a usar:
| Tabla | Columna | Valor de ejemplo |
|---|---|---|
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"} |
Concatenar: || y CONCAT
En PostgreSQL y DuckDB la forma estándar de unir cadenas es el operador ||. CONCAT() hace lo mismo en forma de función:
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 |
Pero no son del todo iguales: tratan NULL de forma distinta.
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 |
Con ||, un solo NULL convierte todo el resultado en NULL. CONCAT se salta los NULL. concat_ws (concatenate with separator, concatenar con separador) pone el separador entre los valores y también se salta los NULL, lo que viene muy bien para montar direcciones o etiquetas de ruta a partir de partes opcionales. Si construyes una etiqueta con || y algunas filas salen vacías, casi siempre la culpa es de una columna con NULL: envuélvela en COALESCE(col, '') o cambia a CONCAT.
Otras bases de datos. || y CONCAT funcionan en PostgreSQL, DuckDB, Oracle y SQLite. MySQL usa CONCAT (allí || significa OR lógico salvo que se active un modo especial) y SQL Server usa + o CONCAT.
UPPER, LOWER y nombres con mayúscula inicial
UPPER y LOWER cambian entre mayúsculas y minúsculas. Los nombres de los pasajeros están guardados en mayú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 |
PostgreSQL tiene initcap(passenger_name), que pone en mayúscula la primera letra de cada palabra de una vez (Valeriy Tikhonov). DuckDB no tiene initcap, así que la consulta de arriba lo construye a mano: primera letra en mayúscula y el resto en minúscula.
Pero el uso más importante de LOWER es comparar. La comparación de cadenas distingue mayúsculas y minúsculas: WHERE city = 'moscow' no encuentra nada. Compara lower(city) = 'moscow' o usa ILIKE, que es un LIKE que no distingue mayúsculas:
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 |
ILIKE funciona en PostgreSQL y DuckDB, pero no en MySQL ni en SQL Server, donde distinguir o no mayúsculas depende de la intercalación (collation) de la columna.
SUBSTRING, LEFT, RIGHT y SPLIT_PART: recortar texto
substring(texto, inicio, longitud) devuelve una parte de la cadena. Las posiciones empiezan en 1 y, si omites la longitud, obtienes todo hasta el final. Los números de vuelo son el código de la aerolínea PG más cuatro cifras:
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 sigue siendo texto ('0405'): conviértelo a número si necesitas ordenar o comparar numéricamente, porque si no '1000' queda antes que '999'. LEFT(texto, n) y RIGHT(texto, n) son atajos para los primeros o los últimos n caracteres. PostgreSQL también acepta la forma estándar substring(texto FROM 3 FOR 4); MySQL y SQL Server usan igualmente SUBSTRING(texto, 3, 4), mientras que Oracle la llama SUBSTR.
Cuando el texto tiene un delimitador, split_part(texto, delimitador, n) es más cómodo que contar posiciones. Aquí partimos el nombre del pasajero en nombre y apellido, y hacemos lo mismo con 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) y strpos(x, ' ') son lo mismo en PostgreSQL y DuckDB; devuelven 0 si no encuentran la subcadena. split_part existe en PostgreSQL, DuckDB y Snowflake; MySQL tiene en su lugar SUBSTRING_INDEX, y SQL Server no tiene ninguna de las dos.
Una vez extraído un trozo, puedes agrupar por él. Los nombres de pila más frecuentes en los billetes:
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 y REGEXP_REPLACE
replace(texto, buscar, sustituir) cambia todas las apariciones de una subcadena:
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 |
Si la subcadena no está, el valor vuelve sin cambios. Sustituir por '' equivale a borrar.
Cuando lo que hay que sustituir es un patrón y no un texto fijo, usa una expresión regular. Los números de asiento combinan fila y letra (10A); para separarlos, quita las cifras o las 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 |
La opción 'g' significa «global»: sustituye todas las coincidencias, no solo la primera. Sin ella, regexp_replace('12A', '[0-9]', '') da 2A. Funciona igual en PostgreSQL y DuckDB. Para extraer una coincidencia en lugar de sustituirla, PostgreSQL usa substring(texto FROM 'patrón') o regexp_substr (versión 15 o posterior), y DuckDB usa regexp_extract(texto, 'patrón').
TRIM, LTRIM, RTRIM y LPAD
trim quita los espacios del principio y del final, que son la causa más habitual de que dos valores que parecen iguales no coincidan en un 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 |
Las barras verticales hacen visibles los espacios. trim también puede quitar otros caracteres, como los ceros a la izquierda. length(' SVO ') es 6 y length(trim(' SVO ')) es 3, lo que da una forma rápida de encontrar valores sucios: WHERE length(code) <> length(trim(code)).
lpad(texto, longitud, relleno) hace lo contrario y rellena por la izquierda. El siguiente número de vuelo después de PG0405, manteniendo cuatro cifras:
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 un JSON: ->>
Los datos de contacto están en una columna JSON. El operador ->> devuelve un campo como texto, y a partir de ahí se le pueden aplicar todas las funciones de cadenas:
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 |
Una clave que no existe devuelve NULL, no una cadena vacía. De los 366.733 billetes, 199.083 tienen correo electrónico, y se cuentan con COUNT(*) FILTER (WHERE contact_data ->> 'email' IS NOT NULL). ->> funciona con json y jsonb en PostgreSQL y con JSON en DuckDB.
STRING_AGG: muchas filas en una sola cadena
Lo contrario de partir: juntar los valores de un grupo en una sola celda separada por comas. Las ciudades con más de un aeropuerto:
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 en PostgreSQL, DuckDB y SQL Server (2017 o posterior). MySQL la llama GROUP_CONCAT; Oracle y Snowflake, LISTAGG. HAVING se explica en GROUP BY y HAVING.
Referencia rápida: PostgreSQL, DuckDB y otros
| Tarea | PostgreSQL | DuckDB | MySQL | SQL Server |
|---|---|---|---|---|
| Concatenar | ||, CONCAT |
||, CONCAT |
CONCAT |
+, CONCAT |
| Longitud | length, char_length |
length |
CHAR_LENGTH |
LEN |
| Buscar posición | position(x IN s), strpos |
position, strpos |
LOCATE, INSTR |
CHARINDEX |
| Partir por delimitador | split_part |
split_part |
SUBSTRING_INDEX |
STRING_SPLIT (devuelve una tabla) |
| Mayúscula inicial en cada palabra | initcap |
no existe | no existe | no existe |
| Extraer con regex | substring(s FROM re), regexp_substr |
regexp_extract |
REGEXP_SUBSTR |
no viene de serie |
| Unir filas en una cadena | string_agg |
string_agg |
GROUP_CONCAT |
STRING_AGG |
| LIKE sin distinguir mayúsculas | ILIKE |
ILIKE |
depende de la intercalación | depende de la intercalación |
Errores frecuentes
- Usar
||con una columna que admiteNULL. Un soloNULLvacía toda la etiqueta. UsaCONCAT,concat_wsoCOALESCE. - Comparar sin normalizar.
'SVO 'con un espacio al final y'svo'en minúsculas nunca serán iguales a'SVO'. Aplicatrimylower/uppera los dos lados antes de unir por claves de texto. - Ordenar números guardados como texto. Convierte las cifras extraídas a
INTEGERantes deORDER BYoMAX. - Posiciones desplazadas en uno. En SQL las cadenas empiezan en la posición 1, no en la 0, y
positiondevuelve 0 cuando no encuentra nada, así quesubstring(x, 1, position(...) - 1)devuelve una cadena vacía para los valores sin delimitador. - Olvidar la opción
'g'enregexp_replace, con lo que solo cambia la primera coincidencia. - Comillas dobles para el texto. En PostgreSQL
"SVO"es un nombre de columna; el texto va entre comillas simples.
Las fechas tienen su propio juego de funciones, que se explica en funciones de fecha en SQL. Para practicar con tareas tipo entrevista, mira los ejercicios SQL resueltos.
Preguntas frecuentes
¿Cómo se concatenan cadenas en SQL?
En PostgreSQL y DuckDB, con first || ' ' || last o con CONCAT(first, ' ', last). CONCAT ignora los valores NULL; || devuelve NULL si alguna parte es NULL. En MySQL usa CONCAT; en SQL Server, + o CONCAT.
¿Qué diferencia hay entre SUBSTRING y SPLIT_PART?
SUBSTRING recorta por posición (de los caracteres 3 a 6). SPLIT_PART recorta por delimitador (la segunda palabra al partir por espacios). Usa SPLIT_PART cuando los trozos tienen longitudes distintas pero están separados por un carácter conocido.
¿Cómo se quitan los espacios de una cadena en SQL?
trim(x) quita los espacios de los dos extremos; ltrim y rtrim, solo de un lado. Para quitar todos los espacios, también los del medio, usa replace(x, ' ', '').
¿Cómo hago que una comparación en SQL no distinga mayúsculas y minúsculas?
Compara los valores en minúsculas, lower(city) = 'moscow', o usa ILIKE en PostgreSQL y DuckDB. En MySQL y SQL Server las comparaciones a menudo ya no distinguen mayúsculas, según la intercalación.
¿PostgreSQL tiene la función LEN?
No. Usa length(x) o char_length(x). LEN es de SQL Server; ojo, porque el LEN de SQL Server ignora los espacios finales y el length de PostgreSQL sobre text sí los cuenta.
También en English, Português (Brasil), Tiếng Việt, 简体中文