Trying to create a procedure that changes dates when the end date is less than sysdate, but it changes all the dates, so here is the code:
CREATE OR REPLACE PROCEDURE CHANGE_DATES
as
BEGIN
FOR rec IN (SELECT start_period_date,end_period_date FROM mytable) LOOP
DBMS_OUTPUT.PUT_LINE('Old Date:' || rec.start_period_date ||' ' || rec.end_period_date);
IF rec.end_period_date < sysdate THEN
update mytable set start_period_date = sysdate;
update mytable set end_period_date = sysdate + 6;
commit;
--DBMS_OUTPUT.PUT_LINE('New Date:' || begin_date ||' ' ||end_date);
end if;
END LOOP;
END;
You need a WHERE clause (and you can do it all in one UPDATE statement):
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE mytable ( start_period_date, end_period_date ) AS
SELECT DATE '2018-01-01', DATE '2018-01-02' FROM DUAL
/
CREATE OR REPLACE PROCEDURE CHANGE_DATES
AS
start_dates SYS.ODCIDATELIST;
end_dates SYS.ODCIDATELIST;
BEGIN
UPDATE ( SELECT m.*,
( SELECT m.start_period_date FROM DUAL ) AS old_start_date,
( SELECT m.end_period_date FROM DUAL ) AS old_end_date
FROM mytable m )
SET start_period_date = SYSDATE,
end_period_date = SYSDATE + INTERVAL '6' DAY
WHERE end_period_date < SYSDATE
RETURNING old_start_date, old_end_date
BULK COLLECT INTO start_dates, end_dates;
FOR i IN 1 .. start_dates.COUNT LOOP
DBMS_OUTPUT.PUT_LINE( 'Old Date:' || start_dates(i) ||' ' || end_dates(i) );
END LOOP;
-- Do not COMMIT in the procedure, COMMIT in the calling scope.
END;
/
Query 1:
BEGIN
CHANGE_DATES;
COMMIT;
END;
Query 2:
SELECT * FROM mytable
Results:
| START_PERIOD_DATE | END_PERIOD_DATE |
|----------------------|----------------------|
| 2018-05-21T09:06:05Z | 2018-05-27T09:06:05Z |
Related
I'm trying to drop all partitions for a table and I'm running into the following error:
ORA-14083: cannot drop the only partition of a partitioned table ORA-06512:
I know I can drop the table and recreate it but I want to see if my procedure can be modified to use an EXCEPTION that sets the retention to nothing, drops the PARTITION and sets the INTERVAL back to its original value?
Any help and expertise would be greatly appreciated. Below is my test CASE.
CREATE OR REPLACE PROCEDURE ddl(p_cmd varchar2)
authid current_user
is
BEGIN
dbms_output.put_line(p_cmd);
execute immediate p_cmd;
END;
/
CREATE OR REPLACE PROCEDURE
drop_partition(
p_tab varchar2,
p_date date
) authid current_user
is
v_high_value date;
cursor v_cur is
select table_name,
partition_name,
high_value,
partition_position
from user_tab_partitions
where table_name = upper(p_tab);
begin
for v_rec in v_cur loop
execute immediate 'select ' || v_rec.high_value || ' from dual' into v_high_value;
if v_high_value <= trunc(p_date)
then
-- dbms_output.put_line ('partition ' || v_rec.partition_name || ' high value is ' || to_char(v_high_value,'mm/dd/yyyy'));
--ddl( 'ALTER TABLE ' || p_tab || ' DROP PARTITION FOR(DATE ' || TO_CHAR(v_high_value,'YYYY-MM-DD') || ')');
ddl('alter table '||p_tab||' drop partition '||v_rec.partition_name);
end if;
end loop;
END;
/
CREATE TABLE partition_retention
(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
TABLE_NAME VARCHAR2(30),
DAYS NUMBER(6),
CONSTRAINT
partition_retention_pk primary key (table_name));
/
INSERT into partition_retention(TABLE_NAME, DAYS)
WITH data as (
select 'T1', 0
from dual union all
select 'T3', 15
from dual union all
select 'T4', 10
from dual union all
select 'T5', 5
from dual)
SELECT * from data;
/
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 into t1 (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-02-01')
select dt from dt;
/
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
)
LOOP
drop_partition(
td.table_name,
trunc(sysdate-td.days)
);
END LOOP;
END;
/
Your question says "drop the last partition" which can be different to "drop the only partition"
As stated already in comments, you cannot drop the partition when only one partition is left. If you like to remove the data, then you can TRUNCATE the partition.
This PL/SQL Block should cover all your cases:
CREATE TABLE PARTITION_RETENTION (
TABLE_NAME VARCHAR2(30),
RETENTION INTERVAL DAY(3) TO SECOND(0) --> More generic than number of days
);
DECLARE
CANNOT_DROP_LAST_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_LAST_PARTITION, -14758);
CANNOT_DROP_ONLY_ONE_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_ONLY_ONE_PARTITION, -14083);
ts TIMESTAMP;
CURSOR TablePartitions IS
SELECT TABLE_NAME, PARTITION_NAME, p.HIGH_VALUE, t.INTERVAL, RETENTION, DATA_TYPE
FROM USER_PART_TABLES t
JOIN USER_TAB_PARTITIONS p USING (TABLE_NAME)
JOIN USER_PART_KEY_COLUMNS pk ON pk.NAME = TABLE_NAME
JOIN USER_TAB_COLS tc USING (TABLE_NAME, COLUMN_NAME)
JOIN PARTITION_RETENTION r USING (TABLE_NAME);
BEGIN
FOR aPart IN TablePartitions LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts < SYSTIMESTAMP - aPart.RETENTION THEN
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' DROP PARTITION '||aPart.PARTITION_NAME|| ' UPDATE GLOBAL INDEXES';
DBMS_OUTPUT.PUT_LINE('Dropped partittion '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
EXCEPTION
WHEN CANNOT_DROP_ONLY_ONE_PARTITION THEN
DBMS_OUTPUT.PUT_LINE('Cannot drop the only partittion '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' TRUNCATE PARTITION '||aPart.PARTITION_NAME|| ' UPDATE GLOBAL INDEXES';
DBMS_OUTPUT.PUT_LINE('Truncated partittion '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
WHEN CANNOT_DROP_LAST_PARTITION THEN
BEGIN
DBMS_OUTPUT.PUT_LINE('Drop last partittion '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL ()';
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' DROP PARTITION '||aPart.PARTITION_NAME;
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL( '||aPart.INTERVAL||' )';
EXCEPTION
WHEN CANNOT_DROP_ONLY_ONE_PARTITION THEN
-- Depending on the order the "last" partition can be also the "only" partition at the same time
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL( '||aPart.INTERVAL||' )';
DBMS_OUTPUT.PUT_LINE('Cannot drop the only partittion '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' TRUNCATE PARTITION '||aPart.PARTITION_NAME|| ' UPDATE GLOBAL INDEXES';
DBMS_OUTPUT.PUT_LINE('Truncated partittion '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
END;
END;
END IF;
END LOOP;
END;
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 the following query, which returns the number of rows per table in a schema.
Can this also be modified to RETURN the number of columns per table too?
CREATE table stats (
table_name VARCHAR2(128),
num_rows NUMBER,
num_cols NUMBER
);
/
DECLARE
val integer;
BEGIN
for i in (SELECT table_name FROM all_tables WHERE owner = 'Schema')
LOOP
EXECUTE IMMEDIATE 'SELECT count(*) from ' || i.table_name INTO val;
INSERT INTO stats VALUES (i.table_name,val);
END LOOP;
END;
/
You can use the ALL_TAB_COLS dictionary table:
DECLARE
val integer;
BEGIN
FOR i IN (
SELECT table_name,
COUNT(*) AS num_cols
FROM all_tab_cols
WHERE owner = 'Schema'
GROUP BY table_name
)
LOOP
EXECUTE IMMEDIATE 'SELECT count(*) from ' || i.table_name INTO val;
INSERT INTO stats VALUES (i.table_name,val, i.num_cols);
END LOOP;
END;
/
db<>fiddle here
The column HIGH_VALUE has data similar to below, of type LONG, and always has the same length:
TIMESTAMP' 2019-01-30 00:00:00'
How can I convert it to a DATE type without using a function?
My overall goal is to create a result set which can then be used as an inner query for other aggregations. For example, I would like to be able to sum the number of rows over a year according to the date produced by converting the HIGH_VALUE column to a date.
I have only read permissions on this database and therefore cannot create functions. I've seen other solutions on StackOverflow and other sites, but they all require creating a function.
ALL_TAB_PARTITIONS is a standard built-in Oracle table and therefore I'm not including the table structure. In case that's an issue, please let me know and I will create an example table.
An example query and the data one row that results from this query follows. Note that I cannot create tables on this database so I will also need an a method that works without creating a temporary table.
Insert into EXPORT_TABLE (TABLE_OWNER,TABLE_NAME,PARTITION_NAME,HIGH_VALUE,NUM_ROWS)
VALUES ('TO','TN','SYS_P201709','TIMESTAMP'' 2019-01-30 00:00:00''',5053133);
SELECT TABLE_OWNER, TABLE_NAME, PARTITION_NAME, HIGH_VALUE, NUM_ROWS
from ALL_TAB_PARTITIONS;
If you are using Oracle 12c you could still use function but defined inline:
WITH FUNCTION with_function(p_id IN NUMBER) RETURN NUMBER IS
BEGIN
-- logic here
RETURN p_id;
END;
SELECT with_function(id)
FROM your_table
Related: WITH Clause Enhancements in Oracle Database 12c Release 1 (12.1)
For the conversion of a LONG type ( HIGH_VALUE ) to TIMESTAMP,
One option is to use dynamic sql and perform your insert through an anonymous block. No Procedure or function is required.
DECLARE
tstamp TIMESTAMP;
BEGIN
FOR rec IN ( SELECT table_owner,table_name,partition_name,high_value,num_rows
FROM all_tab_partitions
WHERE ROWNUM < 5
) LOOP
EXECUTE IMMEDIATE 'BEGIN :dt := '
|| rec.high_value
|| '; END;'
USING OUT tstamp; --assign the long to an external timestamp variable
INSERT INTO export_table (
table_owner,table_name,partition_name,high_value,num_rows
) VALUES (rec.table_owner,
rec.table_name, rec.partition_name, tstamp, rec.num_rows
);
END LOOP;
END;
/
AS #APC commented, There's also a solution using Pure SQL, which
uses a slightly complex Xml expression.
Combining the pure SQL solution from APC's comment with the enhancements in Oracle 12 to allow functions to be declared in WITH clauses and Kaushik Nayak's method of using EXECUTE IMMEDIATE to convert the string value to a date then you can get this:
Oracle Setup - Test Table & Data:
CREATE TABLE EXPORT_TABLE (
TABLE_OWNER VARCHAR2(30),
TABLE_NAME VARCHAR2(30),
PARTITION_NAME VARCHAR2(30),
HIGH_VALUE LONG,
NUM_ROWS INTEGER
);
INSERT INTO EXPORT_TABLE VALUES ( 'TO', 'TN', 'PN', 'TIMESTAMP ''2019-06-26 12:34:56''', 12345 );
Query:
WITH FUNCTION clobToDate( value IN CLOB ) RETURN DATE
IS
ts DATE;
BEGIN
EXECUTE IMMEDIATE 'BEGIN :ts := ' || value || '; END;' USING OUT ts;
RETURN ts;
END;
SELECT TABLE_OWNER,
TABLE_NAME,
PARTITION_NAME,
clobToDate(
EXTRACTVALUE(
dbms_xmlgen.getxmltype(
'SELECT high_value'
|| ' FROM EXPORT_TABLE'
|| ' WHERE TABLE_OWNER = ''' || t.table_owner || ''''
|| ' AND TABLE_NAME = ''' || t.table_name || ''''
|| ' AND PARTITION_NAME = ''' || t.partition_name || ''''
),
'//text()'
)
) AS HIGH_VALUE,
NUM_ROWS
FROM EXPORT_TABLE t;
Output:
TABLE_OWNER | TABLE_NAME | PARTITION_NAME | HIGH_VALUE | NUM_ROWS
:---------- | :--------- | :------------- | :------------------ | -------:
TO | TN | PN | 2019-06-26 12:34:56 | 12345
db<>fiddle here
Update: If you want to aggregate some columns then:
WITH FUNCTION clobToDate( value IN CLOB ) RETURN DATE
IS
ts DATE;
BEGIN
EXECUTE IMMEDIATE 'BEGIN :ts := ' || value || '; END;' USING OUT ts;
RETURN ts;
END;
SELECT table_owner,
table_name,
MAX( high_value ) AS max_high_value,
SUM( num_rows ) AS total_rows
FROM (
SELECT TABLE_OWNER,
TABLE_NAME,
PARTITION_NAME,
clobToDate(
EXTRACTVALUE(
dbms_xmlgen.getxmltype(
'SELECT high_value'
|| ' FROM EXPORT_TABLE'
|| ' WHERE TABLE_OWNER = ''' || t.table_owner || ''''
|| ' AND TABLE_NAME = ''' || t.table_name || ''''
|| ' AND PARTITION_NAME = ''' || t.partition_name || ''''
),
'//text()'
)
) AS HIGH_VALUE,
NUM_ROWS
FROM EXPORT_TABLE t
)
GROUP BY table_owner, table_name;
db<>fiddle here
I have a table structure like as :
desc temp_table ;
Name Null? Type
student_name VARCHAR2(500)
Is_delete NUMBER(2)
using pl sql procedure I'm trying to store all values of above table into a variable.
If table has more then one row then I'm getting error like
ORA-01422: exact fetch returns more than requested number of rows
So without using cursor how can we store multiple row's in a variable??
Define two collection types:
CREATE OR REPLACE TYPE StringList IS TABLE OF VARCHAR2(4000);
/
CREATE OR REPLACE TYPE BooleanList IS TABLE OF NUMBER(1,0);
/
Use BULK COLLECT INTO:
CREATE OR REPLACE PROCEDURE UPDATE_TABLE
AS
Names StringList;
Del BooleanList;
BEGIN
DBMS_OUTPUT.ENABLE(1000000);
SELECT student_name,
Is_delete
BULK COLLECT INTO
Names,
Del
FROM temp_table
WHERE MODIFIED_DATE >= TRUNC( SYSDATE )
AND MODIFIED_DATE < TRUNC( SYSDATE ) + INTERVAL '1' DAY;
DBMS_OUTPUT.PUT_LINE ('Number Of Names: ' || Names.COUNT );
DBMS_OUTPUT.PUT_LINE ('Number Of Del: ' || Del.COUNT ); -- Will always be the same amount
FOR i IN 1 .. Names.COUNT LOOP
DBMS_OUTPUT.PUT_LINE ( Names(i) || ', ' || Del(i) );
END LOOP;
END;
/