Postgres weekly timetable - postgresql

I want to have a weekly timetable table in postgres, That stores availability time of a person in a week
Then I want to get a time range from a user and check that the person in available in that range or not
How can I do that in postgres , I know that Range type can help me, But I don't know how implement weekly tine table
timetable
╔════════╦═════════════╦═════════════════════╗
║ userId ║ day_of_week ║ during ║
╠════════╬═════════════╬═════════════════════╣
║ 1 ║ 0 ║ [10:00:00 12:00:00] ║
║ 1 ║ 0 ║ [14:30:00 16:45:00] ║
║ 1 ║ 1 ║ [10:00:00 12:00:00] ║
║ 1 ║ 1 ║ [08:30:00 12:00:00] ║
║ 1 ║ 1 ║ [13:30:00 17:00:00] ║
║ 1 ║ 1 ║ [19:30:00 23:00:00] ║
║ 1 ║ 2 ║ [10:00:00 12:00:00] ║
╚════════╩═════════════╩═════════════════════╝
day_of_week => The day of the week as Sunday(0) to Saturday(6)
I want to know , is user #1 available in a specific time range?
(2020-05-31 11:00, 2020-05-31 11:30) => true
(2020-05-31 11:00, 2020-05-31 15:30) => false
(2020-05-31 11:00, 2020-06-02 00:00) => false
the timetable is just a sample , and You can offer better schema
A user can be available 24 hours, i.e in Sunday-Tuesday
timetable
╔════════╦═════════════╦═════════════════════╗
║ userId ║ day_of_week ║ during ║
╠════════╬═════════════╬═════════════════════╣
║ 2 ║ 0 ║ [00:00:00 24:00:00] ║
║ 2 ║ 1 ║ [00:00:00 24:00:00] ║
║ 2 ║ 2 ║ [00:00:00 24:00:00] ║
╚════════╩═════════════╩═════════════════════╝
So we have
(2020-05-31 11:00, 2020-05-31 11:30) => true
(2020-05-31 11:00, 2020-05-31 15:30) => true
(2020-05-31 11:00, 2020-06-02 00:00) => true
(2020-06-02 00:00, 2020-06-03 01:00) => false

If you can split up the input value (parameter) into each day, you can do something like this:
select bool_and(tt.user_id is not null)
from (
values
(0, timerange('11:00', '24:00')),
(1, timerange('00:00', '24:00'))
) as t(dow, day_range)
left join timetable tt
on tt.day_of_week = t.dow
and tt.during #> t.day_range
and tt.user_id = 2;
If you are limited by passing a timestamp range, you will need to turn the range into the corresponding number of days and time ranges per week day.
with parm (user_id, input) as (
values
-- these are essentially your query parameters
(2, tsrange('2020-05-31 11:00', '2020-06-02 00:00', '[)'))
), days as (
select user_id,
timerange(
case
when g.day::date = lower(input)::date then lower(input)::time
when g.day::date > lower(input)::date and g::date <= upper(input)::date then time '00:00'
end,
case
when g.day::date = upper(input)::date then upper(input)::time
when g.day::date < upper(input)::date and g::date >= lower(input)::date then time '24:00'
end, '()') day_range,
extract(dow from g.day) as dow
from parm
cross join generate_series(lower(input)::date, upper(input)::date, interval '1 day') as g(day)
)
select d.day_range as input_range,
d.dow as input_dow,
tt.user_id,
tt.during,
tt.user_id is not null as is_match
from days d
left join timetable tt
on d.dow = tt.day_of_week
and tt.during #> d.day_range
and tt.user_id = d.user_id
Listing each column of the result is mainly there to show how it works. If you just need a true/false result, you can do bool_and(tt_user_id is not null) as I did in the first select. The first CTE (with parm) can be removed if you replace the parameter in the second CTE directly.

Related

PostgreSQL - make two selects in two columns

