SQL Query SUM issue - sql

I am writing a SQL query where i want to list down and calculate some data every thing looks to work fine but the only problem I am having is when i SUM values in a given column the final result is a overall total but i want this total only to be for a month of October.
The scenario is as followed i need to calculate a total amount of deposits made by costomers over the month of October with other information that works well its only the sum issue.
SQL:
SELECT customers.id AS 'Customer ID',
customers.firstname AS
'Customer First Name',
customers.lastname AS 'Customer Last Name'
,
users.firstname AS
'Employee First Name',
users.lastname AS 'Employee Last Name'
,
positions.currency AS 'Currency',
Sum(customer_deposits.amount) AS 'Total Deposits',
Sum(positions.status) AS 'Total Bets',
Sum(customer_total_statistics.totalbonusdeposits) AS 'Total Bonuses',
positions.date AS 'Date'
FROM customers
LEFT JOIN customer_deposits
ON customers.id = customer_deposits.customerid
LEFT JOIN users
ON customers.employeeinchargeid = users.id
LEFT JOIN positions
ON customers.id = positions.customerid
LEFT JOIN customer_total_statistics
ON customers.id = customer_total_statistics.customerid
WHERE customer_deposits.status = 'approved'
AND positions.status = 'won'
AND positions.date BETWEEN '2013-10-01' AND '2013-11-01'
AND customers.isdemo = '0'
GROUP BY customers.id
ORDER BY customers.id

Well, you don't filter your customer_deposits at all, so the sum over customer_deposits.amount will of course give you the sum of all the deposits. You'll have to add another where to filter customer_deposits by date as well, not just positions.

It looks like you have multiple 1-n relationships so your joins are creating cartesian products. So if you had a customer with 2 deposits and 2 positions:
Positions customer_deposits
CustomerID | Amount CustomerID | Amount
1 | 10 1 | 30
1 | 20 1 | 40
When you do this join:
SELECT c.ID, cd.Amount AS DepositAmount, p.Amount AS PositionAmount
FROM Customers c
INNER JOIN customer_deposits cd
ON c.ID = cd.CustomerID
INNER JOIN Positions p
ON c.ID = p.CustomerID
You end up with 4 rows with all combinations of deposit amount and position amount:
ID DepositAmount PositionAmount
1 10 30
1 10 40
1 20 30
1 20 40
So if you sum this up you end up with incorrect sums because rows are duplicated. You need to move your SUMs to subqueries:
SELECT customers.id AS 'Customer ID',
customers.firstname AS 'Customer First Name',
customers.lastname AS 'Customer Last Name',
users.firstname AS 'Employee First Name',
users.lastname AS 'Employee Last Name',
positions.currency AS 'Currency',
cd.amount AS 'Total Deposits',
P.status AS 'Total Bets',
cs.totalbonusdeposits AS 'Total Bonuses',
positions.date AS 'Date'
FROM customers
INNER JOIN
( SELECT CustomerID, SUM(Amount) AS Amount
FROM customer_deposits
WHERE customer_deposits.status = 'approved'
GROUP BY CustomerID
) cd
ON customers.id = cd.customerid
LEFT JOIN users
ON customers.employeeinchargeid = users.id
INNER JOIN
( SELECT CustomerID, Date, SUM(Status) AS Status
FROM positions
WHERE positions.status = 'won'
AND positions.date BETWEEN '2013-10-01' AND '2013-11-01'
GROUP BY CustomerID, Date
) P
ON customers.id = positions.customerid
LEFT JOIN
( SELECT CustomerID, SUM(totalbonusdeposits) AS totalbonusdeposits
FROM customer_total_statistics
GROUP BY CustomerID
) cs
ON customers.id = customer_total_statistics.customerid
WHERE customers.isdemo = '0';
Note, you have used fields in the where clause for left joined tables which effectively turns them into an inner join, e.g.
positions.status = 'won'
If there is no match in positions, the status will be NULL, and NULL = 'won' evaluates to NULL (not true) so it will exclude all rows where there is no match in position. As such I have changed your LEFT JOIN's to INNER.

Related

How to GROUP BY total amount for all items in order in one row

