¿Por qué las funciones agregadas de SQL son mucho más lentas que Python y Java (o OLAP de Poor Man)?

StackOverflow https://stackoverflow.com/questions/51553

Pregunta

Necesito la opinión de un DBA real.Postgres 8.3 tarda 200 ms en ejecutar esta consulta en mi Macbook Pro, mientras que Java y Python realizan el mismo cálculo en menos de 20 ms (350.000 filas):

SELECT count(id), avg(a), avg(b), avg(c), avg(d) FROM tuples;

¿Es este comportamiento normal cuando se utiliza una base de datos SQL?

El esquema (la tabla contiene respuestas a una encuesta):

CREATE TABLE tuples (id integer primary key, a integer, b integer, c integer, d integer);

\copy tuples from '350,000 responses.csv' delimiter as ','

Escribí algunas pruebas en Java y Python para contexto y aplastan SQL (excepto Python puro):

java   1.5 threads ~ 7 ms    
java   1.5         ~ 10 ms    
python 2.5 numpy   ~ 18 ms  
python 2.5         ~ 370 ms

Incluso sqlite3 es competitivo con Postgres a pesar de asumir que todas las columnas son cadenas (por el contrario:incluso usar simplemente cambiar a columnas numéricas en lugar de números enteros en Postgres da como resultado una desaceleración 10 veces mayor)

Los ajustes que he probado sin éxito incluyen (siguiendo ciegamente algunos consejos web):

increased the shared memory available to Postgres to 256MB    
increased the working memory to 2MB
disabled connection and statement logging
used a stored procedure via CREATE FUNCTION ... LANGUAGE SQL

Entonces mi pregunta es: ¿mi experiencia aquí es normal y esto es lo que puedo esperar al usar una base de datos SQL?Puedo entender que ACID tenga costos, pero en mi opinión esto es una locura.No estoy pidiendo velocidad de juego en tiempo real, pero como Java puede procesar millones de dobles en menos de 20 ms, me siento un poco celoso.

¿Existe una mejor manera de hacer OLAP simple y económico (tanto en términos de dinero como de complejidad del servidor)?He investigado Mondrian y Pig + Hadoop, pero no estoy muy entusiasmado con el mantenimiento de otra aplicación de servidor y no estoy seguro de si me ayudarían.


No, el código Python y el código Java hacen todo el trabajo internamente, por así decirlo.Simplemente genero 4 matrices con 350.000 valores aleatorios cada una y luego tomo el promedio.No incluyo la generación en los tiempos, sólo el paso promedio.La sincronización de los subprocesos de Java utiliza 4 subprocesos (uno por matriz en promedio), lo cual es excesivo, pero definitivamente es el más rápido.

La sincronización de sqlite3 está controlada por el programa Python y se ejecuta desde el disco (no: memoria:)

Me doy cuenta de que Postgres está haciendo mucho más detrás de escena, pero la mayor parte de ese trabajo no me importa ya que se trata de datos de solo lectura.

La consulta de Postgres no cambia el tiempo en ejecuciones posteriores.

Volví a ejecutar las pruebas de Python para incluir la puesta en cola del disco.El tiempo se ralentiza considerablemente hasta casi 4 segundos.Pero supongo que el código de manejo de archivos de Python está prácticamente en C (aunque tal vez no en la biblioteca csv), por lo que esto me indica que Postgres tampoco se transmite desde el disco (o que tienes razón y debería inclinarme). ¡Antes de quien escribió su capa de almacenamiento!)

¿Fue útil?

Solución

Postgres está haciendo mucho más de lo que parece (¡mantener la coherencia de los datos para empezar!)

Si los valores no tienen que ser 100% precisos, o si la tabla se actualiza rara vez, pero ejecuta este cálculo con frecuencia, es posible que desee consultar Vistas materializadas para acelerarlo.

(Tenga en cuenta que no he utilizado vistas materializadas en Postgres, parecen un poco complicadas, pero podrían adaptarse a su situación).

Vistas materializadas

Considere también la sobrecarga de conectarse al servidor y el viaje de ida y vuelta requerido para enviar la solicitud al servidor y regresar.

