How to group by an expression in TSQL and capture the result? - sql

How can I include the results of an expression in a GROUP BY clause and also select the output of the expression ?
Say I have this table:
╔════════════════════════╦═══════════╦═══════╗
║ Forest ║ Animal ║ Count ║
╠════════════════════════╬═══════════╬═══════╣
║ Tongass ║ Hyena ║ 600 ║
║ Tongass ║ Bear ║ 1200 ║
║ Mount Baker-Snoqualmie ║ Wolf ║ 30 ║
║ Mount Baker-Snoqualmie ║ Bunny ║ 2 ║
║ Ozark-St. Francis ║ Pigeon ║ 100 ║
║ Ozark-St. Francis ║ Ostrich ║ 1 ║
║ Bitterroot ║ Tarantula ║ 9001 ║
╚════════════════════════╩═══════════╩═══════╝
I need a row with the count of carnivores in each forest and a row for the count of non-carnivores (if there are any). This is the output I'm looking for in this example:
╔════════════════════════╦═══════════════╦═══════════════╗
║ Forest ║ AnimalsOfType ║ AreCarnivores ║
╠════════════════════════╬═══════════════╬═══════════════╣
║ Tongass ║ 1800 ║ 1 ║
║ Mount Baker-Snoqualmie ║ 2 ║ 0 ║
║ Mount Baker-Snoqualmie ║ 30 ║ 1 ║
║ Ozark-St. Francis ║ 101 ║ 0 ║
║ Bitterroot ║ 9001 ║ 1 ║
╚════════════════════════╩═══════════════╩═══════════════╝
The information for whether or not an animal is carnivorous is encoded in the expression.
What I'd like to do is include the expression in the group-by and reference its result in the select clause:
SELECT TOP (1000)
[Forest],
SUM([COUNT]) AS AnimalsOfType,
AreCarnivores
FROM [Tinker].[dbo].[ForestAnimals]
GROUP BY
Forest,
CASE WHEN ForestAnimals.Animal IN ('Pigeon', 'Ostrich', 'Bunny') THEN 0 ELSE 1 END AS AreCarnivores
However, this is not valid TSQL syntax.
If I include the Animal column in the GROUP BY clause to allow me to rerun the function in the SELECT, I'll get one row per animal type, which is not the desired behavior.
Doing separate selects into temp tables and unioning the results is undesirable because the real version of this query features a large number of expressions which need this behavior in the same result set, which would make for an extremely awkward stored procedure.

Use a CTE:
WITH X AS (
SELECT Forest, Animal, Count,
CASE WHEN ForestAnimals.Animal IN ('Pigeon', 'Ostrich', 'Bunny')
THEN 0
ELSE 1 END AS AreCarnivores
FROM [Tinker].[dbo].[ForestAnimals]
)
SELECT Forest, SUM(Count) AS AnimalsOfType, AreCarnivores
FROM X
Group by Forest, AreCarnivores;
Or be more verbose about it and repeat yourself:
SELECT Forest, SUM(Count) AS AnimalsOfType,
CASE WHEN ForestAnimals.Animal IN ('Pigeon', 'Ostrich', 'Bunny')
THEN 0
ELSE 1 END AS AreCarnivores
FROM [Tinker].[dbo].[ForestAnimals]
GROUP BY Forest, CASE WHEN ForestAnimals.Animal IN ('Pigeon', 'Ostrich', 'Bunny')
THEN 0
ELSE 1 END;
They're equivalent queries to the optimizer.

Related

How to Pivot this table on Oracle?

Can you help me figure out how to pivot this table:
╔═══════════╦═════════════╦══════╦════════╦════════╗
║ Big Group ║ Small Group ║ Kids ║ Adults ║ Elders ║
╠═══════════╬═════════════╬══════╬════════╬════════╣
║ 1 ║ 1 ║ 10 ║ 20 ║ 5 ║
║ 1 ║ 2 ║ 15 ║ 10 ║ 10 ║
║ 2 ║ 1 ║ 20 ║ 0 ║ 15 ║
╚═══════════╩═════════════╩══════╩════════╩════════╝
Into something like this?
╔═══════════╦═════════════╦══════╦════════╦════════╦═════════════╦══════╦════════╦════════╗
║ Big Group ║ Small Group ║ Kids ║ Adults ║ Elders ║ Small Group ║ Kids ║ Adults ║ Elders ║
╠═══════════╬═════════════╬══════╬════════╬════════╬═════════════╬══════╬════════╬════════╣
║ 1 ║ 1 ║ 10 ║ 20 ║ 5 ║ 2 ║ 15 ║ 10 ║ 10 ║
║ 2 ║ 1 ║ 20 ║ 0 ║ 15 ║ ║ ║ ║ ║
╚═══════════╩═════════════╩══════╩════════╩════════╩═════════════╩══════╩════════╩════════╝
The number of small groups per Big group is variable, and that's what is being difficult for me to understand how to do it.
Can anyone help me?
Thanks in advance
There is a way but the overhead of using PIVOT is to provide the list of all values which needs to be pivoted.
As you also need each small group to be pivoted we need to create a virtual column between big group and small group to be used in pivot clause as you see below
with table1
as
(select 1 bg
,1 sg,10 kids
,20 adult
from dual
union all
select 1,2,15,25 from dual
union all
select 2,1,20,0 from dual
)
select *
from
(
select t1.*,t1.bg||'_'||t1.sg piv
from table1 t1
)
pivot
(
max(sg) sg,max(kids) kids,max(adult) adult
for piv in ('1_1' as bg1_sg1
,'1_2' as bg2_sg2
,'2_1' as bg2_sg1)
)
order by bg
Demo

