T-SQL SELECT with multiple variables - sql

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

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

Coalesce not returning 0

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);

SQL Server 2012 not getting values from subqueries

I am pretty new to SQL Server 2012 and I am doing an exercise where I am trying to obtain
1)The EmployeeID, LastName, and FirstName for each employee
2)The OrderID and the total price of the most expensive order
3)The ShipName
I have gotten most of the way there, getting all the info for each employee and the total price of their most expensive order.
Here is the code I have gotten done so far:
select e.EmployeeID, e.FirstName, e.LastName, max(od.totPrice) as mostExpTotOrder
from Employees e
inner join
Orders o
on e.EmployeeID = o.EmployeeID
inner join
(
select OrderID, sum(unitprice) as totPrice
from [Order Details]
where discount = 0
group by OrderID
) od
on o.OrderID = od.OrderID
group by e.EmployeeID, e.FirstName, e.LastName
When I try to enter in o.OrderID and o.ShipName to the outer query, the query grabs every OrderID, whereas I am just trying to get the OrderID of the most expensive order each employee has processed.
select e.EmployeeID, e.FirstName, e.LastName, max(od.totPrice) as mostExpTotOrder, o.OrderID, o.ShipName
from Employees e
inner join
Orders o
on e.EmployeeID = o.EmployeeID
inner join
(
select OrderID, sum(unitprice) as totPrice
from [Order Details]
where discount = 0
group by OrderID
) od
on o.OrderID = od.OrderID
group by e.EmployeeID, e.FirstName, e.LastName, o.OrderID, o.ShipName
Am I missing something pretty basic?
When you add the order Id to the aggregation, Then you get what you ask for.
You are getting the max price per employee PER ORDER.
You didn't mention in any way in your query that you want the order for which the max price was associated to.
To achieve that, you'll need to join the Aggregated Query by the employee Id with the original query, for example by the employee Id and the Price (Assuming both will identify the row uniquely) and then select the order Id.
A small sample to demonstrate:
SELECT *
into #tmp
FROM
(
select 1 as OrderId , 1 as EmployeeId , 100 as Price
UNION
select 2 as OrderId , 1 as EmployeeId , 200 as Price
UNION
select 3 as OrderId , 2 as EmployeeId , 100 as Price
UNION
select 4 as OrderId , 2 as EmployeeId , 200 as Price
UNION
select 5 as OrderId , 2 as EmployeeId , 300 as Price
) a
SELECT a.* , b.OrderId
FROM (
SELECT EmployeeId , Max(Price) Price
FROM #tmp a
group by EmployeeId
) a
inner join #tmp b
on a.EmployeeId = b.EmployeeId
and a.Price = b.Price

Drastic CTE query speed degradation on actions in outer select

