Tsql query for periods by buy / sell? - sql-server

An agent buys products from factory and sells them to normal people.
e.g.
he buy apple ( buy) (1/1/2010)
next day he buys again apple ( buy) (2/1/2010)
later he buys orange ( buy) (2/1/2010)
later he sells apple ( sell) (3/1/2010)
later he sells apple ( sell) (20/1/2010)
<=here the cycle of buy/sell for apple has finished.
sells the orange (22/1/2010)
i need Query result that tells me for each cycle - this :>
item | numberOfItems | timeFromBeginingToVanish
------------------------------------------------------
apple 2 19 days // ( 20/1 - 1/1)
orange 1 20 days //(22/1 - 2/1)
notice :
he can buy another apple and not to sell him - this WONT be in the list.
only when the stack is FIRST TO BE FINISHED

Edit: I have added another filter (HAVING clause) to display only those cycles that are completed .
This solution uses recursive CTE:
1) to compute a running total (RunningTotal) for every item and
2) to generate a group id (PseudoDenseRank) for every item cycle.
CREATE TABLE [Transaction]
(
TransactionId INT IDENTITY(10,10) PRIMARY KEY
,Item VARCHAR(100) NOT NULL
,TransactionType CHAR(1) NOT NULL
,Qty INT NOT NULL
,TransactionDate DATE NOT NULL
,CHECK( TransactionType IN ('B', 'S') ) --Buy, Sell
);
INSERT [Transaction]
SELECT 'apple', 'B', 1, '2010-01-01'
UNION ALL
SELECT 'apple', 'B', 1, '2010-01-02'
UNION ALL
SELECT 'orange','B', 1, '2010-01-03'
UNION ALL
SELECT 'apple', 'S', 1, '2010-01-03'
UNION ALL
SELECT 'apple', 'S', 1, '2010-01-20'
UNION ALL
SELECT 'orange','S', 1, '2010-01-22'
UNION ALL
SELECT 'apple', 'B', 2, '2010-02-01'
UNION ALL
SELECT 'orange','B', 3, '2010-02-02'
UNION ALL
SELECT 'apple', 'S', 1, '2010-02-03'
UNION ALL
SELECT 'apple', 'S', 1, '2010-02-10'
UNION ALL
SELECT 'orange','S', 1, '2010-02-10'
UNION ALL
SELECT 'orange','S', 1, '2010-02-11';
DECLARE #Results TABLE
(
TransactionId INT NOT NULL
,Item VARCHAR(100) NOT NULL
,TransactionType CHAR(1) NOT NULL
,Qty INT NOT NULL
,TransactionDate DATE NOT NULL
,RowNum INT NOT NULL
,PRIMARY KEY (Item, RowNum)
);
INSERT #Results
SELECT *
,ROW_NUMBER() OVER(PARTITION BY t.Item ORDER BY t.TransactionDate ASC, t.TransactionType ASC, t.TransactionId ASC) RowNum
FROM [Transaction] t;
WITH CteRecursive
AS
(
SELECT q.Item
,q.RowNum
,CASE WHEN q.TransactionType = 'B' THEN q.Qty END QtyBuy
,CASE WHEN q.TransactionType = 'S' THEN q.Qty END QtySell
,q.TransactionDate
,CASE WHEN q.TransactionType = 'B' THEN q.Qty WHEN q.TransactionType = 'S' THEN -q.Qty END AS RunningTotal
,1 AS PseudoDenseRank
FROM #Results q
WHERE q.RowNum = 1
UNION ALL
SELECT prev.Item
,crt.RowNum
,CASE WHEN crt.TransactionType = 'B' THEN crt.Qty END QtyBuy
,CASE WHEN crt.TransactionType = 'S' THEN crt.Qty END QtySell
,crt.TransactionDate
,prev.RunningTotal + CASE WHEN crt.TransactionType = 'B' THEN crt.Qty WHEN crt.TransactionType = 'S' THEN -crt.Qty END
,CASE WHEN prev.RunningTotal = 0 THEN prev.PseudoDenseRank + 1 ELSE prev.PseudoDenseRank END
FROM CteRecursive prev
INNER JOIN #Results crt ON prev.Item = crt.Item
AND prev.RowNum + 1 = crt.RowNum
)
SELECT q.Item
,q.PseudoDenseRank AS CycleNumber
,SUM(q.QtyBuy) AS QtyBuyTotal
,SUM(q.QtySell) AS QtySellTotal
,CASE WHEN ISNULL(SUM(q.QtyBuy), 0) - ISNULL(SUM(q.QtySell),0) = 0 THEN 'Complete' ELSE 'Incomplete' END AS CycleStatus
,MIN(q.TransactionDate) AS CycleStartDate
,MAX(q.TransactionDate) AS CycleEndDate
,CONVERT(VARCHAR(25), MIN(q.TransactionDate), 112) + ' - ' + CONVERT(VARCHAR(25), MAX(q.TransactionDate), 112) AS CycleInterval
FROM CteRecursive q
GROUP BY q.Item, q.PseudoDenseRank
HAVING ISNULL(SUM(q.QtyBuy), 0) - ISNULL(SUM(q.QtySell),0) = 0
ORDER BY q.Item, q.PseudoDenseRank;
DROP TABLE [Transaction];
Results:
Item CycleNumber QtyBuyTotal QtySellTotal CycleStatus CycleStartDate CycleEndDate CycleInterval
------ ----------- ----------- ------------ ----------- -------------- ------------ -----------------------
apple 1 2 2 Complete 2010-01-01 2010-01-20 2010-01-01 - 2010-01-20
apple 2 2 2 Complete 2010-02-01 2010-02-10 2010-02-01 - 2010-02-10
orange 1 1 1 Complete 2010-01-03 2010-01-22 2010-01-03 - 2010-01-22
--The next row is eliminated by the filter from HAVING
orange 2 3 2 Incomplete 2010-02-02 2010-02-11 2010-02-02 - 2010-02-11

