Cerrábamos el módulo 5 con una lista de carencias: sigues devolviendo nombre y apellidos en dos columnas cuando quieres una, sigues sin poder poner un texto en mayúsculas, sin extraer el gramaje de un nombre de producto y sin sacar el dominio de un correo. Este módulo empieza por ahí, porque el texto es lo que más se manipula al presentar resultados y lo que más divergencias tiene entre motores. Pero antes de la primera función hay que aclarar una confusión que arrastra mucha gente durante años: la diferencia entre una función escalar y una función de agregación. Las de agregación las viste en 04-04; las de este módulo son de la otra familia. Si esa distinción te queda clara, el resto es vocabulario.

Contenido

  1. Escalar frente a agregada: una fila entra, una fila sale
  2. Medir y cambiar la caja: LENGTH, UPPER, LOWER, INITCAP
  3. Limpiar y rellenar: TRIM, LPAD, RPAD
  4. Extraer, localizar y trocear
  5. Componer texto: CONCAT_WS, FORMAT y el || de 02-02
  6. Extracción con expresiones regulares
  7. Casos reales de TiendaVerde
  8. Funciones sobre columnas e índices
  9. Tabla comparativa por motor
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. Escalar frente a agregada: una fila entra, una fila sale

Una función escalar recibe uno o varios valores de una misma fila y devuelve un valor para esa fila. Si la consulta lee 4 filas, el resultado tiene 4 filas.

SELECT id,
       nombre,
       LENGTH(nombre) AS longitud
FROM productos
WHERE categoria_id = 4
ORDER BY id;
id nombre longitud
14 Infusión de manzanilla ecológica 20 uds 39
15 Té verde matcha ceremonial 30 g 31
16 Kombucha de jengibre 750 ml 27
17 Zumo de naranja prensado en frío 1 L 36

Una función de agregación recibe un conjunto de filas y devuelve un solo valor:

SELECT COUNT(*)            AS productos,
       MAX(LENGTH(nombre)) AS longitud_maxima,
       MIN(LENGTH(nombre)) AS longitud_minima
FROM productos
WHERE categoria_id = 4;
productos longitud_maxima longitud_minima
4 39 27

Una sola fila. Y fíjate en MAX(LENGTH(nombre)): combina las dos familias. Primero LENGTH se aplica a cada fila (escalar), después MAX colapsa los cuatro resultados en uno (agregada). El orden nunca es al revés.

Función escalar Función de agregación
Entrada Los valores de una fila Los valores de muchas filas
Salida Un valor por fila Un valor por grupo
Filas del resultado Las mismas que había Una por grupo (o una sola sin GROUP BY)
Ejemplos LENGTH, UPPER, ROUND, COALESCE COUNT, SUM, AVG, MIN, MAX
¿Puede ir en el WHERE? No (para eso está HAVING, 04-06)
Lección Módulo 6 04-04

La regla en una frase: una función escalar transforma; una de agregación resume. UPPER(nombre) transforma cada nombre; COUNT(nombre) resume todos en un número. Todas las funciones del módulo 6 son escalares.

  1. Medir y cambiar la caja

Función Qué hace Ejemplo Resultado
LENGTH(s), CHAR_LENGTH(s) Número de caracteres (sinónimos) LENGTH('Té verde matcha ceremonial 30 g') 31
OCTET_LENGTH(s) Número de bytes OCTET_LENGTH('Té verde matcha ceremonial 30 g') 32
UPPER(s) Todo a mayúsculas UPPER('Miel de azahar cruda 500 g') MIEL DE AZAHAR CRUDA 500 G
LOWER(s) Todo a minúsculas LOWER('Té Verde Matcha') té verde matcha
INITCAP(s) Primera letra de cada palabra en mayúscula, resto en minúscula INITCAP('miel de azahar cruda') Miel De Azahar Cruda

Los dos primeros números —31 y 32— resumen una fuente inagotable de errores: en UTF-8, un carácter acentuado ocupa dos bytes.

SELECT nombre, LENGTH(nombre) AS caracteres, OCTET_LENGTH(nombre) AS bytes
FROM productos WHERE id IN (1, 7, 15) ORDER BY id;
nombre caracteres bytes
Aceite de oliva virgen extra 500 ml 35 35
Champú sólido de romero 80 g 28 30
Té verde matcha ceremonial 30 g 31 32

