Pourquoi les fonctions d'agrégation SQL sont-elles tellement plus lentes que Python et Java (ou Poor Man's OLAP)

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

Question

J'ai besoin de l'avis d'un vrai DBA.Postgres 8.3 prend 200 ms pour exécuter cette requête sur mon Macbook Pro tandis que Java et Python effectuent le même calcul en moins de 20 ms (350 000 lignes) :

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

Est-ce un comportement normal lors de l'utilisation d'une base de données SQL ?

Le schéma (le tableau contient les réponses à une enquête) :

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

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

J'ai écrit quelques tests en Java et Python pour le contexte et ils écrasent SQL (sauf pour Python pur) :

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

Même sqlite3 est compétitif avec Postgres même s'il suppose que toutes les colonnes sont des chaînes (par contraste :même en utilisant simplement le passage aux colonnes numériques au lieu des entiers dans Postgres, cela entraîne un ralentissement 10x)

Les réglages que j'ai essayés sans succès incluent (en suivant aveuglément quelques conseils 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

Ma question est donc la suivante : mon expérience ici est-elle normale et c'est ce à quoi je peux m'attendre lorsque j'utilise une base de données SQL ?Je peux comprendre qu'ACID doit avoir des coûts, mais c'est un peu fou à mon avis.Je ne demande pas de vitesse de jeu en temps réel, mais comme Java peut traiter des millions de doubles en moins de 20 ms, je me sens un peu jaloux.

