Recherche d'optimisation des requêtes DuckDB

Suite aux discussions menées sur le verrouillage de mémoire RAM par DuckDB, il serait opportun de vérifier si nous pouvons optimiser les requête les plus lourdes.

La pagination est l’une des réflexions à mener.

Ci-dessous pour exemple l’une de mes pages les plus lourdes pour le suivi des énergies, chaque graphique contenant 8 courbes (susceptibles d’être augmentée par ailleurs) et le tout étant détaillé pour chaque phases (L1/L2/L3) + le total.

Les visualisations temporelles jusque 3 mois est fluide. Mais l’affichage à l’année est plus lente, soit environ 10s.

Requete initiale à l’affichage du dashboard avec des graphiques sur la dernière heure :


Début de chargement au passage à l’année sur le 1er graphique

Fin de chargement

Détail requête

Détail temporel de la requete

Suite à la mise à jour 4.66.3, je confirme que je mets « seulement » 1s de plus pour l’affichage à l’année et aucune différence ressentie pour le reste :

@pierre-gilles, n’hésite pas à me dire si tu veux d’autres détails

Pas besoin de pagination, on fait de l’échantillonnage, on ne renvoie que max 300 lignes par graphique :slight_smile:

Je vais regarder avec ces informations, merci pour le post détaillé !

1 « J'aime »

Est-ce que tu peux faire des tests sur ta base ?

J’aimerais voir ce que ça donne avec cette requête :

SELECT
    TIME_BUCKET(INTERVAL ? MINUTES, created_at) AS created_at,
    AVG(value) AS value,
    MAX(value) AS max_value,
    MIN(value) AS min_value,
    SUM(value) AS sum_value,
    COUNT(value) AS count_value
FROM
    t_device_feature_state
WHERE device_feature_id = ?
AND created_at > ?
GROUP BY 1
ORDER BY created_at;

Il faut remplacer les ? :

  • Le premier remplace le par 1752
  • Le deuxième par le device_feature_id le plus complet que tu as
  • Le troisième par la date d’il y a un an, soit '2024-12-14T10:04:11.478Z'

Si tu veux comparer, voilà la requête actuelle:

  WITH intervals AS (
        SELECT
            created_at,
            value,
            NTILE(300) OVER (ORDER BY created_at) AS interval
        FROM
            t_device_feature_state
        WHERE device_feature_id = ?
        AND created_at > ?
    )
    SELECT
        MIN(created_at) AS created_at,
        AVG(value) AS value,
        MAX(value) AS max_value,
        MIN(value) AS min_value,
        SUM(value) AS sum_value,
        COUNT(value) AS count_value
    FROM
        intervals
    GROUP BY
        interval
    ORDER BY
        created_at;
1 « J'aime »

Une question, pourquoi 1752 ? Cela fait des buckets de 29h12 ??

Sinon, voilà les résultats :

  1. Agrégation par bucket temporel - Requete TimeBucket
SELECT
    TIME_BUCKET(INTERVAL 1752 MINUTES, created_at) AS created_at,
    AVG(value) AS value,
    MAX(value) AS max_value,
    MIN(value) AS min_value,
    SUM(value) AS sum_value,
    COUNT(value) AS count_value
FROM
    t_device_feature_state
WHERE
    device_feature_id = 'e9079170-4654-4fad-b726-23a36c793905'
    AND created_at > TIMESTAMP '2024-12-14 10:04:11.478'
GROUP BY 1
ORDER BY created_at;


Résultat

  • 301 lignes retournées
  • Temps d’exécution ≈ 0,486 s
  • Buckets temporels fixes (~29h)
  1. Découpage par quantiles - Requete NTILE(300)
WITH intervals AS (
    SELECT
        created_at,
        value,
        NTILE(300) OVER (ORDER BY created_at) AS interval
    FROM
        t_device_feature_state
    WHERE
        device_feature_id = 'e9079170-4654-4fad-b726-23a36c793905'
        AND created_at > TIMESTAMP '2024-12-14 10:04:11.478'
)
SELECT
    MIN(created_at) AS created_at,
    AVG(value) AS value,
    MAX(value) AS max_value,
    MIN(value) AS min_value,
    SUM(value) AS sum_value,
    COUNT(value) AS count_value
