Alternative to cast function in join when it comes to stored proc optimisation? - sql

What is the best alternative for cast function used to convert datatype in SQL query inside stored proc where join is used?
I have read often that its not good to use function and need help with optimization of query below.
SELECT
A.DATE,
A.Value curr ,
ISNULL( CAST( CONVERT ( DECIMAL(18,4), CASE WHEN #INV = 0 THEN NULLIF(ISNULL(tblRate.currBid,0),0) ELSE 1.00 / NULLIF(ISNULL(tblRate.currBid,0) ,0) END ) AS VARCHAR) ,#TempHardcodeValue) AS currBid,
A.Value +' - '+ A.currName As ConcatenatedCurr
FROM #TEMPTable A
LEFT JOIN tblCurrRates (NOLOCK) tblRate ON A.Date = cast(tblRate.currDate as date) AND A.currCode = tblRate.currCode
ORDER BY A.Date ,A.Value
As you can see cast function is used, so is there any other way to reqrite without cast?

Actually, SQL Server is probably okay with the cast. The one exception to using functions is casting date/times to dates -- SQL Server recognizes the conversion for the use of indexes.
But, you can do:
FROM #TEMPTable A LEFT JOIN
tblCurrRates r
ON r.currDate >= A.Date AND
r.currDate < DATEADD(day, 1, A.Date) AND
r.currCode = a.currCode
This would allow the code to use an index on tblCurrRates(currCode, currDate). I suspect, though, that your code would also use that index.

Related

SQL Calculate 20 business days, remove bank hols and reference to another query

Sorry I am quite new to creating functions in SQL Server 2008 R2. I have largely been able to get by using T-SQL statements.
However I need to create a report that returns records with a date (program start date), that part is simple enough, however for each row date I want to calculate a target completion date based on 20 business days. I also want to avoid counting bank holidays too. I have a table named dCalendar which holds every day for the last few year with flags saying whether each day is a workday or bank holiday.
I have found lots of stuff on how to calculate the number of business days between two dates but this is more tricky.
I have created this function
ALTER function [warehouse].[MS_fnAddBusinessDays]
(#StartDate datetime,
#nDays int)
RETURNS TABLE
AS
RETURN
(SELECT calDt
FROM
(SELECT
ROW_NUMBER() OVER (ORDER BY calDt ASC) AS rownumber,
calDt
FROM
warehouse.dCalendar
WHERE
(calDt >= #StartDate)
AND (weekDayFg = 1)
AND (BankHolidayFg = 0)) AS Results
WHERE
(rownumber = #nDays)
and can call it using the following
SELECT *
FROM
(SELECT
ROW_NUMBER() OVER (ORDER BY calDt ASC) AS rownumber,
calDt, BankHolidayFg, weekDayFg, dateStr
FROM
warehouse.dCalendar
WHERE
(calDt >= CONVERT(DATETIME, '2016-12-11 00:00:00', 102))
AND (weekDayFg = 1) AND (BankHolidayFg = 0)) AS TblResults
WHERE
(rownumber = 20)
I just cannot work out how to embed this within the following example where progStartDate is the date i want to calculate the target date from
SELECT
end_user.fContactVwDn.client_no,
end_user.fProgrammeVwDn.prtyProgrammeType,
end_user.fProgrammeVwDn.progStartDate,
SUM(CASE WHEN comtContactMeetingType = 'Visit' THEN 1 ELSE 0 END) AS InitialVisitTotal,
MAX(CASE WHEN comtContactMeetingType = 'Visit' THEN contPlannedFromDate ELSE 0 END) AS InitialVisitDate
FROM
end_user.fContactVwDn
INNER JOIN
warehouse.fContactProgramme ON end_user.fContactVwDn.contKy = warehouse.fContactProgramme.contKy
INNER JOIN
end_user.fProgrammeVwDn ON warehouse.fContactProgramme.progGuid = end_user.fProgrammeVwDn.progprogGuid
GROUP BY
end_user.fProgrammeVwDn.prtyProgrammeType,
end_user.fProgrammeVwDn.progStartDate, end_user.fContactVwDn.client_no
HAVING
(end_user.fProgrammeVwDn.prtyProgrammeType = 'Application')
AND (end_user.fProgrammeVwDn.progStartDate > CONVERT(DATETIME, '2016-07-01 00:00:00', 102))
Any help would be much appreciated.
The function looks OK. I would add TOP, though, to avoid scanning the whole Calendar table. I hope, calDt is a primary key.
ALTER function [warehouse].[MS_fnAddBusinessDays]
(#StartDate datetime,
#nDays int)
RETURNS TABLE
AS
RETURN
(
SELECT calDt
FROM
(
SELECT TOP(#nDays)
ROW_NUMBER() OVER (ORDER BY calDt ASC) AS rownumber,
calDt
FROM
warehouse.dCalendar
WHERE
(calDt >= #StartDate)
AND (weekDayFg = 1)
AND (BankHolidayFg = 0)
ORDER BY calDt ASC
) AS Results
WHERE
rownumber = #nDays
)
This function is inline table-valued function, which means that it returns a table, not a scalar value. This is good, because scalar functions usually make queries slow, but inline (single-statement) table-valued functions can be inlined by the optimiser.
To call such function use CROSS APPLY. It was originally introduced to SQL Server specifically for calling table-valued functions, but it can do much more (it is so called lateral join).
I wrapped your original query in a CTE to make the final query readable.
WITH
CTE
AS
(
SELECT
end_user.fContactVwDn.client_no,
end_user.fProgrammeVwDn.prtyProgrammeType,
end_user.fProgrammeVwDn.progStartDate,
SUM(CASE WHEN comtContactMeetingType = 'Visit' THEN 1 ELSE 0 END) AS InitialVisitTotal,
MAX(CASE WHEN comtContactMeetingType = 'Visit' THEN contPlannedFromDate ELSE 0 END) AS InitialVisitDate
FROM
end_user.fContactVwDn
INNER JOIN warehouse.fContactProgramme ON end_user.fContactVwDn.contKy = warehouse.fContactProgramme.contKy
INNER JOIN end_user.fProgrammeVwDn ON warehouse.fContactProgramme.progGuid = end_user.fProgrammeVwDn.progprogGuid
GROUP BY
end_user.fProgrammeVwDn.prtyProgrammeType,
end_user.fProgrammeVwDn.progStartDate,
end_user.fContactVwDn.client_no
HAVING
(end_user.fProgrammeVwDn.prtyProgrammeType = 'Application')
AND (end_user.fProgrammeVwDn.progStartDate > CONVERT(DATETIME, '2016-07-01 00:00:00', 102))
)
SELECT
CTE.client_no,
CTE.prtyProgrammeType,
CTE.progStartDate,
CTE.InitialVisitTotal,
CTE.InitialVisitDate,
F.calDt
FROM
CTE
CROSS APPLY [warehouse].[MS_fnAddBusinessDays](CTE.progStartDate, 20) AS F
;

Optimizing SQL Server Query (CTE)

This query takes forever to run. Does anybody have any good tips on how I can optimize it?
WITH CTE (Lockindate, Before5, After5) AS (SELECT nl.Lockindate,
(CASE WHEN CAST(RIGHT(FirstLockActivity,8) AS time(1)) <= '17:00' THEN
'Before 5 PM' END) AS before5,
(CASE WHEN CAST(RIGHT(FirstLockActivity,8) AS time(1)) >= '17:00' THEN
'After 5 PM' END) AS after5
FROM netlock nl WITH(NOLOCK)
JOIN rate rs WITH(NOLOCK)
ON nl.id=rs.id
WHERE nl.lockindate BETWEEN '2016-08-01' AND '2016-08-31')
SELECT lockindate, COUNT(After5), COUNT(Before5)
FROM CTE
GROUP BY lockindate
Although it won't speed up things all that much IN THIS CASE (*), the conversions from one datatype to another and then another again are 'not optimal'.
(CASE WHEN CAST(RIGHT(FirstLockActivity,8) AS time(1)) <= '17:00' THEN 'Before 5 PM' END) AS before5,
First of all you do an implicit conversion from a datetime [FirstLockActivity] to a string. The reason for this being that the Right() function expects a string, and hence the conversion.
Doing implicit conversions can be dangerous. Depending on the configuration of your server (and even of your connection which by itself might be influenced by the regional settings of your operating system) this can result in surprising results, not all of which will have the expected last 8 characters!
FYI: have a look here: https://msdn.microsoft.com/en-us/library/ms187928.aspx?f=255&MSPPError=-2147217396 . As you can't explicitly pass the 'style' with CAST I've always suggested to people to rather use Convert(), ESPECIALLY when converting from datetimes to string, and vice versa.
After that you take the right-most 8 characters and then convert these into a time(1). I'm not sure why you want to use a time(1) over a time(0) since you're only interested in the hour part, but in the end it won't make much difference I guess.
Anyway, doing all these conversions takes CPU and thus time.
Assuming this thing isn't too dumbed down from what you really want to do, the target of the query is to return an indication on how many entries there were before and after 5 PM for each lockindate. As such, all you need to do is calculate the hour of each FirstLockActivy and decide from there.
=> Hour(xx) and DatePart(hour, xx) will both return the required information for a fraction of the CPU-cost of those conversions.
Also, you can easily get the before/after in one CASE construction.
WITH CTE (LockinDate, Before5PM)
AS (SELECT Lockindate,
(CASE WHEN Hour(FirstLockActivity) < 17 THEN 1 ELSE 0 END) AS Before5PM
FROM netlock nl WITH (NOLOCK)
JOIN rate rs WITH (NOLOCK)
ON nl.id=rs.id
WHERE nl.lockindate BETWEEN Convert(datetime, '2016-08-01', 105) AND Convert(datetime, '2016-08-31', 105))
SELECT LockinDate,
After5 = SUM(1 - Before5PM),
Before5 = SUM(Before5PM)
FROM CTE
GROUP BY LockinDate
Assuming the amount of (relevant) records in the tables is huge, then this will have some effect on the duration, but given the speed of modern processors it probably won't be shocking. Off course, when you're on a busy server and there isn't all that much (free) CPU going around, then the effect will be much more noticable.
That said, as for performance I'd suggest to check the indexes on the netlock and rate table. Ideally netlock has a clustered index (or PK) on the id field and a non-clustered index on lockindate. Additionally, the rate table has a clustered index on the id field with some additional other fields I'm not aware of. If the latter isn't the case, having a non-clustered index on the id field with the FirstLockActivity field in the included column would be great too.
If you want to have this query without the CTE, you can simply copy paste the CTE into a subquery/derived table like this:
SELECT LockinDate,
After5 = SUM(1 - Before5PM),
Before5 = SUM(Before5PM)
FROM (SELECT Lockindate,
(CASE WHEN Hour(FirstLockActivity) < 17 THEN 1 ELSE 0 END) AS Before5PM
FROM netlock nl WITH (NOLOCK)
JOIN rate rs WITH (NOLOCK)
ON nl.id=rs.id
WHERE nl.lockindate BETWEEN Convert(datetime, '2016-08-01', 105) AND Convert(datetime, '2016-08-31', 105)) A
GROUP BY LockinDate
Or, worked out a little more you'd get
SELECT LockinDate,
After5 = SUM(1 - (CASE WHEN Hour(FirstLockActivity) < 17 THEN 1 ELSE 0 END)),
Before5 = SUM( (CASE WHEN Hour(FirstLockActivity) < 17 THEN 1 ELSE 0 END))
FROM netlock nl WITH (NOLOCK)
JOIN rate rs WITH (NOLOCK)
ON nl.id=rs.id
WHERE nl.lockindate BETWEEN Convert(datetime, '2016-08-01', 105) AND Convert(datetime, '2016-08-31', 105)) A
GROUP BY LockinDate
PS: wrote all this in the browser, there might be some typo's and none the code is tested, prepare to have to fiddle around a bit to get it working =)
PS: if you can't get the query plan, you might still be able to use SET STATISTICS TIME to more easily compare one version of the query to another.
(*: In case you do this kind of conversions inside the WHERE clause or a JOIN clause, it will confuse the optimizer and the results could be devastating for the performance of your query)
You don't need a CTE:
SELECT nl.Lockindate,
SUM(CASE WHEN CAST(RIGHT(FirstLockActivity,8) AS time(1)) <= '17:00' THEN 1 ELSE 0 END) AS before5,
SUM(CASE WHEN CAST(RIGHT(FirstLockActivity,8) AS time(1)) > '17:00' THEN 1 ELSE 0 END) AS after5
FROM netlock nl WITH(NOLOCK)
JOIN rate rs WITH(NOLOCK)
ON nl.id=rs.id
WHERE nl.lockindate BETWEEN '2016-08-01' AND '2016-08-31'
GROUP BY nk.lockindate
But this might result in exactly the same plan. Then you need to check why it's slow (missing indexes on the join/group by columns?)

SQL Server DATE conversion error

Here is my query:
SELECT
*
FROM
(SELECT
A.Name, AP.PropertyName, APV.Value AS [PropertyValue],
CONVERT(DATETIME, APV.VALUE, 101) AS [DateValue]
FROM dbo.Account AS A
JOIN dbo.AccountProperty AS AP ON AP.AccountTypeId = A.AccountTypeId
JOIN dbo.AccountPropertyValue AS APV ON APV.AccountPropertyId = APV.AccountPropertyId
AND APV.AccountId = A.AccountId
WHERE
A.AccountTypeId = '19602AEF-27B2-46E6-A068-7E8C18B0DD75' --VENDOR
AND AP.PropertyName LIKE '%DATE%'
AND ISDATE(APV.Value) = 1
AND LEN(SUBSTRING( REVERSE(APV.Value), 0 , CHARINDEX( '/', REVERSE(APV.Value)))) = 4 --ENSURE 4 digit year
) AS APV
WHERE
APV.DateValue < GETDATE()
It results in the following error:
Conversion failed when converting date and/or time from character string.
If you comment out the WHERE APV.DateValue < GETDATE() clause then there is no error and I get the 300+ rows. When I enable the WHERE clause I get the error.
So you are going to tell me my data is jacked up right? Well that's what I thought, so I tried to figure out where the problem in the data was, so I started using TOP() to isolate the location. Problem was once I use the TOP() function the error went away, I only have 2000 rows of data to begin with. So I put a ridiculous TOP(99999999) on the inner SELECT and now the entire query works.
The inner SELECT returns the same number of rows with or without the TOP().
WHY???
FYI, this is SQL that works:
SELECT
*
FROM
(SELECT TOP(99999999)
A.Name, AP.PropertyName, APV.Value AS [PropertyValue],
CONVERT(DATETIME, APV.VALUE, 101) AS [DateValue]
FROM dbo.Account AS A
JOIN dbo.AccountProperty AS AP ON AP.AccountTypeId = A.AccountTypeId
JOIN dbo.AccountPropertyValue AS APV ON APV.AccountPropertyId = APV.AccountPropertyId
AND APV.AccountId = A.AccountId
WHERE
A.AccountTypeId = '19602AEF-27B2-46E6-A068-7E8C18B0DD75' --VENDOR
AND AP.PropertyName LIKE '%DATE%'
AND ISDATE(APV.Value) = 1
AND LEN(SUBSTRING(REVERSE(APV.Value), 0 , CHARINDEX( '/', REVERSE(APV.Value)))) = 4
) AS APV
WHERE
APV.DateValue < GETDATE()
The problem that you are facing is that SQL Server can evaluate the expressions at any time during the query processing -- even before the WHERE clause gets evaluated. This can be a big benefit for performance. But, the consequence is that errors can be generated by rows not in the final result set. (This is true of divide-by-zero as well as conversion errors.)
Fortunately, SQL Server has a work-around for the conversion problem. Use try_convert():
TRY_CONVERT( DATETIME, APV.VALUE, 101) AS [DateValue]
This returns NULL rather than an error if there is a problem.
The reason why some versions work and others don't is because of the order of execution. There really isn't a way to predict what does and does not work -- and it could change if the execution plan for the query changes for other reasons (such as table statistics). Hence, use try_convert().
My guess is that your date is such that APV.VALUE contains also data that cannot be converted into a date, and should be filtered out using the other criteria?
Since SQL Server can decide to limit the data first using the criteria you have given:
APV.DateValue < CONVERT( DATETIME, GETDATE(),101)
And if there is data that cannot be converted into the date, then you will get the error.
To make it more clear, this is what is being filtered:
CONVERT( DATETIME, APV.VALUE, 101) AS [DateValue]
And if there is any data that cannot be converted into a date using 101 format, the filter using getdate() will fail, even if the row would not be included in the final result set for example because AP.PropertyName does not contain DATE.
Since you're using SQL Server 2012, using try_convert instead of convert should fix your problem
And why it works with top? In that case SQL Server cannot use the criteria from the outer query, because then the result might change, because it might affect the number of rows returned by top
Because number of records in the table < 999..99. And regarding the error it seems like SQL engine evaluates the WHERE clause after converting to date so you can try this:
SELECT *
FROM (
SELECT A.Name
, AP.PropertyName
, APV.Value AS [PropertyValue]
,
CASE
WHEN SDATE(APV.Value) = 1
THEN CONVERT( DATETIME, APV.VALUE, 101)
ELSE NULL
END AS [DateValue]
FROM dbo.Account AS A
JOIN dbo.AccountProperty AS AP
ON AP.AccountTypeId = A.AccountTypeId
JOIN dbo.AccountPropertyValue AS APV
ON APV.AccountPropertyId = APV.AccountPropertyId
AND APV.AccountId = A.AccountId
WHERE A.AccountTypeId = '19602AEF-27B2-46E6-A068-7E8C18B0DD75' --VENDOR
AND AP.PropertyName LIKE '%DATE%'
AND LEN( SUBSTRING( REVERSE(APV.Value), 0 , CHARINDEX( '/', REVERSE(APV.Value)))) = 4 --ENSURE 4 digit year
) AS APV
WHERE APV.DateValue IS NOT NULL AND APV.DateValue < GETDATE()

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!

Get date from substring in where clause of a sql select *EDITED*

I have a date stored within a text field with other text. Why didn't they just put this in a date field? I have no idea, but I do not have the power to change it now. Elsewhere in the code, I am doing this to get the records where this date is in a certain range. It works fine.
For Each i As InventoryItem In inventoryList
index = i.Notes.IndexOf("on {") + 4
scanned_dt = i.Notes.Substring(index, i.Notes.Length - index - 1)
If Date.Parse(scanned_dt) >= startDate And Date.Parse(scanned_dt) <= endDate Then
...
I am now trying to get a total of items for a certain date range.
This sql statement works to get the total for all dates. How can I update the Where clause to only count the items where i.Notes contains a date between startDate and endDate
Dim sql As String = "Select COUNT(inv_PartNum) from lester.inventory i join lester.vendor v on v.vendor_ID = i.vendor_ID Where v.vendor_Name = '" + vendorName + "' AND i.inv_Desc LIKE '%" + size + "%'"
*EDITED*
I came up with this sql select statement:
SELECT COUNT(i.inv_PartNum) FROM cdms.lester.inventory AS I
join CDMS.lester.vendor AS v on v.vendor_ID = i.vendor_ID Where v.vendor_Name = 'JVE-285'
AND CONVERT(DATETIME,
SUBSTRING(i.inv_Notes, CharIndex('on {', i.inv_Notes)+4, len(i.inv_Notes)-(CharIndex('on {', i.inv_Notes) + 4)
),101) BETWEEN '01-01-2011' AND '04-04-2011'
But I am getting this error:
The conversion of a char data type to a datetime data type resulted in an out-of-range datetime value.
In trying to figure it out I created the following sql select statement:
SELECT * FROM (
SELECT i.inv_Notes,
CONVERT(DATETIME,SUBSTRING(i.inv_Notes, CharIndex('on {',i.inv_Notes)+4,LEN(i.inv_Notes)-(CharIndex('on {', i.inv_Notes) + 4)),101) AS d
FROM cdms.lester.inventory AS i
join CDMS.lester.vendor AS v ON v.vendor_ID = i.vendor_ID WHERE v.vendor_Name = 'JVE-285') AS s
My inv_Notes column contains a string like "Assigned to Tool Trailer {JVE-285} on {4/8/2011}"
When I run the query as shown above, I get my inv_Notes column along with the date column. The dates all show in this format "2011-04-08 00:00:00.000" and no errors are thrown.
However as soon as I add a WHERE clause, I get the error: The conversion of a char data type to a datetime data type resulted in an out-of-range datetime value.
I've tried formatting the date every which way, but always get the error...
WHERE s.d > CONVERT(DATETIME, '2011-1-1', 101)
WHERE s.d < GetDate()
WHERE s.d > '20110101'
WHERE s.d >= '2011-01-01'
WHERE s.d >= '01-01-2011'
WHERE s.d > '01/01/2011'
EDIT
I've also tried
WHERE s.d IS NOT NULL
and get the same error. This obviously isn't working the way I think it's working b/c at the point of the WHERE, the conversion should have already successfully happened.
SOLUTION
Got this working
SELECT * FROM (
SELECT i.inv_Notes,
SUBSTRING(i.inv_Notes, CharIndex('} on {',i.inv_Notes)+6,LEN(i.inv_Notes)-(CharIndex('} on {', i.inv_Notes) + 6))
AS d
FROM cdms.lester.inventory AS i
join CDMS.lester.vendor AS v ON v.vendor_ID = i.vendor_ID WHERE v.vendor_Name = 'JVE-285') AS s
WHERE PARSENAME(REPLACE(s.d, '/', '.'), 1)+
RIGHT('00'+PARSENAME(REPLACE(s.d, '/', '.'), 3),2)+
RIGHT('00'+PARSENAME(REPLACE(s.d, '/', '.'),2),2) BETWEEN '20110407' AND '20110409'
I tried to cast that to int and do an integer comparison but then I get an error. Figured out that the problem was that some of the inv_Notes fields contain data like "Item Added {3/11/2011}" None of those meet the conditions for the inner select, so in my mind if it's not selected in the inner select, it shouldn't be a problem for the condition of the outer select. However, it was trying to cast 2011{311 to int and throwing an error. I'm sure it was trying to cast that to date and that's why I had the previous problems.
Try adding something similar to this WHERE clause into your statement:
WHERE CAST(SUBSTRING(Notes, CHARINDEX(Notes, 'on {') + 4, Length?) AS Date) BETWEEN StartDate And EndDate
The idea been that you extract the relevant part of the string by combining the SubString and CharIndex methods, and then convert this expression to a date format so that it can be used with the Between Operator.
Best of Luck
Part 2 Update:
As you have been able to select the date but not use it in the where clause, I suggest using it as a select statement and then wrapping this in another statement e.g:
SELECT *
FROM
(SELECT CAST(SUBSTRING(Notes, CHARINDEX(Notes, 'on {') + 4, Length?) AS Date) AS dtmNotes)
WHERE dtmNotes BETWEEN Start And End
This is just to illustrate, but include the whole of your first select statement in the wrapping.
Try this...
assuming that the table with the embedded date is called InventoryList and the column is called inv_Notes
SELECT *
FROM InventoryList i
WHERE
CAST(
substring(
i.inv_Notes,
patindex('%on {%',i.inv_Notes) +4,
patindex('%[0-9][0-9][0-9][0-9]}%',i.inv_Notes)-patindex('%on {%',i.inv_Notes)
) as Datetime)
BETWEEN '12/1/2011' AND '12/23/2011
EDIT: More restrictive looking for a "9/9999}" pattern
SELECT *
FROM InventoryList i
WHERE
CAST(
substring(
i.inv_Notes,
patindex('%on {%',i.inv_Notes) +4,
patindex('%[0-9]/[0-9][0-9][0-9][0-9]}%',i.inv_Notes)-patindex('%on {%',i.inv_Notes)
) as Datetime)
BETWEEN '12/1/2011' AND '12/23/2011