El primer nombre no tiene tildes y los dos números coinciden; los otros dos, sí. Cuando VARCHAR(n) limita caracteres (PostgreSQL) esto da igual; cuando limita bytes (MySQL con ciertos tipos, Oracle con VARCHAR2(n BYTE)) es la causa clásica del value too long con nombres españoles.

Tres avisos sobre INITCAP: capitaliza todas las palabras, incluidas preposiciones (Miel De Azahar, no Miel de azahar), lo que para títulos comerciales queda mal y para normalizar ciudades es perfecto; respeta los acentos (INITCAP('CEPILLO DE DIENTES DE BAMBÚ')Cepillo De Dientes De Bambú); y no existe en MySQL, SQLite ni SQL Server, siendo la función más echada de menos al portar código desde PostgreSQL u Oracle.

UPPER y LOWER tratan bien las tildes porque dependen de la colación de la base, la misma es-ES-x-icu que configuraste para el ORDER BY en 02-05.

  1. Limpiar y rellenar

Función Qué hace Ejemplo Resultado
TRIM(s) Quita espacios a ambos lados TRIM(' Valencia ') 'Valencia'
LTRIM(s) / RTRIM(s) Solo izquierda / solo derecha RTRIM(' Valencia ') ' Valencia'
TRIM(BOTH 'x' FROM s) Quita el carácter indicado, no espacios TRIM(BOTH '0' FROM '00123400') '1234'
TRIM(LEADING '0' FROM s) / TRIM(TRAILING '0' FROM s) Solo un lado TRIM(LEADING '0' FROM '00123400') '123400'
LPAD(s, n, relleno) Rellena por la izquierda hasta n caracteres LPAD('7', 5, '0') '00007'
RPAD(s, n, relleno) Rellena por la derecha RPAD('Valencia', 12, '.') 'Valencia....'

TRIM(BOTH 'x' FROM s) es sintaxis SQL estándar, no una llamada normal, y por eso lleva palabras clave en lugar de comas. Se usa al limpiar importaciones: ceros a la izquierda de un código, comillas sobrantes de un CSV, puntos finales.

Dos trampas. TRIM(BOTH 'ab' FROM s) no quita la cadena 'ab': quita cualquiera de los caracteres a o b. Y LPAD garantiza longitud exacta: si la cadena es más larga la recorta (LPAD('Valencia', 5, '.')'Valen').

  1. Extraer, localizar y trocear

Función Qué hace Ejemplo Resultado
SUBSTRING(s FROM p FOR n) n caracteres desde la posición p SUBSTRING('Aceite de oliva' FROM 1 FOR 6) 'Aceite'
SUBSTRING(s FROM p) Desde p hasta el final (SUBSTRING(s, p, n) es la variante con comas) SUBSTRING('Aceite de oliva virgen extra 500 ml' FROM 30) '500 ml'
LEFT(s, n) / RIGHT(s, n) Primeros / últimos n caracteres RIGHT('Valencia', 3) 'cia'
LEFT(s, -n) Todos menos los n últimos LEFT('Valencia', -3) 'Valen'
POSITION(sub IN s) Posición de la primera aparición POSITION('oliva' IN 'Aceite de oliva') 11
STRPOS(s, sub) Igual, argumentos al revés STRPOS('[email protected]', '@') 15
REPLACE(s, viejo, nuevo) Sustituye todas las apariciones REPLACE('… extra 500 ml', '500 ml', '1 L') '… extra 1 L'
SPLIT_PART(s, sep, n) Trocea por un separador, devuelve el trozo n SPLIT_PART('[email protected]', '@', 2) 'example.com'
REVERSE(s) / REPEAT(s, n) Invierte / repite n veces REPEAT('*', 5) '*****'

Tres cosas que hay que memorizar: las posiciones empiezan en 1, no en 0 (si vienes de un lenguaje de programación, es el error de la primera semana); POSITION y STRPOS devuelven 0 cuando no encuentran, no NULL, así que puedes compararlos sin arrastrar la lógica de tres valores de 04-03; y SPLIT_PART devuelve la cadena vacía si el trozo n no existe, tampoco NULL.

SPLIT_PART es una de las funciones más útiles de PostgreSQL y no tiene equivalente directo en la mayoría de motores. Lo que con STRPOS + SUBSTRING + un - 1 fácil de olvidar cuesta dos expresiones anidadas, con ella es una llamada:

SELECT email,
       SPLIT_PART(email, '@', 1) AS usuario,
       SPLIT_PART(email, '@', 2) AS dominio
