Oracle - How to sum related items? - sql

I have a set of data I am wanting to report on (in Oracle 12c). I am wanting to show the sum of certain groupings, based on the value of the 'ITEM' column. If ITEM = ('A', 'B' or 'C'), then they get "grouped" together and the values for columns Cost1 and Cost2 are summed up. This is called "Group1". Group2 contains ITEMSs ('D, E, F'). There are only 6 groupings, made up of around 13 static/fixed items. I know exactly what the items are in each grouping.
Here is a set of sample data, with borders showing what should be grouped together. The 2nd listing shows what the output should look like. I think i am wanting to sum within a case statement, but I just can't seem to wrap my head around it...

The CASE statement is good for this type of one-off value transformation:
SELECT
Year,
CASE
WHEN Item IN ('A', 'B', 'C') THEN 'Group1'
WHEN Item IN ('D', 'E', 'F') THEN 'Group2'
ELSE 'Others' END AS Item,
SUM(Cost1),
SUM(Cost2)
FROM myTable
GROUP BY
Year,
CASE
WHEN Item IN ('A', 'B', 'C') THEN 'Group1'
WHEN Item IN ('D', 'E', 'F') THEN 'Group2'
ELSE 'Others' END;
Note that the GROUP BY expressions must be stated exactly as they are in the column selection list, except you have to drop the alias (AS Item).

