SQL Group By Number Of Users Within Range - mysql

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.

Related

SQL query for cumulative sum or subtract

how to create columns from unique values of a column and do a cumulative addition or subtraction.
I have a record of transactions like this
transaction_id
created_at
transaction_type
amount
124
2020-08-06 17:00:09
2
25.00
123
2020-08-06 17:00:03
1
50.00
There are various types of transactions, which in turn have different effects .Some results are withdrawals from th account, some are deposits and others don’t affect it at all. This information is summarized in another table (let’s call it ‘transaction_types’), as shown below:
id
description
effect
1
Manual Deposit
add
2
Direct Payment
subtract
id in the table transaction_type is a foreign key transactions. transaction_type = transactions_type.id
now if the initial amount is $10000 I need to create a table like this
transaction_id
initial_balance
deposit
withdrawal
final_balance
123
100000
50
100050
124
100050
25
100025
I don't know how to create new columns based on unique values in some columns also to start a cumulative sum from 10000. This is the query I tried select f.transaction_id, t.created_at, sum(case when d.effect = 'subtract' then -1 else 1 end * amount) from f inner join d on f.transaction_type = d.id
WITH RECURSIVE RowNums AS
(
SELECT
transaction_id,
created_at,
CAST(amount AS SIGNED) AS amount,
effect,
ROW_NUMBER() OVER
(
ORDER BY
created_at,
transaction_id
) AS RowNum
FROM transactions t
JOIN transaction_types tt
ON t.transaction_type = tt.id
),
Balances AS
(
SELECT
transaction_id,
created_at,
amount,
effect,
100000 AS initial_balance,
100000 +
(
CASE effect
WHEN 'add' THEN amount
WHEN 'subtract' THEN (-(amount))
END
) AS final_balance,
RowNum
FROM RowNums
WHERE RowNum = 1
UNION ALL
SELECT
rn.transaction_id,
rn.created_at,
rn.amount,
rn.effect,
b.final_balance,
b.final_balance +
(
CASE rn.effect
WHEN 'add' THEN rn.amount
WHEN 'subtract' THEN (-(rn.amount))
END
),
rn.RowNum
FROM Balances b
JOIN RowNums rn
ON (b.RowNum + 1) = rn.RowNum
)
SELECT
transaction_id,
initial_balance,
CASE effect
WHEN 'add'
THEN CAST(amount AS CHAR(20))
ELSE ''
END AS deposit,
CASE effect
WHEN 'subtract'
THEN CAST(amount AS CHAR(20))
ELSE ''
END AS withdrawal,
final_balance
FROM Balances
ORDER BY
created_at,
transaction_id
source : https://www.reddit.com/r/sqltutorial/comments/sx3ycb/comment/hxr6f9l/?utm_source=share&utm_medium=web2x&context=3

Get Data According to Group by date field

Here is my table
Which have field type which means 1 is for income and 2 is for expense
Now requirement is for example in table there is two transaction made on 2-10-2018 so i want data as following
Expected Output
id created_date total_amount
1 1-10-18 10
2 2-10-18 20(It calculates all only income transaction made on 2nd date)
3 3-10-18 10
and so on...
it will return an new field which contains only incom transaction made on perticulur day
What i had try is
SELECT * FROM `transaction`WHERE type = 1 ORDER BY created_date ASC
UNION
SELECT()
//But it wont work
SELECT created_date,amount,status FROM
(
SELECT COUNT(amount) AS totalTrans FROM transaction WHERE created_date = created_date
) x
transaction
You can Also See Schema HERE http://sqlfiddle.com/#!9/6983b9
You can Count() the total number of expense transactions using conditional function If(), on a group of created_date.
Similarly, you can Sum() the amount of expense done using If(), on a created_date.
Try the following:
SELECT
`created_date`,
SUM(IF (`type` = 2, `amount`, 0)) AS total_expense_amount,
COUNT(IF (`type` = 2, `id`, NULL)) AS expense_count
FROM
`transaction`
GROUP BY `created_date`
ORDER BY `created_date` ASC
Do you just want a WHERE clause?
SELECT t.created_date, SUM(amount) as total_amount
FROM transaction t
WHERE type = 2
GROUP BY t.created_date
ORDER BY created_date ASC ;

Showing taxes per invoice items from database