FROM clientes WHERE id IN (1, 7, 9) ORDER BY id;
email usuario dominio
[email protected] lucia.martinez example.com
[email protected] sofia.moreira example.pt
[email protected] camille.dubois example.fr

  1. Componer texto: CONCAT_WS, FORMAT y el || de 02-02

En 02-02 usaste || para juntar nombre y apellidos, y en 04-03 descubriste su trampa: si un operando es NULL, el resultado entero es NULL. Aquí están las alternativas.

Función Qué hace Ejemplo Resultado
a || b Concatena. Propaga el NULL 'Lucía' || NULL *(null)*
CONCAT(a, b, c) Concatena e ignora los NULL CONCAT('Lucía', NULL, ' Soler') 'Lucía Soler'
CONCAT_WS(sep, a, b, c) Concatena con separador e ignora los NULL CONCAT_WS(' ', 'Lucía', NULL, 'Soler') 'Lucía Soler'
FORMAT(patron, …) Plantilla con marcadores %s FORMAT('%s: %s €', 'Aceite', 12.50) 'Aceite: 12.50 €'

CONCAT_WS (With Separator) no solo ignora los nulos: tampoco pone el separador donde no hay valor. Retomamos el LEFT JOIN reflexivo de 04-03:

SELECT c.id,
       CONCAT_WS(' ', c.nombre, c.apellidos)     AS cliente,
       ref.nombre || ' ' || ref.apellidos        AS recomendador_pipe,
       CONCAT_WS(' ', ref.nombre, ref.apellidos) AS recomendador_ws
FROM clientes AS c
LEFT JOIN clientes AS ref ON c.referido_por_id = ref.id
WHERE c.id <= 5
ORDER BY c.id;
id cliente recomendador_pipe recomendador_ws
1 Lucía Martínez Soler (null)
2 Carlos Ferrer Ibáñez Lucía Martínez Soler Lucía Martínez Soler
3 Marta Sanchis Gil Lucía Martínez Soler Lucía Martínez Soler
4 Javier Ortega Ruiz (null)
5 Ana Belmonte Roca Carlos Ferrer Ibáñez Carlos Ferrer Ibáñez

Mira las filas 1 y 4: recomendador_pipe es *(null)* y recomendador_ws es la cadena vacía.

La conclusión honesta: CONCAT_WS resuelve el caso "uno de los campos falta". No resuelve "no hay nadie ahí", que necesita un texto explícito como 'Registro directo'. Para eso hace falta COALESCE, en 06-04.

FORMAT construye plantillas al estilo de printf:

SELECT FORMAT('Producto %s: %s unidades a %s €', id, stock, precio) AS ficha
FROM productos WHERE id IN (1, 13) ORDER BY id;
ficha
Producto 1: 120 unidades a 12.50 €
Producto 13: 0 unidades a 13.75 €

Ventajas sobre ||: convierte los tipos sola (no hace falta ::TEXT), la plantilla se lee de un vistazo y %s con un argumento NULL produce la cadena vacía en lugar de anular todo. Los marcadores %I (identificador) y %L (literal entrecomillado) sirven para generar SQL dinámico seguro y aparecerán en el módulo 10.

  1. Extracción con expresiones regulares

En 04-01 usaste LIKE, ILIKE y ~ para decidir si una cadena encaja con un patrón. Aquí las expresiones regulares hacen otra cosa: extraer y sustituir.

Función Qué hace Ejemplo Resultado
REGEXP_REPLACE(s, patron, repl) Sustituye la primera coincidencia REGEXP_REPLACE('a1b2c3', '[0-9]', '#') 'a#b2c3'
REGEXP_REPLACE(s, patron, repl, 'g') Sustituye todas (bandera global) REGEXP_REPLACE('a1b2c3', '[0-9]', '#', 'g') 'a#b#c#'
REGEXP_MATCHES(s, patron) Devuelve un array con los grupos capturados REGEXP_MATCHES('500 ml', '(\d+) (\w+)') {500,ml}
SUBSTRING(s FROM patron) Extrae la primera coincidencia como texto SUBSTRING('Té verde 30 g' FROM '\d+ ?\w+$') '30 g'

El caso práctico: sacar el gramaje del nombre del producto. Los nombres de TiendaVerde terminan con formato (500 ml, 30 g, 1 kg, 20 uds) salvo cuatro que no lo llevan.

