Búsqueda de optimización de consultas DuckDB

Tras las discusiones realizadas sobre el bloqueo de memoria RAM por DuckDB, sería oportuno verificar si podemos optimizar las consultas más pesadas.

La paginación es una de las reflexiones a llevar a cabo.

A continuación, un ejemplo de una de mis páginas más pesadas para el seguimiento de energías, cada gráfico contiene 8 curvas (susceptibles de aumentar, por cierto) y todo detallado para cada fase (L1/L2/L3) + el total.

Las visualizaciones temporales hasta 3 meses son fluidas. Pero la visualización anual es más lenta, aproximadamente 10 segundos.

Consulta inicial al mostrar el panel de control con gráficos de la última hora:


Inicio de carga al pasar al año en el primer gráfico

Fin de carga

Detalle de la consulta

Detalle temporal de la consulta

Tras la actualización 4.66.3, confirmo que solo se tarda 1 segundo más para la visualización anual y ninguna diferencia percibida para el resto:

@pierre-gilles, no dudes en decirme si necesitas más detalles

No hace falta paginación, hacemos muestreo, solo devolvemos un máximo de 300 líneas por gráfico :slight_smile:

Voy a mirar con esta información, ¡gracias por el post detallado!

¿Puedes hacer pruebas en tu base de datos?

Me gustaría ver qué pasa con esta consulta:

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;

Hay que reemplazar los ?:

  • El primero por 1752
  • El segundo por el device_feature_id más completo que tengas
  • El tercero por la fecha de hace un año, es decir, '2024-12-14T10:04:11.478Z'

Si quieres comparar, aquí está la consulta actual:

  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;

Una pregunta, ¿por qué 1752? ¿Eso hace buckets de 29h12?

De lo contrario, aquí están los resultados:

  1. Agregación por bucket temporal - Consulta 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;


Resultado

  • 301 líneas devueltas
  • Tiempo de ejecución ≈ 0,486 s
  • Buckets temporales fijos (~29h)
  1. División por percentiles - Consulta 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;


Resultado

  • 300 líneas devueltas
  • Tiempo de ejecución ≈ 0,370 s
  • Intervalos equilibrados en número de puntos

¡Exacto! Es 525600/300 :smiley:

Por otro lado, el rendimiento no es mejor, y ambos son bastante buenos en cuanto a rendimiento…

Después, en tu gráfico tienes 10 características, por lo que hay que multiplicar por 10, lo que explica tus resultados.

La solución no está ahí, pero tal vez en una consulta común para 10 características.

Bueno, acabo de hacer algunas pruebas… Y diría que el problema no es seguramente las consultas a DuckDB (creo que ya habíamos hecho este tipo de pruebas directamente en dev en Gladys ^^). Debe ser simplemente el renderizado o más bien la construcción de las series…:

  1. Agregación por intervalo temporal - Consulta 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. División por percentiles - Consulta 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;

Por lo tanto, he vuelto a hacer una prueba de rendimiento en el panel de control:

Estudio ChatGPT ^^:

:magnifying_glass_tilted_left: Lectura directa de tu registro de rendimiento

:stopwatch: Duración total

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

:bar_chart: Distribución (panel de la izquierda)

  • Visualización (Rendering) : 2 330 ms
  • Renderizado (Paint / Layout) : 791 ms
  • Script (JS) : 563 ms
  • Carga de red : 27 ms
  • Sistema / no atribuido : ~5 200 ms
  • Total “útil” visible : ~4 s
  • Gran hueco “no atribuido / inactivo / espera” : ~9 s

:backhand_index_pointing_right: Conclusión inmediata :
:cross_mark: no es la red,
:cross_mark: ni DuckDB,
:cross_mark: ni el JavaScript puro,
:backhand_index_pointing_right: es mayoritariamente tiempo de espera antes de que comience el renderizado útil.


:magnifying_glass_tilted_right: Enfoque en la parte de Red (clave para Gladys)

En tu tabla de la derecha:

  • Consulta: aggregated_states (10.5.0.227)
  • Tamaño transferido : 213 ko
  • Tiempo de ejecución : ~260 ms

:backhand_index_pointing_right: Esto confirma lo que sospechábamos:

  • la consulta backend es rápida
  • el payload es razonable
  • < 300 ms, por lo tanto no es el problema

:right_arrow: Ahora podemos decir sin ambigüedad:

Los 10–13 segundos no provienen de las consultas DuckDB.


:brain: ¿Dónde se pierden entonces los ~9 segundos?

Según el flame chart y la línea de tiempo:

:one: Fase “espera / no atribuida” (~9 s)

Esto es típicamente:

  • espera de un estado React completo
  • espera a que todas las series estén listas
  • sincronización antes del montaje del gráfico
  • o lógica tipo:
    • “esperar todas las promesas”
    • “esperar que X se calcule antes de renderizar”

:backhand_index_pointing_right: Anti-patrón clásico en los dashboards complejos.


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

Aquí, estamos claramente en:

  • muchos puntos
  • muchas series
  • probablemente:
    • recálculo de escalas
    • recálculo de leyendas
    • tooltips
    • min/max
    • animaciones

:backhand_index_pointing_right: Es coherente con un gráfico anual multi-curvas.

:receipt: Conclusión (que ya puedes formular)

Ya puedes decir, con pruebas:

Las mediciones de rendimiento de Chrome muestran que:

  • la consulta backend es < 300 ms
  • el payload es ~213 ko
  • el JS puro < 600 ms

Los ~13 s percibidos se deben principalmente a una fase de espera + renderizado gráfico en el front.
La optimización debe hacerse mayoritariamente en el pipeline de renderizado React / gráfico, no en DuckDB.

:compass: Próximas pistas concretas (si quieres ir más allá)

Sin siquiera ver el código, los mecanismos evidentes son:

  • renderizar el gráfico progresivamente (series una por una)
  • limitar el número de puntos antes del renderizado (límite duro)
  • desactivar animaciones / transiciones en las vistas largas
  • virtualizar / canvas puro si no es ya el caso

No estoy de acuerdo con ChatGPT, se ve claramente en tus capturas de pantalla que es la respuesta del backend la que tarda 11 segundos en responder:

Lo cual no es sorprendente, me has mostrado que una consulta tarda 400-500 ms, por lo que 500 ms * 10 = 5 segundos como mínimo :slight_smile:

Precisamente, demuestras mi punto: por ahora, no hacemos una consulta con 10 IDs, ¡hacemos 10 consultas separadas!

Vale, entiendo.
Aquí están los resultados para las 10 consultas separadas:

Estoy en las mismas características que la curva mostrada

Esto demuestra más bien el punto, ya que hay que tener en cuenta que cuando muestras tu panel de control, no solo tienes este widget, sino potencialmente 5 widgets más, por lo que estamos más cerca de 50 consultas :slight_smile:

¡Sí, sí, estoy de acuerdo con lo que dices!

Mmmmh no estoy de acuerdo, porque la visualización anual se envía después del primer cargamento (visualización por hora al actualizar la página). Cuando selecciono el gráfico anual, solo activo ese. De hecho, luego se pueden ver las otras solicitudes ‹ en vivo › agregándose y poniéndose en espera.

Pero estoy de acuerdo en decir que actualmente estamos hablando de un tiempo de espera de solicitudes de DuckDB de 3 a 5 segundos.