How to replace a join with subquery and cte? - sql-server

Hello i'm practicing multi options for each question I solve.
I want to transform this query with join into a query with subqueries and another query with cte.
however I'm finding it hard to do so:
SELECT
C.CompanyName, MAX(OD.UnitPrice*OD.Quantity)MaxOdPrice,
MIN(OD.UNITPRICE*OD.Quantity) MinOdPrice
FROM [Order Details] OD
JOIN Orders O ON OD.OrderID=O.OrderID
JOIN Customers C ON O.CustomerID=C.CustomerID
GROUP BY C.CompanyName
thanks

You will still need JOINS but here's a simplistic CTE that I hope may give some ideas on how to proceed further
;With cteCustomers
AS(
SELECT X.CustomerID, X.CompanyName
FROM Customers X
)
SELECT
C.CompanyName, MAX(OD.UnitPrice*OD.Quantity)MaxOdPrice,
MIN(OD.UNITPRICE*OD.Quantity) MinOdPrice
FROM [Order Details] OD
JOIN Orders O ON OD.OrderID=O.OrderID
JOIN cteCustomers C ON O.CustomerID=C.CustomerID
GROUP BY C.CompanyName

completely untested but I think something like this can do it without joins.
select C.CompanyName,
max(select MAX(UnitPrice * Quantity)
from [Order Details]
where OrderID in (select distinct OrderID
from Orders
where CustomerID in (select CustomerID
from Customers innerC
where innerC.CompanyName = C.CompanyName
)
)
group by OrderID
) AS MaxOdPrice
FROM Customers C
GROUP BY C.CompanyName
I'm only showing MaxOdPrice.

Related

How to join select statements together

I have a sample db with 3 different tables, customers, orders, orderdetails.
The assignment is to show customer name, and address from a customers table and then show each order total amount by order id. Order details has order id several times and it is by unit x price so I have to sum these after performing the calculation.
Customers has a field customerid, which I can use to join with orders which has the same field, the orders table has orderid which I can use to join to orderdetails and sum the order total but I do not know how to put the information together. Customers table does not have the fields to calculate the total order, and only has customerid. So, I'm trying to pinch together from 3 tables where there is some related column but not all present in each table.
I can do 2 separate select statements and each do what I expect but I have been trying to get the info together and have been unable to.
SELECT c.CustomerID, c.[Address], o.orderid
FROM Customers c
Join Orders o
ON c.CustomerID = o.CustomerID
--how to join these together?
SELECT od.orderid, SUM(od.UnitPrice*od.Quantity) as 'Subtotal'
FROM OrderDetails od
Join Orders o
ON od.OrderID = o.OrderID
Group by od.OrderID
I am trying to show this with the following information:
Customer Name, Address, OrderID, and Order Total.
try this -
SELECT c.CustomerID, c.[Address], od.orderid, SUM(od.UnitPrice* od.Quantity) as 'Subtotal'
FROM OrderDetails od
Join Orders o
ON od.OrderID = o.OrderID
join Customers c ON c.CustomerID = o.CustomerID
Group by c.CustomerID, c.[Address], od.OrderID
You can join three tables together like given below. I am using derived table OrderDetails to calculate subtotal at orderId level.
SELECT c.CustomerID, c.[Address], o.orderid, SUM(od.Subtotal) as 'Subtotal'
FROM Customers c
Join Orders o
ON c.CustomerID = o.CustomerID
join (SELECT orderid, SUM(od.UnitPrice*od.Quantity) as Subtotal from OrderDetails od GROUP BY OrderId) as OrderDetails od
ON od.OrderID = o.OrderID
group by c.CustomerID, c.[Address], o.orderid
Mukesh's answer led me directly to the finish, I was able to leave the customerid out of the result with the following. This helped a lot and I appreciate everyone's input.
SELECT c.CompanyName, c.[Address], od.orderid, SUM(od.UnitPrice* od.Quantity) as
'Subtotal'
FROM OrderDetails od
Join Orders o
ON od.OrderID = o.OrderID
join Customers c ON c.CustomerID = o.CustomerID
Group by c.CompanyName, c.[Address], od.OrderID

How to get first and last related event without having to do extra subqueries?

