Aggregate calculation query on single SQL table - mysql

In my database I have a table, payers_payments, consisting of some foreign keys (payer_id, payment_id) and some pivot fields (amount, pays).
| id | payer_id | payment_id | amount | pays |
|----|----------|------------|--------|------|
| 1 | 1 | 1 | 20 | 0 |
| 2 | 2 | 1 | 23 | 1 |
| 4 | 1 | 2 | 14 | 1 |
| 5 | 2 | 2 | 17 | 1 |
| 6 | 1 | 3 | 10 | 1 |
| 7 | 2 | 3 | 0 | 0 |
Each row represents a contribution a payer made towards a payment. The pays field specifies whether that payer should be included in the payment.
The total cost of a payment is the sum of amount for a given payment_id. So if I wanted to see how much payment 2 cost I would run:
SELECT SUM(amount) AS sumOfPayment
FROM payers_payments
WHERE payment_id=2;
Likewise the total amount a given payer (say, payer 1) has contributed is:
SELECT SUM(amount) AS sumOfPayment
FROM payers_payments
WHERE payer_id=1;
Now, what I want to do is use the concept of fair share. Fair share is the total cost of the payment divided by the number of payers who have pay=1 for that payment. The simplest way I can express this is with a sub-query:
SELECT SUM(payers_payments.amount) / (SELECT count(*)
FROM payers_payments
WHERE pays
AND payers_payments.payment_id = 3
) AS FairShare
FROM payers_payments
WHERE payment_id=3
GROUP BY
payers_payments.payment_id;
For a given payer and payment their fair share is defined as:
SELECT IF(pays, FairShare, 0) as payerFairShare
FROM payers_payments
WHERE payer_id = 1
AND payment_id=3; --FairShare is the query as above
My question is that I want a query to get the total fair share for each payer based on the fair share of each payment and whether or not they are included in the payment. (If pays=false then their fair share is 0)
Based on the above data this is the kind of result I'm after for the 2 payers:
| payer_id | total_paid | fair_share |
|----------|------------|------------|
| 1 | 44 | 25.5 |
| 2 | 40 | 58.5 |
Is there a way to achieve this in a single query or must I do some looping of result sets?
I am agnostic on RDMS but something MySQL-like is good.

I would start by writing a query that works what a single share of a payment is. That is, per payment_id, the sum of all the amounts, divided by the number of people it needs to pay. That result can then be joined back to the original data.
SELECT
payers_payments.payer_id,
SUM(payers_payments.amount ) AS total_paid,
SUM(payers_payments.pays * payments.single_share) AS fair_share
FROM
payers_payments
INNER JOIN
(
SELECT
payment_id,
SUM(amount) / SUM(pays) AS single_share
FROM
payers_payments
GROUP BY
payment_id
)
AS payments
ON payers_payments.payment_id = payments.payment_id
GROUP BY
payers_payments.payer_id
It will be of benefit to have indexes on both (payment_id) and (payer_id).
It will be of benefit to have the amount field in a DECIMAL data-type, though you need to consider what you want to do with rounding. (A total payment of 10.00 needs to be divided three ways, 3.33 each and then what do you want to happen to the spare 0.01?)

Related

SQL query SUM() AND GROUP BY

