Alternative to subquery (mysql) - mysql

I have tables that hold payroll header & detail info on a separate table. Schema of each below,
CREATE TABLE `Payroll` (
`payId` int,
`groupId` int,
`startDate` date ,
`endDate` date,
`paymentDate` date
);
insert into Payroll values
(20,2,'2022-06-01','2022-06-30','2022-06-30'),
(21,2,'2022-07-01','2022-07-31','2022-07-31'),
(18,1,'2022-05-01','2022-05-31','2022-05-31'),
(19,1,'2022-07-01','2022-07-31','2022-07-31')
;
CREATE TABLE `PayrollItems` (
`payId` int NOT NULL,
`employeeId` int ,
`payCategory` varchar(45) ,
`value` decimal(15,4) NOT NULL
);
insert into PayrollItems values
(20,12,'salary',200),
(20,12,'housing',500),
(20,13,'salary',400),
(20,14,'salary',1300),
(21,12,'salary',200),
(21,12,'housing',500),
(21,13,'salary',400),
(21,14,'salary',1300),
(18,13,'salary',400),
(18,13,'housing',1300),
(19,14,'salary',500),
(19,14,'housing',1200)
;
I am trying to get a query wherein given a payid i should get the previous payid details. Previous payid is identified by a combination of the paymentDate & groupId fields.
Therefore, for the data above, for payid 19 i should get records of payid 18 i.e each pay item value, as they both are of the same groupid, 1 , and paymentDate of payid 18 is prior to paymentDate of payid 19. There could be more records that have a paymentDate dated prior to payid 19, but only first record dated prior is required.
I tried,
SELECT
y.*
FROM
Payroll ppi2
JOIN
PayrollItems prr3 ON (`prr3`.`payId` = `ppi2`.`payId`)
LEFT JOIN
(SELECT
prr3.employeeId,
ppi2.paymentDate,
ppi2.payId,
ppi2.groupId,
'Last months standard salary' AS Particulars,
MAX(CASE
WHEN TRIM(prr3.payCategory) = 'salary' THEN value
ELSE 0
END) salary,
MAX(CASE
WHEN TRIM(prr3.payCategory) = 'housing' THEN value
ELSE 0
END) housing
FROM
Payroll ppi2
JOIN PayrollItems prr3 ON (`prr3`.`payId` = `ppi2`.`payId`)
AND ppi2.payId = 19
GROUP BY ppi2.payId , prr3.employeeId , ppi2.paymentDate,ppi2.groupId
ORDER BY ppi2.paymentDate DESC) AS y ON (y.groupId = ppi2.groupId)
AND y.paymentDate < ppi2.paymentDate
GROUP BY y.payId,y.employeeId,y.paymentDate,y.groupId,y.Particulars;
but i am not getting any results.
Expected result,given payid = 19, would be,
payid employeeid housing salary
18 13 1300 400
Would there be another way of doing this ?
dbfiddle

WITH
cte AS (
SELECT t1.payid prev_payid
FROM Payroll t1
JOIN Payroll t2 USING (groupId)
WHERE t2.payid = #payid
AND t1.startDate < t2.startDate
ORDER BY t1.startDate DESC LIMIT 1
)
SELECT payId,
employeeId,
SUM(value * (payCategory = 'housing')) housing,
SUM(value * (payCategory = 'salary')) salary
FROM PayrollItems
CROSS JOIN cte
WHERE payid = prev_payid
GROUP BY 1, 2
https://dbfiddle.uk/1LAdyksH

Related

MySQL Attendance Calculation

