【问题标题】:Django with MySQL backend - group by time range带有 MySQL 后端的 Django - 按时间范围分组
【发布时间】:2019-01-10 09:27:21
【问题描述】:

我有这个简单的模型:

models.py

class Ping(models.Model):
    online = models.BooleanField()
    created = models.DateTimeField(db_index=True, default=timezone.now)

    def __str__(self):
        return f'{self.online}, {self.created}'

它给了我以下结果:

mysql [lab]> SELECT * FROM myapp_ping;
+----+--------+----------------------------+
| id | online | created                    |
+----+--------+----------------------------+
|  1 |      1 | 2018-08-02 13:34:09.435292 |
|  2 |      1 | 2018-08-02 13:35:09.520200 |
|  3 |      0 | 2018-08-02 13:36:09.540638 |
|  4 |      0 | 2018-08-02 13:37:10.529783 |
|  5 |      1 | 2018-08-02 13:38:09.779012 |
|  6 |      1 | 2018-08-02 13:39:09.650365 |
|  7 |      1 | 2018-08-02 13:40:09.625543 |
|  8 |      1 | 2018-08-02 13:41:09.892196 |
|  9 |      1 | 2018-08-02 13:42:09.802186 |
| 10 |      1 | 2018-08-02 13:43:09.864551 |
| 11 |      1 | 2018-08-02 13:44:09.960962 |
| 12 |      1 | 2018-08-02 13:45:09.891947 |
| 13 |      0 | 2018-08-02 13:46:09.141727 |
| 14 |      0 | 2018-08-02 13:47:09.142030 |
| 15 |      0 | 2018-08-02 13:48:09.160942 |
| 16 |      0 | 2018-08-02 13:49:09.152879 |
| 17 |      0 | 2018-08-02 13:50:09.280246 |
| 18 |      1 | 2018-08-02 13:51:09.363184 |
| 19 |      1 | 2018-08-02 13:52:09.405863 |
| 20 |      1 | 2018-08-02 13:53:09.403251 |
+----+--------+----------------------------+
20 rows in set (0.00 sec)

有没有办法得到类似这样的输出(online 一直为假的范围):

停机时间:

from                | to                  | duration
2018-08-02 13:36:09 | 2018-08-02 13:37:10 | 1 minute and 1 second
2018-08-02 13:46:09 | 2018-08-02 13:50:09 | 4 minutes and 0 seconds

我不确定这是否可以使用 Django ORM 完成,或者它需要一个原始的 MySQL 查询才能使用类似CASEIF 语句的东西?

更新:2018 年 8 月 8 日星期三 15:13:15 UTC

所以我从@AKX answer 获得了两种解决方案的概念证明:

models.py

class PingManager(models.Manager):
    def downtime_python(self):
        queryset = super().get_queryset().filter(created__gt=timezone.now() - timezone.timedelta(days=30))
        offline = False
        ret = []
        for entry in queryset:
            if not entry.online and not offline:
                offline = True
                _ret = {'start': str(entry.created)}
            if entry.online and offline:
                _ret.update({'end': str(entry.created)})
                ret.append(_ret)
                offline = False
        return ret

    def downtime_sql(self):
        queryset = super().get_queryset().filter(created__gt=timezone.now() - timezone.timedelta(days=30))
        offline = queryset.filter(online=False).order_by('created').first()
        last = queryset.order_by('created').last()
        ret = []
        if offline:
            online = queryset.filter(created__gt=offline.created, online=True).order_by('created').first()
            ret.append({'start': str(offline.created), 'end': str(online.created)})
            while True:
                offline = queryset.filter(created__gt=online.created, online=False).order_by('created').first()
                if offline:
                    online = queryset.filter(created__gt=offline.created, online=True).order_by('created').first()
                if (online and offline) and online.created < last.created:
                    ret.append({'start': str(offline.created), 'end': str(online.created)})
                    continue
                else:
                    break
        return ret

class Ping(models.Model):
    online = models.BooleanField()
    created = models.DateTimeField(db_index=True, default=timezone.now)
    objects = PingManager()

    def __str__(self):
        return f'{self.online}, {self.created}'

