Date query in SQL2008 - tsql

I have the following records depicting shifts in the Shifts table.
ID, Description, Start Time, End Time
1, Morning, 06:00, 13:59
2, Afternoon, 14:00, 21:59
3, Night, 22:00, 05:59
I now need to be able to get the shift relevant to a passed time but get stuck with getting the record for the night shift where the time starts before midnight and ends the following day.
What is the easiest way to query this table to get the correct shift based on a passed time?
TIA - Mike

The SQL 2008 time types might improve it slightly, but try this:
--Your Shift data as a temp table
declare #shifts table (ID int, Description varchar(10), [Start Time] smalldatetime, [End Time] smalldatetime)
insert into #shifts (ID, Description, [Start Time], [End Time])
select 1, 'Morning', '06:00', '13:59'
union all
select 2, 'Afternoon', '14:00', '21:59'
union all
select 3, 'Night', '22:00', '05:59'
-- Time to check
declare #timeToCheck smalldatetime
SET #timeToCheck='04:00'
-- The query to find the matching shift
select * from #shifts where
([Start Time]<[End Time] AND #timeToCheck>=[Start Time] AND #timeToCheck<=[End Time]) OR
([Start Time]>[End Time] AND (#timeToCheck>=[Start Time] OR #timeToCheck<=[End Time]))
Change the #timeToCheck to test it.

If you used a Datetime, it would include the date, you could then query the entire datetime and easily handle the result

you shouldn't need to store the end time, it's implied by the next start time.
if it makes it easier, you could have a second table with shift time ranges with a one-to-many relationship with shifts.
in the second table, add a fourth row with shift 3 ranging from 0 -> 5:59.
so table1 has 3 rows,
table2 has 4 like this:
shiftID shiftTime
3 00:00
1 06:00
2 14:00
3 22:00
if you want, you can add another column named isShiftStart marked true for times 06, 14, and 22, and false for time 00:00

Related

How can I, in T-SQL, examine date intervals to remove overlapping intervals before adding totals together

I am running an analysis on medication prescribing practices. We want to identify whether someone has been on a class of medications for 60 days out of a 90 day quarter. We have a start and end date for each prescription, and the bounds of the quarter (e.g., 4/1/2022 – 6/30/2022). For each prescription I’ve calculated the number of days between the start and end date (only including days that fall within the bounds of the quarter). There are many instances in which multiple drugs within the same class are prescribed someone might try one antidepressant but not like it, so be given another in the same class.
My original strategy was just to total up number of days for each class of medication and see if it’s 60 or over. The days don’t have to be consecutive, but if they overlap, days during an overlap period shouldn’t count twice (which they would in a simple sum).
For instance in the data table below, patient 1 in row 1 should be included as they are over 60 days. Patient 2 should also get in (rows 2 and 3) because the non-overlapping total (57+8) within the same med class gets them to over 60 days. However, patient 3 should NOT get in, even though the total of 32 + 32 is over 60 because the intervals overlap. This means that they were really on the medication class for only 32 days – this is an instance where someone might be on two different antidepressants simultaneously.
It’s not sufficient to just sum the days in the interval, but I also have to include some way to examine whether the intervals are overlapping and only add days if an interval for a given medication class falls outside another interval for that same class.
Row num Patid Med class Start date End date Interval
1 1 A 2022-04-28 2022-09-12 63
2 2 B 2022-05-03 2022-06-29 57
3 2 B 2022-04-21 2022-04-29 8
4 3 A 2022-01-19 2022-05-03 32
5 3 A 2022-01-19 2022-05-03 32
I’m having a hard time figuring out how to do this. Note, I'm limited to just using SQL for this.
Code that produced the above data. I would embed this in another query to generate a total interval but need to deal with the overlap issue.
DECLARE #startdt DATE;
DECLARE #enddt DATE;
SET #startdt='4/1/2022'
SET #enddt='6/30/2022'
--for q4 fy2022-23 (4/1/2022-6/30/2022)`
SELECT DISTINCT
rx.patid, d.medication_category as medcat, start_date, end_date,
-- case statement to capture days within quarter only
CASE WHEN start_date<#startdt and end_date>#enddt then 90
WHEN start_date<#startdt and end_date>=#startdt then datediff(d,#startdt,end_date)
WHEN start_date>=#startdt and end_date>#enddt then datediff(d,start_date,#enddt)
ELSE datediff(d,start_date,end_date)
END as interval
FROM rx
INNER JOIN Drug_names_categories d
ON rx.drugname=d.drugname
WHERE start_date<'7/1/2022' and end_date>'3/30/2022'
AND rx.patid IS NOT NULL
AND d.medication_category IS NOT NULL
AND d.medication_category <>''
You can accomplish what you want by generating a calendar table (using a Common Table Expression) of individual days within the test range, joining those days with the prescriptions with overlapping days, and then counting distinct days for each patient and medication category combination.
Something like:
DECLARE #startdt DATE = '2022-04-01';
DECLARE #enddt DATE = '2022-06-30';
DECLARE #threshold INT = 60;
WITH Days AS (
SELECT #startdt AS Day
UNION ALL
SELECT DATEADD(day, 1, Day)
FROM Days
WHERE Day < #enddt
)
SELECT
rx.patid, d.medication_category as medcat,
COUNT(DISTINCT DD.Day) AS days_medicated,
MIN(DD.Day) AS start_date,
MAX(DD.Day) AS end_date
FROM rx
INNER JOIN Drug_names_categories d
ON rx.drugname = d.drugname
INNER JOIN Days DD
ON DD.Day BETWEEN rx.start_date AND rx.end_date
WHERE rx.start_date <= #enddt AND #startdt <= rx.end_date
GROUP BY rx.patid, d.medication_category
HAVING COUNT(DISTINCT DD.Day) >= #threshold
ORDER BY rx.patid, start_date;
If using SQL Server 2022 or later, the Days generator can be simplified by using the new GENERATE_SERIES() function:
WITH Days AS (
SELECT DATEADD(day, S.value, #startdt) AS Day
FROM GENERATE_SERIES(0, DATEDIFF(day, #Startdt, #enddt)) S
)
See this db<>fiddle for an example with some sample data.
I would do this using a date/calendar table, then it's pretty easy.
If you don't already have a date table, this link is one of many that describe how to create one easily ( https://www.mssqltips.com/sqlservertip/4054/creating-a-date-dimension-or-calendar-table-in-sql-server/ )
Here's the script from this link (in case the link dies)
DECLARE #StartDate date = '20100101';
DECLARE #CutoffDate date = DATEADD(DAY, -1, DATEADD(YEAR, 30, #StartDate));
;WITH seq(n) AS
(
SELECT 0 UNION ALL SELECT n + 1 FROM seq
WHERE n < DATEDIFF(DAY, #StartDate, #CutoffDate)
),
d(d) AS
(
SELECT DATEADD(DAY, n, #StartDate) FROM seq
),
src AS
(
SELECT
TheDate = CONVERT(date, d),
TheDay = DATEPART(DAY, d),
TheDayName = DATENAME(WEEKDAY, d),
TheWeek = DATEPART(WEEK, d),
TheISOWeek = DATEPART(ISO_WEEK, d),
TheDayOfWeek = DATEPART(WEEKDAY, d),
TheMonth = DATEPART(MONTH, d),
TheMonthName = DATENAME(MONTH, d),
TheQuarter = DATEPART(Quarter, d),
TheYear = DATEPART(YEAR, d),
TheFirstOfMonth = DATEFROMPARTS(YEAR(d), MONTH(d), 1),
TheLastOfYear = DATEFROMPARTS(YEAR(d), 12, 31),
TheDayOfYear = DATEPART(DAYOFYEAR, d)
FROM d
)
SELECT *
INTO MyDateTable
FROM src
ORDER BY TheDate
OPTION (MAXRECURSION 0);
No that you have your new date table you can join to it to get the list of dates that are within the start and end date, something like
SELECT DISTINCT COUNT(TheDate)
FROM rx
INNER JOIN MyDateTable dt on dt BETWEEN rx.start_date AND rx.end_date
INNER JOIN Drug_names_categories d ON rx.drugname=d.drugname
WHERE start_date<'7/1/2022' and end_date>'3/30/2022'
AND rx.patid IS NOT NULL
AND d.medication_category IS NOT NULL
AND d.medication_category <>''
Obviously this is simple example but you could extend this easily to include all the details you need, the point is that you now have a list of dates or distinct list of dates which you can work with easily.
You could also simply the date range applied by referencing the TheQuarter and TheYear columns. If this is a common task consider extending the date table to contain a comound YearQurater columns (e.g. 2023Q1/202301 etc)

opening hours table design postgresql: how to query this?

I want to store opening hours of a store in a PostgreSQL database. I have a few question about this. First of all, my table design:
Table: opening_hours
--------------------
* id
* store_id
* start_day (0->6)
* start_time (HH:mm:ss)
* duration (HH:mm:ss)
* timezone (default: Europe/Brussels)
Example:
--------
1 1 0 08:00:00 12:00:00 Europe/Brussels
// store ID 1 is open on sundays from 08AM to 08PM (12 hours in total) local time
2 2 6 20:00:00 04:00:00 Europe/Brussels
// store ID 2 is open on saturday from 08PM till 04AM (on sunday) (this is like a pub or something with opening hours at night) local time (so 20:00:00 in Belgium open = 19:00:00 in London open)
I think this is a good design, because now I can make opening hours spanning more than 1 day (like when a pub is open at night). Before that, I was storing opening hours in each day separately so I had to enter 'saturday 20:00:00 -> 23:59:59' + 'sunday 00:00:00 -> 04:00:00' for the opening hours for store ID 2.
How can I query against these rows?
I want to check if a time already exists in the database before adding a new one, and I want to check if a row exists based on the current time of the user (2 different queries).
CURRENT ROWS:
-------------
1 1 1 08:00:00 08:00:00 Europe/Brussels
// open on monday from 08AM until 04PM
2 1 6 20:00:00 12:00:00 Europe/Brussels
// open on saturday from 08PM until sunday 08AM
NEW ROWS:
---------
3 1 1 14:00:00 04:00:00 Europe/Brussels (should not insert this row because it interferes with row ID 1)
// open on monday from 02PM until 06PM
4 1 1 17:00:00 04:00:00 Europe/Brussels (can insert because not interference with row 1)
// open on monday from 05PM until 09PM
5 1 0 07:00:00 05:00:00 Europe/Brussels (can't insert this row because it interferes with row 2).
// open on sunday from 07AM until 12PM
I hope my question is clear. If not, please correct me, I'll try to ask it differently then.
With your new table design actually, it is now a bit tricky, because of durations that can possibly span over the end of a week cycle. Consider for example:
start_day | start_time | duration
-----------+------------+----------
6 | 14:00:00 | 18:00:00
It actually extends into day=0, so it would need to match day=0, t=07:00:00.
When looking for matches against a specific time, you need to check two possibilities (either day, time or day + 7, time fall into one of your intervals). Same thing for overlaps (3 possibilities).
You can define some helper functions:
-- oh: opening hours helper functions
-- oh_tst: convert dow, time into a timestamp after the Epoch
-- (note: Epoch was not a Monday (dow 0), but it doesn't matter,
-- we could use any arbitrary date)
create or replace function oh_tst(dow int, t time) returns timestamp as $$
select '1970-01-01'::timestamp + $1 * interval'1 day' + $2;
$$
language sql immutable;
-- oh_single_matches: internal function (no handling of wrap around)
create or replace function oh_single_matches(start_day int, start_time time, d interval, day int, t time) returns boolean as $$
select oh_tst($4, $5) between oh_tst($1, $2) and oh_tst($1, $2) + $3 - interval'1 millisecond';
$$
language sql immutable;
-- oh_matches: tests if a (day, time) is within an oh interval
-- handle wrap around at end of week
create or replace function oh_matches(start_day int, start_time time, d interval, day int, t time) returns boolean as $$
select oh_single_matches($1, $2, $3, $4, $5)
or oh_single_matches($1, $2, $3, $4, $5 + 7);
$$
language sql immutable;
-- oh_overlaps: test for oh defs overlap (incl wrap-around)
create or replace function oh_overlaps(adow int, astart_time time, aduration interval,
bdow int, bstart_time time, bduration interval) returns boolean as $$
select (oh_tst($1, $2), $3) overlaps (oh_tst($4, $5), $6)
or (oh_tst($1, $2), $3) overlaps (oh_tst($4 + 7, $5), $6)
or (oh_tst($1, $2), $3) overlaps (oh_tst($4 - 7, $5), $6);
$$
language sql immutable;
Examples:
Single match (one day,time against an opening hours definition):
-- intervals are left-close:
select oh_matches(6, '14:00:00'::time, interval'2 hours', 6, '14:00:00'::time);
oh_matches
------------
t
-- ...and right-open (as Nature intended):
select oh_matches(6, '14:00:00'::time, interval'2 hours', 6, '15:59:59'::time);
t
select oh_matches(6, '14:00:00'::time, interval'2 hours', 6, '16:00:00'::time);
f
-- wrap around the end of week
select oh_matches(6, '14:00:00'::time, interval'18 hours', 0, '02:00:00'::time);
t
Interval overlaps:
select oh_overlaps(2, '14:00:00'::time, interval'8 hours',
2, '04:00:00'::time, interval'8 hours');
f
select oh_overlaps(2, '14:00:00'::time, interval'8 hours',
2, '08:00:00'::time, interval'8 hours');
t
-- wraparoud ok
select oh_overlaps(0, '01:00:00'::time, interval'8 hours',
6, '22:00:00'::time, interval'18 hours');
t
Test against a table:
Single timestamp (now()):
select * from mytable
where store_id=5
and oh_matches(start_day, start_time, duration, now()::timestamp);
Check if prospective rows in a candidates table would overlap existing definitions in mytable:
select * from candidates a inner join mytable b using (store_id)
where oh_overlaps(b.start_day, b.start_time, b.duration,
a.start_day, a.start_time, a.duration);

How to select a certain future date based on integer and integer[] in Postgresql?

I am trying to create a query that will return information about a series of future dates. So for example, today is Monday, and I want to get three days worth of information in advance: Tuesday, Wednesday, and Thursday. I understand how to use something like generate_series with a starting and end date to get the rows.
The problem I'm having is, I am selecting an integer for the number of days in advance I want from one table from a second table. But the particular dates will change if one or more of the potential future dates is one where the business is not open. So if the starting date were Thursday, and the business is closed on Sunday, I'd want to get rows for Friday, Saturday, and Monday.
So from the first table with the specifics on which days to get, I'd be selecting an integer (e.g. 3) and an integer[] (e.g. {1,2,3,4,5,6}). My thought was to somehow start with the day of the week of tomorrow (e.g. 2 from SELECT EXTRACT(DOW FROM CURRENT_DATE + '1 days'::interval)if today is tomorrow is Tuesday) and then check if that DOW is inside the array. I'd have a separate counter with the number of extra days I'd need to add to my series, and after looping through until I get three days that aren't skipped, I'd add it to my days ahead number. So starting on Thursday, I'd check Friday (5), it's in the array, increment loop variable and continue. Saturday (6), it's in the array, increment loop variable and continue. Sunday (0), not in the array, add one to the extra days counter and continue. Monday (1), in the array, increment loop variable and continue. That's three, so I'm done. Then add my second counter (1) to the original days ahead (3) and get 4 days worth of information. Days that the business isn't open will be excluded through WHERE conditions, so the total number of days displayed will be consistent.
The problem is, I can conceptualize this solution, but I can't figure out how to put it together syntactically. Here's an approximation of what I think would work:
DO $$
BEGIN
DECLARE
counter integer := 0;
increment_days integer := 1;
WITH future_data AS
(SELECT days_ahead, open_days FROM Stores);
WHILE counter < (SELECT days_ahead FROM future_data) loop
CASE WHEN (SELECT EXTRACT(DOW FROM CURRENT_DATE + (days::text || ' days'::interval))
= ANY(SELECT unnest(open_days) FROM future_data)) THEN
counter := counter + 1;
ELSE counter := counter END;
increment_days := increment_days + 1;
END LOOP;
increment_days := increment_days + days_ahead;
--[...main SELECT query...]
END$$;
I keep getting complains about the way I'm putting this all together. Currently it's a syntax error at WHILE. It seems like I can't do anything but a SELECT statement there.
Rather the trying to figure out how many days in advance just build a function where you provide a start_date and the number of days you want. Then let the function determine the actual dates returned (ie it bypasses Sunday). The following SQL function does that using a recursive CTE rather than attempting to calculate the number of days to look forward. See fiddle
create or replace
function business_day(start_date_in date, num_days_in integer default 3)
returns setof date
language sql
immutable strict
as $$
with recursive get_days (bus_date, num_selected) as
( select case when extract(dow from start_date_in::timestamp) > 0
then start_date_in::timestamp + interval '1 day'
else start_date_in::timestamp + interval '2 day'
end
, 1
union all
select case when extract(dow from bus_date + interval '1 day')>0
then bus_date + interval '1 day'
else bus_date + interval '2 day'
end
, num_selected + 1
from get_days
where num_selected<num_days_in
)
select bus_date::date from get_days ;
$$;

PostgreSQL grouping timestamp by day

I have a table x(x_id, ts), where ts is a timestamp.
And I have a second table y(y_id, day, month, year), which is supposed to have its values from x(ts).
(Both x_id and y_id are serial)
For example:
x y
_x_id_|__________ts__________ _y_id_|_day_|_month_|__year__
1 | '2019-10-17 09:10:08' 1 17 10 2019
2 | '2019-01-26 11:12:02' 2 26 1 2019
However, if on x I have 2 timestamps on the same day but different hour, this how both tables should look like:
x y
_x_id_|__________ts__________ _y_id_|_day_|_month_|__year__
1 | '2019-10-17 09:10:08' 1 17 10 2019
2 | '2019-10-17 11:12:02'
Meaning y can't have 2 rows with the same day, month and year.
Currently, the way I'm doing this is:
INSERT INTO y(day, month, year)
SELECT
EXTRACT(day FROM ts) AS day,
EXTRACT(month FROM ts) AS month,
EXTRACT(year FROM ts) AS year
FROM x
ORDER BY year, month, day;
However, as you probably know, this doesn't check if the timestamps share the same date, so how can I do that?
Thank you for your time!
Assuming you build the unique index as recommended above change your insert to:
insert into y(day, month, year)
select extract(day from ts) as day,
, extract(month from ts) as month,
, extract(year from ts) as year
from x
on conflict do nothing;
I hope your table X is not very large as the above insert (like your original) will attempt inserting a row into Y for every row in X on every execution - NO WHERE clause.
Add a UNIQUE constraint on table y to prevent adding the same date twice.
CREATE UNIQUE INDEX CONCURRENTLY y_date
ON y (year,month,day)
Then add it to y:
ALTER TABLE y
ADD CONSTRAINT y_unique_date
UNIQUE USING INDEX y_date
Note that you'll get an SQL error when the constraint is violated. If you don't want that and just ignore the INSERT, use a BEFORE INSERT trigger, returning NULL when you detect the "date" already exists, or just use ON CONFLICT DO NOTHING in your INSERT statement, as hinted by #Belayer.

Postgres find where dates are NOT overlapping between two tables

I have two tables and I am trying to find data gaps in them where the dates do not overlap.
Item Table:
id unique start_date end_date data
1 a 2019-01-01 2019-01-31 X
2 a 2019-02-01 2019-02-28 Y
3 b 2019-01-01 2019-06-30 Y
Plan Table:
id item_unique start_date end_date
1 a 2019-01-01 2019-01-10
2 a 2019-01-15 'infinity'
I am trying to find a way to produce the following
Missing:
item_unique from to
a 2019-01-11 2019-01-14
b 2019-01-01 2019-06-30
step-by-step demo:db<>fiddle
WITH excepts AS (
SELECT
item,
generate_series(start_date, end_date, interval '1 day') gs
FROM items
EXCEPT
SELECT
item,
generate_series(start_date, CASE WHEN end_date = 'infinity' THEN ( SELECT MAX(end_date) as max_date FROM items) ELSE end_date END, interval '1 day')
FROM plan
)
SELECT
item,
MIN(gs::date) AS start_date,
MAX(gs::date) AS end_date
FROM (
SELECT
*,
SUM(same_day) OVER (PARTITION BY item ORDER BY gs)
FROM (
SELECT
item,
gs,
COALESCE((gs - LAG(gs) OVER (PARTITION BY item ORDER BY gs) >= interval '2 days')::int, 0) as same_day
FROM excepts
) s
) s
GROUP BY item, sum
ORDER BY 1,2
Finding the missing days is quite simple. This is done within the WITH clause:
Generating all days of the date range and subtract this result from the expanded list of the second table. All dates that not occur in the second table are keeping. The infinity end is a little bit tricky, so I replaced the infinity occurrence with the max date of the first table. This avoids expanding an infinite list of dates.
The more interesting part is to reaggregate this list again, which is the part outside the WITH clause:
The lag() window function take the previous date. If the previous date in the list is the last day then give out true (here a time changing issue occurred: This is why I am not asking for a one day difference, but a 2-day-difference. Between 2019-03-31 and 2019-04-01 there are only 23 hours because of daylight saving time)
These 0 and 1 values are aggregated cumulatively. If there is one gap greater than one day, it is a new interval (the days between are covered)
This results in a groupable column which can be used to aggregate and find the max and min date of each interval
Tried something with date ranges which seems to be a better way, especially for avoiding to expand long date lists. But didn't come up with a proper solution. Maybe someone else?