Pourquoi array_agg () est plus lent que le constructeur ARRAY () non agrégé?

13

Je revoyais juste un vieux code écrit pour PostgreSQL pré-8.4 , et j'ai vu quelque chose de vraiment chouette. Je me souviens avoir eu une fonction personnalisée pour faire une partie de cela dans la journée, mais j'ai oublié à quoi cela array_agg()ressemblait. Pour examen, l'agrégation moderne est écrite comme ceci.

SELECT array_agg(x ORDER BY x DESC) FROM foobar;

Cependant, il était une fois, il était écrit comme ça,

SELECT ARRAY(SELECT x FROM foobar ORDER BY x DESC);

Donc, je l'ai essayé avec des données de test ..

CREATE TEMP TABLE foobar AS
SELECT * FROM generate_series(1,1e7)
  AS t(x);

Les résultats ont été surprenants. La méthode #OldSchoolCool a été massivement plus rapide: une accélération de 25%. De plus, la simplifier sans l'ORDRE, a montré la même lenteur.

# EXPLAIN ANALYZE SELECT ARRAY(SELECT x FROM foobar);
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Result  (cost=104425.28..104425.29 rows=1 width=0) (actual time=1665.948..1665.949 rows=1 loops=1)
   InitPlan 1 (returns $0)
     ->  Seq Scan on foobar  (cost=0.00..104425.28 rows=6017728 width=32) (actual time=0.032..716.793 rows=10000000 loops=1)
 Planning time: 0.068 ms
 Execution time: 1671.482 ms
(5 rows)

test=# EXPLAIN ANALYZE SELECT array_agg(x) FROM foobar;
                                                        QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=119469.60..119469.61 rows=1 width=32) (actual time=2155.154..2155.154 rows=1 loops=1)
   ->  Seq Scan on foobar  (cost=0.00..104425.28 rows=6017728 width=32) (actual time=0.031..717.831 rows=10000000 loops=1)
 Planning time: 0.054 ms
 Execution time: 2174.753 ms
(4 rows)

Alors, qu'est-ce qui se passe ici. Pourquoi array_agg , une fonction interne est-elle tellement plus lente que le vaudou SQL du planificateur?

Utilisation de " PostgreSQL 9.5.5 sur x86_64-pc-linux-gnu, compilé par gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005, 64 bits"

Evan Carroll
la source

Réponses:

17

Il n'y a rien de "old school" ou "dépassé" sur un constructeur ARRAY (c'est ce qui ARRAY(SELECT x FROM foobar)est). C'est toujours aussi moderne. Utilisez-le pour l'agrégation simple de tableaux.

Le manuel:

Il est également possible de construire un tableau à partir des résultats d'une sous-requête. Sous cette forme, le constructeur du tableau est écrit avec le mot clé ARRAYsuivi d'une sous-requête entre parenthèses (non entre crochets).

La fonction d'agrégationarray_agg() est beaucoup plus polyvalente en ce qu'elle peut être intégrée dans une SELECTliste avec plus de colonnes, éventuellement plus d'agrégations dans la même SELECT, et des groupes arbitraires peuvent être formés avec GROUP BY. Alors qu'un constructeur ARRAY ne peut renvoyer qu'un seul tableau à partir d'un SELECTretour d'une seule colonne.

Je n'ai pas étudié le code source, mais il semblerait évident qu'un outil beaucoup plus polyvalent est également plus cher.

