display oldest value with newest value at id level - sql

I have a table that looks something like this where ids are displayed with old value with the new value (the one it just got changed into).
IF OBJECT_ID('tempdb.dbo.#TT','U')IS NOT NULL
DROP TABLE #TT;
SELECT *
INTO #TT
FROM (
SELECT 1 AS ID, 'A' AS OldValue, 'B' AS NewValue, CONVERT(DATE,'20190421') AS [Date]
UNION ALL
SELECT 1 AS ID, 'B' AS OldValue, 'C' AS NewValue, CONVERT(DATE,'20190423') AS [Date]
UNION ALL
SELECT 2 AS ID, 'D' AS OldValue, 'E' AS NewValue, CONVERT(DATE,'20190422') AS [Date]
UNION ALL
SELECT 3 AS ID, 'J' AS OldValue, 'K' AS NewValue, CONVERT(DATE,'20190422') AS [Date]
UNION ALL
SELECT 3 AS ID, 'K' AS OldValue, 'L' AS NewValue, CONVERT(DATE,'20190423') AS [Date]
UNION ALL
SELECT 3 AS ID, 'L' AS OldValue, 'M' AS NewValue, CONVERT(DATE,'20190424') AS [Date]
) T
;
I want to display each id and old value with newest value available for them. For example ID=1 and OldValue = A should display C or ID = 3 and OldValue = K should display M
I have tried to do recursive cte as follows:
WITH RecCTE AS (
SELECT ID, OldValue, NewValue
FROM #TT
UNION ALL
SELECT A.ID, B.OldValue, A.NewValue
FROM #TT A
INNER JOIN RecCTE B ON A.ID = B.ID AND B.NewValue = A.OldValue
)
SELECT *
FROM RecCTE
With that recursive query. I am getting some line correctly but I am also getting extra lines in between:
| ID | OldValue | NewValue |
|-----------|---------------|----------------|
| 1 | A | B |
| 1 | B | C |
| 2 | D | E |
| 3 | J | K |
| 3 | K | L |
| 3 | L | M |
| 3 | K | M |
| 3 | J | L |
| 3 | J | M |
| 1 | A | C |
I want to get results like this:
| ID | OldValue | NewValue |
|-----------|---------------|----------------|
| 1 | A | C |
| 1 | B | C |
| 2 | D | E |
| 3 | J | M |
| 3 | K | M |
| 3 | L | M |

I think taking the last row should work:
WITH RecCTE AS (
SELECT ID, OldValue, NewValue, 1 as lev
FROM #TT
UNION ALL
SELECT A.ID, B.OldValue, A.NewValue, lev + 1
FROM #TT A JOIN
RecCTE B
ON A.ID = B.ID AND B.NewValue = A.OldValue
)
SELECT *
FROM (SELECT r.*, ROW_NUMBER() OVER (PARTITION BY id, OldValue ORDER BY lev DESC) as seqnum
FROM RecCTE r
) r
WHERE seqnum = 1;

If the data works the way it looks to me like it works, the following will do it without using recursion:
;WITH cte as (
-- List of all, ordered by ID, with the most recent being the first listed
SELECT
ID
,NewValue
,row_number() over (partition by Id order by Date desc) Ranking
from #TT
)
select
tt.Id
,tt.OldValue
,cte.NewValue
from #TT tt
-- Join table to the "most recent" row for the ID
inner join cte
on cte.ID = tt.Id
and cte.Ranking = 1

Related

Assign unique id based on combination in sql

The data looks like this:
Need to assign id based on the combination of 2 columns and get the id of each value in 2 columns
final output should look like:
I tried with
WITH RNS AS (
SELECT *, ROW_NUMBER() OVER () AS rn
FROM test
),
IDS AS (
SELECT t1.coLA, t1.colB, t1.rn, MIN(COALESCE(t2.rn, t1.rn)) AS id
FROM RNS t1
LEFT JOIN RNS t2 ON t1.rn > t2.rn
AND (t1.colA = t2.colA OR t1.colA = t2.colB OR
t1.colB = t2.colA OR t1.colB = t2.colB)
GROUP BY t1.coLA, t1.colB, t1.rn
ORDER BY t1.rn
)
SELECT colA, colB, DENSE_RANK() OVER (ORDER BY id) AS id
FROM IDS
ORDER BY rn
but not working as expected
Using RECURSIVE CTE in BigQuery, you may try below query
WITH RECURSIVE test AS (
SELECT * EXCEPT(offset)
FROM UNNEST(SPLIT('abaaeghjc', '')) colA WITH OFFSET
JOIN UNNEST(SPLIT('bccdfhikl', '')) colB WITH OFFSET USING (offset)
),
IDS AS (
SELECT *, DENSE_RANK() OVER (ORDER BY colA) id
FROM test t1
WHERE NOT EXISTS (SELECT 1 FROM test t2 WHERE t1.colA = t2.colB)
UNION ALL
SELECT t.*, id FROM IDS i JOIN test t ON i.colB = t.colA
)
SELECT DISTINCT col, id FROM IDS, UNNEST([colA, colB]) col
ORDER BY 1;
Query results:
+-----+----+
| col | id |
+-----+----+
| a | 1 |
| b | 1 |
| c | 1 |
| d | 1 |
| e | 2 |
| f | 2 |
| g | 3 |
| h | 3 |
| i | 3 |
| j | 4 |
| k | 4 |
| l | 1 |
+-----+----+

