MySQL - Calculation of invoice balance grouped by client using 3 tables - mysql

I'm currently working on a small client management system and I came accross a feature I can't yet get to work in pure SQL (currently using PHP foreach with lot of lazy loading, so very slow).
I am trying to get the current balance of each clients, so getting the sold of each invoice (by substracting SUM of payments) payment and SUM them. Each clients has invoices in the systems and each invoices can have multiple payments.
Here's my simplified tables for the purpose of being generic:
[Clients]
- id
- name
[Invoices]
- id
- client_id
- amount
[Payments]
- id
- invoice_id
- amount (positive number)
So basically, I want to get this:
+--------------------+----------------------+
| name | balance |
+--------------------+----------------------+
| Client 1 | 342,46 |
| Client 2 | 0,00 |
+--------------------+----------------------+
To get the invoice sold, I need SUM(invoices.amount) - SUM(payments.amount)) but my query isn't working at all. Here's what I got so far:
SELECT DISTINCT
c.name,
SUM(x.sold) AS balance
FROM
clients AS c
RIGHT JOIN
(
SELECT SUM(i.invoiceAmount - total_payments) AS sold
FROM invoices
WHERE i.client_id = c.id
RIGHT JOIN (
SELECT p.id, p.invoice_id, SUM(p.transactionAmount) AS total_payments
FROM payments AS p
GROUP BY p.invoice_id
) AS p ON p.invoice_id=i.id
GROUP BY i.client_id
) AS x ON x.client_id=c.id
GROUP BY c.name
ORDER BY c.name ASC
Does anybody ever done this before? I never did and it's pretty tricky.
Note: I tried to be as much generic as possible, so it may help other people in a similar situation.
UPDATE:
Using #GordonLinoff answer, I was able to get what I wanted using the query he provided me.
select name, SUM(invoices - IFNULL(payments, 0)) as balance
from (
select name, sum(amount) as invoices, sum(payments) as payments
from (select c.id, c.name, i.id as invoiceid, i.amount, sum(p.amount) as payments
from clients c left join
invoices i
on c.id = i.client_id left join
payments p
on p.invoice_id = i.id
group by c.id, c.name, invoiceid, i.amount
) ci
group by name
) x
The result is exactly as I expected.

Aggregating through hierarchical structures is tricky. Here is one method that joins the tables together and then uses two levels of aggregation:
select name, sum(amount) as invoices, sum(payments) as payments
from (select c.id, c.name, i.id, i.amount, sum(p.amount) as payments
from clients c left join
invoices i
on c.id = i.client_id left join
payments p
on p.invoice_id = i.id
group by c.id, c.name, i.id, i.amount
) ci
group by name;

Related

Ordering rows in a parent table by SUM of column in a child table

I have 3 tables in my database
companies{
id,
name,
address
}
stores{
id,
name,
address,
company_id
}
invoices{
id,
total,
date_time,
store_id
}
As you can see, each store is connected to a company via foreign key, also each invoice is connected to a store.
My question is, how can i write a SQL query which will return all stores by a company and order them by their turnover?
If i use the query:
SELECT s.*,
sum(i.total) as turnover FROM store s
JOIN invoices i
ON i.store_id = s.id
WHERE YEAR(i.date_time) = 2019;
I can see the turnover for one store for a year 2019 for example, but i'm struggling to find a way to get a list of store ordered by their turnover for a certain period.
You're going to need to join all 3 tables:
SELECT *
FROM
companies c
INNER JOIN stores s on s.company_id = c.id
INNER JOIN invoices i ON i.store_id = s.id
That's your entire raw data in detailed list. Then you say you want it for a certain company only:
SELECT *
FROM
companies c
INNER JOIN stores s on s.company_id = c.id
INNER JOIN invoices i ON i.store_id = s.id
WHERE c.name = 'Acme Rubber Co'
Then you only want the stores and the invoices amounts:
SELECT s.name, i.total
FROM
companies c
INNER JOIN stores s on s.company_id = c.id
INNER JOIN invoices i ON i.store_id = s.id
WHERE c.name = 'Acme Rubber Co'
Then you want a row set where each line is a single store and the sum of all invoices for that store:
SELECT s.name, SUM(i.total)
FROM
companies c
INNER JOIN stores s on s.company_id = c.id
INNER JOIN invoices i ON i.store_id = s.id
WHERE c.name = 'Acme Rubber Co'
GROUP BY s.name
Lastly you want them in descending order, highest total first:
SELECT s.name as storename, SUM(i.total) as turnover
FROM
companies c
INNER JOIN stores s on s.company_id = c.id
INNER JOIN invoices i ON i.store_id = s.id
WHERE c.name = 'Acme Rubber Co'
GROUP BY s.name
ORDER BY turnover DESC
The order of evaluation in sql is FROM(with joins), WHERE, GROUP BY, SELECT, ORDER BY which is why I use different names in eg the order by than I do in the group by. Conceptually your db only sees the names of things as output by the immediately previous operation. Mysql isn't actually too picky but some db are - you couldn't say GROUP BY storename in sql server because the SELECT that creates the storename alias hasn't been run at the time the group by is done
Note: I wasn't really sure on what you were looking for in a WHERE - you started by saying "all stores turnover for a certain company" and finished saying you were "struggling to get turnover for a period"
If you want a period, use eg WHERE somedatecolumn BETWEEN '2000-01-01' AND '2000-12-31' (Between is inclusive) or WHERE somedatecolumn >= '2000-01-01' AND somedatecolumn < '2001-01-01' (A good pattern to use if the date includes a time too). It is almost never wise to call a function on a column you're searching with, ie do not do WHERE YEAR(somedatecolumn) = 2000 because it disables indexing on the column and makes the search very slow