Related

Dynamic Operators in SQL

Let's say I have the following table tbl_Rules:
RuleID NameOperator NameValues TypeOperator TypeValue
1 NotIn John In 2
1 NotIn Alex In NULL
1 NotIn Mike In NULL
2 In Mike NotIn 2
And my source table looks like this tbl_Source:
ID Name Type Cost
1 Mike 2 100
2 Cole 2 200
3 Ken 1 300
4 Tara 1 400
5 Mike 1 500
6 Sonya 1 600
7 Ann 2 700
8 Mike 1 800
I want to be able to join these two tables and get the following result tbl_Result:
RuleID Name Type Cost
1 Cole 2 200
1 Ann 2 700
2 Mike 1 500
2 Mike 1 800
If I was writing this query manually my query would look like this:
select 1, Name, Type, Cost
from tbl_Source
Where Name not in ('John', 'Alex', 'Mike') and Type in (2)
union all
select 2, Name, Type, Cost
from tbl_Source
where Name in ('Mike') and Type not in (2)
In my current setup tbl_Rule has 500 records and tbl_Source has 500k records.
Any advice on this can be achieved is greatly appreciated. Limitations:
No CLR functions, No 2017 features (e.g. String_agg)
Update: DDL statements for the above sample can be found here: http://sqlfiddle.com/#!18/9a29f/2/0
Here's one way. I have used cross join to check the rules. There may be better ways with dynamic SQL where rules are implemented in join
declare #tbl_Rules table(
RuleID int
, NameOperator varchar(20)
, NameValues varchar(20)
, TypeOperator varchar(20)
, TypeValue int
)
insert into #tbl_Rules
values
(1, 'NotIn', 'John', 'In', 2)
, (1, 'NotIn', 'Alex', 'In', NULL)
, (1, 'NotIn', 'Mike', 'In', NULL)
, (2, 'In', 'Mike', 'NotIn', 2)
declare #tbl_Source table (
ID int
, Name varchar(20)
, Type int
, Cost int
)
insert into #tbl_Source
values
(1, 'Mike', 2, 100)
, (2, 'Cole', 2, 200)
, (3, 'Ken', 1, 300)
, (4, 'Tara', 1, 400)
, (5, 'Mike', 1, 500)
, (6, 'Sonya', 1, 600)
, (7, 'Ann', 2, 700)
, (8, 'Mike', 1, 800)
;with cte as (
select
distinct Ruleid, a.NameOperator, a.TypeOperator
, NameValues = (
select
'!' + b.NameValues
from
#tbl_Rules b
where
a.RuleID = b.RuleID
and b.NameValues is not null
for xml path('')
) + '!'
, TypeValue = (
select
concat('!', b.TypeValue)
from
#tbl_Rules b
where
a.RuleID = b.RuleID
and b.TypeValue is not null
for xml path('')
) + '!'
from
#tbl_Rules a
)
select
b.RuleID, a.Name, a.Type, a.Cost
from
#tbl_Source a
cross join cte b
where
1 = case
when b.NameOperator = 'In' and charindex('!' + a.Name + '!', b.NameValues) > 0 and b.TypeOperator = 'In' and charindex(concat('!', a.Type, '!'), b.TypeValue) > 0 then 1
when b.NameOperator = 'In' and charindex('!' + a.Name + '!', b.NameValues) > 0 and b.TypeOperator = 'Notin' and charindex(concat('!', a.Type, '!'), b.TypeValue) = 0 then 1
when b.NameOperator = 'NotIn' and charindex('!' + a.Name + '!', b.NameValues) = 0 and b.TypeOperator = 'In' and charindex(concat('!', a.Type, '!'), b.TypeValue) > 0 then 1
when b.NameOperator = 'NotIn' and charindex('!' + a.Name + '!', b.NameValues) = 0 and b.TypeOperator = 'NotIn' and charindex(concat('!', a.Type, '!'), b.TypeValue) = 0 then 1
else 0
end
Output:
RuleID Name Type Cost
---------------------------
1 Cole 2 200
1 Ann 2 700
2 Mike 1 500
2 Mike 1 800
You can try this.
SELECT MAX(R_N.RuleID) RuleID, S.Name, S.Type, S.Cost
FROM tbl_Source S
INNER JOIN tbl_Rules R_N ON
(R_N.NameValues <> S.Name and R_N.NameOperator = 'NotIn' )
OR (R_N.NameValues = S.Name and R_N.NameOperator = 'In' )
INNER JOIN tbl_Rules R_S ON
R_S.RuleID = R_N.RuleID AND
(R_S.TypeValue <> S.Type and R_S.TypeOperator = 'NotIn' )
OR (R_S.TypeValue = S.Type and R_S.TypeOperator = 'In' )
GROUP BY
S.Name, S.Type, S.Cost
HAVING
MAX(R_N.NameOperator) = MIN(R_N.NameOperator)
AND MAX(R_S.TypeOperator) = MIN(R_S.TypeOperator)
Result:
RuleID Name Type Cost
----------- -------------------- ----------- -----------
1 Ann 2 700
1 Cole 2 200
2 Mike 1 500
2 Mike 1 800

