Categorizing in select statement - sql

So i have this table
id | object | type
--------------------------------
1 | blue | color
1 | burger | food
2 | sandwich | food
2 | red | color
2 | coke | beverage
3 | sprite | beverage
3 | coke | beverage
3 | red | color
4 | bacon | food
i have to create a select statement that will show a table with columns id, color, food and beverage. Arranged by ID with their designated things on it.
so my expected result is
id | color | food | beverage
-------------------------------------------
1 | blue | burger |
2 | red | sandwich | coke
3 | red | | sprite
3 | | | coke
4 | | bacon |
as of now i have this code
Select id as id,
Case When I.Type = 'color' Then I.Object End As color,
Case When I.Type = 'food' Then I.Object End As food,
Case When I.Type = 'beverage' Then I.Object End As beverage
From table I
order by id
but the problem with my code is it doesnt group by its ID so it creates multiple rows for every object.
TIA!

You are looking for a pivot query. What is challenging about your problem is that, for a given id and type, there can be more than one object present. To handle this, you can first do a GROUP BY query to CSV aggregate objects for a given type using LISTAGG:
SELECT id,
MAX(CASE WHEN t.type = 'color' THEN t.object ELSE NULL END) AS color,
MAX(CASE WHEN t.type = 'food' THEN t.object ELSE NULL END) AS food,
MAX(CASE WHEN t.type = 'beverage' THEN t.object ELSE NULL END) AS beverage
FROM
(
SELECT id,
LISTAGG(object, ',') WITHIN GROUP (ORDER BY object) AS object,
type
FROM yourTable
GROUP BY id, type
) t
GROUP BY t.id
The inner query first aggregates objects across both id and type, and the outer query is a simple pivot query as you might expect.
Here is a Fiddle which shows an almost identical query in MySQL (Oracle seems to be perpetually broken):
SQLFiddle

You can try with something like the following:
with test(id, object, type) as
(
select 1,'blue', 'color' from dual union all
select 1,'burger', 'food' from dual union all
select 2,'sandwich','food' from dual union all
select 2,'red', 'color' from dual union all
select 2,'coke', 'beverage' from dual union all
select 3,'sprite', 'beverage' from dual union all
select 3,'coke', 'beverage' from dual union all
select 3,'red', 'color' from dual union all
select 4,'bacon', 'food' from dual
)
select id,
max( case when type = 'color'
then object
else null
end
) as color,
max( case when type = 'food'
then object
else null
end
) as food,
max( case when type = 'beverage'
then object
else null
end
) as beverage
from (
select id, object, type, row_number() over ( partition by id, type order by object) row_for_id
from test
)
group by id, row_for_id
order by id, row_for_id
The inner query is the main part, where you handle the case of a single id with many objects of a type; you can modify the ordering by editing the order by object.
The external query can be re-written in different ways, for example with a PIVOT; i used the MAX hoping to make it clear.

Try this
I have achieved using pivot clause
select id,object,type from yourtable
pivot
(
LISTAGG(object, ',') WITHIN GROUP (ORDER BY object)
for type IN
(
'color' AS "color",
'food' AS "food",
'beverage' AS "beverage"
)
)
order by id

