Conditional FROM clause - sql-server

My coworkers are using entity framework and have got 3 (schematically) identical databases. These databases are updated and modified by their application. I am writing another, separate application to gather information about their application.
I am trying to use stored procedures but having trouble. It seems I must have three copies of my query in every stored procedure (one for each database) and JOIN them all at the end. I don't want to have three copies of every query with only the table name changed. Can I specify using a parameter, CASE statement, or something else the table I use in my FROM Clause?

Two options: dynamic SQL, or a UNION ALL statement.
SELECT columnlist
FROM TABLE1
WHERE #param = 'Table1'
UNION ALL
SELECT columnlist
FROM TABLE2
WHERE #param = 'Table2'
UNION ALL
SELECT columnlist
FROM TABLE3
WHERE #param = 'Table3'

Since you are working with stored procedures, you can pass the table name from which you want to query as parameter like
create procedure sp_test
#tab_name varchar(10)
as
begin
if(#tab_name = 'Table1')
select * from Table1
else if (#tab_name = 'Table2')
select * from Table2
else
select * from Table3
end
Then run your SP like
exec sp_test 'Table1'
EDIT:
As per your comment you want to change the DB name in your query. So in DB.HistoryOne JOIN DB.HistoryTwo you want to change the DB to DB1. You can do it like below in a procedure
create procedure sp_DB_change
#DBname varchar(10)
as
begin
declare #sql varchar(200);
set #sql = 'SELECT AVG(DATEDIFF(s, StartDate, OtherStartDate)) AS time1 ,
CAST(OtherStartDate AS Date) AS [Date]
FROM DB.HistoryOne
JOIN DB.HistoryTwo ON HistoryOne.Id = HistoryTwo.Id
WHERE StartDate IS NOT NULL
AND OtherStartDate IS NOT NULL
AND OtherStartDate > DATEADD(d, -7, GETDATE())
GROUP BY CAST(OtherStartDate AS DATE)';
select #sql = REPLACE(#sql,'DB',#newdb)
exec (#sql)
end
Then run your SP like
exec sp_DB_change 'testDB'
So your original query
SELECT AVG(DATEDIFF(s, StartDate, OtherStartDate)) AS time1 ,
CAST(OtherStartDate AS Date) AS [Date]
FROM DB.HistoryOne
JOIN DB.HistoryTwo ON HistoryOne.Id = HistoryTwo.Id
WHERE StartDate IS NOT NULL
AND OtherStartDate IS NOT NULL
AND OtherStartDate > DATEADD(d, -7, GETDATE())
GROUP BY CAST(OtherStartDate AS DATE)
Will be converted to
SELECT AVG(DATEDIFF(s, StartDate, OtherStartDate)) AS time1 ,
CAST(OtherStartDate AS Date) AS [Date]
FROM testDB.HistoryOne
JOIN testDB.HistoryTwo ON HistoryOne.Id = HistoryTwo.Id
WHERE StartDate IS NOT NULL
AND OtherStartDate IS NOT NULL
AND OtherStartDate > DATEADD(d, -7, GETDATE())
GROUP BY CAST(OtherStartDate AS DATE)

Related

How to optimize below my SQL query shown here

This query is written for those users who did not log-in to the system between 1st July to 31 July.
However when we run the query in query analyzer then it's taking more than 2 minutes. But in application side giving error as 'Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding'.
Below query takes start date as 1st July 2022 and get all the users and add those users into temp table called '#TABLE_TEMP' and increases to next date.
Again while loop runs and fetch users for 2nd July and so on until it reaches to 31st July.
Can anyone help on this to optimize the query using CTE or any other mechanism?
H
ow can we avoid While loop for better performance?
DECLARE #TABLE_TEMP TABLE
(
Row int IDENTITY(1,1),
[UserId] int,
[UserName] nvarchar(100),
[StartDate] nvarchar(20),
[FirstLogin] nvarchar(20),
[LastLogout] nvarchar(20)
)
DECLARE #START_DATE datetime = '2022-07-01';
DECLARE #END_DATE datetime = '2022-07-31';
DECLARE #USER_ID nvarchar(max) = '1,2,3,4,5,6,7,8,9';
DECLARE #QUERY nvarchar(max) = '';
WHILE(#START_DATE < #END_DATE OR #START_DATE = #END_DATE)
BEGIN
SET #QUERY = 'SELECT
s.userid AS [UserId],
s.username AS [UserName],
''' + CAST(#START_DATE as nvarchar) + ''' AS [StartDate],
MAX(h.START_TIME) as [FirstLogin],
MAX(ISNULL(h.END_TIME, s.LAST_SEEN_TIME)) as [LastLogout]
FROM USER s
LEFT JOIN USER_LOGIN_HISTORY h ON h.userid = s.userid
LEFT JOIN TEMP_USER_INACTIVATION TUI ON TUI.userid = s.userid AND ('''+ CAST(#START_DATE as nvarchar) +''' BETWEEN ACTIVATED_DATE AND DEACTIVATD_DATE)
WHERE s.userid IN (' + #USER_ID + ')
AND h.userid NOT IN (SELECT userid FROM USER_LOGIN_HISTORY WHERE CAST(START_TIME AS DATE) = '''+ CONVERT(nvarchar,(CAST(#START_DATE AS DATE))) +''') AND ACTIVATED_DATE IS NOT NULL
GROUP BY s.userid, h.userid, s.username, s.last_seen_time
HAVING CAST(MAX(ISNULL(h.END_TIME, s.LAST_SEEN_TIME)) AS DATE) <> '''+ CONVERT(nvarchar,(CAST(#START_DATE AS DATE))) + '''
ORDER BY [User Name]'
INSERT INTO #TABLE_TEMP
EXEC(#QUERY)
SET #START_DATE = DATEADD(DD, 1, #START_DATE)
END
Without the query plan, it's hard to say for sure.
But there are some clear efficiencies to be had.
Firstly, there is no need for a WHILE loop. Create a Dates table which has every single date in it. Then you can simply join it.
Furthermore, do not inject the #USER_ID values. Instead, pass them thorugh as a Table Valued Parameter. At the least, split what you have now into a temp table or table variable.
Do not cast values you want to join on. For example, to check if START_TIME falls on a certain date, you can do WHERE START_TIME >= BeginningOfDate AND START_TIME < BeginningOfNextDate.
The LEFT JOINs are suspicious, especially given you are filtering on those tables in the WHERE.
Use NOT EXISTS instead of NOT IN or you could get incorrect results
DECLARE #START_DATE date = '2022-07-01';
DECLARE #END_DATE date = '2022-07-31';
DECLARE #USER_ID nvarchar(max) = '1,2,3,4,5,6,7,8,9';
DECLARE #userIds TABLE (userId int PRIMARY KEY);
INSERT #userIds (userId)
SELECT CAST(value AS int)
FROM STRING_SPLIT(#USER_ID, ',');
SELECT
s.userid as [UserId],
s.username as [UserName],
d.Date as [StartDate],
MAX(h.START_TIME) as [FirstLogin],
MAX(ISNULL(h.END_TIME, s.LAST_SEEN_TIME)) as [LastLogout]
FROM Dates d
JOIN USER s
LEFT JOIN USER_LOGIN_HISTORY h ON h.userid = s.userid
LEFT JOIN TEMP_USER_INACTIVATION TUI
ON TUI.userid = s.userid
ON d.Date BETWEEN ACTIVATED_DATE AND DEACTIVATD_DATE -- specify table alias (don't know which?)
WHERE s.userid in (SELECT u.userId FROM #userIds u)
AND NOT EXISTS (SELECT 1
FROM USER_LOGIN_HISTORY ulh
WHERE ulh.START_TIME >= CAST(d.date AS datetime)
AND ulh.START_TIME < CAST(DATEADD(day, 1, d.date) AS datetime)
AND ulh.userid = h.userid
)
AND ACTIVATED_DATE IS NOT NULL
AND d.Date BETWEEN #START_DATE AND #END_DATE
GROUP BY
d.Date,
s.userid,
s.username,
s.last_seen_time
HAVING CAST(MAX(ISNULL(h.END_TIME, s.LAST_SEEN_TIME)) AS DATE) <> d.date
ORDER BY -- do you need this? remove if possible.
s.username;
Better to collect dates in a table rather than running query in a loop. Use following query to collect dates between given date range:
DECLARE #day INT= 1
DECLARE #dates TABLE(datDate DATE)
--creates dates table first and then create dates for the given month.
WHILE ISDATE('2022-8-' + CAST(#day AS VARCHAR)) = 1
BEGIN
INSERT INTO #dates
VALUES (DATEFROMPARTS(2022, 8, #day))
SET #day = #day + 1
END
Then to get all dates where user did not login, you have to use Cartesian join and left join as illustrated below
SELECT allDates.userID,
allDates.userName,
allDates.datDate notLoggedOn
FROM
(
--This will reutrun all users for all dates in a month i.e. 31 rows for august for every user
SELECT *
FROM Users,
#dates
) allDates
LEFT JOIN
(
--now get last login date for every user between given date range
SELECT userID,
MAX(login_date) last_Login_date
FROM USER_LOGIN_HISTORY
WHERE login_date BETWEEN '2022-08-01' AND '2022-08-31'
GROUP BY userID
) loggedDates ON loggedDates.last_Login_date = allDates.datDate
WHERE loggedDates.last_Login_date IS NULL --filter out only those users who have not logged in
ORDER BY allDates.userID,
allDates.datDate
From this query you will get every day of month when a user did not logged in.
If there is no need to list every single date when user did not log in, then Cartesian join can be omitted. This will further improve the performance.
I hope this will help.

Can I "left join" days between 2 dates in sql server?

There is a table in SQL Server where data is entered day by day. In this table, data is not filled in some days.
Therefore, there are no records in the table.
Sample: dataTable
I need to generate a report like the one below from this table.
Create a table with all the days of the year. I know that I can output a report by "joining" the "dataTable" table.
But this solution seems a bit strange to me.
Is there another way?
the code i use for temp date table
CREATE TABLE tempDate (
calendarDate date,
PRIMARY KEY (calendarDate)
)
DECLARE
#start DATE= '2021-01-01',
#dateCount INT= 730,
#rowNumber INT=1
WHILE (#rowNumber < #dateCount)
BEGIN
INSERT INTO tempDate values (DATEADD(DAY, #rowNumber, #start))
set #rowNumber=#rowNumber+1
END
GO
select * from tempDate
This is how I join using this table
SELECT
*
FROM
tempDate td WITH (NOLOCK)
LEFT JOIN dataTable dt WITH (NOLOCK) ON dt.reportDate = td.calendarDate
WHERE
td.calendarDate BETWEEN '2021-09-05' AND '2021-09-15'
Create a table with all the days of the year. I know that I can output a report by "joining" the "dataTable" table.
This is the way. You can generate that "table" on the fly if you really want to, but normally the best way is to simply have a calendar table.
You can use common expression tables for dates. The code you need:
IF(OBJECT_ID('tempdb..#t') IS NOT NULL)
BEGIN
DROP TABLE #t
END
CREATE TABLE #t
(
id int,
dt date,
dsc varchar(100),
)
INSERT INTO #t
VALUES
(1, '2021.09.08', 'a'),
(1, '2021.09.09', 'b'),
(1, '2021.09.12', 'c')
DECLARE #minDate AS DATE
SET #minDate = (SELECT MIN(dt) FROM #t)
DECLARE #maxDate AS DATE
SET #maxDate = (SELECT MAX(dt) FROM #t)
;WITH cte
AS
(
SELECT #minDate AS [dt]
UNION ALL
SELECT DATEADD(DAY, 1, [dt])
FROM cte
WHERE DATEADD(DAY, 1, [dt])<=#maxDate
)
SELECT
ISNULL(CAST(t.id AS VARCHAR(10)), '') AS [id],
cte.dt AS [dt],
ISNULL(t.dsc, 'No record has been entered in the table.') AS [dsc]
FROM
cte
LEFT JOIN #t t on t.dt=cte.dt
The fastest method is to use a numbers table, you can get a date list between 2 dates with that:
DECLARE #Date1 DATE, #Date2 DATE
SET #Date1 = '20200528'
SET #Date2 = '20200625'
SELECT DATEADD(DAY,number+1,#Date1) [Date]
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY,number+1,#Date1) < #Date2
If you go go in LEFT JOIN this select, whit your table, you have the result that you want.
SELECT *
FROM (SELECT DATEADD(DAY,number+1,#Date1) [Date]
FROM master..spt_values WITH (NOLOCK)
WHERE type = 'P'
AND DATEADD(DAY,number+1,#Date1) < #Date2 ) as a
LEFT JOIN yourTable dt WITH (NOLOCK) ON a.date = dt.reportDate
WHERE td.[Date] BETWEEN '2021-09-05' AND '2021-09-15'

Creating a monthly report in T-SQL

I have a table with the following columns
CaseID
DateLogged
CompletionDate
I am trying to create a monthly stock report.
I need to identify monthly which cases are New, current and Completed each month
for instance, All cases logged only in August are new cases while all cases completed in august would show completed and all cases logged that are not completed will be current.
DROP TABLE IF EXISTS #temptable
-- Create date variables
SET dateformat ymd
DECLARE #datefrom datetime = CONVERT(DATETIME, '2019-04-01 00:00:00', 121)
DECLARE #dateto datetime = (SELECT CAST(DATEADD(DAY, -DAY(GETDATE()), GETDATE()) AS date))
-- Recursive date table creation
;WITH monthserial AS
(
SELECT #datefrom AS monthdate
UNION ALL
SELET DATEADD(MONTH, 1, monthdate)
FROM monthserial
WHERE monthdate < #dateto
)
SELECT MN.*
INTO #temptable
FROM monthserial MN
SELECT * FROM MainTable VW
CROSS JOIN #temptable TBL
WHERE DateLogged <= monthdate) test
You will need to use case-when for your situation:
select CaseID,
case
when not (CompletionDate is null) then 'Completed'
when DateLogged >= #input then 'New'
else 'Current'
end
from yourtable
order by DateLogged;
EDIT
Since we are interested about the last entry, we can add a where clause, like this:
select CaseID,
case
when not (CompletionDate is null) then 'Completed'
when DateLogged >= #input then 'New'
else 'Current'
end
from yourtable
where not exists (
select 1
from yourtable inner
where inner.CaseID = yourtable.CaseID and inner.CompletionDate < yourtable.CompletionDate
)
order by DateLogged;

Why does CTE inside a stored procedure take a very long time compared to CTE w/o stored procedure?

I have a recursive CTE that calculates a manager hierarchy in an organization.
This below query takes < 2 seconds to finish
WITH OrganisationChart ([Identity], [DisplayName], Title, [Level], Manager) AS
(
SELECT
[Identity], [DisplayName], Title, 0, Manager
FROM
[data].[DailyUserV1]
WHERE
[Identity] = '7276DB4F-33B0-4074-9903-D95D740A8BF3' AND Date = '2015-08-03'
UNION ALL
SELECT
emp.[Identity],
emp.[DisplayName],
emp.Title,
[Level] + 1,
emp.Manager
FROM
[data].[DailyUserV1] emp
INNER JOIN
OrganisationChart ON emp.Manager = OrganisationChart.[Identity]
WHERE
Date = '2015-08-03'
)
SELECT * FROM OrganisationChart
While this same query wrapped inside a stored procedure takes > 15 mins and then times out!
IF (OBJECT_ID('[dbo].[GetOrganizationChart]') IS NOT NULL)
DROP PROCEDURE [dbo].[GetOrganizationChart]
GO
CREATE PROCEDURE [dbo].[GetOrganizationChart]
#identity varchar(256),
#date datetime
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #userId varchar(256);
SET #userId = #identity;
DECLARE #endDate datetime;
SET #endDate = #date;
WITH OrganisationChart ([Identity], [DisplayName], Title, [Level], Manager) AS
(
SELECT
[Identity], [DisplayName], Title, 0, Manager
FROM
[data].[DailyUserV1]
WHERE
[Identity] = #userId AND Date = #endDate
UNION ALL
SELECT
emp.[Identity],
emp.[DisplayName],
emp.Title,
[Level] + 1,
emp.Manager
FROM
[data].[DailyUserV1] emp
INNER JOIN
OrganisationChart ON emp.Manager = OrganisationChart.[Identity]
WHERE
Date = #endDate
)
SELECT * FROM OrganisationChart;
END
GO
EXEC [dbo].[GetOrganizationChart] #identity = '7276DB4F-33B0-4074-9903-D95D740A8BF3', #date = '2015-08-03'
I have ruled out parameter sniffing as a likely cause by using local variables inside the stored procedure. What's going on here?
UPDATE
Here are the links to the query execution plans in case you want to take a look.
cte-without-stored-proc
cte-with-stored-proc
The same problem I've faced earlier and there was parameter sniffing. To overcome this issue I used temporary table instead of CTE and the SP start running smoothly.

I want to create a function which will return table, fetch rows from different synonym based on #Parameter

Let say I have 3 synonyms
snTable1
snTable2
snTable3
I want to create a function to fetch rows based on the parameter passed
fxFromTable (#FromDate, #ToDate, 'snTable1')
will return table from "Select * from snTable1 (#FromDate, #ToDate)"
or
fxFromTable (#FromDate, #ToDate, 'snTable2')
will return table from "Select * from snTable2 (#FromDate, #ToDate)"
or
fxFromTable (#FromDate, #ToDate, 'snTable3')
will return table from "Select * from snTable3 (#FromDate, #ToDate)"
I guess this could be done with executing dynamic sql. But, it can't be used inside a function. So, a stored proc can be used in this case. This could be done something like below.
CREATE PROCEDURE TEST
(
#TableName VARCHAR(50),
#FromDate DATETIME,
#ToDate DATETIME
)
AS
BEGIN
EXEC('SELECT * FROM ' + #TableName + ' WHERE FromDate = '''+ #FromDate + ''' AND ToDate = ''' + #ToDate + ''')
END
If you want to use this in the other stored procedure, you can do something like below:
INSERT #temp EXECUTE TEST
Now, #temp can be used as a normal temp table.
Hope this helps!!
You can create an inline TVF using UNION ALL. The last parameter represents the source type (1 = Sales.SalesOrderHeader, 2 = Sales.SpecialOffer, 3 = HumanResources.Employee).
CREATE FUNCTION dbo.GetData( #From DATETIME, #To DATETIME, #Source TINYINT )
RETURNS TABLE
AS
RETURN
SELECT #Source AS [Source], soh.SalesOrderID BusinessObjectID, soh.OrderDate AS [Date], soh.SalesOrderNumber AS [Descriptor]
FROM Sales.SalesOrderHeader soh
WHERE #Source = 1
AND soh.OrderDate BETWEEN #From AND #To
UNION ALL
SELECT #Source , so.SpecialOfferID, so.StartDate, so.[Description]
FROM Sales.SpecialOffer so
WHERE #Source = 2
AND so.StartDate >= #From
AND so.EndDate <= #To
UNION ALL
SELECT #Source , e.BusinessEntityID, e.HireDate, e.NationalIDNumber
FROM HumanResources.Employee e
WHERE #Source = 3
AND e.HireDate BETWEEN #From AND #To;
GO
Usage:
SELECT *
FROM dbo.GetData('20030101', '20031231', 3);
If you look at execution plan you will see something nice: instead of executing 3 SELECT statements, SQL Server will execute only one SELECT statement, in this case only the last SELECT:
StmtText
|--Compute Scalar(DEFINE:([Union1007]=[AdventureWorks2008].[HumanResources].[Employee].[BusinessEntityID] as [e].[BusinessEntityID], [Union1008]=CONVERT_IMPLICIT(datetime,[AdventureWorks2008].[HumanResources].[Employee].[HireDate] as [e].[HireDate],0), [Union1009]=[AdventureWorks2008].[HumanResources].[Employee].[NationalIDNumber] as [e].[NationalIDNumber]))
|--Clustered Index Scan(OBJECT:([AdventureWorks2008].[HumanResources].[Employee].[PK_Employee_BusinessEntityID] AS [e]), WHERE:([AdventureWorks2008].[HumanResources].[Employee].[HireDate] as [e].[HireDate]>='2003-01-01 00:00:00.000' AND [AdventureWorks2008].[HumanResources].[Employee].[HireDate] as [e].[HireDate]<='2003-12-31 00:00:00.000'))
This example is based on AdventureWorks2008 database.

Resources