ISNULL with COUNT(*)

╔═══════╦═══════╗
║ agent ║ prime ║
╠═══════╬═══════╣
║ Said ║ 100 ║
║ Farid ║ 200 ║
║ Walid ║ 150 ║
║ Said ║ 300 ║
║ Said ║ 250 ║
║ Said ║ 400 ║
║ Farid ║ 300 ║
║ Farid ║ 250 ║
║ Walid ║ 140 ║
╚═══════╩═══════╝
Query
select agent, ISNULL(count(*),0) as NB from [agents]
where prime >= 200
group by agent
The output:
╔═══════╦═══════╗
║ agent ║ prime ║
╠═══════╬═══════╣
║ Farid ║ 3 ║
║ Said ║ 3 ║
╚═══════╩═══════╝
I want to replace the null value with 0 because as you can see the agent 'walid' have no prime >= 200. The results I want to achieve:
agent prime
Farid 3
Said 3
Walid 0
I know Its achievable using a UNION but I want to know why this ISNULL is not functional in this case and If its possible to do.
Your where clause filters Walid from your result set entirely. Try this approach instead:
select agent, sum(case when prime >= 200 then 1 else 0 end) as NB
from [agents]
group by agent
The problem is that the where clause filters everything out. You can accomplish what you want using conditional aggregation:
select agent, sum(case when prime >= 200 then 1 else 0 end) as nb
from agents
group by agent;
As a hint: count(*) does not return NULL. It returns 0, so there is no need to use COALESCE() (or similar logic).
Please use NVL in place of ISNULL. It should give you the desired result I guess.

SQL Junction Query - How To Get Exact Match

