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
Related
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
I am designing a Point Of Sale "POS" database while using MySQL as my DBMS.
When adding products to the products database, I add the product name (ie. "Coca-Cola 2.L") and the UPC code, cost, price, department, supplier and the quantity.
So if I receive an order of 1000 bottles which cost me $1 per bottle
Then let's say 2 months down the road, I have only 100 bottles left in stock "my cost for these is $100". Now, I ordered 5,000 bottles more, but this time because I ordered 5,000 the supplier is giving me a 0.10 off each bottle (ie. cost of $0.90 each.) So, the cost is $4500 for my second order. I want to ensure that I track my profit very accurately. Therefore, the 100 that I bought at $1 I want to be able to track those separately at the $1 cost and the new order at the $0.90 cost.
The way I currently track cost for each sale item, by reading the current cost from the product table and I save it in the sales_transactions_items table along with the sold price, transaction_id, and product_id.
The issue that I am having now is that when I received the 5000 bottle, I changed that cost from $1 to $0.90 which made my profit goes up by (100 bottles x $0.10 saving per bottle) 100x0.10 = $10 in profit that I did not actually profited. After the quantity is sold, this discrepancy occurred because the the quantity value reached 5100 bottles where 100 were purchase at $1 and 5,000 were purchased at $0.90 each.
My question: how to I solve for this issue where I can truly capture how much each item cost me.
Here is a generic enough but oversimplified example of how your schema might look like
CREATE TABLE products
(
`id` int not null auto_increment primary key,
`name` varchar(13),
`price` decimal(12, 2), -- current sale price. You might want to extract it into it's own table `prices`
...
);
CREATE TABLE orders
(
`id` int not null auto_increment primary key,
`date` date,
...
);
CREATE TABLE order_items
(
`id` int not null auto_increment primary key,
`order_id` int not null,
`product_id` int,
`quantity` decimal(12, 3),
`cost` decimal(12, 2),
foreign key (`order_id`) references orders (id),
foreign key (`product_id`) references products (id)
);
CREATE TABLE sales
(
`id` int not null auto_increment primary key,
`date` datetime,
...
);
CREATE TABLE sale_items
(
`id` int not null auto_increment primary key,
`sale_id` int not null,
`product_id` int,
`quantity` decimal(12, 3),
`price` decimal(12, 2),
foreign key (`sale_id`) references sales (id),
foreign key (`product_id`) references products (id)
);
Here is a SQLFiddle demo
This gives you the ability to independently track your costs and sales.
One way to calculate total sales, total actual cost and margin per product
SELECT product_id, p.name, sales_quantity, sales_total, cost_total, sales_total - cost_total margin
FROM
(
SELECT product_id, sales_quantity, sales_total, SUM(
CASE WHEN sales_quantity >= running_quantity
THEN cost * quantity
WHEN sales_quantity BETWEEN running_quantity - quantity AND running_quantity
THEN cost * (sales_quantity - (running_quantity - quantity))
ELSE 0
END) cost_total
FROM
(
SELECT s.*, o.cost, o.quantity, o.running_quantity
FROM
(
SELECT product_id,
SUM(quantity * price) sales_total,
SUM(quantity) sales_quantity
FROM sale_items
GROUP BY product_id
) s JOIN
(
SELECT product_id, cost, quantity, (
SELECT SUM(quantity)
FROM order_items
WHERE product_id = i.product_id
AND order_id <= i.order_id
) running_quantity
FROM order_items i
) o
ON s.product_id = o.product_id
) q
GROUP BY product_id, sales_quantity, sales_total
) q JOIN products p
ON q.product_id = p.id
Sample output:
| PRODUCT_ID | NAME | SALES_QUANTITY | SALES_TOTAL | COST_TOTAL | MARGIN |
|------------|---------------|----------------|-------------|------------|--------|
| 1 | Coca-Cola 2.L | 150 | 187.5 | 145 | 42.5 |
Here is a SQLFiddle demo
You may see that in the example first 100 bottles out of 150 sold cost $100 ($1*100), and the rest 50 cost $45 (0.9*50)
Declare #FromDate DateTime=GetDate()
declare #ToDate DateTime=GetDate()
Select ToDateProfit.ProductID,ToDateProfit.Name,ToDateProfit.sales_quantity- ISNULL(FromDateProfit.sales_quantity,0) as Sales_Quantity
,ToDateProfit.sales_total-ISNULL(FromDateProfit.sales_total,0) as Sales_Total
,ToDateProfit.cost_total-ISNULL(FromDateProfit.cost_total,0) as Cost_Total
,ToDateProfit.margin-ISNULL(FromDateProfit.margin,0) as Margin
From
(
SELECT P.ProductID, p.name, sales_quantity, sales_total, cost_total, sales_total - cost_total margin
FROM
(
SELECT productid, sales_quantity, sales_total, SUM(
CASE WHEN sales_quantity >= running_quantity
THEN PurchasePrice* quantity
WHEN sales_quantity BETWEEN running_quantity - quantity AND running_quantity
THEN PurchasePrice * (sales_quantity - (running_quantity - quantity))
ELSE 0
END) cost_total
FROM
(
SELECT s.*, o.PurchasePrice, o.quantity, o.running_quantity
FROM
(
SELECT ProductID,
SUM((quantity * UnitPrice)/ExchangeRate) sales_total,
SUM(quantity) sales_quantity
FROM SaleDetail
Inner Join Sale on Sale.SaleID=SaleDetail.SaleID
where Convert(Date,Sale.SaleDate)<=Convert(date,#ToDate) --(must minus saledate <FromDate (GetDate() is ToDate) )
GROUP BY ProductID
) s JOIN
(
SELECT productid, BuyingPrice/ExchangeRate as PurchasePrice, quantity, (
SELECT SUM(quantity)
FROM PurchaseDetail
inner join Purchase Pur on Pur.PurchaseID=PurchaseDetail.PurchaseID
WHERE productid = i.productid
AND Pur.Date <= PurI.date ----Fifo (if want lifo change <= to >=)
) running_quantity
FROM PurchaseDetail i
Inner join Purchase PurI on PurI.PurchaseID=i.PurchaseID
) o
ON s.productid = o.productid
)q
GROUP BY productid, sales_quantity, sales_total
) q JOIN product p
ON q.ProductID = p.ProductID
)ToDateProfit
Left Join
(
SELECT P.ProductID, p.name, sales_quantity, sales_total, cost_total, sales_total - cost_total margin
FROM
(
SELECT productid, sales_quantity, sales_total, SUM(
CASE WHEN sales_quantity >= running_quantity
THEN PurchasePrice* quantity
WHEN sales_quantity BETWEEN running_quantity - quantity AND running_quantity
THEN PurchasePrice * (sales_quantity - (running_quantity - quantity))
ELSE 0
END) cost_total
FROM
(
SELECT s.*, o.PurchasePrice, o.quantity, o.running_quantity
FROM
(
SELECT ProductID,
SUM((quantity * UnitPrice)/ExchangeRate) sales_total,
SUM(quantity) sales_quantity
FROM SaleDetail
Inner Join Sale on Sale.SaleID=SaleDetail.SaleID
where Convert(Date,Sale.SaleDate)<Convert(date,#FromDate) --(must minus saledate <FromDate (GetDate() is ToDate) )
GROUP BY ProductID
) s JOIN
(
SELECT productid, BuyingPrice/ExchangeRate as PurchasePrice, quantity, (
SELECT SUM(quantity)
FROM PurchaseDetail
inner join Purchase Pur on Pur.PurchaseID=PurchaseDetail.PurchaseID
WHERE productid = i.productid
AND Pur.Date <= PurI.date ----Fifo (if want lifo change <= to >=)
) running_quantity
FROM PurchaseDetail i
Inner join Purchase PurI on PurI.PurchaseID=i.PurchaseID
) o ON s.productid = o.productid
) q
GROUP BY productid, sales_quantity, sales_total
) q JOIN product p
ON q.ProductID = p.ProductID
)FromDateProfit on FromDateProfit.ProductID=ToDateProfit.ProductID
Inner Join ---------Only Product
(
Select Product.ProductID From Product
Inner Join SaleDetail On SaleDetail.ProductID=Product.ProductID
Inner Join Sale On Sale.SaleID=SaleDetail.SaleID
Where CONVERT(Date,SaleDate)>=CONVERT(Date,#FromDate) And CONVERT(Date,SaleDate)<=CONVERT(Date,#ToDate)
Group By Product.ProductID)SP On SP.ProductID=ToDateProfit.ProductID
"I want to ensure that I track my profit very accurately. Therefore, the 100 that I bought at $1 I want to be able to track those separately at the $1 cost and the new order at the $0.90 cost." - This is not necessary. You have to calculate current average price and calculate the profit using it. You have no serial numbers for each bottle and for that reason even if you want to know the profit for each item - you cannot do it. And from owner's viewpoint, it is important to know the total profit from Coca Cola, not that "from this bottle it is 0.50, and from that bottle it is 0.45".
I build an invoicing system, now i try to get an view with each invoice with the total price and the amount to pay.
I left joined the invoice_part table on the invoice and summed it up, now i want to also join the payments table but when i sum it sometimes takes the payment more than once. Is there a way i can accomplish the join to only take a payment once?
$stmt->from(array('x'=>'invoice'),array('x.id as id','x.ref_id as ref_id','x.start_date as start_date','x.regard as regard','x.project_code as project_code'));
$stmt->joinLeft(array('ip'=>'invoice_part'),'x.id=ip.invoice_id','SUM(ip.price*ip.amount*(100-ip.discount)/100) as price_ex');
$stmt->joinLeft(array('p'=>'payment'),'x.id=p.invoice_id','SUM(ip.price*ip.amount*(100-ip.discount)*(100+tax)/10000)-IFNULL(SUM(p.amount),0) as price_open');
//joins the payment multiple times if there are multiple invoice parts, payment should only be joined once
//note: there can be multiple payments for one invoice
$stmt->group('x.id');
result query:
SELECT `x`.`id` , `x`.`ref_id` , `x`.`start_date` , `x`.`regard` , `x`.`project_code` , `o`.`name` AS `contact` , `d`.`name` AS `department` , `c`.`name` AS `company` , `is`.`name` AS `status` , SUM( ip.price * ip.amount * ( 100 - ip.discount ) /100 ) AS `price_ex` , SUM( ip.price * ip.amount * ( 100 - ip.discount ) * ( 100 + tax ) /10000 ) - IFNULL( SUM( p.amount ) , 0 ) AS `price_open`
FROM `invoice` AS `x`
LEFT JOIN `invoice_part` AS `ip` ON x.id = ip.invoice_id
LEFT JOIN `payment` AS `p` ON x.id = p.invoice_id
GROUP BY `x`.`id`
so when i have 2 invoice parts and 1 payment for an invoice. the payment gets counted twice. how can i only let it be counted once?
You can divide the sum by the number of repetitions like this:
SUM(tbl.field_to_sum) / COUNT(tbl.primary_key) * COUNT(DISTINCT tbl.primary_key)
Also check out this question
I have a table that contains all purchased items.
I need to check which users purchased items in a specific period of time (say between 2013-03-21 to 2013-04-21) and never purchased anything after that.
I can select users that purchased items in that period of time, but I don't know how to filter those users that never purchased anything after that...
SELECT `userId`, `email` FROM my_table
WHERE `date` BETWEEN '2013-03-21' AND '2013-04-21' GROUP BY `userId`
Give this a try
SELECT
user_id
FROM
my_table
WHERE
purchase_date >= '2012-05-01' --your_start_date
GROUP BY
user_id
HAVING
max(purchase_date) <= '2012-06-01'; --your_end_date
It works by getting all the records >= start date, groups the resultset by user_id and then finds the max purchase date for every user. The max purchase date should be <=end date. Since this query does not use a join/inner query it could be faster
Test data
CREATE table user_purchases(user_id int, purchase_date date);
insert into user_purchases values (1, '2012-05-01');
insert into user_purchases values (2, '2012-05-06');
insert into user_purchases values (3, '2012-05-20');
insert into user_purchases values (4, '2012-06-01');
insert into user_purchases values (4, '2012-09-06');
insert into user_purchases values (1, '2012-09-06');
Output
| USER_ID |
-----------
| 2 |
| 3 |
SQLFIDDLE
This is probably a standard way to accomplish that:
SELECT `userId`, `email` FROM my_table mt
WHERE `date` BETWEEN '2013-03-21' AND '2013-04-21'
AND NOT EXISTS (
SELECT * FROM my_table mt2 WHERE
mt2.`userId` = mt.`userId`
and mt2.`date` > '2013-04-21'
)
GROUP BY `userId`
SELECT `userId`, `email` FROM my_table WHERE (`date` BETWEEN '2013-03-21' AND '2013-04-21') and `date` >= '2013-04-21' GROUP BY `userId`
This will select only the users who purchased during that timeframe AND purchased after that timeframe.
Hope this helps.
Try the following
SELECT `userId`, `email`
FROM my_table WHERE `date` BETWEEN '2013-03-21' AND '2013-04-21'
and user_id not in
(select user_id from my_table
where `date` < '2013-03-21' or `date` > '2013-04-21' )
GROUP BY `userId`
You'll have to do it in two stages - one query to get the list of users who did buy within the time period, then another query to take that list of users and see if they bought anything afterwards, e.g.
SELECT userID, email, count(after.*) AS purchases
FROM my_table AS after
LEFT JOIN (
SELECT DISTINCT userID
FROM my_table
WHERE `date` BETWEEN '2013-03-21' AND '2013-04-21'
) AS during ON after.userID = during.userID
WHERE after.date > '2013-04-21'
HAVING purchases = 0;
Inner query gets the list of userIDs who purchased at least one thing during that period. That list is then joined back against the same table, but filtered for purchases AFTER the period , and counts how many purchases they made and filters down to only those users with 0 "after" purchases.
probably won't work as written - haven't had my morning tea yet.
SELECT
a.userId,
a.email
FROM
my_table AS a
WHERE a.date BETWEEN '2013-03-21'
AND '2013-04-21'
AND a.userId NOT IN
(SELECT
b.userId
FROM
my_table AS b
WHERE b.date BETWEEN '2013-04-22'
AND CURDATE()
GROUP BY b.userId)
GROUP BY a.userId
This filters out anyone who has not purchased anything from the end date to the present.
I have the following query which will return the number of users in table transactions who have earned between $100 and $200
SELECT COUNT(users.id)
FROM transactions
LEFT JOIN users ON users.id = transactions.user_id
WHERE transactions.amount > 100 AND transactions.amount < 200
The above query returns the correct result below:
COUNT(users.id)
559
I would like to extend it so that the query can return data in the following format:
COUNT(users.id) : amount
1678 : 0-100
559 : 100-200
13 : 200-300
How can I do this?
You can use a CASE expression inside of your aggregate function which will get the result in columns:
SELECT
COUNT(case when amount >= 0 and amount <= 100 then users.id end) Amt0_100,
COUNT(case when amount >= 101 and amount <= 200 then users.id end) Amt101_200,
COUNT(case when amount >= 201 and amount <= 300 then users.id end) Amt201_300
FROM transactions
LEFT JOIN users
ON users.id = transactions.user_id;
See SQL Fiddle with Demo
You will notice that I altered the ranges from 0-100, 101-200, 201-300 otherwise you will have user ids being counted twice on the 100, 200 values.
If you want the values in rows, then you can use:
select count(u.id),
CASE
WHEN amount >=0 and amount <=100 THEN '0-100'
WHEN amount >=101 and amount <=200 THEN '101-200'
WHEN amount >=201 and amount <=300 THEN '101-300'
END Amount
from transactions t
left join users u
on u.id = t.user_id
group by
CASE
WHEN amount >=0 and amount <=100 THEN '0-100'
WHEN amount >=101 and amount <=200 THEN '101-200'
WHEN amount >=201 and amount <=300 THEN '101-300'
END
See SQL Fiddle with Demo
But if you have many ranges that you need to calculate the counts on, then you might want to consider creating a table with the ranges, similar to the following:
create table report_range
(
start_range int,
end_range int
);
insert into report_range values
(0, 100),
(101, 200),
(201, 300);
Then you can use this table to join to your current tables and group by the range values:
select count(u.id) Total, concat(start_range, '-', end_range) amount
from transactions t
left join users u
on u.id = t.user_id
left join report_range r
on t.amount >= r.start_range
and t.amount<= r.end_range
group by concat(start_range, '-', end_range);
See SQL Fiddle with Demo.
If you don't want to create a new table with the ranges, then you can always use a derived table to get the same result:
select count(u.id) Total, concat(start_range, '-', end_range) amount
from transactions t
left join users u
on u.id = t.user_id
left join
(
select 0 start_range, 100 end_range union all
select 101 start_range, 200 end_range union all
select 201 start_range, 300 end_range
) r
on t.amount >= r.start_range
and t.amount<= r.end_range
group by concat(start_range, '-', end_range);
See SQL Fiddle with Demo
One way to do this would be to use a case/when statement in your group by.
SELECT
-- NB this must match your group by statement exactly
-- otherwise you will get an error
CASE
WHEN amount <= 100
THEN '0-100'
WHEN amount <= 200
THEN '100-200'
ELSE '201+'
END Amount,
COUNT(*)
FROM
transactions
GROUP BY
CASE
WHEN amount <= 100
THEN '0-100'
WHEN amount <= 200
THEN '100-200'
ELSE '201+'
END
If you plan on using the grouping elsewhere, it probably makes sense to define it as a scalar function (it will also look cleaner)
e.g.
SELECT
AmountGrouping(amount),
COUNT(*)
FROM
transactions
GROUP BY
AmountGrouping(amount)
If you want to be fully generic:
SELECT
concat(((amount DIV 100) * 100),'-',(((amount DIV 100) + 1) * 100)) AmountGroup,
COUNT(*)
FROM
transactions
GROUP BY
AmountGroup
Sql Fiddle
Bilbo, I tried to be creative and found a very nice solution [ for those who love math (like me) ]
It's always surprising when MySQL integer division operator solves our problems.
DROP SCHEMA IF EXISTS `stackoverflow3`;
CREATE SCHEMA `stackoverflow3`;
USE `stackoverflow3`;
CREATE TABLE users (
id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(25) NOT NULL DEFAULT "-");
CREATE TABLE transactions(
id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
amount INT UNSIGNED DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id));
INSERT users () VALUES (),(),();
INSERT transactions (user_id,amount)
VALUES (1,120),(2,270),(3, 350),
(2,500), (1,599), (1,550), (3,10),
(3,20), (3,30), (3,50), (3,750);
SELECT
COUNT(t.id),
CONCAT(
((t.amount DIV 100)*100)," to ",((t.amount DIV 100 + 1)*100-1)
) AS amount_range
FROM transactions AS t
GROUP BY amount_range;
Awaiting your questions, Mr. Baggins.