SQL - Tracking Monthly Sales - sql

I am writing a query to summarize sales by month. My problem is that my query only returns records for months with sales. For example, I am looking over a 15 month range. But for one specific part in the example below only 3 of the 15 months had sales.
I'm hoping to have 15 records show up and the other ones have 0's for sales. The reason I am hoping for the additional records is I want to take the standard deviation of this, and dropping records impacts that calculation.
Sample Code:
SELECT I.PartNumber as PartNumber,
YEAR(O.CreateDate) as CreateDateYear,
MONTH(O.CreateDate) as CreateDateMonth,
COUNT(*) as TotalDemand
FROM OrderDetails OD
INNER JOIN Orders O on O.Id = OD.OrderId
INNER JOIN Items I on I.Id = OD.ItemId
WHERE
O.CreateDate >= '1-1-2016'
AND O.CreateDate <= '3-31-2017'
AND I.PartNumber = '5144831-2'
GROUP BY I.PartNumber, YEAR(O.CreateDate) , MONTH(O.CreateDate);
Sample Current Output:
Part # | Year | Month | Demand
5144831-2 2017 1 1
5144831-2 2017 2 3
5144831-2 2016 3 1
Desired Output:
I would want an additional row such as:
5144831-2 2016 11 0
To show there were no sales in Nov 2016.
I do have a temp table #_date_array2 with the possible months/years, I think I need help incorporating a LEFT JOIN.

If you want to use left join, you would not be able to use it directly with the inner join. You can do the inner join inside the parenthesis and then do the left join outside to avoid messing with the results of left join. Try this:
SELECT Z.PartNumber as PartNumber,
YEAR(O.CreateDate) as CreateDateYear,
MONTH(O.CreateDate) as CreateDateMonth,
COUNT(Z.OrderId) as TotalDemand
FROM Orders O
LEFT JOIN
(
SELECT OrderId, PartNumber
FROM
OrderDetails OD
INNER JOIN Items I ON I.Id = OD.ItemId
AND I.PartNumber = '5144831-2'
) Z
ON O.Id = Z.OrderId
AND O.CreateDate >= '1-1-2016'
AND O.CreateDate <= '3-31-2017'
GROUP BY Z.PartNumber, YEAR(O.CreateDate) , MONTH(O.CreateDate);
To get a count of 0 for months with no order, avoid using count(*) and use count(OrderId) as given above.
Note - You will have to make sure the Orders table has all months and years available i.e. if there is no CreateDate value of, say, November 2016 in the Orders table(left table in the join), the output will also not produce this month's entry.
Edit:
Can you try this:
SELECT Z.PartNumber as PartNumber,
YEAR(O.CreateDate) as CreateDateYear,
MONTH(O.CreateDate) as CreateDateMonth,
COUNT(O.OrderId) as TotalDemand
FROM Orders O
RIGHT JOIN
(
SELECT OrderId, PartNumber
FROM
OrderDetails OD
INNER JOIN Items I ON I.Id = OD.ItemId
AND I.PartNumber = '5144831-2'
) Z
ON O.Id = Z.OrderId
AND O.CreateDate >= '1-1-2016'
AND O.CreateDate <= '3-31-2017'
GROUP BY Z.PartNumber, YEAR(O.CreateDate) , MONTH(O.CreateDate);

Assuming you have sales of something in every month, the simplest solution is to switch to conditional aggregation:
SELECT '5144831-2' as PartNumber,
YEAR(O.CreateDate) as CreateDateYear,
MONTH(O.CreateDate) as CreateDateMonth,
SUM(CASE WHEN I.PartNumber = '5144831-2' THEN 1 ELSE 0 END) as TotalDemand
FROM OrderDetails OD INNER JOIN
Orders O
ON O.Id = OD.OrderId INNER JOIN
Items I
ON I.Id = OD.ItemId
WHERE O.CreateDate >= '2016-01-01' AND
O.CreateDate <= '2017-03-31'
GROUP BY YEAR(O.CreateDate) , MONTH(O.CreateDate);
Note: This is something of a hack for solving the problem. More robust solutions involve generating the dates and using LEFT JOIN (or similar functionality). However, this is often the fastest way to get the result.