Here i have table attendances
I need result as shown below
How can i achieve this in mysql without using any programming language
Sql File is Attendances.sql
We can try a pivot query approach, aggregating by user and date:
SELECT
user_id,
DATE(date_time) AS date,
TIMESTAMPDIFF(MINUTE,
MAX(CASE WHEN status = 'IN' THEN date_time END),
MAX(CASE WHEN status = 'OUT' THEN date_time END)) / 60.0 AS hours
FROM yourTable
GROUP BY
user_id,
DATE(date_time);
The caveats of this answer are many. It assumes that there would be only one IN and OUT entry, per user, per day. If a period could cross over dates, then my answer might not generate correct results. Also, if an IN or OUT value be missing, then NULL would be reported for the hours value.
I have Achieve it my self by creating a mysql function and view
Mysql View
CREATE OR REPLACE VIEW `view_attendances` AS
SELECT
`a`.`id` AS `a1_id`,
`a`.`user_id` AS `user_id`,
CAST(`a`.`date_time` AS DATE) AS `date`,
`a`.`date_time` AS `in`,
`a2`.`id` AS `a2_id`,
`a2`.`date_time` AS `out`,
(TIMESTAMPDIFF(SECOND,
`a`.`date_time`,
`a2`.`date_time`) / 3600) AS `hours`
FROM
(`attendances` `a`
JOIN `attendances` `a2` ON (((`a`.`is_confirm` = 1)
AND (`a`.`status` = 'IN')
AND (`a2`.`id` = FN_NEXT_OUT_ATTENDANCE_ID(`a`.`user_id`, `a`.`date_time`, `a`.`status`))
AND (a2.status = 'OUT')
AND (CAST(`a`.`date_time` AS DATE) = CAST(`a2`.`date_time` AS DATE)))))
Mysql Function
CREATE FUNCTION `fn_next_out_attendance_id`( _user_id INT, _attendance_date_time DATETIME, _status VARCHAR(10) ) RETURNS int(11)
BEGIN
DECLARE _id INT(11);
SELECT
id INTO _id
FROM
attendances
WHERE
is_confirm = 1
AND user_id = _user_id
AND date_time > _attendance_date_time
AND `status` <> _status
ORDER BY
date_time ASC LIMIT 1 ;
RETURN if (_id IS NULL, 0, _id);
END

MySQL Inner join last 7 days find top sales

sqlfiddle.com/#!9/ecede/1
Tables and sample data:
CREATE TABLE orders(
order_id INT NOT NULL,
created_at DATE NOT NULL,
country CHAR(10),
total_net_revenue INT NOT NULL,
total_gross_revenue INT NOT NULL,
total_quantity INT NOT NULL
);
INSERT INTO orders VALUES
(101,'2018-03-08',"China",150,250,20),
(102,'2018-03-08',"China",140,280,67),
(103,'2018-03-08',"China",150,190,15),
(111,'2018-02-09',"China",150,190,15),
(104,'2018-03-07',"China",150,190,15);
CREATE TABLE products_inventory(
product_id INT NOT NULL,
product_name CHAR(10),
brand_name CHAR(10),
purchase_price INT NOT NULL,
selling_price INT NOT NULL,
units_remaining_in_inventory INT NOT NULL
);
INSERT INTO products_inventory VALUES
(97,"3ds","Nintendo",100,250,40),
(98,"Switch","Nintendo",140,280,102),
(99,"Mini","Nintendo",40,190,30),
(131,"Fail","Nintendo",40,190,1310);
CREATE TABLE items_in_order(
order_id INT NOT NULL,
country CHAR(10),
brand CHAR(10),
product_name CHAR(10),
product_ID INT NOT NULL,
net_revenue INT NOT NULL,
gross_revenue INT NOT NULL,
quantity INT NOT NULL
);
INSERT INTO items_in_order VALUES
(101,"China","Nintendo","3ds",97,150,250,20),
(102,"China","Nintendo","Switch",98,140,280,67),
(103,"China","Nintendo","Mini",99,150,190,15),
(111,"China","Nintendo","Fail",131,150,190,15),
(104,"China","Nintendo","3ds",97,150,250,20);
Query:
SELECT i1.product_name, i1.product_id, i1.brand,
SUM(i1.net_revenue) AS net_revenue_last7days,
(p2.selling_price - p2.purchase_price) AS item_margin,
remaining_stock
FROM items_in_order i1
INNER JOIN(
SELECT o1.order_id,o1.country,SUM(o1.total_net_revenue) AS net_revenue,
SUM(o1.total_gross_revenue) AS gross_revenue
FROM orders o1
WHERE (created_at >= NOW() - INTERVAL 7 DAY) AND o1.country = "China"
GROUP BY 1,2
) o2
ON o2.order_id = i1.order_id AND o2.country = i1.country AND o2.net_revenue = i1.net_revenue
AND o2.gross_revenue = i1.gross_revenue
INNER JOIN(
SELECT product_id, product_name, brand_name AS brand, purchase_price, selling_price,
SUM(units_remaining_in_inventory) AS remaining_stock
FROM products_inventory p1
GROUP BY 1,2,3
) p2
ON i1.product_id = p2.product_id AND i1.product_name = p2.product_name AND i1.brand = p2.brand
WHERE i1.country = "China" AND i1.brand = "Nintendo"
GROUP BY 1,2,3
ORDER BY 4 DESC
LIMIT 10
I'm trying to get a sum of the revenue on the last 7 days of per products per brand with a join on tables: orders & items_in_order.
Is there a problem with my INNER JOIN? It should show a net revenue for the last 7 days of 300 for the 3ds.
additional info what I'm trying to achieve:
Top revenue per items for Nintendo in China last 7 days
Show units of current inventory of each item
Show the margin for each item
This part of the first JOIN is preventing order #104 from being included:
AND o2.gross_revenue = i1.gross_revenue
The gross revenue in the orders table for order #104 is 190, but it's 250 in the items_in_order table.
If you remove that from the JOIN you get net_revenue_last7days = 300.
http://sqlfiddle.com/#!9/ecede/10

