Coalesce not returning 0 - sql

I am creating a stored procedure to return the total sales for a manager and anyone who reports to them. I want it to return a total of zero if there are no sales but it is currently not returning anything if there are no sales in a given period.
CREATE PROCEDURE prc_ManagerTotalSales #managerID INT,
#beginDate DATE,
#endDate DATE,
#group VARCHAR(15) = 'total'
AS
BEGIN
SELECT
FirstName + ' ' + e.LastName AS Name,
COALESCE(SUM(od.UnitPrice * od.Quantity), 0) AS TotalSales
FROM
dbo.Employees e
LEFT OUTER JOIN dbo.Orders o ON e.EmployeeID = o.EmployeeID
LEFT OUTER JOIN dbo.[Order Details] od ON o.OrderID = od.OrderID
WHERE
(e.EmployeeID = #managerID OR e.ReportsTo = #managerID)
AND o.OrderDate >= #beginDate
AND o.OrderDate <= #endDate
GROUP BY
e.FirstName,
e.LastName
END

You can try this.
SELECT
FirstName + ' ' + e.LastName AS Name,
COALESCE(SUM(od.UnitPrice * od.Quantity), 0) AS TotalSales
FROM
dbo.Employees e
LEFT OUTER JOIN dbo.Orders o ON e.EmployeeID = o.EmployeeID
AND o.OrderDate >= #beginDate
AND o.OrderDate <= #endDate
LEFT OUTER JOIN dbo.[Order Details] od ON o.OrderID = od.OrderID
WHERE
(e.EmployeeID = #managerID OR e.ReportsTo = #managerID)
GROUP BY
e.FirstName,
e.LastName

The problem is the group by . . . if no rows match the condition, no rows are returned.
One method is:
with g as (
<your query here>
)
select g.*
from g
union all
select name, totalsales
from (select NULL as name, 0 as totalsales) x
where not exists (select 1 from g);

Related

SQL double group by

I need to select employees' names from those who has sold products, for the biggest total sum of money in each of the years, in Northwind database. I've managed to create a valid query like this:
WITH TEMP_QUERY AS
(
SELECT
DATEPART(YEAR, OrderDate) AS 'Year'
,FirstName + ' ' + LastName AS 'Employee'
,SUM(UnitPrice * Quantity) AS 'Total year sale'
FROM Employees
INNER JOIN Orders
ON Employees.EmployeeID = Orders.EmployeeID
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
GROUP BY FirstName + ' ' + LastName, DATEPART(YEAR, OrderDate)
)
SELECT
DATEPART(YEAR, OrderDate) AS 'Year'
,FirstName + ' ' + LastName AS 'Employee'
,SUM(UnitPrice * Quantity) AS 'Total year sale'
FROM Employees
INNER JOIN Orders
ON Employees.EmployeeID = Orders.EmployeeID
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
GROUP BY
FirstName + ' ' + LastName
,DATEPART(YEAR, OrderDate)
HAVING SUM(UnitPrice * Quantity) IN
(
SELECT MAX(main.[Total year sale]) FROM TEMP_QUERY AS main
INNER JOIN TEMP_QUERY AS e ON e.Employee = main.Employee
GROUP BY main.Year
)
ORDER BY 1;
However I wonder if there's a simpler way of doing this with or without CTE (probably is)
Database scheme.
https://docs.yugabyte.com/images/sample-data/northwind/northwind-er-diagram.png
First, your main query is the same as those one used in CTE (except HAVING and ORDER BY clauses), so you can just write it as SELECT ... FROM temp_query.
Second, I suggest you not to use the combination of FirstName and LastName for aggregation in CTE, since in theory there can be multiple rows for different employees with same values. You have the EmployeeId you a free to use it instead.
Third, since now the main query does not have any aggregation, you do not need a HAVING clause, it can be replaced by WHERE clause.
Fourth, the filter condition (in HAVING clause of your query) does not need self joining of CTE for getting the maximum of Total year sale. Just select MAX("Total year sale") with GROUP BY year.
Fifth, since I have suggested to use EmployeeId to perform the aggregation in CTE, join the Employees table to get the corresponding FirstName and LastName values for an employee in results.
Finally the query would look like this
WITH employee_total_sales (
SELECT
e.employeeid,
DATEPART(YEAR, OrderDate) AS year,
SUM(UnitPrice * Quantity) AS total_sale
FROM Employees e
INNER JOIN Orders o ON e.EmployeeID = o.EmployeeID
INNER JOIN [Order Details] od ON o.OrderID = od.OrderID
GROUP BY e.employeeid, DATEPART(YEAR, OrderDate)
)
SELECT
year AS "Year",
FirstName + ' ' + LastName AS "Employee",
total_sale AS "Total year sale"
FROM employee_total_sales ets
JOIN employees e ON e.employeeid = ets.employeeid
WHERE total_sales IN (
SELECT
MAX(total_sale)
FROM employee_total_sales
GROUP BY year
)
ORDER BY year

Top sales performer is in each month for a specified year

Using the adventure works 2017 test database I need to see who the top sales performer is in each month for a specified year. The management is only interested in the sales of bike “Components”. Create a stored procedure to get this information.
The year must be an input parameter.
Show the firstname and surname in one field.
Show the total value of the sales and the month for each top performer.
Additional marks will be allocated for using a single statement
So far I have this:
CREATE PROCEDURE getTopSalesByYear (#Year int)
AS
BEGIN
SET NOCOUNT ON
SELECT FirstName + ' ' + LastName AS SalesPerson,
sp.BusinessEntityID,
DATENAME(MONTH,SOH.OrderDate) as SalesMonth,
SUM(SOH.SubTotal) AS TotalSales FROM sales.SalesOrderHeader SOH
INNER JOIN sales.SalesOrderDetail SOD ON SOH.SalesOrderId = SOD.SalesOrderId
INNER JOIN sales.SalesPerson sp on soh.SalesPersonID = sp.BusinessEntityID
INNER JOIN Person.Person p on p.BusinessEntityID = sp.BusinessEntityID
INNER JOIN Production.Product Pr on sod.ProductID = pr.ProductID
INNER JOIN Production.ProductCategory pc on pc.ProductCategoryID = pr.ProductSubcategoryID
INNER JOIN Production.ProductSubcategory psc on pc.ProductCategoryID = pc.ProductcategoryID
WHERE psc.ProductCategoryID = 2
GROUP BY p.FirstName,p.LastName,sp.BusinessEntityID, DATENAME(MONTH,SOH.OrderDate)
ORDER BY TotalSales desc
This is what I have so far but it needs to be a procedure with the year being passed. Also noting that I do not know where to pass the parameter to what value.
You need to use ROW_NUMBER to get the best performer per month.
To check that a date is within a particular year, instead of comapring using the YEAR function, it is best to calculate the beginning and end points. The end should be exclusive, this is called a half-open interval.
You can also group by EOMONTH (end of month) which can be a little more efficient than grouping by DATENAME(MONTH, you can calculate the actual name afterwards.
CREATE PROCEDURE getTopSalesByYear (#Year int)
AS
SET NOCOUNT ON;
SELECT
s.FirstName + ' ' + s.LastName AS SalesPerson,
DATENAME(MONTH, s.SalesMonth) AS SalesMonth,
s.TotalSales
FROM (
SELECT
p.FirstName,
p.LastName,
p.BusinessEntityID,
EOMONTH(SOH.OrderDate) AS SalesMonth,
SUM(SOH.SubTotal) AS TotalSales,
ROW_NUMBER() OVER (PARTITION BY EOMONTH(SOH.OrderDate) ORDER BY SUM(SOH.SubTotal) DESC) AS rn
FROM sales.SalesOrderHeader SOH
INNER JOIN sales.SalesOrderDetail SOD ON SOH.SalesOrderId = SOD.SalesOrderId
INNER JOIN sales.SalesPerson sp on soh.SalesPersonID = sp.BusinessEntityID
INNER JOIN Person.Person p on p.BusinessEntityID = sp.BusinessEntityID
INNER JOIN Production.Product Pr on sod.ProductID = pr.ProductID
WHERE pr.ProductCategoryID = 2
AND SOH.OrderDate >= DATEFROMPARTS(#Year , 1, 1)
AND SOH.OrderDate < DATEFROMPARTS(#Year + 1, 1, 1)
GROUP BY
p.FirstName,
p.LastName,
p.BusinessEntityID,
EOMONTH(SOH.OrderDate)
) s
WHERE s.rn = 1
ORDER BY SalesMonth;
GO
Note that this will only give you results for a month if there are actually sales in that month. If there are no sales, you will not get 0, there will be no row for that month.
Thank you for the assistance, see below finished and working query.
CREATE PROCEDURE getTopSalesByYear (#Year int)
AS
SET NOCOUNT ON;
SELECT
s.FirstName + ' ' + s.LastName AS SalesPerson,
DATENAME(MONTH, s.SalesMonth) AS SalesMonth,
s.TotalSales
FROM (
SELECT
p.FirstName,
p.LastName,
p.BusinessEntityID,
EOMONTH(SOH.OrderDate) AS SalesMonth,
SUM(SOH.SubTotal) AS TotalSales,
ROW_NUMBER() OVER (PARTITION BY EOMONTH(SOH.OrderDate) ORDER BY SUM(SOH.SubTotal) DESC) AS rn
FROM sales.SalesOrderHeader SOH
INNER JOIN sales.SalesOrderDetail SOD ON SOH.SalesOrderId = SOD.SalesOrderId
INNER JOIN sales.SalesPerson sp on soh.SalesPersonID = sp.BusinessEntityID
INNER JOIN Person.Person p on p.BusinessEntityID = sp.BusinessEntityID
INNER JOIN Production.Product Pr on sod.ProductID = pr.ProductID
WHERE Pr.ProductSubcategoryID = 2
AND SOH.OrderDate >= DATEFROMPARTS(#Year , 1, 1)
AND SOH.OrderDate < DATEFROMPARTS(#Year + 1, 1, 1)
GROUP BY
p.FirstName,
p.LastName,
p.BusinessEntityID,
EOMONTH(SOH.OrderDate)
) s
WHERE s.rn = 1
ORDER BY SalesMonth;
GO
--Execute
--Exec getTopSalesByYear 2011

T-SQL SELECT with multiple variables

I am trying to get this output using a SQL statement and the NORTHWIND database:
Employee Name:Nancy Davolio
Number of Sales:345
Total Sales:192107.60
Employee Name:Andrew Fuller
Number of Sales:241
Total Sales:166537.75
Employee Name:Janet Leverling
Number of Sales:321
Total Sales:202812.84
Employee Name:Margaret Peacock
Number of Sales:420
Total Sales:232890.85
Employee Name:Steven Buchanan
Number of Sales:117
Total Sales:68792.28
...and 4 more entries
When I use this statement:
USE Northwind
DECLARE #EmployeeName VARCHAR(40),
#NumberOfSales INT,
#TotalSales DECIMAL(10,2),
#Counter TINYINT = 1,
#NumEmployees INT = IDENT_CURRENT('dbo.Employees');
WHILE #Counter < #NumEmployees
BEGIN
--SELECT #EmployeeName = E.FirstName+' '+E.LastName
--SELECT #NumberOfSales = count(od.OrderID)
SELECT #TotalSales = SUM(unitprice * quantity * (1 - Discount))
FROM Employees E
JOIN Orders AS O ON O.EmployeeID = E.EmployeeID
JOIN [Order Details] AS OD ON OD.OrderID = O.OrderID
WHERE E.EmployeeID = #Counter
PRINT 'Employee Name: '--+ #EmployeeName;
PRINT 'Number of Sales: '--+ LTRIM(STR(#NumberOfSales));
PRINT 'Total Sales: '+CONVERT(varchar(10),#TotalSales);
PRINT '';
SET #Counter += 1;
END
I can get each select to work singly but I cannot figure out the syntax to get a single SELECT statement to do all the work. I should also be able to do this with three SET statements but I've not been able to figure that out either. Pointers to both possibilities would be awesome.
Here's that actual step verbiage:
"Within the loop, use a SELECT statement to retrieve the first and last name of each employee, the number of orders handled by each employee and the total sales amount for each employee (you are processing each employee one by one). You will need to join multiple tables together and use aggregate functions to get a count and a total. Assign the concatenated full name, number of sales and total sales amount to the appropriate variables."
Output should be in Messages tab, no table or format other than the expected output listed above.
There is no need for loop(RBAR - Row By Agonizing Row approach should be avoided if possible):
SELECT EmployeeID
,[Employee Name] = E.FirstName+' '+E.LastName
,[TotalSales] = SUM(unitprice * quantity * (1-Discount))
,[NumberOfSales] = COUNT(DISTINCT o.OrderID)
FROM Employees E
JOIN Orders AS O ON O.EmployeeID = E.EmployeeID
JOIN [Order Details] AS OD ON OD.OrderID = O.OrderID
GROUP BY E.EmployeeID, E.FirstName+' '+E.LastName
ORDER BY E.EmployeeID;
EDIT:
Loop version - assigning multiple variables at once.
USE Northwind
DECLARE #EmployeeName VARCHAR(40),
#NumberOfSales INT,
#TotalSales DECIMAL(10,2),
#Counter TINYINT = 1,
#NumEmployees INT = IDENT_CURRENT('dbo.Employees');
WHILE #Counter < #NumEmployees
BEGIN
SELECT #EmployeeName = E.FirstName+' '+E.LastName
,#NumberOfSales = COUNT(DISTINCT o.OrderID)
,#TotalSales = SUM(unitprice * quantity * (1 - Discount))
FROM Employees E
JOIN Orders AS O ON O.EmployeeID = E.EmployeeID
JOIN [Order Details] AS OD ON OD.OrderID = O.OrderID
WHERE E.EmployeeID = #Counter
GROUP BY E.FirstName+' '+E.LastName;
PRINT 'Employee Name: '+ #EmployeeName;
PRINT 'Number of Sales: '+ LTRIM(STR(#NumberOfSales));
PRINT 'Total Sales: '+ CONVERT(varchar(10),#TotalSales);
PRINT '';
SET #Counter += 1;
END
Please note that using WHILE loop maybe very inefficient when you have gaps(i.e. you are starting from 1 up to IDENT_CURRENT, it may be a situation where you have ids like 1,5, 200671 and you end up with unecessary looping).
EDIT 2:
It seems the GROUP BY is required when multiple assigns take place in the select
I've added GROUP BY because FirstName and LastName was not wrapped with aggregated function. You could skip that clause but then you need to add MIN/MAX function:
SELECT #EmployeeName = MIN(E.FirstName)+' '+MIN(E.LastName)
,#NumberOfSales = COUNT(DISTINCT o.OrderID)
,#TotalSales = SUM(unitprice * quantity * (1 - Discount))
FROM Employees E
JOIN Orders AS O ON O.EmployeeID = E.EmployeeID
JOIN [Order Details] AS OD ON OD.OrderID = O.OrderID
WHERE E.EmployeeID = #Counter;
-- and we are sure that all values for First/Last nane are the same because of
-- WHERE E.EmployeeID = #Counter
Related: Group by clause
In standard SQL, a query that includes a GROUP BY clause cannot refer to nonaggregated columns in the select list that are not named in the GROUP BY clause
This should do it. I used CROSS APPLY to unpivot the set and then format it accordingly. You can read more about it in the article called: "CROSS APPLY an Alternative Method to Unpivot". Since SQL works with sets, input and output from SQL should always be a set in my humble opinion.
I am afraid that the way you formatted might not be a SQL's job but still do-able with a "single" select statement as a set operation:
;WITH CTE AS
(
SELECT
EMPLOYEENAME = E.FirstName +' '+ E.LastName,
NUMBEROFORDERS = COUNT(OD.OrderID),
TOTALSALES = SUM(unitprice * quantity * (1-Discount))
FROM Employees E
INNER JOIN Orders AS O ON O.EmployeeID = E.EmployeeID
INNER JOIN [Order Details] AS OD ON OD.OrderID = O.OrderID
GROUP BY E.FirstName + ' ' + E.LastName
)
SELECT COLNAME, ColValue
FROM CTE
CROSS APPLY ( VALUES ('Employe Name:', EMPLOYEENAME),
('Number of Sales:', LTRIM(STR(NUMBEROFORDERS, 25, 5)) ),
('Total Sales:', LTRIM(STR(TOTALSALES, 25, 5)) ),
('','')
) A (COLNAME, ColValue)
Sample output is following:
COLNAME ColValue
------------- | -------------
Employe Name: | Nancy Davolio
Number of Sales:| 345.00000
Total Sales: | 192107.60432

Using SQL Server, how can I find the topselling products by each salesperson in AdventureWorks2014?

For a homework assignment, I am trying to find the topselling products by each salesperson in January 2012 using AdventureWorks2014.
Here is what I have so far:
SELECT
Person.Person.LastName, Person.Person.FirstName,
Person.Person.MiddleName,
Employee_1.JobTitle,
Sales.SalesPerson.SalesQuota, Sales.SalesOrderHeader.OrderDate,
Production.Product.Name,
SUM(distinct OrderQty) AS Expr2
FROM
Sales.SalesOrderDetail
INNER JOIN
Production.Product ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
INNER JOIN
Sales.SalesOrderHeader ON Sales.SalesOrderDetail.SalesOrderID = Sales.SalesOrderHeader.SalesOrderID
AND Sales.SalesOrderDetail.SalesOrderID = Sales.SalesOrderHeader.SalesOrderID
AND Sales.SalesOrderDetail.SalesOrderID = Sales.SalesOrderHeader.SalesOrderID
INNER JOIN
Sales.SalesPerson ON Sales.SalesOrderHeader.SalesPersonID = Sales.SalesPerson.BusinessEntityID
AND Sales.SalesOrderHeader.SalesPersonID = Sales.SalesPerson.BusinessEntityID
AND Sales.SalesOrderHeader.SalesPersonID = Sales.SalesPerson.BusinessEntityID
AND Sales.SalesOrderHeader.SalesPersonID = Sales.SalesPerson.BusinessEntityID
INNER JOIN
HumanResources.Employee AS Employee_1
INNER JOIN
Person.Person ON Employee_1.BusinessEntityID = Person.Person.BusinessEntityID
ON Sales.SalesPerson.BusinessEntityID = Employee_1.BusinessEntityID
AND Sales.SalesPerson.BusinessEntityID = Employee_1.BusinessEntityID
WHERE
(Sales.SalesOrderHeader.OrderDate BETWEEN '2012-01-01' AND '2012-01-31')
GROUP BY
Person.Person.LastName, Person.Person.FirstName, Person.Person.MiddleName,
Employee_1.JobTitle,
Sales.SalesPerson.SalesQuota, Sales.SalesOrderHeader.OrderDate,
Production.Product.Name
ORDER BY
Person.Person.LastName, Production.Product.Name
I can not figure out how to add all of the orderqty for each individual product. In the top two rows of results I have the same product sold by the same person. I want to add those together and then find the top 5 products that each salesperson has?
Can anyone help?
My initial goal would be to reduce the aggregation dataset down to the fewest required number tables to perform the aggregation (SalesOrderHeader, SalesOrderDetail) and join that to the table with the additional information (e.g. Person, Employee).
I included the Product table in the aggregation subquery, but it could be done after the aggregation query and join to the ProductId (after adding it to the group by and select) instead.
There are many ways to do this, here are some:
cross apply version:
select
p.LastName
, p.FirstName
, p.MiddleName
, e.JobTitle
, s.SalesQuota
, s.OrderDate
, s.ProductName
, s.TotalQty
from Person.Person as p on
inner join HumanResources.Employee as e on e.BusinessEntityID = p.BusinessEntityID
cross apply (
select top 5 /* 5 rows */
soh.SalesPersonID
, p.ProductName
, TotalQty = sum(OrderQty)
from Sales.SalesOrderHeader as soh
inner join Sales.SalesOrderDetail sod on soh.SalesOrderID = sod.SalesOrderID
inner join Production.Product as pr on sod.ProductID = pr.ProductID
where soh.OrderDate between '2012-01-01' and '2012-01-31'
and s.SalesPersonID = p.BusinessEntityID /* per person */
group by soh.SalesPersonID, p.ProductName
order by sum(OrderQty) desc
/* ordered by sum(OrderQty) descending */
) s
top with ties version:
select top 5 with ties
p.LastName
, p.FirstName
, p.MiddleName
, e.JobTitle
, s.SalesQuota
, s.OrderDate
, s.ProductName
, s.TotalQty
from Person.Person as p on
inner join HumanResources.Employee as e on e.BusinessEntityID = p.BusinessEntityID
inner join (
select
soh.SalesPersonID
, p.ProductName
, TotalQty = sum(OrderQty)
from Sales.SalesOrderHeader as soh
inner join Sales.SalesOrderDetail sod on soh.SalesOrderID = sod.SalesOrderID
inner join Production.Product as pr on sod.ProductID = pr.ProductID
where soh.OrderDate between '2012-01-01' and '2012-01-31'
group by soh.SalesPersonID, p.ProductName
) on s.SalesPersonID = p.BusinessEntityID
order by row_number() over (partition by p.BusinessEntityID order by s.TotalQty desc)
/* returns all rows where row_number() over() evaluates to 1,2,3,4 ,or 5 */
common table expression with row_number() version:
with top5 as (
select
p.LastName
, p.FirstName
, p.MiddleName
, e.JobTitle
, s.SalesQuota
, s.OrderDate
, s.ProductName
, s.TotalQty
, rn=row_number() over (partition by p.BusinessEntityID order by s.TotalQty desc)
from Person.Person as p on
inner join HumanResources.Employee as e on e.BusinessEntityID = p.BusinessEntityID
inner join (
select
soh.SalesPersonID
, p.ProductName
, TotalQty = sum(OrderQty)
from Sales.SalesOrderHeader as soh
inner join Sales.SalesOrderDetail sod on soh.SalesOrderID = sod.SalesOrderID
inner join Production.Product as pr on sod.ProductID = pr.ProductID
where soh.OrderDate between '2012-01-01' and '2012-01-31'
group by soh.SalesPersonID, p.ProductName
) on s.SalesPersonID = p.BusinessEntityID
)
select
LastName
, FirstName
, MiddleName
, JobTitle
, SalesQuota
, OrderDate
, ProductName
, TotalQty
from top5
where rn < 6

SQL Distinct Sum

SELECT DISTINCT
E.FirstName + ' ' + E.LastName [Full Name],
P.ProductName,
OD.Quantity
FROM Employees E,
Products P,
[Order Details] OD,
Orders O
WHERE
E.EmployeeID = O.EmployeeID
AND O.OrderID = OD.OrderID
AND OD.ProductID = P.ProductID
In the Northwind gives back duplicate FullNames and ProductNames because of the Quantity which is changed (because of the date shipped each time).
I want to present only a Name to a specific ProductName with the Total Quantity and not divided.
You need to use GROUP BY with SUM:
SELECT
e.FirstName + ' ' + e.LastName AS [Full Name],
p.ProductName,
SUM(od.Quantity) AS [Quantity]
FROM Employees e
INNER JOIN Orders o
ON o.EmployeeID = e.EmployeeID
INNER JOIN [Order Details] od
ON od.OrderID = o.OrderID
INNER JOIN Products p
ON p.ProductID = od.ProductID
GROUP BY
e.FirstName + ' ' + e.LastName,
p.ProductName
Note, you need to stop using the old-style JOIN syntax.
I think,it was a good question for discussion.
Correct query always depend upon your actual requirement.
I think your table is too much normalise.In such situation most of them will also keep Employeeid in order_detail table.
At the same time,most of them keep sum value in Order table.
Like sum of quantity,sum of amount etc per orderid in order table.
you can also create view without aggregate function joining all the table.
IMHO,Using Group By clause on so many column and that too on varchar column is bad idea.
Try something like this,
;With CTE as
(
SELECT
E.FirstName + ' ' + E.LastName [Full Name],
O.OrderID,od.qty,P.ProductName
FROM Employees E
inner join Orders O on E.EmployeeID = O.EmployeeID
inner join [Order Details] OD on o.orderid=od.orderid
inner join [Products] P on p.ProductID=od.ProductID
)
,CTE1 as
(
select od.orderid, sum(qty) TotalQty
from CTE c
group by c.orderid
)
select c.[Full Name],c1.TotalQty, P.ProductName from cte c
inner join cte1 c1 on c.orderid=c1.orderid