Capítulo 5 de 15 12 secciones 19 min

Texto y fechas, donde los cuatro se separan

Limpiar nombres, buscar sin que importen las mayúsculas y hacerle cuentas a una fecha en PostgreSQL, MySQL, SQL Server y SQLite.

El 80% del SQL es igual en los cuatro motores, y este capítulo va del 20% que no: pegar texto es || en PostgreSQL y SQLite, CONCAT() en MySQL y + en SQL Server; LIKE distingue mayúsculas solo en PostgreSQL; y SQLite no tiene tipo fecha, guarda texto en formato AAAA-MM-DD. Todo lo ejecutable corre sobre tienda.db.

Un cliente me pidió el ranking de sus marcas. Doce locales, doce marcas, un Excel de media hora. Corrí la consulta y me salieron veintitrés.

No había veintitrés marcas. Había doce, y once estaban escritas con mayúsculas y con dos espacios delante porque alguien las cargó copiando de otro sistema. Para la base, Market Central y MARKET CENTRAL son dos cosas distintas. Y no te avisa. Te entrega el reporte con cara de que todo está bien 🙃

Este capítulo va de eso: de texto sucio y de fechas que no son fechas. Son los dos sitios donde los cuatro motores más se separan, así que aquí la tabla de dialectos vale doble.

La tienda que aparecía dos veces

Vamos a ver la suciedad con nuestros ojos. El truco es meter el texto entre corchetes: los espacios de los bordes son invisibles hasta que pones algo al lado.

SELECT id, '[' || nombre || ']' AS entre_corchetes
FROM clientes
WHERE nombre <> TRIM(nombre)
ORDER BY id
LIMIT 5;
id  entre_corchetes
--  ----------------------------
12  [  MARKET CENTRAL 012  ]
13  [  CAFE DEL PUERTO 013  ]
20  [  ALMACENES VEGA 020  ]
26  [  MINIMARKET EL SOL 026  ]
32  [  AUTOSERVICIO NORTE 032  ]

Dieciocho clientes de ciento veinte vienen así. Y fíjate que además están en mayúsculas, que es la otra mitad del problema.

Ahora la consulta que me dio veintitrés. La idea es quitarle los tres dígitos del final al nombre para quedarme con la marca.

SELECT COUNT(DISTINCT SUBSTR(nombre, 1, LENGTH(nombre) - 4)) AS marcas
FROM clientes;
marcas
------
23
SELECT COUNT(DISTINCT UPPER(TRIM(SUBSTR(TRIM(nombre), 1, LENGTH(TRIM(nombre)) - 4)))) AS marcas
FROM clientes;
marcas
------
12

Veintitrés contra doce. La misma pregunta, la misma base, el mismo día. Lo único que cambió es que la segunda limpia antes de contar.

Esa consulta se lee de adentro hacia afuera, como las muñecas rusas: TRIM quita los espacios, SUBSTR corta el código, TRIM otra vez limpia el espacio que quedó, UPPER pone todo en mayúsculas y recién ahí COUNT(DISTINCT ...) cuenta. Cinco funciones que vamos a ver una por una 🧼

Y guárdate la regla, que es la que de verdad importa: antes de agrupar por un texto, límpialo. Siempre. No importa qué tan confiable te digan que es la base.

Juntar texto

SELECT nombre || ' - ' || ciudad AS ficha
FROM clientes
ORDER BY id
LIMIT 4;
ficha
-----------------------------
Minimarket El Sol 001 - Piura
Minimarket El Sol 002 - Piura
Minimarket El Sol 003 - Piura
Mayorista Peru 004 - Lima

Esas dos barras verticales son el "pega esto con esto". Y son la primera cosa que cambia de motor a motor.

Pegar dos textos

PostgreSQLnombre || ' - ' || ciudad
MySQLCONCAT(nombre, ' - ', ciudad)
SQL Servernombre + ' - ' + ciudad
SQLitenombre || ' - ' || ciudad

