Comment obtenir l'agrégat d'une fonction de fenêtre dans Postgres?

11

J'ai une table contenant deux colonnes de permutations / combinaisons de tableaux entiers, et une troisième colonne contenant une valeur, comme ceci:

CREATE TABLE foo
(
  perm integer[] NOT NULL,
  combo integer[] NOT NULL,
  value numeric NOT NULL DEFAULT 0
);
INSERT INTO foo
VALUES
( '{3,1,2}', '{1,2,3}', '1.1400' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0.9280' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,2,1}', '{1,2,3}', '0' ),
( '{3,2,1}', '{1,2,3}', '0.8000' )

Je veux connaître la moyenne et l'écart type pour chaque permutation, ainsi que pour chaque combinaison. Je peux le faire avec cette requête:

SELECT
  f1.perm,
  f2.combo,
  f1.perm_average_value,
  f2.combo_average_value,
  f1.perm_stddev,
  f2.combo_stddev,
  f1.perm_count,
  f2.combo_count
FROM
(
  SELECT
    perm,
    combo,
    avg( value ) AS perm_average_value,
    stddev_pop( value ) AS perm_stddev,
    count( * ) AS perm_count
  FROM foo
  GROUP BY perm, combo
) AS f1
JOIN
(
  SELECT
    combo,
    avg( value ) AS combo_average_value,
    stddev_pop( value ) AS combo_stddev,
    count( * ) AS combo_count
  FROM foo
  GROUP BY combo
) AS f2 ON ( f1.combo = f2.combo );

Cependant, cette requête peut devenir assez lente lorsque j'ai beaucoup de données, car la table "foo" (qui, en réalité, se compose de 14 partitions chacune avec environ 4 millions de lignes) doit être analysée deux fois.

Récemment, j'ai appris que Postgres prend en charge les «fonctions de fenêtre», qui sont essentiellement comme un GROUP BY pour une colonne particulière. J'ai modifié ma requête pour les utiliser comme ceci:

SELECT
  perm,
  combo,
  avg( value ) as perm_average_value,
  avg( avg( value ) ) over w_combo AS combo_average_value,
  stddev_pop( value ) as perm_stddev,
  stddev_pop( avg( value ) ) over w_combo as combo_stddev,
  count( * ) as perm_count,
  sum( count( * ) ) over w_combo AS combo_count
FROM foo
GROUP BY perm, combo
WINDOW w_combo AS ( PARTITION BY combo );

Bien que cela fonctionne pour la colonne "combo_count", les colonnes "combo_average_value" et "combo_stddev" ne sont plus précises. Il semble que la moyenne soit prise pour chaque permutation, puis en moyenne une deuxième fois pour chaque combinaison, ce qui est incorrect.

Comment puis-je réparer cela? Les fonctions de fenêtre peuvent-elles même être utilisées comme optimisation ici?

Scott Small
la source
En supposant la version actuelle Postgres 9.2? Les fonctions de fenêtre sont fournies avec 8.4.
Erwin Brandstetter
Désolé, j'ai oublié de préciser. Oui, j'utilise la dernière version, Postgres 9.2.4.
Scott Small

Réponses:

9

Vous pouvez avoir des fonctions de fenêtre sur le résultat des fonctions d'agrégation dans un seul niveau de requête.

Tout cela fonctionnerait bien après quelques modifications - sauf qu'il échoue pour l'écart-type sur le principe mathématique . Les calculs impliqués ne sont pas linéaires, vous ne pouvez donc pas simplement combiner les écarts-types des sous-populations.

SELECT perm
      ,combo
      ,avg(value)                 AS perm_average_value
      ,sum(avg(value) * count(*)) OVER w_combo /
       sum(count(*)) OVER w_combo AS combo_average_value
      ,stddev_pop(value)          AS perm_stddev
      ,0                          AS combo_stddev  -- doesn't work!
      ,count(*)                   AS perm_count
      ,sum(count(*)) OVER w_combo AS combo_count
FROM   foo
GROUP  BY perm, combo
WINDOW w_combo  AS (PARTITION BY combo);

Car combo_average_valuetu aurais besoin de cette expression

sum(avg(value) * count(*)) OVER w_combo / sum(count(*)) OVER w_combo

Puisque vous avez besoin d'une moyenne pondérée . (La moyenne d'un groupe de 10 membres pèse plus que la moyenne d'un groupe de seulement 2 membres!)

