SQL How to select customers with highest transaction amount by state - sql

I am trying to write a SQL query that returns the name and purchase amount of the five customers in each state who have spent the most money.
Table schemas
customers
|_state
|_customer_id
|_customer_name
transactions
|_customer_id
|_transact_amt
Attempts look something like this
SELECT state, Sum(transact_amt) AS HighestSum
FROM (
SELECT name, transactions.transact_amt, SUM(transactions.transact_amt) AS HighestSum
FROM customers
INNER JOIN customers ON transactions.customer_id = customers.customer_id
GROUP BY state
) Q
GROUP BY transact_amt
ORDER BY HighestSum
I'm lost. Thank you.
Expected results are the names of customers with the top 5 highest transactions in each state.
ERROR: table name "customers" specified more than once
SQL state: 42712

First, you need for your JOIN to be correct. Second, you want to use window functions:
SELECT ct.*
FROM (SELECT c.customer_id, c.name, c.state, SUM(t.transact_amt) AS total,
ROW_NUMBER() OVER (PARTITION BY c.state ORDER BY SUM(t.transact_amt) DESC) as seqnum
FROM customers c JOIN
transaactions t
ON t.customer_id = c.customer_id
GROUP BY c.customer_id, c.name, c.state
) ct
WHERE seqnum <= 5;
You seem to have several issues with SQL. I would start with understanding aggregation functions. You have a SUM() with the alias HighestSum. It is simply the total per customer.

You can get them using aggregation and then by using the RANK() window function. For example:
select
state,
rk,
customer_name
from (
select
*,
rank() over(partition by state order by total desc) as rk
from (
select
c.customer_id,
c.customer_name,
c.state,
sum(t.transact_amt) as total
from customers c
join transactions t on t.customer_id = c.customer_id
group by c.customer_id
) x
) y
where rk <= 5
order by state, rk

There are two valid answers already. Here's a third:
SELECT *
FROM (
SELECT c.state, c.customer_name, t.*
, row_number() OVER (PARTITION BY c.state ORDER BY t.transact_sum DESC NULLS LAST, customer_id) AS rn
FROM (
SELECT customer_id, sum(transact_amt) AS transact_sum
FROM transactions
GROUP BY customer_id
) t
JOIN customers c USING (customer_id)
) sub
WHERE rn < 6
ORDER BY state, rn;
Major points
When aggregating all or most rows of a big table, it's typically substantially faster to aggregate before the join. Assuming referential integrity (FK constraints), we won't be aggregating rows that would be filtered otherwise. This might change from nice-to-have to a pure necessity when joining to more aggregated tables. Related:
Why does the following join increase the query time significantly?
Two SQL LEFT JOINS produce incorrect result
Add additional ORDER BY item(s) in the window function to define which rows to pick from ties. In my example, it's simply customer_id. If you have no tiebreaker, results are arbitrary in case of a tie, which may be OK. But every other execution might return different results, which typically is a problem. Or you include all ties in the result. Then we are back to rank() instead of row_number(). See:
PostgreSQL equivalent for TOP n WITH TIES: LIMIT "with ties"?
While transact_amt can be NULL (has not been ruled out) any sum may end up to be NULL as well. With an an unsuspecting ORDER BY t.transact_sum DESC those customers come out on top as NULL comes first in descending order. Use DESC NULLS LAST to avoid this pitfall. (Or define the column transact_amt as NOT NULL.)
PostgreSQL sort by datetime asc, null first?

Related

SQL: Take 1 value per grouping

