MySQL Compare Result in WHERE clause - mysql

I imagine I'm missing something pretty obvious here.
I'm trying to display a list of 'bookings' where the total charges is higher than the total payments for the booking. The charges and payments are stored in separate tables linked using foreign keys.
My query so far is:
SELECT `booking`.`id`,
SUM(`booking_charge`.`amount`) AS `charges`,
SUM(`booking_payment`.`amount`) AS `payments`
FROM `booking`
LEFT JOIN `booking_charge` ON `booking`.`id` = `booking_charge`.`booking_id`
LEFT JOIN `booking_payment` ON `booking`.`id` = `booking_payment`.`booking_id`
WHERE `charges` > `payments` ///this is the incorrect part
GROUP BY `booking`.`id`
My tables look something like this:
Booking (ID)
Booking_Charge (Booking_ID, Amount)
Booking_Payment (Booking_ID, Amount)
MySQL doesn't seem to like comparing the results from these two tables, I'm not sure what I'm missing but I'm sure it's something which would be possible.

try HAVING instead of WHERE like this
SELECT `booking`.`id`,
SUM(`booking_charge`.`amount`) AS `charges`,
SUM(`booking_payment`.`amount`) AS `payments`
FROM `booking`
LEFT JOIN `booking_charge` ON `booking`.`id` = `booking_charge`.`booking_id`
LEFT JOIN `booking_payment` ON `booking`.`id` = `booking_payment`.`booking_id`
GROUP BY `booking`.`id`
HAVING `charges` > `payments`

One of the problems with the query is the cross join between rows from `_charge` and rows from `_payment`. It's a semi-Cartesian join. Each row returned from `_charge` will be matched with each row returned from `_payment`, for a given `booking_id`.
Consider a simple example:
Let's put a single row in `_charge` for $40 for a particular `booking_id`.
And put two rows into `_payment` for $20 each, for the same `booking_id`.
The query will would return total charges of $80. (= 2 x $40). If there were instead five rows in \'_payment\' for $10 each, the query would return a total charges of $200 ( = 5 x $40)
There's a couple of approaches to addressing that issue. One approach is to do the aggregation in an inline view, and return the total of the charges and payments as a single row for each booking_id, and then join those to the booking table. With at most one row per booking_id, the cross join doesn't give rise to the problem of "duplicating" rows from _charge and/or _payment.
For example:
SELECT b.id
, IFNULL(c.amt,0) AS charges
, IFNULL(p.amt,0) AS payments
FROM booking b
LEFT
JOIN ( SELECT bc.booking_id
, SUM(bc.amount) AS amt
FROM booking_charge bc
GROUP BY bc.booking_id
) c
ON c.booking_id = b.id
LEFT
JOIN ( SELECT bp.booking_id
, SUM(bp.amount) AS amt
FROM booking_payment bp
GROUP BY bp.booking_id
) p
ON p.booking_id = b.id
WHERE IFNULL(c.amt,0) > IFNULL(p.amt,0)
We could make use of a HAVING clause, in place of the WHERE.
The query in this answer is not the only way to get the result, nor is it the most efficient. There are other query patterns that will return an equivalent result.

Related

Combining three tables when two have the same foreign key

Hello I am making some accounting software that allows you to store transactions across many months (stored in table transactions) and categorize those transactions based on a list of available categories (stored in table categories). Finally each month the user can create a budget where they build a list (from a subset of categories) and assign a goal to each entry. These lists are stored in table budget.
I need a query that returns a spending and budget summary for a specified month. This summary would be a table of categories.name, budget.goal, and sum(transactions.amount).
Sometimes a user would have a budget item for the specified month but hasn't made any transactions with that category yet (Out to Eat example). Sometimes a user would have an unexpected expense they didn't budget for (Auto Repair Example) and there will be some categories (like vacation) where the user didn't budget that item and there were no expenses of that category.
SELECT categories.id, categories.name, SUM(transactions.amount)
FROM categories
LEFT JOIN transactions ON categories.id=transactions.category_id
WHERE transactions.date LIKE '2019-08-%' GROUP BY categories.id;
gets me half of what I want and
SELECT categories.id, categories.name, budgets.goal
FROM categories
LEFT JOIN budgets ON categories.id=budgets.category_id
WHERE budgets.date LIKE '2019-08-%' GROUP BY categories.id;
gets me the other half of what I want. Is there a single query that can return results like what I have pictured above? I would be even more thrilled if we could exclude results where both goal and sum are NULL.
If you know that there can be at most one budget per category and month, then you can (LEFT) JOIN both (child) tables to categories:
SELECT
c.id,
categories.name,
MAX(b.goal) as budget_goal,
SUM(t.amount) as total_txn_amount
FROM categories c
LEFT JOIN budgets b
ON c.id = b.category_id
AND b.date LIKE '2019-08-%'
LEFT JOIN transactions t
ON c.id = t.category_id
GROUP BY categories.id
HAVING COALESCE(budget_goal, total_txn_amount) IS NOT NULL;
Note that while we know that there can be only one budget per group, the engine doesn't, and might claim that b.goal must be either in the GROUP BY clause or used in an aggregate function. So we use MAX(b.goal) to avoid that error.
To improve the performance of the first JOIN I would change
AND b.date LIKE '2019-08-%'
to
AND b.date >= '2019-08-01'
AND b.date < '2019-08-01' + INTERVAL 1 MONTH
and create a composite index on (category_id, date).
Also to force the uniqueness of category and month combination in the budgets table, I would create a virtual column like
year_month VARCHAR(7) as (LEFT(date, 7))
and a UNIQUE KEY on (category_id, year_month)
Then you can use
LEFT JOIN budgets b
ON c.id = b.category_id
AND b.date = '2019-08'
You can try below query to find the proper result :
SELECT c.name,b.goal,sum(ct.amount) FROM era.categories c left join budget b on b.cat_id=c.id left join transactions ct on ct.cat_id=c.id WHERE b.date LIKE '2019-08-%' group by ct.cat_id;
Thanks.