Robin: My comment to you (under Tim Biegeleisen's answer) is partially incorrect. There IS a pivot-based solution; however, the "groups" are not by id, but instead they are by id AND rank within your three "categories". For this solution (or ANY solution that does not use dynamic SQL) to work, it is necessary that all the "types" (and their names) be known beforehand, and they must be hardcoded in the SQL query.
NOTE: In this solution, I assumed that for each id, the "objects" within the same "type" are associated to each other based on their alphabetical order (so, for example, for id = 3, "coke" is associated with "red" and "sprite" is associated with NULL, unlike your sample output). I asked you about that right below your Question - if there are additional rules you did not share with us, requiring a different pairing of objects of different types, it may or may not be possible to adapt the solution to meet those additional rules.
EDIT: On closer look, this is pretty much what Aleksej provided, without using the explicit pivot syntax. His solution has the advantage that it would work in older versions of Oracle (before 11.1 where pivot first became available).
QUERY (including test data in the first CTE):
with
inputs ( id, object, type ) as (
select 1, 'blue' , 'color' from dual union all
select 1, 'burger' , 'food' from dual union all
select 2, 'sandwich' , 'food' from dual union all
select 2, 'red' , 'color' from dual union all
select 2, 'coke' , 'beverage' from dual union all
select 3, 'sprite' , 'beverage' from dual union all
select 3, 'coke' , 'beverage' from dual union all
select 3, 'red' , 'color' from dual union all
select 4, 'bacon' , 'food' from dual
),
r ( id, object, type, rn ) as (
select id, object, type, row_number() over (partition by id, type order by object)
from inputs
)
select id, color, food, beverage
from r
pivot ( max(object) for type in ( 'color' as color, 'food' as food,
'beverage' as beverage))
order by id, rn
;
OUTPUT:
ID COLOR FOOD BEVERAGE
---- -------- -------- --------
1 blue burger
2 red sandwich coke
3 red coke
3 sprite
4 bacon

Related

How can I count the total no of rows which is not containing some value in array field? It should include null values as well

| names |
| -----------------------------------|
| null |
| null |
| [{name:'test'},{name:'test1'}] |
| [{name:'test'},{name:'test1'}] |
| [{name:'test1'},{name:'test2'}] |
I want to count the no of rows which does not have the value 'test' in the name key.
Here it should give answer as 3 (Row no 1, 2 and 5th row) because all these row do not contain the value 'test'.
Use below approach
select count(*)
from your_table
where 0 = ifnull(array_length(regexp_extract_all(names, r"\b(name:'test')")), 0)
you can test it with below data (that resemble whatever you presented in your question)
with your_table as (
select null names union all
select null union all
select "[{name:'test'},{name:'test1'}]" union all
select "[{name:'test'},{name:'test1'}]" union all
select "[{name:'test1'},{name:'test2'}]"
)
with output
Below approach will work,
with your_table as (
select null names union all
select null union all
select [('name','test'),('name','test1')] union all
select [('name','test'),('name','test1')] union all
select [('name','test1'),('name','test2')]
)
SELECT COUNT(*) as count FROM your_table
WHERE NOT EXISTS (
SELECT 1
FROM UNNEST(your_table.names) AS names
WHERE names IN (('name','test'))
)

How to select and transfer specific string values into new column in SQL?

I have a table of items that can be arranged into groups. The table contains an attribute column with string values, as an example I chose color. For some items, the color column is empty.
ItemID
Group
Color
001
A
'blue'
002
A
'blue'
003
A
'blue'
004
A
'blue'
005
A
101
B
'red'
102
B
'red'
103
B
104
B
105
B
'green'
From this table I want to select only the items that do not have a color assigned yet and display a proposed color in a new column. The proposed color should be taken from the item with the highest ID value within each group.
The desired output should look like this:
ItemID
Group
Proposed Color
005
A
'blue'
103
B
'green'
104
B
'green'
I have no idea how to select specific values from the color column based on values from another column (in this case ItemID) and assign them to a different row within my table.
Any help (also keywords describing the concept associated with my problem...) would be greatly appreciated!
(edit) This is what I've tried so far:
WITH temp_id AS (
SELECT max(ItemID), GroupName
FROM myTable
WHERE Color IS NOT NULL
GROUP BY GroupName
)
SELECT myTable.*, temp_id.*
FROM myTable
JOIN temp_id
ON temp_id.groupName = myTable.groupName;
But the output is missing the color column and I wasn't able to add a condition WHERE Color IS NOT NULL to my outer selection.
As you're close, I'll give you the answer, feel free to ask if you don't understand :
SQL Fiddle
Query 1:
-- full query
SELECT mt.itemID, mt.Group, tColor.Color as proposedColor
FROM myTable as mt
LEFT JOIN (
SELECT tMax.Group, tC.Color
FROM (
SELECT max(t.ItemID) ItemID, t.Group
FROM myTable as t
WHERE t.Color IS NOT NULL
GROUP BY t.Group
) tMax
LEFT JOIN myTable tC
ON tC.ItemID = tMax.ItemID
) tColor
ON tColor.Group = mt.Group
WHERE mt.Color IS NULL
Results:
| itemID | Group | proposedColor |
|--------|-------|---------------|
| 5 | A | blue |
| 103 | B | green |
| 104 | B | green |
Query 2:
-- details :
-- 1) the get max id per group
SELECT max(t.ItemID) ItemID, t.Group
FROM myTable as t
WHERE t.Color IS NOT NULL
GROUP BY t.Group
Results:
| ItemID | Group |
|--------|-------|
| 4 | A |
| 105 | B |
Query 3:
-- 2) the color per group
SELECT tMax.Group, tC.Color
FROM (
SELECT max(t.ItemID) ItemID, t.Group
FROM myTable as t
WHERE t.Color IS NOT NULL
GROUP BY t.Group
) tMax
LEFT JOIN myTable tC
ON tC.ItemID = tMax.ItemID
Results:
| Group | Color |
|-------|-------|
| A | blue |
| B | green |
Query 4:
-- 3) the row that need a proposed color
SELECT mt.itemID, mt.Group, null as proposedColor
FROM myTable as mt
-- LEFT JOIN...
WHERE mt.Color IS NULL
Results:
| itemID | Group | proposedColor |
|--------|-------|---------------|
| 5 | A | (null) |
| 103 | B | (null) |
| 104 | B | (null) |
You have to join again with myTable in order to get the Color of the MaxID you calculated in the temp_id table:
WITH temp_id AS (
SELECT max(ItemID) AS MaxId, GroupName
FROM myTable
WHERE Color IS NOT NULL
GROUP BY GroupName
)
SELECT myTable.*, T2.Color
FROM myTable
JOIN temp_id
ON temp_id.groupName = myTable.groupName
JOIN myTable T2
ON T2.ItemID = temp_id.MaxID;
Fiddle here.
This could work as well:
WITH test_data AS
(
SELECT '001' AS "item_id", 'A' AS "group", 'blue' AS "color" FROM DUAL UNION ALL
SELECT '002', 'A', 'blue' FROM DUAL UNION ALL
SELECT '003', 'A', 'blue' FROM DUAL UNION ALL
SELECT '004', 'A', 'blue' FROM DUAL UNION ALL
SELECT '005', 'A', NULL FROM DUAL UNION ALL
SELECT '101', 'B', 'red' FROM DUAL UNION ALL
SELECT '102', 'B', 'red' FROM DUAL UNION ALL
SELECT '103', 'B', NULL FROM DUAL UNION ALL
SELECT '104', 'B', NULL FROM DUAL UNION ALL
SELECT '105', 'B', 'green' FROM DUAL
)
SELECT items."item_id", items."group", colors."proposed color"
FROM (SELECT td."group", MIN(td."color") AS "proposed color"
FROM test_data td
GROUP BY td."group") colors
INNER JOIN (SELECT td."item_id", td."group"
FROM test_data td
WHERE td."color" IS NULL) items
ON items."group" = colors."group"
ORDER BY items."item_id";
You can use a MAX window function to retrieve the value of "Color" with respect to each ItemID, then get the rows that had "Color" assigned null:
SELECT ItemID,
Group_,
MaxColor AS Color
FROM (SELECT *,
MAX(Color) OVER(
PARTITION BY Group_
ORDER BY CAST(ItemID AS UNSIGNED)) AS MaxColor
FROM tab) cte
WHERE Color IS NULL
Check the demo here.
Note: If you don't care about selecting only those specific rows with NULL values, you can just use the window function as follows.
SELECT ItemID,
Group,
COALESCE(Color, MAX(Color) OVER(
PARTITION BY Group
ORDER BY CAST(ItemID AS UNSIGNED))) AS Color
FROM tab
The COALESCE function will give priority to the Color field, and when it is null, it will catch the MAX window function output.