SELECT id,
       nombre,
       SUBSTRING(nombre FROM '[0-9]+ ?(ml|kg|uds|g|L)$')         AS gramaje,
       REGEXP_REPLACE(nombre, '\s*[0-9]+ ?(ml|kg|uds|g|L)$', '') AS nombre_corto
FROM productos
WHERE id IN (1, 2, 11, 14, 18)
ORDER BY id;
id nombre gramaje nombre_corto
1 Aceite de oliva virgen extra 500 ml 500 ml Aceite de oliva virgen extra
2 Arroz integral ecológico 1 kg 1 kg Arroz integral ecológico
11 Estropajo vegetal de luffa (pack 3) (null) Estropajo vegetal de luffa (pack 3)
14 Infusión de manzanilla ecológica 20 uds 20 uds Infusión de manzanilla ecológica
18 Cepillo de dientes de bambú (null) Cepillo de dientes de bambú

Los cuatro productos sin gramaje —11, 12, 13 y 18— devuelven NULL en SUBSTRING y el nombre intacto en REGEXP_REPLACE: si no hay coincidencia, no hay sustitución.

Detalle de la alternancia (ml|kg|uds|g|L): kg va antes que g. Al revés, el motor encajaría la g de kg y el resultado sería erróneo. La alternancia prueba las opciones en el orden escrito.

Trampa de REGEXP_MATCHES: devuelve un conjunto de filas, no un valor; puesta en el SELECT, las filas sin coincidencia desaparecen (los 20 productos quedarían en 16). Para un valor por fila usa SUBSTRING(… FROM patron) o, desde PostgreSQL 15, REGEXP_SUBSTR(s, patron).

  1. Casos reales de TiendaVerde

7.1. Ficha de cliente

SELECT id,
       CONCAT_WS(' ', nombre, apellidos)                   AS nombre_completo,
       LEFT(nombre, 1) || '.' || LEFT(apellidos, 1) || '.' AS iniciales,
       UPPER(SPLIT_PART(apellidos, ' ', 1))                AS primer_apellido,
       SPLIT_PART(email, '@', 2)                           AS dominio
FROM clientes
WHERE id <= 4
ORDER BY id;
id nombre_completo iniciales primer_apellido dominio
1 Lucía Martínez Soler L.M. MARTÍNEZ example.com
2 Carlos Ferrer Ibáñez C.F. FERRER example.com
3 Marta Sanchis Gil M.S. SANCHIS example.com
4 Javier Ortega Ruiz J.O. ORTEGA example.com

SPLIT_PART(apellidos, ' ', 1) aprovecha que los dos apellidos van juntos separados por un espacio: es el "backfill que parte una columna" anunciado en 05-06.

7.2. Reparto de clientes por dominio

SELECT SPLIT_PART(email, '@', 2) AS dominio,
       COUNT(*)                  AS clientes
FROM clientes
GROUP BY SPLIT_PART(email, '@', 2)
ORDER BY clientes DESC, dominio;
dominio clientes
example.com 11
example.fr 2
example.pt 2

Conviven las dos familias de la sección 1: SPLIT_PART es escalar y se calcula fila a fila; COUNT(*) resume cada grupo. Y el GROUP BY repite la expresión completa, no el alias: es el "agrupar por expresión" que 04-05 dejó apuntado.

7.3. Normalizar ciudades

Los datos de TiendaVerde están limpios, pero un fichero de importación real nunca lo está. La receta canónica es una cadena de tres funciones, en este orden:

SELECT INITCAP(LOWER(TRIM('  vaLENcia  '))) AS ciudad_normalizada;
ciudad_normalizada
Valencia

INITCAP(LOWER(TRIM(x))) convierte ' vaLENcia ', 'VALENCIA' y 'valencia' en el mismo 'Valencia': TRIM quita los bordes, LOWER iguala la caja y INITCAP la reconstruye. Es el patrón que usarás siempre que agrupes por una columna de texto introducida a mano.

7.4. Ofuscar el correo electrónico

SELECT id,
       email,
       LEFT(SPLIT_PART(email, '@', 1), 2)
         || REPEAT('*', LENGTH(SPLIT_PART(email, '@', 1)) - 2)
         || '@' || SPLIT_PART(email, '@', 2) AS email_ofuscado
FROM clientes
WHERE id <= 3
ORDER BY id;
id email email_ofuscado
1 [email protected] lu************@example.com
2 [email protected] ca***********@example.com
3 [email protected] ma***********@example.com