Unique UnPivot SQL

Here is how the data looks like currently
OrderNo, OrderDate, Order_PROD1, Order_Unit1, Order_IP1_Date, Order_PROD2,
1 12/20/2017 17383 894YU 12/23/2017 49348
Order_Unit2, Order_IP2_Date ...... Order_PROD30, Order_Unit30,
489UI 11/12/2015
The way i want to transform is
OrderNo, OrderDate, Order_Prod, Order_Unit, Order_IP_Date
1 12/20/2017 17383 894YU 12/23/2017
1 12/20/2017 49348 489UI 11/12/2015
1 12/20/2017 Order_Prod3* Order_Unit3* Order_IP3_Date*
1 12/20/2017 Order_Prod4* Order_Unit4* Order_IP4_Date*
Order_Prod3* = Value of column Order_Prod3
Order_Prod4* = Value of column Order_Prod4
Here is the query i have so far
select Orderid, OrderDate, Order_Prod, Order_unit, Order_IP_Date
from tbl
unpivot
(
Order_Prod ??????
for Order_Prod in (Order_Prod1, Order_Prod2, Order_Prod3)???
) unpiv;
Not sure how to un-pivot on multiple columns..
A Dynamic version (Notice I added a second record as an illustration)
Declare #YourTable table (OrderNo int,OrderDate date,Order_PROD1 varchar(25),Order_Unit1 varchar(25),Order_IP1_Date date,Order_PROD2 varchar(25),Order_Unit2 varchar(25),Order_IP2_Date date,Order_PROD3 varchar(25),Order_Unit3 varchar(25),Order_IP3_Date date)
Insert Into #YourTable values
(1,'2017-12-20','17383','894YU','2017-12-23','9999','AAA-894YU','2017-12-31','a9999','bAAA-894YU','2017-12-28'),
(2,'2017-12-22','17999','89999','2017-12-27','8888','BBB-894YU','2017-12-29','b8888','bBBB-894YU','2017-12-30')
Declare #XML xml = (Select *,RN=Row_Number() over (Partition By OrderNo Order By OrderNo) from #YourTable for XML RAW)
Select OrderNo
,OrderDate
,OrderRow = Replace(Substring(Item,PatIndex('%[0-9]%',Item),2),'_','')
,Order_Prod = max(case when Item Like 'Order_Prod%' then Value else null end)
,Order_Unit = max(case when Item Like 'Order_Unit%' then Value else null end)
,Order_IP_Date = max(case when Item Like 'Order_IP%' then Value else null end)
From (
Select OrderNo = r.value('#OrderNo','int')
,OrderDate = r.value('#OrderDate','date')
,RN = r.value('#RN','int')
,Item = attr.value('local-name(.)','varchar(100)')
,Value = attr.value('.','varchar(max)')
From #XML.nodes('/row') as A(r)
Cross Apply A.r.nodes('./#*') AS B(attr)
Where attr.value('local-name(.)','varchar(100)') not in ('OrderNo','OrderDate','RN')
) A
Group By OrderNo,OrderDate,RN,Replace(Substring(Item,PatIndex('%[0-9]%',Item),2),'_','')
Returns
OrderNo OrderDate OrderRow Order_Prod Order_Unit Order_IP_Date
1 2017-12-20 1 17383 894YU 2017-12-23
1 2017-12-20 2 9999 AAA-894YU 2017-12-31
1 2017-12-20 3 a9999 bAAA-894YU 2017-12-28
2 2017-12-22 1 17999 89999 2017-12-27
2 2017-12-22 2 8888 BBB-894YU 2017-12-29
2 2017-12-22 3 b8888 bBBB-894YU 2017-12-30
Perhaps a Cross Apply may help here. UnPivot has greater performance, but you will have a little more flexibility.
Declare #YourTable table (OrderNo int,OrderDate date,Order_PROD1 varchar(25),Order_Unit1 varchar(25),Order_IP1_Date date,Order_PROD2 varchar(25),Order_Unit2 varchar(25),Order_IP2_Date date,Order_PROD3 varchar(25),Order_Unit3 varchar(25),Order_IP3_Date date)
Insert Into #YourTable values
(1,'2017-12-20','17383','894YU','2017-12-23','9999','AAA-894YU','2017-12-31','a9999','bAAA-894YU','2017-12-28'),
(2,'2017-12-22','17999','89999','2017-12-27','8888','BBB-894YU','2017-12-29','b8888','bBBB-894YU','2017-12-30')
Select A.OrderNo
,A.OrderDate
,B.*
From #YourTable A
Cross Apply ( Values (1,A.Order_Prod1,A.Order_Unit1,A.Order_IP1_Date)
,(2,A.Order_Prod2,A.Order_Unit2,A.Order_IP2_Date)
,(3,A.Order_Prod3,A.Order_Unit3,A.Order_IP3_Date)
-- ...
--,(30,A.Order_Prod30,A.Order_Unit30,A.Order_IP30_Date)
) B (OrderRow,Order_Prod,Order_Unit,Order_IP_Date)
Returns
OrderNo OrderDate OrderRow Order_Prod Order_Unit Order_IP_Date
1 2017-12-20 1 17383 894YU 2017-12-23
1 2017-12-20 2 9999 AAA-894YU 2017-12-31
1 2017-12-20 3 a9999 bAAA-894YU 2017-12-28
2 2017-12-22 1 17999 89999 2017-12-27
2 2017-12-22 2 8888 BBB-894YU 2017-12-29
2 2017-12-22 3 b8888 bBBB-894YU 2017-12-30

