Divide results in two columns depending on the input values? SQL Server - sql

I am using the Nortwind database with SQL Server 2014, I try to make a query to divide the results of the orders in two different years, The format that I want in my query is
category |anio one | anio two
where the years may vary , What I try so far is
SELECT ca.CategoryName , YEAR(o.OrderDate), SUM(ot.UnitPrice*ot.Quantity) as total
FROM Orders o
INNER JOIN [Order Details] ot ON O.OrderID = ot.OrderID
INNER JOIN Products pro ON ot.ProductID = pro.ProductID
INNER JOIN Categories ca ON pro.CategoryID = ca.CategoryID
GROUP BY ca.CategoryName,YEAR(o.OrderDate)
ORDER BY ca.CategoryName;
This gives me the totals of each category for a different year, 1996-1997-1998 in column YEAR(o.OrderDate)
I want to get for example
CategoryName | 1996 | 1997
Beverages |53879,20 | 110424,00
Condiments |19458,30 | 59679,00
....

Use "conditional aggregates".
SELECT
ca.CategoryName
, SUM(case when year(o.OrderDate) = 1996 then ot.UnitPrice * ot.Quantity end) AS total_1996
, SUM(case when year(o.OrderDate) = 1997 then ot.UnitPrice * ot.Quantity end) AS total_1997
FROM Orders o
INNER JOIN [Order Details] ot ON o.OrderID = ot.OrderID
INNER JOIN Products pro ON ot.ProductID = pro.ProductID
INNER JOIN Categories ca ON pro.CategoryID = ca.CategoryID
where o.OrderDate >= '19960101' and o.OrderDate < '19980101'
GROUP BY
ca.CategoryName
ORDER BY
ca.CategoryName
Basically that means use a case expression inside the aggregate function.
I case you are wondering why I have not used "between in the where clause: see
Bad habits to kick : mis-handling date / range queries

You can use PIVOT to get your desired Output
BEGIN TRAN
CREATE TABLE #Temp(CategoryName NVARCHAR(50),[Year]INT,TOTAL DECIMAL(15,2))
INSERT INTO #Temp
SELECT ca.CategoryName , YEAR(o.OrderDate), SUM(ot.UnitPrice*ot.Quantity) as total
FROM Orders o
INNER JOIN [Order Details] ot ON O.OrderID = ot.OrderID
INNER JOIN Products pro ON ot.ProductID = pro.ProductID
INNER JOIN Categories ca ON pro.CategoryID = ca.CategoryID
GROUP BY ca.CategoryName,YEAR(o.OrderDate)
ORDER BY ca.CategoryName;
SELECT * FROM #Temp
GO
select *
from
(
select CategoryName, [Year], TOTAL
from #Temp
) src
pivot
(
sum(TOTAL)
for YEAR in ([1996], [1997]
)) piv;
ROLLBACK TRAN

you can use pivot to get the desired output
CREATE TABLE #TEMP
(
Category VARCHAR(200),
YEAR1 NUMERIC,
Total MONEY
)
INSERT INTO #TEMP
SELECT 'beverages', 1996, 500
union
SELECT 'beverages', 1997, 750
union
SELECT 'Condiments', 1997, 1000
union
SELECT 'Condiments', 1996, 800
SELECT *
FROM
(
SELECT Category,YEAR1, Total FROM #TEMP
) AS SourceTable
PIVOT
(
AVG(Total) FOR YEAR1 IN ( [1996], [1997])
) AS PivotTable;

Related

How can also show null (or zero) values corresponding to Q1 and Q5 values of column on SQLserver?