Aviso importante: esto es enmascaramiento de presentación, no anonimización. El dato original sigue íntegro en la tabla y cualquiera con acceso lo ve. La anonimización real —seudonimización, agregación con umbral mínimo, borrado— es una cuestión legal (RGPD) y de arquitectura, no de funciones de cadena. Úsalo para que un informe no muestre correos completos, nunca como sustituto de una política de protección de datos.

  1. Funciones sobre columnas e índices

Una advertencia para el futuro. Un filtro como WHERE LOWER(email) = '[email protected]' devuelve la fila correcta, pero al envolver la columna en una función el motor deja de comparar valores de la columna y pasa a comparar valores calculados, que ningún índice normal contiene. La solución existe —el índice de expresión, CREATE INDEX … ON clientes (LOWER(email))— pero los índices son el módulo 8 y allí lo verás medido con EXPLAIN en 08-03. De momento quédate con la regla: una función sobre una columna en el WHERE tiene coste. En el SELECT no hay problema: transformar lo que ya has leído es gratis comparado con leerlo.

  1. Tabla comparativa por motor

Tarea PostgreSQL 16 MySQL 8 SQLite SQL Server Oracle
Longitud en caracteres LENGTH, CHAR_LENGTH CHAR_LENGTH (LENGTH da bytes) LENGTH LEN (ignora espacios finales) LENGTH
Longitud en bytes OCTET_LENGTH LENGTH DATALENGTH LENGTHB
Subcadena SUBSTRING(s FROM p FOR n) SUBSTRING, SUBSTR SUBSTR SUBSTRING(s,p,n) (n obligatorio) SUBSTR(s,p,n)
Primeros / últimos LEFT, RIGHT LEFT, RIGHT SUBSTR LEFT, RIGHT SUBSTR
Capitalizar palabras INITCAP no existe no existe no existe INITCAP
Concatenar ||, CONCAT, CONCAT_WS CONCAT, CONCAT_WS (|| solo con PIPES_AS_CONCAT) || +, CONCAT, CONCAT_WS (2012+) ||, CONCAT (solo 2 argumentos)
Posición de una subcadena POSITION, STRPOS LOCATE, INSTR INSTR CHARINDEX INSTR
Rellenar / recortar carácter LPAD, RPAD, TRIM(BOTH 'x' FROM s) igual no existe (printf); TRIM(s,'x') REPLICATE; TRIM('x' FROM s) (2022+) LPAD, RPAD, TRIM
Trocear por separador SPLIT_PART SUBSTRING_INDEX no existe STRING_SPLIT (devuelve tabla) REGEXP_SUBSTR
Sustituir con regex REGEXP_REPLACE REGEXP_REPLACE (8.0+) no existe no existe REGEXP_REPLACE
Repetir / invertir REPEAT, REVERSE REPEAT, REVERSE no existen REPLICATE, REVERSE RPAD, REVERSE

Dos trampas de portabilidad que cuestan tardes enteras. En Oracle, la cadena vacía '' es NULL: LENGTH('') devuelve NULL, no 0, y todo lo que aprendiste en 04-03 sobre distinguir '' de NULL no aplica allí. Y en SQL Server, LEN ignora los espacios finales: LEN('Valencia ') es 8, no 10; para contar de verdad hace falta DATALENGTH.

Errores Comunes y Consejos

  • Confundir escalar con agregada. LENGTH(nombre) da una fila por producto; MAX(LENGTH(nombre)) da una sola. Si el resultado tiene menos filas de las esperadas, mira si has metido un agregado sin querer.
  • Contar desde 0. En SQL las posiciones de cadena empiezan en 1. Y LPAD garantiza longitud exacta, no mínima: si la cadena es más larga, la recorta.
  • Usar || con columnas nulables. Un solo NULL anula la expresión entera. CONCAT_WS para juntar campos, COALESCE (06-04) para poner un texto por defecto.
  • Creer que CONCAT_WS resuelve todos los nulos. Con todos los argumentos nulos devuelve '', que en un informe se lee como celda vacía. Y REGEXP_MATCHES en el SELECT hace desaparecer las filas sin coincidencia: usa SUBSTRING(… FROM patron).
  • Ordenar mal la alternancia de una regex. (g|kg) encaja la g de kg. Lo más específico va primero.
  • Confundir TRIM(BOTH 'ab' FROM s) con quitar la cadena 'ab'. Quita los caracteres a y b sueltos. Y ojo: en PostgreSQL LENGTH cuenta caracteres, en MySQL cuenta bytes.
  • Consejo: normaliza siempre con INITCAP(LOWER(TRIM(x))), en ese orden, antes de agrupar por texto introducido a mano.
  • Consejo: prefiere SPLIT_PART a STRPOS + SUBSTRING cuando el separador sea fijo: se lee mejor y no tiene el - 1 que todo el mundo olvida. Y usa FORMAT para plantillas de más de dos trozos.

