SQL Recursion with Parent / Child for Employees - mysql

I have a little question:
I have a view and want to make a small recursive (my approach) call to a table called employee. There are the employees and a column "reportsTo", which gives the EmployeeId of the respective superior or parent.
There is also an invoice table with all purchases made by customers. The customers have a direct contact person / seller (SupportRepId). Now the annual sales of the employees are to be listed, as well as those of the direct employees. (Reverse ReportTo!)
create view employee_sales_v as
select employee.LastName, employee.ReportsTo, sum(invoice.total) sales,
year(invoice.InvoiceDate) year
from employee
join customer on customer.SupportRepId = employee.EmployeeId
join invoice on invoice.CustomerId = customer.CustomerId
group by employee.LastName, year(invoice.InvoiceDate);
How can I get these sales?
SampleData:
CREATE TABLE `Customer`
(
`CustomerId` INT NOT NULL AUTO_INCREMENT,
`SupportRepId` INT,
CONSTRAINT `PK_Customer` PRIMARY KEY (`CustomerId`)
);
CREATE TABLE `Employee`
(
`EmployeeId` INT NOT NULL AUTO_INCREMENT,
`LastName` NVARCHAR(20) NOT NULL,
`FirstName` NVARCHAR(20) NOT NULL,
`ReportsTo` INT,
CONSTRAINT `PK_Employee` PRIMARY KEY (`EmployeeId`)
);
CREATE TABLE `Invoice`
(
`InvoiceId` INT NOT NULL AUTO_INCREMENT,
`CustomerId` INT NOT NULL,
`InvoiceDate` DATETIME NOT NULL,
`Total` NUMERIC(10,2) NOT NULL,
CONSTRAINT `PK_Invoice` PRIMARY KEY (`InvoiceId`)
);
INSERT INTO `Employee` (`LastName`, `FirstName`, `ReportsTo`) VALUES (N'HGF', N'POI', 0);
INSERT INTO `Employee` (`LastName`, `FirstName`, `ReportsTo`) VALUES (N'XYZ', N'ABC', 1);
INSERT INTO `Customer` (`SupportRepId`) VALUES (2);
INSERT INTO `Customer` (`SupportRepId`) VALUES (2);
INSERT INTO `Invoice` (`CustomerId`, `InvoiceDate`, `Total`) VALUES (1, '2013/1/1', 1.98);
INSERT INTO `Invoice` (`CustomerId`, `InvoiceDate`, `Total`) VALUES (2, '2009/10/2', 3.96);
INSERT INTO `Invoice` (`CustomerId`, `InvoiceDate`, `Total`) VALUES (2, '2010/5/3', 5.94);

... as well as those of the direct employees.
Since only the sales total of the direct employees are needed, recursion is not required in this case.
SQL:
WITH CTE AS
(SELECT SupportRepId, year(i.InvoiceDate) AS YEAR, sum(i.total) total
FROM Customer c
INNER JOIN Invoice i on i.CustomerId = c.CustomerId
GROUP BY year(i.InvoiceDate), SupportRepId) ---CREATE CTE
SELECT
e1.LastName,
e1.ReportsTo,
t1.year,
IFNULL(cte1.total,0.00) AS `My Sales`,
IFNULL(t2.total,0.00) AS `Reportees Sales`
FROM Employee e1
INNER JOIN (SELECT DISTINCT year(i.InvoiceDate) AS year from Invoice i) t1 on 1 = 1 --- ALL DISTINCT YEARS
LEFT JOIN CTE cte1 on e1.EmployeeId = cte1.SupportRepId and t1.year = cte1.year
LEFT JOIN
(SELECT
e2.ReportsTo, cte2.year, sum(cte2.total) total
FROM Employee e2
INNER JOIN CTE cte2 ON e2.EmployeeId = cte2.SupportRepId
GROUP BY e2.ReportsTo, cte2.year) t2 --- GET EMPLOYEE DETAILS
ON t2.ReportsTo = e1.EmployeeId AND t1.year = t2.year
--- WHERE cte1.year is not null || t2.year is not null
ORDER by e1.LastName, t1.year
Details:
--- CREATE CTE : Fetch employees and their sales for each year. Created this subquery as cte since it needs to be reused
--- ALL DISTINCT : Fetch all the distinct year the invoice is generated. This will allow us to generate report for employees even if they have not made any sales on a year. (If that is not required, uncomment WHERE cte1.year is not null || t2.year is not null to filter out employees and reportes with no sales for a year).
--- GET EMPLOYEE DETAILS : This subquery fetches the sales details of the reportees.
Demo:
Fiddle Link over here. I have added additional test cases for validation.
Update: Query update for pre MySQL 8
SELECT
e1.LastName,
e1.ReportsTo,
t1.year,
IFNULL(cte1.total,0.00) AS `My Sales`,
IFNULL(t2.total,0.00) AS `Reportees Sales`
FROM Employee e1
INNER JOIN (SELECT DISTINCT year(i.InvoiceDate) AS year from Invoice i) t1 on 1 = 1 --- ALL DISTINCT YEARS
LEFT JOIN
(SELECT SupportRepId, year(i.InvoiceDate) AS YEAR, sum(i.total) total
FROM Customer c
INNER JOIN Invoice i on i.CustomerId = c.CustomerId
GROUP BY year(i.InvoiceDate), SupportRepId) cte1 on e1.EmployeeId = cte1.SupportRepId and t1.year = cte1.year
LEFT JOIN
(SELECT
e2.ReportsTo, cte2.year, sum(cte2.total) total
FROM Employee e2
INNER JOIN ((SELECT SupportRepId, year(i.InvoiceDate) AS YEAR, sum(i.total) total
FROM Customer c
INNER JOIN Invoice i on i.CustomerId = c.CustomerId
GROUP BY year(i.InvoiceDate), SupportRepId)) cte2
ON e2.EmployeeId = cte2.SupportRepId
GROUP BY e2.ReportsTo, cte2.year) t2 --- GET EMPLOYEE DETAILS
ON t2.ReportsTo = e1.EmployeeId AND t1.year = t2.year
--- WHERE cte1.year is not null || t2.year is not null
ORDER by e1.LastName, t1.year
Fiddle link: https://www.db-fiddle.com/f/td8RaWwYr7AKp1wWGfJ8yK/3