Change db schema or query to return balance for a given period of time

I have came up with the following schema:
CREATE TABLE products
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
quantity INT(10) UNSIGNED NOT NULL,
purchase_price DECIMAL(8,2) NOT NULL,
sell_price DECIMAL(8,2) NOT NULL,
provider VARCHAR(255) NULL,
created_at TIMESTAMP NULL,
PRIMARY KEY (id)
);
# payment methods = {
# "0": "CASH",
# "1": "CREDIT CARD",
# ...
# }
CREATE TABLE orders
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
product_id INT(10) UNSIGNED NOT NULL,
quantity INT(10) UNSIGNED NOT NULL,
payment_method INT(11) NOT NULL DEFAULT 0,
created_at TIMESTAMP NULL,
PRIMARY KEY (id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
# status = {
# "0": "PENDING"
# "1": "PAID"
# }
CREATE TABLE invoices
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
price INT(10) UNSIGNED NOT NULL,
status INT(10) UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP NULL,
PRIMARY KEY (id)
);
# payment methods = {
# "0": 'CASH',
# "1": 'CREDIT CARD',
# ...
# }
CREATE TABLE bills
(
id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
payment_method INT(10) UNSIGNED NOT NULL DEFAULT 0,
price DECIMAL(8,2) NOT NULL,
created_at TIMESTAMP NULL,
PRIMARY KEY (id)
);
And the following query to select a balance:
SELECT ((orders + invoices) - bills) as balance
FROM
(
SELECT SUM(p.sell_price * o.quantity) as orders
FROM orders o
JOIN products p
ON o.product_id = p.id
) orders,
(
SELECT SUM(price) as invoices
FROM invoices
WHERE status = 1
) invoices,
(
SELECT SUM(price) as bills
FROM bills
) bills;
Its working and returning the right balance, but I want to create a chart using Morris.js and I need to change it to return a daily or monthly balance at a given period of time and in this format:
Daily (2017-02-27 to 2017-03-01)
balance | created_at
--------------------------
600.00 | 2017-03-01
50.00 | 2017-02-28
450.00 | 2017-02-27
And monthly (2017-01 to 2017-03)
balance | created_at
--------------------------
200.00 | 2017-03
250.00 | 2017-02
350.00 | 2017-01
What I need to change in my schema or query to return results in this way?
http://sqlfiddle.com/#!9/2289a9/2
Any hints are welcomed. Thanks in advance
Include the created_at date in the SELECT list and a GROUP BY clause in each query.
Ditch the old school comma operator for the join operation, and replace it with a LEFT JOIN.
To return dates for which there are no orders (or no payments, or no invoices) we need a separate row source that is guaranteed to return the date values. As an example, we could use an inline view:
SELECT d.created_dt
FROM ( SELECT '2017-02-27' + INTERVAL 0 DAY AS created_dt
UNION ALL SELECT '2017-02-28'
UNION ALL SELECT '2017-03-01'
) d
ORDER BY d.created_dt
The inline view is just an option. If we had a calendar table that contains rows for the three dates we're interested in, we could make use of that instead. What's important is that we have a query that is guaranteed to return to us exactly three rows with the distinct created_at date values we want to return.
Once we have that, we can add a LEFT JOIN to get the value of "bills" for that date.
SELECT d.created_dt
, b.bills
FROM ( SELECT '2017-02-27' + INTERVAL 0 DAY AS created_dt
UNION ALL SELECT '2017-02-28'
UNION ALL SELECT '2017-03-01'
) d
LEFT
JOIN ( SELECT DATE(bills.created_at) AS created_dt
, SUM(bills.price) AS bills
FROM bills
WHERE bills.created_at >= '2017-02-27'
AND bills.created_at < '2017-03-01' + INTERVAL 1 DAY
GROUP BY DATE(bills.created_at)
) b
ON b.created_dt = d.created_dt
ORDER BY d.created_dt
Extending that to add another LEFT JOIN, to get invoices
SELECT d.created_dt
, i.invoices
, b.bills
FROM ( SELECT '2017-02-27' + INTERVAL 0 DAY AS created_dt
UNION ALL SELECT '2017-02-28'
UNION ALL SELECT '2017-03-01'
) d
LEFT
JOIN ( SELECT DATE(bills.created_at) AS created_dt
, SUM(bills.price) AS bills
FROM bills
WHERE bills.created_at >= '2017-02-27'
AND bills.created_at < '2017-03-01' + INTERVAL 1 DAY
GROUP BY DATE(bills.created_at)
) b
ON b.created_dt = d.created_dt
LEFT
JOIN ( SELECT DATE(invoices.created_at) AS created_dt
, SUM(invoices.price) AS invoices
FROM invoices
WHERE invoices.status = 1
AND invoices.created_at >= '2017-02-27'
AND invoices.created_at < '2017-03-01' + INTERVAL 1 DAY
GROUP BY DATE(invoices.created_at)
) i
ON i.created_dt = d.created_dt
ORDER BY d.created_dt
Similarly, we can a LEFT JOIN to another inline view that returns total orders grouped by DATE(created_at).
It's important that the inline views return distinct value of created_dt, a single row for each date value.
Note that for dev, test and debugging, we can independently execute just the inline view queries.
When a matching row is not returned from a LEFT JOIN, for example no matching row returned from i because there were no invoices on that date, the query is going to return a NULL for the expression i.invoices. To replace the NULL with a zero, we can use the IFNULL function, or the more ANSI standard COALESCE function. For example:
SELECT d.created_dt
, IFNULL(i.invoices,0) AS invoices
, COALESCE(b.bills,0) AS bills
FROM ...
To get the results monthly, we'd need a calendar query that returns one row per month. Let's assume we're going to return a DATE value which as the first day of the month. For example:
SELECT d.created_month
FROM ( SELECT '2017-02-01' + INTERVAL 0 DAY AS created_month
UNION ALL SELECT '2017-03-01'
) d
ORDER BY d.created_month
The inline view queries will need to GROUP BY created_month, so they return a single value for each month value. My preference would be to use a DATE_FORMAT function to return the first day of the month, derived from created_at. But there are other ways to do it. The goal is return a single row for '2017-02-01' and a single row for '2017-03-01'. Note that the date ranges on created_at extend from '2017-02-01' up to (but not including) '2017-04-01', so we get the total for the whole month.
( SELECT DATE_FORMAT(bills.created_at,'%Y-%m-01') AS created_month
, SUM(bills.price) AS bills
FROM bills
WHERE bills.created_at >= '2017-02-01'
AND bills.created_at < '2017-03-01' + INTERVAL 1 MONTH
GROUP BY DATE_FORMAT(bills.created_at,'%Y-%m-01')
) b

