SQL SUM and divide linked tables - mysql

I have the following tables:
create table Cars
(
CarID int,
CarType varchar(50),
PlateNo varchar(20),
CostCenter varchar(50),
);
insert into Cars (CarID, CarType, PlateNo, CostCenter) values
(1,'Coupe','BC18341','CALIFORNIA'),
(2,'Hatchback','AU14974','DAKOTA'),
(3,'Hatchback','BC49207','NYC'),
(4,'SUV','AU10299','FLORIDA'),
(5,'Coupe','AU32703','NYC'),
(6,'Coupe','BC51719','CALIFORNIA'),
(7,'Hatchback','AU30325','IDAHO'),
(8,'SUV','BC52018','CALIFORNIA');
create table Invoices
(
InvoiceID int,
InvoiceDate date,
CostCenterAssigned bit,
InvoiceValue money
);
insert into Invoices (InvoiceID, InvoiceDate, CostCenterAssigned, InvoiceValue) values
(1, '2021-01-02', 0, 978.32),
(2, '2021-01-15', 1, 168.34),
(3, '2021-02-28', 0, 369.13),
(4, '2021-02-05', 0, 772.81),
(5, '2021-03-18', 1, 469.37),
(6, '2021-03-29', 0, 366.83),
(7, '2021-04-01', 0, 173.48),
(8, '2021-04-19', 1, 267.91);
create table InvoicesCostCenterAllocations
(
InvoiceID int,
CarLocation varchar(50)
);
insert into InvoicesCostCenterAllocations (InvoiceID, CarLocation) values
(2, 'CALIFORNIA'),
(2, 'NYC'),
(5, 'FLORIDA'),
(5, 'NYC'),
(8, 'DAKOTA'),
(8, 'CALIFORNIA'),
(8, 'IDAHO');
How can I calculate the total invoice values allocated to that car based on its cost center?
If the invoice is allocated to cars in specific cost centers, then the CostCenterAssigned column is set to true and the cost centers are listed in the InvoicesCostCenterAllocations table linked to the Invoices table by the InvoiceID column. If there is no cost center allocation (CostCenterAssigned column is false) then the invoice value is divided by the total number of cars and summed up.
The sample data in Fiddle: http://sqlfiddle.com/#!18/9bd18/3

The data structure here isn't perfect, hence we need some extra code to solve for this. I needed to gather the amount of cars in each location, as well as to allocate the amounts for each invoice, depending on whether or not it was assigned to a location. I broke out the totals for each invoice type so that you can see the components which are being put together, you won't need those in your final result.
;WITH CarsByLocation AS(
SELECT
CostCenter
,COUNT(*) AS Cars
FROM Cars
GROUP BY CostCenter
UNION ALL
SELECT
''
,COUNT(*) AS Cars
FROM Cars
),CostCenterAssignedInvoices AS (
SELECT
InvoicesCostCenterAllocations.CarLocation
,SUM(invoicevalue) / CarsByLocation.cars AS InvoiceTotal
FROM Invoices
INNER JOIN InvoicesCostCenterAllocations ON invoices.InvoiceID = InvoicesCostCenterAllocations.InvoiceID
INNER JOIN CarsByLocation on InvoicesCostCenterAllocations.CarLocation = CarsByLocation.CostCenter
WHERE CostCenterAssigned = 1 --Not needed, put here for clarification
GROUP BY InvoicesCostCenterAllocations.CarLocation,CarsByLocation.Cars
),UnassignedInvoices AS (
SELECT
'' AS Carlocation
,SUM(invoicevalue)/CarsByLocation.Cars InvoiceTotal
FROM Invoices
INNER JOIN CarsByLocation on CarsByLocation.CostCenter = ''
WHERE CostCenterAssigned = 0
group by CarsByLocation.Cars
)
SELECT
Cars.*
,cca.InvoiceTotal AS AssignedTotal
,ui.InvoiceTotal AS UnassignedTotal
,cca.InvoiceTotal + ui.InvoiceTotal AS Total
FROM Cars
LEFT OUTER JOIN CostCenterAssignedInvoices CCA ON Cars.CostCenter = CCA.CarLocation
LEFT OUTER JOIN UnassignedInvoices UI ON UI.Carlocation = ''
ORDER BY
Cars.CostCenter
,Cars.PlateNo;

Related

In MySQL, return records with specific counts by year