MySQL usa || para el OR lógico, así que ahí las barras no pegan nada: te devuelven 0 o 1 y encima sin error. En SQL Server el + hace de suma y de pegamento según los tipos, que es cómo aparece el "conversion failed" cuando una de las dos columnas es un número.

Si quieres una sola forma que funcione en los cuatro, es CONCAT(): existe en PostgreSQL, en MySQL, en SQL Server desde 2012 y en SQLite desde la versión 3.44. Pero ojo, porque con nulos no se portan igual.

SELECT 'Lima' || NULL AS con_pipes, COALESCE('Lima' || NULL, 'se perdio') AS rescatado;
con_pipes  rescatado
---------  ---------
           se perdio
SELECT CONCAT('Lima', NULL, 'Peru') AS con_concat;
con_concat
----------
LimaPeru

Con || un solo nulo se come toda la frase, igual que en el capítulo 4. Con CONCAT() en SQLite el nulo se ignora y sale LimaPeru.

Qué hace CONCAT cuando uno de los textos es NULL

PostgreSQLignora el NULL
MySQLdevuelve NULL entero
SQL Servertrata el NULL como texto vacío
SQLiteignora el NULL

Tres lo ignoran y MySQL no, así que la misma consulta que en Postgres te arma la ficha del cliente, en MySQL te deja la columna vacía en cuanto falte un dato. Si hay nulos posibles, envuelve cada trozo en COALESCE y deja de depender del motor.

Mayúsculas, minúsculas y espacios que no ves

UPPER y LOWER hacen lo que suena y se escriben igual en los cuatro. TRIM quita los espacios de los dos bordes, y también en los cuatro.

SELECT id,
       LENGTH(nombre) AS largo,
       LENGTH(TRIM(nombre)) AS largo_limpio,
       '[' || TRIM(nombre) || ']' AS limpio
FROM clientes
WHERE id IN (1, 26)
ORDER BY id;
id  largo  largo_limpio  limpio
--  -----  ------------  -----------------------
1   21     21            [Minimarket El Sol 001]
26  25     21            [MINIMARKET EL SOL 026]

El cliente 26 mide 25 caracteres pero su nombre real mide 21. Los cuatro sobrantes son aire, y ese aire es lo que hacía que apareciera como una marca aparte.

Contar cuántos caracteres tiene un texto

PostgreSQLLENGTH(nombre)
MySQLCHAR_LENGTH(nombre)
SQL ServerLEN(nombre)
SQLiteLENGTH(nombre)

Aquí hay dos trampas. En MySQL, LENGTH cuenta BYTES, así que "Ñaña" mide 6 y no 4: para caracteres es CHAR_LENGTH. Y el LEN de SQL Server ignora los espacios del final, así que el cliente 26 mediría 23 y no 25, y la suciedad se te esconde justo cuando la estás buscando.

Quitar los espacios de los bordes

PostgreSQLTRIM(nombre)
MySQLTRIM(nombre)
SQL ServerTRIM(nombre) -- desde 2017; antes LTRIM(RTRIM(nombre))
SQLiteTRIM(nombre)

De las pocas que se escriben igual en los cuatro. Si trabajas contra un SQL Server viejito, el LTRIM(RTRIM(...)) sigue funcionando en todas las versiones, así que es el que no falla nunca.

Cortar y reemplazar

SUBSTR(texto, desde, cuántos) corta un pedazo. Y ojo con algo que confunde el primer día: en SQL se empieza a contar en 1, no en 0. Si vienes de Python, es al revés de lo que tienes en el dedo.

SELECT id,
       '[' || SUBSTR(nombre, -3) || ']' AS ultimos_tres,
       UPPER(TRIM(SUBSTR(TRIM(nombre), 1, LENGTH(TRIM(nombre)) - 4))) AS marca
FROM clientes
WHERE id IN (1, 26)
ORDER BY id;
id  ultimos_tres  marca
--  ------------  -----------------
1   [001]         MINIMARKET EL SOL
26  [6  ]         MINIMARKET EL SOL

