jumps4fun
jumps4fun

Reputation: 4132

How do I select max value from a table, based on unique value on joined tables?

I have three tables. All follow the CRUD-pattern for inserts and updates. I need to make a query qhich selects all latest updates in one table, based on unique values of another table, twice removed through joins: (I have left out the CRUD structure on two of the tables in the example)

table COPY_HISTORY

COPY_ID | DATA_ID | STATUS | TIME  |        
1       |  A      | open   | 10:34 |
1       |  A      | locked | 10:37 |
2       |  A      | open   | 10:38 |
3       |  B      | open   | 11:29 |
4       |  C      | open   | 10:37 |
5       |  D      | locked | 09:34 |       

table DATA_SET

DATA_ID | LOCATION |    
A       | 88       | 
B       | 77       | 
C       | 88       | 
D       | 99       | 
E       | 88       | 
F       | 88       |   

table COPY_RULES

LOCATION_FROM | LOCATION_TO 
55            | 110 
66            | 120
77            | 120
88            | 130
99            | 130

What I need to do, is get the status for each LOCATION_TO from the copy rules table. If a copy is executed, it will be registered in copy history, and a copy will always overwrite all data at a location (110,120,130).

In this case, location 110 has never had a copy happen, and 110 should not be included in the returned data at all. Even though it exists in the rules, that is not relevant. 120 will receive copies from data sets on 66 and 77. However, we have no data set on 66 yet, so only 77 is relevant. B is located at 77, and data set B had a copy history registration inserted at 11:29, with the status open. Since this is the only status belonging to 120, status is open for 120, and overwrite will be possible for location 120.

Location 130 however, receive copies from both 88 and 99. This means that data sets A, C, D, E and F all will copy into 130, as they are all located in 88 or 99. We have two copy history logs for A, and one each for C and D. Meaning I want the status of only the newest registration, in this case, 10:38.

Now, I have tried to select the maximum value from COPY_HISTORY, based on the LOCATION_TO, but I am at an impasse getting the LOCATION_TO to be unique

I try:

SELECT cr.LOCATION_TO, ch.STATUS, ch.TIME FROM COPY_HISTORY ch
JOIN DATA_SET ds ON ch.DATA_ID = ds.DATA_ID
JOIN COPY_RULES cr ON cr.LOCATION_FROM = ds.LOCATION 
WHERE ch.TIME = (SELECT MAX(TIME) FROM COPY_HISTORY
                WHERE COPY_ID = ch.COPY_ID
                AND ch.DATA_ID = ds.DATA_ID
                AND ds.LOCATION = cr.LOCATION_FROM)

That select max time statement is incomplete. The two AND-lines does nothing at all. The only thing I can accomplish, is getting rid of the first line in COPY_HISTORY, meaning I can get all max times for each COPY_ID, but I can not figure out how to filter them to get only the maximum for each LOCATION_TO instead.

I managed to get the max value from one single LOCATION_TO, by doing this:

SELECT cr.LOCATION_TO, ch.STATUS, ch.TIME FROM COPY_HISTORY ch
JOIN DATA_SET ds ON ch.DATA_ID = ds.DATA_ID
JOIN COPY_RULES cr ON cr.LOCATION_FROM = ds.LOCATION 
WHERE ch.TIME = (SELECT MAX(TIME) FROM COPY_HISTORY ch2, DATA_SET ds2, COPY_RULES cs2 
                WHERE ch2.DATA_ID = ds2.DATA_ID
                AND ds2.LOCATION = cr2.LOCATION_FROM 
                AND cr2.LOCATION_TO = 2180)

However, this does not solve my issue of getting the list of statuses for all LOCATION_TO.

DESIRED OUTPUT:

LOCATION_TO | STATUS  | TIME  |
120         | open    | 11:29 |
130         | open    | 10:38 |

Valid rows in COPY_HISTORY would be line 3 and line 4

This is an attempt at an extreme simplification of the actual database structure, so there are risks of typos in this question.

DDL

create table #COPY_HISTORY (COPY_ID int, DATA_ID char(1), [STATUS] varchar(16), [TIME] time)
create table #DATA_SET (DATA_ID char(1), [LOCATION] int)
create table #COPY_RULES (LOCATION_FROM int, LOCATION_TO int)

insert into #COPY_HISTORY
values
(1,'A','open','10:34'),
(1,'A','locked','10:37'),
(2,'A','open','10:38'),
(3,'B','open','11:29'),
(4,'C','open','10:37'),
(5,'D','locked','09:34')

insert into #DATA_SET     
values
('A',88),
('B',77), 
('C',88), 
('D',99), 
('E',88), 
('F',88)


insert into #COPY_RULES
values
(55,110), 
(66,120),
(77,120),
(88,130),
(99,130)

Upvotes: 0

Views: 53

Answers (2)

scratt
scratt

Reputation: 190

I think this query will work for you. As Ryan mentioned in the comments, you can use the ROW_NUMBER analytical function to rank your location records (PARTITION BY) by the time (ORDER BY), then only return the first row for each location (RowOrder = 1).

SELECT * FROM (
SELECT cr.LOCATION_TO, ch.STATUS, ch.TIME, ROW_NUMBER() OVER (PARTITION BY cr.LOCATION_TO ORDER BY ch.TIME desc) AS RowOrder
FROM #COPY_HISTORY ch
JOIN #DATA_SET ds ON ch.DATA_ID = ds.DATA_ID
JOIN #COPY_RULES cr ON cr.LOCATION_FROM = ds.LOCATION 
) ordered_set
WHERE RowOrder = 1

Upvotes: 4

Dohsan
Dohsan

Reputation: 361

If I'm getting the logic correctly:

DECLARE @COPY_HISTORY TABLE
(
COPY_ID INT NOT NULL,
DATA_ID CHAR(1) NOT NULL,
[STATUS] VARCHAR(50) NOT NULL,
[TIME] TIME NOT NULL
);

DECLARE @DATA_SET TABLE
(
DATA_ID CHAR(1) NOT NULL,
LOCATION INT NOT NULL
);

DECLARE @COPY_RULES TABLE
(
LOCATION_FROM INT NOT NULL,
LOCATION_TO INT NOT NULL
);

INSERT INTO @COPY_HISTORY
VALUES (1,'A','open','10:34'),
(1,'A','locked','10:37'),
(2,'A','open','10:38'),
(3,'B','open','11:29'),
(4,'C','open','10:37'),
(5,'D','locked','09:34');

INSERT INTO @DATA_SET
VALUES ('A',88),
('B',77),
('C',88),
('D',99),
('E',88),
('F',88);

INSERT INTO @COPY_RULES
VALUES (55,110),
(66,120),
(77,120),
(88,130),
(99,130);

WITH CTE
AS
(
    SELECT  CR.LOCATION_TO,
            CH.STATUS,
            CH.TIME,
            ROW_NUMBER() OVER(PARTITION BY CR.LOCATION_TO ORDER BY CASE WHEN CH.Status = 'OPEN' THEN 1 ELSE 0 END DESC, CH.TIME DESC) AS RN
    FROM    @COPY_HISTORY AS CH
    INNER
    JOIN    @DATA_SET AS DS
            ON  CH.DATA_ID = DS.DATA_ID
    INNER
    JOIN    @COPY_RULES AS CR
            ON  CR.LOCATION_FROM = DS.LOCATION
)
SELECT  C.LOCATION_TO,
        C.STATUS,
        C.TIME
FROM    CTE AS C
WHERE   RN = 1;

Upvotes: 2

Related Questions