SQL retrieving filtered value in subquery

in this cust_id is a foreign key and ords returns the number of orders for every customers
SELECT cust_name, (
SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id
) AS ords
FROM Customers
The output is correct but i want to filter it to retrieve only the customers with less than a given amount of orders, i don't know how to filter the subquery ords, i tried WHERE ords < 2 at the end of the code but it doesn't work and i've tried adding AND COUNT(*)<2 after the cust_id comparison but it doesn't work. I am using MySQL
Use the HAVING clause (and use a join instead of a subquery).....
SELECT Customers.cust_id, Customers.cust_name, COUNT(*) ords
FROM Orders, Customers
WHERE Orders.cust_id = Customers.cust_id
GROUP BY 1,2
HAVING COUNT(*)<2
If you want to include people with zero orders you change the join to an outer join.
There is no need for a correlated subquery here, because it calculates the value for each row which doesn't give a "good" performance. A better approach would be to use a regular query with joins, group by and having clause to apply your condition to groups.
Since your condition is to return only customers that have less than 2 orders, left join instead of inner join would be appropriate. It would return customers that have no orders as well (with 0 count).
select
cust_name, count(*)
from
customers c
left join orders o on c.cust_id = o.cust_id
group by cust_name
having count(*) < 2

mysql join Get unjoined values too

The below query is based on my payments table. which joins with daily orders table. I have list of bill numbers in my daily table. with this query I get only bill number for which the payment has been made. Would like to get the complete list of bill numbers from daily table and if payment not received then the received column to be empty.And order them with Bill-no Ascending
create or replace view view_pymts
As SELECT p.`order-id`, p.`order-item-code`,daily.Bill_no,daily.tally_sku,daily.`quantity-purchased` as quantity,daily.`item-price`+daily.`shipping-price`as inv_value,daily.rma_rcvd_amt as return_value, round((SUM(p.amount) + z.other),2) AS received
FROM payments p
INNER JOIN daily ON p.`order-item-code`= daily.`order-item-id`
JOIN (
SELECT `order-id`,
SUM(CASE WHEN `order-item-code` IS NULL
THEN amount
ELSE 0.0 END) / (COUNT(DISTINCT `order-item-code`)) AS other
FROM payments
GROUP BY `order-id`
) z
ON p.`order-id` = z.`order-id`
GROUP BY p.`order-id`, p.`order-item-code`
Replace INNER JOIN with LEFT JOIN. This will make all missing fields NULL in the table on the right of the join (the table after the join statement, rather than the one before, which is said to be on the left of the join).
EDIT: I didn't read the question carefully enough, and #minatverma is correct. You could either use a RIGHT JOIN in place of your INNER JOIN (right join doing the opposite of a left join, making null the fields missing in the table on the left), or switch the order of your tables and use a LEFT JOIN.

SQL Join, right ? left ? inner?

working with mySql I would like to list all purchases that customers made on a specific cathegory of products.
So, I had 3 tables: customers (idCustomer, Name) , cathegories (idCategory, CategoryName) and orders (idOrder, idCustomer, idCathegory, Qty, Price)
But I want a listing with ALL of the customers.
Not only the one who bought that specific idCategory
I thought something like:
select sum(Orders.Qty), Customers.Name
from Orders
right join Customers on Orders.idCustomer = Customer.idCustomer
where Orders.idCategory = 'Notebooks'
group by Orders.idCategory
but this statement only lists the records for customers who exists in Orders table.
And I want all of them ( the one who didnt buy, with qty =0 )
thanks in advance
Most people find left join easier to follow than right join. The logic for left join is to keep all rows in the first table, plus additional information from the remaining tables. So, if you want all customers, then that should be the first table.
You will then have a condition on the second table. Conditions on all but the first table should be in the on clause rather than a where. The reason is simple: when there is no match, then the value will be NULL and the where condition will fail.
So, try something like this:
select sum(o.Qty) as sumqty, c.Name
from Customers c left join
Orders o
on o.idCustomer = c.idCustomer and
o.idCategory = 'Notebooks'
group by c.Name;
Finally, the group by should have a relationship to the select clause.
Try this query
select sum(Orders.Qty), Customers.Name
from Customers
right join Orders on Customer.idCustomer = Orders.idCustomer and Orders.idCategory = 'Notebooks'
group by Customers.Name