Cela fonctionne :

SELECT DISTINCT ON (perm, combo)
       perm
      ,combo
      ,avg(value)        OVER wpc AS perm_average_value
      ,avg(value)        OVER wc  AS combo_average_value
      ,stddev_pop(value) OVER wpc AS perm_stddev
      ,stddev_pop(value) OVER wc  AS combo_stddev
      ,count(*)          OVER wpc AS perm_count
      ,count(*)          OVER wc  AS combo_count
FROM   foo
WINDOW wc  AS (PARTITION BY combo)
      ,wpc AS (PARTITION BY perm, combo);

J'utilise ici deux fenêtres différentes et je réduis les lignes avec DISTINCTlesquelles est appliqué même après les fonctions de fenêtre.

Mais je doute sérieusement que ce sera plus rapide que votre requête d'origine. Je suis presque sûr que non.

Meilleures performances avec une disposition de table modifiée

Les tableaux ont une surcharge de 24 octets (légères variations selon le type). En outre, vous semblez avoir quelques éléments par tableau et de nombreuses répétitions. Pour une table énorme comme la vôtre, il serait utile de normaliser le schéma. Exemple de disposition:

CREATE TABLE combo ( 
  combo_id serial PRIMARY KEY
 ,combo    int[] NOT NULL
);

CREATE TABLE perm ( 
  perm_id  serial PRIMARY KEY
 ,perm     int[] NOT NULL
);

CREATE TABLE value (
  perm_id  int REFERENCES perm(perm_id)
 ,combo_id int REFERENCES combo(combo_id)
 ,value numeric NOT NULL DEFAULT 0
);

Si vous n'avez pas besoin d'intégrité référentielle, vous pouvez omettre les contraintes de clé étrangère.

La connexion à combo_idpourrait également être placée dans le tableau perm, mais dans ce scénario, je la stockerais (légèrement dénormalisée) valuepour de meilleures performances.

Cela entraînerait une taille de ligne de 32 octets (en-tête de tuple + remplissage: 24 octets, 2 x int (8 octets), pas de remplissage), plus la taille inconnue de votre numericcolonne. (Si vous n'avez pas besoin d'une précision extrême, une double precisionou même une realcolonne peut aussi le faire.)

Plus d'informations sur le stockage physique dans cette réponse connexe sur SO ou ici:
Configuration de PostgreSQL pour les performances de lecture

Quoi qu'il en soit, ce n'est qu'une fraction de ce que vous avez maintenant et rendrait votre requête beaucoup plus rapide par sa seule taille. Le regroupement et le tri sur des entiers simples sont également beaucoup plus rapides.

Vous devez d' abord agréger dans une sous-requête, puis vous joindre à permet combopour de meilleures performances.

Erwin Brandstetter
la source
Merci pour la réponse claire et concise. Vous avez raison, il semblerait qu'il n'y ait aucun moyen d'obtenir l'écart type d'une population de sous-ensembles de cette manière. Cela étant dit, j'aime la simplicité de votre solution. L'élimination du GROUP BY rend la requête résultante beaucoup plus lisible. Malheureusement, comme vous le soupçonniez, la performance est inférieure à la normale. J'ai dû tuer la requête après avoir exécuté pendant plus de 30 minutes.
Scott Small
@ScottSmall: Vous pourriez faire quelque chose pour les performances ... voir la mise à jour pour répondre.
Erwin Brandstetter
Afin de simplifier ma question, j'ai supprimé les colonnes du footableau qui n'étaient pas pertinentes. En réalité, il y a plusieurs autres colonnes qui ne sont pas utilisées par cette requête, donc je ne suis pas convaincu que la normalisation des permutations et des combinaisons fournirait une augmentation de vitesse significative, pour ce cas d'utilisation particulier.
Scott Small
De plus, les valeurs entières composant chaque permutation et combinaison proviennent d'une autre table de la base de données. La pré-génération de ces données coûte cher en calcul. La longueur maximale d'un perm / combo est de 5, mais 5Pn et 5Cn deviennent assez grands pour de grandes valeurs de n (actuellement autour de 1000, mais croissent quotidiennement) ... de toute façon, l'optimisation est la question d'un autre jour. Merci encore pour toute votre aide Erwin.
Scott Small