Related

How to set a col val based on 2 other cols in a table in mysql?

In mySql I'm trying to set the value of a column (total) based on the values of 2 other columns of the same table.
http://sqlfiddle.com/#!9/e9c27a/1
CREATE TABLE IF NOT EXISTS `order_total` (
`order_total_id` int(10) NOT NULL AUTO_INCREMENT,
`order_id` int(11) NOT NULL,
`code` varchar(32) NOT NULL,
`title` varchar(255) NOT NULL,
`value` decimal(15,4) NOT NULL DEFAULT '0.0000',
`sort_order` int(3) NOT NULL,
PRIMARY KEY (`order_total_id`),
KEY `order_id` (`order_id`)
) ENGINE=MyISAM AUTO_INCREMENT=244 DEFAULT CHARSET=utf8;
INSERT INTO `order_total` (`order_total_id`, `order_id`, `code`, `title`, `value`, `sort_order`) VALUES
(241, 80, 'sub_total', 'Sub-Total', '400.0000', 1),
(242, 80, 'shipping', 'Free Shipping', '10.0000', 3),
(243, 80, 'total', 'Total', '0', 9);
I tried such a code which works in local mySql, but I thought maybe there is a better way to do it.
UPDATE order_total ot
INNER JOIN (
SELECT order_id, value
FROM order_total
WHERE code = 'sub_total'
GROUP BY order_id
) o ON ot.order_id = o.order_id
INNER JOIN (
SELECT order_id, value
FROM order_total
WHERE code = 'shipping'
GROUP BY order_id
) o2 ON ot.order_id = o2.order_id
SET ot.value = o.value + o2.value
WHERE ot.code = 'total' AND ot.order_id = 80
How to do it more efficient?
You can calculate the total using one query as follows:
update order_total ot
INNER JOIN (
SELECT order_id, sum(value) value
FROM order_total
WHERE code = 'sub_total' or code= 'shipping'
GROUP BY order_id
) o ON ot.order_id = o.order_id
SET ot.value = o.value
WHERE ot.code = 'total' AND ot.order_id = 80;
Your current query is technically invalid, because the subqueries are selecting non aggregate columns which do not appear in the GROUP BY clause. But we can fix this, and make the query more succinct, by using conditional aggregation to find the subtotal and shipping values for each order:
UPDATE order_total ot
INNER JOIN
(
SELECT
order_id,
MAX(CASE WHEN code = 'sub_total' THEN value END) AS sub_value,
MAX(CASE WHEN code = 'shipping' THEN value END) AS shipping_value,
FROM order_total
GROUP BY order_id
) o
ON ot.order_id = o.order_id
SET ot.value = o.sub_value + o.shipping_value
WHERE
ot.code = 'total' AND
ot.order_id = 80;
For this particular problem, the accepted answer is the way to go. But if you wanted something other than the sum, then it would not work. My answer would let you do something like this:
SET ot.value = o.sub_value + 2*o.shipping_value
That is, if we wanted to give the shipping value a weight of 2, this answer allows for this to be easily done.

same colum name with different table id in mysql

