How to increment data by week in SQL - sql-server

I am trying to find out the number of stores that had no sales for the last 4 weeks. Because of the way the grid is structured in DevExpress, I need to show the weeks as Wk1, Wk2, Wk3, Wk4 as columns (the data in these columns would be the store count of stores with zero sales).
Right now my data has the Sale Fiscal Week as a row. What I need it to look like is basically like:
Vendor | Store | City | State | Category | Wk1 | Wk2 | Wk3| Wk4
------------------------------------------------------------------------
Prairie Farms | #16141 | Adrian | MI | 2% Gallon | 1 | 0 | 1 | 1
Prairie Farms | #16141 | Adrian | MI | Whole Gallon | 1 | 1 | 0 | 0
instead of
Vendor | Store | City | State | Category | Sale Cal Wk | Sale Fiscal Wk
--------------------------------------------------------------------------
Prairie Farms | #16141 | Adrian | MI | 2% Gallon | 2015-10-23 | 38
Prairie Farms | #16141 | Adrian | MI | Whole Gallon | 2015-10-23 | 38
I have included my code which works so far. I have a count(store) function as 'ZeroStore' in the final select statement to indicate the store as a Zero Sales store.
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF 1=0
BEGIN
SET FMTONLY OFF
END
DECLARE #4WeeksAgo date
DECLARE #Today date
SET #4WeeksAgo = DATEADD(day,-28,getdate())
SET #Today = cast(GETDATE() as date)
DECLARE #maxweek as tinyint
SET #maxweek = case when (SELECT DGFiscalWeek
FROM scans.dbo.DimDate
WHERE CAST(Actualdate as DATE)=dateadd(dd,0,CAST(getdate() as DATE))) = 1 THEN 52 ELSE
(SELECT DGFiscalWeek FROM scans.dbo.DimDate WHERE CAST(Actualdate as DATE)=dateadd(dd,0,CAST(getdate() as DATE)))-1 END
DECLARE #LYear as int
SET #LYear = cast ((select max(FiscalYr) from Scans.dbo.Scans)-1 as varchar(6))
DECLARE #Year as int
SET #Year = #LYear+1
CREATE TABLE #Initialize
(
[Master Vendor] varchar (100),
[Dairy Vendor] varchar (100),
Store float,
City varchar (50),
State varchar(5),
District int,
Category varchar(20),
[Sale Cal Wk] date,
SaleYear int,
[Sale Fiscal Week] tinyint,
Units int,
Sales money,
Grade int
)
INSERT INTO #Initialize
SELECT DISTINCT
d.[Master Vendor], d.[Dairy Vendor], d.[Store],
City, State, District,
'2% Gallon',
CAST(MAX(ActualDate) AS DATE) AS [Sale Cal Wk],
DGFiscalYear,
DGFiscalWeek,
Units = 0,
Sales = 0,
Grade = 0
FROM
Scans.dbo.DGStores s
FULL JOIN
Scans.dbo.DimDate dd ON s.State <> 'XX'
FULL JOIN
Tableau.dbo.DollarGeneralDairyDistributors d ON d.Store = s.Store
WHERE
((DGFiscalYear = #LYear AND DGFiscalWeek >= #maxweek) OR
(DGFiscalYear = #Year AND DGFiscalWeek BETWEEN 1 AND #maxweek))
AND d.Store IN (SELECT Store
FROM DollarGeneralDairyDistributors)
GROUP BY
d.[Master Vendor], d.[Dairy Vendor], District, d.Store,
DGFiscalYear, DGFiscalWeek, City, State
INSERT INTO #Initialize
SELECT DISTINCT
d.[Master Vendor] ,
d.[Dairy Vendor] ,
d.[Store],
City,
[State],
District,
Category = 'Whole Gallon',
cast(max(ActualDate) as DATE) as [Sale Cal Wk],
DGFiscalYear,
DGFiscalWeek,
Units=0,
Sales=0,
Grade=0
FROM
Scans.dbo.DGStores s
FULL JOIN
Scans.dbo.DimDate dd
ON
s.State <> 'XX'
FULL JOIN
Tableau.dbo.DollarGeneralDairyDistributors d
ON
d.Store = s.Store
WHERE
((DGFiscalYear = #LYear AND DGFiscalWeek >= #maxweek) OR (DGFiscalYear = #Year AND DGFiscalWeek BETWEEN 1 AND #maxweek))
AND
d.Store IN
(SELECT Store
FROM DollarGeneralDairyDistributors)
GROUP BY d.[Master Vendor], d.[Dairy Vendor] ,District, d.Store, DGFiscalYear, DGFiscalWeek, City, State
CREATE TABLE #Update
(Store varchar(15),
Category varchar(25),
FiscalYr int,
FiscalWk tinyint,
Units int,
Sales money)
INSERT #Update
SELECT DISTINCT Store, Category, FiscalYr, FiscalWk,
isnull(sum(isnull(SumUnits,0)),0) as Units,
isnull(sum(isnull(SumSales,0)),0) as Sales
FROM scans.dbo.Scans sc
JOIN [Scans].[dbo].DollarGeneralDairyCategory c
ON sc.ItemSku = c.ItemSku
WHERE
FiscalYr >= #LYear
GROUP BY[Store], FiscalYr, FiscalWk, Category
UPDATE #Initialize
SET Units = u.Units,
Sales = u.Sales,
Grade=100
FROM #Update u
WHERE #Initialize.[Sale Fiscal Week] = u.FiscalWk AND #Initialize.SaleYear = u.FiscalYr
AND #Initialize.Store=u.Store AND #Initialize.Category = u.Category
SELECT *
, COUNT(store) AS 'ZeroStore'
FROM #Initialize
GROUP BY [Sale Cal Wk], [Master Vendor], [Dairy Vendor], Store, City, State, District, Category, SaleYear, [Sale Fiscal Week], Units, Sales, Grade
HAVING SUM( Units) = 0 AND [Sale Cal Wk] BETWEEN #4WeeksAgo AND #Today
drop table #Initialize,#Update
END
Thank you very much for any input or help.

Related

Find biggest order by client in Microsoft SQL Server

I am looking for some elegant solution to find preferred channel by client.
As an input we get list of transactions, which contains clientid, date, invoice_id, channel and amount. For every client we need to find preferred channel based on amount.
In case some specific client has 2 channels - outcome should be RANDOM among those channels.
Input data:
Clients ID | Date | Invoice Id | Channel | Amount
-----------+------------+------------+---------+--------
Client #1 | 01-01-2020 | 0000000001 | Retail | 90
Client #1 | 07-01-2020 | 0000000002 | Website | 180
Client #2 | 08-01-2020 | 0000000003 | Retail | 70
Client #2 | 09-01-2020 | 0000000004 | Website | 70
Client #3 | 10-01-2020 | 0000000005 | Retail | 140
Client #4 | 11-01-2020 | 0000000006 | Retail | 70
Client #4 | 13-01-2020 | 0000000007 | Website | 30
Desired output:
Clients ID | Top-Channel
-----------+-----------------
Client #1 | Website >> website 180 > retail 90
Client #2 | Retail >> random choice from Retail and Website
Client #3 | Retail >> retail 140 > website 0
Client #4 | Retail >> retail 70 > website 30
Usually to solve such tasks I do some manipulations with GROUP BY, add a random number which is less than 1, and many other tricks. But most probably, there is a better solution.
This is for Microsoft SQL Server
If you have the totals, then you can use window functions:
select t.*
from (select t.*,
row_number() over (partition by client_id order by amount desc) as seqnum
from t
) t
where seqnum = 1;
If you need to aggregate to get the totals, the same approach works with aggregation:
select t.*
from (select t.client_id, t.channel, sum(amount) as total_amount,
row_number() over (partition by client_id order by sum(amount) desc) as seqnum
from t
group by t.client_id, t.channel
) t
where seqnum = 1;
So keeping your desired output in mind, I wrote the following T-SQL without using group by
declare #clients_id int =1
declare #clients_id_max int = (select max(clients_id) from random)
declare #tab1 table (clients_id int, [Top-Channel] nvarchar(10), amount int)
declare #tab2 table (clients_id int, remarks nvarchar (100))
while #clients_id <= #clients_id_max
begin
if ((select count(*) from random where clients_id =#clients_id) > 1)
begin
insert into #tab2 select top 1 a.clients_id, a.channel +' '+ cast (a.amount as nvarchar(5)) +' ; '+ b.channel +' '+ cast (b.amount as nvarchar(5)) as remarks
from random a, random b where a.clients_id =#clients_id and a.clients_id = b.clients_id and a.channel <> b.channel
order by a.amount desc
end
else
begin
insert into #tab2 select a.clients_id, a.channel +' '+ cast (a.amount as nvarchar(5)) as remarks
from random a, random b where a.clients_id =#clients_id and a.clients_id = b.clients_id
order by a.amount desc
end
insert into #tab1 select top 1 clients_id, Channel as [Top-Channel], amount from random where clients_id = #clients_id order by amount desc
set #clients_id = #clients_id +1
end
select a.clients_id, a.[Top-Channel], b.Remarks from #tab1 a join #tab2 b on a.clients_id = b.clients_id
[Query Output
: https://i.stack.imgur.com/7RzcV.jpg ]
This will work:
select distinct ID,FIRST_VALUE(Channel) over (partition by ID order by amount desc,NEWID())
from Table1

Optimization of SQL Server Query

My problem is that I created a query that takes too long to execute.
City | Department | Employee | Attendance Date | Attendance Status
------------------------------------------------------------------------
C1 | Dept 1 | Emp 1 | 2016-01-01 | ABSENT
C1 | Dept 1 | Emp 2 | 2016-01-01 | LATE
C1 | Dept 2 | Emp 3 | 2016-01-01 | VACANCY
So I want to create a view that contains same data and adds a column that contains the total number of employees (that serves me later in a SSRS project to determine the percentage of each status).
So I created a function that makes simple select filtering by department and date.
and this is the query that uses the function:
SELECT City, Department, Employee, [Attendence Date], [Attendance Status], [Get Department Employees By Date](Department, [Attendence Date]) AS TOTAL
FROM attendenceTable
This is the function:
CREATE FUNCTION [dbo].[Get Department Employees By Date]
(
#deptID int = null,
#date datetime = null
)
RETURNS nvarchar(max)
AS
BEGIN
declare #result int = 0;
select #result = count(*) from attendenceTable where DEPT_ID = #deptID and ATT_DATE_G = #date;
RETURN #result;
END
The problem is that query takes too long (I mean very long time) to execute.
Any Suggestion of optimization?
Your function is a scalar function, which is run once for every row in the result set (~600,000) times, and is a known performance killer. It can be rewritten into an inline table-valued function, or if the logic is not required elsewhere, a simple group, count & join would suffice:
WITH EmployeesPerDeptPerDate
AS ( SELECT DEPT_ID ,
ATT_DATE_G ,
COUNT(DISTINCT Employee) AS EmployeeCount
FROM attendenceTable
GROUP BY DEPT_ID ,
ATT_DATE_G
)
SELECT A.City ,
A.Department ,
A.Employee ,
A.[Attendence Date] ,
A.[Attendance Status] ,
ISNULL(B.EmployeeCount, 0) AS EmployeeCount
FROM attendenceTable AS A
LEFT OUTER JOIN EmployeesPerDeptPerDate AS B ON A.DEPT_ID = B.DEPT_ID
AND A.ATT_DATE_G = B.ATT_DATE_G;

Calculating interest across multiple interest rates

I have a table where I store interest rates, each with a start date where it became applicable. Later-dated entries in the table supersede earlier entries. I have to query this table with a start date, an end date, and an amount. From these values I need to end up with an overall interest amount that takes the different interest rates for the date span into account.
CREATE TABLE [dbo].[Interest_Rates](
[Interest_Rate] [float] NULL,
[Incept_Date] [datetime] NULL
) ON [PRIMARY]
GO
I have four 'bands' of interest rates:
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (10, CAST(N'2001-05-03 11:12:16.000' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (11.5, CAST(N'2014-01-07 10:49:28.433' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (13.5, CAST(N'2016-03-01 00:00:00.000' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (15.5, CAST(N'2016-05-01 00:00:00.000' AS DateTime))
GO
What I'd like to know is whether it's possible to calculate the interest rate for a period of time beginning at a time when the interest rate was, say, 11.5%, and ending at a later time when the interest rate has risen twice to 13.5%, within a single query.
It seems like the interest calculation for each 'band' can be done using the wonderful Suprotim Agarwal's example as follows:
DECLARE #StartDate DateTime
DECLARE #EndDate DateTime
DECLARE #Amount Float
SET #StartDate = '2014-04-22'
SET #EndDate = '2016-04-13'
SET #Amount = 150000.00
SELECT
#Amount*(POWER(1.1550, CONVERT(NUMERIC(8,3),
DATEDIFF(d, #StartDate, #EndDate)/365.25))) - #Amount
as TotalInterest
(Interest rate at 15.5% in above example)
Where I'm getting stuck is at working out how to interrelate the calculation with the Interest Rates table such that the join takes into account which 'band' each subsection of the date span falls into.
Any help or advice would be much appreciated.
tl;dr: the completed query is the last code block at the end of this long explanation.
Let's walk through this step-by-step and then present the final solution as one query. A few steps are needed to solve this problem.
1) Figure out which rates our desired date range covers
2) Devise a clever way to choose those rates
3) Combine those dates and rates in such a way to give us that total interest accrued.
Some Preliminary Notes
Since your example calculation of interest rate considers days as its finest resolution, I just use datatypes date instead of datetime. If you need a finer resolution, let me know and I can update.
I'm using the following declared variables
declare #EndOfTime date = '2049-12-31' -- This is some arbitrary end of time value that I chose
declare #StartDate Date = '2012-04-22' -- I made this earlier to cover more rates
declare #EndDate Date = '2016-04-13'
declare #Amount Float = 100000.00 -- I changed it to a softer number
1) Date Intervals
Right now, your interest_rates table lists dates like this:
+ ------------- + ----------- +
| interest_rate | incept_date |
+ ------------- + ----------- +
| 10 | 2001-05-03 |
| 11.5 | 2014-01-07 |
| 13.5 | 2016-03-01 |
| 15.5 | 2016-05-01 |
+ ------------- + ----------- +
But you want it to list intervals like this:
+ ------------- + ------------ + ------------ +
| interest_rate | inter_begin | inter_end |
+ ------------- + ------------ + ------------ +
| 10 | 2001-05-03 | 2014-01-06 |
| 11.5 | 2014-01-07 | 2016-02-29 |
| 13.5 | 2016-03-01 | 2016-04-30 |
| 15.5 | 2016-05-01 | 2049-12-31 |
+ ------------- + ------------ + ------------ +
The following query can turn your date list into intervals:
select i1.interest_rate
, i1.incept_date as inter_begin
, isnull(min(i2.incept_date) - 1,#EndOfTime) as inter_end
from #interest i1
left join #interest i2 on i2.incept_date > i1.incept_date
group by i1.interest_rate, i1.incept_date
Note: I'm playing a bit loose with the date arithmetic here without using the dateadd() command.
Keeping track of the date intervals like this makes selecting the applicable rates much easier.
2) Choosing the Rates
Now we can select records that sit within our desired range by using the above query as a CTE. This query is a little tricky, so take some time to really understand it.
; with
intervals as (
-- The above query/table
)
select *
from intervals
where inter_begin >= (
select inter_begin -- selects the first rate covered by our desired interval
from intervals
where #StartDate between inter_begin and inter_end
)
and inter_end <= (
select inter_end -- selects the last rate covered by our desired interval
from intervals
where #EndDate between inter_begin and inter_end
)
This effectively filters out any rates we don't care about and leaves us with
+ ------------- + ------------ + ------------ +
| interest_rate | inter_begin | inter_end |
+ ------------- + ------------ + ------------ +
| 10 | 2001-05-03 | 2014-01-06 |
| 11.5 | 2014-01-07 | 2016-02-29 |
| 13.5 | 2016-03-01 | 2016-04-30 |
+ ------------- + ------------ + ------------ +
3) Calculate the Interest
Now we have everything we need, and calculating the interest is just a matter selecting the right things from this table. Most of what you wrote for your calculation remains the same; the main changes are in the datediff() command. Using #StartDate and #EndDate won't give us an accurate count of the days spent at each specific rate. We run into the same problem by using inter_begin and inter_end. Instead, we must use a case statement, something like
datediff(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end
)
Put this in the above Query to get
; with
intervals as (...) -- same as above
select *
, DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end) as days_active
, #Amount*(POWER((1+interest_rate/100),
convert(float,
DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end
)
)/365.25)
) - #Amount as Actual_Interest
from ... -- same as above
which gives us this table
+ ------------- + ------------ + ------------ + ----------- + --------------- +
| interest_rate | inter_begin | inter_end | days_active | Actual_interest |
+ ------------- + ------------ + ------------ + ----------- + --------------- +
| 10 | 2001-05-03 | 2014-01-06 | 624 | 17683.63 |
| 11.5 | 2014-01-07 | 2016-02-29 | 786 | 26283.00 |
| 13.5 | 2016-03-01 | 2016-04-30 | 43 | 1501.98 |
+ ------------- + ------------ + ------------ + ----------- + --------------- +
Finally, put this in a CTE and take the sum of the Actual_interest field:
declare #EndOfTime date = '2049-12-31' -- This is some arbitrary end of time value that I chose
declare #StartDate Date = '2012-04-22' -- I made this earlier to cover more rates
declare #EndDate Date = '2016-04-13'
declare #Amount Float = 100000.00 -- I changed it to a softer number
; with
intervals as (
select i1.interest_rate
, i1.incept_date as inter_begin
, isnull(min(i2.incept_date) - 1,#EndOfTime) as inter_end
from #interest i1
left join #interest i2 on i2.incept_date > i1.incept_date
group by i1.interest_rate, i1.incept_date
)
, interest as (
select *
, DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end) as days_active
, #Amount*(POWER((1+interest_rate/100),
convert(float,
DATEDIFF(day,
case when #StartDate > inter_begin then #StartDate else inter_begin end,
case when #EndDate < inter_end then #EndDate else inter_end end
)
)/365.25)
) - #Amount as Actual_Interest
from intervals
where inter_begin >= (
select inter_begin -- selects the first rate covered by our desired interval
from intervals
where #StartDate between inter_begin and inter_end
)
and inter_end <= (
select inter_end -- selects the last rate covered by our desired interval
from intervals
where #EndDate between inter_begin and inter_end
)
)
select sum(actual_interest) as total_interest
from interest
Perhaps a little more than you were looking for, but in this example, you can calculate all loans in one query.
You may also notice the last 3 columns which represent Total Number of Days, Total Interest Earned and the Total Weighted Average Interest Rate
Example
Declare #Interest_Rate table (interest_rate money,Incept_Date datetime)
Insert Into #Interest_Rate values
(10 ,'2001-05-03 11:12:16.000'),
(11.5,'2014-01-07 10:49:28.433'),
(13.5,'2016-03-01 00:00:00.000'),
(15.5,'2016-05-01 00:00:00.000')
Declare #Loan table (Id int,StartDate date, EndDate date,Amount money)
Insert Into #Loan values
(1,'2014-01-01','2015-11-17',150000),
(1,'2015-11-18','2016-12-31',175000), -- Notice Balance Change
(2,'2016-01-01','2020-06-15',200000)
Select A.ID
,A.Amount
,DateR1 = min(D)
,DateR2 = max(D)
,Days = count(*)
,B.Interest_Rate
,Interest_Earned = cast(sum(((A.Amount*B.Interest_Rate)/B.DIY)/100.0) as decimal(18,2))
,Total_Days = sum(count(*)) over (Partition By A.ID)
,Total_Int_Earned = sum(cast(sum(((A.Amount*B.Interest_Rate)/B.DIY)/100.0) as decimal(18,2))) over (Partition By A.ID)
,Total_WAIR = sum(A.Amount * count(*) * B.interest_rate) over (Partition By A.ID)/ sum(A.Amount * count(*)) over (Partition By A.ID)
From #Loan A
Join (
Select D
,D1
,interest_rate
,DIY = 365.0 + IIF(Year(D) % 4 = 0 , 1 , 0 )
From ( Select Top (DateDiff(DD,(Select cast(min(Incept_Date) as date) from #Interest_Rate),cast(GetDate() as date))+1) D=DateAdd(DD,-1+Row_Number() Over (Order By (Select NULL)),(Select cast(min(Incept_Date) as date) from #Interest_Rate)) From master..spt_values N1,master..spt_values N2 ) A
Join (
Select interest_rate
,D1 = cast(Incept_Date as Date)
,D2 = cast(DateAdd(DAY,-1,Lead(Incept_Date,1,GetDate()) over (Order by Incept_Date)) as date)
From #Interest_Rate
) B on D between D1 and D2
) B on D Between StartDate and EndDate
Group By A.ID,A.Amount,B.D1,B.Interest_Rate
Returns

Sequential SQL inserts when triggered by CROSS APPLY

This process has several steps which are reflected in various tables of a database:
Production --> UPDATE to the inventory table using something like
UPDATE STOR SET
STOR.BLOC1 = T.BLOC1,
STOR.BLOC2 = T.BLOC2,
STOR.BLOC3 = T.BLOC3,
STOR.PRODUCTION = T.PROD,
STOR.DELTA = T.DELTA
FROM BLDG B INNER JOIN STOR S
ON S.B_ID = B.B_ID
CROSS APPLY dbo.INVENTORIZE(B.B_ID) AS T;
The above feeds a log table with a TRIGGER like this:
CREATE TRIGGER trgrCYCLE
ON STOR
FOR UPDATE
AS
INSERT INTO dbo.INVT
(TS, BLDG, PROD, ACT, VAL)
SELECT CURRENT_TIMESTAMP, B_ID, PRODUCTION,
CASE WHEN DELTA < 0 THEN 'SELL' ELSE 'BUY' END,
DELTA
FROM inserted WHERE COALESCE(DELTA,0) <> 0
And finally, every update should INSERT a row into a financials table which I added to the TRIGGER above:
INSERT INTO dbo.FINS
(COMPANY, TS, COST2, BAL)
SELECT CORP, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
The problem is with this line:
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
which is meant to calculate the latest balance of an account. But because the CROSS APPLY treats all the INSERTS as a batch, the calculation is done off of the same last record and I get an incorrect balance figure. Example:
COST BALANCE
----------------
1,000 <-- initial balance
-150 850
-220 780 <-- should be 630
What would be the way to solve that? A trigger on the FINS table instead for the balance calculation?
Understanding existing logic in your query
UPDATE statement will fire a trigger only once for a set or batch satisfying join conditions, Inserted statement will have all the records that are being updated. This is because of BATCH processing not because of CROSS APPLY but because of UPDATE.
In this query of yours
SELECT CORP, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
For each CORP from an Outer query, same BAL will be returned.
(SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)
That being said, your inner query will be replaced by 1000(value you used in your example) every time CORP = 'XYZ'
SELECT CORP, CURRENT_TIMESTAMP, COST, (1000- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
Now your inserted statement has all the records that are being inserted. So every record's cost will be subtracted by 1000. Hence you are getting unexpected result.
Suggested solution
As per my understanding, you want to calculate some cumulative frequency kind of thing. Or last running total
Data Preparation for problem statement. Used my dummy data to give you an idea.
--Sort data based on timestamp in desc order
SELECT PK_LoginId AS Bal, FK_RoleId AS Cost, AddedDate AS TS
, ROW_NUMBER() OVER (ORDER BY AddedDate DESC) AS Rno
INTO ##tmp
FROM dbo.M_Login WHERE AddedDate IS NOT NULL
--Check how data looks
SELECT Bal, Cost, Rno, TS FROM ##tmp
--Considering ##tmp as your inserted table,
--I just added Row_Number to apply Top 1 Order by desc logic
+-----+------+-----+-------------------------+
| Bal | Cost | Rno | TS |
+-----+------+-----+-------------------------+
| 172 | 10 | 1 | 2012-12-05 08:16:28.767 |
| 171 | 10 | 2 | 2012-12-04 14:36:36.483 |
| 169 | 12 | 3 | 2012-12-04 14:34:36.173 |
| 168 | 12 | 4 | 2012-12-04 14:33:37.127 |
| 167 | 10 | 5 | 2012-12-04 14:31:21.593 |
| 166 | 15 | 6 | 2012-12-04 14:30:36.360 |
+-----+------+-----+-------------------------+
Alternative logic for subtracting cost from last running balance.
--Start a recursive query to subtract balance based on cost
;WITH cte(Bal, Cost, Rno)
AS
(
SELECT t.Bal, 0, t.Rno FROM ##tmp t WHERE t.Rno = 1
UNION ALL
SELECT c.Bal - t.Cost, t.Cost, t.Rno FROM ##tmp t
INNER JOIN cte c ON t.RNo - 1 = c.Rno
)
SELECT * INTO ##Fin FROM cte;
SELECT * FROM ##Fin
Output
+-----+------+-----+
| Bal | Cost | Rno |
+-----+------+-----+
| 172 | 0 | 1 |
| 162 | 10 | 2 |
| 150 | 12 | 3 |
| 138 | 12 | 4 |
| 128 | 10 | 5 |
| 113 | 15 | 6 |
+-----+------+-----+
You have to tweet your columns little bit to get this functionality into your trigger.
I think you can try a trigger on the Fins.
You can use IDENT_CURRENT('Table')) to take the last primary key from the table and make a select.
I think it's better than "select top 1".
To to take the last balance value, set a variable last_bal = select bal from FINS where primary_key = Ident_Current("FINS")
well
first sql is a game where it work with groups or rather "set" so always you have think about that.
if you work with a simple item is correct, it maybe be better approach
declare #myinsert table(id int identity(1,1), company VArchar(35), ts datetime, cost2 smallmoney, bal smallmoney)
insert into #myinsert(company,ts, cost2, bal)
SELECT CORP, CURRENT_TIMESTAMP, COST,
FROM inserted WHERE COALESCE(COST,0) <> 0
declare #current int
select #current = min(id) from #myinsert
while exists(select * from #myinsert where id = #current)
begin
INSERT INTO dbo.FINS
(COMPANY, TS, COST2, BAL)
SELECT COMPANY, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = my.COMPANY ORDER BY TS DESC)- COST)
from #myinsert my where id = #current
select #current = min(id) from #myinsert where id > #current
end
i am not giving you exact query .For a moment forget trigger.Because you are unable to test your query .
I suggest to use Output clause .This will atleast help you to construct proper query and test it.
this query is running ok,(if you can use merge then that is best).
Declare #t table
(
BLOC1,BLOC2,BLOC3 ,PRODUCTION ,DELTA --whatever column is require here
)
UPDATE STOR SET
STOR.BLOC1 = T.BLOC1,
STOR.BLOC2 = T.BLOC2,
STOR.BLOC3 = T.BLOC3,
STOR.PRODUCTION = T.PROD,
STOR.DELTA = T.DELTA
Output inserted.BLOC1 ,inserted.BLOC2, and so on into #t
FROM BLDG B INNER JOIN STOR S
ON S.B_ID = B.B_ID
CROSS APPLY dbo.INVENTORIZE(B.B_ID) AS T;
now you have inserted value in table variable #t
SELECT CORP, CURRENT_TIMESTAMP, COST,
BAL,Row_Number() over(partition by company order by TS desc) RN
FROM #t inner join FINS on COMPANY = CORP
WHERE COALESCE(COST,0) <> 0
Verify this query till here.Think of optimizing or trigger later on.
I think i gave good suggestion.and I guess subtraction is not a problem.I am telling to put everything in output clause and analyze the query and test it.
you can use CTE inside trigger also but how will you test it.
;With CTE as
(
SELECT CORP, CURRENT_TIMESTAMP, COST,BAL
ROW_NUMBER()over(ORDER BY TS DESC )rn
FROM inserted
inner join FINS on COMPANY = CORP
WHERE COALESCE(COST,0) <> 0
)
select * from CTE --check this what you are getting
Something like that, Isn't complete.
CREATE TRIGGER trgrCYCLE
ON STOR
FOR UPDATE
AS
begin
declare #last_bal int
declare #company varchar(50)
declare #ts --type
declare #cost int
declare #bal --type
--etc whatever you need
select #company = company, #ts= ts , #cost = cost , #bal = bal from INSERTED
--others selects and sets
set #last_bal = select bal from dbo.FINS where you_primary_key = IDENT_CURRENT('FINS'))
set #last_bal = #last_bal - #cost
Insert INTO FINS (company, ts, cost2, bal) VALUES (#company, #ts, #cost, #last_bal) where --your conditions
end
If, similar to #Shantanu's method, you could associate a sequence with inserted, the virtual table associated with the trigger you could do this by subtracting all the COSTs that come before the current record.
This could be accomplished by adding a rowversion to STOR, which will be updated automatically with each delete.
Then instead of:
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
from inserted ...
make the rowversion RV, and:
(SELECT SUM(X.B) FROM
(SELECT TOP 1 BAL B
FROM FINS
WHERE COMPANY = CORP
ORDER BY TS DESC
UNION
SELECT -COST B
FROM inserted ii
WHERE ii.RV >= i.RV AND ii.CORP = i.CORP
) AS X)
FROM inserted i WHERE COALESCE(COST,0) <> 0
Should do what you want. You could conceivably do this with a timestamp that was more find-grained than CURRENT_TIMESTAMP which, I believe, goes down only to seconds but that requires you update it in the UPDATE statement. The rowversion may cause problems with your STOR insert statements.

islands and gaps tsql

I have been struggling with a problem that should be pretty simple actually but after a full week of reading, googling, experimenting and so on, my colleague and we cannot find the proper solution. :(
The problem: We have a table with two values:
an employeenumber (P_ID, int) <--- identification of employee
a date (starttime, datetime) <--- time employee checked in
We need to know what periods each employee has been working.
When two dates are less then #gap days apart, they belong to the same period
For each employee there can be multiple records for any given day but I just need to know which dates he worked, I am not interested in the time part
As soon as there is a gap > #gap days, the next date is considered the start of a new range
A range is at least 1 day (example: 21-9-2011 | 21-09-2011) but has no maximum length. (An employee checking in every #gap - 1 days should result in a period from the first day he checked in until today)
What we think we need are the islands in this table where the gap in days is greater than #variable (#gap = 30 means 30 days)
So an example:
SOURCETABLE:
P_ID | starttime
------|------------------
12121 | 24-03-2009 7:30
12121 | 24-03-2009 14:25
12345 | 27-06-2011 10:00
99999 | 01-05-2012 4:50
12345 | 27-06-2011 10:30
12345 | 28-06-2011 11:00
98765 | 13-04-2012 10:00
12345 | 21-07-2011 9:00
99999 | 03-05-2012 23:15
12345 | 21-09-2011 12:00
45454 | 12-07-2010 8:00
12345 | 21-09-2011 17:00
99999 | 06-05-2012 11:05
99999 | 20-05-2012 12:45
98765 | 26-04-2012 16:00
12345 | 07-07-2012 14:00
99999 | 01-06-2012 13:55
12345 | 13-08-2012 13:00
Now what I need as a result is:
PERIODS:
P_ID | Start | End
-------------------------------
12121 | 24-03-2009 | 24-03-2009
12345 | 27-06-2012 | 21-07-2012
12345 | 21-09-2012 | 21-09-2012
12345 | 07-07-2012 | (today) OR 13-08-2012 <-- (less than #gap days ago) OR (last date in table)
45454 | 12-07-2010 | 12-07-2010
45454 | 17-06-2012 | 17-06-2012
98765 | 13-04-2012 | 26-04-2012
99999 | 01-05-2012 | 01-06-2012
I hope this is clear this way, I already thank you for reading this far, it would be great if you could contribute!
I've done a rough script that should get you started. Haven't bothered refining the datetimes and the endpoint comparisons might need tweaking.
select
P_ID,
src.starttime,
endtime = case when src.starttime <> lst.starttime or lst.starttime < DATEADD(dd,-1 * #gap,GETDATE()) then lst.starttime else GETDATE() end,
frst.starttime,
lst.starttime
from #SOURCETABLE src
outer apply (select starttime = MIN(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and sub.starttime > DATEADD(dd,-1 * #gap,src.starttime)) frst
outer apply (select starttime = MAX(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and src.starttime > DATEADD(dd,-1 * #gap,sub.starttime)) lst
where src.starttime = frst.starttime
order by P_ID, src.starttime
I get the following output, which is a litle different to yours, but I think its ok:
P_ID starttime endtime starttime starttime
----------- ----------------------- ----------------------- ----------------------- -----------------------
12121 2009-03-24 07:30:00.000 2009-03-24 14:25:00.000 2009-03-24 07:30:00.000 2009-03-24 14:25:00.000
12345 2011-06-27 10:00:00.000 2011-07-21 09:00:00.000 2011-06-27 10:00:00.000 2011-07-21 09:00:00.000
12345 2011-09-21 12:00:00.000 2011-09-21 17:00:00.000 2011-09-21 12:00:00.000 2011-09-21 17:00:00.000
12345 2012-07-07 14:00:00.000 2012-07-07 14:00:00.000 2012-07-07 14:00:00.000 2012-07-07 14:00:00.000
12345 2012-08-13 13:00:00.000 2012-08-16 11:23:25.787 2012-08-13 13:00:00.000 2012-08-13 13:00:00.000
45454 2010-07-12 08:00:00.000 2010-07-12 08:00:00.000 2010-07-12 08:00:00.000 2010-07-12 08:00:00.000
98765 2012-04-13 10:00:00.000 2012-04-26 16:00:00.000 2012-04-13 10:00:00.000 2012-04-26 16:00:00.000
The last two output cols are the results of the outer apply sections, and are just there for debugging.
This is based on the following setup:
declare #gap int
set #gap = 30
set dateformat dmy
-----P_ID----|----starttime----
declare #SOURCETABLE table (P_ID int, starttime datetime)
insert #SourceTable values
(12121,'24-03-2009 7:30'),
(12121,'24-03-2009 14:25'),
(12345,'27-06-2011 10:00'),
(12345,'27-06-2011 10:30'),
(12345,'28-06-2011 11:00'),
(98765,'13-04-2012 10:00'),
(12345,'21-07-2011 9:00'),
(12345,'21-09-2011 12:00'),
(45454,'12-07-2010 8:00'),
(12345,'21-09-2011 17:00'),
(98765,'26-04-2012 16:00'),
(12345,'07-07-2012 14:00'),
(12345,'13-08-2012 13:00')
UPDATE: Slight rethink. Now uses a CTE to work out the gaps forwards and backwards from each item, then aggregates those:
--Get the gap between each starttime and the next and prev (use 999 to indicate non-closed intervals)
;WITH CTE_Gaps As (
select
p_id,
src.starttime,
nextgap = coalesce(DATEDIFF(dd,src.starttime,nxt.starttime),999), --Gap to the next entry
prevgap = coalesce(DATEDIFF(dd,prv.starttime,src.starttime),999), --Gap to the previous entry
isold = case when DATEDIFF(dd,src.starttime,getdate()) > #gap then 1 else 0 end --Is starttime more than gap days ago?
from
#SOURCETABLE src
cross apply (select starttime = MIN(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and sub.starttime > src.starttime) nxt
cross apply (select starttime = max(starttime) from #SOURCETABLE sub where src.p_id = sub.p_id and sub.starttime < src.starttime) prv
)
--select * from CTE_Gaps
select
p_id,
starttime = min(gap.starttime),
endtime = nxt.starttime
from
CTE_Gaps gap
--Find the next starttime where its gap to the next > #gap
cross apply (select starttime = MIN(sub.starttime) from CTE_Gaps sub where gap.p_id = sub.p_id and sub.starttime >= gap.starttime and sub.nextgap > #gap) nxt
group by P_ID, nxt.starttime
order by P_ID, nxt.starttime
Jon most definitively has shown us the right direction. Performance was horrible though (4million+ records in the database). And it looked like we were missing some information. With all that we learned from you we came up with the solution below. It uses elements of all the proposed answers and cycles through 3 temptables before finally spewing results but performance is good enough, as well as the data it generates.
declare #gap int
declare #Employee_id int
set #gap = 30
set dateformat dmy
--------------------------------------------------------------- #temp1 --------------------------------------------------
CREATE TABLE #temp1 ( EmployeeID int, starttime date)
INSERT INTO #temp1 ( EmployeeID, starttime)
select distinct ck.Employee_id,
cast(ck.starttime as date)
from SERVER1.DB1.dbo.checkins pd
inner join SERVER1.DB1.dbo.Team t on ck.team_id = t.id
where t.productive = 1
--------------------------------------------------------------- #temp2 --------------------------------------------------
create table #temp2 (ROWNR int, Employeeid int, ENDOFCHECKIN datetime, FIRSTCHECKIN datetime)
INSERT INTO #temp2
select Row_number() OVER (partition by EmployeeID ORDER BY t.prev) + 1 as ROWNR,
EmployeeID,
DATEADD(DAY, 1, t.Prev) AS start_gap,
DATEADD(DAY, 0, t.next) AS end_gap
from
(
select a.EmployeeID,
a.starttime as Prev,
(
select min(b.starttime)
from #temp1 as b
where starttime > a.starttime and b.EmployeeID = a.EmployeeID
) as Next
from #temp1 as a) as t
where datediff(day, prev, next ) > 30
group by EmployeeID,
t.Prev,
t.next
union -- add first known date for Employee
select 1 as ROWNR,
EmployeeID,
NULL,
min(starttime)
from #temp1 ct
group by ct.EmployeeID
--------------------------------------------------------------- #temp3 --------------------------------------------------
create table #temp3 (ROWNR int, Employeeid int, ENDOFCHECKIN datetime, STARTOFCHECKIN datetime)
INSERT INTO #temp3
select ROWNR,
Employeeid,
ENDOFCHECKIN,
FIRSTCHECKIN
from #temp2
union -- add last known date for Employee
select (select count(*) from #temp2 b where Employeeid = ct.Employeeid)+1 as ROWNR,
ct.Employeeid,
(select dateadd(d,1,max(starttime)) from #temp1 c where Employeeid = ct.Employeeid),
NULL
from #temp2 ct
group by ct.EmployeeID
---------------------------------------finally check our data-------------------------------------------------
select a1.Employeeid,
a1.STARTOFCHECKIN as STARTOFCHECKIN,
ENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN a1.ENDOFCHECKIN ELSE b1.ENDOFCHECKIN END,
year(a1.STARTOFCHECKIN) as JaarSTARTOFCHECKIN,
JaarENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN year(a1.ENDOFCHECKIN) ELSE year(b1.ENDOFCHECKIN) END,
Month(a1.STARTOFCHECKIN) as MaandSTARTOFCHECKIN,
MaandENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN month(a1.ENDOFCHECKIN) ELSE month(b1.ENDOFCHECKIN) END,
(year(a1.STARTOFCHECKIN)*100)+month(a1.STARTOFCHECKIN) as JaarMaandSTARTOFCHECKIN,
JaarMaandENDOFCHECKIN = CASE WHEN b1.ENDOFCHECKIN <= a1.STARTOFCHECKIN THEN (year(a1.ENDOFCHECKIN)*100)+month(a1.STARTOFCHECKIN) ELSE (year(b1.ENDOFCHECKIN)*100)+month(b1.ENDOFCHECKIN) END,
datediff(M,a1.STARTOFCHECKIN,b1.ENDOFCHECKIN) as MONTHSCHECKEDIN
from #temp3 a1
full outer join #temp3 b1 on a1.ROWNR = b1.ROWNR -1 and a1.Employeeid = b1.Employeeid
where not (a1.STARTOFCHECKIN is null AND b1.ENDOFCHECKIN is null)
order by a1.Employeeid, a1.STARTOFCHECKIN

Resources