Verwenden der Fensterfunktion zum Übertragen des ersten Nicht-Null-Werts in einer Partition

11

Stellen Sie sich eine Tabelle vor, in der Besuche aufgezeichnet werden

create table visits (
  person varchar(10),
  ts timestamp, 
  somevalue varchar(10) 
)

Betrachten Sie diese Beispieldaten (Zeitstempel als Zähler vereinfacht)

ts| person    |  somevalue
-------------------------
1 |  bob      |null
2 |  bob      |null
3 |  jim      |null
4 |  bob      |  A
5 |  bob      | null
6 |  bob      |  B
7 |  jim      |  X
8 |  jim      |  Y
9 |  jim      |  null

Ich versuche, den letzten Nicht-Null-Wert der Person auf alle zukünftigen Besuche zu übertragen, bis sich dieser Wert ändert (dh der nächste Nicht-Null-Wert wird).

Die erwartete Ergebnismenge sieht folgendermaßen aus:

ts|  person   | somevalue | carry-forward 
-----------------------------------------------
1 |  bob      |null       |   null
2 |  bob      |null       |   null
3 |  jim      |null       |   null
4 |  bob      |  A        |    A
5 |  bob      | null      |    A
6 |  bob      |  B        |    B
7 |  jim      |  X        |    X
8 |  jim      |  Y        |    Y
9 |  jim      |  null     |    Y

Mein Versuch sieht so aus:

 select *, 
  first_value(somevalue) over (partition by person order by (somevalue is null), ts rows between UNBOUNDED PRECEDING AND current row  ) as carry_forward

 from visits  
 order by ts

Hinweis: Der Wert (ein Wert ist null) wird zum Sortieren mit 1 oder 0 ausgewertet, damit ich den ersten Wert ungleich Null in der Partition erhalten kann.

Das Obige gibt mir nicht das Ergebnis, nach dem ich suche.

maxTrialfire
quelle
Könnten Sie einfach die pg_dumpfür Ihre Testdaten einfügen, anstatt die Daten in eine psql-Ausgabe und das Schema für die Tabelle einzufügen? pg_dump -t table -d databaseWir brauchen das Erstellen und die COPYBefehle.
Evan Carroll
1
@a_horse_with_no_name, der eine Antwort verdient.
Ypercubeᵀᴹ

Antworten:

11

Die folgende Abfrage erzielt das gewünschte Ergebnis:

select *, first_value(somevalue) over w as carryforward_somevalue
from (
  select *, sum(case when somevalue is null then 0 else 1 end) over (partition by person order by id ) as value_partition
  from test1

) as q
window w as (partition by person, value_partition order by id);

Beachten Sie die Null-Fall-Anweisung - wenn IGNORE_NULL von Postgres-Fensterfunktionen unterstützt würde, wäre dies nicht erforderlich (wie von @ ypercubeᵀᴹ erwähnt).

maxTrialfire
quelle
5
Auch der einfachecount(somevalue) over (...)
ypercubeᵀᴹ
4

Das Problem liegt in der Kategorie der Lücken und Inseln. Es ist schade, dass Postgres noch nicht IGNORE NULLin Fensterfunktionen implementiert hat, wie FIRST_VALUE()es sonst trivial wäre, mit einer einfachen Änderung in Ihrer Abfrage.

Es gibt wahrscheinlich viele Möglichkeiten, dies mithilfe von Fensterfunktionen oder rekursiven CTEs zu lösen.

Ich bin mir nicht sicher, ob dies der effizienteste Weg ist, aber ein rekursiver CTE löst das Problem:

with recursive 
    cf as
    (
      ( select distinct on (person) 
            v.*, v.somevalue as carry_forward
        from visits as v
        order by person, ts
      ) 
      union all
        select 
            v.*, coalesce(v.somevalue, cf.carry_forward)
        from cf
          join lateral  
            ( select v.*
              from visits as v
              where v.person = cf.person
                and v.ts > cf.ts
              order by ts
              limit 1
            ) as v
            on true
    )
select cf.*
from cf 
order by ts ;
ypercubeᵀᴹ
quelle
Es löst zwar das Problem, ist jedoch komplexer als es sein muss. Siehe meine Antwort unten
maxTrialfire
1
Ja, deine Antwort scheint gut zu sein!
Ypercubeᵀᴹ