Following statement allows me to retrieve information about a table's partitions:
select table_name, partition_name, high_value from user_tab_partitions where table_name = 'T1';
problem is that, for some unknown reason, high_value column's values are expressed as:
TO_DATE(' 2015-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')
is it possible to retrieve them as date? Or at least do something like "eval" on that expression?
That field is defined as a long type, so depending on the partitioning scheme, it can hold various values (a string like 'ABC', a number 123, a string representation of a date, whatever).
Anyway, sounds like you want to get data from a particular partition, and you're trying to figure out the partition name from the metadata. If thats the case, you can simply use the partition for clause:
select * from my_partitioned_table partition for (to_date('20150801', 'YYYYMMDD'));
Which would select data from the partition housing that particular date (assuming your partitioning by date). This is particularly useful in interval partitioning, where Oracle assigns partition names like SYS_xxxx which seem arbitrary at best.
If you'd like to drop partitions older than a given date, its a bit more tricky. The above syntax is for selecting data, not DDL (alter table). To do that, you could do something like this (loosely tested):
Create a function to identify which partitions hold data with dates less than a given reference date:
create or replace function fn_partition_is_earlier(i_part_tab_name varchar2, i_partition_position number, i_ref_date in date)
return number
is
l_date_str varchar2(2000);
l_date date;
begin
execute immediate 'select high_value from all_tab_partitions where table_name = :tab and partition_position = :pos'
into l_date_str
using i_part_tab_name, i_partition_position;
execute immediate 'select ' || l_date_str || ' from dual' into l_date;
if (l_date < i_ref_date) then
return 1;
end if;
return 0;
end;
Use the function as follows:
with part_name as (
select partition_name
from (
select fn_partition_is_earlier(p.table_name, p.partition_position, to_date('20130501', 'YYYYMMDD')) should_drop_flag, p.*
from all_tab_partitions p
where table_name = 'MY_TAB'
)
where should_drop_flag = 1
)
select 'alter table MY_TAB drop partition ' || part_name.partition_name || ' update global indexes;'
from part_name;
The output would give you the script you'd have the DBAs run off hours.
Hope that helps.
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I have created an interval partitioned table with interval ( NUMTODSINTERVAL(1,'day') ).
As transaction data gets logged to this table on a daily basis, oracle automatically creates daily partitions.
There is a requirement to truncate the previous day's partition on a daily basis without affecting current day partition.
Since delete is producing fragmentation, I've been asked to go with truncate or drop.
Welcome to SO!
Please see below a basic example of truncating/dropping preceding partitions from an interval partitioned table, based on a date that you pass.
Note: You may need to fine tune this further to accommodate the various scenarios and edge cases based on specific data loading behaviors and requirements.
For illustration, I'm creating a sample table with interval partitioning as follows ...
create table testpart
( value_date date )
partition by range ( value_date )
interval ( NUMTODSINTERVAL(1,'day') )
( partition p1 values less than ( date '2020-01-01' ));
Checking default partition on the newly created table ..
select table_name, partition_name, interval from dba_tab_partitions where table_name = 'TESTPART';
|TABLE_NAME |PARTITION_NAME |INTERVAL |
|TESTPART |P1 |NO |
Inserting data current date (e.g. 16th April) and previous date (15th April) ..
insert into testpart select date '2020-04-15' from dual;
insert into testpart select date '2020-04-16' from dual;
commit;
Checking inserted data ..
select * from testpart;
| VALUE_DATE |
| 4/15/2020 |
| 4/16/2020 |
Checking the automatically created interval partitions along with the high_value converted to date format (explanations mentioned alongside in comments) ...
Note: I've adapted the logic from this post on SO
select
table_name,
partition_name,
interval,
to_date ( -- convert to date datatype
trim ( -- trim whitespaces
'''' from regexp_substr (
extractvalue ( -- return scalar value of the single text node viz., high_value
dbms_xmlgen.getxmltype ( -- converts the results of a SQL query into XML format
'select high_value from all_tab_partitions where table_name='''
|| table_name
|| ''' and partition_name = '''
|| partition_name
|| ''''),
'//text()'),
'''.*?''')), -- regex pattern matching to fetch value between the first set of quotes
'syyyy-mm-dd hh24:mi:ss')
high_value_in_date_format
FROM all_tab_partitions
WHERE table_name = 'TESTPART'
;
|TABLE_NAME |PARTITION_NAME |INTERVAL |HIGH_VALUE_IN_DATE_FORMAT |
|TESTPART |SYS_P9064429 |YES |4/17/2020 |
|TESTPART |SYS_P9064428 |YES |4/16/2020 |
|TESTPART |P1 |NO |1/1/2020 |
Creating a procedure to accept date and match that to high_value to identify immediately preceding partition in the specified table and truncating/dropping that partition ...
create or replace procedure int_part_housekeeping
( p_date date,
p_table_name varchar2
)
as
l_part_name varchar2(30);
begin
-- identifying partition based on high value in all_tab_partitions
select partition_name
into l_part_name
from all_tab_partitions
where table_name = p_table_name
and
to_date ( -- convert to date datatype
trim ( -- trim whitespaces
'''' from regexp_substr (
extractvalue ( -- return scalar value of the single text node
dbms_xmlgen.getxmltype ( -- converts the results of a SQL query into XML format
'select high_value from all_tab_partitions where table_name='''
|| table_name
|| ''' and partition_name = '''
|| partition_name
|| ''''),
'//text()'),
'''.*?''')), -- regex pattern matching to fetch value between the first set of quotes
'syyyy-mm-dd hh24:mi:ss') = p_date
;
-- truncating preceding partition
dbms_output.put_line('Trucating partition for preceding interval partition :' || l_part_name );
execute immediate 'ALTER TABLE ' || p_table_name || ' TRUNCATE PARTITION (' || l_part_name || ')';
-- dropping preceding partition (note: interval needs to be reset before and after the drop operation)
dbms_output.put_line('Dropping partition for preceding interval partition :' || l_part_name);
execute immediate 'ALTER TABLE ' || p_table_name || ' SET INTERVAL ()';
execute immediate 'ALTER TABLE ' || p_table_name || ' DROP PARTITION (' || l_part_name || ')';
execute immediate 'ALTER TABLE ' || p_table_name || ' SET INTERVAL ( NUMTODSINTERVAL(1,''day'') )';
exception
when others then
dbms_output.put_line(sqlerrm);
raise;
end;
Execute the procedure passing current date and table name ..
set serveroutput on;
begin
int_part_housekeeping(date'2020-04-16','TESTPART');
end;
Output:
Trucating partition for preceding interval partition :SYS_P9064428
Dropping partition for preceding interval partition :SYS_P9064428
PL/SQL procedure successfully completed.
Checking if the required partitions were dropped ...
|TABLE_NAME |PARTITION_NAME |INTERVAL |HIGH_VALUE_IN_DATE_FORMAT |
|TESTPART |SYS_P9064429 |NO |4/17/2020 |
|TESTPART |P1 |NO |1/1/2020 |
You may then execute this procedure on-demand or use dbms scheduler to run at specific times.
If this suits your requirement, feel free to accept and vote
I am a developer. In my table, I had a date range partitions.
I want to get partition names which are defined for a table between two dates.
I tried with below query and it is returning all the partitions on a table.
select * from USER_TAB_PARTITIONS WHERE TABLE_NAME = 'TABLE NAME' ORDER BY PARTITION_NAME;
My requirement is , I will pass two dates as inputs and between those two dates i want to get partition names.
Please suggest query.
That's not very simple; the major obstacle is user_tab_partitions.high_value datatype, which is long, and it is difficult to work with. Usually you get
ORA-00932: inconsistent datatypes: expected - got LONG
error.
However, using a few steps, it can be done. Have a look at this example.
Create a partitioned table and insert a few rows into it:
SQL> CREATE TABLE test_part
2 (
3 datum DATE,
4 text VARCHAR2 (10)
5 )
6 PARTITION BY RANGE (datum)
7 INTERVAL ( NUMTODSINTERVAL (1, 'day') )
8 (PARTITION p0 VALUES LESS THAN (TO_DATE ('01.01.2020', 'dd.mm.yyyy')));
Table created.
SQL> INSERT INTO test_part
2 SELECT DATE '2015-08-15', 'Little' FROM DUAL
3 UNION ALL
4 SELECT DATE '2020-03-26', 'Foot' FROM DUAL;
2 rows created.
What does user_tab_partitions say?
SQL> SELECT table_name, partition_name, high_value
2 FROM user_tab_partitions
3 WHERE table_name = 'TEST_PART';
TABLE_NAME PARTITION_NAME HIGH_VALUE
--------------- --------------- -----------------------------------
TEST_PART P0 TO_DATE(' 2020-01-01 00:00:00', 'SY
YYY-MM-DD HH24:MI:SS', 'NLS_CALENDA
R=GREGORIA
TEST_PART SYS_P63 TO_DATE(' 2020-03-27 00:00:00', 'SY
YYY-MM-DD HH24:MI:SS', 'NLS_CALENDA
R=GREGORIA
So, you'd want to extract date part from the high_value column. The first step is kind of a stupid one - create a new table; basically CTAS:
SQL> CREATE TABLE temp_utp
2 AS
3 SELECT table_name, partition_name, TO_LOB (high_value) high_value
4 FROM user_tab_partitions;
Table created.
For simplicity (in further steps), I'll create a view based on that table which will extract date value (line #5):
SQL> CREATE OR REPLACE VIEW v_utp
2 AS
3 SELECT table_name,
4 partition_name,
5 TO_DATE (SUBSTR (high_value, 12, 10), 'rrrr-mm-dd') datum
6 FROM temp_utp;
View created.
The rest is easy now:
SQL> SELECT *
2 FROM v_utp
3 WHERE datum < DATE '2020-02-15';
TABLE_NAME PARTITION_NAME DATUM
--------------- --------------- ----------
TEST_PART P0 2020-01-01
SQL>
OK, you'd use two date parameters which would then lead to between in the final query, but that's easy to modify.
Major drawback here is CTAS which creates temp_utp table; you'd have to recreate it as frequently as you add new partitions into the main table. One option is to do it in a scheduled manner, e.g. using a database job (see dbms_job and/or dbms_scheduler documentation if you don't know how) which would schedule a stored procedure which will then use dynamic SQL, i.e. execute immediate to create temp_utp. You don't have to recreate a view - it will become valid as soon as a new temp_utp table is created.
I was trying to find solution for the same problem and found that creating a function to convert high value to date works fine like below
CREATE OR REPLACE FUNCTION get_high_value_date(p_partition_name IN VARCHAR2)
RETURN DATE
IS
l_varchar_date VARCHAR2(4000);
l_date DATE;
BEGIN
EXECUTE IMMEDIATE 'SELECT high_value FROM all_tab_partitions WHERE partition_name = :1 ' INTO l_varchar_date
USING p_partition_name;
EXECUTE IMMEDIATE 'SELECT '||l_varchar_date||' FROM dual' INTO l_date;
RETURN l_date;
EXCEPTION WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ( 'Exc: '||SQLERRM );
RETURN NULL;
END;
Then you can use this for getting date value
SELECT get_high_value_date (partition_name) partition_date, partition_name
FROM all_tab_partitions
WHERE table_name = :table
I am trying to write a Procedure which essentially drops partitions from several tables which are stored in multiple schemas. End goal is to then create a dbms scheduler which will run this procedure every day and check for partitions that hold data older than 6 months. How to add functionality of looking for partitions across multiple schemas ?
I have created a Procedure which drops a partition only from a specific table.
PROCEDURE purge_ops_log_range_parts IS
BEGIN
FOR partition_rec IN (SELECT partition_name
,high_value
FROM user_tab_partitions
WHERE table_name =
'OPSWIRE_LOG_RANGE_PARTS')
LOOP
IF SYSDATE >= add_months(to_date(substr(partition_rec.high_value
,12
,19)
,'YYYY-MM-DD HH24:MI:SS')
,6)
THEN
execute_immediate('ALTER TABLE OPS_LOG_RANGE_PARTS DROP PARTITION ' ||
partition_rec.partition_name);
END IF;
END LOOP;
END purge_ops_log_range_parts;
Output is deleting partition from a specific table only however it does not look for multiple tables in various schemas.
Use the DBA_TAB_PARTITIONS or ALL_TAB_PARTITIONS views instead of USER_TAB_PARTITIONS. The former two views contain a TABLE_OWNER (i.e. schema) column which should help you accomplish your goal.
You can then parameterize your procedure to take the owner and table names as parameters:
PROCEDURE purge_ops_log_range_parts(pinOwner IN VARCHAR2,
pinTable_name IN VARCHAR2)
IS
BEGIN
FOR partition_rec IN (SELECT partition_name
,high_value
FROM DBA_TAB_PARTITIONS
WHERE TABLE_OWNER = pinOwner AND
table_name = pinTable_name)
LOOP
IF SYSDATE >= add_months(to_date(substr(partition_rec.high_value
,12
,19)
,'YYYY-MM-DD HH24:MI:SS')
,6)
THEN
execute_immediate('ALTER TABLE ' || pinOwner || '.' ||
pinTable_name || ' DROP PARTITION ' ||
partition_rec.partition_name);
END IF;
END LOOP;
END purge_ops_log_range_parts;
Best of luck.
PROCEDURE purge_partitions
(
p_owner IN VARCHAR2
,p_name IN VARCHAR2
,p_retention_period IN NUMBER
) IS
BEGIN
FOR partition_rec IN (SELECT partition_name
,high_value
FROM dba_tab_partitions
WHERE table_owner = p_owner
AND table_name = p_name)
LOOP
IF SYSDATE >= add_months(to_date(substr(partition_rec.high_value
,12
,19)
,'YYYY-MM-DD HH24:MI:SS')
,p_retention_period)
THEN
execute_immediate('ALTER TABLE ' || p_owner || '.' ||
p_name || ' DROP PARTITION ' ||
partition_rec.partition_name)
END IF;
END LOOP;
END purge_partitions;
Purge_Partitions procedure deals with dropping partitions based on
specific retention priods mentioned in a seperate Config Table. I am
now trying to enhance this functionality which will take care of
rebuilding global indexes of those partitioned tables. Not sure how to go about this, any help is highly appreciated.
Consider the update_index_clauses, which keeps the indexes valid.
See the documentation and consideration for global indexes here
in your case it will be:
alter table ttttt drop partition pppppp update global indexes;
Alternatively let the indices be invalidated in the DROP PARTITION and rebuild them with alter index xxx rebuild. You can get the list of indexes to rebuild from this query
select OWNER, INDEX_NAME
from all_indexes
where owner = 'ooo' and table_name = 'tttttt' and status = 'UNUSABLE';
I have a table with a few hundred partitions and I am generally interested on the latest 35.
Accordingly I am trying to create views which would access these dynamically. i.e. always use the latest in case ones are created.
The query:
select PARTITION_NAME,
PARTITION_POSITION,
NUM_ROWS,
AVG_ROW_LEN
from all_tab_partitions
where
table_name = 'MY_TABLE'
AND PARTITION_NAME <> 'P_LAST'
AND PARTITION_POSITION < (SELECT MAX(PARTITION_POSITION)
FROM all_tab_partitions) - 35
order by 2 DESC
;
Seems to return me the partition names I'm interested, however, I don't manage to use it's results to select the partitions. e.g.:
CREATE OR REPLACE VIEW MY_VIIEW AS
WITH t AS ( [Above query] )
SELECT * FROM
MY_TABLE PARTITION (SELECT /*+ FIRST_ROWS(1) */ PARTITION_NAME
from t);
(not the actual view, just an example)
So how do I do that? How do I create a view which will acess always the latest partition (execpt of "MAX")?
I am using Oracle 10g
thanks
You can do it using PL/SQL only
create or replace package my_table_ is
type t_records is table of my_table%rowtype;
function getpart(c_parts sys_refcursor) return t_records pipelined;
end;
create or replace package body my_table_ is
function getpart(c_parts sys_refcursor) return t_records pipelined is
v_partition all_tab_partitions.partition_name%type;
v_row my_table%rowtype;
c_mytab sys_refcursor;
begin
loop
fetch c_parts into v_partition;
exit when c_parts%notfound;
open c_mytab for 'select * from my_table partition ('||v_partition||')';
loop
fetch c_mytab into v_row;
exit when c_mytab%notfound;
pipe row (v_row);
end loop;
end loop;
end;
end;
Now you can
select * from table(my_table_.getpart(cursor(<QUERY_RETURNING_PARTITION_NAMES>)));
May be you can construct view's query using batch of union all statements with partition name in each statement, e.g.
create view p as
select * from my_table partition (part1)
union all
select * from my_table partition (part1)
...
union all
select * from my_table partition (part35)
Ok... I don't think your can use the Partition-Names, but you can use the Starting-Values of the Partitions to select the Data matching these Partitions...
So you View would look like this:
SELECT * FROM my_table WHERE date_col > get_part_limit( 'my_table', 35 ):
Where date_col is the column you use for partitioning - and get_part_limit is a stored function you write like this:
...
BEGIN
SELECT high_value FROM all_tab_partitions
INTO local_var
WHERE table_name = parameter_name
AND PARTITION_POSITION = MAX... - 35
EXECUTE IMMEDIATE 'SELECT '||local_var||' FROM DUAL' INTO local_return_value;
RETURN local_return_value;
END;
partitions are designed to be transparent for the data, so when you write a query, you simply don't know how your data is stored.
I see only one possibility to hit a particular partition: your WHERE clause should match values to the partitioned columns of latest (or latest 5) partition.
Next question is to build this WHERE clause on the fly. You already know that there is plenty of information in oracle dictionary. So you will read that and create a constructor to convert metadata conditions back into SQL.
irl we do exactly the same thing and use falco's solution like.
Here is my code:
create or replace function longToDate( myOwner varchar2,
mytable_name in varchar2,
mypartition_name in varchar2
) return date
as
cDate date;
cvar varchar2(1024);
rq varchar2(1024);
infiniteValue EXCEPTION;
PRAGMA EXCEPTION_INIT(infiniteValue, -00904);
begin
select high_value into cvar FROM dba_tab_partitions t where t.table_owner=myOwner and table_name=mytable_name and partition_name=mypartition_name;
rq:='select '||cvar||' from dual';
execute immediate rq into cDate;
return cdate;
EXCEPTION
WHEN infiniteValue
then return'01 jan 3000';
when others
then return null;
end longToDate;
Ant the view is something like this
create or replace view last_35 as
with maxdate as
(select longToDate(p.table_owner,p.table_name,p.partition_name) mydate,
rank()over(order by p.partition_position desc) mypos,
p.* from all_tab_partitions p
where p.table_name='MY_TABLE'
)
select /*+full(a)*/* from MY_TABLE a, maxdate
where MY_TABLE.partition_name>maxdate.mydate
and maxdate.mypos=35