I made this exercise on SQL server: write a query that lists for each cluster the quantity of products that fall within it. The company wants to obtain an analysis of sales with respect to the average quantity of each product present in each order, classifying them into six clusters: Q1 (<15), Q2 (15-20), Q3 (21-25), Q4 (26-30), Q5 (31-35), Q6(>35). Write a query that lists, for each product, the product name and the cluster to which it belong. The database is northwind
select count(ProductName) as prod_num ,cluster
from (
select ProductName,
case
when avg(Quantity) < 15 then 'Q1'
when avg(Quantity) <= 20 then 'Q2'
when avg(Quantity) between 21 and 25 then 'Q3'
when avg(Quantity) between 26 and 30 then 'Q4'
when avg(Quantity) between 31 and 35 then 'Q5'
else 'Q6'
end
as cluster
from [Order Details] od join Products pr on od.ProductID=pr.ProductID
group by ProductName
) as clusters
group by cluster
order by cluster
OUTPUT
22 Q2
35 Q3
18 Q4
2 Q6
I also need to display values for Q1 and Q5.
You can always seed your initial counts, for example:
declare #clusters table (prod_num int, cluster nchar(2));
insert into #clusters values
(0, 'Q1'),(0, 'Q2'),(0, 'Q3'),(0, 'Q4'),(0, 'Q5'),(0, 'Q6');
select
t1.cluster,
t1.prod_num + isnull(t2.prod_num, 0) as prod_num
from
#clusters t1
left join
(
select count(ProductName) as prod_num ,cluster
from (
select ProductName,
case
when avg(Quantity) < 15 then 'Q1'
when avg(Quantity) between 15 and 20 then 'Q2'
when avg(Quantity) between 21 and 25 then 'Q3'
when avg(Quantity) between 26 and 30 then 'Q4'
when avg(Quantity) between 31 and 35 then 'Q5'
else 'Q6'
end
as cluster
from [Order Details] od join Products pr on od.ProductID=pr.ProductID
group by ProductName
) as clusters
group by cluster
) t2
on t1.cluster = t2.cluster
order by t1.cluster;
Now we have an initial count of zero for all groups and add to that the counts we found in our query.
Untested so please let me know if you find errors...
You don't need a temp table or table variable, you can use a virtual VALUES clause to generate all the rows.
You can also significantly simplify this by putting the range numbers into that table also.
select
t1.cluster,
count(t2.AvgQuantity) as prod_num
from (VALUES
('Q1', -999999, 15),
('Q2', 15, 20),
('Q3', 20, 25),
('Q4', 25, 30),
('Q5', 30, 35),
('Q6', 35, 999999)
) t1(cluster, low, hi)
left join (
select
ProductName,
avg(Quantity) as AvgQuantity
from Products pr
join [Order Details] od on od.ProductID = pr.ProductID
group by
pr.Id,
pr.ProductName
) t2 on t2.AvgQuantity > t1.low AND t2.AvgQuantity <= t1.hi
group by
t1.cluster
order by
t1.cluster;
For efficiency (as well as possibly accuracy), you should also group by the product ID or primary key.
Note that the above query only gets you results for products that were actually sold. To include all products, change the innermost join to a left join
You can make your query much simpler by storing the clusters and the ranges in a table (so that you can reuse it across other similar queries that use the same breakdown). I use a #temp table here but there's no reason this couldn't be a static, permanent dimension table.
CREATE TABLE #clusters(cluster char(2), lo int, hi int,
INDEX cix_cl CLUSTERED(lo,hi));
INSERT #clusters VALUES('Q1', 0,14),('Q2',15,20),('Q3',21,25),
('Q4',26,30),('Q5',31,35),('Q6',36,2000000000);
SELECT prod_num = COUNT(p.ProductName), cl.cluster
FROM #clusters AS cl
LEFT OUTER JOIN
(
SELECT pr.ProductName, avgQ = AVG(od.Quantity)
FROM dbo.[Order Details] AS od
INNER JOIN dbo.Products AS pr
ON od.ProductID = pr.ProductID
GROUP BY pr.ProductName
) AS p
ON p.avgQ BETWEEN cl.lo AND cl.hi
GROUP BY cl.cluster;
Working example in this fiddle.
And again, unless ProductName is not unique and you really care about averages across different IDs with the same name as opposed to just product IDs, the join is unnecessary and you could simplify even further:
SELECT prod_num = COUNT(p.ProductID), cl.cluster
FROM #clusters AS cl
LEFT OUTER JOIN
(
SELECT ProductID, avgQ = AVG(Quantity)
FROM dbo.[Order Details]
GROUP BY ProductID
) AS p
ON p.avgQ BETWEEN cl.lo AND cl.hi
GROUP BY cl.cluster;

Select Sold and unsold product from same table in SQL Server for last month

