CTE To Use Alternate Select When Select Returns No Rows
-
29-09-2020 - |
Question
I have created an involved CTE
, with no recursion, but with multiple selects being done as it processes the business logic of the requirement. Though, I have been advised that the logic needed for the scenario where no rows are returned, should instead to return all rows found from one of the middle CTE
s select instead of the final operation.
How can this be achieved?
Note the following example is basic interpretation of my process and the operation in question occurs after multiple cascaded CTE selects are done an essentially chained together.
Example (as found on SQLFiddle)
In a User Table there are two records where the ids of UserId
s are 1
and 2
. The first CTE
s select, TargetUsers
will return no rows requiring the the third CTE, ComputeWithUsers
, to use the AllUsers
instead.
WITH TargetUsers AS
(
SELECT *
FROM Users
WHERE UserId > 5
)
, AllUsers AS
(
SELECT * FROM Users
)
, ComputeWithUsers
(
-- This CTE will determine it needs to use `AllUsers` and not `TargetUsers`
)
Or is there a way to make the TargetUsers
select smart enough to extract all users when its main operation returns no rows?
Solution
Here is the way I would do this, assuming the "do it all in one query" isn't just a pointless, artificial requirement.
SELECT cols INTO #x FROM dbo.Users WHERE UserID > 5;
IF @@ROWCOUNT > 0
SELECT cols FROM #x;
ELSE
SELECT cols FROM dbo.Users;
If the "do it all in one query" is a hard requirement and you can't use a #temp table, or you really have complex logic you've dumbed down from us and/or don't want to repeat, then you're going to have to pay for those choices in performance, I'm afraid. This looks like it should be quite efficient, but there are going to be more hits to the base table than most people would hope / expect:
;WITH TargetUsers AS
(
SELECT cols FROM dbo.Users WHERE UserID > 5
),
AllUsers AS
(
SELECT cols FROM dbo.Users
WHERE NOT EXISTS (SELECT 1 FROM TargetUsers)
-- this will only return rows if the first CTE is empty
),
ComputeWithUsers AS
(
SELECT cols FROM TargetUsers
UNION ALL
SELECT cols FROM AllUsers
)
...;
In fact if UserID is the primary key and the filter really is on UserID, this might be better as follows (but you'll have to check the plans to see, depends on a variety of factors):
;WITH TargetUserIDs AS
(
SELECT UserID FROM dbo.Users WHERE UserID > 5
),
AllUserIDs AS
(
SELECT UserID FROM dbo.Users
WHERE NOT EXISTS (SELECT 1 FROM TargetUsers)
-- this will only return rows if the first CTE is empty
),
ComputeWithUsers AS
(
SELECT UserID FROM TargetUserIDs
UNION ALL
SELECT UserID FROM AllUserIDs
)
...
-- and join to dbo.Users to get other columns necessary
-- deferring that to as late as possible
Hopefully your queries are using suboptimal practices due to simplification, and not like that in production, but please give these a read:
OTHER TIPS
Assuming the performance is acceptable, you can use the following as a means of getting all users when the initial filter fails to return any results.
SELECT *
FROM Users
WHERE UserID > 5
OR NOT EXISTS (SELECT * FROM Users WHERE UserID > 5)
You're just setting a contrived OR that will allow all records to pass the filter if the base select returns nothing.