How to pick multiple values from the table and add them dynamically for picking another column

SELECT
AgentID,Seat1,SeatUpdated_1,Seat2,SeatUpdated_2,
Seat3,SeatUpdated_3,nTimesSeatChanged,
DATEDIFF(MS,(F.SeatUpdated_1),(F.SeatUpdated_3)) AvgTime
FROM ##final F
Now I have to pick SeatUpdated_3 in the diff function based on column nTimesSeatChanges.
If it have value 2 for any agent, the selected column should be SeatUpdated_2
From the limited information would look at how you store the information first of all. It looks like you are storing related values across column. It would be simpler to work with if you looked at storing them as rows.
Current:
AgentID Seat1 SeatUpdated_1 Seat2 SeatUpdated_2 Seat3 SeatUpdated_3 nTimesSeatChanged
------- ----- ------------- ----- ------------- ----- ------------- -----------------
1 11 21 01/02/2015 31 01/03/2015 2
TO :
TableKey AgentID Seat SeatUpdated
-------- ------- ---- -----------
1 1 11 01/01/2015
2 1 12 01/02/2015
3 1 13 01/03/2015
4 2 22 02/02/2015
5 2 23 02/03/2015
Then work in simple queries to get the end result. I'm not an expert by any means but this is how I would approach it.
;
--Some sample data
WITH CTE_DATA as
(
SELECT '1' as TableKey, '1' as 'AgentID','11' as'Seat','01/01/2015' as 'SeatUpdated'
UNION
SELECT '2','1','12','01/02/2015'
UNION
SELECT '3','1','13','01/03/2015'
UNION
SELECT '4','2','22','02/02/2015'
UNION
SELECT '5','2','23','02/03/2015'
)
,
--Get Min Seat
CTE_Min AS (
SELECT AgentID
,MIN(Seat) AS min_seat
FROM CTE_DATA
GROUP BY AgentID
)
--Get max seat
,CTE_Max AS (
SELECT AgentID
,MAX(Seat) AS max_seat
FROM CTE_DATA
GROUP BY AgentID
)
--Stick them together
,CTE_Join AS (
SELECT Min.AgentID
,Min.min_seat
,max.max_seat
FROM CTE_Min min
JOIN CTE_Max max ON min.AgentID = max.AgentID
)
--Get the date
,CTE_JoinDate AS (
SELECT j.*
,d1.SeatUpdated AS min_date
,d2.SeatUpdated AS max_date
FROM CTE_Join j
LEFT JOIN CTE_DATA d1 ON j.AgentID = d1.AgentID
AND j.min_seat = d1.Seat
LEFT JOIN CTE_DATA d2 ON j.AgentID = d2.AgentID
AND j.max_seat = d2.Seat
)
--Work out nSeats
,CTE_nSeats AS (
SELECT AgentID
,COUNT(1) AS nSeats
FROM CTE_DATA
GROUP BY AgentID
)
--Final result set
SELECT j.*
,DATEDIFF(DAY, min_date, max_date) AS DIFF_Days
,n.nSeats
FROM CTE_JoinDate j
LEFT JOIN CTE_nSeats n ON j.AgentID = n.AgentID