Consideraría que 200 ms para algo como esto es bastante bueno. Una prueba rápida en mi servidor Oracle, la misma estructura de tabla con aproximadamente 500.000 filas y sin índices, toma aproximadamente de 1 a 1,5 segundos, lo cual es casi todo simplemente Oracle absorbiendo los datos. fuera del disco.

La verdadera pregunta es: ¿200 ms son suficientes?

-------------- Más --------------------

Estaba interesado en resolver esto usando vistas materializadas, ya que nunca he jugado con ellas.Esto está en Oracle.

Primero creé un MV que se actualiza cada minuto.

create materialized view mv_so_x 
build immediate 
refresh complete 
START WITH SYSDATE NEXT SYSDATE + 1/24/60
 as select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

Si bien es refrescante, no se devuelven filas.

SQL> select * from mv_so_x;

no rows selected

Elapsed: 00:00:00.00

Una vez que se actualiza, es MUCHO más rápido que realizar la consulta sin formato.

SQL> select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:05.74
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

Si lo insertamos en la tabla base, el resultado no se puede ver inmediatamente en el MV.

SQL> insert into so_x values (1,2,3,4,5);

1 row created.

Elapsed: 00:00:00.00
SQL> commit;

Commit complete.

Elapsed: 00:00:00.00
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

Pero espere aproximadamente un minuto y el MV se actualizará detrás de escena y el resultado se obtendrá tan rápido como podría desear.

SQL> /

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899460 7495.35823 22.2905352 5.00276078 2.17647059

Elapsed: 00:00:00.00
SQL> 

Esto no es ideal.Para empezar, no es en tiempo real, las inserciones/actualizaciones no serán visibles de inmediato.Además, tiene una consulta en ejecución para actualizar el MV, ya sea que lo necesite o no (esto se puede ajustar a cualquier período de tiempo o según demanda).Pero esto muestra cuánto más rápido puede hacerle parecer un MV al usuario final, si puede vivir con valores que no son del todo precisos.

Otros consejos

Yo diría que su esquema de prueba no es realmente útil.Para completar la consulta de base de datos, el servidor de base de datos sigue varios pasos:

  1. analizar el SQL
  2. elaborar un plan de consulta, i.mi.decidir qué índices utilizar (si corresponde), optimizar, etc.
  3. si se utiliza un índice, busque en él los punteros a los datos reales, luego vaya a la ubicación apropiada en los datos o
  4. si no se utiliza ningún índice, escanear toda la mesa para determinar qué filas son necesarias
  5. cargar los datos del disco en una ubicación temporal (con suerte, pero no necesariamente, la memoria)
  6. realizar los cálculos count() y avg()

Entonces, crear una matriz en Python y obtener el promedio básicamente omite todos estos pasos, excepto el último.Como la E/S de disco se encuentra entre las operaciones más costosas que debe realizar un programa, este es un defecto importante en la prueba (consulte también las respuestas a esta pregunta Pregunté aquí antes).Incluso si lees los datos del disco en tu otra prueba, el proceso es completamente diferente y es difícil saber qué tan relevantes son los resultados.

Para obtener más información sobre dónde pasa su tiempo Postgres, sugeriría las siguientes pruebas:

  • Compare el tiempo de ejecución de su consulta con un SELECT sin las funciones de agregación (es decir,mi.cortar paso 5)
  • Si encuentra que la agregación genera una desaceleración significativa, intente si Python lo hace más rápido, obteniendo los datos sin procesar a través del SELECT simple de la comparación.

Para acelerar su consulta, primero reduzca el acceso al disco.Dudo mucho que sea la agregación la que se tome el tiempo.

Hay varias maneras de hacerlo:

  • Caché de datos (¡en la memoria!) para acceso posterior, ya sea a través de las capacidades propias del motor de base de datos o con herramientas como memcached
  • Reduzca el tamaño de sus datos almacenados
  • Optimizar el uso de índices.A veces esto puede significar omitir el uso del índice por completo (después de todo, también es acceso al disco).Para MySQL, creo recordar que se recomienda omitir índices si se supone que la consulta recupera más del 10% de todos los datos de la tabla.
  • Si su consulta hace un buen uso de los índices, sé que para las bases de datos MySQL es útil colocar índices y datos en discos físicos separados.Sin embargo, no sé si eso se aplica a Postgres.
  • También puede haber problemas más sofisticados, como intercambiar filas al disco si por alguna razón el conjunto de resultados no se puede procesar completamente en la memoria.Pero dejaría ese tipo de investigación hasta que me encuentre con serios problemas de rendimiento que no pueda encontrar otra manera de solucionar, ya que requiere conocimiento sobre muchos pequeños detalles ocultos en su proceso.