I have two tables: Customers and CustomerEvents. Each customer can have 0 or more events.
I'm working on a report that's supposed to look something like this:
CustomerName | FirstEventDate | FirstEventMessage | LastEventDate | LastEventText
---------------------------------------------------------------------------------------
Customers 'R' US | 2018-01-01 | Customer registered | 2018-04-06 | Customer Quit
The actual query is going to be a lot larger than that, but this is the general gist of it.
If I only needed the dates, I could easily do it using aggregates:
SELECT
c.Name AS ContactName
, MAX(e.DateTime) AS FirstEventDate
, '???' AS FirstEventMessage
, MIN(e.DateTime) AS FirstEventDate
, '???' AS FirstEventMessage
FROM Contacts c
LEFT JOIN CustomerEvents e ON e.ContactId = c.Id
GROUP BY c.Name
Unfortunately, that leaves out the corresponding messages.
I've managed to solve it using a subquery for each message, but that gets super expensive, so I was wondering if there's another approach you'd suggest. The full query is more complex than the example will have joins on different kinds of events, each displaying the date and message of the first and last event.
As you asked me, to create an answer out of my comment, here you are:
With version 2012 Microsoft introduced some new windowing functions. Usefull for you are FIRST_VALUE() and LAST_VALUE(). Both need an OVER() clause to specify the sort order and - if needed - a partitioning rule.
This should work (but I do not know your tables and data):
SELECT
c.Name AS ContactName
, MIN(e.DateTime) AS FirstEventDate
, FIRST_VALUE(e.EventMessage) OVER(ORDER BY e.DateTime) AS FirstEventMessage
, MAX(e.DateTime) AS LastEventDate
, LAST_VALUE(e.EventMessage) OVER(ORDER BY e.DateTime) AS LastventMessage
FROM Contacts c
LEFT JOIN CustomerEvents e ON e.ContactId = c.Id
GROUP BY c.Id,c.Name;
But be warned: If your e.DateTime is not unique (per Contact), you will get a random "first" value...
Hints
Make sure to have indexes on DateTime and ContactId and
add the c.Id to your GROUP BY
An alternative was to replace the LEFT JOIN CustomerEvents with a row-wise executed correlated sub-query. This has the advantage, that you can be sure, that both (and more) values are taken from the same row.
OUTER APPLY (SELECT TOP 1 e.[DateTime],e.[EventMessage]
FROM CustomerEvents AS e
WHERE e.ContactId=c.Id
ORDER BY e.EventMessage ASC) AS FirstEvent
--same with DESC
OUTER APPLY (SELECT TOP 1 e.[DateTime],e.[EventMessage]
FROM CustomerEvents AS e
WHERE e.ContactId=c.Id
ORDER BY e.EventMessage DESC) AS LastEvent
Then use the columns in your query like
FirstEvent.DateTime AS FirstDateTime
,FirstEvent.EventMessage AS FirstMessage
,LastEvent.DateTime AS LastDateTime
,LastEvent.EventMessage AS LastMessage
Try this CTE approach, adjust the columns to SELECT clause according to you need.
;WITH CTE
AS(
SELECT
*,
ROW_NUMBER() OVER(PARTITION BY c.Name ORDER BY e.DateTime) AS RowAsc,
ROW_NUMBER() OVER(PARTITION BY c.Name ORDER BY e.DateTime DESC) AS RowDesc
FROM Contacts c
LEFT JOIN CustomerEvents e ON e.ContactId = c.Id
)
SELECT
c1.*
, c2.*
FROM CTE c1
INNER JOIN CTE c2 ON c1.Name = c2.Name
AND c1.RowAsc = 1
AND c2.RowDesc = 1
Try Below Approach
SELECT
c.Name AS CustomerName
, MAX(e.DateTime) AS LastEventDate
, B.Message AS FirstEventMessage
, MIN(e.DateTime) AS FirstEventDate
, D.Message AS FirstEventMessage
FROM #Customers c
LEFT JOIN #CustomerEvents e ON e.CustomerId = c.Id
LEFT JOIN
(
SELECT A.CustomerId, A.Message
FROM
(
SELECT
CustomerId, Message, Row_Number()over(Partition By CustomerId order by DATETIME DESC) as No
FROM
#CustomerEvents
) A
WHERE
A.No = 1
) B ON B.CustomerId = C.Id
LEFT JOIN
(
SELECT A.CustomerId, A.Message
FROM
(
SELECT
CustomerId, Message, Row_Number()over(Partition By CustomerId order by DATETIME) as No
FROM
#CustomerEvents
) A
WHERE
A.No = 1
) d ON d.CustomerId = C.Id
GROUP BY
c.Name, B.Message, D.Message

How I do SQL Pivot with respect to given query?