Does Oracle LISTAGG work with GROUP BY and PIVOT?

I have this table:
ID CATEGORY SCORE
-------------------------
1 A 2
1 B 1
and am trying to get the avg(score) for each category:
ID SCORE_A_AVG SCORE_B_AVG
------------------------------
1 1.5 1.5
I've tried this but all results are null
select p.* from (
select ID, LISTAGG(CATEGORY, ',') within group (order by CATEGORY), avg(score) score
from foo
group by ID)
PIVOT (avg(score) score
FOR category IN ('A' as A_AVG, 'B' as B_AVG)) p;
I can get results for one or the other category by NOT using LISTAGG:
... MIN(CATEGORY) ...
Yes, it does work but you need the aggregation in the PIVOT:
SELECT *
FROM test_data
PIVOT (
AVG( score ) AS avg_score,
LISTAGG( score, ',' ) WITHIN GROUP ( ORDER BY score ) AS scores
FOR category IN (
'A' AS a,
'B' AS b
)
)
So for the test data:
CREATE TABLE test_data ( ID, CATEGORY, SCORE ) AS
SELECT 1, 'A', 2 FROM DUAL UNION ALL
SELECT 1, 'A', 3 FROM DUAL UNION ALL
SELECT 1, 'B', 1 FROM DUAL UNION ALL
SELECT 1, 'B', 2 FROM DUAL
this outputs:
ID | A_AVG_SCORE | A_SCORES | B_AVG_SCORE | B_SCORES
-: | ----------: | :------- | ----------: | :-------
1 | 2.5 | 2,3 | 1.5 | 1,2
db<>fiddle here
I would just use conditional aggregation:
select id,
avg(case when category = 'A' then score end) as score_a,
avg(case when category = 'B' then score end) as score_b
from foo
group by id;
I have no idea why you are thinking listagg() rather than avg().