I have a very simplified table / view like below to illustrate the issue:
The stock column represents the current stock quantity of the style at the retailer. The reason the stock column is included is to avoid joins for reporting. (the table is created for reporting only)
I want to query the table to get what is currently in stock, grouped by stylenumber (across retailers). Like:
select stylenumber,sum(sold) as sold,Max(stock) as stockcount
from MGTest
I Expect to get Stylenumber, Total Sold, Most Recent Stock Total:
A, 6, 15
B, 1, 6
But using ...Max(Stock) I get 10, and with (Sum) I get 25....
I have tried with over(partition.....) also without any luck...
How do I solve this?
I would answer this using window functions:
SELECT Stylenumber, Date, TotalStock
FROM (SELECT M.Stylenumber, M.Date, SUM(M.Stock) as TotalStock,
ROW_NUMBER() OVER (PARTITION BY M.Stylenumber ORDER BY M.Date DESC) as seqnum
FROM MGTest M
GROUP BY M.Stylenumber, M.Date
) m
WHERE seqnum = 1;
The query is a bit tricky since you want a cumulative total of the Sold column, but only the total of the Stock column for the most recent date. I didn't actually try running this, but something like the query below should work. However, because of the shape of your schema this isn't the most performant query in the world since it is scanning your table multiple times to join all of the data together:
SELECT MDate.Stylenumber, MDate.TotalSold, MStock.TotalStock
FROM (SELECT M.Stylenumber, MAX(M.Date) MostRecentDate, SUM(M.Sold) TotalSold
FROM [MGTest] M
GROUP BY M.Stylenumber) MDate
INNER JOIN (SELECT M.Stylenumber, M.Date, SUM(M.Stock) TotalStock
FROM [MGTest] M
GROUP BY M.Stylenumber, M.Date) MStock ON MDate.Stylenumber = MStock.Stylenumber AND MDate.MostRecentDate = MStock.Date
You can do something like this
SELECT B.Stylenumber,SUM(B.Sold),SUM(B.Stock) FROM
(SELECT Stylenumber AS 'Stylenumber',SUM(Sold) AS 'Sold',MAX(Stock) AS 'Stock'
FROM MGTest A
GROUP BY RetailerId,Stylenumber) B
GROUP BY B.Stylenumber
if you don't want to use joins
My solution, like that of Gordon Linoff, will use the window functions. But in my case, everything will turn around the RANK window function.
SELECT stylenumber, sold, SUM(stock) totalstock
FROM (
SELECT
stylenumber,
SUM(sold) OVER(PARTITION BY stylenumber) sold,
RANK() OVER(PARTITION BY stylenumber ORDER BY [Date] DESC) r,
stock
FROM MGTest
) T
WHERE r = 1
GROUP BY stylenumber, sold

How do I proceed on this query

I want to know if there's a way to display more than one column on an aggregate result but without it affecting the group by.
I need to display the name alongside an aggregate result, but I have no idea what I am missing here.
This is the data I'm working with:
It is the result of the following query:
select * from Salesman, Sale,Buyer
where Salesman.ID = Buyer.Salesman_ID and Buyer.ID = sale.Buyer_ID
I need to find the salesman that sold the most stuff (total price) for a specific year.
This is what I have so far:
select DATEPART(year,sale.sale_date)'year', Salesman.First_Name,sum(sale.price)
from Salesman, Sale,Buyer
where Salesman.ID = Buyer.Salesman_ID and Buyer.ID = sale.Buyer_ID
group by DATEPART(year,sale.sale_date),Salesman.First_Name
This returns me the total sales made by each salesman.
How do I continue from here to get the top salesman of each year?
Maybe the query I am doing is completely wrong and there is a better way?
Any advice would be helpful.
Thanks.
This should work for you:
select *
from(
select DATEPART(year,s.sale_date) as SalesYear -- Avoid reserved words for object names
,sm.First_Name
,sum(s.price) as TotalSales
,row_number() over (partition by DATEPART(year,s.sale_date) -- Rank the data within the same year as this data row.
order by sum(s.price) desc -- Order by the sum total of sales price, with the largest first (Descending). This means that rank 1 is the highest amount.
) as SalesRank -- Orders your salesmen by the total sales within each year, with 1 as the best.
from Buyer b
inner join Sale s
on(b.ID = s.Buyer_ID)
inner join Salesman sm
on(sm.ID = b.Salesman_ID)
group by DATEPART(year,s.sale_date)
,sm.First_Name
) a
where SalesRank = 1 -- This means you only get the top salesman for each year.
First, never use commas in the FROM clause. Always use explicit JOIN syntax.
The answer to your question is to use window functions. If there is a tie and you wand all values, then RANK() or DENSE_RANK(). If you always want exactly one -- even if there are ties -- then ROW_NUMBER().
select ss.*
from (select year(s.sale_date) as yyyy, sm.First_Name, sum(s.price) as total_price,
row_number() over (partition by year(s.sale_date)
order by sum(s.price) desc
) as seqnum
from Salesman sm join
Sale s
on sm.ID = s.Salesman_ID
group by year(s.sale_date), sm.First_Name
) ss
where seqnum = 1;
Note that the Buyers table is unnecessary for this query.

