Dropping a NOTNULL contraint results in a full table scan - oracle

I have a table that contains about 25 million records and has some NOTNULL-constraint on several fields.
When i drop one of these NOTNULL contraints a full table scan is executed (which takes quite a lot of time). I can see that in the session browser of a second instance of TOAD (i use TOAD to drop the constraint).
Is there a way to avoid this full table scan when a constraint gets dropped?

This suggests the column causing the full table scan has a default value, possibly from adding the column to the table while data already existed, with a not-null constraint (which is only possibly since 11gR1).
As a demo, without a default value:
create table t42 (id number);
alter table t42 add (some_col number not null);
select data_default, default_length from user_tab_columns where column_name = 'SOME_COL';
DATA_DEFAULT DEFAULT_LENGTH
------------------------------------------------------------ --------------
insert into t42 (id, some_col)
select level, 0 from dual
connect by level <= 100000;
insert into t42 (id, some_col)
select 100000 + level, 1 from dual
connect by level <= 10000;
select some_col, count(*) from t42 group by some_col;
SOME_COL COUNT(*)
---------- ----------
1 10000
0 100000
set timing on
alter table t42 modify (some_col null);
Table T42 altered.
Elapsed: 00:00:00.056
But with a default value:
create table t42 (id number);
insert into t42 (id)
select level from dual
connect by level <= 100000;
alter table t42 add (some_col number default 0 not null);
select data_default, default_length from user_tab_columns where column_name = 'SOME_COL';
DATA_DEFAULT DEFAULT_LENGTH
------------------------------------------------------------ --------------
0 2
insert into t42 (id, some_col)
select 100000 + level, 1 from dual
connect by level <= 10000;
select some_col, count(*) from t42 group by some_col;
SOME_COL COUNT(*)
---------- ----------
1 10000
0 100000
set timing on
alter table t42 modify (some_col null);
Table T42 altered.
Elapsed: 00:00:04.734
Now the alter takes much longer, because it has to actually update all the pre-constraint rows to physically have a value zero. After the alter you see the same data, even if you change the default value (before or after the original alter; though if you do it before you potentially have a small window where a constraint violation could occur):
alter table t42 modify (some_col default null);
select data_default, default_length from user_tab_columns where column_name = 'SOME_COL';
DATA_DEFAULT DEFAULT_LENGTH
------------------------------------------------------------ --------------
null 4
select some_col, count(*) from t42 group by some_col;
SOME_COL COUNT(*)
---------- ----------
1 10000
0 100000
There isn't really any way around this, other than adding a new column without the default (which will take at least as long, and probably cause other side-effects).
Notice that the default value has change from not being set at all to explicitly being null. When you insert a row there isn't any practical difference - the column value ends up null either way - but you can't completely remove the default once it's been set.
It's also interesting that if you change the column default without dropping the constraint, that has no effect on the reported value for the rows that were using it - they would still show as zero. Oracle seems to be storing that constraint default somewhere else, which makes sense.
Any rows inserted after the default/not-null column was added will have an actual value stored in the table anyway, and changing the default will affect subsequent insertions - but the rows that already existed before the constraint was added behave as if they had actually been updated to whatever default value was specified when the constraint was added.
This change in 11g was mainly to speed the column addition up, and stop you having to to separate steps to add the column without a constraint, then update all existing rows (which was the slow bit), and then altering the table again to add the constraint. This mechanism lets you do it (almost) instantly with just a metadata change. But that update cost still has to be pain if the constraint is then removed.

Related

Creating List partition to an already Existing Table

I am trying to Create a list partition Based on the column "REFRESH_FLAG_Y" which has only Y and N as its Values, Below is the Alter Table used to Create the partition
ALTER TABLE "EDW"."LABOR_SCHEDULE_DAY_F" MODIFY
PARTITION BY LIST ("REFRESH_FLAG")
(PARTITION "REFRESH_FLAG_Y" VALUES ('Y') ,
PARTITION "REFRESH_FLAG_N" VALUES ('N')) ;
COMMIT;
But Whenever I execute the code I get an Error message
ERROR at line 1:
ORA-14400: inserted partition key does not map to any partition
You did tag the question with Oracle 11g tag; do you really use it?
This is a 12c example; it works if everything is OK:
SQL> create table labor_schedule_day_f as
2 select 1 id, 'Y' refresh_flag from dual union all
3 select 2 id, 'N' refresh_flag from dual;
Table created.
SQL> alter table labor_schedule_Day_f modify
2 partition by list (refresh_flag)
3 (partition refresh_flag_y values ('Y'),
4 partition refresh_flag_n values ('N')
5 );
Table altered.
Error you reported means this:
SQL> drop table labor_schedule_day_f;
Table dropped.
SQL> create table labor_schedule_day_f as
2 select 1 id, 'Y' refresh_flag from dual union all
3 select 2 id, 'N' refresh_flag from dual;
Table created.
Insert a row whose REFRESH_FLAG isn't Y nor N (so it violates the rule you specified):
SQL> insert into labor_schedule_day_f values (3, 'X');
1 row created.
Using the same ALTER TABLE statement as previously:
SQL> alter table labor_schedule_Day_f modify
2 partition by list (refresh_flag)
3 (partition refresh_flag_y values ('Y'),
4 partition refresh_flag_n values ('N')
5 );
alter table labor_schedule_Day_f modify
*
ERROR at line 1:
ORA-14400: inserted partition key does not map to any partition
SQL>
See? Error you got, which means that
which has only Y and N as its Values
isn't true.
P.S. You'd get the same result even if refresh_flag was NULL for some rows.

