提问人:Kevin 提问时间:4/4/2018 最后编辑:Kevin 更新时间:4/5/2018 访问量:651
为什么此查询会根据我如何排列 DateTime 算术而给出不同的结果?
Why does this query give different results depending on how I arrange my DateTime arithmetic?
问:
我使用 SqlAlchemy 创建了一个表,.每条记录都有一个字段 ,用于存储 DateTime。我想查找日期晚于八小时前的所有记录。Record
date
我想出了四种方法来编写过滤器,它们都涉及比较当前时间、记录时间和八小时时间增量的简单算术。问题是:这些筛选器中有一半返回 8 小时窗口之外的行。
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
import datetime
Base = declarative_base()
class Record(Base):
__tablename__ = 'record'
id = Column(Integer, primary_key=True)
date = Column(DateTime, nullable=False)
engine = create_engine('sqlite:///records.db')
Base.metadata.create_all(engine)
DBSession = sessionmaker(bind=engine)
session = DBSession()
#if the db is empty, add some records to the database with datetimes corresponding to one year ago and one hour ago and yesterday
now = datetime.datetime(2018, 4, 4, 10, 0, 0)
if not session.query(Record).all():
session.add(Record(date = now - datetime.timedelta(days=365)))
session.add(Record(date = now - datetime.timedelta(days=1)))
session.add(Record(date = now - datetime.timedelta(hours=1)))
delta = datetime.timedelta(hours=8)
#these are all equivalent to "records from the last eight hours"
criterion = [
(now - Record.date < delta),
(Record.date > now - delta),
(delta > now - Record.date),
(now - delta < Record.date),
]
for idx, crit in enumerate(criterion):
query = session.query(Record).filter(crit)
print("\n\nApproach #{}.".format(idx))
print("Generated statement:")
print(query.statement)
records = query.all()
print("{} row(s) retrieved.".format(len(records)))
for record in query.all():
print(record.id, record.date)
结果:
Approach #0.
Generated statement:
SELECT record.id, record.date
FROM record
WHERE :date_1 - record.date < :param_1
3 row(s) retrieved.
1 2017-04-04 10:00:00
2 2018-04-03 10:00:00
3 2018-04-04 09:00:00
Approach #1.
Generated statement:
SELECT record.id, record.date
FROM record
WHERE record.date > :date_1
1 row(s) retrieved.
3 2018-04-04 09:00:00
Approach #2.
Generated statement:
SELECT record.id, record.date
FROM record
WHERE :date_1 - record.date < :param_1
3 row(s) retrieved.
1 2017-04-04 10:00:00
2 2018-04-03 10:00:00
3 2018-04-04 09:00:00
Approach #3.
Generated statement:
SELECT record.id, record.date
FROM record
WHERE record.date > :date_1
1 row(s) retrieved.
3 2018-04-04 09:00:00
方法 1 和 3 是正确的 - 它们返回一小时前的记录,而不是一天前或一年前的记录。方法 0 和 2 不正确,因为它们除了返回一小时前的记录外,还返回一天前的记录和一年前的记录。
是什么导致了这种差异?我注意到 #1 和 #3 生成的语句仅参数化单个 datetime 对象,而 #0 和 #2 同时参数化 datetime 对象和 timedelta 对象。时间增量是否以一种不寻常的方式参数化,使它们不适合这种算术?
答:
正如 unutbu 所指出的,当对象用作不支持本机 Interval
类型的数据库的绑定参数时,它们将转换为相对于“epoch”(1.1970年1月)。SQLite就是这样一个数据库,MySQL也是如此。打开日志记录时,另一个值得注意的事情是,这些值将作为 ISO 格式的字符串进行存储和传递。timedelta
datetime
DATETIME 列在 SQLite 中具有 NUMERIC 关联性,但由于 ISO 格式的字符串无法无损转换为数值,因此它们会保留其 TEXT 存储类。另一方面,这很好,因为在 SQLite 中存储日期和时间数据的 3 种方法是
- ISO8601字符串形式的文本 (“YYYY-MM-DD HH:MM:SS.SSS“)。
- REAL as 儒略日数字,根据公历,自公元前 4714 年 11 月 24 日格林威治中午以来的天数。
- INTEGER 作为 Unix 时间,自 1970-01-01 00:00:00 UTC 以来的秒数。
但是,当您尝试在数据库中执行算术运算时,事情会变得更加有趣:
In [18]: session.execute('SELECT :date_1 - record.date FROM record',
...: {"date_1": now}).fetchall()
2018-04-04 20:47:35,045 INFO sqlalchemy.engine.base.Engine SELECT ? - record.date FROM record
INFO:sqlalchemy.engine.base.Engine:SELECT ? - record.date FROM record
2018-04-04 20:47:35,045 INFO sqlalchemy.engine.base.Engine (datetime.datetime(2018, 4, 4, 10, 0),)
INFO:sqlalchemy.engine.base.Engine:(datetime.datetime(2018, 4, 4, 10, 0),)
Out[18]: [(1,), (0,), (0,)]
原因是所有数学运算符都将其操作数转换为 NUMERIC 存储类,即使结果值是有损的——或者没有意义。在本例中,将解析年份部分,忽略其余部分。
由于任何 INTEGER 或 REAL 值都小于任何 TEXT 或 BLOB 值,因此生成的整数值与给定的 ISO 格式间隔字符串之间的所有比较都为 true:
In [25]: session.execute(text('SELECT :date_1 - record.date < :param_1 FROM record')
...: .bindparams(bindparam('param_1', type_=Interval)),
...: {"date_1": now, "param_1": delta}).fetchall()
...:
2018-04-04 20:55:36,952 INFO sqlalchemy.engine.base.Engine SELECT ? - record.date < ? FROM record
INFO:sqlalchemy.engine.base.Engine:SELECT ? - record.date < ? FROM record
2018-04-04 20:55:36,952 INFO sqlalchemy.engine.base.Engine (datetime.datetime(2018, 4, 4, 10, 0), '1970-01-01 08:00:00.000000')
INFO:sqlalchemy.engine.base.Engine:(datetime.datetime(2018, 4, 4, 10, 0), '1970-01-01 08:00:00.000000')
Out[25]: [(1,), (1,), (1,)]
有些人可能会说所有这些都是漏洞百出的抽象,但是在SQLAlchemy中为数据库实现之间的所有差异提供解决方案将是一项艰巨的(或不可能的)任务。就我个人而言,我发现它更方便,因为它不会妨碍,但允许按原样使用数据库的功能,但使用漂亮的 Python DSL。如果您确实需要在单个代码库中支持不同数据库中的时间差异,请使用合适的特定于数据库的编译器创建自定义构造。
要实际计算 SQLite 中的差异并与给定的总秒数进行比较,您需要使用 strftime()
函数将 ISO 格式的字符串转换为自纪元以来的秒数。julianday()
也可以工作,只要您也转换 Python 并将结果转换为秒。例如,将 2 个行为异常的比较替换为:timedelta
datetime
# Not sure if your times were supposed to be UTC or not
now_ts = now.replace(tzinfo=datetime.timezone.utc).timestamp()
delta_s = delta.total_seconds()
# Not quite pretty...
criterion = [
(now_ts - func.strftime('%s', Record.date) < delta_s,
(Record.date > now - delta),
(delta_s > now_ts - func.strftime('%s', Record.date)),
(now - delta < Record.date),
]
评论
now - Record.date
now - delta
print(now - delta)
2018-04-04 02:00:00
print(now - record.date)
1 day, 0:00:00
record
1:00:00
record
now - Record.date
now - delta
ArgumentError
.filter
echo=True
delta
param_1
1970-01-01 08:00:00.000000