tables
person_id (primary key)
phs_people (person_id,first_name,last_name)
phs_cutomers (person_id,company_name)
phs_waiters (person_id,commission)
person_id is key between them.
So my question how can retrive customers firstname and last name, waiter firstname and lastname via person_id?
SELECT
c.first_name AS customer_Fist_name,
c.last_name AS Customer_LastName,
c.first_name AS WaiterFirstName,
c.last_name AS Waiter_LastName,
invoice_number, amount_tendered, sale_time, DATE_FORMAT( sale_time, '%d-%m-%Y' ) AS sale_date, phs_sales.sale_id AS sale_id, SUM( item_unit_price * quantity_purchased * ( 1 - discount_percent /100 ) ) AS amount_due
FROM (
phs_sales
)
LEFT JOIN phs_people c ON c.person_id = phs_sales.customer_id
AND person_id = phs_sales.waiter_id
JOIN phs_sales_items ON phs_sales_items.sale_id = phs_sales.sale_id
LEFT JOIN (
SELECT sale_id, SUM( payment_amount ) AS amount_tendered
FROM phs_sales_payments
WHERE payment_type <> 'Check'
GROUP BY sale_id
) AS payments ON payments.sale_id = phs_sales.sale_id
GROUP BY sale_id
ORDER BY sale_time DESC
LIMIT 25
if I execute this query, I get the following error:
customer_Fist_name NULL,Customer_LastName NULL, WaiterFirstName NULL, Waiter_LastName NULL,
You want to do JOIN's two times on the same table but with different values (customer's data and waiter's data), but you just use a JOIN once and give both conditions there.
To fix this, your have to JOIN the phs_people-Table twice like this:
...
LEFT JOIN phs_people AS c1 ON c1.person_id = phs_sales.customer_id
LEFT JOIN phs_people AS c2 ON c2.person_id = phs_sales.waiter_id
...
and then select the correct data like this:
SELECT
c1.first_name AS customer_Fist_name,
c1.last_name AS Customer_LastName,
c2.first_name AS WaiterFirstName,
c2.last_name AS Waiter_LastName,
...
PS: With this query, you should still get multiple NULL-Values, that's because half of your phs_sales-Table is filled with empty fields...

Insert in to a new table results from multiple queries

I have these queries and I want to insert these results in to a single temporary table. How Can I do that?
select date(max(created_date)) AS 'Last Shipped Date',
sum(item_count) AS 'Last Shipped Units'
from order
where is_shipped = 1
AND date(shipped_date) = (select date(max(shipped_date)) from order);
select
count(distinct o.key) AS 'ACTIVE ',
count(od.ean) AS 'Active_ Units'
from order o
left join order_details od on o.id = od. order_details
Where o.is_active = 1;
select count(distinct order_key) AS 'Total_Orders_Shipped_Yesterday',
sum(item_count) AS 'Total_units_Shipped_yesterday'
from order
where datediff(curdate(), modified_date)=1
AND is_shipped =1;
select count(distinct liquidation_order_id) AS 'orders cancelled',
count(ean) AS 'Units cancelled'
from order_details
where datediff(curdate(), modified_date)=1
AND order_details_status_ =4;
There may be a way to do it in one query, but it will be complicated. It's easier to just do a series of UPDATE queries that fill in the appropriate columns in the table by joining with the other queries that calculate the values.
CREATE TEMPORARY TABLE tempTable (
`Last Shipped Date` DATE,
`Last Shipped Units` INT,
Active INT,
Active_Units INT,
Total_Orders_Shipped_Yesterday INT,
Total_units_Shipped_yesterday INT,
`orders cancelled` INT,
`Units cancelled` INT);
INSERT INTO tempTable (`Last Shipped Date`, `Last Shipped Units`)
select date(max(created_date)) AS 'Last Shipped Date',
sum(item_count) AS 'Last Shipped Units'
from order
where is_shipped = 1
AND date(shipped_date) = (select date(max(shipped_date)) from order);
UPDATE tempTable AS t
JOIN order o
left join order_details od on o.id = od. order_details
SET t.Active = count(distinct o.key), t.Active_Units = count(od.ean)
Where o.is_active = 1;
UPDATE tempTable AS t
JOIN order
SET t.Total_Orders_Shipped_Yesterday = count(distinct order_key),
t.Total_units_Shipped_yesterday = SUM(item_count)
where datediff(curdate(), modified_date)=1
AND is_shipped =1;
UPDATE tempTable AS t
JOIN order_details
SET t.`orders cancelled` = count(distinct liquidation_order_id),
t.`Units cancelled` = COUNT(ean)
where datediff(curdate(), modified_date)=1
AND order_details_status_ =4;

sql select all if column has only two particular values