Mira el cliente 26: pedí los últimos tres caracteres y me trajo 6 y dos espacios, porque los últimos tres caracteres de verdad son espacios. La suciedad no se arregla sola en ningún paso, hay que limpiarla antes de cortar. Por eso la columna marca lleva el TRIM por dentro.

Cortar un pedazo de texto

PostgreSQLSUBSTRING(nombre FROM 1 FOR 10) -- también SUBSTR(nombre, 1, 10)
MySQLSUBSTRING(nombre, 1, 10)
SQL ServerSUBSTRING(nombre, 1, 10) -- los tres argumentos son obligatorios
SQLiteSUBSTR(nombre, 1, 10)

SUBSTRING(texto, desde, cuántos) con esos tres argumentos funciona en los cuatro y es la forma que yo escribo. Lo que no viaja es el número negativo para contar desde el final: en SQL Server no existe y hay que usar RIGHT(nombre, 3).

REPLACE cambia un texto por otro, y esta sí se escribe igual en todos.

SELECT id, direccion, REPLACE(direccion, 'nro', 'N.') AS normalizada
FROM direcciones
ORDER BY id
LIMIT 3;
id  direccion        normalizada
--  ---------------  --------------
1   Av. 847 nro 331  Av. 847 N. 331
2   Av. 283 nro 161  Av. 283 N. 161
3   Av. 672 nro 178  Av. 672 N. 178

Reemplazar un texto por otro

PostgreSQL, MySQL, SQL Server y SQLiteREPLACE(direccion, 'nro', 'N.')

Una de las poquísimas que se escribe idéntica en los cuatro. Reemplaza TODAS las apariciones, no solo la primera.

Buscar, y la trampa de las mayúsculas

En el capítulo 3 vimos LIKE y quedó dicho que se comporta distinto en cada motor. Ahora lo vamos a medir, que es otra cosa.

SELECT COUNT(*) AS con_like
FROM clientes
WHERE nombre LIKE '%Sol%';
con_like
--------
11
SELECT COUNT(*) AS con_glob
FROM clientes
WHERE nombre GLOB '*Sol*';
con_glob
--------
10

Once y diez. La diferencia es el cliente 26, el de MINIMARKET EL SOL 026: el LIKE de SQLite no distingue mayúsculas y lo encuentra, y el GLOB sí distingue y lo deja fuera.

Y aquí está lo que quiero que se te quede: esa misma consulta con LIKE te devuelve 10 en PostgreSQL, porque ahí el LIKE sí distingue mayúsculas. Mismo SQL, misma data, distinto número. Sin error, sin aviso, sin nada 😳

SELECT COUNT(*) AS a_prueba_de_motor
FROM clientes
WHERE UPPER(nombre) LIKE '%SOL%';
a_prueba_de_motor
-----------------
11

Buscar sin que importen las mayúsculas

PostgreSQLnombre ILIKE '%sol%' -- el LIKE normal SÍ distingue
MySQLnombre LIKE '%sol%' -- el collation por defecto no distingue
SQL Servernombre LIKE '%sol%' -- según el collation de la base
SQLitenombre LIKE '%sol%' -- no distingue, pero solo en letras sin tilde

La única que da el mismo número en los cuatro es UPPER(columna) LIKE '%TEXTO%'. Cuesta seis letras más y te ahorra la conversación de por qué el reporte de la nube y el de tu laptop no coinciden.

Un detalle peruano que muerde: el LIKE de SQLite ignora mayúsculas solo en el alfabeto inglés. Con Ñ o con vocales con tilde vuelve a distinguir, así que '%ÑAÑA%' no encuentra Ñaña. Nuestros nombres de negocio están llenos de tildes, o sea que esto no es un caso raro de manual, es el martes 🇵🇪

Las fechas no son fechas

Esta es la parte que hay que leer despacio, porque de aquí salen los errores más caros y ninguno da error.

