I have a table in my db that stored both successful and failed log-in attempts. I am creating a stored procedure that allow us to delete records which are older than X days old. So far so good, but I want to raise it a notch (or two) and allow us to specify whether to delete records on whether the [Success] column is true, false or both. I am having some issues with the concatenation of the script that needs to be executed, though.
Here is what I did so far:
-- CREATE PROCEDURE [dbo].[sp_delete_log_attempts]
DECLARE @backDays INT = 1 -- Default to 30 days (one test finishes)
DECLARE @successArg BIT = NULL -- default to both true and false success logins
DECLARE @successAnd VARCHAR(50)
DECLARE @query VARCHAR(MAX)
SET @successAnd = CASE
WHEN @successArg = 'true' THEN
'AND [Success] = ''true'''
WHEN @successArg = 'false' THEN
'AND [Success] = ''false'''
ELSE
'AND [Success] = ''true'' OR [Success] = ''false'''
END
PRINT @successAnd -- just for debugging purposes
SET @query = 'SELECT * FROM [audit].LoginAttempt WHERE [TimeStamp] <= DATEADD(day,-' + @backDays + ', GETDATE())'
EXEC @query
At this point I am just trying to select the rows based on the @backDays variable, but I cannot concatenate the @query string with the variable. Not sure of what I am doing wrong here. I am fairly new to dynamic queries, though.
Best Answer
I don't think you even need dynamic SQL here. You can use variables in the query.
(Not tested as no sample data was provided. Just to demonstrate the idea.)
If you look close at it, you'll see, that the entire
CASE ... END
is the left operand of an=
operation. The right side of the operation is1
.Unfortunately SQL Server knows no Boolean type we could have directly returned from the
CASE ... END
. (Others, e.g. PostgreSQL do.) So we have to trick it using an expression it can use for a Boolean operation. That's why we use the=
operation.Now we need a way of making this operation evaluate to true when the conditions we actually want to test are met. So the idea is, if we come to the decision, that they are met, we return one designated value for our left side of the
=
. On the other side, we just use that value literally. The=
will be true then. If our conditions don't meet, we return any other value for the left side, and the=
won't be true. As a value to indicate the conditions were met, lets choose the1
. It's close to a representation of a Boolean we might have in mind. (But we could have chosen nearly anything (NOT NULL
, otherwise we had to change the=
operation toIS NULL
).).So how can we make our left expression to return
1
, when the conditions are met?Well, we can use a
CASE ... END
statement to return a value based on certain conditions. You already know theCASE ... END
. It's somewhat similar to aswitch
orif else
constructs in C-like languages (or with different names in other procedural languages).What we need to test is if the input variable
@successArg
is null or not. If it's null, that means the caller doesn't care, whether the logged login attempts were successful or not. Otherwise the value of@successArg
indicates whether he wants only successful logins (@successArg = 1
) or only unsuccessful (@successArg = 0
). This will give us to cases on top: Either ignore the successfulness or take it into account. Hence we have two (main) branches in ourCASE ... END
branching about@successArg IS NOT NULL
;Let's start with the easier case, when
@successArg IS NULL
. That's the 'ELSE' branch. Here, we don't care about if the login attempt was successful or not. Being successful or not successful (think of it a Boolean expression) is always true, for any row. So we just return our indicator, that this was a match,1
.When
@successArg IS NULL
,@successArg
holds the value, the rows must have in the column[Success]
. This is given only if@successArg = [Success]
. As already mentioned,@successArg = [Success]
alone won't be something SQL Server could handle in that context. So we need our true indicating value1
to be returned when@successArg = [Success]
for the current row, something else otherwise.One way of doing this would be another, inner
CASE ... WHEN
or we might use a correlated subquery. It's "correlated" as it uses values of columns from the current row of the outer query. And it's "sub", well because it's "sub", viewed from the position of the outer query. Furthermore, we use the feature in SQL Server that aSELECT
with noFROM
will produce one record if theWHERE
of that query is true (or if there is none), otherwise an empty set. So let's produce a record with one column holding1
when@successArg = [Success]
. Such a record, with only one column, will implicitly be casted to a scalar if needed, hence it's suitable for our=
operation outside. When@successArg <> [Success]
, that subquery will produce an empty set. The empty set in that scalar context will be implicitly casted toNULL
. AsNULL = 1
isn't true, this will do it for us.One side note on this: It also can be done without a
CASE ... END
but simply a Boolean expression:That follows our thoughts from above. In case
@successArg IS NULL
we want "true" regardless of anything else. Otherwise we only want "true" when[Success] = @successArg
. In any other case "false". (Note: The parenthesis are needed to override the precedence ofAND
overOR
because the previous condition.)In terms of how the query is executed, hence performance, I don't think this makes a significant difference, if any (but frankly I don't know this for sure). The solution with a
CASE ... WHEN
though, might be easier to read and therefore maintain. The Boolean only solution, on the other hand, also works in systems, that are not capable of aCASE ... END
.