Displaying listagg values based on values in another column (part 2) - sql

I posted a similar question previously here: Displaying LISTAGG values in a cell based on the value of another column.
In that question, I was using UPDATE to store a comma delimited list.
I got a solution for that but I realized thanks to the contributors, that this is not the best way to do so.
I am using Oracle APEX and need to display this data in a table.
ID Schedule Days
001 Weekend Saturday, Sunday
I have 3 tables to work with. They are, for simplicity, DAYS, SCHEDULES and DAY_SCHEDULES.
DAYS table
Day_ID - PK
Day_Order
Day_Name
Schedule (I added this column myself, the others were already provided)
SCHEDULES table
Schedule_ID - PK
Schedule (only two possible values: 'Weekend' or 'Weekday')
Days
Several other columns but not relevant
DAY_SCHEDULES table
Day_schedules_id - PK
Schedule_ID - FK
Day_ID - FK
I have tried several ways to make the Days column display a list of the corresponding days but none seem to work. My main attempts have been fiddling with SELECT/CASE statements but it still will not display anything even when the syntax is all correct.
I cannot make an FK between the two tables as neither Schedules column is unique.
I am not sure what to try next so any help or tips are appreciated!
Thank you,
Hassan

I think this is what you are trying to achieve (you do not need the DAYS column in the schedule table as you can calculate it from the days table):
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE DAYS ( Day_ID, Day_Order, Day_Name, Schedule ) AS
SELECT LEVEL,
LEVEL - 1,
TO_CHAR(
TRUNC( SYSDATE, 'IW' ) + LEVEL - 1,
'FMDay',
'NLS_LANGUAGE=ENGLISH'
),
CASE
WHEN LEVEL <= 5
THEN 'Weekday'
ELSE 'Weekend'
END
FROM DUAL
CONNECT BY LEVEL <= 7;
CREATE TABLE SCHEDULES ( Schedule_ID, Schedule, Other ) AS
SELECT 1, 'Weekend', 'ABC' FROM DUAL UNION ALL
SELECT 2, 'Weekday', 'DEF' FROM DUAL;
Query 1:
SELECT s.*,
d.days
FROM schedules s
INNER JOIN
( SELECT schedule,
LISTAGG( day_name, ', ' )
WITHIN GROUP ( ORDER BY day_order ) AS days
FROM days
GROUP BY schedule ) d
ON ( s.schedule = d.schedule )
Results:
| SCHEDULE_ID | SCHEDULE | OTHER | DAYS |
|-------------|----------|-------|----------------------------------------------|
| 2 | Weekday | DEF | Monday, Tuesday, Wednesday, Thursday, Friday |
| 1 | Weekend | ABC | Saturday, Sunday |

Related

Select records by month and year between two dates

I have the table record_b. I want to select the records of an specific month and year between begin_date and end_date.
id
begin_date
end_date
2
2022-09-04
2022-10-03
3
2022-10-04
2022-10-31
4
2022-11-04
2022-12-03
5
2022-12-04
2023-01-03
6
2023-01-04
2023-02-03
7
2023-02-04
null
eg1:
Input: 2023-01
Output should be the record with id 5 and 6
eg2:
Input: 2022-12
Output should be the record with id 4 and 5
I have tried using between however there is a problem evaluating the months after the year.
and v_year BETWEEN EXTRACT(YEAR FROM PC.begin_date)
AND EXTRACT(YEAR FROM PC.end_date)
AND v_month BETWEEN EXTRACT(MONTH FROM PC.begin_date)
AND EXTRACT(MONTH FROM PC.end_date)
A very basic dictate is when you have a date store it as a date.
This can be further extended to when you need to process dates then process dates.
Most of the nothing else will be needed - no conversion, extract, date_part, epoch - just dates.
The task here is to find those rows where a specified Year-Month (yyyy-mm) falls within the period begin and end dates from a table.
Realize that if any portion of the specified year-month falls within the period then the first day of that month (yyyy-mm-01) falls within that period.
You can use the make_date() function to get the first of the specified month. Then JOIN that result with between dates.
with input_val(yr_mon) as (values (:yyyymm)) --select * from input_val
, tgt_date(dt) as
( select 0make_date(substring(yr_mon,1,4)::integer
,substring(yr_mon,6,2)::integer
,01
)
from input_val
) --select * from tgt_date;
select rb.*
from tgt_date t
join record_b rb
on t.dt between date_trunc('month',rb.begin_date)
and date_trunc('month',rb.end_date);
The above however does NOT handle well data point 7 with a null end date (nor would it handle a null start date). But should it?
If so a null value is often interpreted as there in no ending date, which basically says all dates on or after the start date are included.
You can handle the situation by converting the period to a daterange, which will handle it without getting into null processing logic, then use the range containment operator.
with input_val(yr_mon) as (values (:yyyymm)) --select * from input_val
, tgt_date(dt) as
( select make_date(substring(yr_mon,1,4)::integer
,substring(yr_mon,6,2)::integer
,01
)
from input_val
) --select * from tgt_date;
select rb.*
from tgt_date t
join record_b rb
on t.dt <# daterange(date_trunc('month',rb.begin_date)::date
,date_trunc('month',rb.end_date)::date
, '[]'
);
Finally, depending on how you you use the results, you can hide this whole thing within a SQL function, which can then be used in an SQL statement.
create or replace function periods_with_year_month(year_mm text)
returns setof record_b
language sql
as $$
with tgt_date(dt) as
(select make_date(substring(year_mm,1,4)::integer
,substring(year_mm,6,2)::integer
,01
)
)
select rb.*
from tgt_date t
join record_b rb
on t.dt <# daterange( date_trunc('month',rb.begin_date)::date
, date_trunc('month',rb.end_date)::date
, '[]'
);
$$;
See demo here. Unfortunately ,db<>fiddle is non-interactive, so parameters of yyyy-mm are hard coded.

