Agrupación o ventana


13

Tengo una situación que creo que se puede resolver utilizando la función de ventana, pero no estoy seguro.

Imagina la siguiente tabla

CREATE TABLE tmp
  ( date timestamp,        
    id_type integer
  ) ;

INSERT INTO tmp 
    ( date, id_type )
VALUES
    ( '2017-01-10 07:19:21.0', 3 ),
    ( '2017-01-10 07:19:22.0', 3 ),
    ( '2017-01-10 07:19:23.1', 3 ),
    ( '2017-01-10 07:19:24.1', 3 ),
    ( '2017-01-10 07:19:25.0', 3 ),
    ( '2017-01-10 07:19:26.0', 5 ),
    ( '2017-01-10 07:19:27.1', 3 ),
    ( '2017-01-10 07:19:28.0', 5 ),
    ( '2017-01-10 07:19:29.0', 5 ),
    ( '2017-01-10 07:19:30.1', 3 ),
    ( '2017-01-10 07:19:31.0', 5 ),
    ( '2017-01-10 07:19:32.0', 3 ),
    ( '2017-01-10 07:19:33.1', 5 ),
    ( '2017-01-10 07:19:35.0', 5 ),
    ( '2017-01-10 07:19:36.1', 5 ),
    ( '2017-01-10 07:19:37.1', 5 )
  ;

Me gustaría tener un nuevo grupo en cada cambio en la columna id_type. Por ejemplo, primer grupo de 7:19:21 a 7:19:25, segundo inicio y finalización a las 7:19:26, y así sucesivamente.
Después de que funcione, quiero incluir más criterios para definir grupos.

En este momento, utilizando la consulta a continuación ...

SELECT distinct 
    min(min(date)) over w as begin, 
    max(max(date)) over w as end,   
    id_type
from tmp
GROUP BY id_type
WINDOW w as (PARTITION BY id_type)
order by  begin;

Obtengo el siguiente resultado:

begin                   end                     id_type
2017-01-10 07:19:21.0   2017-01-10 07:19:32.0   3
2017-01-10 07:19:26.0   2017-01-10 07:19:37.1   5

Si bien me gustaría:

begin                   end                     id_type
2017-01-10 07:19:21.0   2017-01-10 07:19:25.0   3
2017-01-10 07:19:26.0   2017-01-10 07:19:26.0   5
2017-01-10 07:19:27.1   2017-01-10 07:19:27.1   3
2017-01-10 07:19:28.0   2017-01-10 07:19:29.0   5
2017-01-10 07:19:30.1   2017-01-10 07:19:30.1   3
2017-01-10 07:19:31.0   2017-01-10 07:19:31.0   5
2017-01-10 07:19:32.0   2017-01-10 07:19:32.0   3
2017-01-10 07:19:33.1   2017-01-10 07:19:37.1   5

Después de resolver este primer paso, agregaré más columnas para usar como reglas para dividir grupos, y estos otros serán anulables.

Versión Postgres: 8.4 (Tenemos Postgres con Postgis, por lo que no es fácil de actualizar. Las funciones de Postgis cambian de nombre y hay otros problemas, pero espero que ya estemos reescribiendo todo y la nueva versión use una versión más nueva 9.X con postgis 2.x)


Respuestas:


4

Por algunos puntos

  • No llame a una mesa no temporal tmp que se vuelve confusa.
  • No use texto para marcas de tiempo (lo está haciendo en su ejemplo, podemos decirlo porque la marca de tiempo no se truncó y tiene .0 )
  • No llame a un campo que tenga tiempo date. Si tiene fecha y hora, es una marca de tiempo (y la almacena como una)

Es mejor usar una función de ventana.

SELECT id_type, grp, min(date), max(date)
FROM (
  SELECT date, id_type, count(is_reset) OVER (ORDER BY date) AS grp
  FROM (
    SELECT date, id_type, CASE WHEN lag(id_type) OVER (ORDER BY date) <> id_type THEN 1 END AS is_reset
    FROM tmp
  ) AS t
) AS g
GROUP BY id_type, grp
ORDER BY min(date);

Salidas

 id_type | grp |          min          |          max          