SELECT id, fecha, TYPEOF(fecha) AS tipo, monto, TYPEOF(monto) AS tipo_monto
FROM pedidos
ORDER BY id
LIMIT 3;
id  fecha       tipo  monto   tipo_monto
--  ----------  ----  ------  ----------
1   2025-05-30  text  892.06  real
2   2025-06-03  text  731.09  real
3   2025-09-23  text  407.39  real

text. La columna fecha no guarda fechas, guarda texto que a nosotras nos parece una fecha. Y no es que la base esté mal hecha: SQLite no tiene tipo fecha, punto. Los otros tres sí.

Cómo se guarda una fecha

PostgreSQLDATE, TIMESTAMP
MySQLDATE, DATETIME, TIMESTAMP
SQL ServerDATE, DATETIME2
SQLiteno existe: se guarda como TEXT en formato AAAA-MM-DD

Que SQLite no tenga tipo fecha suena a defecto y en la práctica funciona, porque el formato AAAA-MM-DD ordena y compara bien como texto: el 2025 va antes que el 2026 tanto en el calendario como en el diccionario. Lo que no funciona es cualquier otro formato.

Y ahí está la trampa. Mira lo que pasa si escribes la fecha como la escribimos en Perú.

SELECT COUNT(*) AS pedidos_de_2026
FROM pedidos
WHERE fecha >= '01/01/2026';
pedidos_de_2026
---------------
900

Novecientos. O sea todos los pedidos de la tabla, incluidos los de 2025.

¿Por qué? Porque está comparando texto letra por letra, como el diccionario. El 0 de 01/01/2026 va antes que el 2 de 2025-05-30, así que todas las fechas le parecen mayores. Cero errores, cero avisos, un número que está mal.

SELECT COUNT(*) AS pedidos_de_2026
FROM pedidos
WHERE fecha >= '2026-01-01';
pedidos_de_2026
---------------
292

Doscientos noventa y dos. Ese es el bueno.

Y si intentas convertir la fecha peruana a fecha de verdad, tampoco te avisa.

SELECT DATE('24/06/2026') AS a_la_peruana,
       DATE('2026-06-24') AS en_iso;
a_la_peruana  en_iso
------------  ----------
              2026-06-24

La primera sale vacía, que como vimos en el capítulo 4 es NULL. SQLite no entendió la fecha y en vez de reclamar, se encogió de hombros.

La regla, y va en mayúsculas porque me ha costado caro: dentro de la base, las fechas se escriben AAAA-MM-DD. El formato bonito se pone al final, cuando el número ya está calculado.

El error que sí es error

Ahora prueba lo que hace todo el mundo que viene de MySQL o de SQL Server.

SELECT YEAR(fecha) AS anio
FROM pedidos
LIMIT 3;
OperationalError: no such function: YEAR

no such function: YEAR. Y este es de los buenos, en serio 🙌 Porque te lo dice de frente en vez de devolverte un número equivocado.

YEAR() existe en MySQL y en SQL Server, no existe en SQLite, y en PostgreSQL tampoco: ahí es EXTRACT. En SQLite todo lo de fechas pasa por una sola función, STRFTIME.

SELECT COUNT(*) AS pedidos_de_junio_2026
FROM pedidos
WHERE STRFTIME('%Y-%m', fecha) = '2026-06';
pedidos_de_junio_2026
---------------------
50

Sacar el año de una fecha

PostgreSQLEXTRACT(YEAR FROM fecha)
MySQLYEAR(fecha) -- también EXTRACT(YEAR FROM fecha)
SQL ServerYEAR(fecha) -- también DATEPART(year, fecha)
SQLiteCAST(STRFTIME('%Y', fecha) AS INTEGER)

EXTRACT es la forma del estándar y funciona en PostgreSQL y en MySQL. SQL Server no la tiene. Para el mes es igual: MONTH() en dos, EXTRACT en dos, STRFTIME('%m', ...) en SQLite.

El CAST del final no es decoración: STRFTIME devuelve texto, así que sin él te llevas '2026' entre comillas y no puedes sumarlo ni restarlo.

Hoy, hace 90 días y el primero del mes