Erwin Brandstetter
la source
array_aggdoit garder une trace de l'ordre de ses entrées où le ARRAYconstructeur semble faire quelque chose à peu près équivalent à a UNIONcomme expression en interne. Si je devais deviner, il array_aggfaudrait probablement plus de mémoire. Je n'ai pas été en mesure de le tester de manière exhaustive, mais sur PostgreSQL 9.6 fonctionnant sur Ubuntu 16.04, la ARRAY()requête avec ORDER BYutilisé une fusion externe et était plus lente que la array_aggrequête. Comme vous l'avez dit, à moins de lire le code, votre réponse est la meilleure explication que nous ayons.
Jeff
@Jeffrey: Vous avez trouvé un cas de test où array_agg()est plus rapide que le constructeur de tableau? Pour un cas simple? Très peu probable, mais si c'est probablement le cas, car Postgres a basé sa décision pour un plan de requête sur des statistiques inexactes des paramètres de coût. Je n'ai jamais vu array_agg()surpasser un constructeur de tableau et j'ai testé plusieurs fois.
Erwin Brandstetter
1
@Jeffrey: Pas d'effets de mise en cache trompeurs? Avez-vous exécuté chaque requête plusieurs fois? J'aurais besoin de voir la définition de la table, les cardinalités et la requête exacte pour en dire plus.
Erwin Brandstetter
1
Ce n'est pas une vraie réponse. De nombreux outils polyvalents peuvent fonctionner aussi bien que des outils plus spécifiques. Si être polyvalent est en effet ce qui le rend plus lent, qu'en est-il de sa polyvalence?
Gavin Wahl
1
@Jeffrey: On dirait que Postgres choisit un algorithme de tri différent pour chaque variante (basé sur des estimations de coûts et des statistiques de table). Et il finit par choisir une méthode inférieure pour le constructeur ARRAY, ce qui indique qu'un ou plusieurs facteurs dans le calcul du coût estimé sont trop éloignés. C'est sur une table temporaire? L'avez-vous VACUUM ANALYZEfait avant d'exécuter les requêtes? Considérez: dba.stackexchange.com/a/18694/3684
Erwin Brandstetter
5

Je crois que la réponse acceptée par Erwin pourrait être ajoutée avec ce qui suit.

Habituellement, nous travaillons avec des tables régulières avec des indices, au lieu de tables temporaires (sans indices) comme dans la question d'origine. Il est utile de noter que les agrégations, telles que ARRAY_AGG, ne peuvent pas tirer parti des indices existants lorsque le tri est effectué pendant l'agrégation .

Par exemple, supposez la requête suivante:

SELECT ARRAY(SELECT c FROM t ORDER BY id)

Si nous avons un index activé t(id, ...), l'index pourrait être utilisé, en faveur d'un balayage séquentiel activé tsuivi d'un tri activé t.id. De plus, si la colonne de sortie enveloppée dans le tableau (ici c) fait partie de l'index (comme un index activé t(id, c)ou un index inclus t(id) include(c)), il peut même s'agir d'une analyse d'index uniquement.

Maintenant, réécrivons cette requête comme suit:

SELECT ARRAY_AGG(c ORDER BY id) FROM t

Maintenant, l'agrégation n'utilisera pas l'index et elle doit trier les lignes en mémoire (ou pire encore pour les grands ensembles de données, sur disque). Ce sera toujours une analyse séquentielle tsuivie d'une agrégation + tri .

Pour autant que je sache, cela n'est pas documenté dans la documentation officielle, mais peut être dérivé de la source. Cela devrait être le cas pour toutes les versions actuelles, v11 incluse.

pbillen
la source
2
Bon point. Mais en toute équité, les requêtes avec array_agg()ou des fonctions d' agrégation similaires peuvent encore des index de levier avec une sous - requête comme: SELECT ARRAY_AGG(c) FROM (SELECT c FROM t ORDER BY id) sub. La ORDER BYclause par agrégat est ce qui empêche l'utilisation de l'index dans votre exemple. Un constructeur de tableau est plus rapide que array_agg()lorsque l'un ou l'autre peut utiliser le même index (ou aucun). Ce n'est tout simplement pas aussi polyvalent. Voir: dba.stackexchange.com/a/213724/3684
Erwin Brandstetter
1
C'est une distinction importante à faire. J'ai légèrement modifié ma réponse pour préciser que cette remarque n'est valable que lorsque la fonction d'agrégation doit être triée. Vous pourriez en effet encore bénéficier de l'index dans le cas simple, car PostgreSQL semble donner une certaine garantie que l'agrégation se produira dans le même ordre que celui défini dans la sous-requête, comme expliqué dans le lien. C'est plutôt cool. Je me demande cependant si cela reste valable dans le cas des tables partitionnées et / ou des tables FDW et / ou des travailleurs parallèles - et si PostgreSQL peut tenir cette promesse dans les futures versions.
pbillen
Pour mémoire, je n'avais nullement l'intention de douter de la réponse acceptée. Je pensais seulement que c'était un bon ajout à la raison de l'existence et de l'utilisation d'indices en combinaison avec l'agrégation.
pbillen
1
Il est un bon ajout.
Erwin Brandstetter