Actualizar:

Me acabo de dar cuenta de que parece que no necesitas índices para la consulta anterior y lo más probable es que tampoco estés usando ninguno, por lo que mi consejo sobre índices probablemente no fue útil.Lo siento.Aún así, diría que la agregación no es el problema, pero sí el acceso al disco.Dejaré el índice, de todos modos, aún podría tener alguna utilidad.

Volví a probar con MySQL especificando ENGINE = MEMORY y no cambia nada (todavía 200 ms).Sqlite3 que utiliza una base de datos en memoria también proporciona tiempos similares (250 ms).

Las matemáticas aquí parece correcto (al menos el tamaño, ya que así de grande es la base de datos sqlite :-)

Simplemente no me creo el argumento de la lentitud de las causas del disco, ya que todo indica que las tablas están en la memoria (todos los chicos de Postgres advierten contra esforzarse demasiado para fijar tablas en la memoria, ya que juran que el sistema operativo lo hará mejor que el programador). )

Para aclarar los tiempos, el código Java no se lee desde el disco, lo que la convierte en una comparación totalmente injusta si Postgres lee desde el disco y calcula una consulta complicada, pero eso realmente no viene al caso, la base de datos debería ser lo suficientemente inteligente como para traer una pequeña tabla en la memoria y precompilar un procedimiento almacenado en mi humilde opinión.

ACTUALIZACIÓN (en respuesta al primer comentario a continuación):

No estoy seguro de cómo probaría la consulta sin usar una función de agregación de una manera que fuera justa, ya que si selecciono todas las filas, pasaré mucho tiempo serializando y formateando todo.No estoy diciendo que la lentitud se deba a la función de agregación, aún podría deberse simplemente a una sobrecarga debido a la concurrencia, la integridad y los amigos.Simplemente no sé cómo aislar la agregación como única variable independiente.

Esas son respuestas muy detalladas, pero en su mayoría plantean la pregunta: ¿cómo puedo obtener estos beneficios sin salir de Postgres, dado que los datos caben fácilmente en la memoria, requieren lecturas simultáneas pero no escrituras y se consultan con la misma consulta una y otra vez?

¿Es posible precompilar la consulta y el plan de optimización?Pensé que el procedimiento almacenado haría esto, pero en realidad no ayuda.

Para evitar el acceso al disco es necesario almacenar en caché toda la tabla en la memoria, ¿puedo obligar a Postgres a hacerlo?Sin embargo, creo que ya lo está haciendo, ya que la consulta se ejecuta en solo 200 ms después de ejecuciones repetidas.

¿Puedo decirle a Postgres que la tabla es de solo lectura para que pueda optimizar cualquier código de bloqueo?

Creo que es posible estimar los costos de construcción de consultas con una tabla vacía (los tiempos oscilan entre 20 y 60 ms)

Todavía no puedo ver por qué las pruebas de Java/Python no son válidas.Postgres simplemente no está haciendo mucho más trabajo (aunque todavía no he abordado el aspecto de la concurrencia, solo el almacenamiento en caché y la construcción de consultas)

ACTUALIZAR:No creo que sea justo comparar los SELECTS como se sugiere al pasar 350,000 a través del controlador y los pasos de serialización en Python para ejecutar la agregación, ni siquiera omitir la agregación ya que la sobrecarga en el formato y la visualización es difícil de separar del tiempo.Si ambos motores están funcionando con datos de memoria, debería ser una comparación de manzanas con manzanas, aunque no estoy seguro de cómo garantizar que eso ya esté sucediendo.

No sé cómo agregar comentarios, ¿tal vez no tengo suficiente reputación?