Casi todo reporte de negocio empieza igual: "lo de los últimos 90 días". Para eso hay que saber hacerle cuentas a una fecha.

SELECT DATE('2026-06-24', '-90 days') AS hace_90_dias,
       DATE('2026-06-24', '+1 month') AS en_un_mes,
       DATE('2026-06-24', 'start of month') AS inicio_de_mes;
hace_90_dias  en_un_mes   inicio_de_mes
------------  ----------  -------------
2026-03-26    2026-07-24  2026-06-01
SELECT COUNT(*) AS ultimos_90_dias
FROM pedidos
WHERE fecha >= DATE('2026-06-24', '-90 days');
ultimos_90_dias
---------------
166

Ciento sesenta y seis pedidos en los últimos 90 días de la base. Puse la fecha a mano porque el último pedido de esta tienda es del 24 de junio de 2026 y quiero que a ti te salga el mismo número que a mí. En tu trabajo, en cambio, vas a querer "hoy", y ahí es donde los cuatro se ponen creativos.

La fecha de hoy

PostgreSQLCURRENT_DATE
MySQLCURDATE() -- también CURRENT_DATE
SQL ServerCAST(GETDATE() AS DATE)
SQLiteDATE('now') -- también CURRENT_DATE

CURRENT_DATE funciona en tres de los cuatro; el que se queda fuera es SQL Server, que tiene CURRENT_TIMESTAMP pero no CURRENT_DATE. Y en SQLite el DATE('now') te da la hora UTC, o sea cinco horas adelantada de Lima: si te importa el borde del día, es DATE('now', 'localtime').

Restarle 90 días a una fecha

PostgreSQLfecha - INTERVAL '90 days'
MySQLDATE_SUB(fecha, INTERVAL 90 DAY)
SQL ServerDATEADD(day, -90, fecha)
SQLiteDATE(fecha, '-90 days')

Cuatro sintaxis distintas para la misma resta, y ninguna se parece a otra. Esta es la fila que yo tengo pegada al monitor.

Ponerle formato al final

SELECT id, fecha, STRFTIME('%d/%m/%Y', fecha) AS a_la_peruana
FROM pedidos
ORDER BY id
LIMIT 3;
id  fecha       a_la_peruana
--  ----------  ------------
1   2025-05-30  30/05/2025
2   2025-06-03  03/06/2025
3   2025-09-23  23/09/2025

Mostrar una fecha como 24/06/2026

PostgreSQLTO_CHAR(fecha, 'DD/MM/YYYY')
MySQLDATE_FORMAT(fecha, '%d/%m/%Y')
SQL ServerFORMAT(fecha, 'dd/MM/yyyy') -- también CONVERT(varchar, fecha, 103)
SQLiteSTRFTIME('%d/%m/%Y', fecha)

Cuatro nombres distintos y encima dos idiomas de máscara: MySQL y SQLite usan %d/%m/%Y, PostgreSQL usa DD/MM/YYYY y SQL Server usa dd/MM/yyyy con la M en mayúscula para el mes, porque la m minúscula ahí son los minutos.

Y una cosa de orden que vale para los cuatro: el formato se pone en el SELECT, nunca en el WHERE ni en el ORDER BY. Si ordenas por '24/06/2026' estás ordenando por día, y diciembre de 2025 te va a quedar antes que enero del mismo año.

El día de la semana también sale de ahí, y sirve para preguntas de negocio de verdad.

SELECT COUNT(*) AS pedidos_en_domingo
FROM pedidos
WHERE STRFTIME('%w', fecha) = '0';
pedidos_en_domingo
------------------
129

En SQLite el domingo es el 0. En MySQL DAYOFWEEK() también arranca el domingo pero en 1, y en SQL Server DATEPART(weekday, ...) depende de una configuración de la sesión. Si vas a usar el día de la semana, compruébalo con una fecha que sepas antes de creerle.

Cuánto tiempo pasó

SELECT nombre,
       fecha_alta,
       CAST(JULIANDAY('2026-06-24') - JULIANDAY(fecha_alta) AS INTEGER) AS dias_con_nosotros