I have Database table and trying to write query to find sold and not sold product list from one table.
Table is Below
Expecting Result
How do i get this result? i am using CTE to create Tamp table and with all services and then do left join but it dose give me only product sold in Feb, but i want all product with no sell too.
You can cross join the products and the dates, and then bring the table with a left join:
select
p.product,
t.quantity_sold,
d.yr,
d.mn
from (select distinct product from mytable) p
cross join (select distinct yr, mn from mytable) d
left join mytable t
on t.product = p.product
and t.yr = d.yr
and t.mn = d.mn
This puts nulls for rows with no sale - that's presumably a numeric column so you generally don't want to write a string like 'Not Sold' into it.
If there is a possibility of duplicate (product, yr, mn), you might want to use outer aggregation:
select
p.product,
sum(t.quantity_sold) quantity_sold,
d.yr,
d.mn
from (select distinct product from mytable) p
cross join (select distinct yr, mn from mytable) d
left join mytable t
on t.product = p.product
and t.yr = d.yr
and t.mn = d.mn
group by p.product, d.yr, d.mn

Joining 3 tables in SQL showing all 3 tables

I am a basic user of SQL but need to join 3 tables together to sow a) sales b) returns and c) profit
I currently have the following code
select * from (
select SUM(Return_Amount) , 'Return' as type, monthname(Return_Date) as month_
from returns
group by month_
union
select SUM(Order_Total_Cost) , 'Sales' as type, monthname(Order_Date) as month_
from sales
group by month_
union
select SUM(profit) as profit_ , 'Profit' as type, month_
from(
select sell_price-cost_price as profit , monthname(order_date) month_
from sales
join order_item
on order_item.order_No = sales.order_No
join returns
on returns.order_no = sales.order_No
join supplier
on supplier.Product_ID = order_item.Product_ID
) B group by month_
) A order by month_;
This is showing as below:
387 Return August
182 Sales August
867 Profit August
733 Return July
109 Sales July
646 Profit July
596 Return June
I want it to show with Return, Sales & Profit as separate columns instead of all types listed in one.
Any help would be greatly appreciated.
Thanks
Why not join them?
SELECT x.month_
, x.returns_
, y.sales_
, z.profit_
FROM
(SELECT SUM(Return_Amount) AS returns_
, monthname(Return_Date) AS month_
FROM RETURNS
GROUP BY month_) x
INNER JOIN
(SELECT SUM(Order_Total_Cost) AS sales_
, monthname(Order_Date) AS month_
FROM sales
GROUP BY month_) y ON x.month_ = y.month_
INNER JOIN
(SELECT SUM(profit) AS profit_
, month_
FROM
(SELECT sell_price-cost_price AS profit
, monthname(order_date) month_
FROM sales
INNER JOIN order_item ON order_item.order_No = sales.order_No
INNER JOIN RETURNS ON returns.order_no = sales.order_No
INNER JOIN supplier ON supplier.Product_ID = order_item.Product_ID) B
GROUP BY month_) z ON x.month_ = z.month_
You could turn the three unioned queries to subqueries and join them. This assumes that each subquery always produces one record per month.
Also: you probably want to join and month and year, in case your data spans over multiple years (which eventually happens in any live dataset). I would also recommend using numeric years and month (as returned by year() and month()), which would produce more efficient joins than month names.
select
r.month_name,
r.total_return_amount,
s.total_sales,
p.total_profit
from
(
select
year(return_date) yr,
month(return_date) mh,
monthname(return_date) month_name
sum(return_amount) total_return_amount,
from returns
group by
year(return_date),
month(return_date),
monthname(return_date)
) r
inner join (
select
year(order_date) yr,
month(order_date) mh,
sum(order_total_cost) total_sales
from sales
group by
year(order_date),
month(order_date)
) s on s.yr = r.yr and s.mh = r.mh
inner join (
select
year(order_date) yr,
month(order_date) mh,
sum(sell_price - cost_price) as total_profit ,
from sales
inner join order_item on order_item.order_no = sales.order_no
inner join returns on returns.order_no = sales.order_no
inner join supplier on supplier.product_id = order_item.product_id
group by
year(order_date),
month(order_date)
) p on p.yr = r.yr and p.mh = r.mh

Sum of all values except the first