I have a MySQL table like this:
acco_id | room_id | arrival | amount | persons | available
1 | 1 | 2015-19-12 | 3 | 4 | 1
1 | 2 | 2015-19-12 | 1 | 10 | 1
1 | 1 | 2015-26-12 | 4 | 4 | 1
1 | 2 | 2015-26-12 | 2 | 10 | 1
2 | 3 | 2015-19-12 | 2 | 6 | 0
2 | 4 | 2015-19-12 | 1 | 4 | 1
What im trying to achieve is a single query with a result like:
acco_id | max_persons_available
1 | 22
2 | 4
I tried using a GROUP BY accommodation_id using a query like:
SELECT
accommodation_id,
SUM(amount * persons) as max_persons_available
FROM
availabilities
WHERE
available = 1
GROUP BY
accommodation_id
Only now the result of acco_id uses all arrival dates. When I add arrival to the query no more unique acco_id's.
Does anyone know a good Single SQL which can use the table indexes?
If I'm understanding the question correct (the last part is a bit confusing). You want to have the accomodation id and numbers as you have now but limited to specific arrival dates.
If so the following statement should do exactly that as it is not necessary to put arrival into the select if you "just" use it in the where statement. As else you would need to put it into the group by and thus have non unique accomodation id's.
SELECT
accommodation_id,
SUM(amount * persons) as max_persons_available
FROM
availabilities
WHERE
available = 1 and arrival >= '2015-12-19' and arrival < '2015-10-26'
GROUP BY
accommodation_id
I guess (reading your question) what you are looking for is this but im not sure as your question is a bit unclear:
SELECT
accommodation_id,
arrival,
SUM(amount * persons) as max_persons_available
FROM
availabilities
WHERE
available = 1
GROUP BY
accommodation_id, arrival

Update table A based on count in table B

I have 2 tables (SALESMAN, SOLD), where the SALES table records what cars were sold each day. At night a job runs that must increase the SOLD count in the SALESMAN table. For example, here are two tables:
SALESMAN SALES
+-------------+-----------+------+ +------------+---------+
| SALESMANID | NAME | SOLD | | SALESMANID | VEHICLE |
| 1 | Bob | 1 | | 1 | GM |
| 2 | Charlie | 7 | | 1 | Chrys |
| 3 | Dave | 0 | | 1 | GM |
+-------------+-----------+------+ | 3 | Dodge |
| 3 | GM |
| 2 | Hummer |
+------------+---------+
After the UPDATE has run, Bob's sold count will increase to 4, Charlie's sold count will increase to 8, and Dave's sold count will increase to 2. I'm trying to create something like:
UPDATE SALESMAN SET SOLD=SOLD+(
SELECT COUNT(*)
FROM SALES
WHERE SALESMAN.SALESMANID = SALES.SALESMANID
)
Is this the right way to solve the problem?
I found a similar question here: (Updating one SQL table based on data in another table) but it's not clear if it will selectively updates table A in their example, or all records in table A.
UPDATE: I fixed the typo above but it still doesn't work. 0 rows affected when I run the query.
yes its right your query just change this
WHERE SALESAN.SALESMANID
to
WHERE SALESMAN.SALESMANID
your demo
i dont know why you didnt try it your self before asking a question.
INSERT INTO SALESMAN (SALESMANID, SOLD) (SELECT SALEMANID, COUNT(*) as c FROM SOLD GROUP BY SALEMANID) ON DUPLICATE KEY UPDATE SOLD = c
You may need to name the select and use .c
If the sales table is deleted after this nightly process runs, then this should work
Update m Set
Sold = sold +
(Select Count(*) From Sales
Where SalesmanId = m.SalesmanId)
From Salesman m
UPDATE SALESMAN a,
(SELECT SALESMANID, COUNT(*) SALE_COUNT
FROM SALES
group by SALESMANID) b
set a.SOLD=a.SOLD+ b.SALE_COUNT
WHERE a.SALESMANID = b.SALESMANID;
see SQL Fiddle

Using a Join with Grouped Data Rows

