You can do this by combining your versioning and your value into a single binary column, then selecting the max. The query in it's shortest form would be:
SELECT t.ID,
Value1 = CONVERT(VARCHAR(50), SUBSTRING(MAX(CONVERT(BINARY(4), t.Versioning)
+ CONVERT(VARBINARY(50), t.Value1)), 5, 50)),
Value2 = CONVERT(VARCHAR(50), SUBSTRING(MAX(CONVERT(BINARY(4), t.Versioning)
+ CONVERT(VARBINARY(50), t.Value2)), 5, 50)),
Value3 = CONVERT(INT, SUBSTRING(MAX(CONVERT(BINARY(4), t.Versioning)
+ CONVERT(VARBINARY(50), t.Value3)), 5, 50))
FROM YourTable AS t
GROUP BY ID;
To explain what is going on, I will focus on just value 3, with cutdown sample data.
The first step in the process is just to combine the ordering column, and the value column into a single binary value:
SELECT *,
BinaryValue3 = CONVERT(BINARY(2), t.Versioning) + CONVERT(BINARY(2), t.Value3)
FROM (VALUES (1, 2, 1), (1, 4, 2), (1, NULL, 3)) AS t (ID, Value3, Versioning)
Which gives:
ID Value3 Versioning BinaryValue3
--------------------------------------
1 2 1 0x00010002
1 4 2 0x00020004
1 NULL 3 NULL
We then take the maximum of the binary value. This relies on two things:
- The fact that concatenating NULL will yield NULL, so there is only a binary value for non-null records
- Since binary values will sort from left to right, the
MAX
function will always pick up the binary value with the highest versioning number
Then once we have our maximum binary value (0x00020004
), it is just a case of extracting the right hand side, and converting it back to the original data type.
Full Working Demo
DECLARE @T TABLE
(
ID INT NOT NULL,
Value1 VARCHAR(50),
Value2 VARCHAR(50),
value3 INT,
Versioning INT NOT NULL,
PRIMARY KEY (ID, Versioning)
);
INSERT @T (ID, Value1, Value2, Value3, Versioning)
VALUES
(1, 'sport', 'tennis', 2, 1),
(1, NULL, NULL, 4, 2),
(1, NULL, 'football', NULL, 3),
(1, 'game', NULL, NULL, 4);
SELECT t.ID,
Value1 = CONVERT(VARCHAR(50), SUBSTRING(MAX(CONVERT(BINARY(4), t.Versioning)
+ CONVERT(VARBINARY(50), t.Value1)), 5, 50)),
Value2 = CONVERT(VARCHAR(50), SUBSTRING(MAX(CONVERT(BINARY(4), t.Versioning)
+ CONVERT(VARBINARY(50), t.Value2)), 5, 50)),
Value3 = CONVERT(INT, SUBSTRING(MAX(CONVERT(BINARY(4), t.Versioning)
+ CONVERT(VARBINARY(50), t.Value3)), 5, 50))
FROM @T AS t
GROUP BY ID;
You can also use this method with window functions to add the last non-null value to each row, so if you wanted to fill in all nulls, with the last non null value you can:
DECLARE @T TABLE
(
ID INT NOT NULL,
Value1 VARCHAR(50),
Value2 VARCHAR(50),
value3 INT,
Versioning INT NOT NULL,
PRIMARY KEY (ID, Versioning)
);
INSERT @T (ID, Value1, Value2, Value3, Versioning)
VALUES
(1, 'sport', 'tennis', 2, 1),
(1, NULL, NULL, 4, 2),
(1, NULL, 'football', NULL, 3),
(1, 'game', NULL, NULL, 4);
SELECT t.ID,
ActualValue1 = t.Value1,
ActualValue2 = t.Value2,
ActualValue3 = t.Value3,
LastNonNUllValue1 = CONVERT(VARCHAR(50), SUBSTRING(MAX(Value1Bin) OVER(PARTITION BY t.ID ORDER BY t.Versioning), 5, 50)),
LastNonNUllValue2 = CONVERT(VARCHAR(50), SUBSTRING(MAX(Value2Bin) OVER(PARTITION BY t.ID ORDER BY t.Versioning), 5, 50)),
LastNonNUllValue3 = CONVERT(INT, SUBSTRING(MAX(Value3Bin) OVER(PARTITION BY t.ID ORDER BY t.Versioning), 5, 50)),
t.Versioning
FROM @T AS t
CROSS APPLY
( SELECT Value1Bin = CONVERT(BINARY(4), t.Versioning) + CONVERT(VARBINARY(50), t.Value1),
Value2Bin = CONVERT(BINARY(4), t.Versioning) + CONVERT(VARBINARY(50), t.Value2),
Value3Bin = CONVERT(BINARY(4), t.Versioning) + CONVERT(VARBINARY(50), t.Value3)
) AS b
ORDER BY t.Versioning;
Which gives:
ID ActualValue1 ActualValue2 ActualValue3 LastNonNUllValue1 LastNonNUllValue2 LastNonNUllValue3 Versioning
------------------------------------------------------------------------------------------------------------------------------
1 sport tennis 2 sport tennis 2 1
1 NULL NULL 4 sport tennis 4 2
1 NULL football NULL sport football 4 3
1 game NULL NULL game football 4 4
For more reading see Itzik Ben-Gan's The Last non NULL Puzzle