based on all of your comments on other posts etc it seems like you have a table that has a date range you want and you want to be able to run the analysis for multiple/all of the part numbers. So the main issue is you will need a cartesian join between your date table and partnumbers that were sold during that time in order to accomplish you "0"s when not sold.
;WITH cteMaxMinDates AS (
SELECT
MinDate = MIN(DATEFROMPARTS(CreateDateYear,CreateDateMonth,1))
,MaxDate = MAX(DATEFROMPARTS(CreateDateYear,CreateDateMonth,1))
FROM
#_date_array2
)
;WITH cteOrderDetails AS (
SELECT
d.CreateDateYear
,d.CreateDateMonth
,I.PartNumber
FROM
#_date_array2 d
INNER JOIN Orders o
ON d.CreateDateMonth = MONTH(o.CreateDate)
AND d.CreateDateYear = YEAR(o.CreateDate)
INNER JOIN OrderDetails od
ON o.Id = od.OrderId
INNER JOIN Items i
ON od.ItemId = i.Id
AND i.PartNumber = '5144831-2'
)
, cteDistinctParts AS (
SELECT DISTINCT PartNumber
FROM
cteOrderDetails
)
SELECT
d.CreateDateYear
,d.CreateDateMonth
,I.PartNumber
,COUNT(od.PartNumber) as TotalDemand
FROM
#_date_array2 d
CROSS JOIN cteDistinctParts p
LEFT JOIN cteOrderDetails od
ON d.CreateDateYear = od.CreateDateYear
AND d.CreateDateMonth = od.CreateDateMonth
AND p.PartNumber = od.PartNumber
GROUP BY
d.CreateDateYear
,d.CreateDateMonth
,I.PartNumber
To get ALL part numbers simply remove AND i.PartNumber = '5144831-2' join condition.

Related

Joining onto NULL if Record does not exist SQL

I have a query that selects customers from a table CustomerDetails, and left joins onto another table (CustomerActivity) to get their last login time and finally left joins onto another table (OpenOrderDetails) to get their last open order (if applicable). I also have a big WHERE clause filtering this data
A customer can only have one record in the OpenOrderDetails table at anytime. My query looks like the following:
SELECT CD.*, H.LastCustomerLoginTime, OD.OrderFiledDate, OD.OrderCompletedDate
FROM CustomerDetails CD
LEFT JOIN CustomerActivity H ON H.CustomerID = CD.CustomerID
LEFT JOIN OpenOrderDetails OD ON CD.CustomerID = OD.CustomerID
WHERE CD.OrderStatus IN (1,2,3)
AND (CustomerType = 1 or (CustomerType = 3 and CustomerActive IN (1,2)))
AND (OD.OrderFiledDate IS NULL OR CD.TimeStamp >= OD.OrderFiledDate)
AND (OD.OrderCompletedDate IS NULL OR CD.TimeStamp <= OD.OrderCompletedDate)
My issue is that this query only returns customer records that have a record in the OpenOrderDetails table. How do I return every customer, and OrderFiledDate/OrderCompletedDate if present, and NULL if a record for that customer does not exist in the OpenOrderDetails table?
When doing a LEFT JOIN, referencing ANY of the right side table columns in the WHERE clause will turn it into an INNER JOIN. To get around this, simply remove any and all predicates that reference "right side" columns from the WHERE clause and move them to the LEFT JOIN condition.
Something along these lines...
SELECT
CD.*,
H.LastCustomerLoginTime,
OD.OrderFiledDate,
OD.OrderCompletedDate
FROM
CustomerDetails CD
LEFT JOIN CustomerActivity H
ON H.CustomerID = CD.CustomerID
LEFT JOIN OpenOrderDetails OD
ON CD.CustomerID = DLECS.CustomerID
AND ( OD.OrderFiledDate IS NULL
OR CD.TimeStamp >= OD.OrderFiledDate
)
AND ( OD.OrderCompletedDate IS NULL
OR CD.TimeStamp <= OD.OrderCompletedDate
)
WHERE
CD.OrderStatus IN ( 1, 2, 3 )
AND (
CustomerType = 1
OR
(
CustomerType = 3
AND CustomerActive IN ( 1, 2 )
)
);
Just move some OpenOrderDetails conditions to join clause
SELECT CD.*, H.LastCustomerLoginTime, OD.OrderFiledDate, OD.OrderCompletedDate
FROM CustomerDetails CD
LEFT JOIN CustomerActivity H ON H.CustomerID = CD.CustomerID
LEFT JOIN OpenOrderDetails OD ON CD.CustomerID = DLECS.CustomerID
AND (OD.OrderFiledDate IS NULL OR CD.TimeStamp >= OD.OrderFiledDate)
AND (OD.OrderCompletedDate IS NULL OR CD.TimeStamp <= OD.OrderCompletedDate)
WHERE CD.OrderStatus IN (1,2,3)
AND (CustomerType = 1 or (CustomerType = 3 and CustomerActive IN (1,2)))