I have two tables, invoices and deposits.
They look a bit like this:
INVOICES
id | paymentType | grossTotal | dateTime
1 | Cash | 1000 | UNIX TIME
2 | Card | 1350 | UNIX TIME
3 | Card | 1250 | UNIX TIME
4 | Card | 750 | UNIX TIME
DEPOSITS
id | paymentType | invNo | dateTime | amount
1 | Cash | 1 | UNIX TIME | 150
2 | Card | 2 | UNIX TIME | 350
The deposits are always past dates, and the invoice dates will be, for example today, so I want to determine the balance paid today on an invoice, ie, invoices.grossTotal - deposits.amount, and list by payment Type.
So, in the table example above, there is £850 would have been paid on Invoice 1 and £1000 on invoice 2, this is simple to achieve with one or two rows of each, but when grouping payment types and deposit invoices I am stuck...
SELECT
invoices.id,
sum(grossTotal)-IFNULL(depositsCheck.previouslyPaid,0) as todayTotal,
depositsCheck.previouslyPaid, sum(grossTotal) as grossTotal
FROM `invoices`
LEFT JOIN (SELECT SUM(amount) as previouslyPaid, invNo
FROM deposits
GROUP BY invNo) depositsCheck ON depositsCheck.invNo=invoices.id
GROUP BY invoices.paymentType ORDER BY id DESC
The SQL Query above, will work for the item paid for with CASH, but not for the Card payments, because, grouping invoices.paymentType means that the id column from the invoices table is no longer correct, so the JOIN fails if this row has an id to which no deposit relates.
How can I run a query as above, but ensuring that I can join the deposits table on any instance of an invoice, grouped by payment type that matches the grouped column id records?
I am using mySql, so please post joins that MySql can do! :D
The problem is that when you use GROUP BY, you can only SELECT aggregates and the columns you have grouped on.
invoices.id is a column you tried to select, but did not group. I think you probably want to add this column to the GROUP BY clause.
SELECT
invoices.id,
sum(grossTotal)-IFNULL(depositsCheck.previouslyPaid,0) as todayTotal,
depositsCheck.previouslyPaid, sum(grossTotal) as grossTotal
FROM `invoices`
LEFT JOIN (SELECT SUM(amount) as previouslyPaid, invNo
FROM deposits
GROUP BY invNo) depositsCheck ON depositsCheck.invNo=invoices.id
GROUP BY invoices.paymentType, invoices.id ORDER BY id DESC
For the example tables you gave, it will probably give:
id | paymentType | grossTotal | dateTime | previouslyPaid
1 | Cash | 1000 | UNIX TIME | 150
2 | Card | 1350 | UNIX TIME | 350
3 | Card | 1250 | UNIX TIME | 0
4 | Card | 750 | UNIX TIME | 0
But in general, you will have something like:
id | paymentType | grossTotal | dateTime | previouslyPaid
1 | Cash | 1000 | UNIX TIME | 150
1 | Card | 1000 | UNIX TIME | 300
2 | Cash | 1350 | UNIX TIME | 350
2 | Card | 1350 | UNIX TIME | 350
Where you can see above, for invoice 1, 150 was paid in cash, and 300 was paid by card.

How to improve the fields name

