How to number consecutive records per island? - sql

I have a table which looks like:
group date color
A 1-1-2019 R
A 1-2-2019 Y
B 1-1-2019 R
B 1-2-2019 Y
B 1-3-2019 Y
B 1-4-2019 R
B 1-5-2019 R
B 1-6-2019 R
And it's ordered by group and date. I want an extra column showing sequential number of consecutive color 'R' for each group.
Required output:
group date color rank
A 1-1-2019 R 1
A 1-2-2019 Y null
B 1-1-2019 R 1
B 1-2-2019 Y null
B 1-3-2019 Y null
B 1-4-2019 R 1
B 1-5-2019 R 2
B 1-6-2019 R 3
I've tried to use window function with partition by group and color columns but it returns output below which is not correct.
Wrong Query and Output:
SELECT
*,
RANK() OVER (PARTITION BY group, color order by group, date) as rank
FROM table
group date color rank
A 1-1-2019 R 1
A 1-2-2019 Y null
B 1-1-2019 R 1
B 1-2-2019 Y null
B 1-3-2019 Y null
B 1-4-2019 R 2
B 1-5-2019 R 3
B 1-6-2019 R 4
I'm wondering if it's doable in SQL, or should I switch to another language (like Python)?

This is how it can be done using window functions. First we create a CTE which has a flag which indicates that a new sequence has started, then from that we generate one which counts sequence numbers. Finally we count rows within each sequence to get the rank:
WITH cte AS (SELECT `group`, date, color,
COALESCE(color = LAG(color) OVER(ORDER BY `group`, date), 0) AS samecolor
FROM `table`),
sequences AS (SELECT `group`, date, color,
SUM(samecolor = 0) OVER (ORDER BY `group`, date) AS seq_num
FROM cte)
SELECT `group`, date, color,
ROW_NUMBER() OVER (PARTITION BY seq_num) AS `rank`
FROM sequences
ORDER BY `group`, date
Output:
group date color rank
A 1-1-2019 R 1
A 1-2-2019 Y 1
B 1-1-2019 R 1
B 1-2-2019 Y 1
B 1-3-2019 Y 2
B 1-4-2019 R 1
B 1-5-2019 R 2
B 1-6-2019 R 3
Demo on dbfiddle
Note that this query also gives ranking for Y values, if you want those to be NULL replace the definition of rank with this:
CASE WHEN color = 'Y' THEN NULL
ELSE ROW_NUMBER() OVER (PARTITION BY seq_num)
END AS `rank`