How can i add order, group by and limit for each arr in query?
users table:
╔════╦══════════════╦
║ id ║ name ║
╠════╬══════════════╬
║ 1 ║ Jeff Atwood ║
║ 2 ║ Geoff Dalgas ║
║ 3 ║ Jarrod Dixon ║
║ 4 ║ Joel Spolsky ║
╚════╩══════════════╩
Output query example without filter and limit:
SELECT JSON_AGG(u) filter (where id > 1) as arr1,
JSON_AGG(u) filter (where id < 3) as arr2
FROM users u
Expected:
╔═══════════════════╦═════════════════════╦
║ arr1 ║ arr2 ║
╠═══════════════════╬═════════════════════╬
║ [{id:1, name: ''},║ [{id:1, name: ''}, ║
║ {id:2, name: ''}] ║ {id:2, name: ''}] ║
╚═══════════════════╩══════════════════════
Query arguments example for one array:
SELECT *
FROM ps
LEFT JOIN u on u.id = ps.id
WHERE ps.date <= now()
GROUP BY ps.id
ORDER BY ps.date DESC
Order BY
You can put order by with in aggregate function like this:
SELECT json_agg(users.* order by id desc) FILTER (WHERE id > 1 ) AS arr1,
json_agg(users.*) FILTER (WHERE id < 2) AS arr2
FROM users;
As per my understanding LIMIT is not allowed to use in this way i.e. array wise

Postgres make_date handle exception