问题:

  1. 我应该为此创建一个静态方法还是自定义 manger 是正确的解决方案?

  2. 如果两个计算都在内存中运行,为什么执行时间会有如此巨大的差异?有没有办法改进并使其更像python等效方法?

测试:

# python manage.py shell
Python 3.6.5 (default, Apr 10 2018, 17:08:37) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from myapp.models import Ping

In [2]: Ping.objects.downtime_sql()[0]
Out[2]: 
{'start': '2018-07-13 16:32:16.009356+00:00',
 'end': '2018-07-13 16:33:15.942784+00:00'}

In [3]: Ping.objects.downtime_python()[0]
Out[3]: 
{'start': '2018-07-13 16:32:16.009356+00:00',
 'end': '2018-07-13 16:33:15.942784+00:00'}

In [4]: Ping.objects.downtime_sql() == Ping.objects.downtime_python()
Out[4]: True

In [5]: import timeit

In [6]: timeit.timeit(stmt=Ping.objects.downtime_python, number=1)
Out[6]: 5.720254830084741

In [7]: timeit.timeit(stmt=Ping.objects.downtime_sql, number=1)
Out[7]: 0.25946347787976265

【问题讨论】:

  • 我不确定即使是 SQL case/if 语句也能得到那个结果,因为结果行取决于前面的行。不过,这在 Python 中很容易实现。
  • 在这些方法中queryset 有多大? downtime_python() 需要从数据库中加载所有这些并将它们“反序列化”到模型中。
  • 它非常大,探针每分钟运行一次,所以 30 天是 ~40256
  • 这就是为什么它要慢得多。 :D

标签: python mysql django django-models


【解决方案1】:

扩展我的评论:

我不确定即使是 SQL case/if 语句也能得到那个结果,因为结果行取决于前面的行。不过,这在 Python 中很容易实现。

  1. 显而易见的方法是循环遍历Ping.objects.all()(或Ping.objects.iterator())并跟踪online 变量以形成您想要的“条纹”。这样做的缺点是您确实需要遍历每个对象,这最终会很慢(和/或耗尽您的内存)。
  2. 使用更多查询但内存少得多的更复杂的方法是找到第一个离线的Ping 对象,然后找到下一个(时间上)再次在线的Ping 对象——即会形成一个连胜。然后冲洗并重复此操作,直到用完要检查的 Ping 对象。

编辑