Select data with a join from 3 tables

I just started learning SQL now and trying to figure out this scenario:
We have 3 tables:
Clients (ClientID, Name, Surname, Age)
Products (ProductID, Name, Price)
Purchases (PurchaseID, Date, ClientID, ProductID)
What would be the best SQL query that will show the amount of purchases (total amount per client) made by clients?
It must only show the clients who made more than 1 purchase.
The result should contain the following fields: Full name, Quantity of purchases, Total amount.
I've got this query but it only joins two tables. How do I join the third table (Products.Price) as well and calculate the total amount per client?
SELECT CONCAT(IFNULL(Name,''),' ', IFNULL(Surname,'')) as FullName,
COUNT(purchaseId) as "Quantity of purchases"
FROM Purchases as P
INNER JOIN Clients as C
on P.ClientID = C.ClientID
GROUP BY C.ClientID,Name, Surname
HAVING COUNT(PurchaseId) > 1;
I would recommend that you use CONCAT_WS() to combine the first name. This handles NULL values more elegantly than your solution.
SELECT CONCAT_WS(c.Name, c.Surname) as FullName,
COUNT(*) as num_purchases,
SUM(pr.price) as total_price
FROM Clients c INNER JOIN
Purchases p
ON P.ClientID = C.ClientID INNER JOIN
Products pr
ON pr.ProductID = p.ProductID
GROUP BY CONCAT_WS(c.Name, c.Surname)
HAVING COUNT(DISTINCT p.PurchaseId) > 1;
Note the COUNT(DISTINCT) in the HAVING clause. This ensures that the clients have at least two purchases. If you only want clients with at least two products or purchases, then you can use COUNT(*) -- but your question is about purchases.
You can try below -
SELECT CONCAT(IFNULL(Name,''),' ', IFNULL(Surname,'')) as FullName,
COUNT(purchaseId) as "Quantity of purchases",sum(price) as totalamount
FROM Purchases as P
INNER JOIN Clients as C
on P.ClientID = C.ClientID
inner join Products p1 on p.productid=p1.productid
GROUP BY C.ClientID,Name, Surname
HAVING COUNT(PurchaseId) > 1

MySQL SUM function in multiple joins

Hi so this is my case I have those tables
Customer {id,name}
Charges {id,amount,customer_id}
Taxes {id,amount,charge_id}
so I want to SUM amount of charges and taxes then group by customer id here is my query
SELECT SUM(ch.amount),SUM(t.amount)
FROM Customer c
LEFT JOIN Charges ch ON ch.customer_id = c.id
LEFT JOIN Taxes t ON t.charge_id = ch.id
GROUP BY c.id;
so in case I have 1 charge for customer than I have 2 taxes for that charge when I use SUM function it's counting amount of charge twice for example in case to show me 10$ it' showing me 20$
I know how can I fix that through subqueries, but I want to know is there any option to get correct value without subqueries like query I use above what can I modify there to fix that.
Thanks !
UPDATED ANSWER WITHOUT SUBQUERIES
SELECT
SUM(CASE WHEN #ch_id != ch.id
THEN ch.amount END) AS ch_amount,
SUM(t.amount) AS t_sum,
c.*,
#ch_id := ch.id
FROM
Customer c
LEFT JOIN charges ch ON c.id = ch.reservation_id
LEFT JOIN taxes t ON ch.id = t.charge_id
GROUP BY rs.id;
You want to know if you can do this without subqueries. No, you can't.
If a row in Charges has more than one corresponding row in Taxes, you can't simply join the tables without duplicating Charges rows. Then, as you have discovered, when you sum them up, you'll get multiple copies.
You need a way to get a virtual table (a subquery) with one row for each Charge.
SELECT ch.customer_id,
ch.amount amount,
tx.tax tax
FROM Charges
LEFT JOIN (
SELECT SUM(amount) tax,
charge_id
FROM Taxes
GROUP BY charge_id
) tx ON ch.id = tx.charge_id
You can then join that subquery to your Customer table to summarize sales by customer.
This is a pain because of the multiple hierarchies. I would suggest:
SELECT c.id, ch.charge_amount, ch.taxes_amount
FROM Customer c LEFT JOIN
(SELECT ch.customer_id, SUM(ch.amount) as charge_amount,
SUM(t.taxes_amount) as taxes_amount
FROM Charges ch LEFT JOIN
(SELECT t.charge_id, SUM(t.amounts) as taxes_amount
FROM taxes t
GROUP BY t.charge_id
) t
ON t.charge_id = ch.id
GROUP BY ch.customer_id
) ch
ON ch.customer_id = c.id;
You are not going to be able to fix this without subqueries of one form or another, if there are multiple charges for a customer or multiple taxes on a charge.

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