So I'm storing the user's birthday and month. I'm sending them birthday deals and these deals expire each at a different interval, that is "number of days after their birthday".
I have constructed the formula successfully (below is a chunk of it) but am left with this problem:
('2015-10-10'::date >= make_date(2015, users.birth_month, users.birth_day) ...
If the user was born on Feb 29, make_date would raise an exception for invalid years ( ERROR: date field value out of range: 2015-02-29 )
How can I gracefully handle this? (I have other ways of handling this but they require me to give special treatment for leap years)
This can be achieved by using combination of make_date and interval PostgreSQL fucntions (see https://www.postgresql.org/docs/9.6/functions-datetime.html for more info).
The trick is to ensure the date from make_date is always valid. This can be easily achieved by using the 1st day of the month and then use interval (minus 1 day) to increment the desired number of days (while avoiding ERROR: date field value out of range errors).
This is exemplified by creating a little table users: -
create table users (
id serial primary key,
birth_day integer,
birth_month integer
);
... and populating it with some data ...
insert into users (birth_day, birth_month) values
(1, 2),
(28, 2),
(29, 2),
(30, 2),
(31, 2);
If the year is 2015 you can run the following query: -
select birth_day,
birth_month,
make_date(2015, birth_month, 1) + (birth_day - 1) as birthday
from users;
╔═══════════╦═════════════╦══════════════╗
║ birth_day ║ birth_month ║ birthday ║
╠═══════════╬═════════════╬══════════════╣
║ 1 ║ 2 ║ "2015-02-01" ║
║ 28 ║ 2 ║ "2015-02-28" ║
║ 29 ║ 2 ║ "2015-03-01" ║ <---- wrapped to march 1st
║ 30 ║ 2 ║ "2015-03-02" ║
║ 31 ║ 2 ║ "2015-03-03" ║
╚═══════════╩═════════════╩══════════════╝
However, if the year is 2016 (leap year) then it's: -
select birth_day,
birth_month,
make_date(2016, birth_month, 1) + (birth_day - 1) as birthday
from users;
╔═══════════╦═════════════╦══════════════╗
║ birth_day ║ birth_month ║ birthday ║
╠═══════════╬═════════════╬══════════════╣
║ 1 ║ 2 ║ "2016-02-01" ║
║ 28 ║ 2 ║ "2016-02-28" ║
║ 29 ║ 2 ║ "2016-02-29" ║
║ 30 ║ 2 ║ "2016-03-01" ║ <---- wrapped to march 1st
║ 31 ║ 2 ║ "2016-03-02" ║
╚═══════════╩═════════════╩══════════════╝
Thanks a_horse_with_no_name for pointing out that you don't need to use interval if it's days - simple addition is all you need i.e. + ((birth_day - 1) || ' days')::interval can be simplified to + (birth_day - 1)
Okay, I have added a special case for when the birthday is 02-29. This would solve my problem but I would totally love to hear other suggestions.
('2015-10-10'::date >=
case when users.birth_month = 2 and users.birth_day = 29 make_date(2015,2, 28)
else make_date(2015, users.birth_month, users.birth_day)
end
)
The user in this case would lose a day (I can make them win a day by doing march 1st) but you get the point =)

Getting this group by and max query to work

I have the following table players with an:
ID;color;race;score
1;"red";"elf";400
2;"blue";"elf";500
3;"green";"elf";300
4;"blue";"elf";200
5;"red";"elf";700
6;"red";"troll";100
7;"blue";"troll";400
8;"green";"troll";500
9;"red";"troll";400
10;"yellow";"troll";1000
11;"red";"nord";900
12;"green";"nord";100
13;"yellow";"nord";500
14;"blue";"nord";7000
I want per race, the maximum score and the color and ID of that player. Like this
elf 700 red 5
nord 7000 blue 14
troll 1000 yellow 10
The first two column I can get with:
select category,max(score)
from players
group by category;
But I'm unable to add the color and ID of that player. How do I do this?
You can use RANK function:
WITH cte AS
(
SELECT *, RANK() OVER(PARTITION BY race ORDER BY score DESC) AS r
FROM players
)
SELECT race, score, color, id
FROM cte
WHERE r = 1;
LiveDemo
Output:
╔═══════╦═══════╦════════╦════╗
║ race ║ score ║ color ║ id ║
╠═══════╬═══════╬════════╬════╣
║ elf ║ 700 ║ red ║ 5 ║
║ nord ║ 7000 ║ blue ║ 14 ║
║ troll ║ 1000 ║ yellow ║ 10 ║
╚═══════╩═══════╩════════╩════╝

complex SQL INSERT not working

I am working on migrating data from 1 row by X columns in one table to X rows in another table. In the first table, multiple boolean records AF.AdditionalFieldsBoolean15 (and 14, 13, 12, etc) are stored in a single row. I need to create a single row to represent each 'true' boolean column.
I have been working with the following code, which executes flawlessly, except that it only inserts 1 record. When I run the SELECT statement in the second code block, I return 12 records. How do I ensure that I am iterating over all records?
BEGIN TRAN TEST
DECLARE #id uniqueidentifier = NEWID()
DECLARE #dest uniqueidentifier = 'AC34C8E5-8859-4E74-ACF2-54B3804AE9C9'
DECLARE #person uniqueidentifier
SELECT #person = GP.Oid FROM <Redacted>.GenericPerson AS GP
INNER JOIN <Redacted>.AdditionalFields AS AF
ON GP.AdditionalFields = AF.Oid
WHERE AF.AdditionalBoolean15 = 'true'
INSERT INTO <Redacted>.Referral (Oid, Destination, Person)
OUTPUT INSERTED.*
VALUES (#id, #dest, #person)
Select statement that returns 12 records
SELECT *
FROM WS_Live.dbo.GenericPerson AS GP
INNER JOIN WS_Live.dbo.AdditionalFields AS AF
ON GP.AdditionalFields = AF.Oid
WHERE AF.AdditionalBoolean15 = 'true'
|
|
|
|
|
|
--------------SOLUTION (EDIT)--------------------
Thanks to M.Ali I was able to muddle through pivot tables and worked out the following solution. Just wanted to post with some explanation in case anyone needs this in the future.
INSERT INTO <Redacted>.Referral (Person, Destination, Oid)
OUTPUT INSERTED.*
SELECT Person
, Destination
, NEWID() AS Oid
FROM
(
SELECT
GP.Oid AS Person,
AF.AdditionalBoolean15 AS 'AC34C8E5-8859-4E74-ACF2-54B3804AE9C9',
AF.AdditionalBoolean14 AS '7DE4B414-42E0-4E39-9432-6DC9F60A5512',
AF.AdditionalBoolean8 AS '5760A126-AD15-4FF4-B608-F1C4220C7087',
AF.AdditionalBoolean13 AS '4EFFB0FB-BB6C-4425-9653-D482B6C827AC',
AF.AdditionalBoolean17 AS '0696C571-EEFA-4FE6-82DA-4FF6AB96CC98',
AF.AdditionalBoolean4 AS 'FF381D63-A76C-46F1-8E2C-E2E3C69365BF',
AF.AdditionalBoolean20 AS 'C371E419-4E34-4F46-B07D-A4533491D944',
AF.AdditionalBoolean16 AS '1F0D1221-76D7-4F1F-BB7A-818BB26E0590',
AF.AdditionalBoolean18 AS 'C6FD53A8-37B9-4519-A825-472722A158C9',
AF.AdditionalBoolean19 AS 'BEBD6ED6-AF0A-4A05-A1C1-060B2926F83E'
FROM <Redacted>.GenericPerson GP
INNER JOIN <Redacted>.AdditionalFields AF
ON GP.AdditionalFields = AF.Oid
)AS cp
UNPIVOT
(
Bool FOR Destination IN ([AC34C8E5-8859-4E74-ACF2-54B3804AE9C9],
[7DE4B414-42E0-4E39-9432-6DC9F60A5512],
[5760A126-AD15-4FF4-B608-F1C4220C7087],
[4EFFB0FB-BB6C-4425-9653-D482B6C827AC],
[0696C571-EEFA-4FE6-82DA-4FF6AB96CC98],
[FF381D63-A76C-46F1-8E2C-E2E3C69365BF],
[C371E419-4E34-4F46-B07D-A4533491D944],
[1F0D1221-76D7-4F1F-BB7A-818BB26E0590],
[C6FD53A8-37B9-4519-A825-472722A158C9],
[BEBD6ED6-AF0A-4A05-A1C1-060B2926F83E])
)AS up
WHERE Bool = 'true'
ORDER BY Person, Destination
First of all, I'm not sure why this SELECT NEWID() at the top worked, where I have received errors when trying to SELECT NEWID() before.
I felt a little creative about using statements like
AF.AdditionalBoolean19 AS 'BEBD6ED6-AF0A-4A05-A1C1-060B2926F83E'
because the table I was inserting into required a GUID from another table that represented a plaintext 'Name'. There is no table linking each column name to that GUID, so I think this was the best way, but I would like to hear if anyone can think of a better way.
A demo how you can unpivot your AdditionalBooleanN columns and rather then doing it row by row just use where clause to filter result and insert into intended destination tables.
Test Data
DECLARE #TABLE TABLE
(ID INT , dest INT, Person INT, Bol1 INT, Bol2 INT, Bol3 INT)
INSERT INTO #TABLE VALUES
(1 , 100 , 1 , 1 , 1 , 1) ,
(2 , 200 , 2 , 1 , 1 , 0) ,
(3 , 300 , 3 , 1 , 0 , 0) ,
(4 , 400 , 4 , 0 , 0 , 0)
Query
-- INSERT INTO Destination_Table (ID , Dest, Person, bol_Column)
SELECT * --<-- Only select columns that needs to be inserted
FROM #TABLE t
UNPIVOT ( Value FOR Bool_Column IN (Bol1, Bol2, Bol3) )up
-- WHERE Bool_Column = ??
Result
╔════╦══════╦════════╦═══════╦═════════════╗
║ ID ║ dest ║ Person ║ Value ║ Bool_Column ║
╠════╬══════╬════════╬═══════╬═════════════╣
║ 1 ║ 100 ║ 1 ║ 1 ║ Bol1 ║
║ 1 ║ 100 ║ 1 ║ 1 ║ Bol2 ║
║ 1 ║ 100 ║ 1 ║ 1 ║ Bol3 ║
║ 2 ║ 200 ║ 2 ║ 1 ║ Bol1 ║
║ 2 ║ 200 ║ 2 ║ 1 ║ Bol2 ║
║ 2 ║ 200 ║ 2 ║ 0 ║ Bol3 ║
║ 3 ║ 300 ║ 3 ║ 1 ║ Bol1 ║
║ 3 ║ 300 ║ 3 ║ 0 ║ Bol2 ║
║ 3 ║ 300 ║ 3 ║ 0 ║ Bol3 ║
║ 4 ║ 400 ║ 4 ║ 0 ║ Bol1 ║
║ 4 ║ 400 ║ 4 ║ 0 ║ Bol2 ║
║ 4 ║ 400 ║ 4 ║ 0 ║ Bol3 ║
╚════╩══════╩════════╩═══════╩═════════════╝

PostgreSQL - How to get empty (null) records from the same table?

I would like to get data from my table, including empty rows. For instance: I want to know, if there was some activity during last 30 minutes. If there was something, then I can get data and it works, but how to include those records without any activity? All my data are in the same table.
Thanks!
╔═════╦═════════════════════╦══════╗
║ SRV ║ DATE ║ FLAG ║
╠═════╬═════════════════════╬══════╣
║ 1 ║ 2013-01-01 08:10:12 ║ 4 ║
║ 1 ║ 2013-01-01 08:11:24 ║ 4 ║
║ 1 ║ 2013-01-01 08:12:01 ║ 5 ║
║ 1 ║ 2013-01-01 08:12:14 ║ 5 ║
║ 2 ║ 2013-01-01 08:20:44 ║ 4 ║
║ 2 ║ 2013-01-01 08:23:11 ║ 5 ║
║ 1 ║ 2013-01-01 08:24:09 ║ 4 ║
║ 1 ║ 2013-01-01 08:28:54 ║ 5 ║
║ 1 ║ 2013-01-01 08:30:01 ║ 4 ║
║ 3 ║ 2013-01-01 08:32:31 ║ 4 ║
║ 3 ║ 2013-01-01 08:32:45 ║ 4 ║
║ 1 ║ 2013-01-01 08:35:21 ║ 4 ║
╚═════╩═════════════════════╩══════╝
I want to get number of flags with status 4, in last 10 minutes in that case, grouped by SRV. When there was no flag 4 in specified time period, there should be record with SRV and NULL value. In this case, my query should return:
╔═════╦═══════╦══╗
║ SRV ║ COUNT ║ ║
╠═════╬═══════╬══╣
║ 1 ║ 2 ║ ║
║ 2 ║ NULL ║ ║
║ 3 ║ 2 ║ ║
╚═════╩═══════╩══╝
I was able to count existing flags using:
select srv, count(*)
from table1
where flag=4 and date >= now() - interval '10m'
group by svr
order by 1
Try with SUM, this will return 0
select srv, SUM( CASE WHEN flag=4 THEN 1 ELSE 0 END )
from table1
where date >= now() - interval '10m'
group by svr
order by 1
If you really want NULL, then replace 0 with NULL:
select srv,
CASE SUM( CASE WHEN flag=4 THEN 1 ELSE 0 END )
WHEN 0 THEN NULL
ELSE SUM( CASE WHEN flag=4 THEN 1 ELSE 0 END )
END
from table1
where date >= now() - interval '10m'now() - interval '10m'
group by svr
order by 1
In case there are no records with some SRV value in last 30 minutes, then the above queries don't list this SRV.
To correct this, you can use this query:
select srv,
SUM( CASE WHEN flag=4 AND date >= (now() - interval '10m')
THEN 1 ELSE 0 END )
from table1
group by srv
order by 1
but this query will always read the whole table, so may be ineficcient.
The below version reads all SRV values (if there is an index on SRV column, this operation should be fast), and perform the aggregation only on a subset of the table (last 30 minutes), therefore may be faster than the previous one:
SELECT q1.srv, q2.cnt
FROM (
SELECT DISTINCT srv
FROM table1
) q1
LEFT JOIN (
select srv, count( * ) as cnt
from table1
where flag=4 AND date >= now() - interval '10m'
group by svr
) q2
ON q1.srv = q2.srv
I would do this really simply - first, I would use some table with all possible values of srv (I used array, in order to keep it simple) and then just count all flags for that srv value:
SELECT
s.x, CASE WHEN s.num = 0 THEN NULL ELSE s.num END
FROM (
SELECT x, (
SELECT COUNT(1)
FROM table1
WHERE flag = 4 AND date >= NOW() - INTERVAL '10m' AND srv = x
) AS num
FROM
UNNEST(ARRAY[1, 2, 3]) AS x
ORDER by x) AS s;
This returns:
x | num
---+-----
1 | 2
2 |
3 | 2
(3 rows)
If you have srv values in some table, just replace UNNEST(ARRAY[..]) with
this table's name.