How to use pivot to select and flatten table?

I'm trying to select from a table that essentially is a hierarchy of groups and fields in each group. Each row has a group id column and I'm trying to flatten it into rows of each group id and their fields.
For example
group id | field1
1 | a
1 | b
1 | a
1 | b
2 | c
2 | d
2 | c
2 | d
3 | e
3 | f
3 | g
3 | e
3 | f
3 | g
4 | h
It is guaranteed that a group will map to the same fields values so group 1 will always have the same number of rows with field 'a' as with field 'b'.
The target is this:
group id | field1 | field2 | field 3
1 | a | b | null
2 | c | d | null
3 | e | f | g
4 | h | null | null
I have been playing with over (order by group id) but I haven't made any progress with that or pivots either.
I wouldn't use pivot. I would use conditional aggregation and dense_rank():
select group_id,
max(case when seqnum = 1 then field1 end) as field1,
max(case when seqnum = 2 then field1 end) as field2,
max(case when seqnum = 3 then field1 end) as field3
from (select t.*,
dense_rank() over (partition by group_id order by field1) as seqnum
from t
) t
group by group_id
Not sure this will solve your problem. If you are generating any report then you can use LISTAGG function.
select listagg( field_1 , ',') within group (order by group_id)
from (
select distinct group_id, field_1 from table
);

T-SQL How to change hierarchy levels to ascend instead of descend?

I have very simple query that show all parents of all childs:
;with cte as (
select 1 as Level, Child, Parent from TABLE
union all
select t.Level + 1, t.Child, t.Parentfrom TABLE t
inner join cte on t.Child = cte.Parent
)
select distinct * from t
output:
Level|Child|Parent|
3 | A | |
2 | B | A |
1 | C | B |
1 | D | B |
How do I achieve the levels to be in ascending order, so that the top parent begins with 1, for example:
Level|Child|Parent|
1 | A | |
2 | B | A |
3 | C | B |
3 | D | B |
Thank you.
You can use DENSE_RANK in your final query. Something like:
SELECT DENSE_RANK() OVER (ORDER BY [Level] DECS) AS [Level]
Start with parents and work your way down:
with cte as (
select 1 as Level, Child, Parent
from TABLE
where parent is null
union all
select cte.Level + 1, t.Child, t.Parent
from TABLE t join
cte
on cte.Child = t.Parent
)
select *
from cte;
Here is a db<>fiddle.

Suggest SQL query for given use case

Original Table
Id | Time | Status
------------------
1 | 5 | T
1 | 6 | F
2 | 3 | F
1 | 2 | F
2 | 4 | T
3 | 7 | F
2 | 3 | T
3 | 1 | F
4 | 7 | H
4 | 6 | S
4 | 5 | F
4 | 4 | T
5 | 5 | S
5 | 6 | F
Expected Table
Id | Time | Status
------------------
1 | 6 | F
3 | 7 | F
4 | 5 | F
I want all the distinct ids who have status as F but time should be maximum, if for any id status is T for given maximum time then that id should not be picked. Also only those ids should be picked who have at-least one T. For e.g 4 will not be picked at it doesn't have any 'T' as status.
Please help in writing the SQL query.
You can use EXISTS and NOT EXISTS in the WHERE clause:
select t.*
from tablename t
where t.status = 'F'
and exists (select 1 from tablename where id = t.id and status = 'T')
and not exists (
select 1
from tablename
where id = t.id and status in ('F', 'T') and time > t.time
)
See the demo.
Results:
| Id | Time | Status |
| --- | ---- | ------ |
| 1 | 6 | F |
| 4 | 5 | F |
Try the below way -
select * from tablename t
where time = (select max(time) from tablename t1 where t.id=t1.id and Status='F')
and Status='F'
the following should work
select id,max(time) as time,status
from table
where status='F'
group by id,status
select id, max(time), status
from stuff s
where status = 'F'
and id not in (
select id
from stuff s2
where s2.id = s.id
and s2.time > s.time
and s2.status = 'T')
group by id, status;
You can see the Fiddle here.
As I understand it, you want to find the highest time for each ID (max(time)) where the status is F, but only if there isn't a later record where the status is 'T'. The sub query filters out records where there exists a later record where the status is T.
WITH MAX_TIME_ID AS (
SELECT
ID
,MAX(TIME) AS MAX_TIME
GROUP BY
ID
)
SELECT
O.*
FROM
ORIGINAL_TABLE O
INNER JOIN
MAX_TIME_ID MAX
ON
O.ID = MAX.ID
WHERE
O.STATUS = 'F'
The CTE will find the max time for each ID and the inner join with the where clause on the status will select it only if the latest is 'F'.
I would just use window functions:
select t.*
from (select t.*
row_number() over (partition by id order by time desc) as seqnum,
sum(case when status = 'T' then 1 else 0 end) over (partition by id) as num_t
from t
) t
where num_t > 0 and
seqnum = 1 and status = 'F';
There is a another fun way to do this just with aggregation:
select id, max(time) as time, 'F' as status
from t
group by id
having sum(case when status = 'T' then 1 else 0 end) > 0 and
max(time) = max(case when status 'F' then time end);

