Połącz dwie tabele zdarzeń w jedną oś czasu

12

Biorąc pod uwagę dwie tabele:

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

Chciałbym napisać kwerendę, która zwraca wartości ts, fooi barktóry reprezentuje jednolity obraz najnowszych wartości. Innymi słowy, jeśli foozawiera:

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

i barzawierał:

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

Chcę kwerendy, która zwraca:

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

Jeśli obie tabele mają jednocześnie zdarzenie, kolejność nie ma znaczenia.

Udało mi się stworzyć potrzebną strukturę, używając sumy wszystkich i wartości zastępczych:

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

co da mi liniową oś czasu nowych wartości, ale nie jestem w stanie wypracować sposobu zapełniania wartości zerowych na podstawie poprzednich wierszy. Próbowałem lagfunkcji okna, ale AFAICT będzie patrzeć tylko na poprzedni wiersz, a nie rekurencyjnie do tyłu. Patrzyłem na rekurencyjne CTE, ale nie jestem pewien, jak skonfigurować warunki początkowe i końcowe.

Christopher Currie
źródło
Czy wartości zmieniają się w czasie fooi barściśle rosną w miarę upływu czasu, czy też przypadek testowy wprowadza w błąd pod tym względem?
Erwin Brandstetter,
2
Aby ocalić kogokolwiek innego, sqlfiddle.com/#!15/511414
Craig Ringer
1
Zamiast zmieniać charakter pytania po udzieleniu odpowiedzi, proszę zadać nowe pytanie . Zawsze możesz połączyć się z tym linkiem w celach informacyjnych. (Możesz nawet podać własną odpowiedź, jeśli ją masz). Oryginalna wersja powinna być interesująca dla ogółu społeczeństwa. Nie pakujmy się zbyt wiele w jedno pytanie.
Erwin Brandstetter,
Przepraszam za przeciążenie. Usunąłem obserwację i dodałem ją jako nowe pytanie .
Christopher Currie,

Odpowiedzi:

7

Użyj a FULL [OUTER] JOINw połączeniu z dwiema rundami funkcji okna :

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;

Ponieważ count()nie liczy wartości NULL, dogodnie zwiększa się tylko z każdą wartością inną niż null, tworząc grupy, które będą miały tę samą wartość. W zewnętrznym SELECT, min()(a max()) również nie uwzględnia wartości NULL, przez wybranie jednego wartość niezerową w grupie. Voilá.

Powiązana FULL JOINsprawa:

Jest to jeden z tych przypadków, w których rozwiązanie proceduralne może być po prostu szybsze, ponieważ może wykonać zadanie w jednym skanie. Podobnie jak ta funkcja 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;

Połączenie:

SELECT * FROM f_merge_foobar();

db <> skrzypce tutaj , demonstrując oba.

Powiązana odpowiedź wyjaśniająca #variable_conflict use_column:

Erwin Brandstetter
źródło
Ciekawy problem, prawda? Myślę, że wydajne rozwiązanie prawdopodobnie wymaga stworzenia coalescepodobnej do funkcji okna.
Craig Ringer
@CraigRinger: Rzeczywiście. Żałuję, zastanawiam się, myślę ... że to jakoś powinno być możliwe bez podzapytania, ale nie znalazłem sposobu. Jest to jeden z tych przypadków, w których funkcja plpgsql będzie szybsza, ponieważ może skanować każdą tabelę raz.
Erwin Brandstetter,
@Christopher: Byłbym zainteresowany wydajnością każdego wariantu w twojej konfiguracji. EXPLAIN ANALYZE, najlepszy z 5 ...?
Erwin Brandstetter,
2
Szkoda, że ​​Postgres jeszcze nie zaimplementował IGNORE NULLS(tak jak Oracle: sqlfiddle.com/#!4/fab35/1 ).
ypercubeᵀᴹ
1
@ypercube: Tak, Oracle simple w ogóle nie przechowuje wartości NULL i dlatego nie może odróżnić wartości ''NULL od.
Erwin Brandstetter,