【问题标题】:Django raw sql/postgres time zone confusionDjango 原始 sql/postgres 时区混淆
【发布时间】:2016-02-16 15:28:09
【问题描述】:

我在最后一天试图从我的数据库中获取时间序列的聚合。我尝试使用 Django ORM,但很快放弃并返回 SQL。我认为没有办法将 PSQL generate_series 与它一起使用,我认为他们更喜欢您使用 itertools 或 python 中的其他方法。

我有一个很像这样的模型:

class Vote(models.Model):
    value = models.IntegerField(default=0)
    timestamp = models.DateTimeField('date voted', auto_now_add=True)
    location = models.ForeignKey('location', on_delete=models.CASCADE)

我想要做的是随着时间的推移显示一系列指标 - 目前,当前用户在当天每小时的聚合。用户设置了时区(默认为“美国/芝加哥”)。我一直在使用 postgres 查询,插入大量的 AS TIME ZONE 强制转换,以解决查询的边界和返回值。我昨晚深夜让它返回了正确的结果,但今天早上,它又关闭了。我知道我正在做一些非常愚蠢的事情。由于 Postgres 处理 AT TIME ZONE 的奇怪方式(更正为 UTC 而不是 FROM),我什至使用了双重转换时间戳

再次,我想显示用户当天每个小时的聚合桶,直到/包括“现在”。

这是我当前的查询:

WITH hour_intervals AS (
    SELECT * FROM generate_series(date_trunc('day',(SELECT TIMESTAMP 'today' AT TIME ZONE 'UTC' AT TIME ZONE %s)), (LOCALTIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE %s), '1 hour') start_time
)

SELECT f.start_time,
COUNT(id) total,
COUNT(CASE WHEN value > 0 THEN 1 END) AS positive_votes,
COUNT(CASE WHEN value = 0 THEN 1 END) AS indifferent_votes,
COUNT(CASE WHEN value < 0 THEN 1 END) AS negative_votes,
SUM(CASE WHEN value > 0 THEN 2 WHEN value = 0 THEN 1 WHEN value < 0 THEN -4 END) AS score

FROM votes_vote m
RIGHT JOIN hour_intervals f 
        ON m.timestamp AT TIME ZONE %s >= f.start_time AND m.timestamp AT TIME ZONE %s < f.start_time + '1 hour'::interval
        AND m.location_id = %s
GROUP BY f.start_time
ORDER BY f.start_time

调试信息
Django 1.9.2,我的 settings.py 有 USE_TZ=True
Postgres 9.5.2,我的 django 登录角色有

ALTER ROLE yesno_django
  SET client_encoding = 'utf8';
ALTER ROLE yesno_django
  SET default_transaction_isolation = 'read committed';
ALTER ROLE yesno_django
  SET TimeZone = 'UTC';

更新 再摆弄一下查询,现在这是今天投票的有效查询...

WITH hour_intervals AS (
    SELECT * FROM generate_series((SELECT TIMESTAMP 'today' AT TIME ZONE 'UTC'), (LOCALTIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE %s), '1 hour') start_time
)

SELECT f.start_time,
COUNT(id) total,
COUNT(CASE WHEN value > 0 THEN 1 END) AS positive_votes,
COUNT(CASE WHEN value = 0 THEN 1 END) AS indifferent_votes,
COUNT(CASE WHEN value < 0 THEN 1 END) AS negative_votes,
SUM(CASE WHEN value > 0 THEN 2 WHEN value = 0 THEN 1 WHEN value < 0 THEN -4 END) AS score

FROM votes_vote m
RIGHT JOIN hour_intervals f 
        ON m.timestamp AT TIME ZONE %s >= f.start_time AND m.timestamp AT TIME ZONE %s < f.start_time + '1 hour'::interval
        AND m.location_id = %s
GROUP BY f.start_time
ORDER BY f.start_time

为什么我之前在昨晚 7 点到 10 点之间完美运行的查询,但今天却失败了?我是否应该期望这个新查询也会失败?