I need to calculate total amount for all items in the order, I used STRING_AGG() but the output divided the amounts in multiple rows.
This is the SELECT statement :
SELECT b.order_id as 'Order Id',
string_agg(e.testname,CHAR(13)) as 'Test',
string_agg(d.PRICE,CHAR(13)) as 'Price',
string_agg(d.test_vat,CHAR(13)) as '15% Vat',
sum(convert(float,d.TOTAL_AMOUNT)) as 'Total'
FROM patients a , lab_orders b ,customers c , order_details d , labtests e
where a.patient_no = b.patient_no
and b.custid = c.custid
and b.order_id = d.order_id
and d.testid = e.testid
and b.ORDER_ID=2000000272
group by b.order_id , d.TOTAL_AMOUNT
The output :
Order Id Test Price 15% Vat Total
2000000272 (TSH) Free T3 (FT3) Free T4 (FT4) 90 90 90 13.5 13.5 13.5 310.5
2000000272 SGPT(ALT) SGOT (AST) 40 40 6 6 92
2000000272 Patient Time (PT) 60 9 69
Why the total divided to 3 rows because of GROUP BY how to fix this and show the total for all items 310.5+92+69 = 471.5 in one row only.
Just remove the total amount from the group by clause:
group by b.order_id --, d.total_amount
This guarantees just one row per order_id, while, on the other hand, having total_amount in the group by clause breaks into a new row for every different value of tuple (order_id, total_amount).
Side notes:
use standard joins! old-school, implicit joins are legacy syntax, that should not be used in new code
don't use single quotes for identifiers; use square brackets, or better yet, identifiers that do not require quoting
don't store numbers as strings
So:
select lo.order_id
string_agg(lt.testname, char(13)) as Test,
string_agg(od.price, char(13)) as Price,
string_agg(od.test_vat, char(13)) as [15% Vat],
sum(convert(float, od.total_amount)) as Total
from patients p,
inner join lab_orders lo on p.patient_no = lo.patient_no
inner join locustomers c on lo.custid = c.custid
inner join order_details od on lo.order_id = od.order_id
inner join labtests lt on od.testid = lt.testid
where lo.order_id = 2000000272
group by lo.order_id

Multiple COUNT / GROUP BY statements return unexpected results

Querying a postgreSQL 9.4 db, I want to be able to see how often each customer is interacting with each employee based on previous orders. My aim is to retrieve data in the following format:
CUSTOMER EMPLOYEE INTERACTIONS CUSTOMER_TOTAL
Customer1 EmployeeA 30 50
Customer1 EmployeeB 20 50
Customer2 EmployeeD 6 15
Customer2 EmployeeA 6 15
Customer2 EmployeeC 3 15
...where I have a separate record in the results for every combination of customer and employee (assuming at least one order has taken place between the two).
I want to include a column containing the number of orders between a customer and each individual employee (See column 3 above), and another column with the total number of orders for each customer overall (see column 4 above).
I've written the following query:
SELECT customer.name as Customer, employee.name as Employee,
SUM(CASE WHEN orders.employee_id = employee.id AND orders.customer_id = customer.id THEN 1 ELSE 0 END) AS Interactions,
SUM(CASE WHEN orders.customer_id = customer.id THEN 1 ELSE 0 END) AS Customer_Total
FROM tblcustomer customer
JOIN tblorder orders ON orders.customer_id = customer.id
LEFT JOIN tblemployee employee ON employee.id = orders.employee_id
GROUP BY customer.name, employee.name
ORDER BY Customer, Interactions DESC;
Which returned the following results:
CUSTOMER EMPLOYEE INTERACTIONS CUSTOMER_TOTAL
Customer1 EmployeeA 30 30
Customer1 EmployeeB 20 20
Customer2 EmployeeD 6 6
Customer2 EmployeeA 6 6
Customer2 EmployeeC 3 3
All rows / columns appear as expected, except for the final column. Instead of a count of total orders for each customer, it has returned only the orders where the employee is also a match. Where have I gone wrong?
I think you should be using LEFT JOIN here:
SELECT customer.name as Customer,
employee.name as Employee,
SUM(CASE WHEN orders.employee_id = employee.id AND
orders.customer_id = customer.id
THEN 1 ELSE 0 END) AS Interactions,
SUM(CASE WHEN orders.customer_id = customer.id
THEN 1 ELSE 0 END) AS Customer_Total
FROM tblcustomer customer
LEFT JOIN tblorder orders
ON orders.customer_id = customer.id
LEFT JOIN tblemployee employee
ON employee.id = orders.employee_id
GROUP BY customer.name,
employee.name
ORDER BY Customer,
Interactions DESC;
My hunch is that your INNER JOINs are filtering out records which would comprise the customer total. By using LEFT JOIN you are retaining these records.
One left join with tblEmployee would do the task. Inner join with tblEmployee will filter and get only those records which are number of orders between a customer and each individual employee.
SELECT customer.name as Customer,
employee.name as Employee,
SUM(CASE WHEN orders.employee_id = employee.id AND
orders.customer_id = customer.id
THEN 1 ELSE 0 END) AS Interactions,
SUM(CASE WHEN orders.customer_id = customer.id
THEN 1 ELSE 0 END) AS Customer_Total
FROM tblcustomer customer
JOIN tblorder orders
ON orders.customer_id = customer.id
LEFT JOIN tblemployee employee
ON employee.id = orders.employee_id
GROUP BY customer.name,
employee.name
ORDER BY Customer,
Interactions DESC;

