Даты и время в RethinkDB
RethinkDB имеет встроенную поддержку времени с точностью до миллисекунды и часовыми поясами. Некоторые преимущества:
- Время интегрировано с официальными драйверами, которые автоматически преобразуют его в соответствующий тип времени вашей среды.
- Запросы учитывают часовой пояс, поэтому вы можете задавать вопросы, например: «Произошло ли это событие в понедельник в часовом поясе, где оно было записано?»
- Время используется в качестве индексов, поэтому вы можете эффективно извлекать события по времени их происхождения.
- Операции со временем выполняются как чистые ReQL-запросы, что означает, что даже сложные запросы с датой и временем могут быть эффективно распределены по кластеру.

Быстрый пример
Сначала создадим таблицу и вставим некоторые события. Первое событие вставим, используя объект времени по умолчанию, а второе — с помощью конструктора epoch_time:
r.table_create('events').run(conn)
from datetime import datetime
r.table('events').insert([
{'id': 0, 'timestamp': r.expr(datetime.now(r.make_timezone('-07:00')))},
{'id': 1, 'timestamp': r.epoch_time(1376436769.923)}
]).run(conn)
Теперь извлечем их:
> list(r.table('events').run(conn))
[{u'id': 0,
u'timestamp': datetime.datetime(2015, 2, 2, 11, 56, 31, 250000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102ca0c50>)},
{u'id': 1,
u'timestamp': datetime.datetime(2013, 8, 13, 23, 32, 49, 923000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102ca0fd0>)}]
Обратите внимание, что оба вставленных времени возвращаются как объекты Python datetime. Они находятся в разных часовых поясах, потому что datetime.now требует указания часового пояса, а r.epoch_time создаёт время UTC (не зная и не заботясь о часовом поясе клиента). В этом примере мы используем r.make_timezone, ReQL-команду для Python, но можно использовать pytz или другую библиотеку Python для часовых поясов.
Теперь мы можем фильтровать по этому времени:
> r.table('events').filter(r.row['timestamp'].hours() > 20).run(conn)
[{u'id': 1,
u'timestamp': datetime.datetime(2013, 8, 13, 23, 32, 49, 923000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102cbde90>)}]
> r.table('events').filter(r.row['timestamp'].in_timezone('-02:00').hours() > 20).run(conn)
[{u'id': 0,
u'timestamp': datetime.datetime(2015, 2, 2, 11, 56, 31, 250000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102ca0c50>)},
{u'id': 1,
u'timestamp': datetime.datetime(2013, 8, 13, 23, 32, 49, 923000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102ca0fd0>)}]
Или создать вторичный индекс по нему:
> r.table('events').index_create('timestamp').run(conn)
{'created': 1}
> r.table('events').between(r.epoch_time(1376436769.913),
r.epoch_time(1376436769.933),
index='timestamp').run(conn)
[{u'id': 1,
u'timestamp': datetime.datetime(2013, 8, 13, 23, 32, 49, 923000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102cbde90>)}]
Технические детали
Время хранится на сервере как количество секунд с начала эпохи (UTC) с точностью до миллисекунд плюс часовой пояс. В настоящее время доступны только временные смещения от UTC с точностью до минуты, но в будущем может быть добавлена поддержка часовых поясов, учитывающих летнее время. Часовые пояса представлены строками в формате ISO 8601.
Два времени считаются равными, если равны их значения в эпоху (UTC), независимо от часового пояса. Это верно как для сравнений, так и для операций с индексами. Временные отметки сравниваются с плавающей точкой с точностью до миллисекунды.
Большинство операций с датами определены только для годов в диапазоне [1400, 10000] (но обратите внимание, что время в году 10000 нельзя вывести в формате даты ISO 8601).
Прыжки секунд в настоящее время не полностью поддерживаются: 2012-06-30T23:59:60 и 2012-07-01T00:00:00 интерпретируются как одно и то же время.
Вставка времени
Время можно вставлять, просто передав объект Python datetime.datetime. Обратите внимание, что драйвер Python требует, чтобы объекты datetime содержали информацию о часовом поясе. (Большинство драйверов по умолчанию используют UTC, если информация о часовом поясе отсутствует; для сторонних драйверов см. документацию).
r.table('events').insert(
{'id': 2, 'timestamp': datetime.now(r.make_timezone('00:00'))}
).run()
Также можно использовать r.now (которое сервер интерпретирует как время получения запроса в UTC) или создать время с помощью r.time, r.epoch_time, или r.iso8601.
> r.now().to_iso8601().run(conn)
"2013-08-09T18:53:15.012+00:00"
> r.time(2013, r.august, 9, 18, 53, 15.012, '-07:00').to_iso8601().run(conn)
"2013-08-09T18:53:15.012-07:00"
> r.epoch_time(1376074395.012).to_iso8601().run(conn)
"2013-08-09T18:53:15.012+00:00"
> r.iso8601("2013-08-09T18:53:15.012-07:00").to_iso8601().run(conn)
"2013-08-09T18:53:15.012-07:00"
Время может быть первичным ключом таблицы. Два времени считаются равными, если у них одинаковое количество миллисекунд с начала эпохи (UTC), независимо от часового пояса.
> r.table('t').insert(
{'id': r.iso8601("2013-08-09T11:58:00.1111-07:00")}
).run(conn)
{'deleted': 0, 'errors': 0, 'inserted': 1, 'replaced:' 0, 'skipped': 0, 'unchanged': 0}
> r.table('t').insert(
{'id': r.iso8601("2013-08-09T10:58:00.1112-08:00")}
).run(conn)
{'deleted': 0, 'errors': 1, 'inserted': 0, 'replaced:' 0, 'skipped': 0, 'unchanged': 0, 'first_error': 'Duplicate primary key `id`: ...'}
Также можно вставить время, вставив объект-псевдотип. Это полезно, например, если вы экспортировали строку с помощью time_format='raw' (см. Извлечение времени ниже).
Примечание: Избегайте использования ключей, соответствующих регулярному выражению
^\$reql_.+\$$в ваших объектах. RethinkDB считает их зарезервированными ключевыми словами.
> r.expr({'$reql_type$': 'TIME',
'epoch_time': 1376075362.662,
'timezone': '+00:00'}).to_iso8601().run(conn)
"2013-08-09T19:09:22.662+00:00"
Извлечение времени
По умолчанию время преобразуется в родные объекты времени при извлечении из сервера. Это можно изменить, передав параметр time_format в run. Доступны native, значение по умолчанию, и raw.
> r.now().run(conn)
datetime.datetime(2015, 2, 2, 19, 34, 10, 776000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102ca12d0>)
> r.now().in_timezone('-07:00').run(conn)
datetime.datetime(2015, 2, 2, 12, 34, 55, 642000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x102ca1250>)
> r.now().run(conn, time_format='raw')
{u'$reql_type$': u'TIME',
u'epoch_time': 1422905756.693,
u'timezone': u'-07:00'}
Также можно преобразовать объект времени на сервере с помощью to_epoch_time или to_iso8601.
> r.now().to_epoch_time().run(conn)
1376075986.574
> r.now().to_iso8601().run(conn)
"2013-08-09T19:19:46.574+00:00"
Работа со временем
Есть только три полезных вещи, которые можно сделать с временем: изменить его, сравнить его с другим временем или извлечь часть из него.
Изменение времени
Можно переместить время в другой часовой пояс:
> r.now().to_iso8601().run(conn)
"2013-08-09T12:48:59.103-07:00"
> r.now().in_timezone('-06:00').to_iso8601().run(conn)
"2013-08-09T13:49:15.503-06:00"
Также можно добавить или вычесть продолжительность (в секундах):
> (r.time(2015, 1, 1, 'Z') + 86400).run(conn)
datetime.datetime(2015, 1, 2, 0, 0, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x10e681fd0>)
Если вы вычитаете два времени, вы получите продолжительность:
> (r.time(2015, 1, 2, 'Z') - r.time(2015, 1, 1, 'Z')).run(conn)
86400
Сравнение времен
Определены все обычные операторы сравнения:
> (r.epoch_time(1376081287.982) < r.now()).run(conn)
True
Временные отметки сравниваются только с точностью до миллисекунды:
> r.epoch_time(1376081287.9821).eq(r.epoch_time(1376081287.9822)).run(conn)
True
Также есть команда during, которая позволяет проверить, находится ли время в определенном диапазоне времени.
Извлечение частей времени
Если у вас есть время, вы можете извлечь определенную часть (например, месяц или часы) относительно текущего часового пояса. (Полный список см. в справочнике по API).
> r.now().run(conn)
2013-08-09 13:53:00 -0700
> r.now().month().run(conn)
8
> r.now().hours().run(conn)
13
> r.now().in_timezone('-06:00').hours().run(conn)
14
Мы используем определение недели ISO 8601, которое начинается с понедельника, представленного как 1.
> r.now().day_of_week().run(conn)
5 # Friday
Мы определяем r.monday...r.sunday и r.january...r.december для удобства:
> r.now().day_of_week().eq(r.friday).run(conn)
true
Мы также позволяем разбить время на дату и текущее время суток (время и продолжительность соответственно):
> r.now().to_epoch_time().run(conn)
1376351312.744
> r.now().date().to_epoch_time().run(conn)
1376265600
> r.now().time_of_day().run(conn)
85712.744
Объединение всего
Комбинируя эти операции, можно писать довольно полезные запросы в чистом ReQL. Например, предположим, у вас есть таблица продаж вашей компании, и вы хотите узнать, сколько валовой прибыли приходится на сотрудников, работавших сверхурочно:
r.table('sales').filter(lambda sale:
# Weekends are overtime.
sale['time'].day_of_week().eq(r.saturday) |
sale['time'].day_of_week().eq(r.sunday) |
# Weekdays outside 9-5 are overtime.
(sale['time'].hours() < 9) |
(sale['time'].hours() >= 17)
).sum('dollars').run(conn)
Если временные метки хранятся с часовыми поясами, этот запрос будет работать, даже если у вас есть продажи из разных офисов в разных странах (если все они работают с 9 до 17 по местному времени).
Поскольку этот запрос является чистым ReQL, весь запрос будет эффективно распределён по кластеру без каких-либо вычислений на стороне клиента.
Кроме того, поскольку это ReQL, отдельные части запроса легко комбинируются. Если вы решите получить эти данные по месяцам, вы можете просто добавить group:
r.table('sales').filter(lambda sale:
# Weekends are overtime.
sale['time'].day_of_week().eq(r.saturday) |
sale['time'].day_of_week().eq(r.sunday) |
# Weekdays outside 9-5 are overtime.
(sale['time'].hours() < 9) |
(sale['time'].hours() >= 17)
).group(lambda sale: sale['time'].month()).sum('dollars').run(conn)
Объекты родного времени
RethinkDB принимает объекты Python datetime:
from datetime import datetime
Драйвер Python выдаст ошибку, если вы передадите ему datetime без часового пояса.
> r.expr(datetime.now()).run(conn)
ReqlDriverError: Cannot convert datetime to ReQL time object
without timezone information. You can add timezone information with
the third party module "pytz" or by constructing ReQL compatible
timezone values with r.make_timezone("[+-]HH:MM"). Alternatively,
use one of ReQL's builtin time constructors, r.now, r.time, or r.iso8601.
Чтобы передать допустимый объект времени драйверу Python, можно сделать одно из двух.
Используйте r.make_timezone:
r.expr(datetime.now(r.make_timezone('-07:00'))).run(conn)
Или используйте модуль pytz:
from pytz import timezone
r.expr(datetime.now(timezone('US/Pacific'))).run(conn)
Примечание: Объекты RqlTzinfo нельзя напрямую сериализовать в JSON.
import json
today = r.expr(datetime.datetime.now(timezone('US/Pacific'))).run(conn)
json.dumps(today)
TypeError: datetime.datetime(2015, 6, 3, 14, 46, 11, 142000, tzinfo=<rethinkdb.ast.RqlTzinfo object at 0x103c5a890>) is not JSON serializable
Разрешить это, передав time_format="raw" необязательный аргумент в run:
import json
today = r.expr(datetime.datetime.now(timezone('US/Pacific'))).run(conn,
time_format="raw")
json.dumps(today)
'{"timezone": "-07:00", "$reql_type$": "TIME", "epoch_time": 1433368112.289}'
© RethinkDB contributors
Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
https://rethinkdb.com/docs/dates-and-times/python/