I would like the answer to this question to be DBMS agnostic, but if it is relevant I am using Access SQL.
Please keep note that this is a simplified version of what I am trying to do.
Now, consider I have the following three tables.
My main fruits table(tblFruits):
╔═════════╦═══════════╦
║ fruitID ║ fruitName ║
╠═════════╬═══════════╬
║ 1 ║ Apple ║
║ 2 ║ Orange ║
║ 3 ║ Grapefruit║
╚═════════╩═══════════╩
A junction table to link many tags to 1 fruit(tblFruitTagJunc):
╔════════════════╦═════════╦═════════════╗
║ fruitTagJuncID ║ fruitID ║ tagID ║
╠════════════════╬═════════╬═════════════╣
║ 1 ║ 1 ║ 1 ║
║ 2 ║ 1 ║ 2 ║
║ 3 ║ 1 ║ 4 ║
║ 4 ║ 1 ║ 5 ║
║ 5 ║ 2 ║ 3 ║
║ 6 ║ 3 ║ 3 ║
║ 7 ║ 3 ║ 6 ║
╚════════════════╩═════════╩═════════════╝
And finally a tag table to tag my fruits(tblTag):
╔═════════╦═══════════╗
║ tagID ║ tag ║
╠═════════╬═══════════╣
║ 1 ║ Tasty ║
║ 2 ║ Red ║
║ 3 ║ Orange ║
║ 4 ║ Shiny ║
║ 5 ║ Delicious ║
║ 6 ║ Awful ║
╚═════════╩═══════════╝
Thanks to This Blog Post for letting me be lazy)
This essentially says that:
Apples are (Red, Shiny, Tasty, Delicious)
Oranges are (Orange)
Grapefruits are (Orange, Awful)
Now say that I want to select those fruits that have the tag 'Orange' and no others. With the data presented, that would be only the one with fruitName = 'Orange'. I am currently doing this:
SELECT F.fruitName
FROM tblFruits F
INNER JOIN tblFruitTagJunc AS FTJ on F.fruitID = FTJ.fruitID
INNER JOIN tbltag as T ON FTJ.tagID = T.tagID
WHERE T.tag in('Orange')
GROUP BY F.fruitName
HAVING count(T.tag) = 1
This would return both Orange AND Grapfruit in the result, but I only wanted Orange.
The reason I am doing the SQL statement this way is that different types of fruits may have the same name but different tags OR different fruits may have all but one of the same tags.
EDIT:
SQLFiddle as requested.
You are on the right track, but you need conditional aggregation in the having clause rather than a where clause. When you use where, you never see the other tags.
So:
SELECT F.fruitName
FROM tblFruits as F INNER JOIN
tblFruitTagJunc AS FTJ
on F.fruitID = FTJ.fruitID INNER JOIN
tbltag as T
ON FTJ.tagID = T.tagID
GROUP BY F.fruitName
HAVING SUM(iif(t.tag in ('Orange'), 1, 0) > 0 AND
COUNT(t.tag) = 1;
Note that the "right" way to express conditionality is using CASE rather than IIF(). Also Access usually requires lots of ugly parentheses around joins, which I am also leaving out.

Subquery multiple rows oracle

I have a table containing these values:
╔════╦═══════════════╗
║ ID ║ TYPE ║
╠════╬═══════════════╣
║ 1 ║ FUEL PUMP ║
║ 2 ║ FIRE ALARM ║
║ 3 ║ FIRE PUMP ║
║ 4 ║ SAFETY SHOWER ║
╚════╩═══════════════╝
(query is: SELECT DISTINCT TYPE FROM EQUIPMENT)
And a query which returns the following:
╔═════════════╦══════════════╗
║ Room ║ Equipment ID ║
╠═════════════╬══════════════╣
║ Locker Room ║ 1 ║
║ Hallway ║ 1 ║
║ Foyer ║ 2 ║
║ Office 1 ║ 3 ║
║ Office 2 ║ 2 ║
╚═════════════╩══════════════╝
I have tried to make the EQUIPMENT.TYPE field display by using a subquery within the SELECT and WHERE statements of the query which generates the above table. However, I am getting the:ORA-01427: single-row subquery returns more than one row 01427. 00000 - "single-row subquery returns more than one row". I asusme this is because the EQUIPMENT ID value is returned more than once.
Is it possible to do this with a join?
select room, type
from equipment join rooms
on rooms.equipment_id = equipment.id
Assuming the other table is called ROOMS.

SQL: Count distinct values from one column based on multiple criteria in other columns

I am trying to do count distinct values based on multiple criteria.
Sample data exercise included below.
Table1
╔════════╦════════╦══════╗
║ Bug ID ║ Status ║ Test ║
╠════════╬════════╬══════╣
║ 1 ║ Open ║ w ║
║ 2 ║ Closed ║ w ║
║ 3 ║ Open ║ w ║
║ 4 ║ Open ║ x ║
║ 4 ║ Open ║ x ║
║ 5 ║ Closed ║ x ║
║ 5 ║ Closed ║ x ║
║ 5 ║ Closed ║ y ║
║ 6 ║ Open ║ z ║
║ 6 ║ Open ║ z ║
║ 6 ║ Open ║ z ║
║ 7 ║ Closed ║ z ║
║ 8 ║ Closed ║ z ║
╚════════╩════════╩══════╝
Desired Query Results
╔══════╦═══════════╦════════════╗
║ Test ║ Open Bugs ║ Total Bugs ║
╠══════╬═══════════╬════════════╣
║ w ║ 2 ║ 3 ║
║ x ║ 1 ║ 2 ║
║ y ║ 0 ║ 1 ║
║ z ║ 1 ║ 3 ║
╚══════╩═══════════╩════════════╝
A given Bug can be found in multiple Tests, multiple times for the same Test(ex: 6), or both (ex: 5).
The following query works fine to accurately deliver 'Total Bugs'
SELECT
Test,
COUNT(DISTINCT Bug ID) AS "Total Bugs"
FROM
Table1
GROUP BY Test
My research has led me to variations on the following query. They miss the distinct bugs and therefore return the incorrect results (shown below the query) for the 'Open Bugs' column
SELECT
Test,
SUM(CASE WHEN Status <> 'Closed' THEN 1 ELSE 0 END) AS "Open Bugs"
FROM
Table1
GROUP BY Test
╔══════╦═══════════╗
║ Test ║ Open Bugs ║
╠══════╬═══════════╣
║ w ║ 2 ║
║ x ║ 2 ║
║ y ║ 0 ║
║ z ║ 3 ║
╚══════╩═══════════╝
Of course my end result must deliver both count columns in one table (rather than using separate queries as I have done for demonstration purposes).
I would like not rely on multiple subqueries because my live example will have more than two columns with counts from the same table but various criteria.
I am working with SQL Server (not sure release).
Any help is greatly appreciated.
You can have a conditional count(distinct) by using this code:
SELECT Test, COUNT(DISTINCT "Bug ID") AS "Total Bugs",
count(distinct (CASE WHEN "Status" <> 'Closed' THEN "Bug ID" END)) as "Open Bugs"
FROM Table1
GROUP BY Test
The case statement checks the condition. When true, it returns the Bug ID. When not present, it defaults to NULL, so the id does not get counted.