Getting average difference between two dates in minutes

I am trying to get avg receiving time in minutes for each outlet.
I have tow tables Orders and ReceivedOrders.
Orders
OrderID OutletID OrderDate
1 1 2017-04-10 17:04:41.000
ReceivedOrders
ReceivingID OrderID ReceivingDate
1 1 2017-04-10 17:06:31.000
i have tried the below query but its reruns zero as avg receiving time
SQL Query
SELECT Outlets.OutletName , avg(datediff(MM, Orders.OrderDate, ReceivedOrders.ReceivingDate)) as Receive
FROM dbo.Orders INNER JOIN
dbo.Outlets ON dbo.Orders.OutletID = dbo.Outlets.OutletID INNER JOIN
dbo.ReceivedOrders ON dbo.Orders.OrderID = dbo.ReceivedOrders.OrderID
group by dbo.Outlets.OutletName
Output
OutletName Receive
Outlet1 0
Use datediff with millisecond option:
select Outlets.OutletName,
avg(datediff(ms, Orders.OrderDate, ReceivedOrders.ReceivingDate)) / 60000 as Receive
from dbo.Orders
inner join dbo.Outlets on dbo.Orders.OutletID = dbo.Outlets.OutletID
inner join dbo.ReceivedOrders on dbo.Orders.OrderID = dbo.ReceivedOrders.OrderID
group by dbo.Outlets.OutletName
Your code is doing exactly what you are specifying. You are getting the date diff in months, not minutes.
When using date parts, just spell out the full name of the date part:
SELECT ol.OutletName,
avg(datediff(minute, o.OrderDate, ro.ReceivingDate)) as Receive
FROM dbo.Orders o INNER JOIN
dbo.Outlets ol
ON o.OutletID = ol.OutletID INNER JOIN
dbo.ReceivedOrders ro
ONo.OrderID = ro.OrderID
GROUP BY ol.OutletName;
The above counts minute boundaries between two values. You may want parts of minutes, in which case I would use a smaller unit. Milliseconds are definitely an option, but they can overflow pretty easily if the dates are even a few months apart. So, you might really want something more like this:
SELECT ol.OutletName,
avg(datediff(second, o.OrderDate, ro.ReceivingDate)/60.0) as minutesToReceive
FROM dbo.Orders o INNER JOIN
dbo.Outlets ol
ON o.OutletID = ol.OutletID INNER JOIN
dbo.ReceivedOrders ro
ONo.OrderID = ro.OrderID
GROUP BY ol.OutletName;
Note the use of 60.0 rather than 60 to force non-integer arithmetic.

SELECT records with condition that filters the last chronilogical multiple and specific value of a column