I'm working on SQL Server 2012. Trying to Pivot table data.
I have tried below following Query,
SELECT CategoryName, 1996_Val, 1997_Val
FROM(
SELECT
Categories.CategoryName,
Sum(CONVERT(money,("Order Details".UnitPrice*Quantity*(1-Discount)/100))*100) AS Sales,
YEAR(Orders.ShippedDate) AS ShippingYear
FROM Orders
INNER JOIN [Order Details] ON Orders.OrderID = [Order Details].OrderID
INNER JOIN Products ON [Order Details].ProductID = Products.ProductID
INNER JOIN Categories ON Products.CategoryID = Categories.CategoryID
WHERE (((Orders.ShippedDate) Between '19960101' And '19971231'))
GROUP BY Categories.CategoryID, Categories.CategoryName,YEAR(Orders.ShippedDate)
)p
PIVOT
(MAX(Sales) For ShippingYear IN(1996,1997)) AS pvt
ORDER BY Categories.CategoryID
which didn't work. Please help me...
I think you got the pivot a bit wrong. I think it should look something like this:
SELECT
*
FROM
(
SELECT
Categories.CategoryName,
YEAR(Orders.ShippedDate) AS ShippingYear,
CONVERT(money,([Order Details].UnitPrice*Quantity*(1-Discount)/100))*100 as Sales
FROM
Orders
INNER JOIN [Order Details] ON Orders.OrderID = [Order Details].OrderID
INNER JOIN Products ON [Order Details].ProductID = Products.ProductID
INNER JOIN Categories ON Products.CategoryID = Categories.CategoryID
WHERE (((Orders.ShippedDate) Between '19960101' And '19971231'))
) AS SourceTable
PIVOT
(
SUM(Sales)
FOR CategoryName IN ([Confections],[Meat/Poultry],
[Beverages],[Grains/Cereals],[Seafood])
) AS pvt

select count over partition by

I am learning window functions in sql server. I am using AdventrueWorks2012 database for practice. I want to calculate total number of sales and purchases for each item in the store.
The classic solution can be like
SELECT ProductID,
Quantity,
(SELECT Count(*)
FROM AdventureWorks.Purchasing.PurchaseOrderDetail
WHERE PurchaseOrderDetail.ProductID = p.ProductID) TotalPurchases,
(SELECT Count(*)
FROM AdventureWorks.Sales.SalesOrderDetail
WHERE SalesOrderDetail.ProductID = p.ProductID) TotalSales
FROM (SELECT DISTINCT ProductID,
Quantity
FROM AdventureWorks.Production.ProductInventory) p
Trying to convert to window functions gives me wrong results:
SELECT DISTINCT d.ProductID,
Quantity,
Count(d.ProductID)
OVER(
PARTITION BY d.ProductID) TotalPurchases,
Count(d2.ProductID)
OVER(
PARTITION BY d2.ProductID) TotalSales
FROM (SELECT DISTINCT ProductID,
Quantity
FROM AdventureWorks.Production.ProductInventory) p
INNER JOIN AdventureWorks.Purchasing.PurchaseOrderDetail d
ON p.ProductID = d.ProductID
INNER JOIN AdventureWorks.Sales.SalesOrderDetail d2
ON p.ProductID = d2.ProductID
ORDER BY d.ProductID
Why this is wrong? How can I correct it?
You should change INNER JOIN to LEFT JOIN
Because when you inner join, result will miss productid which from ProductInventory table does not have PurchaseOrderDetail or SalesOrderDetail.

Alias name issue in SQL