create a temporary sql table using recursion as a loop to populate custom time interval

Suppose you have a table like:
id subscription_start subscription_end segment
1 2016-12-01 2017-02-01 87
2 2016-12-01 2017-01-24 87
...
And wish to generate a temporary table with months.
One way would be to encode the month date as:
with months as (
select
'2016-12-01' as 'first',
'2016-12-31' as 'last'
union
select
'2017-01-01' as 'first',
'2017-01-31' as 'last'
...
) select * from months;
So that I have an output table like:
first_day last_day
2017-01-01 2017-01-31
2017-02-01 2017-02-31
2017-03-01 2017-03-31
I would like to generate a temporary table with a custom interval (above), without manually encoding all the dates.
Say the interval is of 12 months, for each year, for as many years there are in the db.
I'd like to have general approach to compute the months table with the same output as above.
Or, one may adjust the range to a custom interval (months split an year in 12 parts, but one may want to split a time in a custom interval of days).
To start, I was thinking to use recursive query like:
with months(id, first_day, last_day, month) as (
select
id,
first_day,
last_day,
0
where
subscriptions.first_day = min(subscriptions.first_day)
union all
select
id,
first_day,
last_day,
months.month + 1
from
subscriptions
left join months on cast(
strftime('%m', datetime(subscriptions.subscription_start)) as int
) = months.month
where
months.month < 13
)
select
*
from
months
where
month = 1;
but it does not do what I'd expect: here I was attempting to select the first row from the table with the minimum date, and populate a table at interval of months, ranging from 1 to 12. For each month, I was comparing the string date field of my table (e.g. 2017-03-01 = 3 is march).
The query above does work and also seems a bit complicated, but for the sake of learning, which alternative would you propose to create a temporary table months without manually coding the intervals ?

UPDATE month and year to current but leave day

I have situation in Oracle DB where I need to UPDATE every month some dates in table following this condition:
1) If date in table like '03.06.2017' UPDATE to '03.11.2017'
2) If date in table like '29.06.2016' UPDATE to '29.11.2017'
2) If date in table like '15.02.2016' UPDATE to '15.11.2017'
So basically always UPDATE part of date(month, year) to current month/year but always leave day as it is.
Edit:
It will be all months from 1-12 not only June. I need to do something like this... UPDATE table SET date = xx.(month from sysdate).(year from sysdate) WHERE... xx (day) leave as it is in DB.
Br.
You can use MONTHS_BETWEEN to determine how many months you need to add and then use the ADD_MONTHS function:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE dates ( value ) AS
SELECT DATE '2017-06-03' FROM DUAL UNION ALL
SELECT DATE '2016-06-29' FROM DUAL UNION ALL
SELECT DATE '2016-02-15' FROM DUAL UNION ALL
SELECT DATE '2016-03-31' FROM DUAL;
Update:
UPDATE dates
SET value = ADD_MONTHS(
value,
CEIL( MONTHS_BETWEEN( TRUNC( SYSDATE, 'MM' ), value ) )
);
Query 1:
SELECT * FROM dates
Results:
| VALUE |
|----------------------|
| 2017-11-03T00:00:00Z |
| 2017-11-29T00:00:00Z |
| 2017-11-15T00:00:00Z |
| 2017-11-30T00:00:00Z | -- There are not 31 days in November
Probably you want
update your_table
set this_date = add_months(this_date, 5)
where ...
This will add five months to the selected dates.
Your edited question says you want to update all the dates to the current month and year; you can automate it like this ...
update your_table
set this_date = add_months(this_date,
months_between(trunc(sysdate,'mm'), trunc(this_date, 'mm')))
-- or whatever filter you require
where this_date between trunc(sysdate, 'yyyy') and sysdate
/
Using month_between() guarantees that you won't get invalid dates such as '2017-11-31'. You say in a comment that all the dates will be < 05.mm.yyyy but your sample data disagrees. Personally I'd go with a solution that doesn't run the risk of data integrity issues, because the state of your data tomorrow may will be different from its state today.
Check out the LiveSQL demo.
I would start off with something like this to get my dates and then craft an update from it (substitute old_date with your date column and source_table with the table name):
select old_date, to_char(sysdate, 'YYYY-MM-') || to_char(old_date, 'DD') from source_table;