Selecting a summed value from a subquery that relies on a joined table

So I'm the lucky guy who gets to optimize a query for our application that's taking far too long for the data we're getting. The data we're looking for isn't significantly complex, but the crappy database design is making it a lot harder then it should be (which is great, because I'm the one who designed it about a year ago).
The general idea is we're trying to calculate the total sales (they buy something that increases their balance) and the total payments (they paid money against their balance) for each customer.
The tables:
Customers
id
company
Sales (invoices):
id
customer_id
Payments (account_payments)
id
customer_id
transaction_id (links to invoice_transactions)
Transactions (invoice_transactions)
id
invoice_id (links to invoices, null if payment)
amount
If a user makes a sale, the info is recorded in invoices and invoice_transactions, with invoice_transactions having the invoice_id of the invoices record that contains the customer_id.
If the user makes a payment, the info is recording in account_payments and invoice_transactions, with invoice_transaction having an invoice_id of NULL, and account_payments containing the transaction_id as well as the customer_id.
I know, it's horrible... And I thought I was being clever! Well, I thought the problem through, and came up with a decent solution:
SELECT SQL_NO_CACHE
c.company,
(SELECT SUM(amount) FROM sales),
(SELECT SUM(amount) FROM payments)
FROM customers c
JOIN invoices i ON i.customer_id = c.id
JOIN invoice_transactions sales ON i.invoice_id = sales.id
JOIN account_payments ap ON ap.customer_id = c.id
JOIN invoice_transactions payments ON ap.transaction_id = payments.id
Which does absolutely nothing except give me an error "#1146 - Table 'db.sales' doesn't exist". I'm guessing it has something to do with sub queries being read prior to joins, but I honestly have no idea. And unfortunately I have no idea of another way to approach this problem... Much appreciated if anyone could give me a hand!
I think the best approach would be to separate the the elements Sales and Payments into subqueries, your current method is cross joining all payments with all invoices before doing the aggregation.
SELECT c.ID,
c.Company,
COALESCE(Sales.Amount, 0) AS Sales,
COALESCE(Payments.Amount, 0) AS Payments
FROM Customers c
LEFT JOIN
( SELECT Customer_ID, SUM(Amount) AS Amount
FROM Invoices
INNER JOIN invoice_transactions
ON Invoice_ID = Invoices.ID
GROUP BY Customer_ID
) As Sales
ON Sales.Customer_ID = c.ID
LEFT JOIN
( SELECT Customer_ID, SUM(Amount) AS Amount
FROM Account_Payments
INNER JOIN invoice_transactions tr
ON tr.ID = Transaction_ID
GROUP BY Customer_ID
) AS Payments
ON Payments.Customer_ID = c.ID;
This will include customers with no invoices and no payments. You can change the left joins to inner joins to manipulate this.
Working Example on SQL Fiddle
Your query doesn't make sense.
After doing all the joining, why not just use the tables in the "from" clause:
SELECT c.company, SUM(sales.amount), SUM(payments.amount)
FROM customers c JOIN invoices i ON i.customer_id = c.id JOIN
invoice_transactions sales ON i.invoice_id = sales.id JOIN
account_payments ap ON ap.customer_id = c.id JOIN
invoice_transactions payments ON ap.transaction_id = payments.id
group by c.company
Just giving a table an alias in the "from" clause does not make it available in subqueries elsewhere in the query.
I also added a GROUP BY clause, since your query seems to be aggregating by company.