Move Functions from Where Clause to Select Statement - sql

I have a union query that runs abysmally slow I believe mostly because there are two functions in the where clause of each union. I am pretty sure that there is no getting around the unions, but there may be a way to move the functions from the where of each. I won't post ALL of the union sections because I don't think it is necessary as they are all almost identical with the exception of one table in each. The first function was created by someone else but it takes a date, and uses the "frequency" value like "years, months, days, etc." and the "interval" value like 3, 4, 90 to calculate the new "Due Date". For instance, a date of today with a frequency of years, and an interval of 3, would produce the date 4/21/2025. Here is the actual function:
ALTER FUNCTION [dbo].[ReturnExpiration_IntervalxFreq](#Date datetime2,#int int, #freq int)
RETURNS datetime2
AS
BEGIN
declare #d datetime2;
SELECT #d = case when #int = 1 then null-- '12-31-9999'
when #int = 2 then dateadd(day,#freq,#date)
when #int = 3 then dateadd(week,#freq,#date)
when #int = 4 then dateadd(month,#freq,#date)
when #int = 5 then dateadd(quarter,#freq,#date)
when #int = 6 then dateadd(year,#freq,#date)
end
RETURN #d;
The query itself is supposed to find and identify records whose Due Date has past or is within 90 days of the current date. Here is what each section of the union looks like
SELECT
R.RequirementId
, EC.EmployeeCompanyId
, EC.CompanyId
, DaysOverdue =
CASE WHEN
R.DueDate IS NULL
THEN
CASE WHEN
EXISTS(SELECT 1 FROM tbl_Training_Requirement_Compliance RC WHERE RC.EmployeeCompanyId = EC.EmployeeCompanyId AND RC.RequirementId = R.RequirementId AND RC.Active = 1 AND ((DATEDIFF(DAY, R.DueDate, GETDATE()) > -91 OR R.DueDate Is Null ) OR (DATEDIFF(DAY, dbo.ReturnExpiration_IntervalxFreq(TRC.EffectiveDate, R.IntervalId, R.Frequency), GETDATE()) > -91)) OR R.IntervalId IS NULL)
THEN
DateDiff(day,ISNULL(dbo.ReturnExpiration_IntervalxFreq(TRC.EffectiveDate, R.IntervalId, R.Frequency), '12/31/9999'),getdate())
ELSE
0
END
ELSE
DATEDIFF(day,R.DueDate,getdate())
END
,CASE WHEN
EXISTS(SELECT 1 FROM tbl_Training_Requirement_Compliance RC WHERE RC.EmployeeCompanyId = EC.EmployeeCompanyId AND RC.RequirementId = R.RequirementId AND RC.Active=1 AND (GETDATE() > dbo.ReturnExpiration_IntervalxFreq(RC.EffectiveDate, R.IntervalId, R.Frequency) OR R.IntervalId IS NULL))
THEN
CONVERT(VARCHAR(12),dbo.ReturnExpiration_IntervalxFreq(TRC.EffectiveDate, R.IntervalId, R.Frequency), 101)
ELSE
CONVERT(VARCHAR(12),R.DueDate,101)
END As DateDue
FROM
#Employees AS EC
INNER JOIN dbo.tbl_Training_Requirement_To_Position TRP ON TRP.PositionId = EC.PositionId
INNER JOIN #CompanyReqs R ON R.RequirementId = TRP.RequirementId
LEFT OUTER JOIN tbl_Training_Requirement_Compliance TRC ON TRC.EmployeeCompanyId = EC.EmployeeCompanyId AND TRC.RequirementId = R.RequirementId AND TRC.Active = 1
WHERE
NOT EXISTS(SELECT 1
FROM tbl_Training_Requirement_Compliance RC
WHERE RC.EmployeeCompanyId = EC.EmployeeCompanyId
AND RC.RequirementId = R.RequirementId
AND RC.Active = 1
)
OR (
(DATEDIFF(DAY, R.DueDate, GETDATE()) > -91
OR R.DueDate Is Null )
OR (DATEDIFF(DAY, dbo.ReturnExpiration_IntervalxFreq(TRC.EffectiveDate, R.IntervalId, R.Frequency), GETDATE()) > -91))
UNION...
It is supposed to exclude records that either don't exist at all on the tbl_Training_Requirement_Compliance table, or if they do exist, once the frequency an intervals have been calculated, would have a new due date that is within 90 days of the current date. I am hoping that someone with much more experience and expertise in SQL Server can show me a way, if possible, to remove the functions from the WHERE clause and help the performance of this stored procedure.

Related

My SQL - How to run different WHERE statements on different weekdays, like one for monday and another for friday

I missunderstod this a lot and thanks again for all the help.
I eventually found a solution for my case and it didn't even involve anything like
IF Monday THEN SELECT
I didn't even needed a CASE-statement.
What I had to do was to put different WHERE statements with an OR in between.
I have stated my final code bolow:
SELECT * FROM
(SELECT
t.pay_date
, t.supp_name
, t.client AS row_client
, t.ip_status
, t.bank_account
, t.remitt_curr AS remitt_curr
, t.remitt_id
, t.apar_id
, t.payment_id
FROM
aipheader t
WHERE
DATEPART(DW, GETDATE()) IN (2,3,4)
AND
t.pay_date BETWEEN '2019-01-01' AND DATEADD(DAY,-4,GETDATE())
OR
DATEPART(DW, GETDATE()) IN (5,6)
AND
t.pay_date BETWEEN '2019-01-01' AND DATEADD(DAY,-2,GETDATE())) x
ORDER BY 1
The statement above will execute the OR-block that is true, in my case the one with the rigt weekday.
********* OLD QUESTIONS BELOW ***********
I want to run different SELECT statements on different days off the week.
I can actually get the following code to work:
select
case
when DATEPART(DW, GETDATE()) = 4 then
(select 'HELLO' )
ELSE
(select 'GOODBYE')
end
On a wendsday the code above returns "HELLO", every other workday it would returns "Goodbye". So far so good!
As long as the select statement only return one value it seams to work, but I realy want a full table like below.
The only differance here should be the [* FROM table] part, and it breaks it all:
select
case
when DATEPART(DW, GETDATE()) = 4 then
(select * FROM table1)
ELSE
(select * FROM table2)
end
If I can't even make the above to work I can not make different SELECTs for different workdays, so this is the hard ting to figure out.
I have tried to encapsulate things with () and add a few SELECTs in different ways but it do not work, so it is probably some SQL principal.
I get an error that I think says that this is ok if it is only one value that's returned:
SqlState 37000 Native 116 [Microsoft][ODBC SQL Server Driver][SQL
Server]Only one expression can be specified in the select list when
the subquery is not introduced with EXISTS.
THANKS FOR ALL OF THE REPLYS I NOW UNDERSTAND IT A BIT BETTER.
I was not looking to add a column, I tought I could make a complete new SELECT statement. I now see that my CASE statement only adds a column, as you point out.
So then my first ide might be a better aproce, to actually modify the WHERE-clause in the select statement.
I have now tried that but it does not work.
SELECT * FROM aipheader t
WHERE
CASE
WHEN DATEPART(DW, GETDATE()) = 4 THEN
t.pay_date = '2019-10-15'
ELSE
t.pay_date = '2019-10-17'
END
It returns the error:
SqlState 37000 Native 102 [Microsoft][ODBC SQL Server Driver][SQL
Server]Incorrect syntax near '='.
I have tried the simpler:
SELECT * FROM aipheader t
WHERE
t.pay_date BETWEEN DATEADD(DAY,-120,GETDATE()) AND DATEADD(DAY,-5,GETDATE())
The above is a simpler form, but that one returns the right values.
But the one obove that one with the CASE-clause return the error about "="-sign.
Maybee the CASE part in a WHERE statement do not work.
After your edit, the correct way to filter your date would be the following:
SELECT
*
FROM
aipheader t
WHERE
t.pay_date = CASE WHEN DATEPART(DW, GETDATE()) = 4 THEN '2019-10-15' ELSE '2019-10-17' END
However, do you really want to hard-code the 2019-10-15 and 2019-10-17 dates? Seems like these should be computed automatically as time goes.
Be careful when using DATEPART with DW, since the week number starting point can actually change depending on the server's and/or current session settings. Check this example:
DECLARE #TestDate DATE = '2020-01-01' -- Wednesday
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
SELECT DATEPART(DW, #TestDate) -- Returns 3
SET DATEFIRST 7 -- 1: Sunday, 7: Saturday
SELECT DATEPART(DW, #TestDate) -- Returns 4!
So whenever checking for a particular day of the week, make sure to force the DATEFIRST session parameter to a particular value so it's consistent with your checks.
I do not fully understand what you are trying to achieve.
Of course your statement will not work, because your select statement in the case when returns multiple columns and rows. That cannot work, because your case when statement will display one column!
So what you can do is the following:
select
case
when DATEPART(DW, GETDATE()) = 4 then
'HELLO'
ELSE
'GOODBYE'
end as [someField],
*
from
[table]
This query will have all rows and columns from table as output + the someField displaying HELLO or GOODBYE.
If you want to display different data from the table on the different dates (wednesday or not wednesday), you can alter the query this way:
select
case
when DATEPART(DW, GETDATE()) = 4 then
[table].[field1]
ELSE
[table].[field2]
end as [someField],
*
from
[table]
Is this ur looking for ? Sub query should return 1 row and specify col name
select
case
when DATEPART(DW, GETDATE()) = 4 then
(select top 1 col1 from table1 )
ELSE
(select top 1 col2 from table2 )
end as result
From table
CASE WHEN can only return a single value for each THEN, and never a Result Set with multiple rows and/or columns. That is just how CASE WHEN works.
You can achieve what you need like this:
DECLARE #DAY INT = DATEPART(DW, GETDATE())
select * FROM table
WHERE (#DAY = 4 AND table.SomeCol = SomeValue)
OR (#DAY = 5 AND table.SomeCol = SomeOtherValue)
... repeat for other days
Or like this:
DECLARE #DAY INT = DATEPART(DW, GETDATE())
IF #DAY = 4
(select * FROM table WHERE table.SomeCol = SomeValue)
ELSE IF #DAY = 5
(select * FROM table WHERE table.SomeCol = SomeOtherValue)
... repeat for other days
Or like this:
DECLARE #DAY INT = DATEPART(DW, GETDATE())
select * FROM table
WHERE #DAY = 4 AND table.SomeCol = SomeValue
UNION ALL
select * FROM table
WHERE #DAY = 5 AND table.SomeCol = SomeOtherValue
UNION ALL
... repeat for other days
My preference would probably be either the 1st or 2nd form.
A query must return before-known columns. If the tables table1 and table2 have different columns, you cannot write one query that returns, say, table1's five columns one day and table2's nine columns another.
As long as the resulting columns stay the same, however, you can join optionally. Here is a small example:
select
p.project_id,
p.project_name,
coalesce(c1.name, c2.name) as team_member,
coalesce(c1.salary, c2.salary * (1.0 + c2.tax / 100.0)) as team_member_salary,
coalesce(c1.job_name, c2.job_title) as team_member_job
from project p
left join crew1 c1 on c1.project_id = p.project_id and datepart(dw, getdate()) = 4
left join crew2 c2 on c2.project_id = p.project_id and datepart(dw, getdate()) <> 4
order by p.project_id;
Not sure I really understand what you are trying to achieve here. This example assumes that you are reading from different tables based on the date and will return all of the rows from both tables:
select * from (
select 'HELLO' as col1, * from table1 where DATEPART(DW, GETDATE()) = 4
UNION ALL
select 'GOODBYE' as col1, * from table2 where DATEPART(DW, GETDATE()) <> 4
)
order by col1

How do I calculate coverage dates during a period of time in SQL against a transactional table?

I'm attempting to compile a list of date ranges like so:
Coverage Range: 10/1/2016 - 10/5/2016
Coverage Range: 10/9/2016 - 10/31/2016
for each policy in a database table. The table is transactional, and there is one cancellation transaction code, but three codes that can indicate coverage has begun. Also, there can be instances where the codes that indicate start of coverage can occur in sequence (start on 10/1, then another start on 10/5, then cancel on 10/14). Below is an example of a series of transactions that I would like to generate the above results from:
TransID PolicyID EffDate
NewBus 1 9/15/2016
Confirm 1 9/17/2016
Cancel 1 10/5/2016
Reinst 1 10/9/2016
Cancel 1 10/15/2016
Reinst 1 10/15/2016
PolExp 1 3/15/2017
SO in this dataset, I want the following results for the date range 10/1 - 10/31
Coverage Range: 10/1/2016 - 10/5/2016
Coverage Range: 10/9/2016 - 10/31/2016
Note that since the cancel and reinstatement happen on the same day, I'm excluding them from the results set. I tried pairing the transactions with subqueries:
CONVERT(varchar(10),
CASE WHEN overall.sPTRN_ID in (SELECT code FROM #cancelTransCodes)
-- This is a coverage cancellationentry
THEN -- Set coverage start date using previous paired record
CASE WHEN((SELECT MAX(inn.PD_EffectiveDate) FROM PolicyData inn WHERE inn.sPTRN_ID in (SELECT code FROM #startCoverageTransCodes)
and inn.PD_EffectiveDate <= overall.PD_EffectiveDate
and inn.PD_PolicyCode = overall.PD_PolicyCode) < #sDate) THEN #sDate
ELSE
(SELECT MAX(inn.PD_EffectiveDate) FROM PolicyData inn WHERE inn.sPTRN_ID in (SELECT code FROM #startCoverageTransCodes)
and inn.PD_EffectiveDate <= overall.PD_EffectiveDate
and inn.PD_PolicyCode = overall.PD_PolicyCode)
END
ELSE -- Set coverage start date using current record
CASE WHEN (overall.PD_EffectiveDate < #sDate) THEN #sDate ELSE overall.PD_EffectiveDate END END, 101)
as [Effective_Date]
This mostly works except for the situation I listed above. I'd rather not rewrite this query if I can help it. I have a similar line for expiration date:
ISNULL(CONVERT(varchar(10),
CASE WHEN overall.sPTRN_ID in (SELECT code FROM #cancelTransCodes) -- This is a coverage cancellation entry
THEN -- Set coverage end date with current record
overall.PD_EffectiveDate
ELSE -- check for future coverage end dates
CASE WHEN
(SELECT COUNT(*) FROM PolicyData pd WHERE pd.PD_EffectiveDate > overall.PD_EffectiveDate and pd.sPTRN_ID in (SELECT code FROM #cancelTransCodes)) > 1
THEN -- There are future end dates
CASE WHEN((SELECT TOP 1 pd.PD_ExpirationDate FROM PolicyData pd
WHERE pd.PD_PolicyCode = overall.PD_PolicyCode
and pd.PD_EntryDate between #sDate and #eDate
and pd.sPTRN_ID in (SELECT code FROM #cancelTransCodes))) > #eDate
THEN #eDate
ELSE
(SELECT TOP 1 pd.PD_ExpirationDate FROM PolicyData pd
WHERE pd.PD_PolicyCode = overall.PD_PolicyCode
and pd.PD_EntryDate between #sDate and #eDate
and pd.sPTRN_ID in (SELECT code FROM #cancelTransCodes))
END
ELSE -- No future coverage end dates
CASE WHEN(overall.PD_ExpirationDate > #eDate) THEN #eDate ELSE overall.PD_ExpirationDate END
END
END, 101), CONVERT(varchar(10), CASE WHEN(overall.PD_ExpirationDate > #eDate) THEN #eDate ELSE overall.PD_ExpirationDate END, 101))
as [Expiration_Date]
I can't help but feel like there's a simpler solution I'm missing here. So my question is: how can I modify the above portion of my query to accomodate the above scenario? OR What is the better answer? If I cam simplify this, I would love to hear how.
Here's the solution I ended up implementing
I took a simplified table where I boiled all the START transaction codes to START and all the cancel transaction codes to CANCEL. When I viewed the table based on that, it was MUCH easier to watch how my logic affected the results. I ended up using a simplified system where I used CASE WHEN clauses to identify specific scenarios and built my date ranges based on that. I also changed my starting point away from looking at cancellations and finding the related starts, and reversing it (find starts and then related calcellations). So here's the code I implemented:
/* Get Coverage Dates */
,cast((CASE WHEN sPTRN_ID in (SELECT code FROM #startCoverageTransCodes) THEN
CASE WHEN (cast(overall.PD_EntryDate as date) <= #sDate) THEN #sDate
WHEN (cast(overall.PD_EntryDate as date) > #sDate AND cast(overall.PD_EntryDate as date) <= #eDate) THEN overall.PD_EntryDate
WHEN (cast(overall.PD_EntryDate as date) > #eDate) THEN #eDate
ELSE cast(overall.PD_EntryDate as date) END
ELSE
null
END) as date) as Effective_Date
,cast((CASE WHEN sPTRN_ID in (SELECT code FROM #startCoverageTransCodes) THEN
CASE WHEN (SELECT MIN(p.PD_EntryDate) FROM PolicyData p WITH (NOLOCK) WHERE p.sPTRN_ID in (SELECT code FROM #cancelTransCodes) AND p.PD_EntryDate > overall.PD_EntryDate AND p.PD_PolicyCOde = overall.PD_PolicyCode) > #eDate THEN #eDate
ELSE ISNULL((SELECT MIN(p.PD_EntryDate) FROM PolicyData p WITH (NOLOCK) WHERE p.sPTRN_ID in (SELECT code FROM #cancelTransCodes) AND p.PD_EntryDate > overall.PD_EntryDate AND p.PD_PolicyCOde = overall.PD_PolicyCode), #eDate) END
ELSE
CASE WHEN (SELECT MAX(p.PD_EntryDate) FROM PolicyData p WITH (NOLOCK) WHERE p.sPTRN_ID in (SELECT code FROM #startCoverageTransCodes) AND p.PD_EntryDate > overall.PD_EntryDate AND p.PD_PolicyCOde = overall.PD_PolicyCode) > #eDate THEN #eDate
ELSE (SELECT MAX(p.PD_EntryDate) FROM PolicyData p WITH (NOLOCK) WHERE p.sPTRN_ID in (SELECT code FROM #startCoverageTransCodes) AND p.PD_EntryDate > overall.PD_EntryDate AND p.PD_PolicyCOde = overall.PD_PolicyCode)
END END) as date) as Expiration_Date
As you can see, I relied on subqueries in this case. I had a lot of this logic as joins, which caused extra rows where I didn't need them. So by making the date range logic based on sub-queries, I ended up speeding the stored procedure up by several seconds, bringing my execution time to under 1 second where before it was between 2-5 seconds.
There might be a simpler solution, but I just do not see it right now.
The outline for each step is:
Generate dates for date range, which you do not need to do if you have a calendar table.
Transform the incoming data set as you described in your question (skipping start/cancel on the same day); and add the next EffDate for each row.
Explode the data set with a row for each day between the generated ranges of step 2.
Reduce the data set back down based on consecutive days of converage.
test setup: http://rextester.com/GUNSO45644
/* set date range */
declare #fromdate date = '20161001'
declare #thrudate date = '20161031'
/* generate dates in range -- you can skip this if you have a calendar table */
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
select top (datediff(day, #fromdate, #thrudate)+1)
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1, #fromdate))
from n as deka
cross join n as hecto /* 100 days */
cross join n as kilo /* 2.73 years */
cross join n as [tenK] /* 27.3 years */
order by [Date]
)
/* reduce test table to desired input*/
, pol as (
select
Coverage = case when max(TransId) in ('Cancel','PolExp')
then 0 else 1 end
, PolicyId
, EffDate = case when max(TransId) in ('Cancel','PolExp')
then dateadd(day,1,EffDate) else EffDate end
, NextEffDate = oa.NextEffDate
from t
outer apply (
select top 1
NextEffDate = case
when i.TransId in ('Cancel','PolExp')
then dateadd(day,1,i.EffDate)
else i.EffDate
end
from t as i
where i.PolicyId = t.PolicyId
and i.EffDate > t.EffDate
order by
i.EffDate asc
, case when i.TransId in ('Cancel','PolExp') then 1 else 0 end desc
) as oa
group by t.PolicyId, t.EffDate, oa.NextEffDate
)
/* explode desired input by day, add row_numbers() */
, e as (
select pol.PolicyId, pol.Coverage, d.Date
, rn_x = row_number() over (
partition by pol.PolicyId
order by d.Date
)
, rn_y = row_number() over (
partition by pol.PolicyId, pol.Coverage
order by d.date)
from pol
inner join dates as d
on d.date >= pol.EffDate
and d.date < pol.NextEffDate
)
/* reduce to date ranges where Coverage = 1 */
select
PolicyId
, FromDate = convert(varchar(10),min(Date),120)
, ThruDate = convert(varchar(10),max(Date),120)
from e
where Coverage = 1
group by PolicyId, (rn_x-rn_y);
returns:
+----------+------------+------------+
| PolicyId | FromDate | ThruDate |
+----------+------------+------------+
| 1 | 2016-10-01 | 2016-10-05 |
| 1 | 2016-10-09 | 2016-10-31 |
+----------+------------+------------+

correct count of grouped results

I have a procedure:
ALTER PROCEDURE [dbo].[GetActualFeedbackQueueTree]
#dtNow datetime
as
BEGIN
select
count(f.Id) as [Total],
f.AccountCode,
f.AccountName,
f.Utc,
f2.CityCode,
f2.CityName
from
InnerPortal.Feedback.QueueFeedback f
left join
InnerPortal.Feedback.QueueFeedback f2
on
f2.AccountCode = f.AccountCode
where
(f.Done is null or f.Done = 0) and
(f.Busy is NULL or f.Busy = 0) and
((DATEPART(hour, DATEADD(HOUR, f.Utc, #dtNow)) >= 9 ) and
(DATEPART(hour, DATEADD(HOUR, f.Utc, #dtNow)) <= 20))
group by
f.AccountCode, f2.CityCode,
f2.CityName, f.AccountName, f.Utc
END
I group rows by AccountName and by CityName. As result we have something like a tree. The problem is the [Total] not calculates correctly.
Then I get a select for a special AccountCode the count if much less then get me as result the procedure. For example:
select count(f.Id) from Feedback.QueueFeedback f where f.AccountCode = '01507'
returns 16 rows but the procedure result is 256.
The target is to get a count of collected rows with the same account. How to make it work correctly?
Thanks.
Software: T-Sql, Ms Sql server 2012
Pretty sure you want
count(distinct(f.Id))

SQL Merging 4 Queries to one

Im having a slight issue merging the following statements
declare #From DATE
SET #From = '01/01/2014'
declare #To DATE
SET #To = '31/01/2014'
--ISSUED SB
SELECT
COUNT(pm.DateAppIssued) AS Issued,
pm.Lender,
pm.AmountRequested,
p.CaseTypeID
FROM BPS.dbo.tbl_Profile_Mortgage AS pm
INNER JOIN BPS.dbo.tbl_Profile AS p
ON pm.FK_ProfileId = p.Id
WHERE CaseTypeID = 2
AND (CONVERT(DATE,DateAppIssued, 103)
Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103))
And Lender > ''
GROUP BY pm.Lender,p.CaseTypeID,pm.AmountRequested;
--Paased
SELECT
COUNT(pm.DatePassed) AS Passed,
pm.Lender,
pm.AmountRequested,
p.CaseTypeID
FROM BPS.dbo.tbl_Profile_Mortgage AS pm
INNER JOIN BPS.dbo.tbl_Profile AS p
ON pm.FK_ProfileId = p.Id
WHERE CaseTypeID = 2
AND (CONVERT(DATE,DatePassed, 103)
Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103))
And Lender > ''
GROUP BY pm.Lender,p.CaseTypeID,pm.AmountRequested;
--Received
SELECT
COUNT(pm.DateAppRcvd) AS Received,
pm.Lender,
pm.AmountRequested,
p.CaseTypeID
FROM BPS.dbo.tbl_Profile_Mortgage AS pm
INNER JOIN BPS.dbo.tbl_Profile AS p
ON pm.FK_ProfileId = p.Id
WHERE CaseTypeID = 2
AND (CONVERT(DATE,DateAppRcvd, 103)
Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103))
And Lender > ''
GROUP BY pm.Lender,p.CaseTypeID,pm.AmountRequested;
--Offered
SELECT
COUNT(pm.DateOffered) AS Offered,
pm.Lender,
pm.AmountRequested,
p.CaseTypeID
FROM BPS.dbo.tbl_Profile_Mortgage AS pm
INNER JOIN BPS.dbo.tbl_Profile AS p
ON pm.FK_ProfileId = p.Id
WHERE CaseTypeID = 2
AND (CONVERT(DATE,DateOffered, 103)
Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103))
And Lender > ''
GROUP BY pm.Lender,p.CaseTypeID,pm.AmountRequested;
Ideally I would like the result of theses query's to show as follows
Issued, Passed , Offered, Received,
All in one table
Any Help on this would be greatly appreciated
Thanks
Rusty
I'm fairly certain in this case the query can be written without the use of any CASE statements, actually:
DECLARE #From DATE = '20140101'
declare #To DATE = '20140201'
SELECT Mortgage.lender, Mortgage.amountRequested, Profile.caseTypeId,
COUNT(Issue.issued) as issued,
COUNT(Pass.passed) as passed,
COUNT(Receive.received) as received,
COUNT(Offer.offered) as offered
FROM BPS.dbo.tbl_Profile_Mortgage as Mortgage
JOIN BPS.dbo.tbl_Profile as Profile
ON Mortgage.fk_profileId = Profile.id
AND Profile.caseTypeId = 2
LEFT JOIN (VALUES (1, #From, #To)) Issue(issued, rangeFrom, rangeTo)
ON Mortgage.DateAppIssued >= Issue.rangeFrom
AND Mortgage.DateAppIssued < Issue.rangeTo
LEFT JOIN (VALUES (2, #From, #To)) Pass(passed, rangeFrom, rangeTo)
ON Mortgage.DatePassed >= Pass.rangeFrom
AND Mortgage.DatePassed < Pass.rangeTo
LEFT JOIN (VALUES (3, #From, #To)) Receive(received, rangeFrom, rangeTo)
ON Mortgage.DateAppRcvd >= Receive.rangeFrom
AND Mortgage.DateAppRcvd < Receive.rangeTo
LEFT JOIN (VALUES (4, #From, #To)) Offer(offered, rangeFrom, rangeTo)
ON Mortgage.DateOffered >= Offer.rangeFrom
AND Mortgage.DateOffered < Offer.rangeTo
WHERE Mortgage.lender > ''
AND (Issue.issued IS NOT NULL
OR Pass.passed IS NOT NULL
OR Receive.received IS NOT NULL
OR Offer.offered IS NOT NULL)
GROUP BY Mortgage.lender, Mortgage.amountRequested, Profile.caseTypeId
(not tested, as I lack a provided data set).
... Okay, some explanations are in order, because some of this is slightly non-intuitive.
First off, read this blog entry for tips about dealing with date/time/timestamp ranges (interestingly, this also applies to all other non-integral types). This is why I modified the #To date - so the range could be safely queried without needing to convert types (and thus ignore indices). I've also made sure to choose a safe format - depending on how you're calling this query, this is a non issue (ie, parameterized queries taking an actual Date type are essentially format-less).
......
COUNT(Issue.issued) as issued,
......
LEFT JOIN (VALUES (1, #From, #To)) Issue(issued, rangeFrom, rangeTo)
ON Mortgage.DateAppIssued >= Issue.rangeFrom
AND Mortgage.DateAppIssued < Issue.rangeTo
.......
What's the difference between COUNT(*) and COUNT(<expression>)? If <expression> evaluates to null, it's ignored. Hence the LEFT JOINs; if the entry for the mortgage isn't in the given date range for the column, the dummy table doesn't attach, and there's no column to count. Unfortunately, I'm not sure how the interplay between the dummy table, LEFT JOIN, and COUNT() here will appear to the optimizer - the joins should be able to use indices, but I don't know if it's smart enough to be able to use that for the COUNT() here too....
(Issue.issued IS NOT NULL
OR Pass.passed IS NOT NULL
OR Receive.received IS NOT NULL
OR Offer.offered IS NOT NULL)
This is essentially telling it to ignore rows that don't have at least one of the columns. They wouldn't be "counted" in any case (well, they'd likely have 0) - there's no data for the function to consider - but they would show up in the results, which probably isn't what you want. I'm not sure if the optimizer is smart enough to use this to restrict which rows it operates over - that is, turn the JOIN conditions into a way to restrict the various date columns, as if they were in the WHERE clause too. If the query runs slow, try adding the date restrictions to the WHERE clause and see if it helps.
You could either as Dan Bracuk states use a union, or you could use a case-statement.
declare #From DATE = '01/01/2014'
declare #To DATE = '31/01/2014'
select
sum(case when (CONVERT(DATE,DateAppIssued, 103) Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103)) then 1 else 0 end) as Issued
, sum(case when (CONVERT(DATE,DatePassed, 103) Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103)) then 1 else 0 end) as Passed
, sum(case when (CONVERT(DATE,DateAppRcvd, 103) Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103)) then 1 else 0 end) as Received
, sum(case when (CONVERT(DATE,DateOffered, 103) Between CONVERT(DATE,#From,103) and CONVERT(DATE,#To,103)) then 1 else 0 end) as Offered
, pm.Lender
, pm.AmountRequested
, p.CaseTypeID
FROM BPS.dbo.tbl_Profile_Mortgage AS pm
INNER JOIN BPS.dbo.tbl_Profile AS p
ON pm.FK_ProfileId = p.Id
WHERE CaseTypeID = 2
And Lender > ''
GROUP BY pm.Lender,p.CaseTypeID,pm.AmountRequested;
Edit:
What I've done is looked at your queries.
All four queries have identical Where Clause, with the exception of the date comparison. Therefore I've created a new query, which selects all your data which might be used in one of the four counts.
The last clause; the data-comparison, is moved into a case statement, returning 1 if the row is between the selected date-range, and 0 otherwise. This basically indicates whether the row would be returned in your previous queries.
Therefore a sum of this column would return the equivalent of a count(*), with this date-comparison in the where-clause.
Edit 2 (After comments by Clockwork-muse):
Some notes on performance, (tested on MS-SQL 2012):
Changing BETWEEN to ">=" and "<" inside a case-statement does not affect the cost of the query.
Depending on the size of the table, the query might be optimized quite a lot, by adding the dates in the where clause.
In my sample data (~20.000.000 rows, spanning from 2001 to today), i got a 48% increase in speed by adding.
or (DateAppIssued BETWEEN #From and #to )
or (DatePassed BETWEEN #From and #to )
or (DateAppRcvd BETWEEN #From and #to )
or (DateOffered BETWEEN #From and #to )
(There were no difference using BETWEEN and ">=" and "<".)
It is also worth nothing that i got a 6% increase when changing the #From = '01/01/2014' to #From '2014-01-01' and thus omitting the convert().
Eg. an optimized query could be:
declare #From DATE = '2014-01-01'
declare #To DATE = '2014-01-31'
select
sum(case when (DateAppIssued >= #From and DateAppIssued < #To) then 1 else 0 end) as Issued
, sum(case when (DatePassed >= #From and DatePassed < #To) then 1 else 0 end) as Passed
, sum(case when (DateAppRcvd >= #From and DateAppRcvd < #To) then 1 else 0 end) as Received
, sum(case when (DateOffered >= #From and DateOffered < #To) then 1 else 0 end) as Offered
, pm.Lender
, pm.AmountRequested
, p.CaseTypeID
FROM BPS.dbo.tbl_Profile_Mortgage AS pm
INNER JOIN BPS.dbo.tbl_Profile AS p
ON pm.FK_ProfileId = p.Id
WHERE 1=1
and CaseTypeID = 2
and Lender > ''
and (
(DateAppIssued >= #From and DateAppIssued < #To)
or (DatePassed >= #From and DatePassed < #To)
or (DateAppRcvd >= #From and DateAppRcvd < #To)
or (DateOffered >= #From and DateOffered < #To)
)
GROUP BY pm.Lender,p.CaseTypeID,pm.AmountRequested;
I do however really like Clockwork-muse's answer, as I prefer joins to case-statements, where posible :)
The all-in-one queries here in other answers are certainly elegant, but if you are in a rush to get something working as a one-off, or if you agree the following approach is easy to read and maintain when you have to revisit it some time down the road (or someone else less skilled has to work out what's going on) - here's a skeleton of a Common Table Expression alternative which I believe is quite clear to read :
WITH Unioned_Four AS
( SELECT .. -- first select : Issued
UNION ALL
SELECT .. -- second : Passed
UNION ALL
SELECT .. -- Received
UNION ALL
SELECT .. -- Offered
)
SELECT
-- group fields
-- SUMs of the count fields
FROM Unioned_Four
GROUP BY .. -- etc
Obviously the fields have to match in the 4 parts of the UNION, requiring dummy fields returning zero in each one.
So you could have kept the simple approach that you started with, but wrapped it up as a derived table using the CTE syntax to allow you to have the four counts all on one row per GROUPing. Also if you have to add extra filtering to specific queries of the four, then it's easier to meddle with the individual SELECTs - the flipside being (of course) that further requirements for all four would need to be duplicated!

I'm trying to make an union of selects with a while loop, getting errors

declare #year int
set #year = 2009
while(year <= 2020)
begin
SELECT A.dsformfieldvalue as Instrumento ,
AVG(DATEDIFF(day, B.DTSTARTDATE , C.DTENDDATE)) as TempoMedio ,
'Jul/2009 a Dez/2009' as Periodo
from wfflow_form_field_log A
right join wfflow_execute_task B on A.codflowexecute = B.codflowexecute
right join wfflow_execute_task C on B.codflowexecute = C.codflowexecute
where A.codflow in (326, 439)
and A.codfield = 2498
and B.codtask = 7064
and C.codtask = 7095
and CONVERT(CHAR(4), B.DTSTARTDATE, 120) = #year
and CONVERT(CHAR(4), B.DTSTARTDATE, 100) in ('Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
set #year = #year + 1
group by A.dsformfieldvalue
union all
end
Basically what I'm trying to do is drastically reduce the number of repeated code, as I have to union a whole bunch of selects. I'm trying with a while loop but it is not working. Any input?
I think what you're trying to do is this
select *
from wfflow_form_field_log A
right join wfflow_execute_task B on
A.codflowexecute = B.codflowexecute
where year(b.dtstartdate) between 2009 and 2020
and month(b.dtstartdate)>=7
and ... -- other filters
Your SQL is syntactically invalid. The general form of a select1 statement is
select <result-columns>
from <from-clause-including-joins>
where <where-criteria>
group by <group-by-criteria>
having <having-criteria>
order by <order-by-criteria>
The general form of union/union all is
<select-statement>
UNION [ALL]
<select-statement>
...
where each select statement returns the same number of columns and the corresponding columns in each select statement are of the same type or of a type implicitly convertible to the type of the column in the 1st select statement.
You've placed your statement
set #year = #year + 1
so that it is a part of the select statement.
Further you've added a UNION ALL with no select statement following.
Fix those problems and you should be good. But...as another answer pointed out, what you're really trying to do is select a range of years. You don't need a union: you just need suitable grouping.
If it was my query, I'd do something like this (if I'm understanding your intent correctly):
declare #dtFrom datetime = '2009-01-01 00:00:00.000' -- start of period
declare #dtThru datetime = '2020-12-31 23:59:59.996' -- end of period
select year = datepart(year,b.dtstartdate) ,
period = case datepart(month,b.dtstartdate) / 6 when 1 then 'Jan-Jun' else 'Jul-Dec' end ,
instrumento = a.dsformfieldvalue ,
tempomento = avg( datediff(day, b.dtstartdate , c.dtenddate ) )
from wfflow_form_field_log a
right join wfflow_execute_task b on A.codflowexecute = B.codflowexecute
and B.codtask = 7064
and b.dtStartDate between #dtFrom and #dtThru
right join wfflow_execute_task d on B.codflowexecute = C.codflowexecute
and C.codtask = 7095
where A.codflow in (326, 439)
and A.codfield = 2498
group by datepart(year,b.dtStartDate) ,
case datepart(month,b.dtstartdate) / 6 when 1 then 'Jan-Jun' else 'Jul-Dec' end ,
A.dsformfieldvalue
order by 1,2,3 -- order by the first 3 columns (the grouping columns)
Easy!