I am trying to improve the fields name to be more obvious what it means.. is the fields name ok? or what can be changed to make it more obvious?
mysql> select * from order_items;
+----+----------+--------+----------+------------+-------+
| id | order_id | name | quantity | item_price | total |
+----+----------+--------+----------+------------+-------+
| 1 | 5 | Item 1 | 2 | 3.00 | 6.00 |
| 2 | 5 | Item 2 | 1 | 2.00 | 2.00 |
+----+----------+--------+----------+------------+-------+
mysql> select * from orders;
+----+---------+---------+-----------------+---------------+----------------+----------+---------------+------------+----------------+------------+----------------------+--------------------+--------------------+----------------------+
| id | shop_id | user_id | shipping_method | shipping_fees | payment_method | card_fee | commision_fee | item_total | customer_total | shop_total | shop_total_remaining | our_commission_net | our_commission_vat | our_commission_gross |
+----+---------+---------+-----------------+---------------+----------------+----------+---------------+------------+----------------+------------+----------------------+--------------------+--------------------+----------------------+
| 5 | 29 | 9 | delivery | 1.00 | card | 0.50 | 13 | 8.00 | 9.50 | 9.00 | 7.83 | 1.17 | 0.23 | 1.40 |
+----+---------+---------+-----------------+---------------+----------------+----------+---------------+------------+----------------+------------+----------------------+--------------------+--------------------+----------------------+
Fields name description:
item_total Total cost from order_items.order_id = 5
customer_total Total cost for customer to pay (item_total + card_fee + shipping_fee)
shop_total Total order for the shop (item_total + shipping_fees)
shop_total_remaining Total remaining to pay back to shop (shop_total - our_commission_net)
our_commission_net Commission I will make (commission_fee * shop_total / 100)
our_commission_gross Commission inc VAT (our_commission_net + our_commission_vat)
Did I add unnecessary fields?
I used PHP to calculate the cost.
I can see you've actually added many fields that where not necessary. Think of it this way: If you can explain how to calculate a field by applying any operations to other fields, then it is a calculated field.
Those fields shouldn't be part of a minimalistic design as they can be obtained by operating over other fields. Let's go for the most complex example in your tables.
If you wanted to get the total of all the items for an order in your example you would just select the item_total field. But what happens if it wasn't there? Could you still get that number? Yes. This is how:
select sum(oi.total) from order_items oi
inner join order o on (oi.order_id = o.id)
where (o.id = 5)
Now, we've got rid of one field, but can we remove the order_items.total field and still get this result? Yes. Because it is also a calculated field. This is how:
select sum(oi.quantity * oi.item_price) from order_items oi
inner join order o on (oi.order_id = o.id)
where (o.id = 5)
Applying a similar pattern you can get rid of all the fields you've mentioned. And then you'll have a minimal design.
One thing that worths mentioning is that calculating fields is more complex than just querying the value so they are more expensive in terms of CPU and HD usage. The advantage of calculated fields is that you avoid data redundancy and save a bit of space too :)

constructing a query for the following table

can anyone generate a query for me.
Lets say i have a table sales(saleID, date_of_sales, customerID, itemID, saleprice)
date_of_sales is the datetime field which stores the time of the sale.
customerID is self exlpaining tells to whom item was sold.
itemID is ID of the item sold.
saleprice is the price that the item was sold.
I want to construct a query which will give out the detail of the last purchase by each customers. this could be done by using date_of_sales.
Example table
saleID | date_of_sales | customerID | itemID | saleprice
101 | 2008-01-01 | C2000 | I200 | 650 |
102 | 2010-01-01 | C2000 | I333 | 200 |
103 | 2007-01-01 | C3333 | I111 | 800 |
104 | 2009-12-12 | C3333 | I222 | 100 |
this is the example data table, there are only two customer for simplicity.
customer C2000 did his last purchase
on 2010-01-01
customer C3333 did his last purchase
on 2009-12-12
I want to get a result like this
customerID | date_of_sales | itemID | saleprice
C2000 | 2010-01-01 | I333 | 200 |
C3333 | 2009-12-12 | I222 | 100 |
This might be what you are looking for...
SELECT *
FROM sales
WHERE sales.date_of_sales = (SELECT MAX(date_of_sales)
FROM sales s2
WHERE s2.customerID = sales.customerID);
There is a slight problem with it; if there were two sales on the same day to the same customer, you'll get two rows (unless your date-of-sales column includes the time as well). I think the same applies to the answer above, though.
Additionally, if you DO want to get results based on only a SINGLE entry of the maximum date, I would use the query by #Sachin Shanbhag above, but add a maximum sales ID value too... Since that would be implied as sequential, whichever was entered last would probably be the most recent.
SELECT S.* FROM
sales S
INNER JOIN
( SELECT
customerID,
MAX(date_of_sales) dos,
MAX(SalesID) maxSale
FROM
sales
GROUP BY customerID
) S2 ON S.customerID = S2.customerID
AND S.date_of_sales = S2.dos
AND S.SalesID = S2.maxSale