select based on calculated value, optimization

i need to select among other fields the age of a customer at the time he/she bought some product of a specific brand etc, WHERE the customer was for example between 30 and 50 years old.i wrote this query (getAge just uses DATEDIFF to return the age in years)
SELECT DISTINCT customers.FirstName, customers.LastName,
products.ProductName,
dbo.getAge(customers.BirthDate,sales.Datekey)
AS Age_when_buying
FROM sales
INNER JOIN dates ON sales.Datekey=dates.Datekey
INNER JOIN customers ON sales.CustomerKey=customers.CustomerKey
INNER JOIN products ON sales.ProductKey=products.ProductKey
INNER JOIN stores ON sales.StoreKey=stores.StoreKey
WHERE stores.StoreName = 'DribleCom Europe Online Store' AND
products.BrandName = 'Proseware' AND
dbo.getAge(customers.BirthDate, sales.Datekey) >= 30 AND
dbo.getAge(customers.BirthDate, sales.Datekey) <=50
and it works but i calculate the age three times.I tried to assign age_when_buying to a variable but it didn't work.My next thought was to use cursor but i feel that there is a more simple way i am missing.The question is: which is the appropriate way to solve this or what are my options?
Assuming that you only have a limited number of filters you'd like to apply, you could use a Common Table Expression to restructure your query.
I personally find it easier to see all the joins and such in one place, while the filters are similarly grouped together at the bottom...
WITH CTE AS(
select customers.FirstName
, customers.LastName
, dbo.getAge(customers.BirthDate,sales.Datekey) AS Age_when_buying
, sales.StoreName
, products.BrandName
, products.ProductName
from sales
INNER JOIN customers on sales.CustomerKey=customers.CustomerKey
INNER JOIN products ON sales.ProductKey = products.ProductKey
INNER JOIN stores ON sales.StoreKey = stores.StoreKey
)
SELECT DISTINCT FirstName, LastName, ProductName, Age_when_buying
FROM CTE
WHERE StoreName = 'DribleCom Europe Online Store'
AND BrandName = 'Proseware'
AND Age_when_buying BETWEEN 30 AND 50
You should use Cross Apply.
SELECT DISTINCT customers.FirstName, customers.LastName,
products.ProductName,
age.age AS Age_when_buying
FROM sales
INNER JOIN dates ON sales.Datekey=dates.Datekey
INNER JOIN customers ON sales.CustomerKey=customers.CustomerKey
INNER JOIN products ON sales.ProductKey=products.ProductKey
INNER JOIN stores ON sales.StoreKey=stores.StoreKey
CROSS APPLY
(select dbo.getAge(customers.BirthDate, sales.Datekey) as age) age
WHERE stores.StoreName = 'DribleCom Europe Online Store' AND
products.BrandName = 'Proseware' AND
age.age >= 30 AND
age.age <=50
You could use a WITH clause :
WITH Customers_Info (CustomerFirstName, CustomerLastName, CustomerKey, AgeWhenBuying)
AS
(
SELECT customers.FirtName,
customers.LastName,
CustomerKey
dbo.getAge(customers.BirthDate, sales.DateKey) As AgeWhenBuying
FROM customers
JOIN sale USING(CustomerKey)
)
SELECT FirstName,
LastName,
products.ProductName,
Customers_Info.AgeWhenBuying
FROM Customers_Info
JOIN sale USING(CustomerKey)
JOIN products USING(ProductKey)
JOIN stores USING(StoreKey)
WHERE stores.StoreName = 'DribleCom Europe Online Store'
AND products.BrandName = 'Proseware'
AND Customers_Info.AgeWhenBuying >= 30
AND Customers_Info.AgeWhenBuying <= 50;

