YoY & YTD - Taking into account the timestamp, as well as date, on records in the previous year - sql

My question is, what do I need to change in the WHERE clause for the SP to take into account the timestamp, as well as the date itself, on the CreatedDate field. For example...If the report was run at 1pm today, it would only bring back records made up to 1pm today this time last year [2015] and obviously all records created in 2016 so far. At present, 2015 is bringing back all records up to the end of the day. Any help would be much appreciated. Claire
#CreatedFrom15 DateTime = NULL,
#CreatedTo15 DateTime = NULL,
#CreatedFrom16 DateTime = NULL,
#CreatedTo16 DateTime = NULL,
SELECT
BookingID,
CreatedYear,
CreatedDate,
NightDuration,
HolidayCost,
TRAVPropertyTypeCategory
FROM Vw_TRAVLodgeResortGlampingFinancialByNights
WHERE
((CreatedYear = DATEPART(YEAR,GETDATE())-1 AND CreatedDate BETWEEN #CreatedFrom15 AND #CreatedTo15) AND (CreatedDate >= #CreatedFrom15 OR #CreatedFrom15 IS NULL) AND (CreatedDate <= #CreatedTo15 OR #CreatedTo15 IS NULL)
OR
(CreatedYear = DATEPART(YEAR,GETDATE()) AND CreatedDate BETWEEN #CreatedFrom16 AND #CreatedTo16) AND (CreatedDate >= #CreatedFrom16 OR #CreatedFrom16 IS NULL)AND(CreatedDate <= #CreatedTo16 OR #CreatedTo16 IS NULL))

can you specify the dates of the parameters?
anyhow, the reference to the year seems unnecessary and also you can include the null case inside the between like this-
where CreatedDate between isnull(#createdFrom16,'20160101') and isnull(#CreatedTo16,'20161231')

Related

SQL Server: compare only month and day - SARGable

I have a table storing a datetime column, which is indexed. I'm trying to find a way to compare ONLY the month and day (ignores the year totally).
Just for the record, I would like to say that I'm already using MONTH() and DAY(). But I'm encountering the issue that my current implementation uses Index Scan instead of Index Seek, due to the column being used directly in both functions to get the month and day.
There could be 2 types of references for comparison: a fixed given date and today (GETDATE()). The date will be converted based on time zone, and then have its month and day extracted, e.g.
DECLARE #monthValue DATETIME = MONTH(#ConvertDateTimeFromServer_TimeZone);
DECLARE #dayValue DATETIME = DAY(#ConvertDateTimeFromServer_TimeZone);
Another point is that the column stores datetime with different years, e.g.
1989-06-21 00:00:00.000
1965-10-04 00:00:00.000
1958-09-15 00:00:00.000
1965-10-08 00:00:00.000
1942-01-30 00:00:00.000
Now here comes the problem. How do I create a SARGable query to get the rows in the table that match the given month and day regardless of the year but also not involving the column in any functions? Existing examples on the web utilise years and/or date ranges, which for my case is not helping at all.
A sample query:
Select t0.pk_id
From dob t0 WITH(NOLOCK)
WHERE ((MONTH(t0.date_of_birth) = #monthValue AND DAY(t0.date_of_birth) = #dayValue))
I've also tried DATEDIFF() and DATEADD(), but they all end up with an Index Scan.
Adding to the comment I made, on a Calendar Table.
This will, probably, be the easiest way to get a SARGable query. As you've discovered, MONTH([YourColumn]) and DATEPART(MONTH,[YourColumn]) both cause your query to become non-SARGable.
Considering that all your columns, at least in your sample data, have a time of 00:00:00 this "works" to our advantage, as they are effectively just dates. This means we can easily JOIN onto a Calendar Table using something like:
SELECT dob.[YourColumn]
FROM dob
JOIN CalendarTable CT ON dob.DateOfBirth = CT.CalendarDate;
Now, if we're using the table from the above article, you will have created some extra columns (MonthNo and CDay, however, you can call them whatever you want really). You can then add those columns to your query:
SELECT dob.[YourColumn]
FROM dob
JOIN CalendarTable CT ON dob.DateOfBirth = CT.CalendarDate
WHERE CT.MonthNo = #MonthValue
AND CT.CDay = #DayValue;
This, as you can see, is a more SARGable query.
If you want to deal with Leap Years, you could add a little more logic using a CASE expression:
SELECT dob.[YourColumn]
FROM dob
JOIN CalendarTable CT ON dob.DateOfBirth = CT.CalendarDate
WHERE CT.MonthNo = #MonthValue
AND CASE WHEN DATEPART(YEAR, GETDATE()) % 4 != 0 AND CT.CDat = 29 AND CT.MonthNo = 2 THEN 28 ELSE CT.Cdat END = #DayValue;
This treats someone's birthday on 29 February as 28 February on years that aren't leap years (when DATEPART(YEAR, GETDATE()) % 4 != 0).
It's also, probably, worth noting that it'll likely be worth while changing your DateOfBirth Column to a date. Date of Births aren't at a given time, only on a given date; this means that there's no implicit conversion from datetime to date on your Calendar Table.
Edit: Also, just noticed, why are you using NOLOCK? You do know what that does, right..? Unless you're happy with dirty reads and ghost data?

Why there is a behavior difference of SQL count(*) vs SQL count(numeric)

I know that
count(*) - will returns the total count of all rows including nulls.
count(colName) - will returns the total count of all rows in which colName is not null.
Today one of my college got an issue with count() in SQL. He was trying to get the count of rows from a view after applying some date filter.
View Return Data structure
[Year] VARCHAR(4)
,[Month] VARCHAR(4)
,AType VARCHAR(20)
,PActualsID INT
,EID VARCHAR(12)
,CID INT
,CGId INT
,EMargin NUMERIC(17,3)
,Period DATETIME
,PLID INT
,PCID INT
,PSID INT
,VPID INT
,VSID INT
,STID INT
Query 1
SELECT * from vw_ActualAllocation_New
where EntityId = '442105' and
Period >= '01-Jan-2017' AND Period < '01-Jan-2018'
This returns around 94 records.
Query 2
SELECT Count(*) from vw_ActualAllocation_New
where EID = '442105' and
Period >= '01-Jan-2017' AND Period < '01-Jan-2018'
This returns an error
Msg 241, Level 16, State 1, Line 1 Conversion failed when converting
date and/or time from character string.
Query 3
SELECT Count(EMargin) from vw_ActualAllocation_New
where EID = '442105' and
Period >= '01-Jan-2017' AND Period < '01-Jan-2018'
This returns me count as 94.
Please note that EMargin is a NUMERIC datatype and all other types
such as int and varchar returns the same error.
Please share your thoughts on difference between these two behaviors.
SQL Server Environment: Microsoft SQL Server 2012 (Build 7601: Service Pack 1) (Hypervisor)
UPDATE - View Code
CREATE VIEW [dbo].[vw_ActualAllocation_New]
SELECT D.Year, D.Month, A.AType, A.PAID, D.EntityID, D.CustomerID
,B.CGId, SUM(A.EBITRMargin) AS EBITRMargin
,CONVERT(DATETIME,D.Month + '-01-' + D.Year) AS Period, D.PLID, D.PCID
,D.PSID, D.VPID, D.VSID,D.STID
FROM dbo.AAllocations AS A
INNER JOIN dbo.PActuals AS D ON D.PActualsID = A.PActualsID AND D.Active = 1
INNER JOIN dbo.Customer AS B ON D.CustomerID = B.CustomerID AND D.EntityID =
B.EntityID
INNER JOIN dbo.AStatus AS C ON A.ASID = C.ASID
WHERE (A.Active = 1) AND (C.Active = 1) AND (C.Reference = 'Actuals') AND
(C.Status = 1)
GROUP BY D.Year, D.Month, A.AType, A.PAID, D.EntityID, D.CustomerID, B.CGId,
D.PLID, D.PCID, D.PSID, D.VPID, D.VSID, D.STID
Update on Conclusion
Reached a conclusion as suggested by Gordon, and if you feels that you may have another thoughts, please post it here
Also I tried with the data from view, into a new table and its working fine. Issue happens while accessing directly from view. The view generation happens with lot of data and impossible to post it here because of its huge size and privacy agreements. Thanks for understanding my limitations and helping me
The most likely cause of the problem is this line of code:
CONVERT(DATETIME, D.Month + '-01-' + D.Year) AS Period
In SQL Server, you should never use CONVERT() from a string to a date without specifying the format or using standard formats (YYYYMMDD is preferred by SQL Server but I consider YYYY-MM-DD to be acceptable as well).
In older versions of SQL Server, you can do:
CONVERT(DATE, d.Year + RIGHT('00' + D.Month, 2) + '01') as period
This conversion will always work. In newer versions, use datefromparts():
DATEFROMPARTS(d.Year, d.Month, 1) as Period
Why is this happening? I speculate that the date format is being interpreted as DD-MM-YYYY instead of MM-DD-YYYY. In other words, what you think is Feb 1st is really Jan 2nd.
Further, the period values for entity '442105' all convert to a reasonable dates. The WHERE clause filters out the bad values. The problem is with other entities and the issue, as Damien points out is where the values get evaluated in the execution engine.

Find available openings using SQL

I have a table of appointments with records having two fields - start_date and end_date, both datetime. There is no overlap of time periods in the table.
Given a specific period (search_start and search_end), I need to generate a list of all openings between those appointments (from and to) using SQL.
For example: given two appointments in the table:
September 15, 2016 08:00 to September 15, 2016 09:00
September 15, 2016 10:00 to September 15, 2016 12:00
And given search parameters start= September 1, 2016 00:00 and end= September 30, 2016 23:59, the results should be
September 1, 2016 00:00 to September 15, 2016 08:00
September 15, 2016 09:00 to September 15, 2016 10:00
September 15, 2016 12:00 to September 30, 2016 23:59
Here is a script to generate a sample table:
CREATE TABLE [dbo].[Table_1](
[from_date] [datetime] NOT NULL,
[to_date] [datetime] NULL,
CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED ( [from_date] ASC )
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
INSERT [dbo].[Table_1] ([from_date], [to_date]) VALUES (CAST(0x0000A6820083D600 AS DateTime), CAST(0x0000A682009450C0 AS DateTime))
INSERT [dbo].[Table_1] ([from_date], [to_date]) VALUES (CAST(0x0000A68200A4CB80 AS DateTime), CAST(0x0000A68200C5C100 AS DateTime))
I am using MSSQL 2008 R2
Using your values I got the output you wanted : )
DECLARE #start datetime = '2016-09-01 00:00:00'
DECLARE #finish datetime = '2016-09-30 23:59:00'
WITH rn AS (SELECT *, ROW_NUMBER() OVER (ORDER BY start) AS rn FROM opening)
SELECT CASE WHEN r1.rn = 1 THEN #start
ELSE r2.finish
END as START,
CASE WHEN r1.rn IS NULL THEN #finish
ELSE r1.start
END AS FINISH
FROM rn r1
FULL OUTER JOIN rn r2
ON r1.rn = r2.rn + 1
WHERE ISNULL(r1.start, 0) != #start
opening is your schedule/appointment table. start is the starting date in your table and finish is the end date in your table. #start is the starting date and #finish is the end date. You obvious don't need to use #start, #finish. I just put it there for testing.
SELECT Start_Date, End_Date
FROM TableName
BETWEEN Start_Date AND End_Date
This question seems easy?? I am not sure, if I have oversimplified your question?
You just have to make sure, you add the time bit as well!
I am thinking like Mfredy on this question. But I will give my standard spiel on what to look out for on data time calculations.
Here are some common issues.
Some (like Mysql) sql engines store the data in GMT (some time zone in england). Then they calculate the number of hours off of GMT to get the actual time. So if you are 8 hours from GMT you may be 8 hours off if you used a MsSql client connecting to a MySql database. Because MsSql does not understand this 8 hour delta for GMT used by MySql. You can also see this in applications and operating systems.
The other issue is you are thinking in dates you have to make sure you are thinking in Date and Time unless you converted the date time to a date.
Watch out for < vs <=. If you are filtering for the end date. Say for example Last day in january is to be included but first day of feb is not. It is better to do < 02/01 that it is to do <= 1/31. You may chop off records if you compare using <= and don't go down to the last millisecond before 02/01.
I've had a go trying to re-create your scenario, and then solve it. In some places I've simplified things for the sake of the example, but you should be able to extrapolate it.
I'm sure there are also far more elegant ways, but is a starting point, and I quite enjoyed figuring it out.
The approach I wanted to take was build a list of the available 'time slots'
First, I created a table with the available Hours for bookings
CREATE TABLE [dbo].[HOURS](
HourID [int] NULL)
INSERT INTO dbo.[hours] VALUES (9),(10),(11),(12),(13),(14),(15),(16),(17)
Then did the same for the minute intervals (I went with 5 min intervals for simplicity)
CREATE TABLE [dbo].MINS(
MinID [int] NULL )
INSERT INTO dbo.mins VALUES (5),(10),(15),(20),(25),(30),(35),(40),(45),(50),(55),(60)
and same again for dates I wanted to work with
CREATE TABLE [dbo].DATES(
DATES [Date] NULL)
INSERT INTO dbo.DATES values
('20160901'),('20160902')
Using these tables, I created a view listing all available 'slots'
CREATE VIEW AllTimeSlots AS
SELECT
cast(dates AS datetime) + cast(DATEADD(hour, hourid, DATEADD(minute, minid, DATEADD(second, 00, 0))) AS datetime) AS TimeSlot
FROM dbo.[hours] CROSS JOIN dbo.mins CROSS JOIN dates
I then created a table containing the appointments
CREATE TABLE [dbo].Appointments(
AppointmentStart [Datetime] NULL,
AppointmentEnd [DateTime] null
)
INSERT INTO dbo.Appointments
VALUES
('20160901 10:40:00 AM','20160901 10:55 AM'),('20160901 01:00:00 PM','20160901 02:05:00 PM')
Then, I created another view with married up the slots and the bookings. Take note of the joins I'm using to block out all slots between the two times
CREATE VIEW SlotAvailability
AS
SELECT TimeSlot,CASE WHEN AppointmentStart IS NULL THEN 'Free' ELSE 'Booked' END AS [Availability]
FROM (
SELECT Timeslot,apt.AppointmentStart, apt.AppointmentEnd FROM
dbo.AllTimeSlots ats
LEFT OUTER JOIN dbo.Appointments apt
on ats.TimeSlot >= appointmentstart and ats.timeslot <= appointmentend) q1
Doing
Select Timeslot, [availability] from SlotAvailability where [availability] = 'free'
will list all the available time slots.
The last bit, which I haven't quite got to (and ran out of time for now) is then converting this to a start-end time for each 'free' slot - tried a couple of methods, but didn't crack it - I think if you join that table (view) to itself of timeslot+5mins, you should be able to then min/max values based on whether it's the start / end of a free block
The way I normally address this today is to use the APPLY operator to join the table to itself. There's not enough info in the question to craft a complete query for you, but here's the general pattern:
SELECT *
FROM MyTable t1
OUTER APPLY (
SELECT TOP 1 *
FROM MyTable t
WHERE t.KeyFields = t1.KeyFields
AND t.SequenceField > t1.SequenceField
) t2
Now I can compare each row directly with the one the follows in my WHERE clause, such that the WHERE clause the filter it down to only show rows where there is a gap.
You can also sometimes accomplish this via the LAG or LEAD Windowing Functions.

which operators are correct for my command

I worked with sql server 2008 and I used this command:
SELECT Min(id),
[idfromindex]
FROM ordersview
WHERE [idfromindex] IN(SELECT DISTINCT [idfromindex]
FROM ordersview
WHERE isobserve = 'True'
AND ispay = 'True'
AND orderdate >= '1390/09/01'
AND orderdate <= '1391/09/27')
GROUP BY [idfromindex]
But if there is a record with exactly orderdate='1390/09/01' or orderdate='1391/09/27' value it will not show them ,it only shows the result greater and lower than them but I want to show the equal one too if it exists.
orderDate type is Date
Wouldn't changing the condition to being >= '1390/09/01' and < '1391/09/28' make more sense? That way you remove the time components if a date is registered as being 1390/09/01 23:59'. Date conditions tend to assume the exact time is00:00`. Of course it depends on what your column datatype is (datetime vs. date).
DATETIME data type contain both date and time information
If you provide only the date part, SQL sets the time part as the beginning of the day, which is midnight...
So Add a +1 on right or adding -1 on left will solver your problem..
SELECT DISTINCT [idfromindex]
FROM ordersview
WHERE isobserve = 'True'
AND ispay = 'True'
AND orderdate >= dateadd(day,1,'1390/09/01')
AND orderdate <= dateadd(day,1,'1391/09/27')

How to store absolute and relative date ranges in SQL database?

I'm trying to model a DateRange concept for a reporting application. Some date ranges need to be absolute, March 1, 2011 - March 31, 2011. Others are relative to current date, Last 30 Days, Next Week, etc. What's the best way to store that data in SQL table?
Obviously for absolute ranges, I can have a BeginDate and EndDate. For relative ranges, having an InceptionDate and an integer RelativeDays column makes sense. How do I incorporate both of these ideas into a single table without implementing context into it, ie have all four columns mentioned and use XOR logic to populate 2 of the 4.
Two possible schemas I rejected due to having context-driven columns:
CREATE TABLE DateRange
(
BeginDate DATETIME NULL,
EndDate DATETIME NULL,
InceptionDate DATETIME NULL,
RelativeDays INT NULL
)
OR
CREATE TABLE DateRange
(
InceptionDate DATETIME NULL,
BeginDaysRelative INT NULL,
EndDaysRelative INT NULL
)
Thanks for any advice!
I don't see why your second design doesn't meet your needs unless you're in the "no NULLs never" camp. Just leave InceptionDate NULL for "relative to current date" choices so that your application can tell them apart from fixed date ranges.
(Note: not knowing your DB engine, I've left date math and current date issues in pseudocode. Also, as in your question, I've left out any text description and primary key columns).
Then, either create a view like this:
CREATE VIEW DateRangesSolved (Inception, BeginDays, EndDays) AS
SELECT CASE WHEN Inception IS NULL THEN Date() ELSE Inception END,
BeginDays,
EndDays,
FROM DateRanges
or just use that logic when you SELECT from the table directly.
You can even take it one step further:
CREATE VIEW DateRangesSolved (BeginDate, EndDate) AS
SELECT (CASE WHEN Inception IS NULL THEN Date() ELSE Inception END + BeginDays),
(CASE WHEN Inception IS NULL THEN Date() ELSE Inception END + EndDays)
FROM DateRanges
Others are relative to current date, Last 30 Days, Next Week, etc.
What's the best way to store that data in SQL table?
If you store those ranges in a table, you have to update them every day. In this case, you have to update each row differently every day. That might be a big problem; it might not.
There usually aren't many rows in that kind of table, often less than 50. The table structure is obvious. Updating should be driven by a cron job (or its equivalent), and you should run very picky exception reports every day to make sure things have been updated correctly.
Normally, these kinds of reports should produce no output if things are fine. You have the added complication that driving such a report from cron will produce no output if cron isn't running. And that's not fine.
You can also create a view, which doesn't require any maintenance. With a few dozen rows, it might be slower than a physical table, but it might still fast enough. And it eliminates all maintenance and administrative work for these ranges. (Check for off-by-one errors, because I didn't.)
create view relative_date_ranges as
select 'Last 30 days' as range_name,
(current_date - interval '30' day)::date as range_start,
current_date as range_end
union all
select 'Last week' as range_name,
(current_date - interval '7' day)::date as range_start,
current_date as range_end
union all
select 'Next week' as range_name,
(current_date + interval '7' day)::date as range_start,
current_date as range_end
Depending on the app, you might be able to treat your "absolute" ranges the same way.
...
union all
select 'March this year' as range_name,
(extract(year from current_date) || '-03-01')::date as range_start,
(extract(year from current_date) || '-03-31')::date as range_end
Put them in separate tables. There is absolutely no reason to have them in a single table.
For the relative dates, I would go so far as to simply make the table the parameters you need for the date functions, i.e.
CREATE TABLE RelativeDate
(
Id INT Identity,
Date_Part varchar(25),
DatePart_Count int
)
Then you can know that it is -2 WEEK or 30 DAY variance and use that in your logic.
If you need to see them both at the same time, you can combine them LOGICALLY in a query or view without needing to mess up your data structure by cramming different data elements into the same table.
Create a table containing the begindate and the offset. The precision of the offset is up to you to decide.
CREATE TABLE DateRange(
BeginDate DATETIME NOT NULL,
Offset int NOT NULL,
OffsetLabel varchar(100)
)
to insert into it:
INSERT INTO DateRange (BeginDate, Offset, OffsetLabel)
select '20110301', DATEDIFF(sec, '20110301', '20110331'), 'March 1, 2011 - March 31, 2011'
Last 30 days
INSERT INTO DateRange (BeginDate, Duration, OffsetLabel)
select '20110301', DATEDIFF(sec, current_timestamp, DATEADD(day, -30, current_timestamp)), 'Last 30 Days'
To display the values later:
select BeginDate, EndDate = DATEADD(sec, Offset, BeginDate), OffsetLabel
from DateRange
If you want to be able to parse the "original" vague descriptions you will have to look for a "Fuzzy Date" or "Approxidate" function. (There exists something like this in the git source code. )