how to select rows with same column_a but different column_b?

I want to select rows in sql server, there's my questions below:
Table1
--------------------------
| Name | Type |
--------------------------
| A | 1 |
| A | 2 |
| B | 1 |
| B | 3 |
| A | 3 |
| C | 1 |
| C | 3 |
| D | 1 |
| D | 2 |
| D | 3 |
| . | . |
| . | . |
Select rows like below:
Table2
--------------------------
| Name | Type |
--------------------------
| A | 1 |
| A | 2 |
| A | 3 |
| D | 1 |
| D | 2 |
| D | 3 |
| . | . |
| . | . |
The select rules is...
Show Name and Type which Type must have 1,2 and 3.
Example: A had 1,2,3 types,so i would select it.
Example: B only has 1,2 types,so i wouldn't select it.
You can use window functions for this:
select name, type
from (
select
t.*,
sum(case when type in (1, 2, 3) then 1 else 0 end)
over(partition by name) cnt
from mytable t
) t
where cnt = 3
This assumes that each (name, type) tuple occurs only once in the original table, which is consistant with your sample data.
Demo on DB Fiddle:
name | type
:--- | ---:
A | 1
A | 2
A | 3
D | 1
D | 2
D | 3
You could use INNER JOINs on the three Type columns to achieve this:
SELECT Table1.[Name],
Table1.[Type]
FROM Table1
INNER JOIN (
SELECT [Name]
FROM Table1
WHERE ([Type] = 1)
) A ON A.[Name] = Table1.[Name]
INNER JOIN (
SELECT [Name]
FROM Table1
WHERE ([Type] = 2)
) B ON B.[Name] = A.[Name]
INNER JOIN (
SELECT [Name]
FROM Table1
WHERE ([Type] = 3)
) C ON C.[Name] = A.[Name]
This outputs:
Name Type
A 1
A 2
A 3
D 1
D 2
D 3
The matching sqlfiddle.
This works by returning rows that contain [Type] = 1, and then ONLY matching rows where [Type] = 2 and [Type] = 3. Then this is joined back to your main table and the results are returned.
Get the names with group by name and set the condition in the having clause:
select * from Table1
where name in (
select name
from Table1
group by name
having count(distinct type) = 3
)
If there are for the column Type other values than 1, 2, 3 then:
select * from Table1
where type in (1, 2, 3) and name in (
select name
from Table1
where type in (1, 2, 3)
group by name
having count(distinct type) = 3
)
See the demo.
Results:
> Name | Type
> :--- | ---:
> A | 1
> A | 2
> A | 3
> D | 1
> D | 2
> D | 3
you can use string_agg if it is sql server 2017 and above or Azure SQL as below:
Select * from #yourTable yt join (
select [name], string_agg([Type], ',') as st_types
from #YourTable
group by [name] ) a
on yt.name = a.[name] and a.st_types like '%1,2,3%'
I give you this, this will work if you have:
A 1
A 2
A 3
A 2
It will then only give you B.
SELECT *
FROM Table1
WHERE Name in (
SELECT Name from
(
SELECT Name, Type, count(Name) c from Table1 where Type = 1
GROUP BY Name, Type
HAVING count(Name) = 1
UNION
SELECT Name, Type, count(Name) c from Table1 where Type = 2
GROUP by Name, Type
HAVING count(Name) = 1
UNION
SELECT Name, Type, count(Name) c from Table1 where Type = 3
GROUP by Name, Type
HAVING count(Name) = 1) t
GROUP by name
HAVING count(c) = 3)
Here is the DEMO