When the num_rows is null in all_tab_statistics in oracle

I want to know when the num_rows is set to null in all_tab_statistics table.
One of my queries is taking a bad plan because the table is having num_rows as blank.
The table is partitioned. So the table is freshly created and ideally, it should have 0 rows. What could be the reason for this and how to avoid this.
Also when we are running the job on the freshly created table which is having blank num_rows(in stats) the rows got inserted in between the process is more than 0 but since we can't analyze the table between the process it took wrong plan (because of blank num_rows)
I have created a table
CREATE table empty
(
dummy NUMBER
);
First time running this query gives
select table_name, num_rows from sys.all_tab_statistics where owner = 'HR' and table_name = 'EMPTY';
TABLE_NAME NUM_ROWS
---------- --------
EMPTY <NULL>
Then I inserted some rows
INSERT INTO empty SELECT SALARY FROM EMPLOYEES;
COMMIT;
And got the same result
TABLE_NAME NUM_ROWS
---------- --------
EMPTY <NULL>
Running this statement populated the row for EMPTY table in all_tab_statistics
ANALYZE TABLE empty COMPUTE STATISTICS;
I got
TABLE_NAME NUM_ROWS
---------- --------
EMPTY 107

Altering table property to default when NULL only for new inserts

Is there any way in Oracle that we can set Default column value when NULL only for New inserts? I don't want to change the Existing records if they have NULL.
I want to do this at table level. Not with NVL insert logic.
As far as I know, if you alter the table and set a default value for a column, it should only affect new records which would come in via an insert, not existing records.
ALTER TABLE yourTable MODIFY (col VARCHAR(100) DEFAULT 'some value');
Using the above approach, col values which are already NULL should remain NULL, at least from the point of view of inserts not changing those NULL values. And newly inserted records which do not specify a value for col should receive the default value some value.
Here's a demonstration which shows what's going on.
First, a test table and some inserts:
SQL> create table test (id number, col varchar2(10));
Table created.
SQL> insert into test (id, col) values (1, null);
1 row created.
SQL> insert into test (id, col) values (2, 'Littlefoot');
1 row created.
SQL> select * from test;
ID COL
---------- ----------
1
2 Littlefoot
Alter the table so that newly added rows contain 'some value' for the COL column:
SQL> alter table test modify col default 'some value';
Table altered.
OK; and now, important part of the story: pay attention to following:
SQL> -- this won't work as you wanted, because I explicitly inserted NULL into COL
SQL> insert into test (id, col) values (3, null);
1 row created.
SQL> -- this will work, because COL is omitted from the INSERT statement
SQL> insert into test (id) values (4);
1 row created.
SQL> select * From test;
ID COL
---------- ----------
1
2 Littlefoot
3
4 some value
SQL>
See? If you explicitly put NULL into a column, it won't get the default value.
However, if you were on 12c (I know, you aren't - just saying, for future reference), there's yet another option: DEFAULT ON NULL. It goes like this:
SQL> alter table test modify col default on null 'some value';
alter table test modify col default on null 'some value'
*
ERROR at line 1:
ORA-02296: cannot enable (SCOTT.) - null values found
Ooops! Won't work if there are NULLs in the column. I know #2 that you don't want to modify existing rows, but - for this demonstration, I'll do that:
SQL> update test set col = 'x' where col is null;
2 rows updated.
SQL> alter table test modify col default on null 'some value';
Table altered.
OK; let's see how it behaves: I'm explicitly inserting NULL into the column. In the previous example, it didn't put 'some value' in there, but left it NULL. How about now?
SQL> insert into test (id, col) values (5, null);
1 row created.
SQL> select * From test;
ID COL
---------- ----------
1 x
2 Littlefoot
3 x
4 some value
5 some value
Nice; we have 'some value' in the column.
Now you have some more info about the issue; see if it helps.
As Littlefoot mentioned, If you explicitly put NULL into a column, it won't get the default value.
If no value is mentioned for the column in the insert query, it uses DEFAULT. But, an explicit NULL overrides the default expression.
For 12c and above you can use the DEFAULT ON NULL option.
For prior versions, only way as far as I can tell is to replicate that functionality through a TRIGGER
CREATE TABLE YOURTABLE ( yourcolumn VARCHAR(100) );
CREATE OR REPLACE TRIGGER trg_mod_yourtabcol BEFORE
INSERT ON yourtable
FOR EACH ROW
WHEN ( new.yourcolumn IS NULL )
BEGIN
:new.yourcolumn := 'SOME DEFAULT VALUE';
END;
/
INSERT INTO YOURTABLE(yourcolumn) VALUES(NULL);
select * from YOURTABLE;
Table YOURTABLE created.
Trigger TRG_MOD_YOURTABCOL compiled
1 row inserted.
YOURCOLUMN
----------------------------------------------------------------------------------------------------
SOME DEFAULT VALUE
1 row selected.

delete row by id

I'm in the middle of creating a tool similar to the SQL Developer table data viewer. My db is Oracle based.
I simply need to delete eg.: 'row number 3' from a SELECT result. That table doesn't have any PK nor unique records. I've tried various techniques with ROWNUM etc. but no luck.
Oracle has a ROWID pseudocolumn that you can use for this purpose in simple cases.
select rowid, ... from your_table where ... ;
delete from your_table where rowid = <what you got above>;
If your interface allows the user to make complex views/joins/aggregates, then knowing what the user intended to delete (so knowing what set of rowids to gather and what set of tables to delete from) is going to be tricky.
Warning: rowids are unique only within a given table, and, quoting the above documentation:
If you delete a row, then Oracle may reassign its rowid to a new row inserted later.
So be very, very careful if you do this.
Assuming that it is a standard heap-organized table (index-organized tables and clusters potentially introduce additional complexity), if you don't have any other way to identify a row, you can use the ROWID pseudocolumn. This gives you information about the physical location of a row on disk. This means that the ROWID for a particular row can change over time and the ROWID can and will be reused when you delete a row and then a subsequent INSERT operation inserts a new row that happens to be in the same physical location on disk. For most applications, it is reasonable to assume that the ROWID will remain constant between the time that you execute the query and the time that you issue the DELETE but you shouldn't try to store the ROWID for any period of time.
For example, if we create a simple two-column table and a few rows
SQL> create table foo( col1 number, col2 varchar2(10) );
Table created.
SQL> insert into foo values( 1, 'Justin' );
1 row created.
SQL> insert into foo values( 1, 'Justin' );
1 row created.
SQL> insert into foo values( 2, 'Bob' );
1 row created.
SQL> insert into foo values( 2, 'Charlie' );
1 row created.
SQL> commit;
Commit complete.
We can SELECT the ROWID and then DELETE the third row using the ROWID
SQL> select *
2 from foo;
COL1 COL2
---------- ----------
1 Justin
1 Justin
2 Bob
2 Charlie
SQL> select rowid, col1, col2
2 from foo;
ROWID COL1 COL2
------------------ ---------- ----------
AAAfKXAAEAABt7vAAA 1 Justin
AAAfKXAAEAABt7vAAB 1 Justin
AAAfKXAAEAABt7vAAC 2 Bob
AAAfKXAAEAABt7vAAD 2 Charlie
SQL> delete from foo where rowid = 'AAAfKXAAEAABt7vAAC';
1 row deleted.
SQL> select * from foo;
COL1 COL2
---------- ----------
1 Justin
1 Justin
2 Charlie
Try using ROWID instead of ROWNUM.

Oracle: how to drop a subpartition of a specific partition

I am using an oracle 11 table with interval partitioning and list subpartitioning like this (simplified):
CREATE TABLE LOG
(
ID NUMBER(15, 0) NOT NULL PRIMARY KEY
, MSG_TIME DATE NOT NULL
, MSG_NR VARCHAR2(16 BYTE)
) PARTITION BY RANGE (MSG_TIME) INTERVAL (NUMTOYMINTERVAL (1,'MONTH'))
SUBPARTITION BY LIST (MSG_NR)
SUBPARTITION TEMPLATE (
SUBPARTITION login VALUES ('FOO')
, SUBPARTITION others VALUES (DEFAULT)
)
(PARTITION oldvalues VALUES LESS THAN (TO_DATE('01-01-2010','DD-MM-YYYY')));
How do I drop a specific subpartitition for a specific month without knowing the (system generated) name of the subpartition? There is a syntax "alter table ... drop subpartition for (subpartition_key_value , ...)" but I don't see a way to specify the month for which I am deleting the subpartition. The partition administration guide does not give any examples, either. 8-}
You can use the metadata tables to get the specific subpartition name:
SQL> insert into log values (1, sysdate, 'FOO');
1 row(s) inserted.
SQL> SELECT p.partition_name, s.subpartition_name, p.high_value, s.high_value
2 FROM user_tab_partitions p
3 JOIN
4 user_tab_subpartitions s
5 ON s.table_name = p.table_name
6 AND s.partition_name = p.partition_name
7 AND p.table_name = 'LOG';
PARTITION_NAME SUBPARTITION_NAME HIGH_VALUE HIGH_VALUE
--------------- ------------------ ------------ ----------
OLDVALUES OLDVALUES_OTHERS 2010-01-01 DEFAULT
OLDVALUES OLDVALUES_LOGIN 2010-01-01 'FOO'
SYS_P469754 SYS_SUBP469753 2012-10-01 DEFAULT
SYS_P469754 SYS_SUBP469752 2012-10-01 'FOO'
SQL> alter table log drop subpartition SYS_SUBP469752;
Table altered.
If you want to drop a partition dynamically, it can be tricky to find it with the ALL_TAB_SUBPARTITIONS view because the HIGH_VALUE column may not be simple to query. In that case you could use DBMS_ROWID to find the subpartition object_id of a given row:
SQL> insert into log values (4, sysdate, 'FOO');
1 row(s) inserted.
SQL> DECLARE
2 l_rowid_in ROWID;
3 l_rowid_type NUMBER;
4 l_object_number NUMBER;
5 l_relative_fno NUMBER;
6 l_block_number NUMBER;
7 l_row_number NUMBER;
8 BEGIN
9 SELECT rowid INTO l_rowid_in FROM log WHERE id = 4;
10 dbms_rowid.rowid_info(rowid_in =>l_rowid_in ,
11 rowid_type =>l_rowid_type ,
12 object_number =>l_object_number,
13 relative_fno =>l_relative_fno ,
14 block_number =>l_block_number ,
15 row_number =>l_row_number );
16 dbms_output.put_line('object_number ='||l_object_number);
17 END;
18 /
object_number =15838049
SQL> select object_name, subobject_name, object_type
2 from all_objects where object_id = '15838049';
OBJECT_NAME SUBOBJECT_NAME OBJECT_TYPE
--------------- --------------- ------------------
LOG SYS_SUBP469757 TABLE SUBPARTITION
As it turns out, the "subpartition for" syntax does indeed work, though that seems to be a secret Oracle does not want to tell you about. :-)
ALTER TABLE TB_LOG_MESSAGE DROP SUBPARTITION FOR
(TO_DATE('01.02.2010','DD.MM.YYYY'), 'FOO')
This deletes the subpartition that would contain MSG_TIME 2010/02/01 and MSG_NR FOO. (It is not necessary that there is an actual row with this exact MSG_TIME and MSG_NR. It throws an error if there is no such subpartition, though.)
Thanks for the post - it was very useful for me.
One observation though on the above script to identify the partition and delete it:
The object_id returned by dbms_rowid.rowid_info is not the object_id of the all_objects table. It is actually the data_object_id. It is observed that usually these ids match. However, after truncating the partitioned table several times, these ids diverged in my database. Hence it might be reasonable to instead use the data_object_id to find out the name of the partition:
select object_name, subobject_name, object_type
from all_objects where data_object_id = '15838049';
From the table description of ALL_OBJECTS:
OBJECT_ID Object number of the object
DATA_OBJECT_ID Object number of the segment which contains the object
http://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_rowid.htm
In the sample code provided in the above link, DBMS_ROWID.ROWID_OBJECT(row_id) is used instead to derive the same information that is given by dbms_rowid.rowid_info. However, the documentation around this sample mentions that it is a data object number from the ROWID.
Examples
This example returns the ROWID for a row in the EMP table, extracts
the data object number from the ROWID, using the ROWID_OBJECT function
in the DBMS_ROWID package, then displays the object number:
DECLARE object_no INTEGER; row_id ROWID; ... BEGIN
SELECT ROWID INTO row_id FROM emp
WHERE empno = 7499; object_no := DBMS_ROWID.ROWID_OBJECT(row_id); DBMS_OUTPUT.PUT_LINE('The obj. # is
'|| object_no); ...

Resources