FROM
    intervals
GROUP BY
    interval
ORDER BY
    created_at;


Résultat

  • 300 lignes retournées
  • Temps d’exécution ≈ 0,370 s
  • Intervalles équilibrés en nombre de points

Exact ! C’est 525600/300 :smiley:

Par contre, la performance n’est pas meilleure, et les deux sont même plutôt bonnes niveau performance…

Après, dans ton graphique, tu as 10 fonctionnalités, donc il faut faire * 10, ce qui explique tes performances.

La solution n’est peut-être pas là, mais peut-être sur une requête commune pour 10 fonctionnalités

1 « J'aime »

Bon bah, je viens de faire des tests … Et comme ça je dirais que le problème n’est surement pas les requetes à DuckDB (je crois d’ailleurs qu’on avait déjà fait ce genre de tests directement en dev dans Gladys ^^). Ce doit être simplement le rendu ou plutot la construction des séries … :

  1. Agrégation par bucket temporel - Requete TimeBucket
SELECT
    device_feature_id,
    TIME_BUCKET(INTERVAL 1752 MINUTES, created_at) AS created_at,
    AVG(value) AS value,
    MAX(value) AS max_value,
    MIN(value) AS min_value,
    SUM(value) AS sum_value,
    COUNT(value) AS count_value
FROM t_device_feature_state
WHERE device_feature_id IN (
	'eaa982b7-a3c8-4b4d-b2d0-5a5cf5245938',
    'e9079170-4654-4fad-b726-23a36c793905',
    'c59fca37-54f3-418b-b27c-857e08e2a0fb',
    'da79df66-dcff-438e-859c-39b641fd15e9',
    '5eba10ac-4c3a-479c-aba7-a1e74a00d914',
    '4b5e90f0-a86b-405b-8ca1-953705ef79a3',
    'c92f8fa3-d841-4692-bd3a-e5addb760a73',
    'b5445f10-7aa9-41bf-8cae-d2f30498652f',
    '08e90478-4bb2-4a08-986f-3d6f212c95f1',
    '85311f8c-334f-4b7e-8294-2b841c0b8fe8'
)
AND created_at > TIMESTAMP '2024-12-14 10:04:11.478'
GROUP BY
    device_feature_id,
    created_at
ORDER BY
    device_feature_id,
    created_at;

  1. Découpage par quantiles - Requete NTILE(300)
WITH intervals AS (
    SELECT
        device_feature_id,
        created_at,
        value,
        NTILE(300) OVER (
            PARTITION BY device_feature_id
            ORDER BY created_at
        ) AS interval
    FROM t_device_feature_state
    WHERE device_feature_id IN (
		'eaa982b7-a3c8-4b4d-b2d0-5a5cf5245938',
	    'e9079170-4654-4fad-b726-23a36c793905',
	    'c59fca37-54f3-418b-b27c-857e08e2a0fb',
	    'da79df66-dcff-438e-859c-39b641fd15e9',
	    '5eba10ac-4c3a-479c-aba7-a1e74a00d914',
	    '4b5e90f0-a86b-405b-8ca1-953705ef79a3',
	    'c92f8fa3-d841-4692-bd3a-e5addb760a73',
	    'b5445f10-7aa9-41bf-8cae-d2f30498652f',
	    '08e90478-4bb2-4a08-986f-3d6f212c95f1',
	    '85311f8c-334f-4b7e-8294-2b841c0b8fe8'
    )
    AND created_at > TIMESTAMP '2024-12-14 10:04:11.478'
)
SELECT
    device_feature_id,
    MIN(created_at) AS created_at,
    AVG(value) AS value,
    MAX(value) AS max_value,
    MIN(value) AS min_value,
    SUM(value) AS sum_value,
    COUNT(value) AS count_value
FROM intervals
GROUP BY
    device_feature_id,
    interval
ORDER BY
    device_feature_id,
    created_at;

J’ai donc refait un test de performance sur le dashboard :

Etude ChatGPT ^^ :

:magnifying_glass_tilted_left: Lecture directe de ton enregistrement Performance

:stopwatch: Durée totale

  • Total ≈ 13 190 ms (13,2 s)

