Kittoes0124
Kittoes0124

Reputation: 5080

How can I get the first result for each account in this SQL query?

I'm trying to write a query that follows this logic:

Find the first following status code of an account that had a previous status code of X.

So if I have a table of:

id    account_num    status_code
64        1               X
82        1               Y
72        2               Y
87        1               Z
91        2               X
103       2               Z

The results would be:

id   account_num     status_code
82        1               Y
103       2               Z

I've come up with a couple of solutions but I'm not all that great with SQL and so they've been pretty inelegeant thus far. I was hoping that someone here might be able to point me in the right direction.

View:

SELECT account_number, id
FROM   table
WHERE  status_code = 'X'

Query:

SELECT account_number, min(id)
FROM   table
INNER JOIN view
ON table.account_number = view.account_number
WHERE table.id > view.id

At this point I have the id that I need but I'd have to write ANOTHER query that uses the id tolook up the status_code.

Edit: To add some context, I'm trying to find calls that have a status_code of X. If a call has a status_code of X we want to dial it a different way the next time we make an attempt. The aim of this query is to provide a report that will show the results of the second dial if the first dial resulted an X status code.

Upvotes: 3

Views: 403

Answers (4)

Serge Belov
Serge Belov

Reputation: 5803

Here's a SQL Server solution.

UPDATE

The idea is to avoid a number of NESTED LOOP joins as proposed by Olaf because they roughly have O(N * M) complexity and thus extremely bad for your performance. MERGED JOINS complexity is O(NLog(N) + MLog(M)) which is much better for real world scenarios.

The query below works as follows:

RankedCTE is a subquery that assigns a row number to each id partioned by account and sorted by id which represents the time. So for the data below the output of this

SELECT 
    id, 
    account_num, 
    status_code,
    ROW_NUMBER() OVER (PARTITION BY account_num ORDER BY id DESC) AS item_rank
FROM dbo.Test

would be:

id          account_num status_code item_rank
----------- ----------- ----------- ----------
87          1           Z           1
82          1           Y           2
64          1           X           3
103         2           Z           1
91          2           X           2
72          2           Y           3

Once we have them numbered we join the result on itself like this:

WITH RankedCTE AS
(
    SELECT 
        id, 
        account_num, 
        status_code,
        ROW_NUMBER() OVER (PARTITION BY account_num ORDER BY id DESC) AS item_rank
    FROM dbo.Test
)
SELECT 
    *
FROM
    RankedCTE A
    INNER JOIN RankedCTE B ON 
            A.account_num = B.account_num
            AND A.item_rank = B.item_rank - 1    

which will give us an event and a preceding event in the same table

id          account_num status_code item_rank   id          account_num status_code item_rank
----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
87          1           Z           1           82          1           Y           2
82          1           Y           2           64          1           X           3
103         2           Z           1           91          2           X           2
91          2           X           2           72          2           Y           3

Finally, we just have to take the preceding event with code "X" and the event with code not "X":

    WITH RankedCTE AS
    (
        SELECT 
            id, 
            account_num, 
            status_code,
            ROW_NUMBER() OVER (PARTITION BY account_num ORDER BY id DESC) AS item_rank
        FROM dbo.Test
    )
    SELECT 
        A.id, 
        A.account_num, 
        A.status_code
    FROM 
        RankedCTE A
        INNER JOIN RankedCTE B ON 
            A.account_num = B.account_num
            AND A.item_rank = B.item_rank - 1
            AND A.status_code <> 'X'
            AND B.status_code = 'X'

Query plans for this query and @Olaf Dietsche solution (one of the versions) are below.

query plans

Data setup script

CREATE TABLE dbo.Test
(
    id int not null PRIMARY KEY,
    account_num int not null,
    status_code nchar(1)
)
GO

INSERT dbo.Test (id, account_num, status_code)
SELECT 64 ,       1,               'X' UNION ALL
SELECT 82 ,       1,               'Y' UNION ALL
SELECT 72 ,       2,               'Y' UNION ALL
SELECT 87 ,       1,               'Z' UNION ALL
SELECT 91 ,       2,               'X' UNION ALL
SELECT 103,       2,               'Z'

Upvotes: 2

Olaf Dietsche
Olaf Dietsche

Reputation: 74078

SQL Fiddle with subselect

select id, account_num, status_code
from mytable
where id in (select min(t1.id)
             from mytable t1
             join mytable t2 on t1.account_num = t2.account_num
                             and t1.id > t2.id
                             and t2.status_code = 'X'
             group by t1.account_num)

and SQL Fiddle with join, both for MS SQL Server 2012, both returning the same result.

select id, account_num, status_code
from mytable
join (select min(t1.id) as min_id
      from mytable t1
      join mytable t2 on t1.account_num = t2.account_num
                      and t1.id > t2.id
                      and t2.status_code = 'X'
      group by t1.account_num) t on id = min_id

Upvotes: 2

Victor Sorokin
Victor Sorokin

Reputation: 12006

Here's query, with your data, checked under PostgreSQL:

SELECT t0.*
FROM so13594339 t0 JOIN
 (SELECT min(t1.id), t1.account_num
   FROM so13594339 t1, so13594339 t2
   WHERE t1.account_num = t2.account_num AND t1.id > t2.id AND t2.status_code = 'X'
   GROUP BY t1.account_num
 ) z
 ON t0.id = z.min AND t0.account_num = z.account_num;

Upvotes: 0

Majid Laissi
Majid Laissi

Reputation: 19788

SELECT MIN(ID), ACCOUNT_NUM, STATUS_CODE FROM (
    SELECT ID,  ACCOUNT_NUM, STATUS_CODE
    FROM ACCOUNT A1
    WHERE EXISTS 
       (SELECT 1 
        FROM ACCOUNT A2 
        WHERE A1.ACCOUNT_NUM = A2.ACCOUNT_NUM
         AND A2.STATUS_CODE = 'X'
         AND A2.ID < A1.ID)
    ) SUB
GROUP BY ACCOUNT_NUM

Here's an SQLFIDDLE

Upvotes: 0

Related Questions