Moving Median, Mode in T-SQL

I am using SQL Server 2012 and I know it is quite simple to calculate moving averages.
But what I need is to get the mode and the median for a defined window frame like so (with a window of 2 preceding to current row; month unique):
MONTH | CODE | MEDIAN | MODE
1 0 0 0
2 3 1.5 0
3 2 2 0
4 2 2 2
5 2 2 2
6 5 2 2
7 3 3 2
If several values qualify as mode, than pick the first.
I commented my code thoroughly. Read my comments on my Mode calculations and let me know it needs tweaking. Overall, it's a relatively simple query. It just has a lot of ugly subqueries and it has a lot of comments. Check it out:
DECLARE #Table TABLE ([Month] INT,[Code] INT);
INSERT INTO #Table
VALUES (1,0),
(2,3),
(3,2),
(4,2), --Try commenting this out to test my special mode thingymajig
(5,2),
(6,5),
(7,3);
WITH CTE
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY [Month]) row_num,
[Month],
CAST(Code AS FLOAT) Code
FROM #Table
)
SELECT [Month],
Code,
ISNULL((
SELECT CASE
--When there is only one previous value at row_num = 2, find Mean of first two codes
WHEN A.row_num = 2 THEN (LAG(B.code,1) OVER (ORDER BY [Code]) + B.Code)/2.0
--Else find middle code value of current and previous two rows
ELSE B.Code
END
FROM CTE B
--How subquery relates to outer query
WHERE B.row_num BETWEEN A.row_num - 2 AND A.row_num
ORDER BY B.[Code]
--Order by code and offset by 1 so don't select the lowest value, but fetch the one above the lowest value
OFFSET 1 ROW FETCH NEXT 1 ROW ONLY),
0) AS Median,
--I did mode a little different
--Instead of Avg(D.Code) you could list the values because with mode,
--If there's a tie with more than one of each number, you have multiple modes
--Instead of doing that, I simply return the mean of the tied modes
--When there's one, it doesn't change anything.
--If you were to delete the month 4, then your number of Codes 2 and number of Codes 3 would be the same in the last row.
--Proper mode would be 2,3. I instead average them out to be 2.5.
ISNULL((
SELECT AVG(D.Code)
FROM (
SELECT C.Code,
COUNT(*) cnt,
DENSE_RANK() OVER (ORDER BY COUNT(*) DESC) dnse_rank
FROM CTE C
WHERE C.row_num <= A.row_num
GROUP BY C.Code
HAVING COUNT(*) > 1) D
WHERE D.dnse_rank = 1),
0) AS Mode
FROM CTE A
Results:
Month Code Median Mode
----------- ---------------------- ---------------------- ----------------------
1 0 0 0
2 3 1.5 0
3 2 2 0
4 2 2 2
5 2 2 2
6 5 2 2
7 3 3 2
If I understood your requirements correctly, your source table contains MONTH and CODE columns, and you want to calculate MEDIAN and MODE.
The query below calculates MEDIAN and MODE with moving window <= than 3 month ("2 preceding to current row") and returns the results matching your example.
-----------------------------------------------------
--Demo data
-----------------------------------------------------
CREATE TABLE #Data(
[Month] INT NOT NULL,
[Code] INT NOT NULL,
CONSTRAINT [PK_Data] PRIMARY KEY CLUSTERED
(
[Month] ASC
));
INSERT #Data
([Month],[Code])
VALUES
(1,0),
(2,3),
(3,2),
(4,2),
(5,2),
(6,5),
(7,3);
-----------------------------------------------------
--Query
-----------------------------------------------------
DECLARE #PrecedingRowsLimit INT = 2;
WITH [MPos] AS
(
SELECT [R].[Month]
, [RB].[Month] AS [SubId]
, [RB].[Code]
, ROW_NUMBER() OVER(PARTITION BY [R].[Month] ORDER BY [RB].[Code]) AS [RowNumberInPartition]
, CASE
WHEN [R].[Count] % 2 = 1 THEN ([R].[Count] + 1) / 2
ELSE NULL
END AS [MedianPosition]
, CASE
WHEN [R].[Count] % 2 = 0 THEN [R].[Count] / 2
ELSE NULL
END AS [MedianPosition1]
, CASE
WHEN [R].[Count] % 2 = 0 THEN [R].[Count] / 2 + 1
ELSE NULL
END AS [MedianPosition2]
FROM
(
SELECT [RC].[Month]
, [RC].[RowNumber]
, CASE WHEN [RC].[Count] > #PrecedingRowsLimit + 1 THEN #PrecedingRowsLimit + 1 ELSE [RC].[Count] END AS [Count]
FROM
(
SELECT [Month]
, ROW_NUMBER() OVER(ORDER BY [Month]) AS [RowNumber]
, ROW_NUMBER() OVER(ORDER BY [Month]) AS [Count]
FROM #Data
) [RC]
) [R]
INNER JOIN #Data [RB]
ON [R].[Month] >= [RB].[Month]
AND [RB].[Month] >= [R].[RowNumber] - #PrecedingRowsLimit
)
SELECT DISTINCT [M].[Month]
, [ORIG].[Code]
, COALESCE([ME].[Code],([M1].[Code] + [M2].[Code]) / 2.0) AS [Median]
, [MOD].[Mode]
FROM [MPos] [M]
LEFT JOIN [MPOS] [ME]
ON [M].[Month] = [ME].[Month]
AND [M].[MedianPosition] = [ME].[RowNumberInPartition]
LEFT JOIN [MPOS] [M1]
ON [M].[Month] = [M1].[Month]
AND [M].[MedianPosition1] = [M1].[RowNumberInPartition]
LEFT JOIN [MPOS] [M2]
ON [M].[Month] = [M2].[Month]
AND [M].[MedianPosition2] = [M2].[RowNumberInPartition]
INNER JOIN
(
SELECT [MG].[Month]
, FIRST_VALUE([MG].[Code]) OVER (PARTITION BY [MG].[Month] ORDER BY [MG].[Count] DESC , [MG].[SubId] ASC) AS [Mode]
FROM
(
SELECT [Month] , MIN([SubId]) AS [SubId], [Code] , COUNT(1) AS [Count]
FROM [MPOS]
GROUP BY [Month] , [Code]
) [MG]
) [MOD]
ON [M].[Month] = [MOD].[Month]
INNER JOIN #Data [ORIG]
ON [ORIG].[Month] = [M].[Month]
ORDER BY [M].[Month];