Find Intersection Between Date Ranges In PostgreSQL

I have records with a two dates check_in and check_out, I want to know the ranges when more than one person was checked in at the same time.
So if I have the following checkin / checkouts:
Person A: 1PM - 6PM
Person B: 3PM - 10PM
Person C: 9PM - 11PM
I would want to get 3PM - 6PM (Overlap of person A and B) and 9PM - 10PM (overlap of person B and C).
I can write an algorithm to do this in linear time with code, is it possible to do this via a relational query in linear time with PostgreSQL as well?
It needs to have a minimal response, meaning no overlapping ranges. So if there were a result which gave the range 6PM - 9PM and 8PM - 10PM it would be incorrect. It should instead return 6PM - 10pm.
Assumptions
The solution heavily depends on the exact table definition including all constraints. For lack of information in the question I'll assume this table:
CREATE TABLE booking (
booking_id serial PRIMARY KEY
, check_in timestamptz NOT NULL
, check_out timestamptz NOT NULL
, CONSTRAINT valid_range CHECK (check_out > check_in)
);
So, no NULL values, only valid ranges with inclusive lower and exclusive upper bound, and we don't really care who checks in.
Also assuming a current version of Postgres, at least 9.2.
Query
One way to do it with only SQL using a UNION ALL and window functions:
SELECT ts AS check_id, next_ts As check_out
FROM (
SELECT *, lead(ts) OVER (ORDER BY ts) AS next_ts
FROM (
SELECT *, lag(people_ct, 1 , 0) OVER (ORDER BY ts) AS prev_ct
FROM (
SELECT ts, sum(sum(change)) OVER (ORDER BY ts)::int AS people_ct
FROM (
SELECT check_in AS ts, 1 AS change FROM booking
UNION ALL
SELECT check_out, -1 FROM booking
) sub1
GROUP BY 1
) sub2
) sub3
WHERE people_ct > 1 AND prev_ct < 2 OR -- start overlap
people_ct < 2 AND prev_ct > 1 -- end overlap
) sub4
WHERE people_ct > 1 AND prev_ct < 2;
SQL Fiddle.
Explanation
In subquery sub1 derive a table of check_in and check_out in one column. check_in adds one to the crowd, check_out subtracts one.
In sub2 sum all events for the same point in time and compute a running count with a window function: that's the window function sum() over an aggregate sum() - and cast to integer or we get numeric from this:
sum(sum(change)) OVER (ORDER BY ts)::int
In sub3 look at the count of the previous row
In sub4 only keep rows where overlapping time ranges start and end, and pull the end of the time range into the same row with lead().
Finally, only keep rows, where time ranges start.
To optimize performance I would walk through the table once in a plpgsql function like demonstrated in this related answer on dba.SE:
Calculate Difference in Overlapping Time in PostgreSQL / SSRS
Idea is to divide time in periods and save them as bit values with specified granularity.
0 - nobody is checked in one grain
1 - somebody is checked in one grain
Let's assume that granularity is 1 hour and period is 1 day.
000000000000000000000000 means nobody is checked in that day
000000000000000000000110 means somebody is checked between 21 and 23
000000000000011111000000 means somebody is checked between 13 and 18
000000000000000111111100 means somebody is checked between 15 and 22
After that we do binary OR on the each value in the range and we have our answer.
000000000000011111111110
It can be done in linear time. Here is an example from Oracle but it can be transformed to PostgreSQL easily.
with rec (checkin, checkout)
as ( select 13, 18 from dual
union all
select 15, 22 from dual
union all
select 21, 23 from dual )
,spanempty ( empt)
as ( select '000000000000000000000000' from dual) ,
spanfull( full)
as ( select '111111111111111111111111' from dual)
, bookingbin( binbook) as ( select substr(empt, 1, checkin) ||
substr(full, checkin, checkout-checkin) ||
substr(empt, checkout, 24-checkout)
from rec
cross join spanempty
cross join spanfull ),
bookingInt (rn, intbook) as
( select rownum, bin2dec(binbook) from bookingbin),
bitAndSum (bitAndSumm) as (
select sum(bitand(b1.intbook, b2.intbook)) from bookingInt b1
join bookingInt b2
on b1.rn = b2.rn -1 ) ,
SumAll (sumall) as (
select sum(bin2dec(binbook)) from bookingBin )
select lpad(dec2bin(sumall - bitAndSumm), 24, '0')
from SumAll, bitAndSum
Result:
000000000000011111111110

