Oracle 11g SQL: Pivot Table -- Variable number of columns - sql

I am trying to take a table and pivot the values of two columns to their own columns. The twist is that there can be variable numbers of entries per anchor. Here is a toy table:
CREATE TABLE ATTRS
( WIDGET VARCHAR2(15),
A_NAME VARCHAR2(15),
A_VALUE VARCHAR2(15)
);
INSERT INTO ATTRS VALUES ('BOOK','PAGES','1000');
INSERT INTO ATTRS VALUES ('BOOK','COLOR','GREEN');
INSERT INTO ATTRS VALUES ('BOOK','LAST','TWAIN');
INSERT INTO ATTRS VALUES ('BOOK','FIRST','MARK');
INSERT INTO ATTRS VALUES ('CELLPHONE','BRAND','SAMSUNG');
INSERT INTO ATTRS VALUES ('LAPTOP','BRAND','LENOVO');
INSERT INTO ATTRS VALUES ('LAPTOP','COLOR','BLACK');
INSERT INTO ATTRS VALUES ('LAPTOP','BATTERY','STANDARD');
I will know the maximum number of unique A_NAME that can occur (we'll let it be 4 in this example) and want output like this:
WIDGET | A_NAME1 | A_VALUE1 | A_NAME2 | A_VALUE2 | A_NAME3 | A_VALUE3 | A_NAME4 | A_VALUE4
------------------------------------------------------------------------------------------
BOOK | PAGES | '1000' | COLOR | 'GREEN' | LAST | 'TWAIN' | FIRST | 'MARK'
CELLPHONE | BRAND | 'SAMSUNG'| (null) | (null) | (null) | (null) | (null) | (null)
LAPTOP | COLOR | 'BLACK' | BRAND | 'LENOVO' | BATTERY | 'STANDARD' | (null) | (null)
Note that order does not matter, i.e. if two A_NAME are the same, they need not be in the same column.
Thanks.

I'd have to think for a minute about how to accomplish this with the new (in 11g) pivot operator. Knowing that there are at most 4 rows per widget, though, you can do an old-school pivot along the lines of
SELECT widget,
max(CASE WHEN rn = 1 THEN a_name ELSE NULL END) a_name1,
max(CASE WHEN rn = 1 THEN a_value ELSE NULL END) a_value1,
max(CASE WHEN rn = 2 THEN a_name ELSE NULL END) a_name2,
max(CASE WHEN rn = 2 THEN a_value ELSE NULL END) a_value2,
max(CASE WHEN rn = 3 THEN a_name ELSE NULL END) a_name3,
max(CASE WHEN rn = 3 THEN a_value ELSE NULL END) a_value3,
max(CASE WHEN rn = 4 THEN a_name ELSE NULL END) a_name4,
max(CASE WHEN rn = 4 THEN a_value ELSE NULL END) a_value4
FROM( SELECT widget,
a_name,
a_value,
row_number() over (partition by widget
order by a_name, a_value) rn
FROM attrs )
GROUP BY widget

Related

Update next column in table if previous column value is not null

When a person receives a score, an entry is added into the table #uniqueScores:
Pid | Date | Score
I have a stored procedure returning a table #people with the score columns containing the data from #uniqueScores (that fall within the past 3 months)
Pid | S1 | S2 | S3 | S4 | S5
I have a small test dataset, however I'm having trouble getting any scores beyond the first score registered to a user to appear in Score2 or beyond.
Here is my test dataset
Pid | Date | Score
#1 | 2020/07/01 | 8
#1 | 2020/09/15 | 8
#2 | 2020/09/21 | 3
#3 | 2020/10/01 | 5
#4 | 2020/10/18 | 6
#4 | 2020/10/31 | 2
My update statement, to update the Person column with the data
BEGIN
UPDATE #people
SET [Score5] = (CASE WHEN ( [p].[Score4] is not null and [p].[Score5] is null ) THEN [us].[Score] ELSE NULL END)
,[Score4] = (CASE WHEN ( [p].[Score3] is not null and [p].[Score4] is null ) THEN [us].[Score] ELSE NULL END)
,[Score3] = (CASE WHEN ( [p].[Score2] is not null and [p].[Score3] is null ) THEN [us].[Score] ELSE NULL END)
,[Score2] = (CASE WHEN ( [p].[Score1] is not null and [p].[Score2] is null ) THEN [us].[Score] ELSE NULL END)
,[Score1] = (CASE WHEN ( [p].[Score1] is null ) THEN [us].[Score] ELSE NULL END)
FROM #people [p] inner join #uniqueScores [us]
on [p].[PersonID] = [us].[PersonID]
WHERE [Date] >= #DateLimit -- within the previous 3 months
END
However, the query isn't updating the table with any but the first eligible values. The returned table looks like this
Pid | S1 | S2 | S3 | S4 | S5
#1 | 8 | null | null | null | null
#2 | 3 | null | null | null | null
#3 | 5 | null | null | null | null
#4 | 6 | null | null | null | null
The first table entry which is ineligible to be considered for the table isn't included which is great, however Person #4's second score is also missing.
I've been looking at PIVOT, WHILE and a CURSOR but I've got no closer to making this work. I'm sure I've missed something simple however I just can't see it.
UPDATE updates each row once. Preaggregate for multiple updates:
UPDATE p
SET Score1 = us.score_1,
Score2 = us.score_2,
Score3 = us.score_3,
Score4 = us.score_4,
Score5 = us.score_5
FROM #people [p] inner join
(SELECT us.PersonID,
MAX(CASE WHEN seqnum = 1 THEN Score END) as score_1,
MAX(CASE WHEN seqnum = 2 THEN Score END) as score_2,
MAX(CASE WHEN seqnum = 3 THEN Score END) as score_3,
MAX(CASE WHEN seqnum = 4 THEN Score END) as score_4,
MAX(CASE WHEN seqnum = 5 THEN Score END) as score_5
FROM (SELECT us.*,
ROW_NUMBER() OVER (PARTITION BY PersonId ORDER BY Date) as seqnum
FROM #uniqueScores us
WHERE [Date] >= #DateLimit -- within the previous 3 months
) us
GROUP BY us.PersonID
) s
ON us.PersonID = p.PersonId;
Note: You don't specify what order you want the scores in. This puts the oldest ones first. Use ORDER BY DESC if you want the newer ones first.

Conditionally select and insert multiple rows into single row in SQL Server

I am looking for someone to point me in the right direction how to traverse result set and insert into table without using cursor. Not even sure how to ask this.
Result set of the select query:
------------------------------------------------------
ID | Name | CreatedBy | CreatedDate
------------------------------------------------------
1 A John 2018-04-30
1 B Sam 2018-04-20
2 A John 2018-04-18
-------------------------------------------------------
Inserted table:
---------------------------------------------------------------------------------------------------------
ID | A_CreatedBy | A_CreatedDate | B_CreatedBy | B_CreatedDate | C_CreatedBy | C_CreatedDate
---------------------------------------------------------------------------------------------------------
1 John 2018-04-30 Sam 2018-04-20 NULL NULL
2 John 2018-04-18 NULL NULL NULL NULL
---------------------------------------------------------------------------------------------------------
Any help would be appreciated.
You can use the conditional aggregation as follows:
select ID,
max(case when name = 'A' then createdby end) as a_createdby,
max(case when name = 'A' then CreatedDate end) as a_CreatedDate,
max(case when name = 'B' then createdby end) as b_createdby,
max(case when name = 'B' then CreatedDate end) as b_CreatedDate,
max(case when name = 'C' then createdby end) as c_createdby,
max(case when name = 'C' then CreatedDate end) as c_CreatedDate
from (your_query) t
group by ID

Selecting all rows conditionally between 2 arbitrary column values in SQL Server

I've joined a number of tables to get to this table. From this table I need to select all of the b_id values that fall between the start end end values that are not null. There could be multiple start and end values in the table. How can I write a SQL Server query to select all of the b_ids between but not including those rows. So for this example table I would need the b_ids 99396 AND 71828
I tried to find a similar question and found something like this but I don't believe I'm using the correct values where they need to be. Is there another way to do it. I have a solution using a cursor, but I'm trying to find a non cursor solution. My friend told me the responses on here can be brutal if you don't word the question a certain way. Please be easy on me lol.
a_id | b_id | sequence | start | end |
---------+-------+----------+-------+-------+
3675151 | 68882 | 1 | null | null |
3675151 | 79480 | 2 | 79480 | null |
3675151 | 99396 | 3 | null | null |
3675151 | 71828 | 4 | null | null |
3675151 | 28911 | 5 | null | 28911 |
3675151 | 27960 | 6 | null | null |
3675183 | 11223 | 1 | null | null |
3675183 | 77810 | 2 | null | null |
3675183 | 11134 | 3 | null | null |
3675183 | 90909 | 4 | null | null |
Is this what you are looking for
select a_id, b_id, sequence
from
table
where
(a_id,sequence )
in
(select a_id, sequence from table t1
where
sequence >
(select sequence from table t2 where t1.a_id = t2.a_id and start is not null)
and
sequence <
(select sequence from table t3 where t1.a_id = t3.a_id and end is not null)
);
Would it be this?
Mark as answer if yes, if not exemplify otherwise.
create table #table (
a_id int
,b_id int
,c_sequence int
,c_start int
,c_end int
)
insert into #table
values
(3675151 ,68882 , 1 , null , null )
,(3675151 ,79480 , 2 , 79480 , null )
,(3675151 ,99396 , 3 , null , null )
,(3675151 ,71828 , 4 , null , null )
,(3675151 ,28911 , 5 , null , 28911)
,(3675151 ,27960 , 6 , null , null )
,(3675183 ,11223 , 1 , null , null )
,(3675183 ,77810 , 2 , 4343 , null )
,(3675183 ,11134 , 3 , null , null )
,(3675183 ,90939 , 4 , null , 1231 )
select
t.*
from #table t
where
exists (select t1.b_id,t1.c_sequence
from #table t1
where t1.c_start is not null
and t.a_id =t1.a_id and t.c_sequence>t1.c_sequence )
and exists (select t1.b_id,t1.c_sequence
from #table t1
where t1.c_end is not null
and t.a_id =t1.a_id
and t.c_sequence<t1.c_sequence
You can use window functions for this:
select t.*
from (select t.*,
max(case when c_start is not null then c_sequence end) over (partition by a_id order by c_sequence) as last_c_start,
max(case when c_end is not null then c_sequence end) over (partition by a_id order by c_sequence) as last_c_end,
min(case when c_end is not null then c_sequence end) over (partition by a_id order by c_sequence desc) as next_c_end
from t
) t
where c_sequence > last_c_start and
c_sequence < next_c_end and
(last_c_start > last_c_end or last_c_end is null);
Here is a db<>fiddle.
The subquery is returning the previous start and next end. That is pretty simply. The where uses this information. The last condition just checks that the most recent "start" is the one that should be considered.
Note: This does not handle more complicated scenarios like start-->start-->end-->end. If that is a possibility, you should ask another question.
EDIT:
Actually, there is an even easier way:
select t.*
from (select t.*,
count(coalesce(c_start, c_end)) over (partition by a_id order by c_sequence) as counter
from t
) t
where c_start is null and c_end is null and
counter % 2 = 1;
This returns rows where there two values are NULL (to avoid the endpoints) and there are an odd number of non-NULL c_start/c_end values up to that row.

How can i check the order of column values(by date) for every unique id?

I have this table, Activity:
| ID | Date of activity | activity |
|----|---------------------|----------|
| 1 | 2016-05-01T13:45:03 | a |
| 1 | 2016-05-02T13:45:03 | b |
| 1 | 2016-05-03T13:45:03 | a |
| 1 | 2016-05-04T13:45:03 | b |
| 2 | 2016-05-01T13:45:03 | b |
| 2 | 2016-05-02T13:45:03 | b |
and this table:
| id | Right order |
|----|-------------|
| 1 | yes |
| 2 | no |
How can I check for every ID if the order of the activities is sumiliar to this order for example ?
a b a b a b ..
of course i'll check according to activity date
In SQL Server 2012+ you could use common table expression with lag(), and then the min() of a case expression that follows your logic like so:
;with cte as (
select *
, prev_activity = lag(activity) over (partition by id order by date_of_activity)
from t
)
select id
, right_order = min(case
when activity = 'a' and isnull(prev_activity,'b')<>'b' then 'no'
when activity = 'b' and isnull(prev_activity,'b')<>'a' then 'no'
else 'yes'
end)
from cte
group by id
rextester demo: http://rextester.com/NQQF78056
returns:
+----+-------------+
| id | right_order |
+----+-------------+
| 1 | yes |
| 2 | no |
+----+-------------+
Prior to SQL Server 2012 you can use outer apply() to get the previous activity instead of lag() like so:
select id
, right_order = min(case
when activity = 'a' and isnull(prev_activity,'b')<>'b' then 'no'
when activity = 'b' and isnull(prev_activity,'b')<>'a' then 'no'
else 'yes'
end)
from t
outer apply (
select top 1 prev_activity = i.activity
from t as i
where i.id = t.id
and i.date_of_activity < t.date_of_activity
order by i.date_of_activity desc
) x
group by id
EDITED - Allows for variable number of Patterns per ID
Perhaps another approach
Example
Declare #Pat varchar(max)='a b'
Declare #Cnt int = 2
Select ID
,RightOrder = case when rtrim(replicate(#Pat+' ',Hits/#Cnt)) = (Select Stuff((Select ' ' +activity From t Where id=A.id order by date_of_activity For XML Path ('')),1,1,'') ) then 'Yes' else 'No' end
From (Select ID,hits=count(*) from t group by id) A
Returns
ID RightOrder
1 Yes
2 No
select id,
case when sum(flag)=0 and cnt_per_id%2=0
and max(case when rnum=1 then activity end) = 'a'
and max(case when rnum=2 then activity end) = 'b'
and min_activity = 'a' and max_activity = 'b'
then 'yes' else 'no' end as RightOrder
from (select t.*
,row_number() over(partition by id order by activitydate) as rnum
,count(*) over(partition by id) as cnt_per_id
,min(activity) over(partition by id) as min_activity
,max(activity) over(partition by id) as max_activity
,case when lag(activity) over(partition by id order by activitydate)=activity then 1 else 0 end as flag
from tbl t
) t
group by id,cnt_per_id,max_activity,min_activity
Based on the explanation the following logic has to be implemented for rightorder.
Check if the number of rows per id are even (Remove this condition if there can be an odd number of rows like a,b,a or a,b,a,b,a and so on)
First row contains a and second b, min activity is a and max activity is b for an id.
Sum of flags (set using lag) should be 0

SQL check if group contains NULL

Is there any function to check if a column in a group contains a NULL, alternatively how would I solve this? Example below of data structure.
id | value
----------
1 | NULL
1 | 56
2 | 98
2 | 14
Result:
id | value
----------
1 | 1
2 | 0
try
select id,
count(*) - count(value) as null_value_count
from your_table
group by id
SQLFiddle demo
Another possibility which doesn't use the fact that count(value) ignores NULL values:
select id,
sum(case when value is null then 1 else 0 end) as null_count
from your_table
group by id;