Fechas en SQL: agrupar por mes, contar días y sumar periodos

Las fechas en SQL son la parte menos estándar del lenguaje: cada motor tiene sus funciones, y a veces dan resultados distintos para la misma pregunta. Este ejemplo resuelve cuatro tareas habituales en SQLite, PostgreSQL y MySQL, con la salida real de cada uno.

Todas las consultas usan la base de ejemplo «tienda» del curso (clientes, productos, pedidos y sus líneas). Las que funcionan en SQLite tienen un enlace para abrirlas en el editor de SQL, que la trae cargada.

Pedidos por mes

Se convierte la fecha en el texto AAAA-MM y se agrupa por él. En SQLite, con strftime:

SELECT strftime('%Y-%m', fecha) AS mes, COUNT(*) AS pedidos
FROM pedidos
GROUP BY mes
ORDER BY mes;

Ábrelo en el editor y cámbialo →

resultado · SQLitemes     | pedidos
--------+--------
2026-01 |       3
2026-02 |       2
2026-03 |       3
2026-04 |       2
2026-05 |       2
2026-06 |       2
2026-07 |       2
2026-08 |       2
2026-09 |       2
2026-10 |       2

En PostgreSQL, to_char(fecha, 'YYYY-MM'), y en MySQL, DATE_FORMAT(fecha, '%Y-%m'); los dos devuelven la misma tabla.

SELECT to_char(fecha, 'YYYY-MM') AS mes, COUNT(*) AS pedidos
SELECT DATE_FORMAT(fecha, '%Y-%m') AS mes, COUNT(*) AS pedidos

Días entre dos fechas

Cuántos días pasaron entre el alta de cada cliente y su primer pedido. SQLite no tiene un tipo fecha: la diferencia se calcula con julianday, que convierte la fecha en un número de días:

-- Días entre el alta de cada cliente y su primer pedido
SELECT c.nombre, c.alta, MIN(p.fecha) AS primer_pedido,
       CAST(julianday(MIN(p.fecha)) - julianday(c.alta) AS INTEGER) AS dias
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre, c.alta
ORDER BY dias DESC
LIMIT 4;

Ábrelo en el editor y cámbialo →

resultado · SQLitenombre         | alta       | primer_pedido | dias
---------------+------------+---------------+-----
Ana Torres     | 2025-01-14 | 2026-01-10    |  361
Luis Fernández | 2025-02-03 | 2026-01-15    |  346
Camila Rojas   | 2025-02-27 | 2026-01-28    |  335
Diego Martínez | 2025-04-09 | 2026-02-19    |  316

En PostgreSQL, restar dos fechas ya da un número de días; en MySQL se usa DATEDIFF. Las dos dan los mismos 361, 346, 335 y 316 días:

-- PostgreSQL
MIN(p.fecha) - c.alta AS dias

-- MySQL
DATEDIFF(MIN(p.fecha), c.alta) AS dias

Los últimos 90 días, y un mes después del 31 de enero

Para que el ejemplo dé siempre lo mismo se usa una fecha fija; en un programa real sería date('now', '-90 days'), CURRENT_DATE - INTERVAL '90 days' o DATE_SUB(CURDATE(), INTERVAL 90 DAY).

-- Pedidos de los 90 días anteriores al 20 de octubre de 2026
-- (en un programa de verdad, date('now', '-90 days'))
SELECT id, fecha, estado
FROM pedidos
WHERE fecha >= date('2026-10-20', '-90 days')
ORDER BY fecha;

SELECT date('2026-10-20', '-90 days') AS desde,
       date('2026-01-31', '+1 month') AS un_mes_despues;

Ábrelo en el editor y cámbialo →

resultado · SQLiteid | fecha      | estado
---+------------+----------
17 | 2026-08-05 | entregado
18 | 2026-08-21 | entregado
19 | 2026-09-09 | enviado
20 | 2026-09-23 | enviado
21 | 2026-10-02 | pendiente
22 | 2026-10-12 | pendiente

desde      | un_mes_despues
-----------+---------------
2026-07-22 | 2026-03-03

Las tres bases devuelven los mismos seis pedidos. Pero fíjate en la última columna: sumar un mes al 31 de enero da 3 de marzo en SQLite. PostgreSQL y MySQL dan otra cosa:

SELECT id, fecha, estado
FROM pedidos
WHERE fecha >= DATE '2026-10-20' - INTERVAL '90 days'
ORDER BY fecha;

SELECT (DATE '2026-10-20' - INTERVAL '90 days')::date AS desde,
       (DATE '2026-01-31' + INTERVAL '1 month')::date AS un_mes_despues;
resultado · PostgreSQL id |   fecha    |  estado   
----+------------+-----------
 17 | 2026-08-05 | entregado
 18 | 2026-08-21 | entregado
 19 | 2026-09-09 | enviado
 20 | 2026-09-23 | enviado
 21 | 2026-10-02 | pendiente
 22 | 2026-10-12 | pendiente

   desde    | un_mes_despues 
------------+----------------
 2026-07-22 | 2026-02-28
