¿Cómo devolver el resultado de un SELECT dentro de una función en PostgreSQL?

106

Tengo esta función en PostgreSQL, pero no sé cómo devolver el resultado de la consulta:

CREATE OR REPLACE FUNCTION wordFrequency(maxTokens INTEGER)
  RETURNS SETOF RECORD AS
$$
BEGIN
    SELECT text, count(*), 100 / maxTokens * count(*)
    FROM (
        SELECT text
    FROM token
    WHERE chartype = 'ALPHABETIC'
    LIMIT maxTokens
    ) as tokens
    GROUP BY text
    ORDER BY count DESC
END
$$
LANGUAGE plpgsql;

Pero no sé cómo devolver el resultado de la consulta dentro de la función PostgreSQL.

Descubrí que el tipo de devolución debería ser SETOF RECORD, ¿verdad? Pero el comando de retorno no es correcto.

¿Cuál es la forma correcta de hacer esto?

Renato Dinhani
fuente
¿Por qué los cuentas? ¿Tiene tokens duplicados en su TABLA de tokens? Además: agregue la definición de la tabla a su pregunta.
wildplasser
1
¿Es esta toda tu función? Si no tiene otras declaraciones en la función, simplemente debe hacerlo LANGUAGE SQL.
jpmc26

Respuestas:

134

Utilizar RETURN QUERY:

CREATE OR REPLACE FUNCTION word_frequency(_max_tokens int)
  RETURNS TABLE (txt   text   -- also visible as OUT parameter inside function
               , cnt   bigint
               , ratio bigint) AS
$func$
BEGIN
   RETURN QUERY
   SELECT t.txt
        , count(*) AS cnt                 -- column alias only visible inside
        , (count(*) * 100) / _max_tokens  -- I added brackets
   FROM  (
      SELECT t.txt
      FROM   token t
      WHERE  t.chartype = 'ALPHABETIC'
      LIMIT  _max_tokens
      ) t
   GROUP  BY t.txt
   ORDER  BY cnt DESC;                    -- potential ambiguity 
END
$func$  LANGUAGE plpgsql;

Llamada:

SELECT * FROM word_frequency(123);

Explicación:

  • Es mucho más práctico definir explícitamente el tipo de retorno que simplemente declararlo como registro. De esta manera, no es necesario que proporcione una lista de definiciones de columna con cada llamada a función. RETURNS TABLEes una forma de hacerlo. Hay otros. Los tipos de datos de los OUTparámetros deben coincidir exactamente con lo que devuelve la consulta.

  • Elija los nombres de los OUTparámetros con cuidado. Son visibles en el cuerpo funcional casi en cualquier lugar. Columnas de calificación de tabla del mismo nombre para evitar conflictos o resultados inesperados. Hice eso para todas las columnas de mi ejemplo.

    Pero tenga en cuenta el posible conflicto de nombres entre el OUTparámetro cnty el alias de la columna del mismo nombre. En este caso particular ( RETURN QUERY SELECT ...) Postgres usa el alias de la columna sobre el OUTparámetro de cualquier manera. Sin embargo, esto puede ser ambiguo en otros contextos. Hay varias formas de evitar confusiones:

    1. Use la posición ordinal del elemento de la lista SELECT: ORDER BY 2 DESC. Ejemplo:
    2. Repite la expresión ORDER BY count(*).
    3. (No se aplica aquí). Establezca el parámetro de configuración plpgsql.variable_conflicto utilice el comando especial #variable_conflict error | use_variable | use_columnen la función. Ver:
  • No utilice "texto" o "contar" como nombres de columna. Ambos son legales para usar en Postgres, pero "contar" es una palabra reservada en SQL estándar y un nombre de función básica y "texto" es un tipo de datos básico. Puede dar lugar a errores confusos. Yo uso txty cnten mis ejemplos.

  • Se agregó un ;error de sintaxis faltante y se corrigió en el encabezado. (_max_tokens int), no (int maxTokens): escriba después del nombre .

  • Mientras trabaja con la división de enteros, es mejor multiplicar primero y dividir después, para minimizar el error de redondeo. Aún mejor: trabaje con numeric(o un tipo de punto flotante). Vea abajo.

Alternativa

Esto es lo que yo creo que la consulta en realidad debe ser similar (el cálculo de una participación relativa por ficha ):

CREATE OR REPLACE FUNCTION word_frequency(_max_tokens int)
  RETURNS TABLE (txt            text
               , abs_cnt        bigint
               , relative_share numeric) AS
$func$
BEGIN
   RETURN QUERY
   SELECT t.txt, t.cnt
        , round((t.cnt * 100) / (sum(t.cnt) OVER ()), 2)  -- AS relative_share
   FROM  (
      SELECT t.txt, count(*) AS cnt
      FROM   token t
      WHERE  t.chartype = 'ALPHABETIC'
      GROUP  BY t.txt
      ORDER  BY cnt DESC
      LIMIT  _max_tokens
      ) t
   ORDER  BY t.cnt DESC;
END
$func$  LANGUAGE plpgsql;

La expresión sum(t.cnt) OVER ()es una función de ventana . Usted podría utilizar un CTE en lugar de la subconsulta - bonito, pero una sub consulta suele ser más barato en casos sencillos como éste.

No se requiere una RETURNdeclaración explícita final (pero se permite) cuando se trabaja con OUTparámetros o RETURNS TABLE(que hace un uso implícito de OUTparámetros).

round()con dos parámetros solo funciona para numerictipos. count()en la subconsulta produce un bigintresultado y un sum()over this bigintproduce un numericresultado, por lo que tratamos con un numericnúmero automáticamente y todo encaja en su lugar.

Erwin Brandstetter
fuente
Muchas gracias a tu respuesta y correcciones. Está funcionando bien ahora (solo cambié el tipo de relación a numérico).
Renato Dinhani
@ RenatoDinhaniConceição ¡Genial! Agregué una versión que puede o no responder a una pregunta adicional que en realidad no ha hecho. ;)
Erwin Brandstetter
Bien, lo único es que creo que necesitas un RETURN;antes de eso END;, al menos yo lo hice, pero estoy haciendo UNION, así que no estoy seguro de si eso lo hace diferente.
yekta
@yekta: agregué información sobre el rol de RETURN. Se corrigió un error no relacionado y se agregaron algunas mejoras mientras lo hacía.
Erwin Brandstetter
1
¿Cuál es la forma de hacer esto cuando no desea restringir lo que está en Return TABLE ()? Es decir, ¿TABLA DE VUELTA (*)?
Nick