Trouble creating SQL Query to update line item amounts

I'm going to preface this question with the disclaimer that creating what I call "complex" queries isn't in my forte. Most of the time, there is a much simpler way to accomplish what I'm trying to accomplish so if the below query isn't up to par, I apologize.
With that said, I have a table that keeps track of Vendor Invoices and Vendor Invoice Items (along with a Vendor Invoice Type and Vendor Invoice Item Type). Our bookkeeper wants a report that simply shows: Vendor | Location | Inv Number | Inv Type | Item Type | Inv Date | Rental Fee | Restock Fee | Shipping Fee | Line Item Cost | Total (Line Item + Fees)
Most of the time, one vendor invoice is one line. However, there are exceptions where a vendor invoice can have many item types, thus creating two rows. Not a big deal EXCEPT the fees (Rental, Restock, Shipping) are attached to the Vendor Invoice table. So, I first created a query that checks the temp table for Invoices that have multiple rows, takes the last row, and zero's out the fees. So that only one line item would have the fee. However, our bookkeeper doesn't like that. Instead, she'd like the fees to be "distributed" among the line items.
So, if a vendor invoice has a $25 shipping charge, has two line items, then each line item would be $12.50.
After working with the query, I got it to update the Last Row to be the adjusted amount but row 1+ would have the original amount.
I'm going to post my entire query here (again - I'm sorry that this may not be the best looking query; however, suggestions are always welcome)
DROP TABLE #tVendorInvoiceReport
DROP TABLE #tSummary
SELECT v.Name AS Vendor ,
vii.Location ,
vi.VendorInvNumber ,
vit.Descr AS InvoiceType ,
vii.VendorInvoiceItemType ,
CONVERT(VARCHAR(10), vi.VendorInvDate, 120) VendorInvDate ,
vi.RentalFee ,
vi.RestockFee ,
vi.ShippingFee ,
SUM(vii.TotalUnitCost) TotalItemCost ,
CONVERT(MONEY, 0) TotalInvoice ,
RowID = IDENTITY( INT,1,1)
INTO #tVendorInvoiceReport
FROM dbo.vVendorInvoiceItems AS vii
JOIN dbo.VendorInvoices AS vi ON vii.VendorInvID = vi.VendorInvID
JOIN dbo.Vendors AS v ON vi.VendorID = v.VendorID
JOIN dbo.VendorInvoiceTypes AS vit ON vi.VendorInvTypeID = vit.VendorInvTypeID
WHERE vi.VendorInvDate >= '2012-01-01'
AND vi.VendorInvDate <= '2012-01-31'
GROUP BY v.Name ,
vii.Location ,
vi.VendorInvNumber ,
vit.Descr ,
vii.VendorInvoiceItemType ,
CONVERT(VARCHAR(10), vi.VendorInvDate, 120) ,
vi.RentalFee ,
vi.RestockFee ,
vi.ShippingFee
ORDER BY v.Name ,
vii.Location ,
vi.VendorInvNumber ,
vit.Descr ,
vii.VendorInvoiceItemType ,
CONVERT(VARCHAR(10), vi.VendorInvDate, 120)
SELECT VendorInvNumber ,
COUNT(RowID) TotalLines ,
MAX(RowID) LastLine
INTO #tSummary
FROM #tVendorInvoiceReport
GROUP BY VendorInvNumber
WHILE ( SELECT COUNT(LastLine)
FROM #tSummary AS ts
WHERE TotalLines > 1
) > 0
BEGIN
DECLARE #LastLine INT
DECLARE #NumItems INT
SET #LastLine = ( SELECT MAX(LastLine)
FROM #tSummary AS ts
WHERE TotalLines > 1
)
SET #NumItems = ( SELECT COUNT(VendorInvNumber)
FROM #tVendorInvoiceReport
WHERE VendorInvNumber IN (
SELECT VendorInvNumber
FROM #tSummary
WHERE LastLine = #LastLine )
)
UPDATE #tVendorInvoiceReport
SET RentalFee = ( RentalFee / #NumItems ) ,
RestockFee = ( RestockFee / #NumItems ) ,
ShippingFee = ( ShippingFee / #NumItems )
WHERE RowID = #LastLine
DELETE FROM #tSummary
WHERE LastLine = #LastLine
--PRINT #NumItems
END
UPDATE #tVendorInvoiceReport
SET TotalInvoice = ( TotalItemCost + RentalFee + RestockFee + ShippingFee )
SELECT Vendor ,
Location ,
VendorInvNumber ,
InvoiceType ,
VendorInvoiceItemType ,
VendorInvDate ,
RentalFee ,
RestockFee ,
ShippingFee ,
TotalItemCost ,
TotalInvoice
FROM #tVendorInvoiceReport AS tvir
I sincerely appreciate anyone who took the time to read this and attempt to point me in the right direction.
Thank you,
Andrew
PS - I did try and remove "WHERE RowID = #LastLine" from the first Update, but that changed the Shipping Fees for the first line with two items to "0.0868" instead of 12.50 ($25/2)
If I understand correctly, you're looking for a way to split something like an invoice shipping fee over one or more invoice items.
I created some sample invoice and invoice item tables shown below and used the
over(partition) clause to split out the shipping per item.
-- sample tables
declare #Invoice table (InvoiceID int, customerID int, Date datetime, ShippingFee float)
declare #InvoiceItem table (InvoiceItemID int identity, InvoiceID int, ItemDesc varchar(50), Quantity float, ItemPrice float)
-- Example 1
insert #Invoice values(1, 800, getdate(), 20);
insert #InvoiceItem values(1, 'Widget', 1, 10.00)
insert #InvoiceItem values(1, 'Wing Nut', 5, 2.00)
insert #InvoiceItem values(1, 'Doodad', 8, 0.50)
insert #InvoiceItem values(1, 'Thingy', 3, 1.00)
-- Example 2
insert #Invoice values(2, 815, getdate(), 15);
insert #InvoiceItem values(2, 'Green Stuff', 10, 1.00)
insert #InvoiceItem values(2, 'Blue Stuff', 10, 1.60)
-- Example 3
insert #Invoice values(3, 789, getdate(), 15);
insert #InvoiceItem values(3, 'Widget', 10, 1.60)
-- query
select
n.InvoiceID,
n.InvoiceItemID,
n.ItemDesc,
n.Quantity,
n.ItemPrice,
ExtendedPrice = n.Quantity * n.ItemPrice,
Shipping = i.ShippingFee / count(n.InvoiceItemID) over(partition by n.InvoiceID)
from #InvoiceItem n
join #Invoice i on i.InvoiceID = n.InvoiceID
Output:
InvoiceID InvoiceItemID ItemDesc Quantity ItemPrice ExtendedPrice Shipping
1 1 Widget 1 10 10 5
1 2 Wing Nut 5 2 10 5
1 3 Doodad 8 0.5 4 5
1 4 Thingy 3 1 3 5
2 5 Green Stuff 10 1 10 7.5
2 6 Blue Stuff 10 1.6 16 7.5
3 7 Widget 10 1.6 16 15

Resources