为什么此查询会根据我如何排列 DateTime 算术而给出不同的结果?

Why does this query give different results depending on how I arrange my DateTime arithmetic?

提问人:Kevin 提问时间:4/4/2018 最后编辑:Kevin 更新时间:4/5/2018 访问量:651

问:

我使用 SqlAlchemy 创建了一个表,.每条记录都有一个字段 ,用于存储 DateTime。我想查找日期晚于八小时前的所有记录。Recorddate

我想出了四种方法来编写过滤器,它们都涉及比较当前时间、记录时间和八小时时间增量的简单算术。问题是:这些筛选器中有一半返回 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 对象。时间增量是否以一种不寻常的方式参数化,使它们不适合这种算术?

Python SQL炼金术

评论

0赞 pault 4/4/2018
会不会是操作顺序?如果在减法周围添加括号会发生什么?
0赞 Kevin 4/4/2018
@pault,有趣的想法。刚刚尝试过 - 括号不会改变结果。
0赞 pault 4/4/2018
值得一试。这很奇怪。如果选择 和 ,结果是什么?查看这些值是否按预期计算。now - Record.datenow - delta
0赞 Kevin 4/4/2018
我看看。 按预期给出,并按预期给出 When 是记录 #1,When 是记录 #2。或者你的意思是我应该尝试使用这些作为标准? 给了我零行,并给出了一个,因为你不能将 datetime 对象传递给 。print(now - delta)2018-04-04 02:00:00print(now - record.date)1 day, 0:00:00record1:00:00recordnow - Record.datenow - deltaArgumentError.filter
1赞 unutbu 4/5/2018
在方法 #0 和 #2 中,打开日志记录(或使用 )显示裸值正在转换为日期时间。1970 年的部分抛弃了其余的比较。方法 #1 和 #3 似乎有效,因为最终比较是在日期时间之间,而不是时间增量之间。echo=Truedeltaparam_11970-01-01 08:00:00.000000

答:

8赞 Ilja Everilä 4/5/2018 #1

正如 unutbu 所指出的,当对象用作不支持本机 Interval 类型的数据库的绑定参数时,它们将转换为相对于“epoch”(1.1970年1月)。SQLite就是这样一个数据库,MySQL也是如此。打开日志记录时,另一个值得注意的事情是,这些值将作为 ISO 格式的字符串进行存储和传递timedeltadatetime

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 个行为异常的比较替换为:timedeltadatetime

# 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),
]