MySQL JOIN with multiple tables and SUMS

I am trying to create a query that will take information out of four tables for a billing system that I am creating. I have the following tables:
Table Invoice
InvoiceID (PK)
ClientID
Date
Status
...
Table Client
ClientID (PK)
ClientName
...
Table InvoiceItem
ItemID (PK)
InvoiceID
Amount
...
Table Payments
PaymentID (PK)
InvoiceID
Amount
...
I need to create a query where I can access information from the Invoice table along with the client name, and the sum of all invoice items and payments associated with the invoice.
I have tried the following:
SELECT
Invoice.InvoiceID,
Invoice.`Date`,
Invoice.Terms,
Invoice.DateDue,
Invoice.Status,
Client.ClinicName,
SUM(InvoiceItem.Amount),
SUM(Payment.PaymentAmount)
FROM Invoice
JOIN (Client, InvoiceItem, Payment) ON
(Client.ClientID=Invoice.ClientID AND
InvoiceItem.InvoiceID=Invoice.InvoiceID AND
Payment.InvoiceID=Invoice.InvoiceID)
And while this kind-of works, it is multiplying the SUM() by the number of records used to get the sum (i.e. if there are two payments - 800,400 - It gives me (800+400)*2 -- 2400). I am guessing that there is something with how I am using the join, and I have honestly never had to use join for more than one table, and I would always use GROUP BY, but I can't seem to get that to work correctly.
To make matters worse, I have been lost to the world of vb.net/MSSQL client-side programming for the past several years, so my MySQL is rather rough.
Your problem is that you can't aggregate over two independent tables at once in a single query. However you can do it using subqueries.
SELECT Invoice.InvoiceID, Invoice.`Date`, Invoice.Terms, Invoice.DateDue, Invoice.Status, Client.ClinicName, InvoiceItemSum.SumOfAmount, PaymentSum.SumOfPaymentAmount
FROM Invoice
INNER JOIN Client ON Client.ClientID = Invoice.ClientID
INNER JOIN (
SELECT InvoiceID, SUM(Amount) AS SumOfAmount
FROM InvoiceItem
GROUP BY InvoiceID
) InvoiceItemSum ON InvoiceItemSum.InvoiceID = Invoice.InvoiceID
INNER JOIN (
SELECT InvoiceID, SUM(PaymentAmount) AS SumOfPaymentAmount
FROM Payment
GROUP BY InvoiceID
) PaymentSum ON PaymentSum.InvoiceID = Invoice.InvoiceID
Here try this one
SELECT a.InvoiceID,
a.`Date`,
a.Terms,
a.DateDue,
a.Status,
b.ClinicName,
SUM(c.Amount),
SUM(d.PaymentAmount)
FROM Invoice a
INNER JOIN Client b
on a.ClientID = b.ClientID
INNER JOIN InvoiceItem c
ON c.InvoiceID = a.InvoiceID
INNER JOIN JOIN Payment d
ON d.InvoiceID = a.InvoiceID
GROUP BY a.InvoiceID,
a.`Date`,
a.Terms,
a.DateDue,
a.Status,
b.ClinicName
can you elaborate more on this?
it is multiplying the SUM() by the number of records used to get the
sum (i.e. if there are two payments - 800,400 - It gives me
(800+400)*2 -- 2400)
Try this:
SELECT
Invoice.InvoiceID,
Invoice.`Date`,
Invoice.Terms,
Invoice.DateDue,
Invoice.Status,
Client.ClinicName,
SUM(InvoiceItem.Amount),
SUM(Payment.PaymentAmount)
FROM Invoice
JOIN Client ON Client.ClientID=Invoice.ClientID
JOIN InvoiceItem ON InvoiceItem.InvoiceID=Invoice.InvoiceID
JOIN Payment ON Payment.InvoiceID=Invoice.InvoiceID
group by 1,2,3,4,5,6;
I did two things to your query:
Created separated joins for each of the child tables
Added a group by, without which the sum won't work correctly (fyi, in all other databases, omitting the group by would actually result in a syntax error)
you can also achive it by "CROSS APPLY"
SELECT Invoice.InvoiceID, Invoice.`Date`, Invoice.Terms, Invoice.DateDue, Invoice.Status, Client.ClinicName, InvoiceItemSum.SumOfAmount, PaymentSum.SumOfPaymentAmount
FROM Invoice
INNER JOIN Client ON Client.ClientID = Invoice.ClientID
CROSS APPLY ( SELECT ISNULL(SUM(Amount),0) AS SumOfAmount
FROM InvoiceItem
WHERE InvoiceID = Invoice.InvoiceID
) InvoiceItemSum
CROSS APPLY ( SELECT ISNULL(SUM(PaymentAmount),0) AS SumOfPaymentAmount
FROM Payment
WHERE InvoiceID = Invoice.InvoiceID
) PaymentSum