How do I run this SQL command?

Details of customers needed are here
Core -- Division_ID = 1
Valid Email -- email LIKE '%#%'
Opt-In: Yes -- Consent_Status_ID = 4
Consent Type Description: Consent to send marketing email -- Consent_Type_ID = 3
Has made a booking -- I assume this will be done anyways becasue i will only display bookings of customers with greater to or equal to 4 and revenue generated from them greater to or equal to 4
Confirmed booking -- Booking_Status_ID = 1
Total Revenue >= 20k
Total Direct Bookings >= 4
Maximum Revenue >= 20k
Details of customers end here
Tables look like this:
Customer
Customer_ID
Title
Initial
Firstname
email
Customer Consent
Customer_ID
consent_status_Id
consent_type_id
Household_ID
Booking
Customer_ID
Booking_ID
Total_Revenue
Customer_ID
Booking_Status_ID
Division_ID
Household
Household_ID
Surname
Query Start
Select distinct
c.Customer_ID,
c.Title,
c.Initial,
c.Firstname,
h.Surname,
c.email,
SUM(b.Total_Revenue) as 'Total Total Revenue',
COUNT(b.Customer_ID) as 'Number of Bookings'
FROM Customer c INNER JOIN Household h ON c.Household_ID=h.Household_ID
INNER JOIN Booking b ON c.Customer_ID=b.Customer_ID
INNER JOIN Customer_consent cc ON cc.Customer_ID=c.Customer_ID
where b.Division_ID = 1
and b.Booking_Status_ID = 1
and c.email LIKE '%#%'
and cc.consent_status_Id = 4
and cc.consent_type_id = 3
and (SELECT SUM(b.Total_Revenue) FROM Booking) >= 20000
and count(b.Customer_ID) >= 4
and MAX(b.Total_Revenue) >= 20000;
Query End
Error I came to was this:
An aggregate may not appear in the WHERE clause unless it is in a subquery contained in a HAVING clause or a select list, and the column being aggregated is an outer reference.
What should my query be?
Thanks!
Your Query has aggregate functions in the select, so you'll need a group by clause any way:
FYI: Untested
Select distinct
c.Customer_ID,
c.Title,
c.Initial,
c.Firstname,
h.Surname,
c.email,
SUM(b.Total_Revenue) as 'Total Total Revenue',
COUNT(b.Customer_ID) as 'Number of Bookings'
FROM Customer c INNER JOIN Household h ON c.Household_ID=h.Household_ID
INNER JOIN Booking b ON c.Customer_ID=b.Customer_ID
INNER JOIN Customer_consent cc ON cc.Customer_ID=c.Customer_ID
where b.Division_ID = 1
and b.Booking_Status_ID = 1
and c.email LIKE '%#%'
and cc.consent_status_Id = 4
and cc.consent_type_id = 3
GROUP BY c.Customer_ID,
c.Title,
c.Initial,
c.Firstname,
h.Surname,
c.email,
HAVING
(SELECT SUM(b.Total_Revenue) FROM Booking) >= 20000
and count(b.Customer_ID) >= 4
and MAX(b.Total_Revenue) >= 20000;
Try something like this:
SELECT *
FROM (SELECT DISTINCT c.CUSTOMER_ID,
c.TITLE,
c.INITIAL,
c.FIRSTNAME,
h.SURNAME,
c.EMAIL,
Sum(b.TOTAL_REVENUE) AS [Total Total Revenue],
Count(b.CUSTOMER_ID) AS [Number of Bookings],
Max(b.TOTAL_REVENUE) AS MaxRevenue
FROM CUSTOMER c
INNER JOIN HOUSEHOLD h
ON c.HOUSEHOLD_ID = h.HOUSEHOLD_ID
INNER JOIN BOOKING b
ON c.CUSTOMER_ID = b.CUSTOMER_ID
INNER JOIN CUSTOMER_CONSENT cc
ON cc.CUSTOMER_ID = c.CUSTOMER_ID
WHERE b.DIVISION_ID = 1
AND b.BOOKING_STATUS_ID = 1
AND c.EMAIL LIKE '%#%'
AND cc.CONSENT_STATUS_ID = 4
AND cc.CONSENT_TYPE_ID = 3) T
WHERE [TOTAL TOTAL REVENUE] >= 20000
AND [NUMBER OF BOOKINGS] >= 4
AND MAXREVENUE >= 20000;

Group By not functioning as desired