FROM clientes
ORDER BY dias_con_nosotros DESC
LIMIT 3;
nombre                          fecha_alta  dias_con_nosotros
------------------------------  ----------  -----------------
Bodega La Esquina 044           2025-01-04  536
Almacenes Vega 014              2025-01-05  535
  RESTAURANTE MIRAFLORES 070    2025-01-05  535

JULIANDAY convierte una fecha en un número de días corridos, así que restar dos julianday da días. Y mira el tercero de la lista, que sigue teniendo su nombre sucio: la limpieza que hicimos arriba no cambió la base, solo la consulta 🧽

Días entre dos fechas

PostgreSQLfecha_fin - fecha_ini -- con columnas DATE devuelve el número de días
MySQLDATEDIFF(fecha_fin, fecha_ini)
SQL ServerDATEDIFF(day, fecha_ini, fecha_fin)
SQLiteJULIANDAY(fecha_fin) - JULIANDAY(fecha_ini)

Fíjate bien en las dos del medio: se llaman igual, DATEDIFF, y reciben los argumentos al revés. La de MySQL va (fin, inicio) y la de SQL Server va (unidad, inicio, fin). Copiar una consulta de un motor al otro te cambia el signo del resultado sin un solo error.

Ejercicios

Los siete corren sobre tienda.db. Intenta antes de abrir 💛

1. La ficha del cliente

Arma una sola columna que diga NOMBRE LIMPIO (segmento, ciudad) para los clientes 1, 12 y 44. El 12 es uno de los sucios.

SELECT UPPER(TRIM(nombre)) || ' (' || segmento || ', ' || ciudad || ')' AS ficha
FROM clientes
WHERE id IN (1, 12, 44)
ORDER BY id;
ficha
-----------------------------------------
MINIMARKET EL SOL 001 (Horeca, Piura)
MARKET CENTRAL 012 (Horeca, Arequipa)
BODEGA LA ESQUINA 044 (Minimarket, Piura)

El TRIM va por dentro del UPPER, aunque en este caso da igual el orden. Lo que no da igual es que esté.

2. Cuánta suciedad hay

Cuenta cuántos clientes tienen espacios en los bordes. Sin subconsultas todavía, que eso es el capítulo 8.

SELECT COUNT(*) AS con_espacios
FROM clientes
WHERE nombre <> TRIM(nombre);
con_espacios
------------
18
SELECT COUNT(*) AS en_mayuscula
FROM clientes
WHERE nombre = UPPER(nombre);
en_mayuscula
------------
18

Dieciocho y dieciocho, y no es casualidad: son los mismos dieciocho registros, que entraron por la misma carga mal hecha. Un 15% de la tabla de clientes 🫠

3. El código de tres dígitos, como número

Saca los tres dígitos del final del nombre y conviértelos en entero, para los clientes 1, 26 y 120. El 26 es sucio, así que ahí está la gracia.

SELECT id, CAST(SUBSTR(TRIM(nombre), -3) AS INTEGER) AS codigo
FROM clientes
WHERE id IN (1, 26, 120)
ORDER BY id;
id   codigo
---  ------
1    1
26   26
120  120

El TRIM tiene que ir dentro del SUBSTR. Si lo pones fuera, cortas primero y limpias después, o sea que te llevas '6 ' y el CAST te devuelve 6 en vez de 26. Sin error.

Y de paso: el CAST se come los ceros de la izquierda, por eso el 001 sale como 1. Si el código es un identificador y no una cantidad, déjalo como texto.

4. Todas las bodegas

Cuenta los clientes cuyo nombre empieza por "Bodega", de forma que dé el mismo número en los cuatro motores.

SELECT COUNT(*) AS bodegas
FROM clientes
WHERE UPPER(TRIM(nombre)) LIKE 'BODEGA%';
bodegas
-------
17

Diecisiete. El UPPER lo hace igual en los cuatro y el TRIM rescata a las que tenían espacios delante, que con LIKE 'BODEGA%' se habrían quedado fuera porque el % está al final y no al principio.

