2013-09-24 5 views
22

Ich habe eine Tabelle in Postgres, die wie folgt aussieht:Wie kann ich den vorherigen Wert ungleich Null effizient auswählen?

# select * from p; 
id | value 
----+------- 
    1 | 100 
    2 |  
    3 |  
    4 |  
    5 |  
    6 |  
    7 |  
    8 | 200 
    9 |   
(9 rows) 

Und ich würde abfragen möchte es wie folgt aussehen:

# select * from p; 
id | value | new_value 
----+-------+---------- 
    1 | 100 |  
    2 |  | 100 
    3 |  | 100 
    4 |  | 100 
    5 |  | 100 
    6 |  | 100 
    7 |  | 100 
    8 | 200 | 100 
    9 |  | 200 
(9 rows) 

ich dies bereits mit einer Unterabfrage tun können, in die Auswahl, aber in meinen realen Daten habe ich 20k oder mehr Zeilen und es wird ziemlich langsam.

Ist dies in einer Fensterfunktion möglich? Ich würde liebend gerne lag(), aber es scheint nicht die IGNORE NULLS Option zu unterstützen.

select id, value, lag(value, 1) over (order by id) as new_value from p; 
id | value | new_value 
----+-------+----------- 
    1 | 100 |  
    2 |  |  100 
    3 |  |  
    4 |  | 
    5 |  | 
    6 |  | 
    7 |  | 
    8 | 200 | 
    9 |  |  200 
(9 rows) 

Antwort

48

Ich fand this answer für SQL Server, der auch in Postgres funktioniert. Da ich es noch nie zuvor gemacht hatte, dachte ich, die Technik sei ziemlich clever. Im Grunde erstellt er eine benutzerdefinierte Partition für die Fensterfunktion, indem er innerhalb einer verschachtelten Abfrage eine case-Anweisung verwendet, die eine Summe inkrementiert, wenn der Wert nicht null ist, und sie andernfalls in Ruhe lässt. Dadurch kann jeder NULL-Abschnitt mit der gleichen Nummer wie der vorherige NULL-Wert gekennzeichnet werden. Hier ist die Abfrage:

SELECT 
    id, value, value_partition, first_value(value) over (partition by value_partition order by id) 
FROM (
    SELECT 
    id, 
    value, 
    sum(case when value is null then 0 else 1 end) over (order by id) as value_partition 

    FROM p 
    ORDER BY id ASC 
) as q 

Und die Ergebnisse:

id | value | value_partition | first_value 
----+-------+-----------------+------------- 
    1 | 100 |    1 |   100 
    2 |  |    1 |   100 
    3 |  |    1 |   100 
    4 |  |    1 |   100 
    5 |  |    1 |   100 
    6 |  |    1 |   100 
    7 |  |    1 |   100 
    8 | 200 |    2 |   200 
    9 |  |    2 |   200 
(9 rows) 
+0

+1 ... Dies ist eine wirklich clevere Lösung, auch ein Index für 'id' und 'value' verbessert die Performance. – MatheusOl

+0

Große Lösung für mich im Jahr 2015! – Chris

+4

Sie können 'count (value)' anstelle von 'sum (case ...)' verwenden, da count Nullen ignoriert. – shaunc

2

Nun, ich kann nicht garantieren, dies der effizienteste Weg ist, aber funktioniert:

SELECT id, value, (
    SELECT p2.value 
    FROM p p2 
    WHERE p2.value IS NOT NULL AND p2.id <= p1.id 
    ORDER BY p2.id DESC 
    LIMIT 1 
) AS new_value 
FROM p p1 ORDER BY id; 

Der folgende Index, der die Unterabfrage für große Datensätze verbessern können:

CREATE INDEX idx_p_idvalue_nonnull ON p (id, value) WHERE value IS NOT NULL; 

Angenommen, die value ist spärlich (zB es gibt viele Nullen), wird es gut laufen.

+0

Dank benötigen! Dies ist effektiv, was ich bereits in Bezug auf eine Unterabfrage habe. Definitiv funktioniert. Ich wusste nicht, dass Postgres Ihnen erlaubt, einen Index zu erstellen und ihn mit einer Bedingung zu kombinieren. Das ist ziemlich toll. – adamlamar

2

Sie können eine benutzerdefinierte Aggregatfunktion in Postgres erstellen. Hier ist ein Beispiel für den int Typen:

CREATE FUNCTION coalesce_agg_sfunc(state int, value int) RETURNS int AS 
$$ 
    SELECT coalesce(value, state); 
$$ LANGUAGE SQL; 

CREATE AGGREGATE coalesce_agg(int) (
    SFUNC = coalesce_agg_sfunc, 
    STYPE = int); 

Dann wie gewohnt abfragen.

SELECT *, coalesce_agg(b) over w, sum(b) over w FROM y 
    WINDOW w AS (ORDER BY a); 

a b coalesce_agg sum 
- - ------------ --- 
a 0   0 0 
b ∅   0 0 
c 2   2 2 
d 3   3 5 
e ∅   3 5 
f 5   5 10 
(6 rows) 
+0

Sie können Pseudo-Typ 'anyelement' anstelle von' int' verwenden, um die Funktion und Aggregatfunktion generisch zu machen. –

+0

Der Aufruf von 'coalesce_agg' erscheint intuitiver. – ddrscott

+0

'coalesce_agg' scheint besser. Obwohl "Koaleszieren" zum Nitpick das erste Argument bevorzugt, ist diese Aggregatfunktion dem letzten Wert vorzuziehen. –

0

Sie können LAST_VALUE mit FILTER verwenden, um zu erreichen, was Sie (zumindest in PG 9,4)

WITH base AS (
SELECT 1 AS id , 100 AS val 
UNION ALL 
SELECT 2 AS id , null AS val 
UNION ALL 
SELECT 3 AS id , null AS val 
UNION ALL 
SELECT 4 AS id , null AS val 
UNION ALL 
SELECT 5 AS id , 200 AS val 
UNION ALL 
SELECT 6 AS id , null AS val 
UNION ALL 
SELECT 7 AS id , null AS val 
) 
SELECT id, val, last(val) FILTER (WHERE val IS NOT NULL) over(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) new_val 
    FROM base 
+0

FILTER mit nicht-aggregierter Window-Funktion ist in Postgres noch nicht implementiert. – pensnarik

Verwandte Themen