SQL Server Compensating for NULL values in SQL Queries

gxwragnw  于 2023-03-28  发布在  其他
关注(0)|答案(3)|浏览(173)

Let's say I have two tables of data that looks like this:

DvTestResults
| DVTR_DeviceNo | DVTR_TestedOnAt | DVTR_TesterNo |
| ------------ | ------------ | ------------ |
| DV00001 | 2022-08-11 14:15:16.000 | 0001 |
| DV00001 | 2022-08-19 21:08:16.000 | NULL |
| DV00001 | 2022-09-22 08:14:32.000 | NULL |
| DV00002 | 2023-06-03 18:18:03.000 | NULL |
| DV00002 | 2023-08-15 19:01:36.000 | 0007 |
| DV00003 | 2022-12-23 08:04:47.000 | 0014 |
| DV00003 | 2023-01-03 10:09:51.000 | 0014 |
| DV00003 | 2023-01-09 08:01:33.000 | 0014 |
| DV00004 | 2023-03-14 11:49:02.000 | 0298 |
| DV00004 | 2023-03-15 09:08:13.000 | 0298 |
| DV00005 | 2022-04-28 16:23:14.000 | NULL |
| DV00005 | 2022-08-14 08:20:56.000 | NULL |

Tester

T_TesterNoT_TesterName
0001John
0007Stacy
0014James
0298Carlos

I want to find the last time each device was tested and who tested it, ordered by device number without any device numbers repeating.

I have the following code:

SELECT DvTestResults.DVTR_DeviceNo, max.LastTimeTested, Tester.T_TesterName 
FROM DvTestResults
INNER JOIN
(
    SELECT DVTR_DeviceNo, MAX(DVTR_TestedOnAt) as LastTimeTested 
    FROM DvTestResults
    GROUP BY DVTR_DeviceNo
) as max
    on max.DVTR_DeviceNo = DvTestResults.DVTR_DeviceNo and max.LastTimeTested = DvTestResults.DVTR_TestedOnAt
INNER JOIN Tester ON Tester.TesterNo = DvTestResults.DVTR_TesterNo
ORDER BY DvTestResults.DVTR_DeviceNo

Unfortunately, while the code works fine for units where the last test's DVTR_TesterNo is not NULL, it does not give values for when it is. What would be a good solution if I wanted to have a list of the last times a device number was tested and who tested even if it came up NULL. Preferably, like with DV00001 in this example, the tester is typically the same name of the last person who tested it (in this case, John) — even though they didn't log in to do the last test that DV00001 did. So, for this example, I'd like an output for DV00001 of:

DVTR_DeviceNoLastTimeTestedT_TesterName
DV000012022-09-22 08:14:32.000John

For a device like DV00005, which has only ever been tested anonymously, I'd like an output of:

DVTR_DeviceNoLastTimeTestedT_TesterName
DV000052022-08-14 08:20:56.000Anonymous

Can anyone help?

rqcrx0a6

rqcrx0a61#

Getting the latest test per device is a typical top-1-per-group problem, which we can approach with row_number() and filtering:

select *
from (
    select r.*,
        row_number() over(partition by DVTR_DeviceNo order by DVTR_TestedOnAt desc) rn
    from DvTestResults r
) r
where rn = 1

We would then left join on the testers table to try and bring the tester’s name.

Preferably, like with DV00001 in this example, the tester is typically the same name of the last person who tested it (in this case, John)

Retrieving the latest tester (hence ignoring null values) is a more complex task in SQL Server. We can either use apply , or more window functions. The latter would be:

select r.DVTR_DeviceNo, r.DVTR_TestedOnAt, coalesce(t.T_TesterName, 'Anonymous') T_TesterName
from (
    select r.*,
        max(DVTR_TesterNo) over(partition by DVTR_DeviceNo, grp) LastDVTR_TesterNo
    from (
        select r.*,
            row_number()         over(partition by DVTR_DeviceNo order by DVTR_TestedOnAt desc) rn,
            count(DVTR_TesterNo) over(partition by DVTR_DeviceNo order by DVTR_TestedOnAt) grp
        from DvTestResults r
    ) r
) r
left join Tester t on t.T_TesterNo = r.LastDVTR_TesterNo
where r.rn = 1

Related: How to make LAG() ignore NULL s in SQL Server

