Как я могу извлечь данные из моей базы данных, используя Django ORM, который аннотирует значения для каждого дня? - PullRequest
0 голосов
/ 10 апреля 2020

У меня есть приложение Django, которое подключено к базе данных MySQL. База данных полна записей - несколько миллионов из них.

Мои модели выглядят так:

class LAN(models.Model):
    ...

class Record(models.Model):
    start_time = models.DateTimeField(...)
    end_time = models.DateTimeField(...)

    ip_address = models.CharField(...)
    LAN = models.ForeignKey(LAN, related_name="records", ...)

    bytes_downloaded = models.BigIntegerField(...)
    bytes_uploaded = models.BigIntegerField(...)

Каждая запись отражает окно времени и показывает, является ли конкретный IP-адрес конкретным Локальная сеть выполняла любую загрузку или загрузку в течение этого окна.

Что мне нужно знать, это то, что: учитывая дату начала и дату окончания, дайте мне таблицу, в которой ДНИ ДАННОЙ ЛВС имели ЛЮБОЕ действие (имеет какие-либо записи )

Пример:

В период с 1 января по 31 января скажите, в какие ДНИ ЛАНА на них были ЛЮБЫЕ записи

Предположим, что один раз в в то время как локальная сеть будет отключаться на несколько дней и в эти дни не будет никаких записей или каких-либо действий.

Мое решение:

Я могу сделать это медленным путем, добавив некоторые методы к моя модель локальной сети:

class LAN(models.Model):
    ...

    # Returns True if there are records for the current LAN between 2 given dates
    # Returns False otherwise
    def online(self, start, end):
        criterion1 = Q(start_time__lt=end)
        criterion2 = Q(end_time__gt=start)

        return self.records.filter(criterion1 & criterion2).exists()

    # Returns a list of days that a LAN was online for between 2 given dates
    def list_online_days(self, start, end):

        start_date = timezone.make_aware(timezone.datetime.strptime(start, "%b %d, %Y"))
        end_date = timezone.make_aware(timezone.datetime.strptime(end, "%b %d, %Y"))
        end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=999999)

        days_online = []
        current_date = start.astimezone()

        while current_date <= end:
            start_of_day = current_date.replace(hour=0, minute=0, second=0, microsecond=0)
            end_of_day = current_date.replace(hour=23, minute=59, second=59, microsecond=999999)

            if self.online(start=start_of_day, end=end_of_day):
                days_online.append(current_date.date())

            current_date += timezone.timedelta(days=1)

        return days_online

В этот момент я могу выполнить:

lan = LAN.objects.get(id=1) # Or whatever LAN I'm interested in
days_online = lan.list_online_days(start="Jan 1, 2020", end="Jan 31, 2020")

Это работает, но в результате выполняется один запрос в день между моей датой начала и датой окончания. В этом случае 31 запрос (1 января, 2 января и др. c.). Это делает его очень, очень медленным для больших периодов времени, так как ему нужно go просмотреть все записи в базе данных 31 раз. Индексация базы данных помогает, но она все еще медленная с достаточным количеством данных в базе данных.

Есть ли способ сделать один запрос к базе данных, чтобы получить то, что мне нужно?

Мне кажется, что это будет выглядеть что-то вроде этого, но я не совсем понимаю это правильно:

lan.records.filter(criterion1 & criterion2).annotate(date=TruncDay('start_time')).order_by('date').distinct().values('date').annotate(exists=Exists(SOMETHING))

Первая часть:

lan.records.filter(criterion1 & criterion2).annotate(date=TruncDay('start_time')).order_by('date').distinct().values('date')

Кажется, дает мне то, что я хочу - одно значение в день, но Я не уверен, как аннотировать результат с помощью поля с существующим значением, которое показывает, существуют ли какие-либо записи в этот день.

Примечание. Это упрощенная версия моего приложения, а не точные модели и поля, поэтому если некоторые вещи могут быть улучшены, например, не использовать CharField для поля ip_address, не слишком фокусироваться на этом

1 Ответ

0 голосов
/ 10 апреля 2020

Ответ оказался проще, чем я думал, в основном потому, что он у меня уже был.

Это:

lan.records.filter(criterion1 & criterion2).annotate(date=TruncDay('start_time')).order_by('date').distinct().values('date').annotate(exists=Exists(Record.objects.filter(pk=OuterRef('pk'))))

Было то, что я ожидал, но все, что он делает, это возврат exists=True за все возвращенные дни, что является точным, но не слишком полезным. Это потому, что любые дни, в которых не было записей о них, уже опущены в результатах.

Это означает, что я могу пропустить весь раздел аннотирования и просто сделать это:

lan.records.filter(criterion1 & criterion2).annotate(date=TruncDay('start_time')).order_by('date').distinct().values('date')

, что уже дает мне список объектов даты и времени, когда присутствовали записи, и пропускает все, где их не было.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...