Adding totals from multiple tables

I'm using Laravel and Eloquent on MySQL. Essentially I am trying to get results from invoices, including their 'total' and 'paid' amounts.
I have 4 tables:
invoices
id int(10),
date_due date,
client_id int(10),
deleted_at datetime
invoices_items
id int(10),
invoice_id int(10),
price decimal(15,2),
quantity int(10)
invoices_payments (this is a pivot table as payments can apply to other invoices too)
payment_id int(10),
invoice_id int(10),
amount decimal(15,2)
payments
id int(10),
payment_date date,
total decimal(15,2)
(there are other fields but they are not relevant)
I've been using this query, based on a few other answers and other research:
select
`invoices`.*,
SUM(
invoices_items.price * invoices_items.quantity
) as total ,
SUM(
invoices_payments.amount
) as paid
from
`invoices`
left join `invoices_items` on `invoices`.`id` = `invoices_items`.`invoice_id`
left join `invoices_payments` on `invoices`.`id` = `invoices_payments`.`invoice_id`
where
`invoices`.`deleted_at` is null
limit
25
The problem I am having is that the result always only returns 1 row (there are 5 invoices in the test db), and the amount for 'total' or 'paid' is not correct.
I'd like to add that there may not be any records in invoices_payments
-- SOLUTION --
Here is the final query in case anyone runs into similar situation
select
`invoices`.*,
COALESCE(SUM(
invoices_items.price * invoices_items.quantity
),0) as total,
COALESCE(SUM(invoices_payments.amount),0) as paid,
COALESCE(SUM(
invoices_items.price * invoices_items.quantity
),0) - COALESCE(SUM(invoices_payments.amount),0) as balance
from
`invoices`
left join `invoices_items` on `invoices`.`id` = `invoices_items`.`invoice_id`
left join `invoices_payments` on `invoices`.`id` = `invoices_payments`.`invoice_id`
where
`invoices`.`deleted_at` is null
group by
`invoices`.`id`
order by
`balance` desc
limit
25
Add a GROUP BY invoices.id after the WHERE