5. El primer trimestre de 2026

Cuántos pedidos y cuántos soles entre el 1 de enero y el 31 de marzo de 2026.

SELECT COUNT(*) AS primer_trimestre_2026, ROUND(SUM(monto), 2) AS soles
FROM pedidos
WHERE fecha BETWEEN '2026-01-01' AND '2026-03-31';
primer_trimestre_2026  soles
---------------------  --------
139                    81510.72

Funciona porque la columna es texto en formato ISO y no lleva hora. El día que esa columna tenga hora, el 31 de marzo a las 3 de la tarde se queda fuera y el BETWEEN te miente. Por eso en producción se escribe >= '2026-01-01' AND fecha < '2026-04-01', que es correcto lleve hora o no.

6. Fin de semana por WhatsApp

Cuántos pedidos de WhatsApp cayeron en sábado o domingo.

SELECT COUNT(*) AS whatsapp_fin_de_semana
FROM pedidos
WHERE canal = 'WhatsApp' AND STRFTIME('%w', fecha) IN ('0', '6');
whatsapp_fin_de_semana
----------------------
70

Setenta de los 231 pedidos de WhatsApp, o sea un 30%, que es casi exactamente lo que pesan dos días de siete. Un dato que suena a hallazgo y no lo es: antes de contárselo a nadie, compara siempre contra lo que saldría por puro azar 📊

7. Escríbelo para los cuatro

Sin ejecutar: los pedidos de los últimos 90 días contados desde hoy, en los cuatro motores.

-- PostgreSQL
SELECT COUNT(*) FROM pedidos WHERE fecha >= CURRENT_DATE - INTERVAL '90 days';

-- MySQL
SELECT COUNT(*) FROM pedidos WHERE fecha >= DATE_SUB(CURDATE(), INTERVAL 90 DAY);

-- SQL Server
SELECT COUNT(*) FROM pedidos WHERE fecha >= DATEADD(day, -90, CAST(GETDATE() AS DATE));

-- SQLite
SELECT COUNT(*) FROM pedidos WHERE fecha >= DATE('now', '-90 days');

Cuatro formas de escribir exactamente lo mismo. No hay salida publicada porque el resultado cambia cada día que la corras, y en este libro no se publica una salida que no se pueda comprobar.

Sobre nuestra base te va a dar un número distinto al mío según el día que la corras, porque "hoy" se mueve y los pedidos no. Y en algún momento va a dar cero, porque el último pedido de la tienda es del 24 de junio de 2026. Los datos de práctica también envejecen 🐣

Lo que te llevas

  • 🧼 Antes de agrupar por un texto: UPPER(TRIM(columna)). Doce marcas se convirtieron en veintitrés por saltarse esto.
  • 🔗 Pegar texto es || en PostgreSQL y SQLite, CONCAT() en MySQL y + en SQL Server. CONCAT() es la que funciona en los cuatro.
  • 🔍 LIKE distingue mayúsculas en PostgreSQL y no en los otros tres. UPPER(columna) LIKE '%TEXTO%' da el mismo número siempre.
  • 📅 SQLite no tiene tipo fecha: son textos en formato AAAA-MM-DD, y ese formato es obligatorio o las comparaciones mienten sin avisar.
  • 🚫 YEAR() no existe en SQLite ni en PostgreSQL. En SQLite todo sale de STRFTIME, y hay que envolverlo en CAST para tener un número.
  • ⚠️ DATEDIFF se llama igual en MySQL y en SQL Server y recibe los argumentos al revés.
  • 🎀 El formato bonito va en el SELECT, nunca en el WHERE ni en el ORDER BY.

En el capítulo 6 llega GROUP BY, que es donde el SQL deja de listar filas y empieza a responder preguntas. Y donde PostgreSQL te va a exigir una cosa que MySQL te deja pasar, con consecuencias.

Que tengas lindo día! 🌸

¿Tienes alguna duda o consulta?