select rows between two character values of a column

I have a table which shows as below:
S.No | Action
1 | New
2 | Dependent
3 | Dependent
4 | Dependent
5 | New
6 | Dependent
7 | Dependent
8 | New
9 | Dependent
10 | Dependent
I here want to select the rows between the first two 'New' values in the Action column, including the first row with the 'New' action. Like [New,New)
For example:
In this case, I want to select rows 1,2,3,4.
Please let me know how to do this.
Hmmm. Let's count up the cumulative number of times that New appears as a value and use that:
select t.*
from (select t.*,
sum(case when action = 'New' then 1 else 0 end) over (order by s_no) as cume_new
from t
) t
where cume_new = 1;
you can do some magic with analytic functions
1 select group of NEW actions, to get min and max s_no
2 select lead of 2 rows
3 select get between 2 sno (min and max)
with t as (
select 1 sno, 'New' action from dual union
select 2,'Dependent' from dual union
select 3,'Dependent' from dual union
select 4,'Dependent' from dual union
select 5,'New' from dual union
select 6,'Dependent' from dual union
select 7,'Dependent' from dual union
select 8,'New' from dual union
select 9,'Dependent' from dual union
select 10,'Dependent' from dual
)
select *
from (select *
from (select sno, lead(sno) over (order by sno) a
from ( select row_number() over (partition by action order by Sno) t,
t.sno
from t
where t.action = 'New'
) a
where t <=2 )
where a is not null) a, t
where t.sno >= a.sno and t.sno < a.a

Selecting groups of rows where at least one row of each group meets a criteria