I have problem with getting tax values from database. I will simplify it as possible i can.
First table is
Invoices
(
`Id`,
`Date`,
`InvoiceNumber`,
`Total`
)
Second table is
`InvoiceItems`
(
`Id`,
`Total`,
`TotalWithoutTax`,
`TotalTax`,
`InvoiceId`
)
InvoiceId is a foreign key for Id column from previous table Invoices
Third table is
`InvoiceItemTaxes`
(
`Id`,
`TaxAmmount`,
`InvoiceItemId`,
`TaxId`
)
and fourth table
`Taxes`
(
`Id`,
`Value`
)
This last table contains three taxes, let's say 3, 10 and 15 percent.
I am trying to get something like this - table with columns InvoiceNumber, Total without taxes, Tax1, Tax2, Tax3 and Total with taxes.
I tried a lot of different approaches and i simply cannot get tax amount for every invoice. End result would be table where i can see every invoice with specified amounts of every tax (sum of each tax amount for every invoice item).
If I'm understanding correctly, you can use conditional aggregation with sum and case to get the breakdown by tax group:
select i.id, i.invoicenumber, i.total as pretaxtotal,
sum(case when t.value = 3 then iit.TaxAmmount end) taxes_3,
sum(case when t.value = 10 then iit.TaxAmmount end) taxes_10,
sum(case when t.value = 15 then iit.TaxAmmount end) taxes_15,
sum(ii.Total) as overalltotal
from invoices i
join InvoiceItems ii on i.id = ii.invoiceid
join InvoiceItemTaxes iit on ii.id = iit.InvoiceItemId
join Taxes t on t.id = iit.taxid
group by i.id, i.invoicenumber, i.total
Some of the fields may be a little off -- the sample data was not complete.

MySQL nested select: am I able to use any data from outer select?

Let's imagine I have the following table users:
id name
1 John
2 Mike
3 Max
And table posts
id author_id date title
1 1 2014-12-12 Post 2
2 1 2014-12-10 Post 1
3 2 2014-10-01 Lorem ipsum
...and so on
And I'd like to have a query containing the following data:
user name
number of user's posts within last week
number of user's posts within last month
I can do it just for each individual user (with id 1 in the following example):
SELECT
`name`,
(SELECT COUNT(*) FROM `posts`
WHERE
`author` = 1 AND
UNIX_TIMESTAMP()-UNIX_TIMESTAMP(`date`) < 7*24*3600) AS `posts7`,
(SELECT COUNT(*) FROM `posts`
WHERE
`author` = 1 AND
UNIX_TIMESTAMP()-UNIX_TIMESTAMP(`date`) < 30*24*3600) AS `posts30`
FROM `users`
WHERE `id` = 1
I suspect that MySQL will allow do this for all users within one query if I could exchange the data between inner and outer SELECT's. I probably using wrong words, but I really hope that people here will understand my needs and I will have some help.
This is NOT the final SQL but gives you the jist...
Select name, sum(case when datewithin7 then 1 else 0 end) as posts7,
sum(case when datewithin30 then 1 else 0 end) as posts30
from name
left join posts on name.id = posts.nameid
GROUP BY name.
Note you need the group by. but I don't have the time to put the case statement together...
Try something like this
SELECT
`name`,
sum(IF(`date` between DATE(NOW()-INTERVAL 7 DAY) and now() , 1, 0) as posts7,
sum(IF(`date` between DATE(NOW()-INTERVAL 30 DAY) and now() , 1, 0) as posts30
FROM
`users` as u, posts as p
WHERE
u.id = p.author_id
GROUP BY
1
Certainly aggregating a non-nested query is the way to solve the problem although both Benni and xQbert have written unbounded queries - which, while satisfying the objective, are very innefficient. Consider (adapted from Benni's answer):
SELECT `name`
, SUM(IF(
`date` between DATE(NOW()-INTERVAL 7 DAY) and now()
, 1
, 0) as posts7
, SUM(IF(
p.author_id IS NULL
, 0
, 1) as posts30
FROM `users` as u
LEFT JOIN posts as p
ON u.id = p.author_id
AND p.date > NOW()-INTERVAL 30 DAY
GROUP BY name
Note that NOT using the conversion to a UNIX timestamp allows the database to use an index (if available) to resolve the query.
However there are scenarios where it is more effective / appropriate to use a nested query. So although it's not the best solution to this problem:
SELECT
`name`
, (SELECT COUNT(*)
FROM `posts` AS p7
WHERE p7.author = users.id
AND p7.`date` > NOW() - INTERVAL 7 DAY) AS `posts7`
, (SELECT COUNT(*)
FROM `posts` AS p30
WHERE p30.author = users.id
AND p30.`date` > NOW() - INTERVAL 30 DAY) AS `posts30`
FROM `users`
WHERE `id` = 1

How to get users that purchased items ONLY in a specific time period (MySQL Database)

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.