Demo, based on the test data created by @marcothesane:
| DVTR_DeviceNo | DVTR_TestedOnAt | T_TesterName |
| ------------ | ------------ | ------------ |
| DV00001 | 2022-09-22 08:14:32.000 | John |
| DV00002 | 2023-08-15 19:01:36.000 | Stacy |
| DV00003 | 2023-01-09 08:01:33.000 | James |
| DV00004 | 2023-03-15 09:08:13.000 | Carlos |
| DV00005 | 2022-08-14 08:20:56.000 | Anonymous |

wrrgggsh

wrrgggsh2#

Based on my new understanding of the problem, this should work:

SELECT DVTR_DeviceNo, MAX(DVTR_TestedOnAt) As DVTR_TestedOnAt
    , coalesce(
        (
         SELECT T_TestName
         FROM (
            SELECT DVTR_TesterNo, DVTR_DeviceNo 
               , row_number() over 
                   (PARTITION BY DVTR_DeviceNo
                    ORDER BY case when DVTR_TesterNo IS NULL THEN 1 ELSE 0 END
                               ,DVTR_TestedOnAt DESC) rn     
            FROM DvTestResults
         ) dtr0
         LEFT JOIN Tester t ON t.T_TesterNo = dtr0.DVTR_TesterNo
         WHERE dtr0.rn = 1 AND dtr0.DVTR_DeviceNo = dtr.DVTR_DeviceNo
        ) 
      , 'Anonymous') T_TestName
FROM DvTestRestuls dtr
GROUP BY DVTR_DeviceNo
ebdffaop

ebdffaop3#

I would work with MAX(dvtr_testerno) OVER(PARTITION BY dvtr_deviceno) , in SQL Server, to compensate for the missing LAST_VALUE(a IGNORE NULLS) to backfill the missing foreign keys where I can, in a subquery on DVTestResults (I call that subquery w_testerno ), and then left join that subquery with the testers; the resulting NULLs for t_testername become 'Anonymous' thanks to an IFNULL() :

WITH
-- your input
dvtestresults(dvtr_deviceno,dvtr_testedonat,dvtr_testerno) AS (
          SELECT 'DV00001','2022-08-11 14:15:16.000',0001
UNION ALL SELECT 'DV00001','2022-08-19 21:08:16.000',NULL
UNION ALL SELECT 'DV00001','2022-09-22 08:14:32.000',NULL
UNION ALL SELECT 'DV00002','2023-06-03 18:18:03.000',NULL
UNION ALL SELECT 'DV00002','2023-08-15 19:01:36.000',0007
UNION ALL SELECT 'DV00003','2022-12-23 08:04:47.000',0014
UNION ALL SELECT 'DV00003','2023-01-03 10:09:51.000',0014
UNION ALL SELECT 'DV00003','2023-01-09 08:01:33.000',0014
UNION ALL SELECT 'DV00004','2023-03-14 11:49:02.000',0298
UNION ALL SELECT 'DV00004','2023-03-15 09:08:13.000',0298
UNION ALL SELECT 'DV00005','2022-04-28 16:23:14.000',NULL
UNION ALL SELECT 'DV00005','2022-08-14 08:20:56.000',NULL
)
,
tester(t_testerno,t_testername) aS (
          SELECT 0001,'John'
UNION ALL SELECT 0007,'Stacy'
UNION ALL SELECT 0014,'James'
UNION ALL SELECT 0298,'Carlos'
)
-- end of your input. Query starts here, replace following comma with "WITH"
,
w_testerno AS (
  SELECT
    dvtr_deviceno
  , dvtr_testedonat
  , MAX(dvtr_testerno) OVER(
      PARTITION BY dvtr_deviceno 
    ) AS dvtr_testerno
  FROM dvtestresults
)
SELECT 
  dvtr_deviceno
, dvtr_testedonat
, isnull(t_testername,'Anonymous') AS t_testermane
FROM      w_testerno
LEFT JOIN tester ON t_testerno = dvtr_testerno
ORDER BY 1,2;
dvtr_devicenodvtr_testedonatt_testermane
DV000012022-08-11 14:15:16John
DV000012022-08-19 21:08:16John
DV000012022-09-22 08:14:32John
DV000022023-06-03 18:18:03Anonymous
DV000022023-08-15 19:01:36Stacy
DV000032022-12-23 08:04:47James
DV000032023-01-03 10:09:51James
DV000032023-01-09 08:01:33James
DV000042023-03-14 11:49:02Carlos
DV000042023-03-15 09:08:13Carlos
DV000052022-04-28 16:23:14Anonymous
DV000052022-08-14 08:20:56Anonymous

相关问题