Usar la función de ventana para transferir el primer valor no nulo en una partición

11

Considere una tabla que registra las visitas

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

Considere estos datos de ejemplo (marca de tiempo simplificada como contador)

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

Estoy tratando de llevar el último valor no nulo de la persona a todas sus visitas futuras hasta que ese valor cambie (es decir, se convierta en el siguiente valor no nulo).

El conjunto de resultados esperado se ve así:

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

Mi intento se ve así:

 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

Nota: el (algún valor es nulo) se evalúa a 1 o 0 con el propósito de ordenar para que pueda obtener el primer valor no nulo en la partición.

Lo anterior no me da el resultado que busco.

maxTrialfire
fuente
¿Podría simplemente pegar los pg_dumpdatos de prueba en lugar de pegar los datos en una salida psql y el esquema de la tabla? pg_dump -t table -d databaseNecesitamos la creación y los COPYcomandos.
Evan Carroll
1
@a_horse_with_no_name que merece ser una respuesta.
ypercubeᵀᴹ

Respuestas:

11

La siguiente consulta logra el resultado deseado:

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);

Tenga en cuenta la declaración de caso nulo: si IGNORE_NULL fuera compatible con las funciones de la ventana de postgres, esto no sería necesario (como lo menciona @ ypercubeᵀᴹ)

maxTrialfire
fuente
55
También lo simplecount(somevalue) over (...)
ypercubeᵀᴹ
4

El problema está en la categoría de problemas de brechas e islas. Es una pena que Postgres aún no haya implementado IGNORE NULLen funciones de ventana como FIRST_VALUE(), de lo contrario, sería trivial, con un simple cambio en su consulta.

Probablemente hay muchas maneras de resolver esto utilizando funciones de ventana o CTE recursivos.

No estoy seguro de si es la forma más eficiente, pero un CTE recursivo resuelve el problema:

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ᵀᴹ
fuente
De hecho, resuelve el problema, sin embargo, es más complejo de lo que debe ser. Vea mi respuesta a continuación
maxTrialfire el
1
Sí, tu respuesta parece buena!
ypercubeᵀᴹ