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

Etiquetas de equipaje de papel con nombres de pasajeros que se cortan, se unen y se reetiquetan sobre una mesa

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

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, 简体中文