I have the following datediff function that is working fine for the most part. But when I pass it some employees data I seem to get an error. How can I modify the function to work in both cases?
Thanks in advance for your expertise and to all who answer. Below is my test CASE
CREATE FUNCTION datediff (p_from timestamp, p_to timestamp)
return varchar2 is
v_from TIMESTAMP := LEAST(p_from, p_to);
v_to TIMESTAMP := GREATEST(p_from, p_to);
l_years PLS_INTEGER;
l_from TIMESTAMP;
l_interval interval day(3) to second(6);
begin
l_years := TRUNC(MONTHS_BETWEEN(v_to, v_from)/12);
l_from := CAST(TRUNC(ADD_MONTHS(v_from, l_years * 12), 'MI') AS TIMESTAMP)
+ NUMTODSINTERVAL( EXTRACT(SECOND FROM v_from), 'SECOND' );
l_interval := (v_to - l_from) DAY(3) TO SECOND(6);
return l_years || ' Years '
|| extract (day from l_interval) || ' Days '
|| extract (hour from l_interval) || ' Hours '
|| extract (minute from l_interval) || ' Minutes '
|| extract (second from l_interval) || ' Seconds';
end datediff;
/
/* works fine */
SELECT
datediff( TO_DATE('2022-04-03 10:11:13','YYYY-MM-DD HH24:MI:SS'),
TO_DATE('1981-04-01 17:48:09','YYYY-MM-DD HH24:MI:SS')) as diff FROM DUAL
DIFF
41 Years 1 Days 16 Hours 23 Minutes 4 Seconds
CREATE TABLE departments( department_id, department_name) AS
SELECT 1, 'IT' FROM DUAL UNION ALL
SELECT 2, 'DBA' FROM DUAL UNION ALL
SELECT 3, 'Sales' FROM DUAL UNION ALL
SELECT 4, 'Programming' FROM DUAL;
CREATE TABLE employees (employee_id, first_name, last_name, hire_date, salary, department_id) AS
SELECT 1, 'Lisa', 'Saladino', DATE '2001-04-03', 160000, 1 FROM DUAL UNION ALL
SELECT 2, 'Sandy', 'Herring', DATE '2001-04-04', 150200, 1 FROM DUAL UNION ALL
SELECT 3, 'Beth', 'Cooper', DATE '2001-04-05', 60700, 1 FROM DUAL UNION ALL
SELECT 4, 'Carol', 'Orr', DATE '2001-04-06', 70125,1 FROM DUAL UNION ALL
SELECT 5, 'Vicky', 'Palazzo', DATE '2001-04-07', 68525,2 FROM DUAL UNION ALL
SELECT 6, 'Cheryl', 'Ford', DATE '2001-04-08', 110000,1 FROM DUAL UNION ALL
SELECT 7, 'Leslee', 'Altman', DATE '2001-04-10', 66666, 1 FROM DUAL UNION ALL
SELECT 8, 'Jill', 'Coralnick', DATE '2001-04-11', 190000, 2 FROM DUAL UNION ALL
SELECT 9, 'Faith', 'Aaron', DATE '2001-04-17', 122000,2 FROM DUAL UNION ALL
SELECT 10, 'Silvio', 'Dante', DATE '2022-10-16', 102150,4 FROM DUAL UNION ALL
SELECT 11, 'Jerry', 'Torchiano', DATE '2022-10-30', 112660,4 FROM DUAL;
/* problem with function here */
SELECT
E.first_name ,
E.last_name ,
E.department_id ,
D.department_name,
datediff (TRUNC(SYSDATE, E.hire_date))
FROM employees E
JOIN departments D
ON E.department_id = D.department_id;
ORA-00932: inconsistent datatypes: expected DATE UNIT got DATE
Related
My apologies for the verbose post but the setup is necessary to show my problem and ask a question.
In the anonymous block below I'm trying to construct a string, which encapsulates the table in a single quote ie 'T1' but I've been struggling for the past hour and can use some help.
Secondly, I purposely left out a row in the table partition_rention for table name T2. I suspect a NULL will be returned into the variable when the statement is executed. Will this work?
if v_days is NULL
then
v_days := 30
END IF;
Thanks in advance to all who answer and your expertise
create table partition_rention
(
TABLE_NAME VARCHAR2(30) NOT NULL,
DAYS NUMBER(6),
CONSTRAINT Check_gt0
CHECK (DAYS> 0)
);
/
INSERT into partition_rention (TABLE_NAME, DAYS)
VALUES
('T1', 15);
/
INSERT into partition_rention (TABLE_NAME, DAYS)
VALUES
('T3', 15);
/
CREATE TABLE t1 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE
)
PARTITION BY RANGE (dt)
INTERVAL (NUMTODSINTERVAL(7,'DAY'))
(
PARTITION OLD_DATA values LESS THAN (TO_DATE('2022-01-01','YYYY-MM-DD'))
);
/
INSERT /*+ APPEND */ into t1 (dt)
with dt (dt, interv) as (
select date '2022-01-01', numtodsinterval(30,'MINUTE') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-01-15')
select dt from dt;
/
create index ix_local on t1 (dt) local;
/
CREATE TABLE t2
(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE
)
PARTITION BY RANGE (dt)
INTERVAL (NUMTODSINTERVAL(1,'DAY'))
(
PARTITION OLD_DATA values LESS THAN (TO_DATE('2022-01-01','YYYY-MM-DD'))
);
/
INSERT /*+ APPEND */ into t2 (dt)
with dt (dt, interv) as (
select date '2022-01-01', numtodsinterval(30,'MINUTE') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-01-15')
select dt from dt;
/
create index ix_global on t2 (dt);
/
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, 'DAY') ) (
PARTITION OLD_DATA VALUES LESS THAN (TIMESTAMP '2022-01-01 00:00:00.000000')
);
/
INSERT /*+ APPEND */ 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';
/
DECLARE
v_str VARCHAR2 (500);
v_days NUMBER := 0;
BEGIN
FOR cur_r IN(
SELECT TABLE_NAME, PARTITIONING_TYPE, COLUMN_NAME, DATA_TYPE
FROM USER_PART_TABLES
JOIN USER_PART_KEY_COLUMNS ON NAME = TABLE_NAME
JOIN USER_TAB_COLS USING (TABLE_NAME, COLUMN_NAME)
where OBJECT_TYPE = 'TABLE' AND
PARTITIONING_TYPE='RANGE' AND
regexp_like(DATA_TYPE,'^DATE$|^TIMESTAMP*')
)
LOOP
--DBMS_OUTPUT.put_line('Table '|| cur_r.table_name);
v_str := 'select DAYS FROM partition_rention into v_days where TABLE_NAME = '||cur_r.table_name||'';
DBMS_OUTPUT.put_line(v_str);
-- execute immediate v_str;
END LOOP;
END;
Statement processed.
select DAYS FROM partition_rention into v_days where TABLE_NAME = T1
select DAYS FROM partition_rention into v_days where TABLE_NAME = T2
select DAYS FROM partition_rention into v_days where TABLE_NAME = T3
There is no reason for dynamic SQL. It would be this:
begin
select DAYS
into v_days
FROM partition_rention
where TABLE_NAME = cur_r.table_name;
exception
when NO_DATA_FOUND THEN
v_days := 30;
end;
If you really insist for dynamic SQL then it would be this one
begin
v_str := 'select DAYS FROM partition_rention where TABLE_NAME = :t';
execute immediate v_str into v_days using cur_r.table_name;
exception
when NO_DATA_FOUND THEN
v_days := 30;
end;
NB, I guess the next step might be to drop outdated partitions. For this have a look at How to drop multiple interval partitions based on date?
If the LOOP statement comes between a BEGIN statement and its matching EXCEPTION or END; then the END LOOP statement has to come between them as well. If i want an EXCEPTION handler that catches errors that may occur within the loop, and then continues the loop then the exception handler doesn't appear to work.
The code was restructured to remove the expectation handler.
I already have a query that finds the tables and columns I'm interested in. Now, for each table in that result set, I want to get the matching days value from the partition_retention table if there is one, and if there is no matching row in partition_retention, I want the table_name anyway with a default value (30) for days. To do this I implemented an outer JOIN like this:
BEGIN
FOR td IN
(
SELECT table_name
, NVL (pr.days, 30) AS days
FROM user_part_tables pt
JOIN user_part_key_columns pkc ON pkc.name = pt.table_name
JOIN user_tab_cols tc USING (table_name, column_name)
LEFT JOIN partition_retention pr USING (table_name)
WHERE pkc.object_type = 'TABLE'
AND pt.partitioning_type = 'RANGE'
AND REGEXP_LIKE (tc.data_type, '^DATE$|^TIMESTAMP*')
ORDER BY table_name -- not needed, but could be handy in debugging
)
LOOP
-- For debugging:
dbms_output.put_line ( td.table_name
|| ' = table_name, '
|| TO_CHAR (td.days)
|| ' = days'
);
-- call procedure to remove old PARTITIONs here.
END LOOP;
END;
/
Output from my sample data:
T1 = table_name, 15 = days
T2 = table_name, 30 = days
T3 = table_name, 5 = days
I have a problematic SQL Sentence, when it runs
SELECT code, my_date, my_time
FROM my_table
WHERE to_date(to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time, 'YYYY-MM-DD HH24:MI:SS') > sysdate - 5
I always get: ORA-01841: (full) year must be between -4713 and +9999, and not be 0
This is the definition of my table:
CREATE TABLE my_table (code NUMBER(10), my_date DATE, my_time VARCHAR2(8));
ALTER TABLE my_table ADD CONSTRAINT pk_my_table PRIMARY KEY (code);
CREATE INDEX i_my_table_001 ON my_table (my_date);
But If I add an extra restriction, I never get the error:
SELECT code, my_date, my_time
FROM my_table
WHERE my_date > trunc(sysdate - 5)
AND to_date(to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time, 'YYYY-MM-DD HH24:MI:SS') > sysdate - 5
All validations are ok. Not invalid dates, not invalid times
Is there any way to find which is the 'offending' record?
Brute force is not an option because table has more than 25M records.
If you are on Oracle 12.2 or higher, you can use the VALIDATE_CONVERSION function to see which rows could be giving you an error.
Example Query
WITH
my_table (code, my_date, my_time)
AS
(SELECT 1, date '2020-01-01', '08:00:00' FROM DUAL
UNION ALL
SELECT 2, date '2020-01-31', '25:00:00' FROM DUAL)
SELECT *
FROM my_table
WHERE validate_conversion (to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time AS DATE, 'YYYY-MM-DD HH24:MI:SS') = 0;
Result
CODE MY_DATE MY_TIME
_______ ______________ ___________
2 2020-01-31 25:00:00
Update
One update that may be a bit easier than cloning your database and upgrading is to create your own function to validate the date and call that from a test SQL statement like the on below. I'm not sure if defining a function within a common table expression is supported in 12.1, but if it is not, you can make it a standalone function and call that from your query.
WITH
FUNCTION validate_date (p_date DATE, p_time VARCHAR2)
RETURN NUMBER
AS
l_test_date DATE;
BEGIN
l_test_date :=
TO_DATE (TO_CHAR (p_date, 'YYYY-MM-DD') || ' ' || p_time, 'YYYY-MM-DD HH24:MI:SS');
RETURN 1;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END;
SELECT *
FROM (SELECT 1 AS code, DATE '2020-01-01' AS my_date, '08:00:00' AS my_time FROM DUAL
UNION ALL
SELECT 2, DATE '2020-01-31', '25:00:00' FROM DUAL) my_table
WHERE validate_date (my_date, my_time) = 0;
I need to replace characters. B for V and vice versa, Z for S and vice versa
in Oracle
DECLARE
lc_word_so VARCHAR2 (500);
lc_word_lst VARCHAR2 (500);
lc_word VARCHAR2 (500) := 'VBZ';
ln_length NUMBER := LENGTH (lc_word);
lc_search VARCHAR2 (2);
lc_replace VARCHAR2 (2);
TYPE typ_search IS VARRAY (6) OF VARCHAR2 (1);
arr_search typ_search := typ_search ('B','V','S','Z');
BEGIN
IF ln_length > 0 THEN
lc_word_so := NULL;
FOR i IN 1 .. arr_search.COUNT LOOP
IF MOD (i, 2) = 0 THEN
lc_search := arr_search (i);
lc_replace := arr_search (i - 1);
ELSE
lc_search := arr_search (i);
lc_replace := arr_search (i + 1);
END IF;
FOR j IN 0 .. ln_length LOOP
lc_word_lst := lc_word_so;
lc_word_so := REGEXP_REPLACE (lc_word, lc_search, lc_replace, 1, j, 'i');
IF lc_word_so = lc_word THEN
EXIT;
ELSE
IF (lc_word_lst IS NULL OR lc_word_lst != lc_word_so) THEN
DBMS_OUTPUT.put_line (lc_word_so);
END IF;
END IF;
END LOOP;
END LOOP;
END IF;
END;
I expect the output:
BBS
BBZ
BVS
BVZ
VBS
VBZ
VVS
VVZ
But the actual output is:
VVZ
BBZ
VBS
You could do this with built-in functionality. The translate() function substitutes the character in the first pattern -'BVSZ' - with the character at the same offset in the second pattern - 'VBZS'. Characters not included in the first pattern are ignored:
with cte as (
select 'BNV' as str from dual union all
select 'ZXS' as str from dual union all
select 'BBS' as str from dual
)
select str
,translate(str, 'BVSZ', 'VBZS') as trns
from cte
This is the solution using translate, but it looks unprofessional:
SELECT TRANSLATE ('VBZ', 'B', 'V') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'V', 'B') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'S', 'Z') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'Z', 'S') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'BV', 'VB') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'VB', 'BV') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'SZ', 'ZS') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'ZS', 'SZ') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'BS', 'VZ') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'VZ', 'BS') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'SB', 'ZV') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'ZV', 'SB') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'BV', 'VB') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'VBZ', 'BVS') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'VZS', 'BSZ') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'BZS', 'VSZ') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'BVZS', 'VBSZ') FROM DUAL
UNION
SELECT TRANSLATE ('VBZ', 'VBZS', 'BVSZ') FROM DUAL
I have a function that have this code
for i in cursor
loop
variavel1=i.id
while i.id=variavel1
loop
---- do someting
--- incriment variavel1 with next i.id
end loop;
end loop;
I need to increment the variavel1 with the next id that i have in the i (object that have the date from the cursor).
If concatenation is everything you need, you can use pure sql:
select id, listagg(text) within group (order by text) as list
from (
select 1 id, 'abc' text from dual union all
select 1 id, 'def' text from dual union all
select 1 id, 'ghi' text from dual union all
select 3 id, 'jkl' text from dual union all
select 7 id, 'mno' text from dual union all
select 7 id, 'pqr' text from dual)
group by id;
Other possibility, like in this PLSQL block:
declare
cursor crsr is
select 1 id, 'abc' text from dual union all
select 1 id, 'def' text from dual union all
select 1 id, 'ghi' text from dual union all
select 3 id, 'jkl' text from dual union all
select 7 id, 'mno' text from dual union all
select 7 id, 'pqr' text from dual;
variavel1 number;
variavel2 varchar2(1000);
begin
for i in crsr loop
if variavel1 is null or variavel1 <> i.id then
if variavel1 is not null then
dbms_output.put_line(variavel1||' - '||variavel2);
end if;
variavel1 := i.id;
variavel2 := i.text;
else
variavel2 := variavel2 || i.text;
end if;
end loop;
dbms_output.put_line(variavel1||' - '||variavel2);
end;
You can also define simple type as table of objects (id, text) and in loop add items to variable of this type.
declare
type tob is record (id number, text varchar2(1000));
type ttb is table of tob;
variavel2 ttb := ttb();
cursor crsr is
select 1 id, 'abc' text from dual union all
select 1 id, 'def' text from dual union all
select 1 id, 'ghi' text from dual union all
select 3 id, 'jkl' text from dual union all
select 7 id, 'mno' text from dual union all
select 7 id, 'pqr' text from dual;
begin
for i in crsr loop
if variavel2.count = 0 or variavel2(variavel2.count).id <> i.id then
variavel2.extend();
variavel2(variavel2.count).id := i.id;
end if;
variavel2(variavel2.count).text := variavel2(variavel2.count).text||i.text;
end loop;
-- now we have array of values
for x in 1..variavel2.count loop
dbms_output.put_line(variavel2(x).id||' - '||variavel2(x).text);
end loop;
end;
I assumed that id is not nullable, if it is you need minor changes. Output for all solutions is:
ID LIST
------ --------------
1 abcdefghi
3 jkl
7 mnopqr
open cursor;
FETCH cursor INTO cursor_result;
WHILE cursor %found
loop
variavel1=cursor_result.id
WHILE( variavel1=cursor_result.id AND cursor %found)
LOOP
---dO SOMITHING----
FETCH cursor INTO cursor_result; ----PASS TO NEXT VALUE
END LOOP;
end loop;
I have a timestamp which represents the current timestamp:
now := CURRENT_TIMESTAMP;
tomorrow := now + INTERVAL '1' DAY;
Now I want to render a random timestamp between those 2 values,
I tried with DBMS_RANDOM but it doesn't work
Any Help?
This will generate a random timestamp between the current timestamp and sysdate+1:
SELECT TO_DATE(SYSDATE, 'MM/DD/YYYY HH24:MI:SS') +
dbms_random.value(0, TO_DATE(SYSDATE, 'MM/DD/YYYY HH24:MI:SS') -
TO_DATE(SYSDATE, 'MM/DD/YYYY HH24:MI:SS')+1)
FROM dual;
As PL/SQL:
DECLARE
l_ran_time TIMESTAMP;
BEGIN
SELECT SYSDATE +
dbms_random.value(0, SYSDATE -
SYSDATE+1)
INTO l_ran_time
FROM dual;
dbms_output.put_line(l_ran_time);
END;
/