Django原始sql / postgres时区混乱

时间:2016-02-16 15:28:10

标签: python django postgresql timezone

我花了最后一天尝试从我的数据库中获取时间序列的聚合。我尝试使用Django ORM,但很快放弃了,然后跑回SQL。我不认为可以使用PSQL generate_series,我认为他们更喜欢你在python中使用itertools或其他方法。

我的模型很像这样:

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)

我想要做的是,随着时间的推移显示一系列指标 - 目前,当前用户每小时的当前聚合。用户设置了时区(默认为' America / Chicago')。我一直在使用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点之间工作得很好但是今天失败了?我是否应该期望这个新查询也会下降?

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

2 个答案:

答案 0 :(得分: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')

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

答案 1 :(得分:0)

如果您正在处理的日期时间字段将允许空值,则可以使用以下内容解决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。