I have the following three tables:
Customers:
Cust_ID,
Cust_Name
Products:
Prod_ID,
Prod_Price
Orders:
Order_ID,
Cust_ID,
Prod_ID,
Quantity,
Order_Date
How do I display each costumer and how much they spent excluding their very first purchase?
[A] - I can get the total by multiplying Products.Prod_Price and Orders.Quantity, then GROUP by Cust_ID
[B] - I also can get the first purchase by using TOP 1 on Order_Date for each customer.
But I couldnt figure out how to produce [A]-[B] in one query.
Any help will be greatly appreciated.
For SQL-Server 2005, 2008 and 2008R2:
; WITH cte AS
( SELECT
c.Cust_ID, c.Cust_Name,
Amount = o.Quantity * p.Prod_Price,
Rn = ROW_NUMBER() OVER (PARTITION BY c.Cust_ID
ORDER BY o.Order_Date)
FROM
Customers AS c
JOIN
Orders AS o ON o.Cust_ID = c.Cust_ID
JOIN
Products AS p ON p.Prod_ID = o.Prod_ID
)
SELECT
Cust_ID, Cust_Name,
AmountSpent = SUM(Amount)
FROM
cte
WHERE
Rn >= 2
GROUP BY
Cust_ID, Cust_Name ;
For SQL-Server 2012, using the FIRST_VALUE() analytic function:
SELECT DISTINCT
c.Cust_ID, c.Cust_Name,
AmountSpent = SUM(o.Quantity * p.Prod_Price)
OVER (PARTITION BY c.Cust_ID)
- FIRST_VALUE(o.Quantity * p.Prod_Price)
OVER (PARTITION BY c.Cust_ID
ORDER BY o.Order_Date)
FROM
Customers AS c
JOIN
Orders AS o ON o.Cust_ID = c.Cust_ID
JOIN
Products AS p ON p.Prod_ID = o.Prod_ID ;
Another way (that works in 2012 only) using OFFSET FETCH and CROSS APPLY:
SELECT
c.Cust_ID, c.Cust_Name,
AmountSpent = SUM(x.Quantity * x.Prod_Price)
FROM
Customers AS c
CROSS APPLY
( SELECT
o.Quantity, p.Prod_Price
FROM
Orders AS o
JOIN
Products AS p ON p.Prod_ID = o.Prod_ID
WHERE
o.Cust_ID = c.Cust_ID
ORDER BY
o.Order_Date
OFFSET
1 ROW
-- FETCH NEXT -- not needed,
-- 20000000000 ROWS ONLY -- can be removed
) AS x
GROUP BY
c.Cust_ID, c.Cust_Name ;
Tested at SQL-Fiddle
Note that the second solution returns also the customers with only one order (with the Amount as 0) while the other two solutions do not return those customers.
Which version of SQL? If 2012 you might be able to do something interesting with OFFSET 1, but I'd have to ponder much more how that works with grouping.
EDIT: Adding a 2012 specific solution inspired by #ypercube
I wanted to be able to use OFFSET 1 within the WINDOW to it al in one step, but the syntax I want isn't valid:
SUM(o.Quantity * p.Prod_Price) OVER (PARTITION BY c.Cust_ID
ORDER BY o.Order_Date
OFFSET 1)
Instead I can specify the row boxing, but have to filter the result set to the correct set. The query plan is different from #ypercube's, but the both show 50% when run together. They each run twice as as fast as my original answer below.
WITH cte AS (
SELECT c.Cust_ID
,c.Cust_Name
,SUM(o.Quantity * p.Prod_Price) OVER(PARTITION BY c.Cust_ID
ORDER BY o.Order_ID
ROWS BETWEEN 1 FOLLOWING
AND UNBOUNDED FOLLOWING) AmountSpent
,rn = ROW_NUMBER() OVER(PARTITION BY c.Cust_ID ORDER BY o.Order_ID)
FROM Customers AS c
INNER JOIN
Orders AS o ON o.Cust_ID = c.Cust_ID
INNER JOIN
Products AS p ON p.Prod_ID = o.Prod_ID
)
SELECT Cust_ID
,Cust_Name
,ISNULL(AmountSpent ,0) AmountSpent
FROM cte WHERE rn=1
My more general solution is similar to peter.petrov's, but his didn't work "out of the box" on my sample data. That might be an issue with my sample data or not. Differences include use of CTE and a NOT EXISTS with a correlated subquery.
CREATE TABLE Customers (Cust_ID INT, Cust_Name VARCHAR(10))
CREATE TABLE Products (Prod_ID INT, Prod_Price MONEY)
CREATE TABLE Orders (Order_ID INT, Cust_ID INT, Prod_ID INT, Quantity INT, Order_Date DATE)
INSERT INTO Customers SELECT 1 ,'Able'
UNION SELECT 2, 'Bob'
UNION SELECT 3, 'Charlie'
INSERT INTO Products SELECT 1, 10.0
INSERT INTO Orders SELECT 1, 1, 1, 1, GetDate()
UNION SELECT 2, 1, 1, 1, GetDate()
UNION SELECT 3, 1, 1, 1, GetDate()
UNION SELECT 4, 2, 1, 1, GetDate()
UNION SELECT 5, 2, 1, 1, GetDate()
UNION SELECT 6, 3, 1, 1, GetDate()
;WITH CustomersFirstOrder AS (
SELECT Cust_ID
,MIN(Order_ID) Order_ID
FROM Orders
GROUP BY Cust_ID
)
SELECT c.Cust_ID
,c.Cust_Name
,ISNULL(SUM(Quantity * Prod_Price),0) CustomerOrderTotalAfterInitialPurchase
FROM Customers c
LEFT JOIN (
SELECT Cust_ID
,Quantity
,Prod_Price
FROM Orders o
INNER JOIN
Products p ON o.Prod_ID = p.Prod_ID
WHERE NOT EXISTS (SELECT 1 FROM CustomersFirstOrder a WHERE a.Order_ID=o.Order_ID)
) b ON c.Cust_ID = b.Cust_ID
GROUP BY c.Cust_ID
,c.Cust_Name
DROP TABLE Customers
DROP TABLE Products
DROP TABLE Orders
Try this. It should do it.
SELECT c1.cust_name ,
c1.cust_id ,
SUM(p1.Prod_Price)
FROM orders o1
JOIN products p1 ON o1.prod_id = p1.prod_id
JOIN customers c1 ON o1.cust_id = c1.cust_id
LEFT JOIN ( SELECT o2.cust_id ,
MIN(o2.Order_Date) AS Order_Date
FROM orders o2
GROUP BY o2.cust_id
) t ON o1.cust_id = t.cust_id
AND o1.Order_Date = t.Order_Date
WHERE t.Order_Date IS NULL
GROUP BY c1.cust_name ,
c1.cust_id
You have to number orders by Customer and then you can have the amount for the first order and next orders with a CTE and ROW_NUMBER() like this:
; WITH NumberedOrders
AS ( SELECT Customers.Cust_Id ,
Customers.Cust_Name ,
ROW_NUMBER() OVER ( ORDER BY Customers.Cust_id ) AS Order_Number ,
Orders.Order_Date ,
Products.Prod_price * Orders.Quantity AS Amount
FROM Orders
INNER JOIN Customers ON Orders.Cust_Id = Customers.Cust_Id
INNER JOIN Products ON Orders.Prod_Id = Products.Prod_Id
)
SELECT Cust_Id ,
SUM(CASE WHEN Order_Number = 1 THEN Amount
ELSE 0
END) AS A_First_Order ,
SUM(CASE WHEN Order_Number = 1 THEN 0
ELSE Amount
END) AS B_Other_orders ,
SUM(Amount) AS C_All_orders
FROM NumberedOrders
GROUP BY Cust_Id
ORDER BY Cust_Id