是的,这是方法 2 的一个(相当优雅,如果你不介意我说的话)具体实现(在 https://github.com/akx/so51656477 找到完整的测试仓库):

class PingQuerySet(models.QuerySet):
    def streaks(self):
        queryset = self.values_list('created', 'online').order_by('created')
        entry = queryset.first()
        while entry:
            next_entry = queryset.filter(created__gt=entry[0], online=(not entry[1])).first()
            yield (entry, next_entry)
            entry = next_entry

它是二元组的生成器:((start_timestamp, start_online), (end_timestamp, end_online) | None)

例如,要获取过去 10 天的涨/跌或跌/涨对,

for start, end in Ping.objects.filter(created__gt=now() - timedelta(days=10)).streaks():
    print(start, end)

会打印类似的东西

[...snip...]

(datetime.datetime(2018, 8, 8, 8, 10, 12, 943500), False) (datetime.datetime(2018, 8, 8, 10, 10, 12, 943500), True)
(datetime.datetime(2018, 8, 8, 10, 10, 12, 943500), True) (datetime.datetime(2018, 8, 8, 11, 10, 12, 943500), False)
(datetime.datetime(2018, 8, 8, 11, 10, 12, 943500), False) (datetime.datetime(2018, 8, 8, 11, 40, 12, 943500), True)
(datetime.datetime(2018, 8, 8, 11, 40, 12, 943500), True) (datetime.datetime(2018, 8, 8, 12, 40, 12, 943500), False)
(datetime.datetime(2018, 8, 8, 12, 40, 12, 943500), False) (datetime.datetime(2018, 8, 8, 16, 40, 12, 943500), True)
(datetime.datetime(2018, 8, 8, 16, 40, 12, 943500), True) (datetime.datetime(2018, 8, 8, 17, 40, 12, 943500), False)
(datetime.datetime(2018, 8, 8, 17, 40, 12, 943500), False) (datetime.datetime(2018, 8, 8, 18, 10, 12, 943500), True)
(datetime.datetime(2018, 8, 8, 18, 10, 12, 943500), True) (datetime.datetime(2018, 8, 8, 19, 40, 12, 943500), False)
(datetime.datetime(2018, 8, 8, 19, 40, 12, 943500), False) (datetime.datetime(2018, 8, 8, 23, 10, 12, 943500), True)
(datetime.datetime(2018, 8, 8, 23, 10, 12, 943500), True) (datetime.datetime(2018, 8, 9, 0, 10, 12, 943500), False)
(datetime.datetime(2018, 8, 9, 0, 10, 12, 943500), False) (datetime.datetime(2018, 8, 9, 3, 10, 12, 943500), True)
(datetime.datetime(2018, 8, 9, 3, 10, 12, 943500), True) (datetime.datetime(2018, 8, 9, 3, 40, 12, 943500), False)
(datetime.datetime(2018, 8, 9, 3, 40, 12, 943500), False) (datetime.datetime(2018, 8, 9, 5, 10, 12, 943500), True)
(datetime.datetime(2018, 8, 9, 5, 10, 12, 943500), True) (datetime.datetime(2018, 8, 9, 5, 40, 12, 943500), False)
(datetime.datetime(2018, 8, 9, 5, 40, 12, 943500), False) (datetime.datetime(2018, 8, 9, 7, 10, 12, 943500), True)
(datetime.datetime(2018, 8, 9, 7, 10, 12, 943500), True) None

一些注意事项:

  • 最后一个end 的值可能是None,这意味着机器仍然处于运行状态或处于运行状态(取决于start 元组的状态值)。
  • 如果您只关心机器停机的时间,只需忽略 start 元组的状态值为 True 的对。
  • 由于这是一个生成器,当你有足够的数据时,你可以停止迭代它,它不会进一步查询。
  • 由于这是QuerySet 扩展方法,您可以根据需要添加其他过滤器(只要它们不过滤online)。例如,如果您有一个 host 字段,则为 Ping.objects.filter(host='example.com').streaks()

【讨论】:

  • 您能否为这两种解决方案提供一些示例?
  • 请看我来自Wed 8 Aug 15:13:15 UTC 2018的更新,非常感谢任何cmets。
  • @HTF 好的,我已经添加了我自己的实现 :)
【解决方案2】:

你可以使用@classmethod,然后按照你想要的方式格式化输出,这里我有一个例子:

from dateutil.relativedelta import relativedelta


class Ping(models.Model):
    online = models.BooleanField()
    created = models.DateTimeField(db_index=True, default=timezone.now)

    def __str__(self):
        return f'{self.online}, {self.created}'

    @classmethod
    def ping_online_duration(cls, is_online):
        first = cls.objects.filter(online=is_online).order_by('created').first()
        last = cls.objects.filter(online=is_online).order_by('created').last()
        return {
            'from': first.created.strftime('%Y-%m-%d %H:%M:%S'),
            'to': last.created.strftime('%Y-%m-%d %H:%M:%S'),
            'duration': (f'{relativedelta(last.created, first.created).minutes} minutes '
                         f'{relativedelta(last.created, first.created).seconds} seconds.')
        }

你可以这样称呼它:

在线组:

Ping.ping_online_duration(True)

{'from': '2018-08-02 15:02:19',
 'to': '2018-08-02 15:03:02',
 'duration': '0 minutes 43 seconds'}

离线组:

Ping.ping_online_duration(False)

{'from': '2018-08-02 15:02:27',
 'to': '2018-08-02 15:03:01',
 'duration': '0 minutes 34 seconds'}

正如我之前所说,您可以根据需要格式化输出。

【讨论】:

  • 我喜欢这个概念,但结果会出错。它将搜索在线为假的第一条和最后一条记录:&gt;&gt;&gt; relativedelta(last.created, first.created) relativedelta(days=+20, hours=+18, seconds=+59, microseconds=+988703) -> '0 minutes 59 seconds.'。顺便说一句,在这种情况下你为什么使用classmethod 方法而不是staticmethod
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-31
  • 2019-02-14
  • 1970-01-01
  • 2020-03-10
  • 2012-10-19
相关资源
最近更新 更多