---------+-----+-----------------------+-----------------------
       3 |   0 | 2017-01-10 07:19:21.0 | 2017-01-10 07:19:25.0
       5 |   1 | 2017-01-10 07:19:26.0 | 2017-01-10 07:19:26.0
       3 |   2 | 2017-01-10 07:19:27.1 | 2017-01-10 07:19:27.1
       5 |   3 | 2017-01-10 07:19:28.0 | 2017-01-10 07:19:29.0
       3 |   4 | 2017-01-10 07:19:30.1 | 2017-01-10 07:19:30.1
       5 |   5 | 2017-01-10 07:19:31.0 | 2017-01-10 07:19:31.0
       3 |   6 | 2017-01-10 07:19:32.0 | 2017-01-10 07:19:32.0
       5 |   7 | 2017-01-10 07:19:33.1 | 2017-01-10 07:19:37.1
(8 rows)

Explicación

Primero necesitamos resets. Los generamos con lag()

SELECT date, id_type, CASE WHEN lag(id_type) OVER (ORDER BY date) <> id_type THEN 1 END AS is_reset
FROM tmp
ORDER BY date;

         date          | id_type | is_reset 
-----------------------+---------+----------
 2017-01-10 07:19:21.0 |       3 |         
 2017-01-10 07:19:22.0 |       3 |         
 2017-01-10 07:19:23.1 |       3 |         
 2017-01-10 07:19:24.1 |       3 |         
 2017-01-10 07:19:25.0 |       3 |         
 2017-01-10 07:19:26.0 |       5 |        1
 2017-01-10 07:19:27.1 |       3 |        1
 2017-01-10 07:19:28.0 |       5 |        1
 2017-01-10 07:19:29.0 |       5 |         
 2017-01-10 07:19:30.1 |       3 |        1
 2017-01-10 07:19:31.0 |       5 |        1
 2017-01-10 07:19:32.0 |       3 |        1
 2017-01-10 07:19:33.1 |       5 |        1
 2017-01-10 07:19:35.0 |       5 |         
 2017-01-10 07:19:36.1 |       5 |         
 2017-01-10 07:19:37.1 |       5 |         
(16 rows)

Luego contamos para obtener grupos.

SELECT date, id_type, count(is_reset) OVER (ORDER BY date) AS grp
FROM (
  SELECT date, id_type, CASE WHEN lag(id_type) OVER (ORDER BY date) <> id_type THEN 1 END AS is_reset
  FROM tmp
  ORDER BY date
) AS t
ORDER BY date

         date          | id_type | grp 
-----------------------+---------+-----
 2017-01-10 07:19:21.0 |       3 |   0
 2017-01-10 07:19:22.0 |       3 |   0
 2017-01-10 07:19:23.1 |       3 |   0
 2017-01-10 07:19:24.1 |       3 |   0
 2017-01-10 07:19:25.0 |       3 |   0
 2017-01-10 07:19:26.0 |       5 |   1
 2017-01-10 07:19:27.1 |       3 |   2
 2017-01-10 07:19:28.0 |       5 |   3
 2017-01-10 07:19:29.0 |       5 |   3
 2017-01-10 07:19:30.1 |       3 |   4
 2017-01-10 07:19:31.0 |       5 |   5
 2017-01-10 07:19:32.0 |       3 |   6
 2017-01-10 07:19:33.1 |       5 |   7
 2017-01-10 07:19:35.0 |       5 |   7
 2017-01-10 07:19:36.1 |       5 |   7
 2017-01-10 07:19:37.1 |       5 |   7
(16 rows)

Luego envolvemos en una subselección GROUP BYy ORDERy seleccione el min max (rango)

SELECT id_type, grp, min(date), max(date)
FROM (
  .. stuff
) AS g
GROUP BY id_type, grp
ORDER BY min(date);

16

1. Funciones de ventana más subconsultas

Cuente los pasos para formar grupos, similares a la idea de Evan , con modificaciones y arreglos:

SELECT id_type
     , min(date) AS begin
     , max(date) AS end
     , count(*)  AS row_ct  -- optional addition
FROM  (
   SELECT date, id_type, count(step OR NULL) OVER (ORDER BY date) AS grp
   FROM  (
      SELECT date, id_type
           , lag(id_type, 1, id_type) OVER (ORDER BY date) <> id_type AS step
      FROM   tmp
      ) sub1
   ) sub2
GROUP  BY id_type, grp
ORDER  BY min(date);

