Project

General

Profile

Bug #69606

Performance: mauvaise requête SQL sur les statistiques du journal

Added by Pierre Ducroquet 4 months ago. Updated 4 months ago.

Status:
En cours
Priority:
Normal
Category:
-
Target version:
-
Start date:
27 September 2022
Due date:
% Done:

0%

Estimated time:
Patch proposed:
Yes
Planning:
No

Description

On voit beaucoup de requêtes de ce genre passer sur la prod, malgré notre machine de course :

SELECT MIN("month"), MAX("month") FROM (
    SELECT 
        DATE_TRUNC('month', "journal_event"."timestamp") AS "month", 
        ("journal_event"."data" ->> 'service_name') AS "service_name", 
        COUNT("journal_event"."id") AS "count" 
    FROM "journal_event" 
    WHERE ("journal_event"."type_id" = 3 AND "journal_event"."timestamp" >= '2019-01-01T00:00:00+01:00'::timestamptz)
    GROUP BY DATE_TRUNC('month', "journal_event"."timestamp"), ("journal_event"."data" ->> 'service_name')
) subquery;

Cette requête est complètement inefficace (800ms sur glc), surtout en l'absence d'index sur (type_id, timestamp).
Si on ajoute un tel index, la requête ne s'en sort pas mieux à cause de son écriture bien trop complexe.

Première simplification:

SELECT MIN("month"), MAX("month") FROM (
    SELECT 
        DATE_TRUNC('month', "journal_event"."timestamp") AS "month" 
    FROM "journal_event" 
    WHERE ("journal_event"."type_id" = 3 AND "journal_event"."timestamp" >= '2019-01-01T00:00:00+01:00'::timestamptz)
) subquery;

On tombe à 350/400ms, ~250ms avec l'index supplémentaire, ce qui est bien mais pas encore bon.
Le cœur du problème est l'inversion de l'ordre logique : on ne veut pas le minimum/maximum du mois des événements, on veut le mois du minimum/maximum des événements.
Si on écrivait la requête comme suit, on obtiendrait un bien meilleur résultat:
SELECT DATE_TRUNC('month', MIN(timestamp)), DATE_TRUNC('month', MAX(timestamp)) FROM "journal_event" 
    WHERE ("journal_event"."type_id" = 3 AND "journal_event"."timestamp" >= '2019-01-01T00:00:00+01:00'::timestamptz);

Là, on tombe à moins d'une milliseconde avec l'index supplémentaire (350 sans l'index).

Côté Django, j'ai identifié le code responsable :

# src/authentic2/apps/journal/utils.py
class Statistics:
    ...
    def __init__(self, qs, time_interval):
        self.time_interval = time_interval
        self.x_labels = self.build_x_labels(qs)
        self._x_labels_indexes = {label: i for i, label in enumerate(self.x_labels)}
        self.series = {}
        self.y_labels = []

    ...

    def build_x_labels(self, qs):
        if self.time_interval == 'timestamp':
            return list(qs.distinct().values_list(self.time_interval, flat=True))

        aggregate = qs.aggregate(min=Min(self.time_interval), max=Max(self.time_interval))

Je vois deux façons de régler le problème :
1) dans build_x_labels, massacrer altérer l'objet qs pour en extraire les critères WHERE, les paramètres du date_trunc, et construire une nouvelle requête à partir de tout ça,
2) modifier les signatures pour pouvoir ajouter un interval_qs qui retournera le min/max de l'axe X.

Ma requête simplifiée se construit a priori bien en Django:

Event.objects.filter(type=3, timestamp__gte=datetime.datetime(2020, 1, 1)).aggregate(min=Trunc(Min("timestamp"), kind='month'), max=Trunc(Max("timestamp"), kind='month'))


Files

History

#1

Updated by Pierre Ducroquet 4 months ago

Premier patch, il manque au moins la migration, mais je pense que ça devrait faire le taf.

#2

Updated by Benjamin Dauvergne 4 months ago

  • Status changed from Solution proposée to En cours
  • Assignee set to Pierre Ducroquet

Il manque un import pour Min/Max (Trunc aussi peut-être).

Also available in: Atom PDF