Fecha y hora en SQL

De todos los tipos de datos de SQL, los temporales son los más complicados 🤯. La complejidad tiene varias causas; estas son algunas de ellas:

  • las múltiples formas de indicar una fecha y una hora
  • la existencia de zonas horarias
  • lo poco evidente que resulta calcular algunos valores a partir de datos temporales. Por ejemplo, lo difícil que es calcular la edad.

Generación de datos temporales

Los datos temporales se pueden obtener de una de estas formas:

  • copiar los datos de una columna existente con un tipo de dato temporal
  • indicar la fecha y la hora mediante una representación en forma de cadena
  • obtener datos temporales llamando a funciones integradas que devuelven un tipo de dato temporal

Representación de datos temporales como cadena

Para indicar la fecha y la hora se usan estos formatos:

TipoFormato por defecto
DATEYYYY-MM-DD
DATETIMEYYYY-MM-DD hh:mm:ss
TIMESTAMPYYYY-MM-DD hh:mm:ss
TIMEhhh:mm:sss
YEARYYYY - formato completo
YY o Y - formato abreviado, que devuelve un año entre 2000 y 2069 para los valores 0-69 y un año entre 1970 y 1999 para los valores 70-99

Además, al indicar una fecha se puede usar cualquier signo de puntuación como separador entre las partes de la fecha o de la hora. También se puede escribir la fecha sin ningún separador, todo junto.

TipoFormato por defecto
DATEYYYY-MM-DD
TIMESTAMPYYYY-MM-DD hh:mm:ss
TIMEhh:mm:ss

Las partes de la fecha se escriben en el orden año-mes-día y se separan con un guion, una barra o un punto; las partes de la hora, solo con dos puntos. La fecha y la hora también se pueden escribir todo junto, pero entonces la fecha se separa de la hora con un espacio o con la letra T: por ejemplo, 20220616T163723.

Ejemplos válidos de valores temporales indicados como cadena:

MySQL 8.4
SELECT  CAST("2022-06-16 16:37:23" AS DATETIME) AS datetime_1,
        CAST("2014/02/22 16*37*22" AS DATETIME) AS datetime_2,
        CAST("20220616163723" AS DATETIME) AS datetime_3,
        CAST("2021-02-12" AS DATE) AS date_1,
        CAST("160:23:13" AS TIME) AS time_1,
        CAST("89" AS YEAR) AS year
datetime_1datetime_2datetime_3date_1time_1year
2022-06-16T16:37:23.000Z2014-02-22T16:37:22.000Z2022-06-16T16:37:23.000Z2021-02-12T00:00:00.000Z160:23:131989
MySQL 8.4
SELECT  CAST('2022-06-16 16:37:23' AS TIMESTAMP) AS timestamp_1,
        CAST('2014/02/22 16:37:22' AS TIMESTAMP) AS timestamp_2,
        CAST('20220616T163723' AS TIMESTAMP) AS timestamp_3,
        CAST('2021-02-12' AS DATE) AS date_1,
        CAST('16:23:13' AS TIME) AS time_1
timestamp_1timestamp_2timestamp_3date_1time_1
2022-06-16T16:37:23.000Z2014-02-22T16:37:22.000Z2022-06-16T16:37:23.000Z2021-02-12T00:00:00.000Z16:23:13

En la consulta anterior se ha usado la función CAST para convertir de forma explícita la cadena en fecha y hora. Es necesaria cuando el servidor no espera un valor temporal y, por tanto, no convierte automáticamente la cadena al tipo necesario. Veremos la conversión de tipos con más detalle en el artículo «Funciones de conversión de tipos, CAST».

Funciones para generar fechas

Si necesitas obtener datos temporales de una cadena que no encaja en ninguno de los formatos que acepta la función CAST, puedes usar funciones especiales para analizar fechas.

MySQL tiene la función integrada STR_TO_DATE, que recibe una cadena cualquiera que contiene una fecha y el formato que la describe.

MySQL 8.4
SELECT STR_TO_DATE('November 13, 1998', '%M %d, %Y') AS date;
date
1998-11-13T00:00:00.000Z

Tienes una descripción más detallada de la función STR_TO_DATE y de sus argumentos en la Referencia SQL.

PostgreSQL tiene la función integrada TO_DATE, que recibe una cadena cualquiera que contiene una fecha y el formato que la describe.

MySQL 8.4
SELECT TO_DATE('November 13, 1998', 'Month DD, YYYY') AS date;
date
1998-11-13T00:00:00.000Z

Tienes una descripción más detallada de la función TO_DATE y de sus argumentos en la Referencia SQL.

En cambio, para generar la fecha o la hora actual no hace falta crear una cadena y convertirla después en fecha, porque hay funciones integradas que devuelven estos valores.

En MySQL son las funciones CURDATE, CURTIME y NOW.

MySQL 8.4
SELECT CURDATE(), CURTIME(), NOW();

En PostgreSQL son las funciones CURRENT_DATE, CURRENT_TIME y NOW.

MySQL 8.4
SELECT CURRENT_DATE, CURRENT_TIME, NOW();

Funciones para extraer datos temporales

A veces no necesitamos la fecha entera, sino solo una parte concreta, por ejemplo, el mes o el año.

Para ello, SQL tiene estas funciones:

FunciónDescripción
YEARDevuelve el año de la fecha indicada
MONTHDevuelve el número del mes del año (de 1 a 12) de la fecha
DAYDevuelve el número del día del mes (de 1 a 31)
HOURDevuelve la hora (de 0 a 23) de un valor de tiempo
MINUTEDevuelve los minutos (de 0 a 59) de un valor de tiempo

Para ello, en PostgreSQL se usa la función EXTRACT:

FunciónDescripción
EXTRACT(YEAR FROM date)Devuelve el año de la fecha indicada
EXTRACT(MONTH FROM date)Devuelve el número del mes del año (de 1 a 12) de la fecha
EXTRACT(DAY FROM date)Devuelve el número del día del mes (de 1 a 31)
EXTRACT(HOUR FROM time)Devuelve la hora (de 0 a 23) de un valor de tiempo
EXTRACT(MINUTE FROM time)Devuelve los minutos (de 0 a 59) de un valor de tiempo

Diferencia entre DATETIME y TIMESTAMP

Hay dos tipos de datos muy parecidos entre sí: DATETIME y TIMESTAMP. Los dos sirven para almacenar fecha y hora, pero tienen varias diferencias que determinan para qué se usa cada uno.

CriterioDATETIMETIMESTAMP
Rangode 1000-01-01 00:00:00
a 9999-12-31 23:59:59
de 1970-01-01 00:00:00
a 2038-01-19 03:14:07
Zona horariaNo se tiene en cuenta
Se muestra tal como se estableció la fecha
Se tiene en cuenta
En las consultas se muestra según la zona horaria de la sesión actual

Diferencia entre TIMESTAMP y TIMESTAMPTZ

Los tipos principales para almacenar fecha y hora son TIMESTAMP (sin zona horaria) y TIMESTAMPTZ (con zona horaria).

CriterioTIMESTAMPTIMESTAMPTZ
Rangode 4713 BC a 294276 ADde 4713 BC a 294276 AD
Zona horariaNo se tiene en cuenta
Se muestra tal como se estableció la fecha
Se tiene en cuenta
En las consultas se muestra según la zona horaria de la sesión actual

Zonas horarias

Como en todo el mundo la gente quiere que el mediodía coincida más o menos con el momento en que el Sol está más alto, nunca se planteó usar una hora universal, y el mundo se dividió en 24 zonas horarias.

Como punto de referencia de la hora se usa UTC (Coordinated Universal Time, tiempo universal coordinado). Todas las demás zonas horarias se pueden describir por el número de horas de desfase respecto a UTC. Por ejemplo, la zona horaria de Moscú se puede describir como UTC+3.

La zona horaria es uno de los ajustes del servidor de bases de datos y se puede establecer:

  • de forma global
  • para la sesión actual
MySQL 8.4
SET GLOBAL time_zone = '+03:00'; -- de forma global
SET time_zone = '+03:00';        -- para la sesión actual

Por tanto, al cambiar la zona horaria, todos los valores de tipo TIMESTAMP se mostrarán según la zona horaria activa en ese momento.

  • para la base de datos
  • para un usuario concreto
  • para la sesión actual
MySQL 8.4
ALTER DATABASE mydb SET timezone = 'Europe/Moscow'; -- para la base de datos
ALTER USER myuser SET timezone = 'Europe/Moscow';   -- para un usuario concreto
SET TIME ZONE 'Europe/Moscow';                      -- para la sesión actual, por nombre de zona
SET TIME ZONE INTERVAL '+03:00' HOUR TO MINUTE;     -- para la sesión actual, por desfase respecto a UTC

Por tanto, al cambiar la zona horaria, todos los valores de tipo TIMESTAMPTZ se mostrarán según la zona horaria activa en ese momento.

Ejemplos de tareas con fechas y horas

Queremos detenernos en las tareas más habituales con datos temporales, en las que se cometen errores a menudo.

Calcular la edad

Cuando la tarea es calcular la edad de una persona a partir de su fecha de nacimiento, es muy tentador 😈 calcular la diferencia entre el año actual y el año de nacimiento:

MySQL 8.4
SELECT YEAR(NOW()) - YEAR('2003-07-03 14:10:26');
MySQL 8.4
SELECT EXTRACT(YEAR FROM NOW()) - EXTRACT(YEAR FROM TIMESTAMP '2003-07-03 14:10:26');

El problema de este enfoque es que no tiene en cuenta si la persona ya ha cumplido años este año o todavía no. Es decir, si en el momento de la consulta ya ha llegado el 3 de julio (07-03), la persona ha celebrado su cumpleaños y ya tiene 20 años; si no, todavía tiene 19. Esta resta no sirve de nada: en los dos casos da 20 años.

Si calcular la edad con la diferencia de años no funciona, quizá te apetezca calcularla con la diferencia de días entre las dos fechas, dividir esa diferencia entre el número de días del año y redondear hacia abajo:

MySQL 8.4
SELECT FLOOR(DATEDIFF(NOW(), '2003-07-03 14:10:26') / 365);
MySQL 8.4
SELECT FLOOR(EXTRACT(DAY FROM NOW() - TIMESTAMP '2003-07-03 14:10:26') / 365);

Esta solución es mucho más precisa que la anterior. Pero no es del todo exacta, porque existen los años bisiestos, que tienen 366 días. Aunque el error al calcular la edad de una sola persona por culpa de los años bisiestos es bastante pequeño, en cálculos como, por ejemplo, la edad media de una lista de personas, el error puede acumularse y distorsionar los valores reales.

Entonces, ¿cómo se calcula la edad correctamente?

Para eso existe una función integrada lista para usar, TIMESTAMPDIFF, que recibe como primer argumento la unidad de medida en la que debe devolver la diferencia entre dos valores temporales.

MySQL 8.4
SELECT TIMESTAMPDIFF(YEAR, '2003-07-03 14:10:26', NOW());

Para ello se usa la función EXTRACT junto con la función AGE, que calcula el intervalo exacto entre dos fechas.

MySQL 8.4
SELECT EXTRACT(YEAR FROM AGE(NOW(), TIMESTAMP '2003-07-03 14:10:26'));