I have an orders data set. I'd like to get email addresses where the count of orders are specific counts for each year. Let's say 2000 = 1, 2001 = 5 or less, 2002 = 3.
select email
from orders
where year in (2000,2001,2002)
That's where I'm stuck. My thought process is pushing me towards using a having clause or a case statement, but I'm at a wall with the condition of considering the counts by year.
In pseudo SQL it'd be:
select email
from orders
where count(year = 2000) = 1
and count(year = 2001) <= 5
and count(year = 2002) = 3
You can't do this in the where clause, you have to group by email and apply your condition in a having clause (or have your group by query as a subquery and use a where condition in an outer query).
select email
from orders
where year in (2000,2001,2003)
group by email
having sum(year = 2000) = 1
and sum(year = 2001) <= 5
and sum(year = 2002) = 3
You can do it as bellow.
Note that you can change the filtred values wthin the where condition for the count value and the associated year.
-- create a table
CREATE TABLE Orders (
id INTEGER PRIMARY KEY,
email VARCHAR(30) NOT NULL,
year int NOT NULL
);
-- insert some values
INSERT INTO Orders VALUES (1, 'test1#mail.com', 2000);
INSERT INTO Orders VALUES (2, 'test2#mail.com', 2001);
INSERT INTO Orders VALUES (3, 'test2#mail.com', 2001);
INSERT INTO Orders VALUES (4, 'test3#mail.com', 2002);
INSERT INTO Orders VALUES (5, 'test2#mail.com', 2001);
INSERT INTO Orders VALUES (6, 'test3#mail.com', 2002);
INSERT INTO Orders VALUES (7, 'test2#mail.com', 2001);
INSERT INTO Orders VALUES (9, 'test2#mail.com', 2001);
INSERT INTO Orders VALUES (10, 'test3#mail.com', 2002);
INSERT INTO Orders VALUES (11, 'test4#mail.com', 2002);
INSERT INTO Orders VALUES (12, 'test4#mail.com', 2001);
INSERT INTO Orders VALUES (13, 'test4#mail.com', 2002);
--sql statement
select result.email from (
select email, year, count(*) As count from Orders where year in (2000,2001,2002)
group by year, email
)result
where
(result.count = 1 and year = 2000)
;
Output:
email
test1#mail.com

Tricky sql query required, finding a sum of a subquery