How do I write SQL for this scenario

Lets consider the orders table in the Northwind database and I need to get the count of order records for the year 1997 and 1998 in a single query how do I do this?
I tried some thing like this...
select COUNT(o.orderid) as 'Count Of Orders 1997', COUNT(o1.orderid) as 'Count Of Orders 1998'
from orders O
inner join orders o1
on o.orderid = o1.orderid
where year(o.orderdate) = 1997
or year(o1.orderdate) = 1998
Please help me in this...
select COUNT(o.orderid) as 'Count Of years'
from orders O
where year(o.orderdate) = 1997
or year(o1.orderdate) = 1998
group by year(o.orderdate)
SELECT
(SELECT COUNT(*) FROM orders WHERE YEAR(orderdate) = 1997)
AS [Orders 1997],
(SELECT COUNT(*) FROM orders WHERE YEAR(orderdate) = 1998)
AS [Orders 1998]
if you want in one row then you can use pivot function as well
with pivot_table as
(
select orderid, orderdate from orders
)
select * from pivot_table
pivot ( count(orderid) as 'count' for to_char(orderdate,'YYYY') in ('1997' , '1998') )
then it will give output like
1997_count | 1998_count
<count of 1997> <count of 1998>
or else you can do folloing for take output in different different rows
SELECT to_char(o.OrderDate,'YYYY'), COUNT(o.OrderId)
FROM Orders o
WHERE to_char(o.OrderDate,'YYYY') = '1997'
or to_char(o.OrderDate,'YYYY') = '1998'
GROUP BY to_char(o.OrderDate,'YYYY')