orders (
o_id INT AUTO_INCREMENT,
o_status TINYINT,
o_description VARCHAR(50),
)
orders_products (
op_id INT AUTO_INCREMENT,
op_order_id INT(11),
op_product_id INT(11),
op_price DECIMAL(19, 2),
)
How to select all orders that have ONLY products with id = 1 and id = 2.
Thank you and sorry from my English...
There are different ways to get the desired result, this utilizes conditional aggregation:
select *
from orders
where o_id in
(
select op_order_id
from orders_products
having count(case when op_product_id = 1 then 1 end) > 0 -- at least one row with 1
and count(case when op_product_id = 2 then 1 end) > 0 -- at least one row with 2
and count(case when op_product_id not in (1,2) then 1 end) = 0 -- no other value
)
Depending on indexes/selectivity EXISTS/NOT EXISTS might be faster:
select o_id
from orders as o
where exists (select *
from orders_products as op
where op.op_order_id = o.o_id
and op.op_product_id = 1) -- at least one row with 1
and exists (select *
from orders_products as op
where op.op_order_id = o.o_id
and op.op_product_id = 2) -- at least one row with 2
and not exists (select *
from orders_products as op
where op.op_order_id = o.o_id
and op.op_product_id not in (1,2)) -- no other value
You could first find all the distinct order and product combination for product 1 or 2 or both, and then look for orders that have both.
create table orders (o_id INT);
create table orders_products (op_order_id INT(11), op_product_id INT(11));
insert into orders values (1), (2);
insert into orders_products values (1, 1), (1, 2), (2, 2);
select o_id from (
select distinct o_id, op_product_id
from orders o
inner join orders_products op on op.op_order_id = o.o_id
where op.op_product_id in (1,2)
) main
group by o_id
having count(*) = 2
Result:
1
Another way to write the query could be like this:
select o_id
from orders o
where exists (select 1 from orders_products where op_order_id = o.o_id and op_product_id = 1)
and exists (select 1 from orders_products where op_order_id = o.o_id and op_product_id = 2)
I would do this using aggregation and having:
select order_id
from order_products op
group by order_id
having sum(product_id = 1) > 0 and
sum(product_id = 2) > 0 and
sum(product_id not in (1, 2)) = 0;
If you want additional information about the order, then just join in the orders table.
Your question is what I call a "set-within-set" query . . . looking for patterns in a hierarchy (that is products within an order). There are several ways to solve this, but the having clause turns out to be quite general.

How to combine these two queries into one? (multiple joins against the same table)

Given two tables, one for workers and one for tasks completed by workers,
CREATE TABLE IF NOT EXISTS `workers` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `workers` (`id`) VALUES
(1);
CREATE TABLE IF NOT EXISTS `tasks` (
`id` int(11) NOT NULL,
`worker_id` int(11) NOT NULL,
`status` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `tasks` (`id`, `worker_id`, `status`) VALUES
(1, 1, 1),
(2, 1, 1),
(3, 1, 2),
(4, 1, 2),
(5, 1, 2);
I'm trying to get the number of tasks each worker has with each status code.
I can say either
SELECT w.*
,COUNT(t1.worker_id) as status_1_count
FROM workers w
LEFT JOIN tasks t1 ON w.id = t1.worker_id AND t1.status = 1
WHERE 1
GROUP BY
t1.worker_id
ORDER BY w.id
or
SELECT w.*
,COUNT(t2.worker_id) as status_2_count
FROM workers w
LEFT JOIN tasks t2 ON w.id = t2.worker_id AND t2.status = 2
WHERE 1
GROUP BY
t2.worker_id
ORDER BY w.id
and get the number of tasks with a single given status code, but when I try to get the counts for multiple task statuses in a single query, it doesn't work!
SELECT w.*
,COUNT(t1.worker_id) as status_1_count
,COUNT(t2.worker_id) as status_2_count
FROM workers w
LEFT JOIN tasks t1 ON w.id = t1.worker_id AND t1.status = 1
LEFT JOIN tasks t2 ON w.id = t2.worker_id AND t2.status = 2
WHERE 1
GROUP BY t1.worker_id
,t2.worker_id
ORDER BY w.id
The tasks table is cross-joining against itself when I would rather it wouldn't!
Is there any way to combine these two queries into one such that we can retrieve the counts for multiple task statuses in a single query?
Thanks!
SELECT w.*,
SUM(t1.status = 1) AS status_1_count,
SUM(t1.status = 2) AS status_2_count
FROM workers w
LEFT JOIN tasks t1 ON w.id = t1.worker_id AND t1.status IN (1, 2)
GROUP BY w.id
ORDER BY w.id;
I'm trying to get the number of tasks each worker has with each status code.
SELECT worker_id, status, COUNT(*)
FROM tasks
GROUP BY worker_id, status;
That's all.
I don't have an instance of MySQL here, but I tested this on a t-sql box and it worked.
select distinct(worker_id),
(select count(*) from tasks where status = 1) as Status1,
(select count(*) from tasks where status = 2) as Status2
from tasks;