SQL query for all the days of a month

i have the following table RENTAL(book_date, copy_id, member_id, title_id, act_ret_date, exp_ret_date). Where book_date shows the day the book was booked. I need to write a query that for every day of the month(so from 1-30 or from 1-29 or from 1-31 depending on month) it shows me the number of books booked.
i currently know how to show the number of books rented in the days that are in the table
select count(book_date), to_char(book_date,'DD')
from rental
group by to_char(book_date,'DD');
my questions are:
How do i show the rest of the days(if let's say for some reason in my database i have no books rented on 20th or 19th or multiple days) and put the number 0 there?
How do i show the number of days only of the current month so(28,29,30,31 all these 4 are possible depending on month or year)... i am lost . This must be done using only SQL query no pl/SQL or other stuff.
The following query would give you all days in the current month, in your case you can replace SYSDATE with your date column and join with this query to know how many for a given month
SELECT DT
FROM(
SELECT TRUNC (last_day(SYSDATE) - ROWNUM) dt
FROM DUAL CONNECT BY ROWNUM < 32
)
where DT >= trunc(sysdate,'mm')
The answer is to create a table like this:
table yearsmonthsdays (year varchar(4), month varchar(2), day varchar(2));
use any language you wish, e.g. iterate in java with Calendar.getInstance().getActualMaximum(Calendar.DAY_OF_MONTH) to get the last day of the month for as many years and months as you like, and fill that table with the year, month and days from 1 to last day of month of your result.
you'd get something like:
insert into yearsmonthsdays ('1995','02','01');
insert into yearsmonthsdays ('1995','02','02');
...
insert into yearsmonthsdays ('1995','02','28'); /* non-leap year */
...
insert into yearsmonthsdays ('1996','02','01');
insert into yearsmonthsdays ('1996','02','02');
...
insert into yearsmonthsdays ('1996','02','28');
insert into yearsmonthsdays ('1996','02','29'); /* leap year */
...
and so on.
Once you have this table done, your work is almost finished. Make an outer left join between your table and this table, joining year, month and day together, and when no lines appear, the count will be zero as you wish. Without using programming, this is your best bet.
In oracle, you can query from dual and use the conncect by level syntax to generate a series of rows - in your case, dates. From there on, it's just a matter of deciding what dates you want to display (in my example I used all the dates from 2014) and joining on your table:
SELECT all_date, COALESCE (cnt, 0)
FROM (SELECT to_date('01/01/2014', 'dd/mm/yyyy') + rownum - 1 AS all_date
FROM dual
CONNECT BY LEVEL <= 365) d
LEFT JOIN (SELECT TRUNC(book_date), COUNT(book_date) AS cnt
FROM rental
GROUP BY book_date) r ON d.all_date = TRUNC(r.book_date)
There's no need to get ROWNUM involved ... you can just use LEVEL in the CONNECT BY:
WITH d1 AS (
SELECT TRUNC(SYSDATE, 'MONTH') - 1 + LEVEL AS book_date
FROM dual
CONNECT BY TRUNC(SYSDATE, 'MONTH') - 1 + LEVEL <= LAST_DAY(SYSDATE)
)
SELECT TRUNC(d1.book_date), COUNT(r.book_date)
FROM d1 LEFT JOIN rental r
ON TRUNC(d1.book_date) = TRUNC(r.book_date)
GROUP BY TRUNC(d1.book_date);
Simply replace SYSDATE with a date in the month you're targeting for results.
All days of the month based on current date
select trunc(sysdate) - (to_number(to_char(sysdate,'DD')) - 1)+level-1 x from dual connect by level <= TO_CHAR(LAST_DAY(sysdate),'DD')
It did works to me:
SELECT DT
FROM (SELECT TRUNC(LAST_DAY(SYSDATE) - (CASE WHEN ROWNUM=1 THEN 0 ELSE ROWNUM-1 END)) DT
FROM DUAL
CONNECT BY ROWNUM <= 32)
WHERE DT >= TRUNC(SYSDATE, 'MM')
In Oracle SQL the query must look like this to not miss the last day of month:
SELECT DT
FROM(
SELECT trunc(add_months(sysdate, 1),'MM')- ROWNUM dt
FROM DUAL CONNECT BY ROWNUM < 32
)
where DT >= trunc(sysdate,'mm')