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
- Escalar frente a agregada: una fila entra, una fila sale
- Medir y cambiar la caja:
LENGTH,UPPER,LOWER,INITCAP - Limpiar y rellenar:
TRIM,LPAD,RPAD - Extraer, localizar y trocear
- Componer texto:
CONCAT_WS,FORMATy el||de 02-02 - Extracción con expresiones regulares
- Casos reales de TiendaVerde
- Funciones sobre columnas e índices
- Tabla comparativa por motor
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
| 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? |
Sí | 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.
- 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.
- 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 caracteresaob. YLPADgarantiza longitud exacta: si la cadena es más larga la recorta (LPAD('Valencia', 5, '.')→'Valen').
- 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;| usuario | dominio | |
|---|---|---|
| [email protected] | lucia.martinez | example.com |
| [email protected] | sofia.moreira | example.pt |
| [email protected] | camille.dubois | example.fr |
- Componer texto:
CONCAT_WS, FORMAT y el || de 02-02
CONCAT_WS, FORMAT y el || de 02-02En 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_WSresuelve 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 faltaCOALESCE, 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.
- 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 elSELECT, las filas sin coincidencia desaparecen (los 20 productos quedarían en 16). Para un valor por fila usaSUBSTRING(… FROM patron)o, desde PostgreSQL 15,REGEXP_SUBSTR(s, patron).
- 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:
| 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_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.
- 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.
- 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
LPADgarantiza longitud exacta, no mínima: si la cadena es más larga, la recorta. - Usar
||con columnas nulables. Un soloNULLanula la expresión entera.CONCAT_WSpara juntar campos,COALESCE(06-04) para poner un texto por defecto. - Creer que
CONCAT_WSresuelve todos los nulos. Con todos los argumentos nulos devuelve'', que en un informe se lee como celda vacía. YREGEXP_MATCHESen elSELECThace desaparecer las filas sin coincidencia: usaSUBSTRING(… FROM patron). - Ordenar mal la alternancia de una regex.
(g|kg)encaja lagdekg. Lo más específico va primero. - Confundir
TRIM(BOTH 'ab' FROM s)con quitar la cadena'ab'. Quita los caracteresaybsueltos. Y ojo: en PostgreSQLLENGTHcuenta 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_PARTaSTRPOS+SUBSTRINGcuando el separador sea fijo: se lee mejor y no tiene el- 1que todo el mundo olvida. Y usaFORMATpara 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:
POSITIONdevuelve la posición del espacio, así queLEFTse lo lleva también. Falta un- 1. - Problema B: cuando no hay espacio,
POSITIONdevuelve0yLEFT(s, 0)es la cadena vacía: los clientes 9 y 10, con un solo apellido, pierden el apellido entero. Con el- 1sería aún peor, porqueLEFT(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) yOCTET_LENGTH(bytes), cambias la caja conUPPER,LOWEReINITCAPy normalizas conINITCAP(LOWER(TRIM(x))). - Limpias con
TRIMy sus variantes, rellenas conLPAD/RPAD(que también recortan), extraes conSUBSTRING/LEFT/RIGHTrecordando que las posiciones empiezan en 1, localizas conPOSITION/STRPOS(que devuelven0si no encuentran) y troceas conSPLIT_PART, la función que elimina toda la aritmética de posiciones. - Compones con
CONCAT_WS, que resuelve la trampa delNULLde 02-02 —salvo cuando todos los argumentos son nulos, caso que espera aCOALESCEen 06-04— y conFORMATpara plantillas. - Extraes y sustituyes con
REGEXP_REPLACEySUBSTRING(… FROM patron), distintos de losLIKEy~de 04-01, que solo decidían si algo encajaba. Y sabes que una función sobre una columna en elWHEREtiene 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
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
