I have below table in oracle, I want Partition Range by Date in Oracle monthly on MyTimestamp column(Data type is number). Can I partition with this column or do I need another column? If I need a new column, what is the data type of the new column and how do I partition with the new column(convert MyTimestamp to new data type and partition )?
----------------------------------------------------------------------------------------------------
| id | MyTimestamp | Name | etc ...
----------------------------------------------------------------------------------------------------
| 0 | 1657640396 | John | ...
| 1 | 1657638832 | Tom | ...
| 2 | 1657640265 | Tom | ...
| 3 | 1657640292 | John | ...
| 4 | 1657640005 | Jack | ...
--------------------------------------------------------------------------------------------------
You can define a virtual column and define partition key on that:
CREATE TABLE ... (
id NUMBER,
MyTimestamp NUMBER
Name VARCHAR2(100),
etc...
PARTITION_KEY TIMESTAMP(0) GENERATED ALWAYS AS (
CAST(TRUNC(TIMESTAMP '1970-01-01 00:00:00 UTC' + MyTimestamp * INTERVAL '1' SECOND) AS TIMESTAMP(0))
) VIRTUAL
)
PARTITION BY RANGE (PARTITION_KEY)
INTERVAL (INTERVAL '1' MONTH)
(
PARTITION P_INITIAL VALUES LESS THAN (TIMESTAMP '2020-01-01 00:00:00')
);
You could also use MyTimestamp directly, however 2'635'200 (i.e. 30.5 days) seconds is just roughly a month
CREATE TABLE ... (
id NUMBER,
MyTimestamp NUMBER
Name VARCHAR2(100),
etc...
)
PARTITION BY RANGE (MyTimestamp)
INTERVAL (2635200)
(
PARTITION P_INITIAL VALUES LESS THAN (1640991600)
);
If you want to PARTITION by DATE, you need a date column. Below is an example with some dummy data.
When new PARTITIONs are automatically added they will have system GENERATED names. I have code to RENAME them to something meaningful if you like.
In addition, you will probably want to implement a RETENTION period for the PARTITION, how long to keep them around. I also implemented that too.
CREATE TABLE t2 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE
)
PARTITION BY RANGE (dt)
INTERVAL(NUMTOYMINTERVAL(1, 'MONTH'))
(
PARTITION OLD_DATA values LESS THAN (TO_DATE('2022-01-01','YYYY-MM-DD'))
);
/
INSERT into t2 (dt)
with dt (dt, interv) as (
select date '2022-01-01', numtodsinterval(1,'DAY') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-07-31')
select dt from dt;
/
By timestamp
CREATE TABLE t3 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt TIMESTAMP)
PARTITION BY RANGE (dt)
INTERVAL ( NUMTODSINTERVAL (1, 'MONTH') ) (
PARTITION OLD_DATA VALUES LESS THAN (TIMESTAMP '2022-01-01 00:00:00.000000')
);
/
INSERT into t3 (dt)
SELECT TIMESTAMP '2022-01-01 00:00:00'
+ (LEVEL - 1) * INTERVAL '5' MINUTE
+ MOD(LEVEL - 1, 10) * INTERVAL '0.1' SECOND
FROM DUAL
CONNECT BY
TIMESTAMP '2022-01-01 00:00:00'
+ (LEVEL - 1) * INTERVAL '5' MINUTE
+ MOD(LEVEL - 1, 10) * INTERVAL '0.1' SECOND < DATE '2022-01-15';
/
Related
So I have this working SQL script that take a date and returns the age from current time to the given date excluding dates defined in a table called exclude dates
SELECT
COUNT(*)
FROM
(
SELECT
ROWNUM rnum
FROM
all_objects
WHERE
ROWNUM <= CAST(current_timestamp AS DATE) - to_date('&2') + 1
)
WHERE
to_char(to_date('&2') + rnum - 1, 'DY') NOT IN ( 'SAT', 'SUN' )
AND NOT EXISTS (
SELECT
NULL
FROM
exclude_dates
WHERE
no_work = trunc(to_date('&2') + rnum - 1)
);
I have a table called
TICKETS
that contains columns named
ID, UPDATED_AT
I want to create a view that uses the above script to return
ID, AGE
where age is the output of the script above.
You code has a few weaknesses.
There is no need for CAST(current_timestamp AS DATE).
If you need the current DATE then simply use TRUNC(SYSDATE)
You don't need to select from all_objects. Better use hierarchical query
SELECT LEVEL as rnum FROM dual CONNECT BY LEVEL <= ...
Using to_date('&2') without a format is usually bad. Either your input value is a string, then you should include the format, e.g. to_date('&2', 'YYYY-MM-DD') or your input value is a DATE, then simply use &2 - never use TO_DATE() on a value which is already a DATE!
Final query could be this one - assuming input parameter is a DATE value:
WITH t AS (
SELECT LEVEL as d
FROM dual
CONNECT BY LEVEL <= TRUNC(SYSDATE) - the_day)
SELECT COUNT(*) AS buisiness_days
FROM t
WHERE TO_CHAR(the_day + d - 1, 'DY', 'NLS_DATE_LANGUAGE = american') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 'x'
FROM exclude_dates
WHERE no_work = TRUNC(the_day + d - 1)
)
However, for me it is not clear how you want to provide this as a view! You would need to create a separate view for each input date, or at least create a new view every day.
I would suggest to create a function:
CREATE OR REPLACE FUNCTION buisiness_days(the_date IN DATE) RETURN INTEGER AS
ret INTEGER;
BEGIN
WITH t AS (
SELECT LEVEL as d
FROM dual
CONNECT BY LEVEL <= TRUNC(SYSDATE) - the_date)
SELECT COUNT(*) AS buisiness_days
INTO ret
FROM t
WHERE TO_CHAR(the_date + d - 1, 'DY', 'NLS_DATE_LANGUAGE = american') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 'x'
FROM exclude_dates
WHERE no_work = TRUNC(the_date + d - 1)
);
RETURN ret;
END;
The function will return a list of dates between the date range you provide so the dates don't have to be stored in a table.
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
/
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
To exclude holidays you need to know what dates they fall on so there needs to be a holiday table.
create table holidays(
holiday_date DATE not null,
holiday_name VARCHAR2(20),
constraint holidays_pk primary key (holiday_date),
constraint is_midnight check ( holiday_date = trunc ( holiday_date ) )
);
INSERT into holidays (HOLIDAY_DATE,HOLIDAY_NAME)
WITH dts as (
select to_date('25-NOV-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'Thanksgiving 2021' from dual union all
select to_date('29-NOV-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'Hanukkah 2021' from dual
)
SELECT * from dts;
This query will provide the count of days between the range, number of working days and number of holidays in the range.
SELECT COUNT (*) AS total_days
, COUNT ( CASE
WHEN h.holiday_date IS NULL
AND d.column_value - TRUNC (d.column_value, 'IW') < 5
THEN 'Business Day'
END
) AS business_days
, COUNT (h.holiday_date) AS holidays
FROM generate_dates_pipelined (DATE '2021-11-01', DATE '2021-11-30') d
LEFT JOIN holidays h ON h.holiday_date = d.column_value;
This query will provide a list of dates excluding sat, sun and holidays that fall between the range.
SELECT
COLUMN_VALUE
FROM
TABLE(generate_dates_pipelined(DATE '2021-11-01',
DATE '2021-11-30')) c
where
to_char(COLUMN_VALUE, 'DY') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.COLUMN_VALUE = h.holiday_date
);
You don't need a function or to use a row generator function and can calculate the number of business days:
CREATE VIEW business_day_ages (ID, AGE) AS
SELECT id,
( TRUNC( SYSDATE, 'IW' ) - TRUNC( updated_at, 'IW' ) ) * 5 / 7
-- Number of full weeks.
+ LEAST( SYSDATE - TRUNC( SYSDATE, 'IW' ), 5 )
-- Add part weeks at the end.
- LEAST( updated_at - TRUNC( updated_at, 'IW' ), 5 )
-- Subtract part weeks at the start.
- COALESCE(
( SELECT SUM(
LEAST(no_work + INTERVAL '1' DAY, SYSDATE)
- GREATEST(no_work, updated_at)
)
FROM exclude_dates
WHERE no_work BETWEEN TRUNC(updated_at) AND SYSDATE
),
0
)
-- Subtract the holiday days.
FROM tickets;
Or, if you are not calculating using part days then you can simplify it to:
CREATE OR REPLACE VIEW business_day_ages (ID, AGE) AS
SELECT id,
( TRUNC( SYSDATE, 'IW' ) - TRUNC( updated_at, 'IW' ) ) * 5 / 7
-- Number of full weeks.
+ LEAST( TRUNC(SYSDATE) - TRUNC( SYSDATE, 'IW' ), 5 )
-- Add part weeks at the end.
- LEAST( updated_at - TRUNC( updated_at, 'IW' ), 5 )
-- Subtract part weeks at the start.
- COALESCE(
( SELECT 1
FROM exclude_dates
WHERE no_work BETWEEN TRUNC(updated_at) AND TRUNC(SYSDATE)
),
0
)
-- Subtract the holiday days.
FROM tickets;
db<>fiddle here
What is the difference, if any, between DATE and TIMESTAMP(0) in Oracle? Both data types occupy 6 bytes, and contain date with time, but without fractional seconds and a timezone.
The TIMESTAMP was added to Oracle about 20 years after DATE to comply with ANSI standard. TIMESTAMP or TIMESTAMP(6) can hold fractions of seconds, so it is different from DATE, but is there any difference between TIMESTAMP(0) and DATE?
One is a DATE data type and one is a TIMESTAMP data type.
They both take up 7 bytes (century, year-of-century, month, day, hour, minute and second).
The DATE will be implicitly formatted by the NLS_DATE_FORMAT session parameter when it is displayed.
The TIMESTAMP will be implicitly formatted by the NLS_TIMESTAMP_FORMAT session parameter when it is displayed.
For example:
CREATE TABLE table_name (
dt DATE,
ts TIMESTAMP(0)
);
INSERT INTO table_name ( dt, ts ) VALUES ( SYSDATE, SYSTIMESTAMP );
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD"T"HH24:MI:SS';
ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD"T"HH24:MI:SS.FF6';
Then:
SELECT dt,
DUMP( dt ),
ts,
DUMP( ts )
FROM table_name;
Outputs:
DT | DUMP(DT) | TS | DUMP(TS)
:------------------ | :---------------------------------- | :------------------------- | :-----------------------------------
2020-10-08T14:21:35 | Typ=12 Len=7: 120,120,10,8,15,22,36 | 2020-10-08T14:21:36.000000 | Typ=180 Len=7: 120,120,10,8,15,22,37
Functions that require a TIMESTAMP input won't perform an implicit CAST from a DATE to a TIMESTAMP.
So:
SELECT FROM_TZ( dt, 'UTC' ) FROM table_name;
SELECT FROM_TZ( CAST( dt AS TIMESTAMP(0) ), 'UTC' ) FROM table_name;
SELECT FROM_TZ( ts, 'UTC' ) FROM table_name;
The first statement raises an ORA-00932: inconsistent datatypes: expected TIMESTAMP got DATE exception but the second and third works.
As mentioned by #WernfriedDomscheit in comments, arithmetic is also different between DATE and TIMESTAMP data types. For example:
SELECT dt - dt,
( dt - dt ) DAY TO SECOND,
ts - ts
FROM table_name
Outputs:
DT-DT | (DT-DT)DAYTOSECOND | TS-TS
----: | :------------------ | :------------------
0 | +00 00:00:00.000000 | +000000000 00:00:00
The top expression is DATE - DATE and the result is the difference in days (expressed as a NUMBER data type). However, the bottom expression is TIMESTAMP - TIMESTAMP and the result is an INTERVAL DAY TO SECOND data type. It is possible to get DATE subtraction to output an INTERVAL DAY TO SECOND data type but, as shown in the middle example, you need to explicitly state that that is the output you require.
Addition to a DATE or TIMESTAMP also has differences. For example:
SELECT dt + 1,
dt + INTERVAL '1' DAY,
ts + 1,
ts + INTERVAL '1' DAY
FROM table_name
Outputs:
DT+1 | DT+INTERVAL'1'DAY | TS+1 | TS+INTERVAL'1'DAY
:------------------ | :------------------ | :------------------ | :-------------------------
2020-10-09T19:52:21 | 2020-10-09T19:52:21 | 2020-10-09T19:52:22 | 2020-10-09T19:52:22.000000
Adding a number or an INTERVAL to both a DATE and a TIMESTAMP is both syntactically correct (but may not be as expected). If you look you will see that the output for adding a number or an interval to a date gives the same output in both cases. However, there is a difference between the output when adding a number to a timestamp compared to adding an interval to a timestamp; this is because you can only add numbers to date values and by adding a number to a timestamp then Oracle has performed an implicit cast from timestamp down to a date so:
SELECT ts + 1 FROM table_name
is effectively performing:
SELECT CAST(ts AS DATE) + 1 FROM table_name
So, if you want to perform arithmetic with a TIMESTAMP data type then use INTERVAL data types rather than numbers.
db<>fiddle here
I have a scenario in which for example,my start_date ='12-SEP-2018 00:01:00' and End_date ='13-SEP-2018 14:55:00' . The difference between the 2 dates must be found out in Hours and minutes like'12:20'. This must be achieved in oracle database. I tried using the following logic :
SELECT
24 * (to_date('2009-07-07 22:00', 'YYYY-MM-DD hh24:mi') - to_date(
'2009-07-07 19:30', 'YYYY-MM-DD hh24:mi')) diff_hours
FROM
dual;
I was able to get the hour difference but unable to get minutes along with it.
CREATE TABLE table_name ( start_date DATE, end_date DATE );
INSERT INTO table_name VALUES ( TIMESTAMP '2009-07-07 19:30:00', TIMESTAMP '2009-07-07 22:00:00' );
Then you can subtract one from the other and cast it to a DAY TO SECOND interval and then just EXTRACT the component parts of the time:
SELECT EXTRACT( DAY FROM difference ) AS days,
EXTRACT( HOUR FROM difference ) AS hours,
EXTRACT( MINUTE FROM difference ) AS minutes,
EXTRACT( SECOND FROM difference ) AS seconds
FROM (
SELECT ( end_date - start_date ) DAY TO SECOND AS difference
FROM table_name
);
Outputs:
DAYS | HOURS | MINUTES | SECONDS
---: | ----: | ------: | ------:
0 | 2 | 30 | 0
or you can use arithmetic to calculate the values:
SELECT TRUNC( 24 * ( end_date - start_date ) ) AS hours,
TRUNC( MOD( 24 * 60 * ( end_date - start_date ), 60 ) ) AS minutes,
ROUND( MOD( 24 * 60 * 60 * ( end_date - start_date ), 60 ) ) AS seconds
FROM table_name;
which outputs:
HOURS | MINUTES | SECONDS
----: | ------: | ------:
2 | 30 | 0
db<>fiddle here
Since you want a string value, an alternative based on your query attempt is to add the difference between your two date values (which is a numeric value, the number of days between them, including fractional days) to an arbitrary fixed date; and then convert the result of that to a string:
SELECT to_char(date '0001-01-01'
+ (to_date('2009-07-07 22:00', 'YYYY-MM-DD hh24:mi') - to_date( '2009-07-07 19:30', 'YYYY-MM-DD hh24:mi')),
'HH24:MI') as diff
FROM dual;
DIFF
-----
02:30
If the difference can exceed 24 hours then you need to decide how to report that; if you want to include days as a separate figure then you can still use this approach, but need to subtract one (if your fixed date is the first) from the difference before formatting as a string:
SELECT to_char(date '0001-01-01'
+ (to_date('2009-07-08 22:00', 'YYYY-MM-DD hh24:mi') - to_date( '2009-07-07 19:30', 'YYYY-MM-DD hh24:mi'))
- 1,
'DDD:HH24:MI') as diff
FROM dual;
DIFF
---------
001:02:30
If you want the 'hours' value to be higher instead - e.g. '26:30' in this example - then it gets rather more complicated; I see #MTO has added the 'arithmetic' approach already so I won't repeat that. But then might be better off going down the extract() route (which you should consider anyway as it's more flexible and elegant...)
I can't seem to figure this out. I have some rows with time in the format 00:00:00 (hh:mm:ss) and i need to calculate the total time it takes for a task.
I am unable to sum this data. Can someone advise on a way to convert this to a format i can sum or a method to calculate the total time for the task.
Thanks for any assistance. This is in an Oracle DB.
Convert your time string to a date and subtract the equivalent date at midnight to give you an number as a fraction of a day. You can then sum this number and convert it to an interval:
Oracle Setup:
CREATE TABLE test_data( value ) AS
SELECT '01:23:45' FROM DUAL UNION ALL
SELECT '12:34:56' FROM DUAL UNION ALL
SELECT '23:45:00' FROM DUAL;
Query:
SELECT NUMTODSINTERVAL(
SUM( TO_DATE( value, 'HH24:MI:SS' ) - TO_DATE( '00:00:00', 'HH24:MI:SS' ) ),
'DAY'
) AS total_time_taken
FROM test_data;
Output:
| TOTAL_TIME_TAKEN |
| :---------------------------- |
| +000000001 13:43:41.000000000 |
db<>fiddle here
Update including durations longer than 23:59:59.
Oracle Setup:
CREATE TABLE test_data( value ) AS
SELECT '1:23:45' FROM DUAL UNION ALL
SELECT '12:34:56' FROM DUAL UNION ALL
SELECT '23:45:00' FROM DUAL UNION ALL
SELECT '48:00:00' FROM DUAL;
Query:
SELECT NUMTODSINTERVAL(
SUM(
DATE '1970-01-01'
+ NUMTODSINTERVAL( SUBSTR( value, 1, HM - 1 ), 'HOUR' )
+ NUMTODSINTERVAL( SUBSTR( value, HM + 1, MS - HM - 1 ), 'MINUTE' )
+ NUMTODSINTERVAL( SUBSTR( value, MS + 1 ), 'SECOND' )
- DATE '1970-01-01'
),
'DAY'
) AS total_time
FROM (
SELECT value,
INSTR( value, ':', 1, 1 ) AS HM,
INSTR( value, ':', 1, 2 ) AS MS
FROM test_data
);
Output:
| TOTAL_TIME |
| :---------------------------- |
| +000000003 13:43:41.000000000 |
db<>fiddle here
Even better would be if you changed your table to hold the durations as intervals rather than as strings then everything becomes much simpler:
Oracle Setup:
CREATE TABLE test_data( value ) AS
SELECT INTERVAL '1:23:45' HOUR TO SECOND FROM DUAL UNION ALL
SELECT INTERVAL '12:34:56' HOUR TO SECOND FROM DUAL UNION ALL
SELECT INTERVAL '23:45:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT INTERVAL '48:00:00' HOUR TO SECOND FROM DUAL;
Query:
SELECT NUMTODSINTERVAL(
SUM( DATE '1970-01-01' + value - DATE '1970-01-01' ),
'DAY'
) AS total_time
FROM test_data;
Output:
| TOTAL_TIME |
| :---------------------------- |
| +000000003 13:43:41.000000000 |
db<>fiddle here
I am using the following query to find out the number of working days in a month excluding the weekends
select payroll_id as payrollId,
(select count(*)
from ( select rownum rnum
from all_objects
where rownum <= to_date(to_char(last_day(to_date('01-'||'MAY'||'2017','DD-MM-YYYY')),'DD')||'MAY'||'2017','DD-MM-YYYY') - to_date('01-'||'MAY'||'2017','DD-MM-YYYY')+1 )
where to_char( to_date('01-'||'MAY'||'2017','DD-MM-YYYY')+rnum-1, 'DY' )
not in ( 'SAT', 'SUN' )) - (select count(*) from admin_holiday where to_char(holiday_date,'DD-MON-YYYY') like '%-MAY-2017%' and holiday_type_id=1) days
from employee where DEL_FLAG=1 order by payrollId
Here, payrollId is the employee_id in employee table and admin_holiday is a table that contains information about the national holidays in a month
My requirement is that if the month is current month then the working days should be upto today's date. For example the current month is MAY/2017 and till today(i.e 22/05/2017) the working days count excluding weekends is 15 (10/05/2017 is a national holiday according to Hindu calendar.
How do I obtain the required result.
NOTE this action has to be performed in a single select statement and no other pl/sql blocks, etc.
Here is how I would approach the equivalent query. By placing the date range calculation into a small cross joined query you can access the result easily throughout the remainder of the query.
SELECT
e.payroll_id AS payrollId
, cj. working_days
FROM employee e
cross join (
select
GREATEST(NEXT_DAY(start_date, 'MON') - start_date - 2, 0)
+ ((NEXT_DAY(end_date, 'MON') - NEXT_DAY(start_date, 'MON'))/7)*5
- GREATEST(NEXT_DAY(end_date, 'MON') - end_date - 3, 0)
- (
SELECT COUNT(holiday)
FROM admin_holiday
WHERE holiday_date BETWEEN start_date AND end_date
)
as working_days
FROM (
select
to_date('20170501','yyyymmdd') as start_date
, to_date('20170522','yyyymmdd') as end_date
from dual
)
) cj
WHERE e.DEL_FLAG = 1
ORDER BY e.payrollId
The working days logic is:
Calculate leading days in first week + (Count the number on mon/fri periods) * 5 - number of trailing days in final week - the additional holidays in a table:
select
GREATEST(NEXT_DAY(start_date, 'MON') - start_date - 2, 0)
+ ((NEXT_DAY(end_date, 'MON') - NEXT_DAY(start_date, 'MON'))/7)*5
- GREATEST(NEXT_DAY(end_date, 'MON') - end_date - 3, 0)
- (
SELECT COUNT(holiday)
FROM admin_holiday
WHERE holiday_date BETWEEN start_date AND end_date
)
as working_days
, start_date
, end_date
, holidays
, end_date - start_date
FROM (
select
trunc(sysdate,'MM') as start_date
, trunc(sysdate) as end_date
from dual
)
example (not there is zero holidays here):
+--------------+--------------+-------------+------------+-------------------------+
| WORKING_DAYS | START_DATE | END_DATE | HOLIDAYS | END_DATE-START_DATE |
+--------------+--------------+-------------+------------+-------------------------+
| 16 | 01.05.2017 | 22.05.2017 | 0 | 21 |
+--------------+--------------+-------------+------------+-------------------------+
To manually set the dates you can do this:
select
GREATEST(NEXT_DAY(start_date, 'MON') - start_date - 2, 0)
+ ((NEXT_DAY(end_date, 'MON') - NEXT_DAY(start_date, 'MON'))/7)*5
- GREATEST(NEXT_DAY(end_date, 'MON') - end_date - 3, 0)
- (
SELECT COUNT(holiday)
FROM admin_holiday
WHERE holiday_date BETWEEN start_date AND end_date
)
as working_days
FROM (
select
to_date('20170501','yyyymmdd') as start_date
, to_date('20170522','yyyymmdd') as end_date
from dual
)