I have a joined table that looks like that:
my goal is to filter all records that was created after the last 'active' value inside LineStatusName Column. (the yellow marked rows in the attached image).
here is what i have done so far, it is almost work as desired, but the problem is that the date that returns from the nested select steatment is not the date of the highest chronological datetime value of 'active' and if i try to do ORDER BY Changes.ChangeDateTim in the end of the nested select i get a syntax error:
Conversion failed when converting the nvarchar value '30-9000241' to data type int.
I will be grateful if someone can suggest a better solution to achieve that task or to improve my query.
SELECT Orders.OrderID,LineStatuses.LineStatusName,OrderTypes.OrderTypeName,
Changes.ChangeDateTime,Orders.ProjectNumber,Changes.Comments,Changes.ChangeTypeID
FROM Orders
INNER JOIN Changes ON Changes.ItemID = Orders.OrderID
INNER JOIN LineStatusSettings ON LineStatusSettings.LineStatusSettingID = Changes.NewValue
INNER JOIN LineStatuses ON LineStatuses.LineStatusID= LineStatusSettings.LineStatusID
INNER JOIN OrderTypes ON OrderTypes.OrderTypeID = LineStatusSettings.OrderTypeID
WHERE Orders.OrderID = 194 AND Orders.Deleted=0
AND
Changes.ChangeDateTime > (
SELECT TOP 1 Changes.ChangeDateTime
FROM Orders
INNER JOIN Changes ON Changes.ItemID = Orders.OrderID
INNER JOIN LineStatusSettings ON LineStatusSettings.LineStatusSettingID = Changes.NewValue
INNER JOIN LineStatuses ON LineStatuses.LineStatusID= LineStatusSettings.LineStatusID
INNER JOIN OrderTypes ON OrderTypes.OrderTypeID = LineStatusSettings.OrderTypeID
WHERE LineStatuses.LineStatusName = 'active'
) AND OrderTypes.OrderTypeName NOT IN ('disconnected line')
ORDER BY Changes.ChangeDateTime
Here is one method:
with jt as (
<your query here>
)
select jt.*
from jt
where jt.date > (select max(jt2.date)
from jt jt2
where jt2.orderid = jt.orderid and jt2.linestatusname = 'Active'
);

Selecting Orders with multiple line items but not when one or more line items contains a defined list

I am having trouble with a SQL Server query. I have a couple of tables involved [Order] (I know, not named well) and [Order Entry].
Order Entry is basically a "Line Item" on an Order (so there are one or more per Order). There are various columns in Order Entry, one of which is ItemID (there is only one ItemID per Order Entry). I want a query that returns all rows (Orders) that don't contain one or more Order Entry's with a list of ItemID's defined in a list.
Here is what I have so far:
SELECT DISTINCT
oe.OrderID, StoreID
FROM
OrderEntry oe
INNER JOIN
[order] o ON o.ID = oe.OrderID
AND o.StoreID = oe.StoreID
AND oe.ItemID NOT IN (60, 856, 857, 858, 902, 59, 240, 57, 217, 853, 855, 854, 41)
What I want to do seem similar to this (below) but I can't figure it out:
SELECT all orders with more than one item and check all items status
Help please! (much appreciated)
If I'm understanding you correctly, you want something like this. Starting your query with the Orders table and using a left join will ensure that you get the orders you're looking for. By left joining with a match on ItemID, you can then check for nulls in your where statement to find the orders that don't have those line items.
select distinct o.OrderID, o.StoreID
from Orders o
left join OrderEntry oe on oe.OrderID = o.ID and oe.StoreID = o.StoreID
and oe.ItemID in (60,856,857,858,902,59,240,57,217,853,855,854,41)
where oe.OrderID is null
So to break this down a bit:
"select distinct... from Orders" means "get me everything from orders"
"left join OrderEntry on..." means "get me all OrderEntry records that meet this criteria; if no records meet the criteria, nulls are OK"
"where oe.OrderID is null" means "I only want to see the items in Orders that had no match in the left join"
If we had used an inner join instead, we'd have lost that "nulls are OK" part, so the where clause wouldn't work.
SELECT DISTINCT oe.OrderID, StoreID
FROM OrderEntry oe
WHERE NOT EXISTS (SELECT *
FROM [order] o
WHERE o.ID = oe.OrderID AND
o.StoreID = oe.StoreID AND
oe.ItemID IN ( 60,856,857,858,902,59,240,57,217,853,855,854,41 ))
Is this what you were after?
Try this:
select OrderId, StoreId
from Order O
where o.orderId not in (select OrderId from
OrderEntry d where d.ItemId IN (10,6,7,5) )
Regards
SELECT oe.OrderID, StoreID
FROM OrderEntry oe
INNER JOIN [order] o
ON o.ID = oe.OrderID
AND o.StoreID = oe.StoreID
GROUP BY oe.OrderID, StoreID
AND SUM (CASE WHEN oe.ItemID NOT IN ( 60,856,857,858,902,59,240,57,217,853,855,854,41 )
THEN 1
ELSE 0
END) = 0
I was able to take a few pieces from several of you (Miguel Guzman - you provided the spark I needed to get this) and got it working. Here is my final Query:
SELECT o.ID, o.StoreID
FROM [Order] o
JOIN PSD_ServiceTicket st
ON o.ID = st.WorkOrderID
AND o.StoreID = st.StoreID
WHERE o.StoreID = 101
AND o.Time >= '10/1/2013'
AND o.Time <= '10/18/2013'
AND o.ID NOT IN (SELECT OrderID
FROM OrderEntry oe
INNER JOIN [order] o
ON o.ID = oe.OrderID
AND o.StoreID = oe.StoreID
WHERE oe.StoreID = 101
AND o.Time >= '10/1/2013'
AND o.Time <= '10/18/2013'
AND oe.ItemID IN ( 60,856,857,858,902,59,240,57,217,853,855,854,41 )
)
AND (st.ServiceTypeID = 1 OR st.ServiceTypeID = 4 )
Thanks to everyone!