I have a complex report (Similar to example below) it returns with only 52 items in 42 seconds (Slow but does complex joins) it drastically slows down when I do the following:
Add a column in the outer select to get a serialized list of items for
each item in the result (Adds 150 seconds) - See function.
Insert result into a variable table (Adds 120 seconds), I though hoped flattening it would reduce the (1) issue but no cigar.
How I understand the execution is that the where is executed first then logic in the select (Should only be done for the 52 result items). However if I run that exact 52 items with the (1) scenario it only takes 7 seconds vs the added 150 when using it on the outer CTE select.
Why could this be and how can I add the column without the bloated execution time?
CREATE FUNCTION PriorShippers
(
#customerId nchar(5)
)
RETURNS varchar(500)
AS
BEGIN
DECLARE #return varchar(500);
with data as
(
select distinct S.CompanyName from Customers C
join Orders O on C.CustomerID = O.CustomerID
join Shippers S on O.ShipVia = S.ShipperID
where C.CustomerID = #customerId
) select #return = STUFF((select CompanyName + ' ' from data FOR XML PATH('')),1,0,'')
return #return
END
The query (I used Northwind database - Install Instructions here for SQL 2012)
DECLARE #categories TABLE
(
Name varchar(100),
SourceCountry varchar(100)
);
insert into #categories VALUES ('Seafood', 'US');
insert into #categories VALUES ('Beverages', 'US');
insert into #categories VALUES ('Condiments', 'US');
insert into #categories VALUES ('Dairy Products', 'India');
insert into #categories VALUES ('Grains/Cereals', 'India');
with data as
(
select C.CustomerID, C.CompanyName,
(CASE WHEN EXISTS(select * from Orders O where O.CustomerID = C.CustomerID) THEN
(select count(distinct CAT.CategoryID) from Orders O
join [Order Details] OD on O.OrderID = OD.OrderID
join Products P on OD.ProductID = P.ProductID
join Categories CAT on P.CategoryID = CAT.CategoryID
where EXISTS(select * from #categories where Name = CAT.CategoryName AND SourceCountry = 'US'))
ELSE 0 END) as 'US Orders',
(CASE WHEN EXISTS(select * from Orders O where O.CustomerID = C.CustomerID) THEN
(select count(distinct CAT.CategoryID) from Orders O
join [Order Details] OD on O.OrderID = OD.OrderID
join Products P on OD.ProductID = P.ProductID
join Categories CAT on P.CategoryID = CAT.CategoryID
where EXISTS(select * from #categories where Name = CAT.CategoryName AND SourceCountry = 'India'))
ELSE 0 END) as 'India Orders'
from Customers C
) select top 10 CompanyName, [US Orders], [India Orders]
-- Below: Adding this have significant slow down
, dbo.PriorShippers(CustomerID)
from data where [US Orders] > 0 Order By [US Orders]
You are executing a user-defined function for each selected row. In short, don't do that.
Move the code in the function into a DTE directly within the report.
Also, I noticed that the joins for your counts are identical. They look like prime candidates for a third DTE that you could simply join to to further improve the performance of the overall query.

SQL-Server, confront Customers with Employees function

I might have a really simple question, but it doesn't seem i can find the solution yet. Basically, i have to write a SQL-Server function, based on Northwind DB. It has to:
Take 2 dates as arguments and display, without repetition these Customer's data ID | Name | City | Address , for those customers where the total purchase he had made from at least one Employee , is greater than the average sale made by this Employee between the two dates.
So the main steps should be:
1. Retrieve the total purchase made from a Customer from each Employee. I know how to get the total purchase from each Company:
SELECT
Customers.CompanyName, SUM(UnitPrice*Quantity)
FROM Orders inner join [Order Details]
ON Orders.OrderID=[Order Details].OrderID INNER JOIN
Customers ON Orders.CustomerID=Customers.CustomerID
GROUP BY Customers.CompanyName
But how can i get those made from each employee?
2.Confront this with the average sales of this Employee between the given dates. I can get the average for each employee:
SELECT FirstName+' '+LastName, AVG(UnitPrice*Quantity)
FROM Orders inner join [Order Details]
ON Orders.OrderID=[Order Details].OrderID
INNER JOIN Employees
ON Orders.EmployeeID=Employees.EmployeeID
WHERE OrderDate BETWEEN #dt1 and #dt2
GROUP BY FirstName+' '+LastName
Note that i'm only pasting the query part, but here, the Employee should depend on the first query (probably this should be put inside a subquery)
Everything should be put inside a single function (it should not be split in two). The Northwind DB diagram is: Northwind Diagram . Please help!
Hope I got the logic right:
create function x (#from datetime, #to datetime)
returns table
as
return (
with cust as (
select o.customerid, o.employeeid, sum(unitprice*quantity) as cust_purchase
from orders o
inner join [order details] d on o.orderid=d.orderid
where o.orderdate between #from and #to
group by o.customerid, o.employeeid
),
emp as (
select o.employeeid, avg(unitprice*quantity) as emp_sale
from orders o
inner join [order details] d on o.orderid=d.orderid
where o.orderdate between #from and #to
group by o.employeeid
)
select c.customerid, c.companyname, c.city, c.address
from cust
inner join emp on cust.employeeid = emp.employeeid
and cust.cust_purchase > emp.emp_sale
inner join customers c on cust.customerid = c.customerid
)
go
select * from x ('19980401', '19980430')