Yo también soy un tipo de MS-SQL y usaríamos DBCC PINTABLE para mantener una tabla en caché, y ESTABLECER ESTADÍSTICAS IO para ver que está leyendo desde la memoria caché y no desde el disco.

No puedo encontrar nada en Postgres para imitar PINTABLE, pero pg_buffercache parece brindar detalles sobre lo que hay en el caché; es posible que desee verificarlo y ver si su tabla realmente se está almacenando en caché.

Un rápido cálculo del reverso del sobre me hace sospechar que estás paginando desde el disco.Suponiendo que Postgres utiliza enteros de 4 bytes, tiene (6 * 4) bytes por fila, por lo que su tabla tiene un mínimo de (24 * 350 000) bytes ~ 8,4 MB.Suponiendo un rendimiento sostenido de 40 MB/s en su disco duro, necesita alrededor de 200 ms para leer los datos (lo cual, Como se señaló, debería ser donde se pasa casi todo el tiempo).

A menos que haya arruinado mis cálculos en alguna parte, no veo cómo es posible que puedas leer 8 MB en tu aplicación Java y procesarlos en los tiempos que estás mostrando, a menos que ese archivo ya esté almacenado en caché por la unidad o tu SO.

No creo que sus resultados sean tan sorprendentes; en todo caso, es que Postgres es tan rápido.

¿La consulta de Postgres se ejecuta más rápido por segunda vez una vez que ha tenido la oportunidad de almacenar en caché los datos?Para ser un poco más justo, su prueba para Java y Python debería cubrir el costo de adquirir los datos en primer lugar (idealmente cargarlos desde el disco).

Si este nivel de rendimiento es un problema para su aplicación en la práctica pero necesita un RDBMS por otras razones, entonces podría considerar memcached.Entonces tendría un acceso en caché más rápido a los datos sin procesar y podría realizar los cálculos en código.

¿Estás utilizando TCP para acceder a Postgres?En ese caso, Nagle está alterando tu sincronización.

Otra cosa que un RDBMS generalmente hace por usted es brindar concurrencia protegiéndolo del acceso simultáneo de otro proceso.Esto se hace colocando cerraduras, y esto genera algunos gastos generales.

Si está tratando con datos completamente estáticos que nunca cambian, y especialmente si se encuentra básicamente en un escenario de "usuario único", entonces el uso de una base de datos relacional no necesariamente le reporta muchos beneficios.

Debe aumentar los cachés de Postgres hasta el punto en que todo el conjunto de trabajo quepa en la memoria antes de que pueda esperar ver un rendimiento comparable al de hacerlo en memoria con un programa.

Gracias por los tiempos de Oracle, ese es el tipo de cosas que estoy buscando (aunque decepcionante :-)

Probablemente valga la pena considerar las vistas materializadas, ya que creo que puedo calcular previamente las formas más interesantes de esta consulta para la mayoría de los usuarios.

No creo que el tiempo de ida y vuelta de las consultas deba ser muy alto, ya que estoy ejecutando las consultas en la misma máquina que ejecuta Postgres, por lo que no puede agregar mucha latencia.

También revisé un poco los tamaños de caché y parece que Postgres depende del sistema operativo para manejar el almacenamiento en caché; mencionan específicamente a BSD como el sistema operativo ideal para esto, por lo que creo que Mac OS debería ser bastante inteligente a la hora de incorporar la tabla. memoria.A menos que alguien tenga parámetros más específicos en mente, creo que el almacenamiento en caché más específico está fuera de mi control.

Al final, probablemente pueda soportar tiempos de respuesta de 200 ms, pero saber que 7 ms es un objetivo posible me hace sentir insatisfecho, ya que incluso tiempos de 20 a 50 ms permitirían a más usuarios tener consultas más actualizadas y deshacerse de una gran cantidad de almacenamiento en caché y trucos precalculados.

Acabo de comprobar los tiempos usando MySQL 5 y son ligeramente peores que los de Postgres.Entonces, salvo algunos avances importantes en el almacenamiento en caché, supongo que esto es lo que puedo esperar en la ruta de la base de datos relacional.

Desearía poder votar algunas de sus respuestas, pero todavía no tengo suficientes puntos.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top