有人可以解释我第一次(或每次)出错的地方吗?

【问题讨论】:

  • 为什么不能使用DATE_TRUNC? Django 有使用它的内置选项。
  • @GwynBleidD 喜欢这样吗? votes = Vote.objects.filter(location=l).filter(timestamp__date=timezone.now().date()).extra({"hour":"date_trunc('hour',timestamp)"}).values("hour").order_by().annotate(score=score_annotation, count=Count('id')) 我认为它已经接近了——我将更多地使用这种方法。谢谢!
  • 我从 SQL 中获取 date_trunc,但如果您不必严格使用您的方法来生成该查询,我可以发布完整的答案,创建几乎相同的结果。
  • @GwynBleidD 我很想看看——实际上,我在上面发布的 QuerySet 不起作用。

标签: python django postgresql timezone


【解决方案1】:

首先,将related_name='votes' 添加到位置的外键中,为了更好地控制,现在使用位置模型可以做到:

from django.db.models import Count, Case, Sum, When, IntegerField
from django.db.models.expressions import DateTime

queryset = location.objects.annotate(
    datetimes=DateTime('votes__timestamp', 'hour', tz),
    positive_votes=Count(Case(
        When(votes__value__gt=0, then=1),
        default=None,
        output_field=IntegerField())),
    indifferent_votes=Count(Case(
        When(votes__value=0, then=1),
        default=None,
        output_field=IntegerField())),
    negative_votes=Count(Case(
        When(votes__value__lt=0, then=1),
        default=None,
        output_field=IntegerField())),
    score=Sum(Case(
        When(votes__value__lt=0, then=-4),
        When(votes__value=0, then=1),
        When(votes__value__gt=0, then=2),
        output_field=IntegerField())),
    ).values_list('datetimes', 'positive_votes', 'indifferent_votes', 'negative_votes', 'score').distinct().order_by('datetimes')

这将为每个位置生成统计信息。您当然可以将其过滤到任何位置或时间范围。

【讨论】:

  • 谢谢!我使用生成的 SQL 并针对我的数据库运行它并得到了正确的结果......但是,当从 Django 调用时,我得到了ValueError: Database returned an invalid value in QuerySet.datetimes(). Are time zone definitions for your database and pytz installed?。看起来可能有错误:code.djangoproject.com/ticket/25937#comment:1
  • 请注意,此过滤器有一个回归。它在没有结果的地方留下了空白,而 generate_series 给出了完整的时间表。
  • 如果 tz 为 none 并且您在 django 中全局启用了时区支持,它将抛出该错误。所以你必须每次都设置时区。是的,该查询的一个缺点是在没有任何投票的情况下省略了几个小时。
  • 我在调用查询之前设置了 tz = timezone.get_current_timezone() 。应该以不同的方式完成吗? timezone.activate(timezone.get_current_timezone()) ?
  • 如果您将USE_TZ 设置设置为True,则必须将时区对象设置为DateTime 的第三个参数。如果您将 USE_TZ 设置为 False,请尝试发送 None。
【解决方案2】:

如果您处理的日期时间字段允许空值,您可以使用以下方法解决https://code.djangoproject.com/ticket/25937

Potato.objects.annotate(
    time=Coalesce(
        TruncMonth('removed', tzinfo=timezone.UTC()),
        Value(datetime.min.replace(tzinfo=timezone.UTC()),
    ).values('time').annotate(c=Count('pk'))

这用一个容易发现的哨兵替换了 NULL 时间。如果你已经在使用datetime.min,你就必须想出别的办法。

我在生产中使用它,但我发现 TruncMonth() 本身的位置会给你当地时间,当你把 Coalesce() 放在它周围时,你只能有天真或 UTC。

【讨论】:

    猜你喜欢
    • 2014-07-02
    • 1970-01-01
    • 1970-01-01
    • 2012-05-15
    • 2012-10-15
    • 1970-01-01
    • 1970-01-01
    • 2012-02-11
    相关资源
    最近更新 更多