Ejercicios

Ejercicio 1

Marketing quiere etiquetas para el catálogo. Para cada producto activo de las categorías 1 y 4, devuelve el id con formato TV-00001 (prefijo, guion y cinco dígitos con ceros a la izquierda), el nombre sin el gramaje y en formato título, el gramaje por separado (o NULL si no lo lleva) y la longitud en caracteres del nombre original. Ordena por id.

Ejercicio 2

Atención al cliente necesita una vista de contacto ofuscada. Devuelve, para cada cliente, el nombre completo en una columna, las iniciales de nombre y de los dos apellidos (L.M.S. para Lucía Martínez Soler) y el correo con el usuario oculto salvo sus dos primeras letras. Añade el dominio de primer nivel (com, pt, fr) y, en una segunda consulta, cuenta cuántos clientes hay de cada uno. (Pista: los apellidos van separados por un espacio; SPLIT_PART y LEFT bastan.)

Ejercicio 3

Un compañero ha escrito LEFT(apellidos, POSITION(' ' IN apellidos)) AS primer_apellido sobre clientes. (1) ¿Qué dos problemas tiene el resultado? Fíjate en los clientes 9 y 10. (2) Corrígelo usando POSITION. (3) Corrígelo usando SPLIT_PART y explica por qué esa versión no tiene ninguno de los dos problemas.

Soluciones

Solución 1

SELECT 'TV-' || LPAD(id::TEXT, 5, '0')                                    AS referencia,
       INITCAP(REGEXP_REPLACE(nombre, '\s*[0-9]+ ?(ml|kg|uds|g|L)$', '')) AS titulo,
       SUBSTRING(nombre FROM '[0-9]+ ?(ml|kg|uds|g|L)$')                  AS gramaje,
       LENGTH(nombre)                                                     AS longitud
FROM productos
WHERE activo = TRUE
  AND categoria_id IN (1, 4)
ORDER BY id;
referencia titulo gramaje longitud
TV-00001 Aceite De Oliva Virgen Extra 500 ml 35
TV-00002 Arroz Integral Ecológico 1 kg 29
TV-00003 Miel De Azahar Cruda 500 g 26
TV-00004 Pasta De Espelta 500 g 22
TV-00005 Tomate Triturado Ecológico 400 g 32
TV-00014 Infusión De Manzanilla Ecológica 20 uds 39
TV-00015 Té Verde Matcha Ceremonial 30 g 31
TV-00016 Kombucha De Jengibre 750 ml 27
TV-00017 Zumo De Naranja Prensado En Frío 1 L 36

9 productos: 5 de Alimentación y 4 de Bebidas. El ::TEXT es obligatorio porque LPAD espera texto y id es entero (conversiones: 06-04). Y ahí se ve el defecto de INITCAP de la sección 2: Aceite De Oliva, con la preposición capitalizada; corregirlo requiere CASE (06-05).

Solución 2

SELECT CONCAT_WS(' ', nombre, apellidos) AS cliente,
       LEFT(nombre, 1) || '.'
         || LEFT(SPLIT_PART(apellidos, ' ', 1), 1) || '.'
         || LEFT(SPLIT_PART(apellidos, ' ', 2), 1) || '.' AS iniciales,
       LEFT(SPLIT_PART(email, '@', 1), 2)
         || REPEAT('*', LENGTH(SPLIT_PART(email, '@', 1)) - 2)
         || '@' || SPLIT_PART(email, '@', 2)              AS contacto,
       SPLIT_PART(email, '.', 3)                          AS pais_dominio
FROM clientes
WHERE id IN (1, 9, 10)
ORDER BY id;
cliente iniciales contacto pais_dominio
Lucía Martínez Soler L.M.S. lu************@example.com com
Camille Dubois C.D.. ca************@example.fr fr
Julien Moreau J.M.. ju***********@example.fr fr

