Create a new field value that shows a difference in value from 2 fields

How can I create a new field value that shows a difference in value from 2 fields? They are numerical values and may vary each month. I only want to show the difference in value if there has been a change in value from the latest month to the previous month.
(it only needs to report on the previous month not every month in the database)
e.g.
table sales
field - amount - integer value
field - monthPeriod - values increment every month, e.g. 1,2,3,4,5,6,7,etc
difference - in the amount from the previous month.

amount | month |
10.22 | 1
10.22 | 2
10.33 | 3
10.33 | 4
10.33 | 5
11.32 | 6

so I would expect to see:

amount | month | difference
10.33 | 3 | 0.11
11.32 | 6 | 0.99

I need to create a new column called difference and subtract the latest month value from the previous month and only report where value is >0, but I'm not sure how to format it.

Thanks

CREATE TABLE #t
(
	aYear int NOT NULL
	,aMonth int NOT NULL
	,amount decimal(19,5) NOT NULL
	,PRIMARY KEY (aYear, aMonth)
);
INSERT INTO #t
VALUES (2024, 1, 10.22)
	,(2024, 2, 10.22)
	,(2024, 3, 10.33)
	,(2024, 4, 10.33)
	,(2024, 5, 10.33)
	,(2024, 6, 11.32);

WITH Diffs
AS
(
	SELECT aYear, aMonth
		,amount - ISNULL(LAG(amount) OVER (ORDER BY aYear, aMonth), amount) AS aDifference
	FROM #t
)
SELECT aYear, aMonth, aDifference
FROM Diffs
WHERE aDifference <> 0
ORDER BY aYear, aMonth;
2 Likes

@Ifor ,

Just a little trick... you don't need the ISNULL here. There is a "Default" to LAG. Here's the syntax from BOL.

LAG (scalar_expression [,offset] [,default])  
    OVER ( [ partition_by_clause ] order_by_clause )

If we make the change in your good code, it will look like this...

WITH Diffs
AS
(
	SELECT aYear, aMonth
		,amount - LAG(amount,1,amount) OVER (ORDER BY aYear, aMonth) AS aDifference
	FROM #t
)
SELECT aYear, aMonth, aDifference
FROM Diffs
WHERE aDifference <> 0
ORDER BY aYear, aMonth;

Results are the same...
image

1 Like

Thanks. I keep forgetting the defaults on LEAD and LAG.

1 Like

thanks @Ifor and @JeffModen
I can see your code works and looks good. I'm just trying to get it to work with our joins around the tables.

1 Like

@Ifor

Not to worry... I only remember that there are defaults. I don't use LEAD or LAG often enough to have the syntax memorized and so I have to look it up every time. :rofl:

Thank you for the feedback. Sometimes, a little "Divide'n'Conquer" in the code is your best friend. For example, storing an interim answer in a Temp Table is NOT the end of the world, can make code much easier to write, and can make the code a whole lot faster (although it's NOT a panacea).

My point here is that too many people have difficulty try to fit something in when an interim Temp Table will do the trick.

To summarize, always remember that "Set Based" < > "All on one query". :grin:

1 Like

I'm just struggling with getting the query to pick up these fields in the second select statement: aYear, aMonth, aDifference.
I can't get them to bind to the query. It was fine when building the first select query, but the 2nd select statement seems to be outside the scope and I get a 'could not be bound' error.

It is all one query. Diffs is a CTE.

@Barnsley

In other words, the second SELECT cannot run without the first because the first makes a temporary object known as a CTE (Common Table Expression) called "DIFFS" and that's what the second SELECT uses. You have to run the whole thing (both SELECTs) for it to work.

Hi 

Hope this helps 

WITH MonthlySales AS (
    SELECT 
        amount, 
        monthPeriod,
        LAG(amount) OVER (ORDER BY monthPeriod) AS PreviousAmount
    FROM 
        sales
)

SELECT 
    amount, 
    monthPeriod, 
    (amount - PreviousAmount) AS difference
FROM 
    MonthlySales
WHERE 
    PreviousAmount IS NOT NULL 
    AND (amount - PreviousAmount) > 0;
1 Like

I understand its one query and its a CTE etc.
But I can't get it to work with all our joins.
Here's what I mean...
so for this sql below:

/*without CTE*/
SELECT        TOP (100) PERCENT PET.PAYELEMENTTYPENAME, P.SURNAME, R.PAYPERIOD, R.PAYYEAR, M.WORKSROLLNUMBER, A.Amount, 
LAG(A.AMOUNT,1) OVER (PARTITION BY M.WORKSROLLNUMBER ORDER BY R.PAYPERIOD,  M.WORKSROLLNUMBER)
AS PrevMonth

FROM            dbo.PAYROLLMEMBER AS M INNER JOIN
                         dbo.FOLDER AS F ON M.PAYROLLMEMBERID = F.PAYROLLMEMBERID INNER JOIN
                         dbo.PERSON AS P ON F.PERSONID = P.PERSONID INNER JOIN
                         dbo.PAYROLLTRANSACTION AS T ON M.PAYROLLMEMBERID = T.PAYROLLMEMBERID INNER JOIN
                         dbo.PAYROLLPAYRUN AS R ON T.PAYROLLPAYRUNID = R.PAYROLLPAYRUNID INNER JOIN
                         dbo.PAYROLLACCENTRY AS A ON T.PAYROLLTRANSACTIONID = A.PAYROLLTRANSACTIONID INNER JOIN
                         dbo.PAYELEMENTTYPE AS PET ON A.PAYELEMENTTYPEID = PET.PAYELEMENTTYPEID

WHERE        (PET.PAYELEMENTTYPENAME LIKE 'WESTFIELD%') AND (R.PAYYEAR = 2024) 

I get the lag code to work and show results above.

//////////////////////////////////////////////////

but with the CTE example, and SQL like this:


WITH myDiff AS (
	
SELECT        TOP (100) PERCENT PET.PAYELEMENTTYPENAME, P.SURNAME, R.PAYPERIOD, R.PAYYEAR, M.WORKSROLLNUMBER, A.Amount, 
LAG(A.AMOUNT,1) OVER (PARTITION BY M.WORKSROLLNUMBER ORDER BY R.PAYPERIOD,  M.WORKSROLLNUMBER)
AS PrevMonth

FROM            dbo.PAYROLLMEMBER AS M INNER JOIN
                         dbo.FOLDER AS F ON M.PAYROLLMEMBERID = F.PAYROLLMEMBERID INNER JOIN
                         dbo.PERSON AS P ON F.PERSONID = P.PERSONID INNER JOIN
                         dbo.PAYROLLTRANSACTION AS T ON M.PAYROLLMEMBERID = T.PAYROLLMEMBERID INNER JOIN
                         dbo.PAYROLLPAYRUN AS R ON T.PAYROLLPAYRUNID = R.PAYROLLPAYRUNID INNER JOIN
                         dbo.PAYROLLACCENTRY AS A ON T.PAYROLLTRANSACTIONID = A.PAYROLLTRANSACTIONID INNER JOIN
                         dbo.PAYELEMENTTYPE AS PET ON A.PAYELEMENTTYPEID = PET.PAYELEMENTTYPEID
WHERE        (PET.PAYELEMENTTYPENAME LIKE 'WESTFIELD%') AND (R.PAYYEAR = 2024) 
)
SELECT 
	R.PAYPERIOD, 
	PET.PAYELEMENTTYPENAME,
	 A.Amount, 
	prevMonth
FROM
	myDiff;
	

I get this error:
sql with CTE error

maybe its some of the other tables that are outside the scope and I need to bring them in some how?

The aliases R., Pet. and A. are out of scope - remove them in the final SELECT. (Keep them in the CTE.) The CTE here is effectively an inline view.

thanks, just seen that if I give these unbound fields an alias in the first query I can get rid of that bound error. So I'm back on track....