How to use pivot to select and flatten table? - sql

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
);

Related

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 list the latest series with no gaps of a given clause?

Given the following example table:
+-----------+
| Id | Name |
+----+------+
| 1 | A |
| 2 | B |
| 3 | B |
| 4 | C |
| 5 | A |
| 6 | B |
| 7 | B |
| 8 | B |
| 9 | B |
| 10 | X |
+----+------+
I would like a query to get the following result:
+----+------+
| 6 | B |
| 7 | B |
| 8 | B |
| 9 | B |
+----+------+
The best query I could do was:
SELECT * FROM
(SELECT id, name, LEAD(id) OVER (ORDER BY id) t
FROM test WHERE name = 'B' ORDER BY id)
WHERE ID <> t-1;
sqlfiddle here
If you want the length and where it starts:
select min(id), max(id)
from (select t.*,
row_number() over (order by id) as seqnum,
row_number() over (partition by name order by id) as seqnum_1
from test t
) t
where name = 'B'
group by (seqnum - seqnum_1)
order by min(id) desc
fetch first 1 row only;
You can join back to the table to get the original rows.
Another method using window functions to count the number of non-Bs after a given row . . . and then choose the first:
select t.*
from (select t.*,
dense_rank() over (order by nonbs_after asc) as grp
from (select t.*,
sum(case when name <> 'B' then 1 else 0 end) over (order by id desc) as nonbs_after
from test t
) t
where name = 'B'
) t
where grp = 1;
Here is a db<>fiddle.

Conditional Group By in SQL

I have the following table that I want to group by type. When there are multiple rows with the same type (e.g., A & B type), I want to preserve the 'value' from the row with the highest rank (i.e., primary > secondary > tertiary..)
rowid | type | rank | value
1 | A | primary | 1
2 | A | secondary | 2
3 | B | secondary | 3
4 | B | tertiary | 4
5 | C | primary | 5
So the resulting table should look like
rowid | type | rank | value
1 | A | primary | 1
3 | B | secondary | 3
5 | C | primary | 5
Any suggestions will be highly appreciated!
p.s., I'm working in MS SQL Server.
You can use row_number(). Here is a simple'ish method:
select t.*
from (select t.*,
row_number() over (partition by type
order by charindex(rank, 'primary,secondary,tertiary')
) as seqnum
from t
) t
where seqnum = 1;
This uses charindex() as a simple method of ordering the ranks.
try this,
;WITH CTE
AS (
SELECT *
,row_number() OVER (
PARTITION BY [type] ORDER BY value
) rn
FROM #t
)
SELECT *
FROM cte
WHERE rn = 1
Another way of doing is with Row_Number and an Order By specifying your rule with CASE.
Schema:
CREATE TABLE #TAB(rowid INT, [type] VARCHAR(1), rankS VARCHAR(50) , value INT)
INSERT INTO #TAB
SELECT 1 , 'A' , 'primary' , 1
UNION ALL
SELECT 2 , 'A' , 'secondary', 2
UNION ALL
SELECT 3 , 'B' , 'secondary' , 3
UNION ALL
SELECT 4 , 'B' , 'tertiary' , 4
UNION ALL
SELECT 5 , 'C' , 'primary' , 5
Now apply rank rule with Row_Number
SELECT * FROM (
SELECT ROW_NUMBER() OVER(PARTITION BY [type] ORDER BY (CASE rankS
WHEN 'primary' THEN 1
WHEN 'secondary' THEN 2
WHEN 'tertiary' THEN 3 END )) AS SNO, * FROM #TAB
)A
WHERE SNO =1
Result:
+-----+-------+------+-----------+-------+
| SNO | rowid | type | rankS | value |
+-----+-------+------+-----------+-------+
| 1 | 1 | A | primary | 1 |
| 1 | 3 | B | secondary | 3 |
| 1 | 5 | C | primary | 5 |
+-----+-------+------+-----------+-------+

Showing values in two different colums based off a joining ID

I'm trying to create a select statement for SQL Server, as per the title.
My data is:
ID | JoiningID | Value |
1 | 1 | 10
2 | 1 | 11
3 | 2 | 10
4 | 2 | 10
5 | 3 | 15
6 | 3 | 9
I'm trying to output three columns which would look like:
JoiningID | Column1 Value | Column2 value
1 | 10 | 11
2 | 10 | 10
3 | 15 | 9
The condition of this is that it needs to be within the select statement as opposed to within a where statement.
Thanks for your time.
You can enumerate the values and then use pivot or conditional aggregation:
select joiningid,
max(case when seqnum = 1 then value end) as value1,
max(case when seqnum = 2 then value end) as value2
from (select t.*, row_number() over (partition by joiningid order by id) as seqnum
from t
) t
group by joiningid;
You could simply do..
SELECT * FROM TABLE1
WHERE JoiningID IN(SELECT JoiningID from TABLE2)
or..
SELECT * FROM TABLE1
WHERE JoiningID NOT IN(SELECT JoiningID from TABLE2)
Or, you could do a JOIN or LEFT JOIN..

Merge multiple rows in SQL with tie breaking on primary key

I have a table with data like the following
key | A | B | C
---------------------------
1 | x | 0 | 1
2 | x | 2 | 0
3 | x | NULL | 4
4 | y | 7 | 1
5 | y | 3 | NULL
6 | z | NULL | 4
And I want to merge the rows together based on column A with largest primary key being the 'tie breaker' between values that are not NULL
Result
key | A | B | C
---------------------------
1 | x | 2 | 4
2 | y | 3 | 1
3 | z | NULL | 4
What would be the best way to achieve this assuming my data is actually 40 columns and 1 million rows with an unknown level of duplications?
Using ROW_NUMBER and conditional aggregation:
SQL Fiddle
WITH cte AS(
SELECT *,
rnB = ROW_NUMBER() OVER(PARTITION BY A ORDER BY CASE WHEN B IS NULL THEN 0 ELSE 1 END DESC, [key] DESC),
rnC = ROW_NUMBER() OVER(PARTITION BY A ORDER BY CASE WHEN C IS NULL THEN 0 ELSE 1 END DESC, [key] DESC)
FROM tbl
)
SELECT
[key] = ROW_NUMBER() OVER(ORDER BY A),
A,
B = MAX(CASE WHEN rnB = 1 THEN B END),
C = MAX(CASE WHEN rnC = 1 THEN C END)
FROM cte
GROUP BY A