:bar_chart: Répartition (panneau de gauche)

  • Affichage (Rendering) : 2 330 ms
  • Rendu (Paint / Layout) : 791 ms
  • Script (JS) : 563 ms
  • Chargement réseau : 27 ms
  • Système / non attribué : ~5 200 ms
  • Total “utile” visible : ~4 s
  • Gros trou “non attribué / idle / attente” : ~9 s

:backhand_index_pointing_right: Conclusion immédiate :
:cross_mark: ce n’est ni le réseau,
:cross_mark: ni DuckDB,
:cross_mark: ni le JavaScript pur,
:backhand_index_pointing_right: c’est majoritairement du temps d’attente avant que le rendu utile démarre.


:magnifying_glass_tilted_right: Focus sur la partie Réseau (clé pour Gladys)

Dans ton tableau à droite :

  • Requête : aggregated_states (10.5.0.227)
  • Taille transférée : 213 ko
  • Temps d’exécution : ~260 ms

:backhand_index_pointing_right: Ça confirme ce qu’on soupçonnait :

  • la requête backend est rapide
  • le payload est raisonnable
  • < 300 ms, donc pas le problème

:right_arrow: On peut désormais dire sans ambiguïté :

Les 10–13 secondes ne viennent pas des requêtes DuckDB.


:brain: Où se perdent alors les ~9 secondes ?

D’après le flame chart et la timeline :

:one: Phase “attente / non attribuée” (~9 s)

C’est typiquement :

  • attente d’un state React complet
  • attente que toutes les séries soient prêtes
  • synchronisation avant montage du graphique
  • ou logique type :
    • “on attend toutes les promesses”
    • “on attend que X soit calculé avant de rendre”

:backhand_index_pointing_right: Anti-pattern classique sur les dashboards complexes.


:two: Phase “Rendering + Layout” (~3 s)

Là, on est clairement sur :

  • beaucoup de points
  • beaucoup de séries
  • probablement :
    • recalcul d’échelles
    • recalcul de légendes
    • tooltips
    • min/max
    • animations

:backhand_index_pointing_right: C’est cohérent avec un graphe annuel multi-courbes.

:receipt: Conclusion (que tu peux déjà formuler)

Tu peux déjà dire, preuves à l’appui :

Les mesures Chrome Performance montrent que :

  • la requête backend est < 300 ms
  • le payload est ~213 ko
  • le JS pur < 600 ms

Les ~13 s ressenties sont principalement dues à une phase d’attente + rendu graphique côté front.
L’optimisation doit donc se faire majoritairement sur le pipeline de rendu React / graphe, pas sur DuckDB.

:compass: Prochaines pistes concrètes (si tu veux aller plus loin)

Sans même voir le code, les leviers évidents sont :

  • rendre le graphe progressivement (séries une par une)
  • limiter le nombre de points avant le rendu (hard cap)
  • désactiver animations / transitions sur les vues longues
  • virtualiser / canvas pur si ce n’est pas déjà le cas

Je suis pas d’accord avec ChatGPT, on voit bien dans tes captures d’écrans que c’est la réponse backend qui prend 11 secondes à répondre :

Ce qui n’est pas étonnant, tu m’as montré qu’une requête mettait 400-500ms, donc 500 ms * 10 = 5 secondes au mieux :slight_smile:

Justement, tu prouves mon point : pour l’instant, on ne fait pas une requête avec 10 IDs, on fait 10 requêtes séparées !

Ok, je comprends en effet.
Voici le résultat pour les 10 requetes séparées :

Je suis sur les mêmes features que la courbe affichées

Ça prouve plutôt le point, vu qu’il faut prendre en compte que quand tu affiches ton tableau de bord, tu n’as pas juste ce widget, mais potentiellement 5 autres widgets, donc on est plus sur 50 requêtes :slight_smile:

Oui oui, je suis en accord avec ce que tu dis !!

Mmmmh ça pas d’accord, puisque l’affichage à l’année est envoyer après le 1er chargement (affichage à l’heure au refresh de la page). Lorsque je sélectionne le graph à l’année, je n’active que celui-ci. On voit d’ailleurs bien ensuite les autres requetes ‹ live › s’ajouter et se mettre en attente à la suite.

Mais je suis ok pour dire qu’actuellement on est plus de l’ordre de 3 à 5s d’attente de requetes DuckDB.

1 « J'aime »