Let's say I have three tables - Orders, OrderDetails, and ProductType - and the Orders table includes a column for Customer. What I need to do is write a query that will show me a list of customers and how many orders each customer has placed, as well as displaying and grouping by another column, which is a boolean based on whether a particular type of product - say, telephones - is in the order.
For example, we might have:
Customer | NumOrders | IncludesPhone
---------------------------------
Jameson | 3 | Yes
Smith | 5 | Yes
Weber | 1 | Yes
Adams | 2 | | No
Jameson | 1 | No
Smith | 7 | No
Weber | 2 | No
However, when I try to write the query for this, I'm getting multiple rows with the same values for Customer and IncludesPhone, each with a different value for NumOrders. Why is this happening? My query is below:
SELECT Customer, COUNT(Customer) AS NumOrders, CASE WHEN (ProductType.Type = 'Phone') THEN 'Yes' ELSE 'No' END AS IncludesPhoneFROM Orders INNER JOIN OrderDetails INNER JOIN ProductTypeGROUP BY Customer, TypeOrder By IncludesPhone, Customer
Change the group by to
GROUP BY Customer,
CASE WHEN (ProductType.Type = 'Phone') THEN 'Yes' ELSE 'No' END
This query should work
SELECT Customer, COUNT(Customer) AS NumOrders,
CASE WHEN (ProductType.Type = 'Phone') THEN 'Yes' ELSE 'No' END AS IncludesPhone
FROM Orders INNER JOIN OrderDetails INNER JOIN ProductType
GROUP BY Customer,
CASE WHEN (ProductType.Type = 'Phone') THEN 'Yes' ELSE 'No' END
Order By IncludesPhone, Customer
Since you're grouping on both Customer and Type, your query returns the count of orders per customer per type. If you only want one row per customer, you should only group by Customer, and then use something like this, to determine whether a given customer bought a phone:
SELECT Customer, COUNT(Customer) AS NumOrders,
CASE
WHEN SUM(CASE WHEN (ProductType.Type = 'Phone') THEN 1 ELSE 0 END) > 0
THEN 'Yes'
ELSE 'No' END AS IncludesPhone
FROM Orders INNER JOIN OrderDetails INNER JOIN ProductType
GROUP BY Customer
Order By IncludesPhone, Customer
The inner sum basically counts the number of phones bought per customer. If this is more than 0, then the customer bought at least one phone and we return "Yes".
That's because you're grouping by Type column, so there could be duplicate rows. For example, for types 'Email' and 'Personal' column IncludesPhone will be 'No', but as you're grouping by Type there would be two records in the output.
To fix this, you can use same expression in the group by clause or use subquery or Common Table Expression:
with cte as (
select
Customer,
case when pt.Type = 'Phone' then 'Yes' else 'No' end as IncludesPhone
from Orders as o
inner join OrderDetails as od -- ???
inner join ProductType as pt -- ???
)
select Customer, IncludesPhone, count(*) as NumOrders
from cte
group by Customer, IncludesPhone
order by IncludesPhone, Customer
using same expression in the group by clause:
select
Customer,
case when pt.Type = 'Phone' then 'Yes' else 'No' end as IncludesPhone,
count(*) as NumOrders
from Orders as o
inner join OrderDetails as od -- ???
inner join ProductType as pt -- ???
group by Customer, case when pt.Type = 'Phone' then 'Yes' else 'No' end
You could try this query:
SELECT x.Customer,x.NumOrders,
CASE WHEN x.NumOrders>0 AND EXISTS(
SELECT *
FROM Orders o
INNER JOIN OrderDetails od ON ...
INNER JOIN ProductType pt ON ...
WHERE o.Customer=x.Customer
AND pt.Type = 'Phone'
) THEN 1 ELSE 0 END IncludesPhone
FROM
(
SELECT Customer,COUNT(Customer) AS NumOrders
FROM Orders
GROUP BY Customer
) x
Order By IncludesPhone, x.Customer;
or this one:
SELECT o.Customer,
COUNT(o.Customer) AS NumOrders,
MAX(CASE WHEN EXISTS
(
SELECT *
FROM OrderDetails od
JOIN ProductType pt ON ...
WHERE o.OrderID=od.OrderID -- Join predicated between Orders and OrderDetails table
AND ProductType.Type = 'Phone'
) THEN 1 ELSE 0 END) AS IncludesPhone
FROM Orders o
GROUP BY Customer
ORDER BY IncludesPhone, o.Customer