Look like you want Group By query:
select Year,
case
when item = 'A' or item = 'B' or item = 'C' then
'Group1'
else
'Group2'
end as Item
Sum(nvl(Cost1, 0)) as Cost1, -- note NVL to prevent nulls from summing
Sum(nvl(Cost2, 0) as Cost2
from MyTable
group by Year,
Item

If the values for groups are not stored then the simplest apporach is to use a case statement and an aggregate function. You could create a cte with the values for easier maintenance; or possibly a pipelined tablefunction
Select year, case when item in ('A','B','C') then 'Group1'
when item in ('D','E','F' then 'GROUP2' else
'Unhandled' end as Item, Sum(Cost1) as cost1, Sum(Cost2) as cost2
From TableName
Group by year, case when item in ('A','B','C') then 'Group1'
when item in ('D','E','F' then 'GROUP2' else
'Unhandled' end

Additional to the mapping with CASE you may extract the mapping logic in the dedicated table myItem
CREATE TABLE myItem
("ITEM" VARCHAR2(1),
"GROUP_ID" VARCHAR2(6),
PRIMARY KEY ("ITEM")
);
and join to this table to get the group before GROUPing
with add_group as (
select a.*, b.group_id from myTable a
left outer join myItem b
on a.item = b.item
)
select .... your GROUP BY query here
This will keep the query more stable even if new items or groups emerges.

Related

Comparing values on previous row to perform calculations on the current row, SQL

sql fiddle: http://sqlfiddle.com/#!4/a717b/1
Here is my Table:
Here is my Code:
select key, status, dat, sysdat,
(case when key = 1 and type = 'Car' status = 'F' then round( sysdat-dat,2) else 0 end ) as Days,
(case when key = 1 and type ='Bus' and Status = 'F' then round( sysdat-dat,2) else 0 end) as Days
from ABC
Expected Output:
So I want to calculate days between the 'dat' column and current date for the following conditions.
1) For every key, The sequence is always car first, bus second.
which means that for every key, only when the status of the car is true we check for the bus.
2) If the Status OF 'CAR' IS 'T' then I don't want to calculate the days
3) If the Status of 'Car' IS 'F' then I want to calculate the days only for the 'Car' and not for 'Bus' because it is always 'car' first 'bus' second
4) If the Status of 'Bus' is 'F' and Status of 'Car' is 'T' then I calculate the days because it matches the condition, 'Car' first and 'Bus' second.
With 2 vehicles
If you always have a car and a bus, and only a car and a bus of the same key, you could self-join the table, and check if either the vehicle a (which you're querying) is Car with status F, or if the related verhicle, b, is a car with status T. In either case you're gonna get a date, and in any other case you don't. That covers your example, and also implies that if car and bus would both be T, the date would still be shown next to the bus only.
select a.key, a.type, a.status, a.dat,
case when
(a.type = 'car' and a.Status = 'F') or -- a is a car and is F
(b.type = 'car' and b.Status = 'T') -- a is related to a car (b), which is T
then
trunc(sysdate) - a.dat
end as DAYS
from
ABC a
join ABC b on b.key = a.key and b.type <> a.type
order by
-- Sort the query by key first, then type.
a.key,
decode(a.type, 'car', 1, 2)
The query above: http://sqlfiddle.com/#!4/a717b/5/0
With N vehicles
If you have more vehicles, a different approach can be better, especially when the number of vehicles is high, or not fixed.
The query below has a list of all the vehicles and their sort order. This is an inline view now, but you could use a separate lookup table for that. A lookup table is even more flexible, because you can just add vehicle types or change their sort order.
Anyway, that lookup table/view can be joined on your main table to have a sort order for each of your record.
You can then make a ranking using window function like rank or dense_rank to introduce a numbering based on that sort order ("falsenumber"), and the fact that status is 'F'. After that, it's easy to put the date on the first row that is F (falsenumber = 1).
with
VW_TYPES as
-- This could be a lookup table as well, instead of this hard-codeslist of unions.
( select 'car' as type, 1 as sortorder from dual union all
select 'bus' as type, 2 as sortorder from dual union all
select 'train' as type, 3 as sortorder from dual union all
select 'airplane' as type, 4 as sortorder from dual union all
select 'rocket' as type, 5 as sortorder from dual),
VW_TYPESTATUS as
( select
a.*,
t.sortorder,
dense_rank() over (partition by key order by case when a.status = 'F' then t.sortorder end) as falsenumber
from
ABC a
join VW_TYPES t on t.type = a.type)
select
ts.key, ts.type, ts.status, ts.dat,
case when ts.falsenumber= 1 then
trunc(sysdate) - ts.dat
end as DAYS
from
VW_TYPESTATUS ts
order by
ts.key, ts.sortorder
The query above: http://sqlfiddle.com/#!4/71f52/8/0
Vehicle types in a separate table: http://sqlfiddle.com/#!4/f055d/1/0
Do note that oracle is case sensitive. 'car' and 'Car' and 'CAR' are not the same thing. Use lower(type) = 'car' if you want to allow type to contain the vehicle type with any casing. Do note that that's bad for using indexes, although I think the impact isn't that bad, since you only got a couple of rows per key.
Alternatively (arguably better), you could introduce a numeric VehicleTypeId in the new types table, and use that id in the ABC table, instead of the string 'Car'.

How to assign multiple values in CASE statement?

I need to assign two values to my select based on a CASE statement. In pseudo:
select
userid
, case
when name in ('A', 'B') then 'Apple'
when name in ('C', 'D') then 'Pear'
end as snack
from
table
;
I am assigning a value for snack. But lets say I also want to assign a value for another variable, drink based on the same conditions. One way would be to repeat the above:
select
userid
, case
when name in ('A', 'B') then 'Apple'
when name in ('C', 'D') then 'Pear'
end as snack
, case
when name in ('A', 'B') then 'Milk'
when name in ('C', 'D') then 'Cola'
end as drink
from
table
;
However, if I have to assign more values based on the same conditions, say food, drink, room, etc. this code becomes hard to maintain.
Is there a better way of doing this? Can I put this in a SQL function, like you would normally do in another (scripting) language and if so, could you please explain how?
When doing things like this I tend to use a join with a table valued constructor:
SELECT t.UserID,
s.Snack,
s.Drink
FROM Table AS T
LEFT JOIN
(VALUES
(1, 'Apple', 'Milk'),
(2, 'Pear', 'Cola')
) AS s (Condition, Snack, Drink)
ON s.Condition = CASE
WHEN t.name IN ('A', 'B') THEN 1
WHEN t.name IN ('C', 'D') THEN 2
END;
I find this to be the most flexible if I need to add further conditions, or columns.
Or more verbose, but also more flexible:
SELECT t.UserID,
s.Snack,
s.Drink
FROM Table AS T
LEFT JOIN
(VALUES
('A', 'Apple', 'Milk'),
('B', 'Apple', 'Milk'),
('C', 'Pear', 'Cola'),
('D', 'Pear', 'Cola')
) AS s (Name, Snack, Drink)
ON s.Name= t.name;
Functions destroy performance. But you could use a common-table-expression(cte):
with cte as
(
Select IsNameInList1 = case when name in ('A', 'B')
then 1 else 0 end,
IsNameInList2 = case when name in ('C', 'D')
then 1 else 0 end,
t.*
from table
)
select
userid
, case when IsNameInList1=1 then 'Apple'
when IsNameInList2=1 then 'Pear'
end as snack
, case when IsNameInList1=1 then 'Milk'
when IsNameInList2=1 then 'Cola'
end as drink
from
cte
;
On this way you have only one place to maintain.
If query performance doesn't matter and you want to use a scalar valued function like this:
CREATE FUNCTION [dbo].[IsNameInList1]
(
#name varchar(100)
)
RETURNS bit
AS
BEGIN
DECLARE #isNameInList bit
BEGIN
SET #isNameInList =
CASE WHEN #name in ('A', 'B')
THEN 1
ELSE 0
END
END
RETURN #isNameInList
END
Then you can use it in your query in this way:
select
userid
, case when dbo.IsNameInList1(name) = 1 then 'Apple'
when dbo.IsNameInList2(name) = 1 then 'Pear'
end as snack
from
table
;
But a more efficient approach would be to use a real table to store them.
Hope this will help you
SELECT userid
,(CASE flag WHEN 1 THEN 'Apple' WHEN 2 THEN 'Pear' WHEN 3 THEN '..etc' END ) as snack
,(CASE flag WHEN 1 THEN 'Milk' WHEN 2 THEN 'Cola' WHEN 3 THEN '..etc' END ) as drink
FROM (
SELECT userid
,( CASE WHEN name IN ('A', 'B') THEN 1
WHEN name IN ('C', 'D') THEN 2
WHEN name IN ('X', 'Y') THEN 3
ELSE 0 END ) AS flag
FROM table ) t
As Ganesh suggested in a comment, I recommend creating a mapping table for this and just do lookups. Much easier to interpet, maintain, and scale - all with better performance.

Query to fetch data matching multiple values across DB rows

Hi I need help on a sql query. The result must match values for a single column across the rows. Here is an example. I need to find out store(s) that must have all of these items for sale: Books, Stationery, and Toys.
Store Items
----- --------
AA PERFUMES
AA TOYS
BB STATIONERY
BB BOOKS
BB TOYS
In the example above, "BB" is the only store that matches all of our criteria and hence the result expected from the query.
I tried query with AND operator (select store from storeitem where items = 'books' and items ='toys' and items='stationery';) and it did not work as it expects all values in the same row and with in operator (select store from storeitem where items in ('books','stationery','toys');) , this doesn't follow must match all values criteria.
Need your help on this.
You could skip using subqueries alltogether and use a HAVING DISTINCT clause to return the stores you need.
SELECT store, COUNT(*)
FROM your_table
WHERE items in ('STATIONAIRY', 'BOOKS', 'TOYS')
GROUP BY
store
HAVING COUNT(DISTINCT items) = 3
;
Example
WITH your_table as (
SELECT 'AA' as Store, 'PERFUMES' as Items FROM dual UNION ALL
SELECT 'AA', 'TOYS' FROM dual UNION ALL
SELECT 'BB', 'STATIONAIRY' FROM dual UNION ALL
SELECT 'BB', 'BOOKS' FROM dual UNION ALL
SELECT 'BB', 'TOYS' FROM dual
)
SELECT store, COUNT(*)
FROM your_table
WHERE items in ('STATIONAIRY', 'BOOKS', 'TOYS')
GROUP BY
store
HAVING COUNT(DISTINCT items) = 3
;
select store
from (
select distinct store, items
from your_table
where items in ('books','stationery','toys')
)
group by store
having count(0) = 3
This is the general approach that should work (not tested on Oracle specifically):
select store from (
select store,
max(case when items = 'stationery' then 1 else 0 end) as has_stationery,
max(case when items = 'books' then 1 else 0 end) as has_books,
max(case when items = 'toys' then 1 else 0 end) as has_toys
from your_table
group by store
) as stores_by_item
where has_stationery = 1 and has_books = 1 and has_toys = 1
If I correctly understand your question, you needed that query:
Select store from storeitem where store in (select store from storeitem where items = 'books') AND store in (select store from storeitem where items ='toys') AND store in (select store from storeitem where items='stationairy')

Count of two types of values in same field in MS-Access

I have this table customerDetail, in which there's a field c_type, in which "a" represents "active" and "d" represents "not-active". Now I have to find the count of both of them in same query.
I used these but no result.
SELECT Count(c_type) AS Active, Count(c_type) AS Not_Active
FROM customerDetail
WHERE c_type="a" OR c_type="d"
of course I know it obviously looks dirty, but I have also tried this, but this didn't worked either-
SELECT
Count(customerDetail.c_type) AS Active,
Count(customerDetail_1.c_type) AS Not_Active
FROM customerDetail INNER JOIN customerDetail AS customerDetail_1
ON customerDetail.Id=customerDetail_1.Id
WHERE (customerDetail.c_type="a") AND (customerDetail_1.c_type="d")
But again it wasn't helpful either, so can anyone please tell me how am I supposed to know the count of both active and non-active in same query?
select c_type, count(*)
from customer_detail
group by c_type
SELECT
SUM(IIF(c_type = "a", 1, 0)) AS Active,
SUM(IIF(c_type = "d", 1, 0)) AS Not_Active,
FROM customerDetail
WHERE c_type IN ("a", "d")
That was for MS Access.
Somehow I missed the tsql tag when first saw this question. In Transact-SQL you can employ a CASE construct, which can be said of as a more powerful equivalent of IIF in Access:
SELECT
SUM(CASE c_type WHEN 'a' THEN 1 ELSE 0 END) AS Active,
SUM(CASE c_type WHEN 'd' THEN 1 ELSE 0 END) AS Not_Active,
FROM customerDetail
WHERE c_type IN ('a', 'd')
Actually, in T-SQL I would use COUNT instead of SUM, like this:
SELECT
COUNT(CASE c_type WHEN 'a' THEN 1 END) AS Active,
COUNT(CASE c_type WHEN 'd' THEN 1 END) AS Not_Active,
FROM customerDetail
WHERE c_type IN ('a', 'd')
Here 1 in each CASE expression can be replaced by anything as long as it is not NULL (NULLs are not counted). If the ELSE part is omitted, like in the query above, ELSE NULL is implied.
The challenge here is your requirement, "in the same query".
It would be easy to create separate queries.
qryActive:
SELECT Count(*) AS Active
FROM customerDetail
WHERE c_type="a"
qryInactive:
SELECT Count(*) AS Not_Active
FROM customerDetail
WHERE c_type="d"
If you need it all in one, you can incorporate them as subqueries.
SELECT a.Active, i.Not_Active
FROM
(SELECT Count(*) AS Active
FROM customerDetail
WHERE c_type="a") AS a,
(SELECT Count(*) AS Not_Active
FROM customerDetail
WHERE c_type="d") AS i
With no JOIN or WHERE condition, you will get a "cross join" (Cartesian product) of the two subqueries. But, since each subquery produces only one row, the composite will consist of only one row.

Is it possible to specify condition in Count()?

Is it possible to specify a condition in Count()? I would like to count only the rows that have, for example, "Manager" in the Position column.
I want to do it in the count statement, not using WHERE; I'm asking about it because I need to count both Managers and Other in the same SELECT (something like Count(Position = Manager), Count(Position = Other)) so WHERE is no use for me in this example.
If you can't just limit the query itself with a where clause, you can use the fact that the count aggregate only counts the non-null values:
select count(case Position when 'Manager' then 1 else null end)
from ...
You can also use the sum aggregate in a similar way:
select sum(case Position when 'Manager' then 1 else 0 end)
from ...
Assuming you do not want to restrict the rows that are returned because you are aggregating other values as well, you can do it like this:
select count(case when Position = 'Manager' then 1 else null end) as ManagerCount
from ...
Let's say within the same column you had values of Manager, Supervisor, and Team Lead, you could get the counts of each like this:
select count(case when Position = 'Manager' then 1 else null end) as ManagerCount,
count(case when Position = 'Supervisor' then 1 else null end) as SupervisorCount,
count(case when Position = 'Team Lead' then 1 else null end) as TeamLeadCount,
from ...
#Guffa 's answer is excellent, just point out that maybe is cleaner with an IF statement
select count(IIF(Position = 'Manager', 1, NULL)) as ManagerCount
from ...
Depends what you mean, but the other interpretation of the meaning is where you want to count rows with a certain value, but don't want to restrict the SELECT to JUST those rows...
You'd do it using SUM() with a clause in, like this instead of using COUNT():
e.g.
SELECT SUM(CASE WHEN Position = 'Manager' THEN 1 ELSE 0 END) AS ManagerCount,
SUM(CASE WHEN Position = 'CEO' THEN 1 ELSE 0 END) AS CEOCount
FROM SomeTable
If using Postgres or SQLite, you can use the Filter clause to improve readability:
SELECT
COUNT(1) FILTER (WHERE POSITION = 'Manager') AS ManagerCount,
COUNT(1) FILTER (WHERE POSITION = 'Other') AS OtherCount
FROM ...
BigQuery also has Countif - see the support across different SQL dialects for these features here:
https://modern-sql.com/feature/filter
You can also use the Pivot Keyword if you are using SQL 2005 or above
more info and from Technet
SELECT *
FROM #Users
PIVOT (
COUNT(Position)
FOR Position
IN (Manager, CEO, Employee)
) as p
Test Data Set
DECLARE #Users TABLE (Position VARCHAR(10))
INSERT INTO #Users (Position) VALUES('Manager')
INSERT INTO #Users (Position) VALUES('Manager')
INSERT INTO #Users (Position) VALUES('Manager')
INSERT INTO #Users (Position) VALUES('CEO')
INSERT INTO #Users (Position) VALUES('Employee')
INSERT INTO #Users (Position) VALUES('Employee')
INSERT INTO #Users (Position) VALUES('Employee')
INSERT INTO #Users (Position) VALUES('Employee')
INSERT INTO #Users (Position) VALUES('Employee')
INSERT INTO #Users (Position) VALUES('Employee')
Do you mean just this:
SELECT Count(*) FROM YourTable WHERE Position = 'Manager'
If so, then yup that works!
I know this is really old, but I like the NULLIF trick for such scenarios, and I found no downsides so far. Just see my copy&pasteable example, which is not very practical though, but demonstrates how to use it.
NULLIF might give you a small negative impact on performance, but I guess it should still be faster than subqueries.
DECLARE #tbl TABLE ( id [int] NOT NULL, field [varchar](50) NOT NULL)
INSERT INTO #tbl (id, field)
SELECT 1, 'Manager'
UNION SELECT 2, 'Manager'
UNION SELECT 3, 'Customer'
UNION SELECT 4, 'Boss'
UNION SELECT 5, 'Intern'
UNION SELECT 6, 'Customer'
UNION SELECT 7, 'Customer'
UNION SELECT 8, 'Wife'
UNION SELECT 9, 'Son'
SELECT * FROM #tbl
SELECT
COUNT(1) AS [total]
,COUNT(1) - COUNT(NULLIF([field], 'Manager')) AS [Managers]
,COUNT(NULLIF([field], 'Manager')) AS [NotManagers]
,(COUNT(1) - COUNT(NULLIF([field], 'Wife'))) + (COUNT(1) - COUNT(NULLIF([field], 'Son'))) AS [Family]
FROM #tbl
Comments appreciated :-)
Here is what I did to get a data set that included both the total and the number that met the criteria, within each shipping container. That let me answer the question "How many shipping containers have more than X% items over size 51"
select
Schedule,
PackageNum,
COUNT (UniqueID) as Total,
SUM (
case
when
Size > 51
then
1
else
0
end
) as NumOverSize
from
Inventory
where
customer like '%PEPSI%'
group by
Schedule, PackageNum
Note with PrestoDB SQL (from Facebook), there is a shortcut:
https://prestodb.io/docs/current/functions/aggregate.html
count_if(x) → bigint
Returns the number of TRUE input values. This
function is equivalent to count(CASE WHEN x THEN 1 END)
SELECT COUNT(*) FROM bla WHERE Position = 'Manager'
In MySQL, boolean expressions evaluate to 0 or 1, so the following aggregation works:
select sum(Position = 'Manager') as ManagerCount
from ...
I think you can use a simple WHERE clause to select only the count some record.