I'm trying to SELECT groups of rows having one row with a certain criteria.
I've tried it with CASE WHEN statements without any success. Keep in mind this table has hundred of records.
What I'm trying to accomplish is this:
One row of the group must have a subcategory equal to "GAMECONSOLE".
Rows having the same category, description and section form one group.
The ID is different so MIN and MAX does not work either.
ID SECTION DESCRIPTION CATEGORY SUBCATEGORY
21349 14010014 TODDLER TOY GAMECONSOLE
21278 14010014 TODDLER TOY BICYCLE
21431 15020021 TODDLER TOY CHESS
In this example the two first rows should be selected because they form one group and one row of the group is a "GAMECONSOLE".
CASE WHEN is used when you have to take a decision within a column expression. Filtering on row level must be done in a WHERE clause:
SELECT T.id, T.section, T.description, T.category, T.subcategory
FROM
myTable T
INNER JOIN myTable S
ON T.section = S.section AND
T.description = S.description AND
T.category = S.category
WHERE
S.subcategory = 'GAMECONSOLE'
You can join the table with itself on the columns that have to be equal. The table with alias S selects the right subategory. T selects all corresponding rows of the groups.
SELECT a1.ID
, a1.SECTION
, a1.DESCRIPTION
, a1.CATEGORY
, a1.SUBCATEGORY
FROM MyTable a1
INNER JOIN MyTable a2 ON a2.DESCRIPTION = a1.DESCRIPTION
AND a2.CATEGORY = a1.CATEGORY
AND a2.SECTION = a1.SECTION
WHERE a2.SUBCATEGORY = 'GAMECONSOLE'
-- you may want to further filter the Where clause and apply a group by or distinct to get the actual results you are wanting
Your description sounds like:
select
...
from
(
select
...
,sum(case when subcategory = 'GAMECONSOLE' then 1 else 0 end)
over (partition by category, description, section) as cnt
from tab
) dt
where cnt > 0
SELECT *
FROM myTable T
WHERE Section = (SELECT Section
FROM myTable Q
WHERE Q.subcategory = 'GAMECONSOLE')
Using an analytic function you can get the answer without using a self join.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST( ID, SECTION, DESCRIPTION, CATEGORY, SUBCATEGORY ) AS
SELECT 1, 1, 'TODDLER', 'TOY', 'GAMECONSOLE' FROM DUAL
UNION ALL SELECT 2, 1, 'TODDLER', 'TOY', 'BICYCLE' FROM DUAL
UNION ALL SELECT 3, 2, 'TODDLER', 'TOY', 'CHESS' FROM DUAL
UNION ALL SELECT 4, 3, 'COMPUTERS', 'SOFTWARE', 'BOOK' FROM DUAL
UNION ALL SELECT 5, 4, 'COMPUTERS', 'SOFTWARE', 'SOFTWARE' FROM DUAL
UNION ALL SELECT 6, 5, 'COMPUTERS', 'HARDWARE', 'MONITOR' FROM DUAL
UNION ALL SELECT 7, 6, 'COMPUTERS', 'HARDWARE', 'GAMECONSOLE' FROM DUAL
UNION ALL SELECT 8, 7, 'COMPUTERS', 'HARDWARE', 'KEYBOARD' FROM DUAL
UNION ALL SELECT 9, 8, 'TODDLER', 'BEDDING', 'BED' FROM DUAL
Query 1:
SELECT ID, SECTION, DESCRIPTION, CATEGORY, SUBCATEGORY
FROM (
SELECT t.*,
COUNT( CASE SUBCATEGORY WHEN 'GAMECONSOLE' THEN 1 END ) OVER ( PARTITION BY DESCRIPTION, CATEGORY ) AS HAS_SUBCATEGORY
FROM TEST t
)
WHERE HAS_SUBCATEGORY > 0
Results:
| ID | SECTION | DESCRIPTION | CATEGORY | SUBCATEGORY |
|----|---------|-------------|----------|-------------|
| 8 | 7 | COMPUTERS | HARDWARE | KEYBOARD |
| 7 | 6 | COMPUTERS | HARDWARE | GAMECONSOLE |
| 6 | 5 | COMPUTERS | HARDWARE | MONITOR |
| 3 | 2 | TODDLER | TOY | CHESS |
| 2 | 1 | TODDLER | TOY | BICYCLE |
| 1 | 1 | TODDLER | TOY | GAMECONSOLE |
Try
SELECT * FROM <TABLE_NAME> WHERE SUBCATEGORY like "GAMECONSOLE";
or
SELECT * FROM <TABLE_NAME> WHERE SUBCATEGORY = "GAMECONSOLE";
Replace <TABLE_NAME> with the actual table name.
Further readings:
https://dev.mysql.com/doc/refman/5.0/en/select.html