CREATE TABLE PROJECT(project_id int not null primary key generated by default as identity,
project_template_id int ,p_name varchar(50) not null unique,
store_intv int not null , p_type int not null,
start_time timestamp ,stop_time timestamp, p_status int not null,
p_scheduled boolean not null, autorecovery_status boolean not null,
p_soource varchar(50)
);
Insert into Project(project_id,project_template_id,p_name,store_intv,p_type,start_time,
stop_time,p_status,p_scheduled,autorecovery_status,p_soource
)Values(1,1,'ABC',10,2,'2022-04-04 9:55:00','2022-04-04 10:55:00',
1,true,true,'this');
Insert into Project(project_id,project_template_id,p_name,store_intv,p_type,start_time,
stop_time,p_status,p_scheduled,autorecovery_status,p_soource
)Values(2,2,'BBB',16,23,'2022-04-05 9:55:00','2022-04-04 10:55:00',
1,true,true,'this');
Insert into Project(project_id,project_template_id,p_name,store_intv,p_type,start_time,
stop_time,p_status,p_scheduled,autorecovery_status,p_soource
)Values(3,3,'CCC',20,32,'2022-01-06 9:55:00','2022-04-04 10:55:00',
1,true,true,'this');
this is my structure of table i want to select tuples whose start_time is > todays date and also start_time >11 pm
Postgres provides a extensive set of date handling functions. In this case use date_trunc to get the current date to its midnight, then add the interval '11 hours' to produce the desired start time. So: (for server local timezone)
select *
from project
where start_time > date_trunc('day', current_date) + interval '11 hours';
Related
There are 3 tables: Event, Booking, and Booking_Day.
The idea is that one can book separate days of the event.
I would like to put a constraint on Booking_Day so that Day has to be within Date_Start and Date_End range of the corresponding Event. I decided to use a function that will do this
create table Event
(
Event_ID int identity
constraint Event_pk
primary key nonclustered,
Date_Start date not null,
Date_End date
)
create table Booking
(
Booking_ID int identity
constraint Booking_pk
primary key nonclustered,
Event_ID int not null
constraint Booking_Event_Event_ID_fk
references Event
)
create table Booking_Day
(
Day date not null,
Booking_ID int not null
constraint Booking_Day_Booking_Booking_ID_fk
references Booking,
constraint Booking_Day_pk
primary key nonclustered (Day, Booking_ID)
)
And the function:
CREATE FUNCTION check_if_in_range (
#Event_id int,
#Day DATE
) RETURNS int
BEGIN
declare #result TABLE (Day DATE,Booking_ID INT,Event_ID INT,Date_start DATE, Data_end DATE)
INSERT into #result
SELECT Booking_Day.Day, Booking.Event_ID, Event.Date_Start, Event.Date_End
FROM ((Booking_Day INNER JOIN Booking on Booking_Day.Booking_ID = B.Booking_ID )
INNER JOIN Event on Event.Event_ID = Booking.Event_ID) WHERE Booking_Day.Day = #Day AND B.Event_ID = #Event_id
return ((#Day >= #result.Date_start) AND (#Day <= #result.Data_end))
END
Because of the primary key constraint on Booking_day table, the above should return only one row.
When trying to add function do database I get “[[S0001][137] Must declare the scalar variable “#result".
How do I deal with it? Is my approach entirely wrong and I don’t need a table within the function for this?
I don't understand why you would be using a table variable for this. You cannot just refer to a table unless you specify a FROM clause -- you are confusing table variables and scalar variables.
But why bother with variables at all?
IF (EXISTS (SELECT 1
FROM Booking_Day bd INNER JOIN
Booking b
ON bd.Booking_ID = B.Booking_ID INNER JOIN
Event e
ON e.Event_ID = b.Event_ID
WHERE b.Day = #Day AND
b.Event_ID = #Event_id AND
#Day >= e.Date_Start AND
#Day <= e.Data_end
)
)
BEGIN
return 1
END;
return 0
tl;dr: I want to generate a dates table in Redshift in order to make a report easier to generate. Preferable without needing large tables already in redshift, needing to upload a csv file.
long version:
I am working on a report where I have to average new items created per day of the week. The date range could span months or more, so there could be, say, 5 Mondays but only 4 Sundays, which can make the math a little tricky. Also, I am not guaranteed an instance of a single item per day, especially once a user starts slicing the data. Which, this is tripping up the BI tool.
The best way to tackle this problem is most likely a dates table. However, most of the tutorials for dates tables use SQL commands that are not available or not fully supported by Redshift (I'm looking at you, generate_series).
Is there an easy way to generate a dates table in Redshift?
The code I was attempting to use: (based on this also-not-working recommendation: http://elliot.land/post/building-a-date-dimension-table-in-redshift )
CREATE TABLE facts.dates (
"date_id" INTEGER NOT NULL PRIMARY KEY,
-- DATE
"full_date" DATE NOT NULL,
-- YEAR
"year_number" SMALLINT NOT NULL,
"year_week_number" SMALLINT NOT NULL,
"year_day_number" SMALLINT NOT NULL,
-- QUARTER
"qtr_number" SMALLINT NOT NULL,
-- MONTH
"month_number" SMALLINT NOT NULL,
"month_name" CHAR(9) NOT NULL,
"month_day_number" SMALLINT NOT NULL,
-- WEEK
"week_day_number" SMALLINT NOT NULL,
-- DAY
"day_name" CHAR(9) NOT NULL,
"day_is_weekday" SMALLINT NOT NULL,
"day_is_last_of_month" SMALLINT NOT NULL
) DISTSTYLE ALL SORTKEY (date_id)
;
INSERT INTO facts.dates
(
"date_id"
,"full_date"
,"year_number"
,"year_week_number"
,"year_day_number"
-- QUARTER
,"qtr_number"
-- MONTH
,"month_number"
,"month_name"
,"month_day_number"
-- WEEK
,"week_day_number"
-- DAY
,"day_name"
,"day_is_weekday"
,"day_is_last_of_month"
)
SELECT
cast(seq + 1 AS INTEGER) AS date_id,
-- DATE
datum AS full_date,
-- YEAR
cast(extract(YEAR FROM datum) AS SMALLINT) AS year_number,
cast(extract(WEEK FROM datum) AS SMALLINT) AS year_week_number,
cast(extract(DOY FROM datum) AS SMALLINT) AS year_day_number,
-- QUARTER
cast(to_char(datum, 'Q') AS SMALLINT) AS qtr_number,
-- MONTH
cast(extract(MONTH FROM datum) AS SMALLINT) AS month_number,
to_char(datum, 'Month') AS month_name,
cast(extract(DAY FROM datum) AS SMALLINT) AS month_day_number,
-- WEEK
cast(to_char(datum, 'D') AS SMALLINT) AS week_day_number,
-- DAY
to_char(datum, 'Day') AS day_name,
CASE WHEN to_char(datum, 'D') IN ('1', '7')
THEN 0
ELSE 1 END AS day_is_weekday,
CASE WHEN
extract(DAY FROM (datum + (1 - extract(DAY FROM datum)) :: INTEGER +
INTERVAL '1' MONTH) :: DATE -
INTERVAL '1' DAY) = extract(DAY FROM datum)
THEN 1
ELSE 0 END AS day_is_last_of_month
FROM
-- Generate days for 81 years starting from 2000.
(
SELECT
'2000-01-01' :: DATE + generate_series AS datum,
generate_series AS seq
FROM generate_series(0,81 * 365 + 20,1)
) DQ
ORDER BY 1;
Which throws this error
[Amazon](500310) Invalid operation: Specified types or functions (one per INFO message) not supported on Redshift tables.;
1 statement failed.
... because, I assume, INSERT and generate_series are not allowed in the same command in Redshift
In asking the question, I figured it out. Oops.
I started with a "facts" schema.
CREATE SCHEMA facts;
Run the following to start a numbers table:
create table facts.numbers
(
number int PRIMARY KEY
)
;
Use this to generate your number list. I used a million to get started
SELECT ',(' || generate_series(0,1000000,1) || ')'
;
Then copy-paste the numbers from your results in the query below, after VALUES:
INSERT INTO facts.numbers
VALUES
(0)
,(1)
,(2)
,(3)
,(4)
,(5)
,(6)
,(7)
,(8)
,(9)
-- etc
^ Make sure to remove the leading comma from the copy-pasted list of numbers
Once you have a numbers table, then you can generate a dates table (again, stealing code from elliot land http://elliot.land/post/building-a-date-dimension-table-in-redshift ) :
CREATE TABLE facts.dates (
"date_id" INTEGER NOT NULL PRIMARY KEY,
-- DATE
"full_date" DATE NOT NULL,
-- YEAR
"year_number" SMALLINT NOT NULL,
"year_week_number" SMALLINT NOT NULL,
"year_day_number" SMALLINT NOT NULL,
-- QUARTER
"qtr_number" SMALLINT NOT NULL,
-- MONTH
"month_number" SMALLINT NOT NULL,
"month_name" CHAR(9) NOT NULL,
"month_day_number" SMALLINT NOT NULL,
-- WEEK
"week_day_number" SMALLINT NOT NULL,
-- DAY
"day_name" CHAR(9) NOT NULL,
"day_is_weekday" SMALLINT NOT NULL,
"day_is_last_of_month" SMALLINT NOT NULL
) DISTSTYLE ALL SORTKEY (date_id)
;
INSERT INTO facts.dates
(
"date_id"
,"full_date"
,"year_number"
,"year_week_number"
,"year_day_number"
-- QUARTER
,"qtr_number"
-- MONTH
,"month_number"
,"month_name"
,"month_day_number"
-- WEEK
,"week_day_number"
-- DAY
,"day_name"
,"day_is_weekday"
,"day_is_last_of_month"
)
SELECT
cast(seq + 1 AS INTEGER) AS date_id,
-- DATE
datum AS full_date,
-- YEAR
cast(extract(YEAR FROM datum) AS SMALLINT) AS year_number,
cast(extract(WEEK FROM datum) AS SMALLINT) AS year_week_number,
cast(extract(DOY FROM datum) AS SMALLINT) AS year_day_number,
-- QUARTER
cast(to_char(datum, 'Q') AS SMALLINT) AS qtr_number,
-- MONTH
cast(extract(MONTH FROM datum) AS SMALLINT) AS month_number,
to_char(datum, 'Month') AS month_name,
cast(extract(DAY FROM datum) AS SMALLINT) AS month_day_number,
-- WEEK
cast(to_char(datum, 'D') AS SMALLINT) AS week_day_number,
-- DAY
to_char(datum, 'Day') AS day_name,
CASE WHEN to_char(datum, 'D') IN ('1', '7')
THEN 0
ELSE 1 END AS day_is_weekday,
CASE WHEN
extract(DAY FROM (datum + (1 - extract(DAY FROM datum)) :: INTEGER +
INTERVAL '1' MONTH) :: DATE -
INTERVAL '1' DAY) = extract(DAY FROM datum)
THEN 1
ELSE 0 END AS day_is_last_of_month
FROM
-- Generate days for 81 years starting from 2000.
(
SELECT
'2000-01-01' :: DATE + number AS datum,
number AS seq
FROM facts.numbers
WHERE number between 0 and 81 * 365 + 20
) DQ
ORDER BY 1;
^ Be sure to set the numbers at the end for the date range you need
As a workaround, you can spin Postgres instance on your local machine, run the code there, export to CSV, then run CREATE TABLE portion only in Redshift and load data from CSV. Since this is a one-time operation it's ok to do, this is what I'm actually doing for new Redshift deployments.
Here is a different suggestion for building the facts.numbers that does not require manual intervention:
Take a system table (guaranteed to exist) of a known or stable size
Cross join that table to itself enough times to get the desired number of rows
Select the row_number() over (order by 1) to turn those created records into an ascending set of numbers
Example using the Redshift system table pg_catalog.pg_operator (which as of Oct 2020 has 659 records):
-- Prep, so that you can copy/paste the code sample
create schema if not exists facts; -- Make sure the schema exists
drop table if exists facts.numbers; -- Avoid an error if that table already exists;
create table facts.numbers -- Create the table definition
(
number int primary key
);
-- The bit you care about
insert into facts.numbers
select row_number() over (order by 1) -- return 1..n in place of the original record
from pg_catalog.pg_operator a -- 659 records
cross join pg_catalog.pg_operator b -- to get 659^2=434k records
cross join pg_catalog.pg_operator c -- to get 659^3=286M records
limit 2000000 -- to limit the result to a reasonable size
;
Extending great ideas above - small fixes for starting from 2nd day of the year instead of 1st (BI tools should not be happy with this miss) + simplification and fix for flag is_last_day_of_month:
CREATE SCHEMA IF NOT EXISTS dimensions; -- Make sure the schema exists
DROP TABLE IF EXISTS dimensions.numbers; -- Avoid an error if that table already exists;
CREATE TABLE dimensions.numbers -- Create the table definition
(
number INT PRIMARY KEY
);
-- Work around for Generate_series() and INSERT INTO by Sam Davey
INSERT INTO dimensions.numbers
SELECT row_number() over (order by 1) -- return 1..n in place of the original record
FROM pg_catalog.pg_operator a -- 659 records
CROSS JOIN pg_catalog.pg_operator b -- to get 659^2=434k records
CROSS JOIN pg_catalog.pg_operator c -- to get 659^3=286M records
LIMIT 1000000 -- to limit the result to a reasonable size
;
-- Elliot solution http://elliot.land/post/building-a-date-dimension-table-in-redshift
CREATE TABLE dimensions.dates (
"date_id" INTEGER NOT NULL PRIMARY KEY,
-- DATE
"full_date" DATE NOT NULL,
-- YEAR
"year_number" SMALLINT NOT NULL,
"year_week_number" SMALLINT NOT NULL,
"year_day_number" SMALLINT NOT NULL,
-- QUARTER
"qtr_number" SMALLINT NOT NULL,
-- MONTH
"month_number" SMALLINT NOT NULL,
"month_name" CHAR(9) NOT NULL,
"month_day_number" SMALLINT NOT NULL,
-- WEEK
"week_day_number" SMALLINT NOT NULL,
-- DAY
"day_name" CHAR(9) NOT NULL,
"day_is_weekday" SMALLINT NOT NULL,
"day_is_last_of_month" SMALLINT NOT NULL
) DISTSTYLE ALL SORTKEY (date_id);
INSERT INTO dimensions.dates
(
"date_id"
,"full_date"
,"year_number"
,"year_week_number"
,"year_day_number"
-- QUARTER
,"qtr_number"
-- MONTH
,"month_number"
,"month_name"
,"month_day_number"
-- WEEK
,"week_day_number"
-- DAY
,"day_name"
,"day_is_weekday"
,"day_is_last_of_month"
)
SELECT
CAST(seq + 0 AS INTEGER) AS date_id,
-- DATE
datum AS full_date,
-- YEAR
CAST(EXTRACT(YEAR FROM datum) AS SMALLINT) AS year_number,
CAST(EXTRACT(WEEK FROM datum) AS SMALLINT) AS year_week_number,
CAST(EXTRACT(DOY FROM datum) AS SMALLINT) AS year_day_number,
-- QUARTER
CAST(TO_CHAR(datum, 'Q') AS SMALLINT) AS qtr_number,
-- MONTH
CAST(EXTRACT(MONTH FROM datum) AS SMALLINT) AS month_number,
TO_CHAR(datum, 'Month') AS month_name,
CAST(EXTRACT(DAY FROM datum) AS SMALLINT) AS month_day_number,
-- WEEK
CAST(TO_CHAR(datum, 'D') AS SMALLINT) AS week_day_number,
-- DAY
TO_CHAR(datum, 'Day') AS day_name,
CASE WHEN TO_CHAR(datum, 'D') IN ('1', '7')
THEN 0
ELSE 1 END AS day_is_weekday,
CASE WHEN LAST_DAY(datum) = datum THEN 1 ELSE 0 END AS day_is_last_of_month
FROM
-- Generate days for 81 years starting from 2000.
(
SELECT
('2000-01-01' :: DATE - interval '1 day')::DATE + number AS datum,
number AS seq
FROM dimensions.numbers
WHERE number between 0 and 81 * 365 + 20
) DQ
ORDER BY 1;
DROP TABLE dimensions.numbers;
If i have a table variable like that how to loop over this var to make some processing :
DECLARE #userData TABLE(
userId int NOT NULL,
dayDate datetime NOT NULL,
transIn datetime NULL,
transOut datetime NULL,
attIn datetime NULL,
attOut datetime NULL,
MissionIn datetime NOT NULL,
MissionOut datetime NOT NULL,
empState varchar(10) NULL
);
INSERT INTO #userData
SELECT userid, trans_date,transtime_in,transtime_out,att_start_time,att_end_time,#Mission_fromdatetime,#Mission_todatetime,day_flag
FROM datatable_o a
WHERE a.userid = #userid AND a.trans_date = #date ORDER BY transtime_in ;
According to the comments the Whole case :
If the work starts at : att_start_time and ends at att_end_time (work period]
Every employee could check -in and check-out many times in the same date so we could follow him .
the check-in stored in transtime_in
and check-out stored in transtime_out
and i have day_flag so i could know the day is 'W' work day or 'E' weekend
Now considering all these information in addition to the emp_num ,date
I want to calc for the an employee External mission over time :
I have four cases :
No check-in-out && Not work day [weekend] So the employee
should take all the mission period as overtime
No check-in-out && Work day [Absent] so the employee should
take only the mission period out of the work period
There are check-in-outs && Not work day [week end] so the
employee should take only the mission period out of these
check-ins-outs
There are check-ins-outs && work day so the employee should take
only the mission period out of these check-ins-outs and at the same
time out of work period .
Example :
emp_num date att_start att_end mission-in mission-out
672 2015-3-4 07:05:00 13:30:00 12:12:00 20:00:00
emp_num date trans_in trans_out
672 2015-3-4 06:54:00 11:10:00
672 2015-3-4 12:00:00 14:05:00
You can loop through your table by taking help from a copy of that table:
with regard to your question assuming your table is:
DECLARE #userData TABLE(
userId int NOT NULL,
/*Other fields*/
);
and the data of your table is:
INSERT INTO #userData
/*A SELECT or values*/
now create a copy of your table as:
DECLARE #userData_2 TABLE(
userId int NOT NULL,
/*Structure should be the same as #userData*/
);
INSERT INTO #userData_2
SELECT * FROM #userData
now you can do the loop and do whatever you want:
DECLARE #userId INT
WHILE EXISTS (SELECT * FROM #userData_2)
BEGIN
SET #userId=(SELECT TOP 1 userId FROM #userData)
/*
DO YOUR TRANSACTION HERE
*/
DELETE FROM #userData_2 WHERE userId=#userID
END
NOTICE: this assumes the userId is unique, if not then you need to have a unique field, or use a composite fields instead of userId.
People work from 10:00AM to 21:00PM except Sundays and public holidays.
Jobs for them are reserved at 15 minute intervals. Job duration is from 15 minutes to 4 hours. Whole job must fit to single day.
How to find first nearest free start times which are not reserved for given duration in Postgres 9.3 starting from current date and time ?
For example, Mary has already reservation at 12:30 .. 16:00 and
John has already reservation at 12:00 to 13:00
Reservat table contains reservations, yksus2 table contains workes and
pyha table contains public holidays. Table structures are below. Reservat structure can changed if this helps.
Query for ealiest start times for duration of 1.5 hours should return
John 2014-10-28 10:00
Mary 2014-10-28 10:00
John 2014-10-28 10:15
Mary 2014-10-28 10:15
John 2014-10-28 10:30
Mary 2014-10-28 10:30
Mary 2014-10-28 11:00
John 2014-10-28 13:00
Mary 2014-10-28 16:00
Mary 2014-10-28 16:15
Mary 2014-10-28 16:30
... etc and also starting from next days
I tried query based on answer in How to return only work time from reservations in PostgreSql? below but it returns wrong result:
MARY 2014-10-28 13:00:00
MARY 2014-10-29 22:34:40.850255
JOHN 2014-10-30 22:34:40.850255
MARY 2014-10-31 22:34:40.850255
MARY 2014-11-03 22:34:40.850255
Also sliding start times 10:00, 10:30 etc are not returned.
How to get proper first reservations?
Query which returns wrong result is:
insert into reservat (objekt2, during) values
('MARY', '[2014-10-28 11:30:00,2014-10-28 13:00:00)'),
('JOHN', '[2014-10-28 10:00:00,2014-10-28 11:30:00)');
with gaps as (
select
yksus,
upper(during) as start,
lead(lower(during),1,upper(during)) over (ORDER BY during) - upper(during) as gap
from (
select
yksus2.yksus,
during
from reservat join yksus2 on reservat.objekt2=yksus2.yksus
where upper(during)>= current_date
union all
select
yksus2.yksus,
unnest(case
when pyha is not null then array[tsrange1(d, d + interval '1 day')]
when date_part('dow', d) in (0, 6) then array[tsrange1(d, d + interval '1 day')]
when d::date = current_Date then array[
tsrange1(d, current_timestamp ),
tsrange1(d + interval '20 hours', d + interval '1 day')]
else array[tsrange1(d, d + interval '8 hours'),
tsrange1(d + interval '20 hours', d + interval '1 day')]
end)
from yksus2, generate_series(
current_timestamp,
current_timestamp + interval '1 month',
interval '1 day'
) as s(d)
left join pyha on pyha = d::date
) as x
)
select yksus, start
from gaps
where gap >= interval'1hour 30 minutes'
order by start
limit 30
Schema:
CREATE EXTENSION btree_gist;
CREATE TABLE Reservat (
id serial primary key,
objekt2 char(10) not null references yksus2 on update cascade deferrable,
during tsrange not null check(
lower(during)::date = upper(during)::date
and lower(during) between current_date and current_date+ interval'1 month'
and (lower(during)::time >= '10:00'::time and upper(during)::time < '21:00'::time)
AND EXTRACT(MINUTE FROM lower(during)) IN (0, 15, 30,45)
AND EXTRACT(MINUTE FROM upper(during)) IN (0, 15, 30, 45)
and (date_part('dow', lower(during)) in (1,2,3,4,5,6)
and date_part('dow', upper(during)) in (1,2,3,4,5,6))
),
EXCLUDE USING gist (objekt2 WITH =, during WITH &&)
);
create or replace function holiday_check() returns trigger language plpgsql stable as $$
begin
if exists (select * from pyha where pyha in (lower(NEW.during)::date, upper(NEW.during)::date)) then
raise exception 'public holiday %', lower(NEW.during) ;
else
return NEW;
end if;
end;
$$;
create trigger holiday_check_i before insert or update on Reservat for each row execute procedure holiday_check();
CREATE OR REPLACE FUNCTION public.tsrange1(start timestamp with time zone,
finish timestamp with time zone ) RETURNS tsrange AS
$BODY$
SELECT tsrange(start::timestamp without time zone, finish::timestamp without time zone );
$BODY$ language sql immutable;
-- Workers
create table yksus2( yksus char(10) primary key);
insert into yksus2 values ('JOHN'), ('MARY');
-- public holidays
create table pyha( pyha date primary key);
Also posted to the pgsql-general mailing list.
Adapted schema
CREATE EXTENSION btree_gist;
CREATE TYPE timerange AS RANGE (subtype = time); -- create type once
-- Workers
CREATE TABLE worker(
worker_id serial PRIMARY KEY
, worker text NOT NULL
);
INSERT INTO worker(worker) VALUES ('JOHN'), ('MARY');
-- Holidays
CREATE TABLE pyha(pyha date PRIMARY KEY);
-- Reservations
CREATE TABLE reservat (
reservat_id serial PRIMARY KEY
, worker_id int NOT NULL REFERENCES worker ON UPDATE CASCADE
, day date NOT NULL CHECK (EXTRACT('isodow' FROM day) < 7)
, work_from time NOT NULL -- including lower bound
, work_to time NOT NULL -- excluding upper bound
, CHECK (work_from >= '10:00' AND work_to <= '21:00'
AND work_to - work_from BETWEEN interval '15 min' AND interval '4 h'
AND EXTRACT('minute' FROM work_from) IN (0, 15, 30, 45)
AND EXTRACT('minute' FROM work_from) IN (0, 15, 30, 45)
)
, EXCLUDE USING gist (worker_id WITH =, day WITH =
, timerange(work_from, work_to) WITH &&)
);
INSERT INTO reservat (worker_id, day, work_from, work_to) VALUES
(1, '2014-10-28', '10:00', '11:30') -- JOHN
, (2, '2014-10-28', '11:30', '13:00'); -- MARY
-- Trigger for volatile checks
CREATE OR REPLACE FUNCTION holiday_check()
RETURNS trigger AS
$func$
BEGIN
IF EXISTS (SELECT 1 FROM pyha WHERE pyha = NEW.day) THEN
RAISE EXCEPTION 'public holiday: %', NEW.day;
ELSIF NEW.day < now()::date OR NEW.day > now()::date + 31 THEN
RAISE EXCEPTION 'day out of range: %', NEW.day;
END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql STABLE; -- can be "STABLE"
CREATE TRIGGER insupbef_holiday_check
BEFORE INSERT OR UPDATE ON reservat
FOR EACH ROW EXECUTE PROCEDURE holiday_check();
Major points
Don't use char(n). Rather varchar(n), or better yet, varchar or just text.
Any downsides of using data type "text" for storing strings?
Don't use the name of a worker as primary key. It's not necessarily unique and can change. Use a surrogate primary key instead, best a serial. Also makes entries in reservat smaller, indexes smaller, queries faster, ...
Update: For cheaper storage (8 bytes instead of 22) and simpler handling I save start and end as time now and construct a range on the fly for the exclusion constraint:
EXCLUDE USING gist (worker_id WITH =, day WITH =
, timerange(work_from, work_to) WITH &&)
Since your ranges can never cross the date border by definition, it would be more efficient to have a separate date column (day in my implementation) and a time range. The type timerange is not shipped in default installations, but easily created. This way you can largely simplify your check constraints.
Use EXTRACT('isodow', ...) to simplify excluding sundays
The day of the week as Monday(1) to Sunday(7)
I assume you want to allow the upper border of '21:00'.
Borders are assumed to be including for the lower and excluding for the upper bound.
The check whether new / updated days lie within a month from "now" is not IMMUTABLE. Moved it from the CHECK constraint to the trigger - else you might run into problems with dump / restore! Details:
Disable all constraints and table checks while restoring a dump
Aside
Besides simplifying input and check constraints I expected timerange to save 8 bytes of storage as compared to tsrange since time only occupies 4 bytes. But it turns out timerange occupies 22 bytes on disk (25 in RAM), just like tsrange (or tstzrange). So you might go with tsrange as well. The principle of query and exclusion constraint are the same.
Query
Wrapped into an SQL function for convenient parameter handling:
CREATE OR REPLACE FUNCTION f_next_free(_start timestamp, _duration interval)
RETURNS TABLE (worker_id int, worker text, day date
, start_time time, end_time time) AS
$func$
SELECT w.worker_id, w.worker
, d.d AS day
, t.t AS start_time
,(t.t + _duration) AS end_time
FROM (
SELECT _start::date + i AS d
FROM generate_series(0, 31) i
LEFT JOIN pyha p ON p.pyha = _start::date + i
WHERE p.pyha IS NULL -- eliminate holidays
) d
CROSS JOIN (
SELECT t::time
FROM generate_series (timestamp '2000-1-1 10:00'
, timestamp '2000-1-1 21:00' - _duration
, interval '15 min') t
) t -- times
CROSS JOIN worker w
WHERE d.d + t.t > _start -- rule out past timestamps
AND NOT EXISTS (
SELECT 1
FROM reservat r
WHERE r.worker_id = w.worker_id
AND r.day = d.d
AND timerange(r.work_from, r.work_to) && timerange(t.t, t.t + _duration)
)
ORDER BY d.d, t.t, w.worker, w.worker_id
LIMIT 30 -- could also be parameterized
$func$ LANGUAGE sql STABLE;
Call:
SELECT * FROM f_next_free('2014-10-28 12:00'::timestamp, '1.5 h'::interval);
SQL Fiddle on Postgres 9.3 now.
Explain
The function takes a _start timestamp as minimum starting time and _duration interval. Be careful to only rule out earlier times on the starting day, not the following days. Simplest by just adding day and time: t + d > _start.
To book a reservation starting "now", just pass now()::timestamp:
SELECT * FROM f_next_free(`now()::timestamp`, '1.5 h'::interval);
Subquery d generates days starting from the input value _day. Holidays excluded.
Days are cross-joined with possible time ranges generated in subquery t.
That is cross-joined to all available workers w.
Finally eliminate all candidates that collide with existing reservations using an NOT EXISTS anti-semi-join, and in particular the overlaps operator && .
Related:
How do you do date math that ignores the year? (for date math example)
Preventing adjacent/overlapping entries with EXCLUDE in PostgreSQL
Calculate working hours between 2 dates in PostgreSQL
Thom Brown in psql-general mailing list recommends the following solution.
It is more readable but Erwin answer looks more optimized.
I have 10 workes and 1 month reservation with 15 minute offsess from 8 to 20:00, so perfomance is hopafully not and issue.
Which to use ?
Which solution is better ?
create table pyha (pyha date primary key);
insert into pyha(pyha) values('2014-10-29');
create table yksus2(yksus char(10) primary key);
insert into yksus2 values ('JOHN'),('MARY');
CREATE EXTENSION btree_gist;
CREATE TABLE reservat
(
reservat_id serial primary key,
objekt2 char(10) not null references yksus2 on update cascade deferrable,
during tstzrange not null,
EXCLUDE USING gist (objekt2 WITH =, during WITH &&),
CONSTRAINT same_date
CHECK (lower(during)::date = upper(during)::date),
CONSTRAINT max_1month_future
CHECK (lower(during) between current_date and current_date+ interval'1 month' ),
CONSTRAINT time_between_1000_and_2100
CHECK (lower(during)::time >= '10:00'::time and upper(during)::time < '21:00'::time),
CONSTRAINT lower_bound_included
CHECK (lower_inc(during)),
CONSTRAINT upper_bound_excluded
CHECK (not upper_inc(during)),
CONSTRAINT start_time_at_15minute_offset
CHECK (EXTRACT(MINUTE FROM lower(during)) IN (0, 15, 30,45)),
-- or (extract(epoch from lower(during)::time)::int % (60*15) = 0)
CONSTRAINT end_time_at_15minute_offset
CHECK (EXTRACT(MINUTE FROM upper(during)) IN (0, 15, 30,45)),
CONSTRAINT duration_between_15min_and_4hours
CHECK (upper(during) - lower(during) between '15 mins'::interval and '4 hours'::interval),
CONSTRAINT exclude_sundays
CHECK (date_part('dow', lower(during)) in (1,2,3,4,5,6) )
);
create or replace function holiday_check() returns trigger language plpgsql stable as $$
begin
if exists (select * from pyha where pyha between lower(NEW.during)::date and upper(NEW.during)::date) then
raise exception 'public holiday %', lower(NEW.during) ;
else
return NEW;
end if;
end;
$$;
create trigger holiday_check_i before insert or update on Reservat for each row execute procedure holiday_check();
INSERT INTO reservat (objekt2, during)
VALUES ('MARY','[2014-10-29 11:30+2,2014-10-29 13:00+2)'::tstzrange);
INSERT INTO reservat (objekt2, during)
VALUES ('JOHN','[2014-10-29 10:00+2,2014-10-29 11:30+2)'::tstzrange);
SELECT yksus2.yksus, times.period
FROM generate_series(now()::date::timestamptz, now()::date::timestamptz + '3 months'::interval, '15 mins'::interval) times(period)
CROSS JOIN yksus2
LEFT JOIN reservat ON tstzrange(times.period,times.period + '1 hour 30 mins'::interval, '[)') && reservat.during
AND yksus2.yksus = reservat.objekt2
LEFT JOIN pyha ON times.period::date = pyha.pyha::date
WHERE reservat.during IS NULL
AND pyha.pyha IS NULL
AND times.period::timetz BETWEEN '10:00'::timetz AND '21:00'::timetz - '1 hour 30 mins'::interval
AND times.period >= now()
AND EXTRACT(isoDOW FROM times.period) != 7 -- exclude sundays
ORDER BY 2, 1
LIMIT 300;
I have a table containing timeslots for booking appointments and i try to figure out a way with a sql statement/view to find adjacent free timeslots for appointments of different duration.
The create table looks like this:
CREATE TABLE timeslot
(
timeslot_id bigserial NOT NULL,
duration bigint,
successor bigint,
predecessor bigint,
start_year character varying NOT NULL,
start_month character varying NOT NULL,
start_day character varying NOT NULL,
start_hour character varying NOT NULL,
start_minute character varying NOT NULL,
end_year character varying NOT NULL,
end_month character varying NOT NULL,
end_day character varying NOT NULL,
end_hour character varying NOT NULL,
end_minute character varying NOT NULL,
employee_id integer NOT NULL,
available_status_id integer,
appoint_calendar_id integer
CONSTRAINT timeslot_id PRIMARY KEY (timeslot_id),
CONSTRAINT appoint_calendar_id FOREIGN KEY (appoint_calendar_id)
REFERENCES appoint_calendar (appoint_calendar_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT available_status_id FOREIGN KEY (available_status_id)
REFERENCES available_status (available_status_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT employee_id FOREIGN KEY (employee_id)
REFERENCES employee (employee_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
Here is example insert data where a available_status_id of 1 means a free timeslot
and a available_status_id of 2 is a free timeslot:
INSERT INTO timeslot(
timeslot_id, duration, successor, predecessor, start_year, start_month,
start_day, start_hour, start_minute, end_year, end_month, end_day,
end_hour, end_minute, employee_id, available_status_id, appoint_calendar_id)
VALUES (11870, 30, null, 11869, "2013", "09",
"02", "18", "00", "2013", "09", "02",
"18", "30", 4, 1, null);
INSERT INTO timeslot(
timeslot_id, duration, successor, predecessor, start_year, start_month,
start_day, start_hour, start_minute, end_year, end_month, end_day,
end_hour, end_minute, employee_id, available_status_id, appoint_calendar_id)
VALUES (11904, 30, 12000, 11999, "2013", "09",
"09", "10", "30", "2013", "09", "09",
"11", "00", 5, 2, 761);
I am looking for a query in postgres to find all free timeslots for appointments of different durations like 15, 30 or 60 minutes. At the moment I just get all free timeslots from the database and iterate over them in Java and add together the minutes of duration until I have found enough adjacent timeslots and return then the first timeslot for each subgroup to be displayed in a calendar. But there must be a better and quicker way in postgres?
Thanks in advance
Edit
Input is the needed duration in minutes (e.g 60), the employee_id (e.g. 5) and a date (e.g. 09.09.2013).
Required output are all subsets that are adjacent (in time), free and have enough duration.
For the above example this could be:
timeslot_id 11904
duration 30
successor 12000
predecessor 11999
start_year 2013
start_month 09
start_day 09
start_hour 10
start_minute 30
end_year 2013
end_month 09
end_day 09
end_hour 11
end_minute 00
employee_id 5
available_status_id 1
appoint_calendar_id null
and
timeslot_id 12000
duration 30
successor 11906
predecessor 11904
start_year 2013
start_month 09
start_day 09
start_hour 11
start_minute 00
end_year 2013
end_month 09
end_day 09
end_hour 11
end_minute 30
employee_id 5
available_status_id 1
appoint_calendar_id null
Personally, I think it's good idea to make this in Java code.
Other option may be creating PostgreSQL function with cursor.
But if you really want to perform this in one SQL request and suppose you have timeslot_id that is differs by exactly one for each consecutive time slot and you can predict maximum appointment duration and number of time slots needed you can try something like this:
select ts1.timeslot_id as start_timeslot_id,
coalesce(t4.timeslot_id, t3.timeslot_id, t2.timeslot_id, t1.timeslot_id) as end_timeslot_id,
coalesce(t4.end_hour, t3.end_hour, t2.end_hour, t1.end_hour)*60+coalesce(t4.end_minute, t3.end_minute, t2.end_minute, t1.end_minute) - t1.start_hour*60+t1.start_minute as duration_minutes
from timeslot ts1
left join timeslot ts2
on ts1.timeslot_id+1 = ts2.timeslot_id
and t12.available_status_id = 1
left join timeslot ts3
on ts2.timeslot_id+1 = ts3.timeslot_id
and ts3.available_status_id = 1
left join timeslot ts4
on ts3.timeslot_id+1 = ts4.timeslot_id
and ts4.available_status_id = 1
where ts1.start_year = '2013' -- these all are your input parameters
and ts1.start_month = '09'
and ts1.start_day = '09'
and employee_id = 5
and coalesce(t4.end_hour, t3.end_hour, t2.end_hour, t1.end_hour)*60+coalesce(t4.end_minute, t3.end_minute, t2.end_minute, t1.end_minute) - t1.start_hour*60+t1.start_minute >= 60 -- duration in minutes
Presumably, this request will give you every possible time slots that are bigger or equals to required one.
I didn't try to run this query against real database, so it might contain errors.
CREATE TABLE appoint_calendar ( appoint_calendar_id SERIAL NOT NULL PRIMARY KEY);
INSERT INTO appoint_calendar(appoint_calendar_id) VALUES (761),(762);
CREATE TABLE employee (employee_id SERIAL NOT NULL PRIMARY KEY);
INSERT INTO employee(employee_id) VALUES (4),(5);
CREATE TABLE available_status (available_status_id SERIAL NOT NULL PRIMARY KEY);
INSERT INTO available_status(available_status_id) VALUES (1),(2);
CREATE TABLE timeslot
( timeslot_id bigserial NOT NULL PRIMARY KEY
, duration bigint
, successor bigint
, predecessor bigint
, start_date timestamp with time zone
, end_date timestamp with time zone
, employee_id integer NOT NULL
REFERENCES employee (employee_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
, available_status_id integer
REFERENCES available_status (available_status_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
, appoint_calendar_id integer
REFERENCES appoint_calendar (appoint_calendar_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
INSERT INTO timeslot(timeslot_id, duration,successor,predecessor,start_date,end_date,employee_id,available_status_id,appoint_calendar_id) VALUES
(11870, 30, null, 11869, '2013-09-02 18:00:00', '2013-09-02 18:30:00', 4, 1, null)
, (11904, 30, 12000, 11999, '2013-09-09 10:30:00', '2013-09-09 11:00:00', 5, 2, 761)
;
Select some timeslots (the interval arithmetic is not entirely correct, YMMV)
SELECT * FROM timeslot ts
WHERE ts.employee_id IN(5)
AND ts.available_status_id IN(1, 2)
AND ts.start_date::date = '2013-09-09'::date
AND ts.end_date >= (ts.start_date + '30 min'::interval)
;