Dans PostgreSQL, existe-t-il une fonction d'agrégation First () de type sécurisé?

21

Réécriture complète de la question

Je recherche une fonction d'agrégation First ().

Ici, j'ai trouvé quelque chose qui fonctionne presque:

CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

Le problème est que lorsqu'une colonne varchar (n) passe par la première fonction (), elle est convertie en varchar simple (sans taille). En essayant de renvoyer la requête dans une fonction en tant qu'élément RETURNS SETOF, j'obtiens l'erreur suivante:

ERREUR: la structure de la requête ne correspond pas au type de résultat de la fonction ) ligne 31 à RETURN QUERY

Dans la même page wiki, il y a un lien vers une version C de la fonction qui remplacerait ce qui précède. Je ne sais pas comment l'installer, mais je me demande si cette version pourrait résoudre mon problème.

Pendant ce temps, existe-t-il un moyen de modifier la fonction ci-dessus afin qu'elle renvoie exactement le même type de la colonne d'entrée?

Alexandre Neto
la source

Réponses:

18

DISTINCT ON()

Juste comme note latérale, c'est précisément ce qui DISTINCT ON()fait (à ne pas confondre avec DISTINCT)

SELECT DISTINCT ON ( expression [, ...] ) conserve uniquement la première ligne de chaque ensemble de lignes où les expressions données sont égales . Les DISTINCT ONexpressions sont interprétées en utilisant les mêmes règles que pour ORDER BY(voir ci-dessus). Notez que la "première ligne" de chaque ensemble est imprévisible, sauf si elle ORDER BYest utilisée pour garantir que la ligne souhaitée apparaît en premier. Par exemple

Donc, si vous deviez écrire,

SELECT myFirstAgg(z)
FROM foo
GROUP BY x,y;

C'est effectivement

SELECT DISTINCT ON(x,y) z
FROM foo;
-- ORDER BY z;

En cela, il faut le premier z. Il y a deux différences importantes,

  1. Vous pouvez également sélectionner d'autres colonnes sans frais d'agrégation supplémentaire.

    SELECT DISTINCT ON(x,y) z, k, r, t, v
    FROM foo;
    -- ORDER BY z, k, r, t, v;
  2. Parce qu'il n'y en a pas, GROUP BYvous ne pouvez pas utiliser de (vrais) agrégats avec.

    CREATE TABLE foo AS
    SELECT * FROM ( VALUES
      (1,2,3),
      (1,2,4),
      (1,2,5)
    ) AS t(x,y,z);
    
    SELECT DISTINCT ON (x,y) z, sum(z)
    FROM foo;
    
    -- fails, as you should expect.
    SELECT DISTINCT ON (x,y) z, sum(z)
    FROM foo;
    
    -- would not otherwise fail.
    SELECT myFirstAgg(z), sum(z)
    FROM foo
    GROUP BY x,y;

N'oublie pas ORDER BY

Aussi, même si je ne l'ai pas mis en gras, je vais maintenant

Notez que la "première ligne" de chaque ensemble est imprévisible, sauf si ORDER BY est utilisé pour garantir que la ligne souhaitée apparaît en premier. Par exemple

Utilisez toujours un ORDER BYavecDISTINCT ON

Utilisation d'une fonction d'agrégation d'ensemble ordonné

J'imagine que beaucoup de gens recherchent first_value, vendus par commande Set de fonctions d' agrégation . Je voulais juste jeter ça là-bas. Cela ressemblerait à ceci, si la fonction existait:

SELECT a, b, first_value() WITHIN GROUP (ORDER BY z)    
FROM foo
GROUP BY a,b;

Mais, hélas, vous pouvez le faire.

SELECT a, b, percentile_disc(0) WITHIN GROUP (ORDER BY z)   
FROM foo
GROUP BY a,b;
Evan Carroll
la source
1
Le problème avec cette réponse est qu'elle ne fonctionne que si vous voulez UN agrégat dans votre liste de sélection, ce qui n'est pas impliqué par la question. Si, par exemple, vous souhaitez sélectionner dans une table et trouver plusieurs premières valeurs ordonnées, DISTINCT ONcela ne fonctionnera pas dans ce cas. Ce n'est pas une fonction d'agrégation, vous filtrez réellement les données et vous ne pouvez donc le faire qu'une seule fois.
DB140141
6

Oui, j'ai découvert un moyen simple avec votre cas en utilisant certaines fonctionnalités de PostgreSQL 9.4+

Voyons cet exemple:

select  (array_agg(val ORDER BY i))[1] as first_value_orderby_i,
    (array_agg(val ORDER BY i DESC))[1] as last_value_orderby_i,
    (array_agg(val))[1] as last_value_all,
    (array_agg(val))[array_length(array_agg(val),1)] as last_value_all
   FROM (
        SELECT i, random() as val
        FROM generate_series(1,100) s(i)
        ORDER BY random()
    ) tmp_tbl

J'espère que cela vous aidera dans votre cas.

Mabu Kloesen
la source
Le problème avec cette solution est qu'elle ne fonctionne pas avec DOMAINles types de données ou d'autres petites exceptions. Il est également beaucoup plus complexe et prend du temps, constituant un tableau de l'ensemble des données. La solution simple serait de créer un agrégat personnalisé, mais jusqu'à présent, je n'ai pas trouvé la solution idéale même avec cela. Les fonctions de fenêtre sont également mauvaises, car elles ne peuvent pas être utilisées de la même manière que vous pourriez utiliser des agrégats (avec des instructions FILTER ou dans CROSS JOIN LATERAL)
AlexanderMP
5

Pas une réponse directe à votre question, mais vous devriez essayer la first_valuefonction fenêtre. Cela fonctionne comme ceci:

CREATE TABLE test (
    id SERIAL NOT NULL PRIMARY KEY,
    cat TEXT,
    value VARCHAR(2)
    date TIMESTAMP WITH TIME ZONE

);

Ensuite, si vous voulez le premier élément de chaque cat(catégorie), vous interrogerez comme ça:

SELECT
    cat,
    first_value(date) OVER (PARTITION BY cat ORDER BY date)
FROM
    test;

ou:

SELECT
    cat,
    first_value(date) OVER w
FROM
    test
WINDOW w AS (PARTITION BY cat ORDER BY date);
Ghislain Leveque
la source
Désolé, je ne pense pas que cela s'applique à mon cas d'utilisation. First_value n'est pas une fonction d'agrégation, affichant tous les enregistrements de avec une certaine valeur commune (votre exemple de chat) qui est évaluée comme étant la première selon un ordre (votre exemple de date). Mon besoin est différent. J'ai besoin, dans la même sélection, d'agréger plusieurs colonnes en choisissant la première valeur non nulle. En d'autres termes, il doit générer un seul enregistrement pour chacune des combinaisons de valeurs dans GROUP BY.
Alexandre Neto
2
Ce qui précède peut être fait au travail en lançant distinctes dans le mélange: select distinct x, first_value(y) over (partition by x), first_value(z) over (partition by x) from .... Probablement inefficace mais suffisant pour que je puisse continuer le prototypage. Certainement quelque chose à revisiter cependant!
Max Murphy