I was trying to write a query for the SQL Server sample DB Northwind. The question was: "Show the most recent five orders that were purchased by a customer who has spent more than $25,000 with Northwind."
In my query the Alias name - "Amount" is not being recognized. My query is as follows:
select top(5) a.customerid, sum(b.unitprice*b.quantity) as "Amount", max(c.orderdate) as Orderdate
from customers a join orders c
on a.customerid = c.customerid
join [order details] b
on c.orderid = b.orderid
group by a.customerid
--having Amount > 25000 --throws error
having sum(b.unitprice*b.quantity) > 25000 --works, but I don't think that this is a good solution
order by Orderdate desc
Pls let me know what I am doing wrong here, as I am a newbie in writing T Sql. Also can this query and my logic be treated as production level query?
TIA,
You must use the aggregate in the query you have. This all has to do with the order in which a SELECT statement is executed. The syntax of the SELECT statement is as follows:
SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY
The order in which a SELECT statement is executed is as follows. Since the SELECT clause isn't executed until after the HAVING clause, you can't use the alias like you can in the ORDER BY clause.
FROM
WHERE
GROUP BY
HAVING
SELECT
ORDER BY
Reference Article: http://www.bennadel.com/blog/70-sql-query-order-of-operations.htm
This is a known limitation in SQL Server, at least, but no idea if it's a bug, intentional or even part of the standard. But the thing is, neither the WHERE or HAVING clauses accept an alias as part of their conditions, you must use only columns from the original source tables, which means that for filtering by calculated expressions, you must copy-paste the very same thing in both the SELECT and WHERE parts.
A workaround for avoiding this duplication can be to use a subquery or cte and apply the filter on the outer query, when the alias is just an "input" table:
WITH TopOrders AS (
select a.customerid, sum(b.unitprice*b.quantity) as "Amount", max(c.orderdate) as Orderdate
from customers a join orders c
on a.customerid = c.customerid
join [order details] b
on c.orderid = b.orderid
group by a.customerid
--no filter here
order by Orderdate desc
)
SELECT TOP(5) * FROM TopOrders WHERE Amount > 25000 ;
Interesting enough, the ORDER BY clause does accepts aliases directly.
You must use Where b.unitprice*b.quantity > 25000 instead of having Amount > 25000.
Having used for aggregate conditions. Your business determine your query condition. If you need to calculate sum of prices that have above value than 25000, must be use Where b.unitprice*b.quantity > 25000 and if you need to show customer that have total price above than 25000 must be use having Amount > 25000 in your query.
select top(5) a.customerid, sum(b.unitprice*b.quantity) as Amount, max(c.orderdate) as Orderdate
from customers a
JOIN orders c ON a.customerid = c.customerid
join [order details] b ON c.orderid = b.orderid
group by a.customerid
having sum(b.unitprice*b.quantity) > 25000 --works, but I don't think that this is a good solution
Order by Amount
I don't have that schema at hand, so table' and column' names might go a little astray, but the principle is the same:
select top (5) ord2.*
from (
select top (1) ord.CustomerId
from dbo.Orders ord
inner join dbo.[Order Details] od on od.OrderId = ord.OrderId
group by ord.CustomerId
having sum(od.unitPrice * od.Quantity) > $25000
) sq
inner join dbo.Orders ord2 on ord2.CustomerId = sq.CustomerId
order by ord2.OrderDate desc;
The Having Clause will works with aggregate function like SUM,MAX,AVG..
You may try like this
SELECT TOP 5 customerid,SUM(Amount)Amount , MAX(Orderdate) Orderdate
FROM
(
SELECT A.customerid, (B.unitprice * B.quantity) As "Amount", C.orderdate As Orderdate
FROM customers A JOIN orders C ON A.customerid = C.customerid
JOIN [order details] B ON C.orderid = B.orderid
) Tmp
GROUP BY customerid
HAVING SUM(Amount) > 25000
ORDER BY Orderdate DESC
The question is little ambiguos.
Show the most recent five orders that were purchased by a customer who
has spent more than $25,000 with Northwind.
Is it asking to show the 5 recent orders by all the customers who have spent more than $25,000 in all of their transactions (which can be more than 5).
The following query shows all the customers who spent $25000 in all of their transactions (not just the recent 5).
In one of the Subquery BigSpenders it gets all the Customers who spent more than $25000.
Another Subquery calculates the total amount for each order.
Then it gets rank of all the orders by OrderDate and OrderID.
Then it filters it by Top 5 orders for each customer.
--
SELECT *
FROM (SELECT C.customerid,
C.orderdate,
C.orderid,
B3.amount,
Row_number()
OVER(
partition BY C.customerid
ORDER BY C.orderdate DESC, C.orderid DESC) Rank
FROM orders C
JOIN
--Get Amount Spend Per Order
(SELECT b2.orderid,
Sum(b2.unitprice * b2.quantity) AS Amount
FROM [order details] b2
GROUP BY b2.orderid) B3
ON C.orderid = B3.orderid
JOIN
--Get Customers who spent more than 25000
(SELECT c.customerid
FROM orders c
JOIN [order details] b
ON c.orderid = b.orderid
GROUP BY c.customerid
HAVING Sum(b.unitprice * b.quantity) > 25000) BigSpenders
ON C.customerid = BigSpenders.customerid) X
WHERE X.rank <= 5

Resources