A relevant part of my db looks as follows (MS Visio, I know I'm pathetic :D):
I need to extract a list consisting of all items in a category as well as bundles. So I have to use UNION. First part of a UNION for your reference (as it sets the data format for the SELECT in the second part of UNION; note that ? signifies where an argument goes in node-mysql):
SELECT `ID`, `Name`, `Description`,
`PictureID`, `SellingPrice`,
`Cost`, 0 AS `Bundle`
FROM `Item`
WHERE `CategoryID`=? AND
`ID` IN (
SELECT `ItemID`
FROM `Stock`
WHERE `CityID`=?
AND `IsLimitless`=1 OR `Quantity`>0
)
So I want to present my Bundles as if they are also items, with all same fields etc.
My attempt:
SELECT `ID`, `Name`, `Description`, `PictureID`,
(
SELECT SUM( // Here SQL indicates a syntax problem
SELECT `ItemAmount`*`PriceModifier`*(
SELECT `SellingPrice`
FROM `Item`
WHERE `ID`=`BundleItem`.`ItemID`
)
FROM `BundleItem` WHERE `BundleID`=`Bundle`.`ID`
)
) AS `SellingPrice`,
(
SELECT SUM(
SELECT `ItemAmount`*(
SELECT `Cost`
FROM `Item`
WHERE `ID`=`BundleItem`.`ItemID`
)
FROM `BundleItem` WHERE `BundleID`=`Bundle`.`ID`
)
) AS `Cost`,
1 AS `Bundle`
FROM `Bundle`
WHERE `ID` IN (
SELECT `BundleID`
FROM `BundleCategory`
WHERE `CategoryID`=?
)
//No need to check bundles for stock due to business logic
I have a faint idea that I'm overcomplicating this, but I can't put my finger on it, unfortunately.
Any advise will be very welcome and thanks in advance for taking your time. <3
Sample data:
Fields of no interest like "Description"/"PictureID"/"SupplierID" will be omitted
for the relevant parts to fit on screen
**Bundle**
ID Name Description PictureID
1 Valentine Pack Blah-blah tasty buy me imgur link in text
**Item**
ID Name SellingPrice Cost CategoryID
1 Movie Ticket 10 2 24
2 Box of Chocolates 5 1 4
3 Teddy Bear 15 3 2
4 Roses 10 4 8
**Stock**
ItemID CityID Quantity IsLimitLess
1 1 25 false
1 2 11 false
2 1 84 false
3 1 33 false
4 1 1 true
4 3 1 true
**BundleItem**
BundleID ItemID ItemAmount PriceModifier
1 1 2 1.25
1 2 1 1
1 3 1 1
1 4 5 0.75
**BundleCategory** (bundle for marketing reasons can appear in different
categories depending on its contents)
BundleID CategoryID
1 4 //Sweets
1 2 //Toys
1 8 //Flowers
Desired output: (For searching CityID 1, CategoryID 8, Flowers)
ID Name (Descr/PicID) SellingPrice Cost Bundle
4 Roses 10 4 false
1 Valentine Pack 82.5 28 true
/*2*10*1.25+ 2*2+ <movie
1*1*5+ 1*1+ <chocolate
1*1*15+ 3*1+ <teddy bear
5*0.75*10 5*4 <roses */
User suggested solutions
As per #drakin8564 's suggestion I tried doing
SELECT `ID`, `Name`, `Description`, `PictureID`,
(
SELECT SUM((
SELECT `ItemAmount`*`PriceModifier`*(
SELECT `SellingPrice`
FROM `Item`
WHERE `ID`=`BundleItem`.`ItemID`
)
FROM `BundleItem` WHERE `BundleID`=`Bundle`.`ID`
))
) AS `SellingPrice`,
(
SELECT SUM((
SELECT `ItemAmount`*(
SELECT `Cost`
FROM `Item`
WHERE `ID`=`BundleItem`.`ItemID`
)
FROM `BundleItem` WHERE `BundleID`=`Bundle`.`ID`
))
) AS `Cost`,
1 AS `Bundle`
FROM `Bundle`
WHERE `ID` IN (
SELECT `BundleID`
FROM `BundleCategory`
WHERE `CategoryID`=8
)
Returns
(1242): Subquery returns more than 1 row.
This happens even when I try SELECT SUM((SELECT ID FROM Item)). Weird.
I commented on other solutions about how good they work. I appreciate all you guys taking part in this. <3
It looks like you had a few syntax issues. Your code worked with a few changes. See comments in query for details.
http://sqlfiddle.com/#!9/ee0725/16
SELECT `ID`, `Name`, `Description`, `PictureID`,
(SELECT SUM(`ItemAmount`*`PriceModifier`*( -- changed order of SELECT and SUM; removed extra SELECT; fixed Parens
SELECT `SellingPrice`
FROM `Item`
WHERE `ID`=`BundleItem`.`ItemID`
))
FROM `BundleItem` WHERE `BundleID`=`Bundle`.`ID`)
AS `SellingPrice`,
(SELECT SUM(`ItemAmount`*( -- changed order of SELECT and SUM; removed extra SELECT; fixed Parens
SELECT `Cost`
FROM `Item`
WHERE `ID`=`BundleItem`.`ItemID`
))
FROM `BundleItem` WHERE `BundleID`=`Bundle`.`ID`)
AS `Cost`,
1 AS `Bundle`
FROM `Bundle`
WHERE `ID` IN (
SELECT `BundleID`
FROM `BundleCategory`
WHERE `CategoryID`=8
);
Something like this should work
SELECT tb.`ID`, MAX(tb.`Name`), MAX(tb.`Description`), MAX(tb.`PictureID`),
SUM(`ItemAmount`*`PriceModifier`*`SellingPrice`) AS `SellingPrice`,
SUM(`ItemAmount`*`Cost`) AS `Cost`,
1 AS `Bundle`
FROM `Bundle` tb
JOIN `BundleItem` tbi on tb.ID=tbi.BundleID
JOIN `Item` ti on tbi.ItemID=ti.ID
WHERE tb.`ID` IN (
SELECT `BundleID`
FROM `BundleCategory`
WHERE `CategoryID`=?
)
GROUP BY tb.ID
//No need to check bundles for stock due to business logic
Your syntax error is because your subquery is not wrapped in (). Examples below.
This will fail:
SELECT SUM(SELECT 1);
This will work:
SELECT SUM((SELECT 1));
Assumption #1: All items must have enough stock in a city for a bundle to be available in that city. (See query comments for how to remove this business rule)
In the sample data, there are no bundles that are fully in stock in any cities - to remedy this, I changed the Quanity for ItemID=4 in CityID=1 from "1" to "5". This created your desired output.
Assumption #2: Stock.Quantity=0 is allowed.
This solution produces query results that contain all Items and Bundles for every City and Category where the Item or Bundle is in stock. The where clause at the bottom filters it to CityID=1 and Category=8 per the original request.
Note: You can paste the Solution and Schema below into www.sqlfiddle.com and see the results.
UPDATE
Fixed BundleCategory join.
Solution
select * from (
select
Stock.CityID,
Item.CategoryID,
Item.ID,
Item.Name,
Item.Description,
Item.SellingPrice,
Item.Cost,
'false' as Bundle
from Item
inner join Stock on Stock.ItemID = Item.ID
where IFNULL(Stock.Quantity,0) > 0 -- remove this to show out of stock items
union
select
BundleSummary.CityID,
BundleCategory.CategoryID,
Bundle.ID,
Bundle.Name,
Bundle.Description,
BundleSummary.SellingPrice as SellingPrice,
BundleSummary.Cost as Cost,
'true' as Bundle
from Bundle
inner join (
select
BundleItem.BundleID,
City.CityID,
MIN(IF(IFNULL(Stock.Quantity, 0) < BundleItem.ItemAmount, 0, 1)) as InStock,
SUM(Item.SellingPrice * BundleItem.ItemAmount * BundleItem.PriceModifier) as SellingPrice,
SUM(Item.Cost * BundleItem.ItemAmount) as Cost
from BundleItem
inner join Item on Item.ID = BundleItem.ItemID
inner join (select distinct CityID from Stock where CityID IS NOT NULL) as City on 1=1
left join Stock on Stock.ItemID = Item.ID and Stock.CityID = City.CityID
group by BundleItem.BundleID, City.CityID
) as BundleSummary on BundleSummary.BundleID = Bundle.ID
inner join BundleCategory on BundleCategory.BundleID = Bundle.ID
where BundleSummary.InStock = 1 -- remove this to show out of stock bundles
) as qry1
where CityID=1 and CategoryID=8;
I also generated a script to create the database schema and populate it with the sample data. Thought this might be helpful to anyone who is using this solution to investigate their own issues.
Schema
create table Item (
ID int,
Name varchar(255),
Description varchar(255),
PictureID int,
SellingPrice DECIMAL(12,4),
Cost DECIMAL(12,4),
SupplierID int,
CategoryID int
);
insert into Item values (1, 'Movie Ticket', '', NULL, 10, 2, NULL, 24);
insert into Item values (2, 'Box of Chocolates', '', NULL, 5, 1, NULL, 4);
insert into Item values (3, 'Teddy Bear', '', NULL, 15, 3, NULL, 2);
insert into Item values (4, 'Roses', '', NULL, 10, 4, NULL, 8);
create table Bundle (
ID int,
Name varchar(255),
Description varchar(255),
PictureID int
);
insert into Bundle values (1, 'Valentine Pack', 'Blah-blah tasty buy me', NULL);
create table Stock (
ItemID int,
CityID int,
Quantity int,
IsLimitless bit
);
insert into Stock values (1, 1, 25, false);
insert into Stock values (1, 2, 11, false);
insert into Stock values (2, 1, 84, false);
insert into Stock values (3, 1, 33, false);
insert into Stock values (4, 1, 5, true);
insert into Stock values (4, 3, 1, true);
create table BundleItem (
BundleID int,
ItemID int,
ItemAmount int,
PriceModifier DECIMAL(12,4)
);
insert into BundleItem values (1, 1, 2, 1.25);
insert into BundleItem values (1, 2, 1, 1);
insert into BundleItem values (1, 3, 1, 1);
insert into BundleItem values (1, 4, 5, 0.75);
create table BundleCategory (
BundleID int,
CategoryID int
);
insert into BundleCategory values (1, 4); -- Sweets
insert into BundleCategory values (1, 2); -- Toys
insert into BundleCategory values (1, 8); -- Flowers

Returning records which only have one specific many to many relation

Given this structure
CREATE TABLE locations
(`id` int, `Name` varchar(128))
;
INSERT INTO locations
(`id`, `Name`)
VALUES
(1, 'Location 1'),
(2, 'Location 2'),
(3, 'Location 3')
;
CREATE TABLE locations_publications
(`id` int, `publication_id` int, `location_id` int)
;
INSERT INTO locations_publications
(`id`, `publication_id`, `location_id`)
VALUES
(1, 1, 1),
(2, 2, 1),
(3, 2, 2),
(4, 1, 3)
;
I would like to find only Location 2 based on the fact that it has only one relation with a publication_id = 2.
It should not return location one due to the fact that it has two relation rows.
This is sort of what I'm looking for but of course dosnt work because it limits the relationship to where publication_id = 2.
select * from locations
join locations_publications on locations_publications.location_id = locations.id
where locations_publications.publication_id = 2
group by (locations.location_id)
having count(*) = 1
You can do this with aggregation:
select location_id
from locations_publications
group by location_id
having count(*) = 1
If a location might have multiple records with the same publication, change the having criteria to count(distinct publication_id) = 1
Given your edits, you can use conditional aggregation for that:
select location_id
from locations_publications
group by location_id
having count(*) = sum(case when publication_id = 2 then 1 else 0 end)

Efficient way of writing subquery join statement in mysql

I have a reviews table like the one below:
A user can up vote or down vote these reviews. For which, I am maintaining another table named review_counts. It looks like the one below:
Here, 1 means up vote and -1 is down vote.
Now, I am joining these two tables such that I will get reviews with total number of up vote counts and down vote counts all together. To achieve this, I have written the below query which is working fine.
SELECT * FROM `reviews` as x
LEFT JOIN
(SELECT count(votes) as vote_up, review_id FROM `review_counts` WHERE votes = 1) as y ON x.review_id = y.review_id
LEFT JOIN
(SELECT count(votes) as vote_down, review_id FROM `review_counts` WHERE votes = -1) as z ON x.review_id = z.review_id
For which, I get the result like this:
Now, the question is that I am using two JOINS on same table to get the vote up and vote down, Is there any other way through which I can achieve similar results using single join statement?
You could do this with a single LEFT JOIN and SUM(CASE WHEN...END):
CREATE TABLE reviews(
id INT,
review_id VARCHAR(10),
review VARCHAR(10)
)
CREATE TABLE review_counts(
id INT,
review_id VARCHAR(10),
votes INT
)
INSERT INTO reviews VALUES
(1, 'review1', 'Review 1'),
(2, 'review2', 'Review 2');
INSERT INTO review_counts VALUES
(1, 'Review1', 1),
(2, 'Review1', 1),
(3, 'Review1', 1),
(4, 'Review1', 1),
(5, 'Review1', 1),
(6, 'Review2', -1),
(7, 'Review2', -1),
(8, 'Review2', -1),
(9, 'Review2', -1),
(10, 'Review2', -1);
SELECT
r.*,
SUM(CASE WHEN c.votes = 1 THEN 1 ELSE 0 END) AS Vote_Up,
SUM(CASE WHEN c.votes = -1 THEN 1 ELSE 0 END) AS Vote_Down
FROM reviews r
LEFT JOIN review_counts c
ON c.review_id = r.review_id
GROUP BY r.id, r.review_id, r.review
DROP TABLE reviews
DROP TABLE review_counts
RESULT
id review_id review Vote_Up Vote_Down
----------- ---------- ---------- ----------- -----------
1 review1 Review 1 5 0
2 review2 Review 2 0 5

Get reply numbers in mysql

I am working on a product review page where it will display several submitted reviews as well as the number of comments to each of them.
I thought I could use
SELECT title AS review_title,COUNT(id_group) AS Approved_reply_number
WHERE approved <> '0'
GROUP BY id_group`
but read somewhere that it isn't possible to copy the id values into another row on the insert process. So if someone submits a review, the id_group field for the reviews has to be left empty.
Here is the table example:
CREATE TABLE product_review
(`ID` int, `title` varchar(21), `id_group` int,`approved` int)
;
INSERT INTO product_review
(`ID`, `title`, `id_group`,`approved`)
VALUES
(1, 'AAA', Null,1),
(2, 'BBB', 1,1),
(3, 'CCC', Null,1),
(4, 'DDD', 3,0),
(5, 'EEE', 1,1),
(6, 'FFF', Null,1),
(7, 'GGG', 6,1),
(8, 'HHH',1,1),
(9, 'III', 6,1)
;
Those that are Null in id_group are the submitted reviews. The rest are replies and they contain the id of their corresponding reviews. I was wondering how can I get an output like this:
review_title approved_reply_number
AAA 3
CCC 0
FFF 2
You can use a self join and count query with group by and also a where clause to filter out reviews only
select t.title review_title ,count(*) approved_reply_number
from product_review t
left join product_review t1 on(t.id = t1.id_group)
where t.id_group is null
group by t.id
Demo