SELECT id, fecha, estado
FROM pedidos
WHERE fecha >= DATE_SUB('2026-10-20', INTERVAL 90 DAY)
ORDER BY fecha;

SELECT DATE_SUB('2026-10-20', INTERVAL 90 DAY) AS desde,
       DATE_ADD('2026-01-31', INTERVAL 1 MONTH) AS un_mes_despues;
resultado · MySQL+----+------------+-----------+
| id | fecha      | estado    |
+----+------------+-----------+
| 17 | 2026-08-05 | entregado |
| 18 | 2026-08-21 | entregado |
| 19 | 2026-09-09 | enviado   |
| 20 | 2026-09-23 | enviado   |
| 21 | 2026-10-02 | pendiente |
| 22 | 2026-10-12 | pendiente |
+----+------------+-----------+
+------------+----------------+
| desde      | un_mes_despues |
+------------+----------------+
| 2026-07-22 | 2026-02-28     |
+------------+----------------+

SQLite suma uno al mes (31 de febrero) y luego normaliza: febrero de 2026 tiene 28 días, así que se pasa 3 días a marzo. PostgreSQL y MySQL se quedan en el último día de febrero. Ninguno está mal; son reglas distintas, y conviene saberlo si calculas vencimientos.

El día de la semana

El 10 de enero de 2026 fue sábado. Cada motor lo numera a su manera:

SELECT fecha, strftime('%w', fecha) AS dia_semana
FROM pedidos WHERE id IN (1, 2, 3);

Ábrelo en el editor y cámbialo →

resultado · SQLitefecha      | dia_semana
-----------+-----------
2026-01-10 | 6
2026-01-15 | 4
2026-01-28 | 3
resultado · PostgreSQL   fecha    | dow | isodow 
------------+-----+--------
 2026-01-10 |   6 |      6
 2026-01-15 |   4 |      4
 2026-01-28 |   3 |      3
resultado · MySQL+------------+-----------+---------+
| fecha      | dayofweek | weekday |
+------------+-----------+---------+
| 2026-01-10 |         7 |       5 |
| 2026-01-15 |         5 |       3 |
| 2026-01-28 |         4 |       2 |
+------------+-----------+---------+
FunciónDomingoLunesSábado
SQLite strftime('%w', …) (texto)016
PostgreSQL EXTRACT(DOW …)016
PostgreSQL EXTRACT(ISODOW …)716
MySQL DAYOFWEEK(…)127
MySQL WEEKDAY(…)605

En SQLite, una fecha es texto

SQLite guarda las fechas como texto y las compara como texto. Funciona porque el formato AAAA-MM-DD ordena igual alfabética que cronológicamente, pero solo si todas siguen ese formato:

-- En SQLite una fecha es texto: si alguien la guarda con otro formato, compara mal
INSERT INTO pedidos (id, cliente_id, fecha, estado) VALUES (23, 3, '2026-9-30', 'pendiente');
INSERT INTO pedidos (id, cliente_id, fecha, estado) VALUES (24, 3, '2026-02-30', 'pendiente');

SELECT id, fecha FROM pedidos
WHERE fecha >= '2026-09-01' AND fecha < '2026-10-01'
ORDER BY fecha;

SELECT id, fecha, typeof(fecha) AS tipo, date(fecha) AS como_fecha
FROM pedidos WHERE id IN (22, 23, 24);

Ábrelo en el editor y cámbialo →

resultado · SQLiteid | fecha
---+-----------
19 | 2026-09-09
20 | 2026-09-23

id | fecha      | tipo | como_fecha
---+------------+------+-----------
22 | 2026-10-12 | text | 2026-10-12
23 | 2026-9-30  | text | NULL
24 | 2026-02-30 | text | 2026-03-02

El pedido 23 ('2026-9-30', sin el cero) no sale en septiembre, y date() ni siquiera lo reconoce. El 30 de febrero se guarda sin quejas. En PostgreSQL y MySQL la columna es de tipo DATE: el primero se normaliza a 2026-09-30 y el segundo se rechaza:

resultado · PostgreSQLERROR:  date/time field value out of range: "2026-02-30"
LINE 1: ...os (id, cliente_id, fecha, estado) VALUES (24, 3, '2026-02-3...
                                                             ^
 id |   fecha    
----+------------
 23 | 2026-09-30
resultado · MySQLERROR 1292 (22007) at line 2: Incorrect date value: '2026-02-30' for column 'fecha' at row 1
+----+------------+
| id | fecha      |
+----+------------+
| 23 | 2026-09-30 |
+----+------------+
En SQLite, guarda siempre las fechas en ISO 8601 (2026-09-30 o 2026-09-30 14:05:00) y, si quieres asegurarte, añade una restricción como CHECK (fecha = date(fecha)), que rechaza todo lo que no sea una fecha válida en ese formato.

Todas las consultas de esta página se ejecutaron antes de publicarla sobre la base de ejemplo «tienda» del curso, en contenedores limpios: SQLite 3.46.1 (el de Python 3.13), PostgreSQL 17 y MySQL 8.4. Los resultados y los mensajes de error están copiados de esa ejecución. Documentación oficial: funciones de fecha de SQLite.