How could I combine these 3 T-SQL statements into one?

I have 3 queries that I want to combine. 1 query is for total sales, 1 is for canceled orders, and 1 is for orders that don't include specific product types. Just need the total sales $$ to output in a table format as they are now. The only thing that changes between the 3 is the where statement. Thanks!
Edit: I realize I said "Just need the total sales $$" ... what I meant was I just need the sales $$ for each query in one table. So $x, $y, $z ... x is the total sales, y is the sales dollars that got cancelled, and z is the sales dollars for the specific items.
SELECT Sum((Items.Total+Items.Shipping)*OrderDetails.Quantity) AS Total
FROM Promo INNER JOIN (Orders INNER JOIN (Items INNER JOIN OrderDetails ON Items.ItemCode = OrderDetails.ItemCode) ON Orders.OrderNumber = OrderDetails.OrderNumber) ON Promo.Promo = Orders.Promo
WHERE ((Promo.OfferType)='Sale') AND ((Items.Date) Between '6/1/2010' And '12/31/2011');
SELECT Sum((Items.Total+Items.Shipping)*OrderDetails.Quantity) AS Canceled
FROM Promo INNER JOIN (Orders INNER JOIN (Items INNER JOIN OrderDetails ON Items.ItemCode = OrderDetails.ItemCode) ON Orders.OrderNumber = OrderDetails.OrderNumber) ON Promo.Promo = Orders.Promo
WHERE (((Promo.OfferType)='Sale') AND ((Items.Date) Between '6/1/2010' And '12/31/2011') AND ((Items.Status)="Canceled"));
SELECT Sum((Items.Total+Items.Shipping)*OrderDetails.Quantity) AS BadItems
FROM Promo INNER JOIN (Orders INNER JOIN (Items INNER JOIN OrderDetails ON Items.ItemCode = OrderDetails.ItemCode) ON Orders.OrderNumber = OrderDetails.OrderNumber) ON Promo.Promo = Orders.Promo
WHERE (((Promo.OfferType)='Sale') AND ((Items.Date) Between '6/1/2010' And '12/31/2011') AND ((Items.ProductType)<>2) AND ((Items.ProductType)<>6));
Thanks!
If you want the results on 1 row:
SELECT Total, Canceled, BadItems
FROM Query1, Query2, Query3
And if you want the results in 1 column, use a UNION:
Query1
UNION
Query2
UNION
Query3
Updated to reflect question clarification:
SELECT Sum(CASE WHEN Items.Status <> 'Canceled' AND Items.ProductType = 4 THEN (Items.Total+Items.Shipping)*OrderDetails.Quantity ELSE 0 END) AS Total ,
Sum(CASE WHEN Items.Status = 'Canceled' AND Items.ProductType = 4 THEN (Items.Total+Items.Shipping)*OrderDetails.Quantity ELSE 0 END) AS Canceled,
Sum(CASE WHEN Items.ProductType <> 4 THEN (Items.Total+Items.Shipping)*OrderDetails.Quantity ELSE 0 END) AS BadItems,
FROM Promo INNER JOIN (Orders INNER JOIN (Items INNER JOIN OrderDetails ON Items.ItemCode = OrderDetails.ItemCode) ON Orders.OrderNumber = OrderDetails.OrderNumber) ON Promo.Promo = Orders.Promo
WHERE ((Promo.OfferType)='Sale') AND ((Items.Date) Between '9/1/2010' And '12/31/2010');
try to use a procedure which will accept three values as input which will suite your were statements and it will give results in one table as want. if you are stuck let me know , i will help.