Check Status of the Duplicate Records

Lets say we have a table named record with 4 fields
id (INT 11 AUTO_INC)
email (VAR 50)
timestamp (INT 11)
status (INT 1)
And the table contains following data
Now we can see that the email address test#xample.com was duplicated 4 times (the record with the lowest timestamp is the original one and all copies after that are duplicates). I can easily count the number of unique records using
SELECT COUNT(DISTINCT email) FROM record
I can also easily find out which email address was duplicated how many times using
SELECT email, count(id) FROM record GROUP BY email HAVING COUNT(id)>1
But now the business question is
How many times STATUS was 1 on all the Duplicate Records?
For example:
For test#example.com there was no duplicate record having status 1
For second#example.com there was 1 duplicate record having status 1
For third#example.com there was 1 duplicate record having status 1
For four#example.com there was no duplicate record having status 1
For five#example.com there were 2 duplicate record having status 1
So the sum of all the numbers is 0 + 1 + 1 + 0 + 2 = 4
Which means there were 4 Duplicate records which had status = 1 In table
Question
How many Duplicate records have status = 1 ?
This is a new solution that works better. It removes the first entry for each email and then counts the rest. It's not easy to read, if possible I would write this in a stored procedure but this works.
select sum(status)
from dude d1
join (select email,
min(ts) as ts
from dude
group by email) mins
using (email)
where d1.ts != mins.ts;
sqlfiddle
original answer below
Your own query to find "which email address was duplicated how many times using"
SELECT email,
count(id) as duplicates
FROM record
GROUP BY email
HAVING COUNT(id)>1
can easily be modified to answer "How many Duplicate records have status = 1"
SELECT email,
count(id) as duplicates_status_sum
FROM record
GROUP BY email
WHERE status = 1
HAVING COUNT(id)>1
Both these queries will answer including the original line so it's actually "duplicates including the original one". You can subtract 1 from the sums if the original one always have status 1.
SELECT email,
count(id) -1 as true_duplicates
FROM record
GROUP BY email
HAVING COUNT(id)>1
SELECT email,
count(id) -1 as true_duplicates_status_sum
FROM record
GROUP BY email
WHERE status = 1
HAVING COUNT(id)>1
If I am not wrong in understanding then your query should be
SELECT `email` , COUNT( `id` ) AS `tot`
FROM `record` , (
SELECT `email` AS `emt` , MIN( `timestamp` ) AS `mtm`
FROM `record`
GROUP BY `email`
) AS `temp`
WHERE `email` = `emt`
AND `timestamp` > `mtm`
AND `status` =1
GROUP BY `email`
HAVING COUNT( `id` ) >=1
First we need to get the minimum timestamp and then find duplicate records that are inserted after this timestamp and having status 1.
If you want the total sum then the query is
SELECT SUM( `tot` ) AS `duplicatesWithStatus1`
FROM (
SELECT `email` , COUNT( `id` ) AS `tot`
FROM `record` , (
SELECT `email` AS `emt` , MIN( `timestamp` ) AS `mtm`
FROM `record`
GROUP BY `email`
) AS `temp`
WHERE `email` = `emt`
AND `timestamp` > `mtm`
AND `status` =1
GROUP BY `email`
HAVING COUNT( `id` ) >=1
) AS t
Hope this is what you want
You can get the count of Duplicate records have status = 1 by
select count(*) as Duplicate_Record_Count
from (select *
from record r
where r.status=1
group by r.email,r.status
having count(r.email)>1 ) t1
The following query will return the duplicate email with status 1 count and timestamp
select r.email,count(*)-1 as Duplicate_Count,min(r.timestamp) as timestamp
from record r
where r.status=1
group by r.email
having count(r.email)>1