Using user variables could keep the rank and previous values to produce the results:
CREATE TABLE tbl (
`group` VARCHAR(1),
`date` VARCHAR(8),
`color` VARCHAR(1)
);
INSERT INTO tbl
(`group`, `date`, `color`)
VALUES
('A', '1-1-2019', 'R'),
('A', '1-2-2019', 'Y'),
('B', '1-1-2019', 'R'),
('B', '1-2-2019', 'Y'),
('B', '1-3-2019', 'Y'),
('B', '1-4-2019', 'R'),
('B', '1-5-2019', 'R'),
('B', '1-6-2019', 'R');
set #seq := 0, #prev := 'B'
SELECT
*,
IF(color='R', #seq := IF(#prev = color, #seq + 1, 1), NULL) AS rank,
#prev := color as prev
FROM tbl
ORDER BY `group`, `date`
group | date | color | rank | prev
:---- | :------- | :---- | ---: | :---
A | 1-1-2019 | R | 1 | R
A | 1-2-2019 | Y | | Y
B | 1-1-2019 | R | 1 | R
B | 1-2-2019 | Y | | Y
B | 1-3-2019 | Y | | Y
B | 1-4-2019 | R | 1 | R
B | 1-5-2019 | R | 2 | R
B | 1-6-2019 | R | 3 | R
db<>fiddle here

Use the window function row_number() for a pure standard SQL solution in Postgres - or any modern RDBMS, even MySQL since version 8:
SELECT grp, the_date, color
, row_number() OVER (PARTITION BY grp, color, part
ORDER BY the_date) AS rnk
FROM (
SELECT *
, row_number() OVER (PARTITION BY grp ORDER BY the_date, color)
- row_number() OVER (PARTITION BY grp, color ORDER BY the_date) AS part
FROM tbl
) sub
ORDER BY grp, the_date, color;
This assumes that the combination (grp, color, the_date) is defined UNIQUE, duplicates would create non-deterministic results.
Substracting the two different row numbers computes a distinct number per island (part). Then you can run row_number() once more, now partitioning by the subgroup additionally. Voilá.
To only see numbers for a particular color, 'R' in the example:
SELECT grp, the_date, color, CASE WHEN color = 'R' THEN rnk END AS rnk
FROM (
<<query from above, without ORDER BY>>
) sub
ORDER BY grp, the_date, color;
While set-based solution are the forté of RDBMS' and typically faster, a procedural solution only needs a single scan for this type of problem, so this plpgsql function should be substantially faster:
CREATE OR REPLACE FUNCTION rank_color(_color text = 'R') -- default 'R'
RETURNS TABLE (grp text, the_date date, color text, rnk int) AS
$func$
DECLARE
_last_grp text;
BEGIN
FOR grp, the_date, color IN
SELECT t.grp, t.the_date, t.color FROM tbl t ORDER BY 1,2
LOOP
IF color = $1 THEN
IF _last_grp = grp THEN
rnk := COALESCE(rnk + 1, 1);
ELSE
rnk := 1;
END IF;
ELSIF rnk > 0 THEN -- minimize assignments
rnk := NULL;
END IF;
RETURN NEXT;
_last_grp := grp;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM rank_color('R');
db<>fiddle here
Looping is not always the wrong solution in a relational database.
Further reading:
Select longest continuous sequence
GROUP BY and aggregate sequential numeric values
Aside: "rank" is a rather misleading name for those row numbers, unless you have duplicates supposed to rank equally ...

Related

LEAD and LAG function by omitting 0

enter image description here
I want to omit 0 while applying LEAD and LAG function.
For highlighted column, prev should be 116.69635888009 and next should be 108.324381114468
I am using the query below:
select
snapshot_date,
assetname,
prev,
monthly_avg_kw,
next,
(prev+next)/2 as avg
FROM
(select
snapshot_date,
assetname,
monthly_avg_kw,
LAG(monthly_avg_kw) OVER(PARTITION BY assetname ORDER BY snapshot_date ASC) as prev,
LEAD(monthly_avg_kw) OVER(PARTITION BY assetname ORDER BY
snapshot_date ASC) as next
from 'TABLE')
where assetname = 'MI6.UPS-2A-2'
Using MySQL, I was able to create this example, which produces the desired behavior.
Adding (b>0) to the order by causes the previous value to be based only on column where b>0. This will produce NULL when the value of b is 0. In that case we use COALESCE to find the previous value just on the order by a.
CREATE TABLE test (
a integer,
b INTEGER);
INSERT INTO test VALUES
(1,1),
(2,2),
(3,0),
(4,4),
(5,5),
(6,6);
SELECT
a,b,
COALESCE(lag(b) over (order by (b>0),a),lag(b) over (order by a)) as BB
FROM test
ORDER BY a;
output:
a
b
BB
1
1
0
2
2
1
3
0
2
4
4
2
5
5
4
6
6
5
see: DBFIDDLE

How to get the last not null value for a column while sorting based upon a timestamp column

Table T has columns A, B, C, TS(timestamp) with values as defined below
A B C TS
d g null 3
h y gh 2
q r null 7
If I write a query like below:
SELECT * from T order by TS desc Limit 1;
It gives me result as:
A B C TS
q r null 7
What I want is to never get a null value. Instead it should display the last not null value from that column, if any.
Desired result:
A B C TS
q r gh 7
Try the OLAP function LAST_VALUE( .... IGNORE NULLS) OVER ....
I saw that you get data that makes sense if you order by ts ascending in the window definition clause - I hope that is what you need ...
WITH
-- your input ...
indata(A,B,C,TS) AS (
SELECT 'd','g',null,3
UNION ALL SELECT 'h','y','gh',2
UNION ALL SELECT 'q','r',null,7
)
-- real query starts here ...
SELECT
LAST_VALUE(a IGNORE NULLS) OVER w AS a
, LAST_VALUE(b IGNORE NULLS) OVER w AS b
, LAST_VALUE(c IGNORE NULLS) OVER w AS c
, ts
FROM indata
WINDOW w AS (ORDER BY ts)
ORDER BY ts DESC
LIMIT 1;
-- out a | b | c | ts
-- out ---+---+----+----
-- out q | r | gh | 7
One method is to fetch each column independently:
select (select a from T where a is not null order by TS desc Limit 1),
(select b from T where b is not null order by TS desc Limit 1),
(select c from T where c is not null order by TS desc Limit 1),
(select ts from T where ts is not null order by TS desc Limit 1)

flatten list of ranges to single result range set

I am trying to "flatten" a list of ranges in a defined order (alphabetically by name in the examples provided) to a single merged result. Newer Ranges overwrite values of older ranges. Conceptually it looks like this, with "e" being the newest range:
0 1 2 3 4 5 6 7
|-------------a-------------|
|---b---|
|---c---|
|---d---|
|---e---|
|-a-|---c---|---e---|-d-|-a-| <-- expected result
To prevent further confusion: The expected result here is indeed correct. The values 0 - 7 are just the ranges' values, not a progression in time. I use integers for simplicity here, but the values might not be discrete but continuous.
Note that b is completely overshadowed and not relevant anymore.
the data may be modeled like this in SQL:
create table ranges (
name varchar(1),
range_start integer,
range_end integer
);
insert into ranges (name, range_start, range_end) values ('a', 0, 7);
insert into ranges (name, range_start, range_end) values ('b', 2, 4);
insert into ranges (name, range_start, range_end) values ('c', 1, 3);
insert into ranges (name, range_start, range_end) values ('d', 4, 6);
insert into ranges (name, range_start, range_end) values ('e', 3, 5);
-- assume alphabetical order by name
It would be perfect if there was a way to directly query the result in SQL, e.g. like this:
select *magic* from ranges;
-- result:
+------+-------------+-----------+
| a | 0 | 1 |
| c | 1 | 3 |
| e | 3 | 5 |
| d | 5 | 6 |
| a | 6 | 7 |
+------+-------------+-----------+
But I suspect that is not realistically feasible, therefore I need to at least filter out all ranges that are overshadowed by newer ones, as is the case for b in the example above. Otherwise the query would need to transfer more and more irrelevant data as the database grows and new ranges overshadow older ones. For the example above, such a query could return all entries except for b, e.g.:
select *magic* from ranges;
-- result:
+------+-------------+-----------+
| a | 0 | 7 |
| c | 1 | 3 |
| d | 4 | 6 |
| e | 3 | 5 |
+------+-------------+-----------+
I was unable to construct such a filter in SQL. The only thing I managed to do is query all data and then calculate the result in code, for example in Java using the Google Guava library:
final RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(0, 7), "a");
rangeMap.put(Range.closedOpen(2, 4), "b");
rangeMap.put(Range.closedOpen(1, 3), "c");
rangeMap.put(Range.closedOpen(4, 6), "d");
rangeMap.put(Range.closedOpen(3, 5), "e");
System.out.println(rangeMap);
// result: [[0..1)=a, [1..3)=c, [3..5)=e, [5..6)=d, [6..7)=a]
Or by hand in python:
import re
from collections import namedtuple
from typing import Optional, List
Range = namedtuple("Range", ["name", "start", "end"])
def overlap(lhs: Range, rhs: Range) -> Optional[Range]:
if lhs.end <= rhs.start or rhs.end <= lhs.start:
return None
return Range(None, min(lhs.start, rhs.start), max(lhs.end, rhs.end))
def range_from_str(str_repr: str) -> Range:
name = re.search(r"[a-z]+", str_repr).group(0)
start = str_repr.index("|") // 4
end = str_repr.rindex("|") // 4
return Range(name, start, end)
if __name__ == '__main__':
ranges: List[Range] = [
# 0 1 2 3 4 5 6 7
range_from_str("|-------------a-------------|"),
range_from_str(" |---b---| "),
range_from_str(" |---c---| "),
range_from_str(" |---d---| "),
range_from_str(" |---e---| "),
# result: |-a-|---c---|---e---|-d-|-a-|
]
result: List[Range] = []
for range in ranges:
for i, res in enumerate(result[:]):
o = overlap(range, res)
if o:
result.append(Range(res.name, o.start, range.start))
result.append(Range(res.name, range.end, o.end))
result[i] = Range(res.name, 0, 0)
result.append(range)
result = sorted(filter(lambda r: r.start < r.end, result), key=lambda r: r.start)
print(result)
# result: [Range(name='a', start=0, end=1), Range(name='c', start=1, end=3), Range(name='e', start=3, end=5), Range(name='d', start=5, end=6), Range(name='a', start=6, end=7)]
The following simple query returns all smallest intervals with top name:
with
all_points(x) as (
select range_start from ranges
union
select range_end from ranges
)
,all_ranges(range_start, range_end) as (
select *
from (select
x as range_start,
lead(x) over(order by x) as range_end
from all_points)
where range_end is not null
)
select *
from all_ranges ar
cross apply (
select max(name) as range_name
from ranges r
where r.range_end >= ar.range_end
and r.range_start <= ar.range_start
)
order by 1,2;
Results:
RANGE_START RANGE_END RANGE_NAME
----------- ---------- ----------
0 1 a
1 2 c
2 3 c
3 4 e
4 5 e
5 6 d
6 7 a
So we need to merge connected intervals with the same names:
Final query without new oracle-specific features
with
all_points(x) as (
select range_start from ranges
union
select range_end from ranges
)
,all_ranges(range_start, range_end) as (
select *
from (select
x as range_start,
lead(x) over(order by x) as range_end
from all_points)
where range_end is not null
)
select
grp,range_name,min(range_start) as range_start,max(range_end) as range_end
from (
select
sum(start_grp_flag) over(order by range_start) grp
,range_start,range_end,range_name
from (
select
range_start,range_end,range_name,
case when range_name = lag(range_name)over(order by range_start) then 0 else 1 end start_grp_flag
from all_ranges ar
cross apply (
select max(name) as range_name
from ranges r
where r.range_end >= ar.range_end
and r.range_start <= ar.range_start
)
)
)
group by grp,range_name
order by 1;
Results:
GRP RANGE_NAME RANGE_START RANGE_END
---------- ---------- ----------- ----------
1 a 0 1
2 c 1 3
3 e 3 5
4 d 5 6
5 a 6 7
Or using actual oracle specific features:
with
all_ranges(range_start, range_end) as (
select * from (
select
x as range_start,
lead(x) over(order by x) as range_end
from (
select distinct x
from ranges
unpivot (x for r in (range_start,range_end))
))
where range_end is not null
)
select *
from all_ranges ar
cross apply (
select max(name) as range_name
from ranges r
where r.range_end >= ar.range_end
and r.range_start <= ar.range_start
)
match_recognize(
order by range_start
measures
first(range_start) as r_start,
last(range_end) as r_end,
last(range_name) as r_name
pattern(STRT A*)
define
A as prev(range_name)=range_name and prev(range_end) = range_start
);
Here is a hierarchical query that would give you the desired output:
WITH ranges(NAME, range_start, range_end) AS
(SELECT 'a', 0, 7 FROM dual UNION ALL
SELECT 'b', 2, 4 FROM dual UNION ALL
SELECT 'c', 1, 3 FROM dual UNION ALL
SELECT 'd', 4, 6 FROM dual UNION ALL
SELECT 'e', 3, 5 FROM dual UNION ALL
SELECT 'f', -3, -2 FROM dual UNION ALL
SELECT 'g', 8, 20 FROM dual UNION ALL
SELECT 'h', 12, 14 FROM dual)
, rm (NAME, range_start, range_end) AS
(SELECT r.*
FROM (SELECT r.NAME
, r.range_start
, NVL(r2.range_start, r.range_end) range_end
FROM ranges r
OUTER apply (SELECT *
FROM ranges
WHERE range_start BETWEEN r.range_start AND r.range_end
AND NAME > r.NAME
ORDER BY range_start, NAME DESC
FETCH FIRST 1 ROWS ONLY) r2
ORDER BY r.range_start, r.NAME desc
FETCH FIRST 1 ROWS ONLY) r
UNION ALL
SELECT r2.NAME
, r2.range_start
, r2.range_end
FROM rm
CROSS apply (SELECT r.NAME
, GREATEST(rm.range_end, r.range_start) range_start
, NVL(r2.range_start, r.range_end) range_end
FROM ranges r
OUTER apply (SELECT *
FROM ranges
WHERE range_start BETWEEN GREATEST(rm.range_end, r.range_start) AND r.range_end
AND NAME > r.NAME
ORDER BY range_start, NAME DESC
FETCH FIRST 1 ROWS ONLY) r2
WHERE r.range_end > rm.range_end
AND NOT EXISTS (SELECT 1 FROM ranges r3
WHERE r3.range_end > rm.range_end
AND (GREATEST(rm.range_end, r3.range_start) < GREATEST(rm.range_end, r.range_start)
OR (GREATEST(rm.range_end, r3.range_start) = GREATEST(rm.range_end, r.range_start)
AND r3.NAME > r.NAME)))
FETCH FIRST 1 ROWS ONLY) r2)
CYCLE NAME, range_start, range_end SET cycle TO 1 DEFAULT 0
SELECT * FROM rm
First you get the first entry ordered by range_start desc, name which will give you the most resent entry with the lowest name.
Then you search for a range with higher name that intersect with this range. If there is one the range_start of this interval will be the range_end of you final interval.
With this start you search more or less the next entry with the same condition.
There is also another less effective but easier and shorter approach: to generate all points and just aggregate them.
For example this simple query will generate all intermediate points:
select x,max(name)
from ranges,
xmltable('xs:integer($A) to xs:integer($B)'
passing range_start as a
,range_end as b
columns x int path '.'
)
group by x
Results:
X M
---------- -
0 a
1 c
2 c
3 e
4 e
5 e
6 d
7 a
Then we can merge them:
select *
from (
select x,max(name) name
from ranges,
xmltable('xs:integer($A) to xs:integer($B)-1'
passing range_start as a
,range_end as b
columns x int path '.'
)
group by x
order by 1
)
match_recognize(
order by x
measures
first(x) as r_start,
last(x)+1 as r_end,
last(name) as r_name
pattern(STRT A*)
define
A as prev(name)=name and prev(x)+1 = x
);
Results:
R_START R_END R
---------- ---------- -
0 1 a
1 3 c
3 5 e
5 6 d
6 7 a
I don't understand your results -- as I've explained in the comments. The "b" should be present, because it is most recent at time 2.
That said, the idea is to unpivot the times and figure out the most recent name at each time -- both beginnings and ends. Then, combine these using gaps-and-islands ideas. This is what the query looks like:
with r as (
select name, range_start as t
from ranges
union all
select null, range_end as t
from ranges
),
r2 as (
select r.*,
(select r2.name
from ranges r2
where r2.range_start <= r.t and
r2.range_end >= r.t
order by r2.range_start desc
fetch first 1 row only
) as imputed_name
from (select distinct t
from r
) r
)
select imputed_name, t,
lead(t) over (order by t)
from (select r2.*,
lag(imputed_name) over ( order by t) as prev_imputed_name
from r2
) r2
where prev_imputed_name is null or prev_imputed_name <> imputed_name;
Here is a db<>fiddle.
Basically the same code should run in Postgres as well.

