Combina dos tablas de eventos en una sola línea de tiempo

12

Dadas dos tablas:

CREATE TABLE foo (ts timestamp, foo text);
CREATE TABLE bar (ts timestamp, bar text);

Me gustaría escribir una consulta que devuelve los valores para ts, fooy barque representa una visión unificada de los valores más recientes. En otras palabras, si está foocontenido:

ts | foo
--------
1  | A
7  | B

y barcontenía:

ts | bar
--------
3  | C
5  | D
9  | E

Quiero una consulta que devuelva:

ts | foo | bar
--------------
1  | A   | null
3  | A   | C
5  | A   | D
7  | B   | D
9  | B   | E

Si ambas tablas tienen un evento al mismo tiempo, el orden no importa.

He podido crear la estructura necesaria usando union all y valores ficticios:

SELECT ts, foo, null as bar FROM foo
UNION ALL SELECT ts, null as foo, bar FROM bar

lo que me dará una línea de tiempo lineal de nuevos valores, pero no soy capaz de resolver cómo llenar los valores nulos en función de las filas anteriores. He probado la lagfunción de ventana, pero AFAICT sólo se verá en la fila anterior, no recursiva hacia atrás. He examinado los CTE recursivos, pero no estoy muy seguro de cómo configurar las condiciones de inicio y finalización.

Christopher Currie
fuente
Son valores en fooy barestrictamente ascendente en el tiempo o en el caso de prueba engañosa a este respecto?
Erwin Brandstetter
2
Para salvar a los demás de la molestia, sqlfiddle.com/#!15/511414
Craig Ringer el
1
En lugar de cambiar la naturaleza de la pregunta después de que se haya dado una respuesta, haga una nueva pregunta . Siempre puede vincular a este para referencia. (Incluso puede proporcionar su propia respuesta si tiene una). La versión original debería ser interesante para el público en general. No empaquemos demasiado en una sola pregunta.
Erwin Brandstetter
Perdón por la sobrecarga. Eliminé el seguimiento y lo agregué como una nueva pregunta .
Christopher Currie

Respuestas:

7

Use a FULL [OUTER] JOIN, combinado con dos rondas de funciones de ventana :

SELECT ts
     , min(foo) OVER (PARTITION BY foo_grp) AS foo
     , min(bar) OVER (PARTITION BY bar_grp) AS bar
FROM (
   SELECT ts, f.foo, b.bar
        , count(f.foo) OVER (ORDER BY ts) AS foo_grp
        , count(b.bar) OVER (ORDER BY ts) AS bar_grp
   FROM   foo f
   FULL   JOIN bar b USING (ts)
   ) sub;

Como count()no cuenta los valores NULL, convenientemente solo aumenta con cada valor no nulo, formando así grupos que compartirán el mismo valor. En el exterior SELECT, min()(o max()) también ignora los valores NULL, eligiendo así el valor no nulo por grupo. Voilá

FULL JOINCaso relacionado :

Es uno de esos casos en los que una solución de procedimiento podría ser más rápida, ya que puede hacer el trabajo en un solo escaneo. Como esta función plpgsql :

CREATE OR REPLACE FUNCTION f_merge_foobar()
  RETURNS TABLE(ts int, foo text, bar text) AS
$func$
#variable_conflict use_column
DECLARE
   last_foo text;
   last_bar text;
BEGIN
   FOR ts, foo, bar IN
      SELECT ts, f.foo, b.bar
      FROM   foo f
      FULL   JOIN bar b USING (ts)
      ORDER  BY 1
   LOOP
      IF foo IS NULL THEN foo := last_foo;
      ELSE                last_foo := foo;
      END IF;

      IF bar IS NULL THEN bar := last_bar;
      ELSE                last_bar := bar;
      END IF;

      RETURN NEXT;
   END LOOP;
END
$func$ LANGUAGE plpgsql;

Llamada:

SELECT * FROM f_merge_foobar();

db <> violín aquí , demostrando ambos.

Respuesta relacionada que explica el #variable_conflict use_column:

Erwin Brandstetter
fuente
Un problema interesante no lo es. Creo que una solución eficiente probablemente requiere la creación de una coalescefunción de ventana similar.
Craig Ringer
@CraigRinger: De hecho. Me encuentro deseando, preguntándome, pensando ... que esto de alguna manera debería ser posible sin preguntar, pero no pude encontrar una manera. Es uno de esos casos donde una función plpgsql será más rápida ya que puede escanear cada tabla una vez.
Erwin Brandstetter
@ Christopher: Me interesaría el rendimiento de cada variante en su configuración. EXPLAIN ANALYZE, mejor de 5 ...?
Erwin Brandstetter
2
Lástima que Postgres aún no se haya implementado IGNORE NULLS(como Oracle lo ha hecho: sqlfiddle.com/#!4/fab35/1 ).
ypercubeᵀᴹ
1
@ypercube: Sí, Oracle simple no almacena valores NULL en absoluto y, en consecuencia, no puede distinguir entre ''y NULL.
Erwin Brandstetter