Existe-t-il une meilleure façon de réaliser un OLAP simple à moindre coût (à la fois en termes d'argent et de complexité du serveur) ?J'ai étudié Mondrian et Pig + Hadoop, mais je ne suis pas très enthousiasmé par la maintenance d'une autre application serveur et je ne suis même pas sûr qu'ils pourraient aider.


Non, le code Python et le code Java font tout le travail en interne pour ainsi dire.Je viens de générer 4 tableaux avec 350 000 valeurs aléatoires chacun, puis je prends la moyenne.Je n'inclus pas la génération dans les timings, seulement l'étape de moyenne.Le timing des threads Java utilise 4 threads (un par tableau en moyenne), ce qui est excessif mais c'est certainement le plus rapide.

Le timing sqlite3 est piloté par le programme Python et s'exécute à partir du disque (et non de la mémoire :)

Je me rends compte que Postgres fait beaucoup plus en coulisses, mais la plupart de ce travail ne m'importe pas puisqu'il s'agit de données en lecture seule.

La requête Postgres ne modifie pas le timing lors des exécutions suivantes.

J'ai réexécuté les tests Python pour inclure la mise en file d'attente du disque.Le timing ralentit considérablement jusqu'à près de 4 secondes.Mais je suppose que le code de gestion des fichiers de Python est à peu près en C (mais peut-être pas dans la bibliothèque csv ?), donc cela m'indique que Postgres ne diffuse pas non plus à partir du disque (ou que vous avez raison et que je devrais m'incliner avant celui qui a écrit sa couche de stockage !)

Était-ce utile?

La solution

Postgres fait bien plus qu'il n'y paraît (maintenir la cohérence des données pour commencer !)

Si les valeurs ne doivent pas nécessairement être exactes à 100 %, ou si la table est rarement mise à jour, mais que vous exécutez souvent ce calcul, vous souhaiterez peut-être examiner les vues matérialisées pour l'accélérer.

(Remarque, je n'ai pas utilisé de vues matérialisées dans Postgres, elles semblent peu piratées, mais pourraient convenir à votre situation).

Vues matérialisées

Tenez également compte de la surcharge liée à la connexion réelle au serveur et de l'aller-retour requis pour envoyer la demande au serveur et inversement.

Je considérerais que 200 ms pour quelque chose comme ça est plutôt bon. Un test rapide sur mon serveur Oracle, la même structure de table avec environ 500 000 lignes et aucun index, prend environ 1 à 1,5 secondes, ce qui est presque uniquement du simple oracle qui aspire les données. hors disque.

La vraie question est : 200 ms sont-ils assez rapides ?

-------------- Plus --------------------

Je souhaitais résoudre ce problème en utilisant des vues matérialisées, car je n'ai jamais vraiment joué avec elles.C'est dans Oracle.

J'ai d'abord créé un MV qui s'actualise toutes les minutes.

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;

Bien que ce soit actualisé, aucune ligne n'est renvoyée

SQL> select * from mv_so_x;

no rows selected

Elapsed: 00:00:00.00

Une fois actualisé, c'est BEAUCOUP plus rapide que de faire la requête brute

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 on insère dans la table de base, le résultat n'est pas immédiatement visible voir le 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> 

Mais attendez environ une minute, et le MV sera mis à jour en coulisses et le résultat sera renvoyé aussi rapidement que vous le souhaitez.

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> 

Ce n'est pas idéal.pour commencer, ce n'est pas en temps réel, les insertions/mises à jour ne seront pas immédiatement visibles.En outre, vous avez une requête en cours d'exécution pour mettre à jour le MV, que vous en ayez besoin ou non (cela peut être adapté à n'importe quelle période ou à la demande).Mais cela montre à quel point un MV peut paraître plus rapide à l'utilisateur final, si vous pouvez vivre avec des valeurs qui ne sont pas tout à fait précises à la seconde près.

Autres conseils

Je dirais que votre schéma de test n'est pas vraiment utile.Pour répondre à la requête de base de données, le serveur de base de données passe par plusieurs étapes :

  1. analyser le SQL
  2. élaborer un plan de requête, i.e.décider des indices à utiliser (le cas échéant), optimiser, etc.
  3. si un index est utilisé, recherchez-y les pointeurs vers les données réelles, puis accédez à l'emplacement approprié dans les données ou
  4. si aucun index n'est utilisé, scannez toute la table pour déterminer quelles lignes sont nécessaires
  5. charger les données du disque dans un emplacement temporaire (espérons-le, mais pas nécessairement, la mémoire)
  6. effectuer les calculs count() et avg()

Ainsi, créer un tableau en Python et obtenir la moyenne ignore essentiellement toutes ces étapes, sauf la dernière.Comme les E/S disque font partie des opérations les plus coûteuses qu'un programme doit effectuer, il s'agit d'un défaut majeur du test (voir aussi les réponses à cette question J'ai déjà demandé ici).Même si vous lisez les données du disque lors de votre autre test, le processus est complètement différent et il est difficile de déterminer la pertinence des résultats.

Pour obtenir plus d'informations sur l'endroit où Postgres passe son temps, je suggère les tests suivants :

  • Comparez le temps d'exécution de votre requête à un SELECT sans les fonctions d'agrégation (c.e.couper l'étape 5)
  • Si vous constatez que l'agrégation entraîne un ralentissement significatif, essayez si Python le fait plus rapidement, en obtenant les données brutes via le simple SELECT à partir de la comparaison.

Pour accélérer votre requête, réduisez d’abord l’accès au disque.Je doute fort que ce soit l'agrégation qui prenne du temps.

Il existe plusieurs façons de procéder :

  • Mettre en cache les données (en mémoire !) pour un accès ultérieur, soit via les propres capacités du moteur de base de données, soit avec des outils comme memcached
  • Réduisez la taille de vos données stockées
  • Optimiser l’utilisation des indices.Parfois, cela peut impliquer d'ignorer complètement l'utilisation de l'index (après tout, il s'agit également de l'accès au disque).Pour MySQL, je me souviens qu'il est recommandé d'ignorer les index si vous supposez que la requête récupère plus de 10 % de toutes les données de la table.
  • Si votre requête fait bon usage des index, je sais que pour les bases de données MySQL, il est utile de placer les index et les données sur des disques physiques séparés.Cependant, je ne sais pas si cela s'applique à Postgres.
  • Il peut également y avoir des problèmes plus sophistiqués, tels que l'échange de lignes sur le disque si, pour une raison quelconque, le jeu de résultats ne peut pas être complètement traité en mémoire.Mais je laisserais ce genre de recherche jusqu'à ce que je rencontre de sérieux problèmes de performances que je ne trouve pas d'autre moyen de résoudre, car cela nécessite des connaissances sur de nombreux petits détails cachés dans votre processus.

Mise à jour:

Je viens de réaliser que vous ne semblez pas avoir besoin d'index pour la requête ci-dessus et que vous n'en utilisez probablement pas non plus, donc mes conseils sur les indices n'ont probablement pas été utiles.Désolé.Pourtant, je dirais que l'agrégation n'est pas le problème mais l'accès au disque l'est.Je vais laisser les éléments d'index, de toute façon, ils pourraient encore être utiles.

J'ai retesté avec MySQL en précisant ENGINE = MEMORY et ça ne change rien (toujours 200 ms).Sqlite3 utilisant une base de données en mémoire donne également des timings similaires (250 ms).

Les maths ici semble correct (au moins la taille, car c'est la taille de la base de données SQLite :-)

Je n'accepte tout simplement pas l'argument des causes de la lenteur du disque, car tout indique que les tables sont en mémoire (les gars de Postgres mettent tous en garde contre toute tentative trop forte d'épingler les tables en mémoire car ils jurent que le système d'exploitation le fera mieux que le programmeur. )

Pour clarifier les timings, le code Java ne lit pas à partir du disque, ce qui en fait une comparaison totalement injuste si Postgres lit à partir du disque et calcule une requête compliquée, mais ce n'est vraiment pas la question, la base de données devrait être suffisamment intelligente pour apporter un petit table en mémoire et précompiler une procédure stockée à mon humble avis.

MISE À JOUR (en réponse au premier commentaire ci-dessous) :

Je ne sais pas comment tester la requête sans utiliser de fonction d'agrégation d'une manière qui serait juste, car si je sélectionne toutes les lignes, cela passera beaucoup de temps à tout sérialiser et à tout formater.Je ne dis pas que la lenteur est due à la fonction d'agrégation, cela pourrait encore être simplement dû à la concurrence, à l'intégrité et aux amis.Je ne sais tout simplement pas comment isoler l'agrégation comme seule variable indépendante.

Ce sont des réponses très détaillées, mais elles soulèvent surtout la question suivante : comment puis-je obtenir ces avantages sans quitter Postgres étant donné que les données tiennent facilement en mémoire, nécessitent des lectures simultanées mais aucune écriture et sont interrogées avec la même requête encore et encore.

Est-il possible de précompiler la requête et le plan d'optimisation ?J'aurais pensé que la procédure stockée ferait cela, mais cela n'aide pas vraiment.

Pour éviter l'accès au disque, il est nécessaire de mettre en cache toute la table en mémoire, puis-je forcer Postgres à le faire ?Je pense que c'est déjà le cas, puisque la requête s'exécute en seulement 200 ms après des exécutions répétées.

Puis-je dire à Postgres que la table est en lecture seule, afin qu'elle puisse optimiser n'importe quel code de verrouillage ?

Je pense qu'il est possible d'estimer les coûts de construction d'une requête avec une table vide (les délais varient de 20 à 60 ms)

Je ne vois toujours pas pourquoi les tests Java/Python ne sont pas valides.Postgres ne fait tout simplement pas beaucoup plus de travail (même si je n'ai toujours pas abordé l'aspect de la concurrence, juste la mise en cache et la construction des requêtes)

MISE À JOUR:Je ne pense pas qu'il soit juste de comparer les SELECTS comme suggéré en extrayant 350 000 étapes de pilote et de sérialisation dans Python pour exécuter l'agrégation, ni même d'omettre l'agrégation car la surcharge de formatage et d'affichage est difficile à séparer du timing.Si les deux moteurs fonctionnent sur des données en mémoire, cela devrait être une comparaison de pommes avec des pommes, mais je ne sais pas comment garantir que cela se produit déjà.

Je n'arrive pas à comprendre comment ajouter des commentaires, peut-être que je n'ai pas assez de réputation ?

Je suis moi-même un gars MS-SQL, et nous utiliserions DBCC PINTABLE pour garder une table en cache, et DÉFINIR LES STATISTIQUES IO pour voir qu'il lit à partir du cache et non du disque.

Je ne trouve rien sur Postgres pour imiter PINTABLE, mais pg_buffercache semble donner des détails sur ce qu'il y a dans le cache - vous voudrez peut-être vérifier cela et voir si votre table est réellement mise en cache.

Un rapide retour en arrière du calcul de l'enveloppe me fait soupçonner que vous effectuez une pagination à partir du disque.En supposant que Postgres utilise des entiers de 4 octets, vous disposez de (6 * 4) octets par ligne, donc votre table fait au minimum (24 * 350 000) octets ~ 8,4 Mo.En supposant un débit soutenu de 40 Mo/s sur votre disque dur, vous avez besoin d'environ 200 ms pour lire les données (ce qui, Comme indiqué, devrait être l'endroit où presque tout le temps est passé).

À moins que j'aie foiré mes calculs quelque part, je ne vois pas comment il est possible que vous puissiez lire 8 Mo dans votre application Java et le traiter dans les délais que vous affichez - à moins que ce fichier ne soit déjà mis en cache par le lecteur ou votre Système d'exploitation.

Je ne pense pas que vos résultats soient si surprenants – c'est plutôt que Postgres est si rapide.

La requête Postgres s'exécute-t-elle plus rapidement une deuxième fois une fois qu'elle a eu la possibilité de mettre les données en cache ?Pour être un peu plus juste, votre test pour Java et Python devrait couvrir le coût d'acquisition des données en premier lieu (idéalement en les chargeant depuis le disque).

Si ce niveau de performances pose un problème pour votre application dans la pratique mais que vous avez besoin d'un SGBDR pour d'autres raisons, vous pouvez alors consulter memcaché.Vous auriez alors un accès plus rapide aux données brutes en cache et pourriez effectuer les calculs dans le code.

Utilisez-vous TCP pour accéder à Postgres ?Dans ce cas, Nagle joue avec votre timing.

Une autre chose qu'un SGBDR fait généralement pour vous est d'assurer la concurrence en vous protégeant de l'accès simultané par un autre processus.Cela se fait en plaçant des verrous, ce qui entraîne des frais généraux.

Si vous avez affaire à des données entièrement statiques qui ne changent jamais, et surtout si vous êtes dans un scénario essentiellement « à utilisateur unique », l'utilisation d'une base de données relationnelle ne vous apporte pas nécessairement beaucoup d'avantages.

Vous devez augmenter les caches de Postgres au point où l'ensemble de l'ensemble de travail tient dans la mémoire avant de pouvoir vous attendre à voir des performances comparables à celles d'un programme en mémoire.

Merci pour les timings Oracle, c'est le genre de choses que je recherche (décevant cependant :-)

Les vues matérialisées valent probablement la peine d'être prises en compte car je pense pouvoir précalculer les formes les plus intéressantes de cette requête pour la plupart des utilisateurs.

Je ne pense pas que le temps d'aller-retour des requêtes devrait être très élevé car j'exécute les requêtes sur la même machine qui exécute Postgres, donc cela ne peut pas ajouter beaucoup de latence ?

J'ai également vérifié la taille du cache, et il semble que Postgres s'appuie sur le système d'exploitation pour gérer la mise en cache. Ils mentionnent spécifiquement BSD comme système d'exploitation idéal pour cela, donc je pense que Mac OS devrait être assez intelligent pour intégrer la table. mémoire.À moins que quelqu'un ait des paramètres plus spécifiques en tête, je pense qu'une mise en cache plus spécifique est hors de mon contrôle.

En fin de compte, je peux probablement supporter des temps de réponse de 200 ms, mais sachant que 7 ms est un objectif possible, je me sens insatisfait, car même des temps de 20 à 50 ms permettraient à davantage d'utilisateurs d'avoir des requêtes plus à jour et de s'en débarrasser. beaucoup de cache et de hacks précalculés.

Je viens de vérifier les timings avec MySQL 5 et ils sont légèrement pires que Postgres.Donc, à moins d'avancées majeures en matière de mise en cache, je suppose que c'est ce à quoi je peux m'attendre en empruntant la voie de la base de données relationnelle.

J'aimerais pouvoir voter certaines de vos réponses, mais je n'ai pas encore assez de points.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top