How to count most consecutive occurrences of a value in a Column in SQL Server

I have a table Attendance in my database.
Date | Present
------------------------
20/11/2013 | Y
21/11/2013 | Y
22/11/2013 | N
23/11/2013 | Y
24/11/2013 | Y
25/11/2013 | Y
26/11/2013 | Y
27/11/2013 | N
28/11/2013 | Y
I want to count the most consecutive occurrence of a value Y or N.
For example in the above table Y occurs 2, 4 & 1 times. So I want 4 as my result.
How to achieve this in SQL Server?
Any help will be appreciated.
Try this:-
The difference between the consecutive date will remain constant
Select max(Sequence)
from
(
select present ,count(*) as Sequence,
min(date) as MinDt, max(date) as MaxDt
from (
select t.Present,t.Date,
dateadd(day,
-(row_number() over (partition by present order by date))
,date
) as grp
from Table1 t
) t
group by present, grp
)a
where Present ='Y'
SQL FIDDLE
You can do this with a recursive CTE:
;WITH cte AS (SELECT Date,Present,ROW_NUMBER() OVER(ORDER BY Date) RN
FROM Table1)
,cte2 AS (SELECT Date,Present,RN,ct = 1
FROM cte
WHERE RN = 1
UNION ALL
SELECT a.Date,a.Present,a.RN,ct = CASE WHEN a.Present = b.Present THEN ct + 1 ELSE 1 END
FROM cte a
JOIN cte2 b
ON a.RN = b.RN+1)
SELECT TOP 1 *
FROM cte2
ORDER BY CT DESC
Demo: SQL Fiddle
Note, the date's in the demo got altered due to the format you posted the dates in your question.