Top 10% of sum() Postgres

I'm looking to pull the top 10% of a summed value on a Postgres sever.
So i'm summing a value with sum(transaction.value) and i'd like the top 10% of the value
From what I gather in your comments, I assume you want to:
Sum transactions per customer to get a total per customer.
List the top 10 % of customers who actually have transactions and spent the most.
WITH cte AS (
SELECT t.customer_id, sum(t.value) AS sum_value
FROM transaction t
GROUP BY 1
)
SELECT *, rank() OVER (ORDER BY sum_value DESC) AS sails_rank
FROM cte
ORDER BY sum_value DESC
LIMIT (SELECT count(*)/10 FROM cte)
Major points
Best to use a CTE here, makes the count cheaper.
The JOIN between customer and transaction automatically excludes customers without transaction. I am assuming relational integrity here (fk constraint on customer_id).
Dividing bigint / int effectively truncates the result (round down to the nearest integer). You may be interested in this related question:
PostgreSQL equivalent for TOP n WITH TIES: LIMIT "with ties"?
I added a sails_rank column which you didn't ask for, but seems to fit your requirement.
As you can see, I didn't even include the table customer in the query. Assuming you have a foreign key constraint on customer_id, that would be redundant (and slower). If you want additional columns from customer in the result, join customer to the result of above query:
WITH cte AS (
SELECT t.customer_id, sum(t.value) AS sum_value
FROM transaction t
GROUP BY 1
)
SELECT c.customer_id, c.name, sub.sum_value, sub.sails_rank
FROM (
SELECT *, rank() OVER (ORDER BY sum_value DESC) AS sails_rank
FROM cte
ORDER BY sum_value DESC
LIMIT (SELECT count(*)/10 FROM cte)
) sub
JOIN customer c USING (customer_id);

get row with max from group by results

I have sql such as:
select
c.customerID, sum(o.orderCost)
from customer c, order o
where c.customerID=o.customerID
group by c.customerID;
This returns a list of
customerID, orderCost
where orderCost is the total cost of all orders the customer has made. I want to select the customer who has paid us the most (who has the highest orderCost). Do I need to create a nested query for this?
You need a nested query, but you don't have to access the tables twice if you use analytic functions.
select customerID, sumOrderCost from
(
select customerID, sumOrderCost,
rank() over (order by sumOrderCost desc) as rn
from (
select c.customerID, sum(o.orderCost) as sumOrderCost
from customer c, orders o
where c.customerID=o.customerID
group by c.customerID
)
)
where rn = 1;
The rank() function ranks the results from your original query by the sum() value, then you only pick those with the highest rank - that is, the row(s) with the highest total order cost.
If more than one customer has the same total order cost, this will return both. If that isn't what you want you'll have to decide how to determine which single result to use. If you want the lowest customer ID, for example, add that to the ranking function:
select customerID, sumOrderCost,
rank() over (order by sumOrderCost desc, customerID) as rn
You can adjust you original query to return other data instead, just for the ordering, and not include it in the outer select.
You need to create nested query for this.
Two queries.

Group related records, but pick certain fields from only the first record