Esto supone que las columnas involucradas son NOT NULL. De lo contrario, necesitas hacer más.

También suponiendo dateque se defina UNIQUE, de lo contrario, debe agregar un desempate a las ORDER BYcláusulas para obtener resultados deterministas. Me gusta:ORDER BY date, id .

Explicación detallada (respuesta a una pregunta muy similar):

Nota en particular:

  • En casos relacionados, lag()con 3 parámetros puede ser esencial para cubrir el caso de la esquina de la primera (o última) fila con elegancia. (El tercer parámetro se usa por defecto si no hay una fila anterior (siguiente).

    lag(id_type, 1, id_type) OVER ()

    Como solo estamos interesados ​​en un cambio real de id_type( TRUE), no importa en este caso particular. NULLy FALSEambos no cuentan como step.

  • count(step OR NULL) OVER (ORDER BY date)es la sintaxis más corta que también funciona en Postgres 9.3 o anterior. count()solo cuenta valores no nulos ...

    En Postgres modernos, la sintaxis equivalente más limpia sería:

    count(step) FILTER (WHERE step) OVER (ORDER BY date)

    Detalles:

2. Resta dos funciones de ventana, una subconsulta

Similar a la idea de Erik con modificaciones:

SELECT min(date) AS begin
     , max(date) AS end
     , id_type
FROM  (
   SELECT date, id_type
        , row_number() OVER (ORDER BY date)
        - row_number() OVER (PARTITION BY id_type ORDER BY date) AS grp
   FROM   tmp
   ) sub
GROUP  BY id_type, grp
ORDER  BY min(date);

Si datese define UNIQUE, como mencioné anteriormente (nunca aclaraste), no dense_rank()tendría sentido, ya que el resultado es el mismo que para row_number()y este último es sustancialmente más barato.

Si nodate está definido (y no sabemos que los únicos duplicados están activados ), todas estas consultas no tienen sentido, ya que el resultado es arbitrario.UNIQUE(date, id_type)

Además, una subconsulta suele ser más barata que un CTE en Postgres. Solo use CTE cuando los necesite .

Respuestas relacionadas con más explicaciones:

En casos relacionados donde ya tenemos un número corriente en la tabla, podemos conformarnos con una sola función de ventana:

3. Máximo rendimiento con la función plpgsql

Como esta pregunta se ha vuelto inesperadamente popular, agregaré otra solución para demostrar el máximo rendimiento.

SQL tiene muchas herramientas sofisticadas para crear soluciones con sintaxis corta y elegante. Pero un lenguaje declarativo tiene sus límites para requisitos más complejos que involucran elementos de procedimiento.

Una función de procedimiento del lado del servidor es más rápida para esto que cualquier cosa publicada hasta ahora porque solo necesita un solo escaneo secuencial sobre la tabla y una sola operación de clasificación . Si hay disponible un índice de ajuste, incluso un solo escaneo de solo índice.

CREATE OR REPLACE FUNCTION f_tmp_groups()
  RETURNS TABLE (id_type int, grp_begin timestamp, grp_end timestamp) AS
$func$
DECLARE
   _row  tmp;                       -- use table type for row variable
BEGIN
   FOR _row IN
      TABLE tmp ORDER BY date       -- add more columns to make order deterministic
   LOOP
      CASE _row.id_type = id_type 
      WHEN TRUE THEN                -- same group continues
         grp_end := _row.date;      -- remember last date so far
      WHEN FALSE THEN               -- next group starts
         RETURN NEXT;               -- return result for last group
         id_type   := _row.id_type;
         grp_begin := _row.date;
         grp_end   := _row.date;
      ELSE                          -- NULL for 1st row
         id_type   := _row.id_type; -- remember row data for starters
         grp_begin := _row.date;
         grp_end   := _row.date;
      END CASE;
   END LOOP;

   RETURN NEXT;                     -- return last result row      
END
$func$ LANGUAGE plpgsql;

Llamada:

SELECT * FROM f_tmp_groups();

Prueba con:

EXPLAIN (ANALYZE, TIMING OFF)  -- to focus on total performance
SELECT * FROM  f_tmp_groups();

Puede hacer que la función sea genérica con tipos polimórficos y pasar el tipo de tabla y los nombres de columna. Detalles:

Si no desea o no puede persistir una función para esto, incluso pagaría crear una función temporal sobre la marcha. Cuesta unos pocos ms.


dbfiddle para Postgres 9.6, comparando el rendimiento de los tres. Basado enel caso de prueba de Jack, modificado.

dbfiddle para Postgres 8.4, donde las diferencias de rendimiento son aún mayores.


Lea esto varias veces, aún no estoy seguro de lo que está hablando con el retraso de tres argumentos o cuándo tendría que usar count(x or null)o incluso qué está haciendo allí. Tal vez podría mostrar algunos ejemplos en los que se requiere, ya que no se requiere aquí. Y, ¿cuál sería el requisito clave para cubrir esos casos de esquina? Por cierto, cambié mi voto a favor solo por el ejemplo pl / pgsql. Eso es realmente genial. (Pero, en general, estoy en contra de las respuestas que resumen otras respuestas o cubren casos de esquina, aunque odio decir que este es un caso de esquina porque no lo entiendo).
Evan Carroll

Las pondría en dos preguntas separadas con respuesta propia porque estoy seguro de que no soy el único que se pregunta qué count(x or null)hace. Estaré encantado de hacerle ambas preguntas si lo prefiere.
Evan Carroll


7

Puede hacer esto como una simple sustracción de ROW_NUMBER()operaciones (o si sus fechas no son únicas, aunque todavía son únicas por id_type, entonces puede usar DENSE_RANK()en su lugar, aunque será una consulta más costosa):

WITH IdTypes AS (
   SELECT
      date,
      id_type,
      Row_Number() OVER (ORDER BY date)
         - Row_Number() OVER (PARTITION BY id_type ORDER BY date)
         AS Seq
   FROM
      tmp
)
SELECT
   Min(date) AS begin,
   Max(date) AS end,
   id_type
FROM IdTypes
GROUP BY id_type, Seq
ORDER BY begin
;

Vea este trabajo en DB Fiddle (o vea la versión DENSE_RANK )

Resultado:

begin                  end                    id_type
---------------------  ---------------------  -------
2017-01-10 07:19:21    2017-01-10 07:19:25    3
2017-01-10 07:19:26    2017-01-10 07:19:26    5
2017-01-10 07:19:27.1  2017-01-10 07:19:27.1  3
2017-01-10 07:19:28    2017-01-10 07:19:29    5
2017-01-10 07:19:30.1  2017-01-10 07:19:30.1  3
2017-01-10 07:19:31    2017-01-10 07:19:31    5
2017-01-10 07:19:32    2017-01-10 07:19:32    3
2017-01-10 07:19:33.1  2017-01-10 07:19:37.1  5

Lógicamente, puede pensar en esto como un simple DENSE_RANK()con un PREORDER BY, es decir, desea la DENSE_RANKtotalidad de los elementos clasificados y desea ordenarlos por fechas, solo tiene que lidiar con el molesto problema del hecho de que en cada cambio en la fecha, DENSE_RANKse incrementará. Lo haces usando la expresión que te mostré arriba. Imagínese si tuviera esta sintaxis: DENSE_RANK() OVER (PREORDER BY date, ORDER BY id_type)donde PREORDERse excluye del cálculo de clasificación y solo ORDER BYse cuenta.

Tenga en cuenta que es importante GROUP BYtanto para la Seqcolumna generada como para la id_typecolumna. SeqNO es único en sí mismo, puede haber superposiciones, también debe agruparlo id_type.

Para leer más sobre este tema:

Ese primer enlace le proporciona un código que puede usar si desea que la fecha de inicio o finalización sea la misma que la fecha de finalización / inicio del período anterior o siguiente (por lo que no hay espacios). Además de otras versiones que podrían ayudarlo en su consulta. Aunque deben traducirse de la sintaxis de SQL Server ...


6

En Postgres 8.4 puedes usar una función RECURSIVA .

Cómo lo hicieron

La función recursiva agrega un nivel a cada id_type diferente, seleccionando las fechas una por una en orden descendente.

       date           | id_type | lv
--------------------------------------
2017-01-10 07:19:21.0      3       8
2017-01-10 07:19:22.0      3       8
2017-01-10 07:19:23.1      3       8
2017-01-10 07:19:24.1      3       8
2017-01-10 07:19:25.0      3       8
2017-01-10 07:19:26.0      5       7
2017-01-10 07:19:27.1      3       6
2017-01-10 07:19:28.0      5       5
2017-01-10 07:19:29.0      5       5
2017-01-10 07:19:30.1      3       4
2017-01-10 07:19:31.0      5       3
2017-01-10 07:19:32.0      3       2
2017-01-10 07:19:33.1      5       1
2017-01-10 07:19:35.0      5       1
2017-01-10 07:19:36.1      5       1
2017-01-10 07:19:37.1      5       1

Luego use MAX (fecha), MIN (fecha) agrupando por nivel, id_type para obtener el resultado deseado.

with RECURSIVE rdates as 
(
    (select   date, id_type, 1 lv 
     from     yourTable
     order by date desc
     limit 1
    )
    union
    (select    d.date, d.id_type,
               case when r.id_type = d.id_type 
                    then r.lv 
                    else r.lv + 1 
               end lv    
    from       yourTable d
    inner join rdates r
    on         d.date < r.date
    order by   date desc
    limit      1)
)
select   min(date) StartDate,
         max(date) EndDate,
         id_type
from     rdates
group by lv, id_type
;

+---------------------+---------------------+---------+
| startdate           |       enddate       | id_type |
+---------------------+---------------------+---------+
| 10.01.2017 07:19:21 | 10.01.2017 07:19:25 |    3    |
| 10.01.2017 07:19:26 | 10.01.2017 07:19:26 |    5    |
| 10.01.2017 07:19:27 | 10.01.2017 07:19:27 |    3    |
| 10.01.2017 07:19:28 | 10.01.2017 07:19:29 |    5    |
| 10.01.2017 07:19:30 | 10.01.2017 07:19:30 |    3    |
| 10.01.2017 07:19:31 | 10.01.2017 07:19:31 |    5    |
| 10.01.2017 07:19:32 | 10.01.2017 07:19:32 |    3    |
| 10.01.2017 07:19:33 | 10.01.2017 07:19:37 |    5    |
+---------------------+---------------------+---------+

Compruébalo: http://rextester.com/WCOYFP6623


5

Aquí hay otro método, que es similar al de Evan y Erwin, ya que utiliza el LAG para determinar las islas. Se diferencia de esas soluciones en que usa solo un nivel de anidamiento, sin agrupamiento y considerablemente más funciones de ventana:

SELECT
  id_type,
  date AS begin,
  COALESCE(
    LEAD(prev_date) OVER (ORDER BY date ASC),
    last_date
  ) AS end
FROM
  (
    SELECT
      id_type,
      date,
      LAG(date) OVER (ORDER BY date ASC) AS prev_date,
      MAX(date) OVER () AS last_date,
      CASE id_type
        WHEN LAG(id_type) OVER (ORDER BY date ASC)
        THEN 0
        ELSE 1
      END AS is_start
    FROM
      tmp
  ) AS derived
WHERE
  is_start = 1
ORDER BY
  date ASC
;

La is_startcolumna calculada en SELECT anidado marca el comienzo de cada isla. Además, el SELECT anidado expone la fecha anterior de cada fila y la última fecha del conjunto de datos.

Para las filas que son el comienzo de sus respectivas islas, la fecha anterior es efectivamente la fecha de finalización de la isla anterior. Así es como lo usa el SELECT principal. Selecciona solo las filas que coinciden con la is_start = 1condición, y para cada fila devuelta muestra las propias de la fila datecomo beginy las siguientes prev_datecomo end. Como la última fila no tiene una fila siguiente, LEAD(prev_date)devuelve un valor nulo, para el cual la función COALESCE sustituye la última fecha del conjunto de datos.

Puedes jugar con esta solución en dbfiddle .

Al introducir columnas adicionales que identifiquen las islas, es probable que desee introducir una subcláusula PARTITION BY en la cláusula OVER de cada función de ventana. Por ejemplo, si desea detectar las islas dentro de los grupos definidos por a parent_id, la consulta anterior probablemente tendrá que verse así:

SELECT
  parent_id,
  id_type,
  date AS begin,
  COALESCE(
    LEAD(prev_date) OVER (PARTITION BY parent_id ORDER BY date ASC),
    last_date
  ) AS end
FROM
  (
    SELECT
      parent_id,
      id_type,
      date,
      LAG(date) OVER (PARTITION BY parent_id ORDER BY date ASC) AS prev_date,
      MAX(date) OVER (PARTITION BY parent_id) AS last_date,
      CASE id_type
        WHEN LAG(id_type) OVER (PARTITION BY parent_id ORDER BY date ASC)
        THEN 0
        ELSE 1
      END AS is_start
    FROM
      tmp
  ) AS derived
WHERE
  is_start = 1
ORDER BY
  date ASC
;

Y si decide optar por la solución de Erwin o Evan, creo que también será necesario agregar un cambio similar.


5

Más por interés académico que como una solución práctica, también puede lograr esto con un agregado definido por el usuario . Al igual que las otras soluciones, esto funcionará incluso en Postgres 8.4, pero como otros han comentado, actualice si puede.

El agregado se maneja nullcomo si fuera diferente foo_type, por lo que las ejecuciones de valores nulos recibirían lo mismo grp, eso puede o no ser lo que desea.

create function grp_sfunc(integer[],integer) returns integer[] language sql as $$
  select array[$1[1]+($1[2] is distinct from $2 or $1[3]=0)::integer,$2,1];
$$;
create function grp_finalfunc(integer[]) returns integer language sql as $$
  select $1[1];
$$;
create aggregate grp(integer)(
  sfunc = grp_sfunc
, stype = integer[]
, finalfunc = grp_finalfunc
, initcond = '{0,0,0}'
);
select min(foo_at) begin_at, max(foo_at) end_at, foo_type
from (select *, grp(foo_type) over (order by foo_at) from foo) z
group by grp, foo_type
order by 1;
begin_at | end_at | foo_type
: -------------------- | : -------------------- | -------:
2017-01-10 07:19:21 | 2017-01-10 07:19:25 | 3
2017-01-10 07:19:26 | 2017-01-10 07:19:26 | 5 5
2017-01-10 07: 19: 27.1 | 2017-01-10 07: 19: 27.1 | 3
2017-01-10 07:19:28 | 2017-01-10 07:19:29 | 5 5
2017-01-10 07: 19: 30.1 | 2017-01-10 07: 19: 30.1 | 3
2017-01-10 07:19:31 | 2017-01-10 07:19:31 | 5 5
2017-01-10 07:19:32 | 2017-01-10 07:19:32 | 3
2017-01-10 07: 19: 33.1 | 2017-01-10 07: 19: 37.1 | 5 5

dbfiddle aquí


4

Esto se puede hacer RECURSIVE CTEpara pasar el "tiempo de inicio" de una fila a la siguiente, y algunos preparativos adicionales (conveniencia).

Esta consulta devuelve el resultado que desea:

WITH RECURSIVE q AS
(
    SELECT
        id_type,
        "date",
        /* We compute next id_type for convenience, plus row_number */
        row_number()  OVER (w) AS rn,
        lead(id_type) OVER (w) AS next_id_type
    FROM
        t
    WINDOW
        w AS (ORDER BY "date") 
)

después de la preparación ... parte recursiva

, rec AS 
(
    /* Anchor */
    SELECT
        q.rn,
        q."date" AS "begin",
        /* When next_id_type is different from Look also at **next** row to find out whether we need to mark an end */
        case when q.id_type is distinct from q.next_id_type then q."date" END AS "end",
        q.id_type
    FROM
        q
    WHERE
        rn = 1

    UNION ALL

    /* Loop */
    SELECT
        q.rn,
        /* We keep copying 'begin' from one row to the next while type doesn't change */
        case when q.id_type = rec.id_type then rec.begin else q."date" end AS "begin",
        case when q.id_type is distinct from q.next_id_type then q."date" end AS "end",
        q.id_type
    FROM
        rec
        JOIN q ON q.rn = rec.rn+1
)
-- We filter the rows where "end" is not null, and project only needed columns
SELECT
    "begin", "end", id_type
FROM
    rec
WHERE
    "end" is not null ;

Puede verificar esto en http://rextester.com/POYM83542

Este método no escala bien. Para una tabla de filas 8_641, toma 7 segundos, para una tabla dos veces ese tamaño, toma 28 segundos. Algunas muestras más muestran tiempos de ejecución parecidos a O (n ^ 2).

El método de Evan Carrol toma menos de 1s (es decir, ¡adelante!), Y se parece a O (n). Las consultas recursivas son absolutamente ineficientes y deben considerarse un último recurso.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.