Retrieve last known value for each column of a row

Not sure about the correct words to ask this question, so I will break it down.
I have a table as follows:
date_time | a | b | c
Last 4 rows:
15/10/2013 11:45:00 | null | 'timtim' | 'fred'
15/10/2013 13:00:00 | 'tune' | 'reco' | null
16/10/2013 12:00:00 | 'abc' | null | null
16/10/2013 13:00:00 | null | 'died' | null
How would I get the last record but with the value ignoring the null and instead get the value from the previous record.
In my provided example the row returned would be
16/10/2013 13:00:00 | 'abc' | 'died' | 'fred'
As you can see if the value for a column is null then it goes to the last record which has a value for that column and uses that value.
This should be possible, I just cant figure it out. So far I have only come up with:
select
last_value(a) over w a
from test
WINDOW w AS (
partition by a
ORDER BY ts asc
range between current row and unbounded following
);
But this only caters for a single column ...
Here I create an aggregation function that collects columns into arrays. Then it is just a matter of removing the NULLs and selecting the last element from each array.
Sample Data
CREATE TABLE T (
date_time timestamp,
a text,
b text,
c text
);
INSERT INTO T VALUES ('2013-10-15 11:45:00', NULL, 'timtim', 'fred'),
('2013-10-15 13:00:00', 'tune', 'reco', NULL ),
('2013-10-16 12:00:00', 'abc', NULL, NULL ),
('2013-10-16 13:00:00', NULL, 'died', NULL );
Solution
CREATE AGGREGATE array_accum (anyelement)
(
sfunc = array_append,
stype = anyarray,
initcond = '{}'
);
WITH latest_nonull AS (
SELECT MAX(date_time) As MaxDateTime,
array_remove(array_accum(a), NULL) AS A,
array_remove(array_accum(b), NULL) AS B,
array_remove(array_accum(c), NULL) AS C
FROM T
ORDER BY date_time
)
SELECT MaxDateTime, A[array_upper(A, 1)], B[array_upper(B,1)], C[array_upper(C,1)]
FROM latest_nonull;
Result
maxdatetime | a | b | c
---------------------+-----+------+------
2013-10-16 13:00:00 | abc | died | fred
(1 row)
Order of rows
The "last row" and the sort order would need to be defined unambiguously. There is no natural order in a set (or a table). I am assuming ORDER BY ts, where ts is the timestamp column.
Like #Jorge pointed out in his comment: If ts is not UNIQUE, one needs to define tiebreakers for the sort order to make it unambiguous (add more items to ORDER BY). A primary key would be the ultimate solution.
General solution with window functions
To get a result for every row:
SELECT ts
, max(a) OVER (PARTITION BY grp_a) AS a
, max(b) OVER (PARTITION BY grp_b) AS b
, max(c) OVER (PARTITION BY grp_c) AS c
FROM (
SELECT *
, count(a) OVER (ORDER BY ts) AS grp_a
, count(b) OVER (ORDER BY ts) AS grp_b
, count(c) OVER (ORDER BY ts) AS grp_c
FROM t
) sub;
How?
The aggregate function count() ignores NULL values when counting. Used as aggregate-window function, it computes the running count of a column according to the default window definition, which is RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. NULL values don't increase the count, so these rows fall into the same peer group as the last non-null value.
In a second window function, the only non-null value per group is easily extracted with max() or min().
Just the last row
WITH cte AS (
SELECT *
, count(a) OVER w AS grp_a
, count(b) OVER w AS grp_b
, count(c) OVER w AS grp_c
FROM t
WINDOW w AS (ORDER BY ts)
)
SELECT ts
, max(a) OVER (PARTITION BY grp_a) AS a
, max(b) OVER (PARTITION BY grp_b) AS b
, max(c) OVER (PARTITION BY grp_c) AS c
FROM cte
ORDER BY ts DESC
LIMIT 1;
Simple alternatives for just the last row
SELECT ts
,COALESCE(a, (SELECT a FROM t WHERE a IS NOT NULL ORDER BY ts DESC LIMIT 1)) AS a
,COALESCE(b, (SELECT b FROM t WHERE b IS NOT NULL ORDER BY ts DESC LIMIT 1)) AS b
,COALESCE(c, (SELECT c FROM t WHERE c IS NOT NULL ORDER BY ts DESC LIMIT 1)) AS c
FROM t
ORDER BY ts DESC
LIMIT 1;
Or:
SELECT (SELECT ts FROM t ORDER BY ts DESC LIMIT 1) AS ts
,(SELECT a FROM t WHERE a IS NOT NULL ORDER BY ts DESC LIMIT 1) AS a
,(SELECT b FROM t WHERE b IS NOT NULL ORDER BY ts DESC LIMIT 1) AS b
,(SELECT c FROM t WHERE c IS NOT NULL ORDER BY ts DESC LIMIT 1) AS c
db<>fiddle here
Old sqlfiddle
Performance
While this should be decently fast, if performance is your paramount requirement, consider a plpgsql function. Start with the last row and loop descending until you have a non-null value for every column required. Along these lines:
GROUP BY and aggregate sequential numeric values
This should work but keep in mind it is an uggly solution
select * from
(select dt from
(select rank() over (order by ctid desc) idx, dt
from sometable ) cx
where idx = 1) dtz,
(
select a from
(select rank() over (order by ctid desc) idx, a
from sometable where a is not null ) ax
where idx = 1) az,
(
select b from
(select rank() over (order by ctid desc) idx, b
from sometable where b is not null ) bx
where idx = 1) bz,
(
select c from
(select rank() over (order by ctid desc) idx, c
from sometable where c is not null ) cx
where idx = 1) cz
See it here at fiddle: http://sqlfiddle.com/#!15/d5940/40
The result will be
DT A B C
October, 16 2013 00:00:00+0000 abc died fred