I'm preforming an aggregate function on multiple records, which are grouped by a common ID. The problem is, I also want to export some other fields, which might be different within the grouped records, but I want to get those certain fields from one of the records (the first one, according to the query's ORDER BY).
Starting point example:
SELECT
customer_id,
sum(order_total),
referral_code
FROM order
GROUP BY customer_id
ORDER BY date_created
I need to query the referral code, but doing it outside of an aggregate function means I have to group by that field as well, and that's not what I want - I need exactly one row per customer in this example. I really only care about the referral code from the first order, and I'm happy to throw out any later referral codes.
This is in PostgreSQL, but maybe syntax from other DBs could be similar enough to work.
Rejected solutions:
Can't use max() or min() because order is significant.
A subquery might work at first, but does not scale; this is an extremely reduced example. My actual query has dozens of fields like referral_code which I only want the first instance of, and dozens of WHERE clauses which, if duplicated in a subquery, would make for a maintenance nightmare.
Well, it's actually pretty simple.
First, let's write a query that will do the aggregation:
select customer_id, sum(order_total)
from order
group by customer_id
now, let's write a query that would return 1st referral_code and date_created for given customer_id:
select distinct on (customer_id) customer_id, date_created, referral_code
from order
order by customer_id, date_created
Now, you can simply join the 2 selects:
select
x1.customer_id,
x1.sum,
x2.date_created,
x2.referral_code
from
(
select customer_id, sum(order_total)
from order
group by customer_id
) as x1
join
(
select distinct on (customer_id) customer_id, date_Created, referral_code
from order
order by customer_id, date_created
) as x2 using ( customer_id )
order by x2.date_created
I didn't test it, so there could be typos in it, but generally it should work.
You will need window functions.
It's kind of GROUP BY, but you can still access the individual rows.
Only used the Oracle equivalent though.
If the date_created is guaranteed to be unique per customer_id, then you can do this:
[simple table]
create table ordertable (customer_id int, order_total int, referral_code char, date_created datetime)
insert ordertable values (1,10, 'a', '2009-01-01')
insert ordertable values (2,15, 'b', '2009-01-02')
insert ordertable values (1,35, 'c', '2009-01-03')
[replace my lame table names with something better :)]
SELECT
orderAgg.customer_id,
orderAgg.order_sum,
referral.referral_code as first_referral_code
FROM (
SELECT
customer_id,
sum(order_total) as order_sum
FROM ordertable
GROUP BY customer_id
) as orderAgg join (
SELECT
customer_id,
min(date_created) as first_date
FROM ordertable
GROUP BY customer_id
) as dateAgg on orderAgg.customer_id = dateAgg.customer_id
join ordertable as referral
on dateAgg.customer_id = referral.customer_id
and dateAgg.first_date = referral.date_created
Perhaps something like:
SELECT
O1.customer_id,
O1.referral_code,
SQ.total
FROM
Orders O1
LEFT OUTER JOIN Orders O2 ON
O2.customer_id = O1.customer_id AND
O2.date_created < O1.date_created
INNER JOIN (
SELECT
customer_id,
SUM(order_total) AS total
FROM
Orders
GROUP BY
customer_id
) SQ ON SQ.customer_id = O1.customer_id
WHERE
O2.customer_id IS NULL
Would something like this do the trick?
SELECT
customer_id,
sum(order_total),
(SELECT referral_code
FROM order o
WHERE o.customer_id = order.customer_id
ORDER BY date_created
LIMIT 1) AS customers_referral_code
FROM order
GROUP BY customer_id, customers_referral_code
ORDER BY date_created
This doesn't require you to maintain the WHERE clause in two places and maintains the order significance, but would get pretty hairy if you needed "dozens of fields" like referral_code. It's also fairly slow (at least on MySQL).
It sounds to me like referral_code and the dozens of fields like it should be in the customer table, not the order table, since they're logically associated 1:1 with the customer, not the order. Moving them there would make the query MUCH simpler.
This might also do the trick:
SELECT
o.customer_id,
sum(o.order_total),
c.referral_code, c.x, c.y, c.z
FROM order o LEFT JOIN (
SELECT referral_code, x, y, z
FROM orders c
WHERE c.customer_id = o.customer_id
ORDER BY c.date_created
LIMIT 1
) AS c
GROUP BY o.customer_id, c.referral_code
ORDER BY o.date_created
SELECT customer_id, order_sum,
(first_record).referral, (first_record).other_column
FROM (
SELECT customer_id,
SUM(order_total) AS order_sum,
(
SELECT oi
FROM order oi
WHERE oi.customer_id = o.customer_id
LIMIT 1
) AS first_record
FROM order o
GROUP BY
customer_id
) q