Los dos clientes franceses tienen un solo apellido, y SPLIT_PART(apellidos, ' ', 2) devuelve la cadena vacía: de ahí el C.D.. con dos puntos seguidos. SPLIT_PART no falla ni devuelve NULL, devuelve '', y el || lo concatena sin protestar. Arreglarlo exige preguntar "¿hay segundo apellido?", es decir NULLIF (06-04) o CASE (06-05).

El recuento —GROUP BY SPLIT_PART(email, '.', 3) con COUNT(*)— da 11 clientes com, 2 fr y 2 pt, los mismos 15 repartidos que en la sección 7.2.

Solución 3

1. Ejecutada sobre los clientes 1, 9 y 10 da Martínez (con espacio final) para el primero y la cadena vacía para los otros dos.

  • Problema A: POSITION devuelve la posición del espacio, así que LEFT se lo lleva también. Falta un - 1.
  • Problema B: cuando no hay espacio, POSITION devuelve 0 y LEFT(s, 0) es la cadena vacía: los clientes 9 y 10, con un solo apellido, pierden el apellido entero. Con el - 1 sería aún peor, porque LEFT(s, -1) devuelve todo menos el último carácter: Duboi.

2 y 3. Con POSITION hay que garantizar que siempre haya un espacio; con SPLIT_PART no hace falta nada:

-- ✅ CORRECTA, pero necesita un truco que hay que comentar
SELECT id, apellidos,
       LEFT(apellidos || ' ', POSITION(' ' IN apellidos || ' ') - 1) AS primer_apellido
FROM clientes WHERE id IN (1, 9, 10) ORDER BY id;

-- ✅ CORRECTA y legible
SELECT id, apellidos, SPLIT_PART(apellidos, ' ', 1) AS primer_apellido
FROM clientes WHERE id IN (1, 9, 10) ORDER BY id;

Ambas devuelven lo mismo:

id apellidos primer_apellido
1 Martínez Soler Martínez
9 Dubois Dubois
10 Moreau Moreau

La segunda no tiene ninguno de los dos problemas porque SPLIT_PART no trabaja con posiciones: trocea por el separador y devuelve el trozo pedido. Si no hay separador, el trozo 1 es la cadena entera. Ni - 1 que olvidar, ni caso especial que tratar. Esa es la moraleja: cuando existe una función que expresa tu intención, úsala en lugar de reconstruirla con aritmética de posiciones.

Conclusión

Ya sabes transformar texto:

  • Distingues una función escalar de una de agregación: la primera transforma fila a fila, la segunda resume muchas filas en una. Todo el módulo 6 es escalar.
  • Mides con LENGTH (caracteres) y OCTET_LENGTH (bytes), cambias la caja con UPPER, LOWER e INITCAP y normalizas con INITCAP(LOWER(TRIM(x))).
  • Limpias con TRIM y sus variantes, rellenas con LPAD/RPAD (que también recortan), extraes con SUBSTRING/LEFT/RIGHT recordando que las posiciones empiezan en 1, localizas con POSITION/STRPOS (que devuelven 0 si no encuentran) y troceas con SPLIT_PART, la función que elimina toda la aritmética de posiciones.
  • Compones con CONCAT_WS, que resuelve la trampa del NULL de 02-02 —salvo cuando todos los argumentos son nulos, caso que espera a COALESCE en 06-04— y con FORMAT para plantillas.
  • Extraes y sustituyes con REGEXP_REPLACE y SUBSTRING(… FROM patron), distintos de los LIKE y ~ de 04-01, que solo decidían si algo encajaba. Y sabes que una función sobre una columna en el WHERE tiene un coste que se mide en el módulo 8.

En la lección siguiente, funciones numéricas, la misma idea aplicada a los números: ROUND con sus hermanas TRUNC, CEIL y FLOOR, la aritmética con MOD, POWER y ABS, y dos trampas que cuestan dinero de verdad. La primera: 10 / 3 no vale 3.33, vale 3, y hay tres formas distintas de arreglarlo. La segunda, más grave: ROUND(2.5) y ROUND(2.5::DOUBLE PRECISION) no devuelven lo mismo en PostgreSQL, y esa diferencia de un céntimo, multiplicada por un millón de líneas de factura, es la razón por la que en 01-04 se dijo que el dinero nunca se guarda en coma flotante. Vamos a demostrarlo.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados