Alter multiple tables' columns length - sql

So, we just found out that 254 tables in our Oracle DBMS have one column named "Foo" with the wrong length- Number(10) instead of Number(3).
That foo column is a part from the PK of the tables.
Those tables have other tables with forigen keys to it.
What I did is:
backed-up the table with a temp table.
Disabled the forigen keys to the table.
Disabled the PK with the foo column.
Nulled the foo column for all the rows.
Restored all the above
But now we found out it's not just couple of tables but 254 tables.
Is there an easy way, (or at least easier than this) to alter the columns length?
P.S. I have DBA permissions.

There's an easier way to generate the scripts that you want, use the system tables user_tables and user_constraints to dynamically generate the DDL. The downside is that this requires downtime. Also note that I use the truncate command rather than delete, which should be faster.
Assuming a simple table that looks like:
create table a (
foo number(10)
, bar number(10)
, constraint pk_a primary key (foo)
, constraint fk_a foreign key ( bar ) references a(foo )
);
This unlovely looking query
select cmd
from (
select table_name
, 1 as stage -- Just used to order by at the end.
, 'create table ' || table_name || '_backup as select * from '
|| table_name || ';' || chr(10) as cmd
-- chr(10) is LF
from user_tab_columns -- View of all columns
where column_name = 'FOO'
and data_precision = 10 -- Length of the number
union all
select table_name
, 3 as stage
, 'truncate table ' || table_name || ';' || chr(10) -- Remove all data
|| 'alter table ' || table_name
|| ' modify ( foo number(3));' || chr(10)
|| 'insert into ' || table_name || ' select * from '
|| table_name || '_backup;' || chr(10)
|| 'drop table ' || table_name || '_backup;' as cmd
from user_tab_columns
where column_name = 'FOO'
and data_precision = 10
union all
select ut.table_name
, 2 as stage
-- Disable the constraint
, 'alter table ' || uc.table_name || ' disable constraint '
|| uc.constraint_name || ';' || chr(10) as cmd
from user_constraints uc -- All named constraints
join user_tab_columns ut
on uc.table_name = ut.table_name
where ut.column_name = 'FOO'
and ut.data_precision = 10
and constraint_type = 'R' -- Foreign Key constraints (see link)
union all
select ut.table_name
, 4 as stage
, 'alter table ' || uc.table_name || ' enable constraint '
|| uc.constraint_name || ';' || chr(10) as cmd
from user_constraints uc
join user_tab_columns ut
on uc.table_name = ut.table_name
where ut.column_name = 'FOO'
and ut.data_precision = 10
and constraint_type = 'R'
)
order by stage
Will produce the following:
create table A_backup as select * from A; -- Create your backup
alter table A disable constraint FK_A; -- Disable FKs
truncate table A; -- Remove all data in the table
alter table A modify ( foo number(3)); -- Reduce the size of the column
insert into A select * from A_backup; -- Replace all the data
drop table A_backup; -- Drop the backup
alter table A enable constraint FK_A; -- Re-enable FKs
Due to the column stage, this won't be done table by table but stage by stage so that all the constraints will be disabled at the same time, which will avoid problems. If you're scared (I would be) then remove the drop of the _backup tables from the query; this means that whatever goes wrong you're safe.
If you're running this in SQL*Plus you also want to include whenever sqlerror exit so that if there's a problem, for instance no more tablespace, you don't truncate things that you haven't backed-up. It might almost be worth running it stage by stage so that you know that everything has completed correctly.
I would suggest testing this on a different user with a few tables to ensure that it does everything you need.

What we have done is:
CREATE TABLE <table_name_backup> as SELECT * <table_name>;
DELETE <table_name>;
ALTER TABLE <table_name> MODIFY (Foo NUMBER(3));
INSERT INTO <table_name> SELECT * FROM <table_name_backup>;
DROP <table_name_backup>;
For all the tables.

Your solution works but is a lot of work and implies downtime.
Since physically a NUMBER(3) is exactly like a NUMBER(10) with a stronger constraint you could add CHECK constraints and get the same logical restriction without downtime:
LOOP
ALTER TABLE <table_name> ADD CONSTRAINT <table_foo_chk> CHECK (foo < 1000);
END LOOP;

Related

Create Primary Constraint from table columns

I have a table named PRIMARY_CONSTRAINTS into the DB such as it has the table name, the constraint name and the column name of the primary constraints which I want to add.
It is an SQL oracle DB.
I have tried something like this but it does not run
ALTER TABLE TABLE_NAME
ADD CONSTRAINT CONSTRAINT_NAME PRIMARY KEY (COLUMN_NAME)
WHERE TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME IN
( SELECT TABLE_NAME,
CONSTRAINT_NAME,
COLUMN_NAME
FROM PRIMARY_CONSTRAINTS );
How can I reference it so I can alter the tables?
Thanks
You can use the following dynamic block to achieve the same:
BEGIN
FOR Q IN (
SELECT
'ALTER TABLE '
|| TABLE_NAME
|| ' ADD CONSTRAINT '
|| CONSTRAINT_NAME
|| ' PRIMARY KEY ( '
|| COLUMN_NAME
|| ')' QRY
FROM
PRIMARY_CONSTRAINTS
) LOOP
EXECUTE IMMEDIATE Q.QRY;
END LOOP;
END;
/
I have created the small demo for you --> db<>fiddle demo
UPDATED ACCORDING TO COMMENT
If there are two entries for a single table then You can use the following block:
BEGIN
FOR Q IN (
SELECT
'ALTER TABLE '
|| TABLE_NAME
|| ' ADD CONSTRAINT '
|| MAX(CONSTRAINT_NAME)
|| ' PRIMARY KEY ( '
|| LISTAGG(COLUMN_NAME,',') WITHIN GROUP (ORDER BY NULL)
|| ')' QRY
FROM
PRIMARY_CONSTRAINTS
GROUP BY TABLE_NAME
) LOOP
EXECUTE IMMEDIATE Q.QRY;
END LOOP;
END;
/
db<>fiddle demo updated
With regard to your comment about multiple columns on the constraints, we could setup the data in PRIMARY_CONSTRAINTS table in the same way it would be used in the Alter Table Statement.
For Example : Suppose we have to use the following statement to create a constraint Alter table add constraint Constraint_Name primary key columnA, columnB;
Then the entry for this particular constraint in your PRIMARY_CONSTRAINTS table will have value columnA , columnB for COLUMN_NAME.
Hope this helps.
Regards
Akash

Oracle Pl/SQL alter table columns

I have created a table2 with SQL query: create table2 as select * from table1
Create table as select ... doesn't retain all column properties from table1 in table2.
Is it possible to generate an SQL file to ALTER table2 column properties (DATA_LENGTH,DATA_PRECISION,DATA_SCALE,DEFAULT_LENGTH,DATA_DEFAULT) with all column properties from table1?
Thanks!
--drop table table2;
create table table1 (
x number(10, 2) default 10,
y varchar2(200) default 'NA'
);
create table table2 as select * from table1;
Let us try for data_default:
create table search_user_tab_columns as
select table_name, column_name, to_lob(DATA_DEFAULT) dd
from user_tab_columns
where table_name = 'TABLE1';
As data_default is a long data in user_tab_columns we have to create an intermediate
search_user_tab_columns (dummy) :-(
select 'ALTER TABLE "' || table_name || '" MODIFY "' || column_name || '" DEFAULT ' || Dd || ';'
from search_user_tab_columns
where table_name = 'TABLE1';
Note:
SELECT DBMS_METADATA.GET_DDL('TABLE','<TABLE_NAME>','<SCHEMA_NAME>') from dual;
This is better way of doing this if creating a table from an existing table . But sometimes you may try this for specific purposes.
If you only need to get the properties.
SELECT DBMS_METADATA.GET_DDL('TABLE','<TABLE_NAME>','<SCHEMA_NAME>') from dual;
try this:
you can see all properties
select column_name as "Name"
, nullable as "Null?"
, concat(concat(concat(data_type,'('),data_length),')') as "Type"
from user_tab_columns
where table_name = 'MY_TABLE';

How to rename a default constraint on Oracle?

I want to rename many constraints (PK, FK, ...etc. ) that have default names which start with 'SYS' to be able to insert the same data in other DB.
I found the following script that I changed to get what I want:
BEGIN
FOR cn IN (
SELECT constraint_name
FROM user_constraints
WHERE constraint_type = 'P'
AND table_name = 'SPECIALITE'
)
LOOP
EXECUTE IMMEDIATE 'ALTER TABLE ' || cn.table_name || ' RENAME CONSTRAINT ' || cn.constraint_name || ' TO PK_' || 'SPECIALITE';
END LOOP;
END;
This script works, but it seems a bit complicated for me, I wonder if it exists something like:
ALTER TABLE 'SPECIALITE' RENAME CONSTRANT (....)
The problem is I don't know the name of constraints, they have a default name, I know only tables where they are.
Is it possible?
As you already know, you need to run two queries.
select constraint_name from user_constraints
where table_name = <table_name> and constraint_type = 'P';
and with the constraint name in hand,
alter table <table_name> rename constraint <old_constr_name> to <new_constr_name>;
This will require copying the constraint name from the first query and pasting it into the second, in the proper place (<old_constr_name>).
If this was all, I wouldn't post an answer. But I remember something I read on AskTom some time ago - a clever way to avoid copying and pasting, using the COLUMN command in SQL*Plus. (This may also work in SQL Developer and Toad.) Something like this:
column constraint_name new_val c -- Note: no semicolon - this is SQL*Plus, not SQL
select constraint_name from user_constraints
where table_name = <table_name> and constraint_type = 'P';
alter table <table_name> rename constraint &c to <new_constr_name>;
If you need to change many PK constraint names, this will save some work. The constraint name returned by the SELECT query is saved in the "new_val" labeled "c" from the SQL*Plus COLUMN command, and it is used in the ALTER TABLE statement.
Add user_cons_columns view to your SQL so you can generate constraint names. Here is a quick and dirty example:
select 'ALTER TABLE ' || c.table_name || ' RENAME CONSTRAINT ' || c.constraint_name || ' TO ' || substr(c.constraint_type || '_' || c.table_name || '_' || replace(wm_concat(cc.column_name), ',', '_'), 0, 30) || ';'
from user_constraints c
join user_cons_columns cc on c.table_name = cc.table_name and c.constraint_name = cc.constraint_name
where c.constraint_name like 'SYS%'
group by c.table_name, c.constraint_name, c.constraint_type;
This SQL generates an executable script with commands like this:
ALTER TABLE TABLENAME RENAME CONSTRAINT SYS_xxxxxx TO C_TABLENAME_COLUMN;
Please note that I'm using undocumented wm_concat function. If you're using Oracle >= 11g consider using listagg instead.
If you don't have the constraint name, you can't do it directly. But you can easily generate the statement.
select 'ALTER TABLE ' || table_name || ' RENAME CONSTRAINT ' || constraint_name || ' TO PK_' || upper( table_name )
from user_constraints
where constraint_type = 'P'
and constraint_name like 'SYS%';
This select will list all tables with a constraint on its primary key that start by 'SYS'and rename it to PK_TABLE_NAME;
You just have to check if all statements looks ok for you and run them.
You also can use the generated column like this
select 'ALTER TABLE ' || table_name || ' RENAME CONSTRAINT ' || constraint_name || ' TO PK_' || upper( table_name )
from user_constraints
where constraint_type = 'P'
and generated = 'GENERATED NAME';
but you will have BIN tables and maybe other unwanted tables

Dynamic query with :new value

I have a table called PKCHANGES that has a few columns, one of which is the primary_key column. What I want is to create a trigger on other tables, and upon an insert I grab some values and post them to the PKCHANGES table. All is fine except for when I try and post the primary key values. I want that in the column primary_key I input the primary key values comma delimited. So if TableX has 3 primary keys, in PKCHANGES (primary_key column) I post value1,value2,value3.
So far I am only managing to get the below as a result and not the actual values
":new.pkCol1:new.pkCol2:new.pkCol3"
My pl/sql block is:
DECLARE
mySql varchar2(5000);
myTable varchar2(10) := 'TableX';
BEGIN
mySql := 'CREATE OR REPLACE TRIGGER ' || 't_1' || ' AFTER INSERT ON ' || myTable || '
FOR EACH ROW
DECLARE
currentPK varchar2(200); --Contains the current primary key value in the loop
result varchar2(200); --Contains the appended string of primary key values
--Cursor that contains primaryKeys for table
CURSOR pks IS
SELECT cols.column_name FROM all_constraints cons, all_cons_columns cols
WHERE cons.constraint_type = ''P''
AND cons.constraint_name = cols.constraint_name
AND cons.table_name = ' || '''' || myTable || '''' || ';
BEGIN
--Loop through primary keys, get the value from the trigger, and append the string.
for current_pk IN pks LOOP
BEGIN
currentPK := '':new.'' || current_pk.column_name;
result:= result || currentPK;
END;
END LOOP;'
||
' --Insert the appended values into the primary_key column
INSERT INTO PKCHANGES(primary_key)' ||
'VALUES (result);'
|| ' END;';
dbms_output.put_line(mySql);
EXECUTE IMMEDIATE mySql;
END;
Any idea?
The primary key of the TableX need not be queried on each insert. It is stable and if once changed, you will change the trigger as well.
This allows you to pop up the logic out of the trigger.
In the first step concatenate the PK. I'd prefer LISTAGG as it handels elegant the delimiter. You get something like :new.COL1||','||:new.COL2||','||:new.COL3
Also make sure the table name is in correct case (I asume upper case; otherwise you need to quote the name).
In the next step generate the trigger, that will basicaly contain only the INSERT
DECLARE
mySql varchar2(5000);
myTable varchar2(10) := 'TableX';
result varchar2(200); -- Contains the concatenated string of primary key column names with delimiters,
-- e.g. ":new.COL1||','||:new.COL2||','||:new.COL3"
BEGIN
SELECT listagg(':new.'||cols.column_name,'||'',''||') within group (order by position) into result
FROM all_constraints cons, all_cons_columns cols
WHERE cons.constraint_type = 'P'
AND cons.constraint_name = cols.constraint_name
AND cons.table_name = upper(myTable);
mySql := 'CREATE OR REPLACE TRIGGER ' || 't_1' || ' AFTER INSERT ON ' || myTable || '
FOR EACH ROW
BEGIN
--Insert the appended values into the primary_key column
INSERT INTO PKCHANGES(primary_key)' ||
'VALUES ('||result||');'
|| ' END;';
dbms_output.put_line(mySql);
EXECUTE IMMEDIATE mySql;
END;
/
Test
create table TableX
(col1 number,
col2 number,
col3 number,
col4 number);
alter table TableX add (primary key (col1, col2, col3));
insert into TableX values (1,2,3,4);
select * from PKCHANGES;
PRIMARY_KEY
-----------
1,2,3

Disable all table constraints in Oracle

How can I disable all table constrains in Oracle with a single command?
This can be either for a single table, a list of tables, or for all tables.
It is better to avoid writing out temporary spool files. Use a PL/SQL block. You can run this from SQL*Plus or put this thing into a package or procedure. The join to USER_TABLES is there to avoid view constraints.
It's unlikely that you really want to disable all constraints (including NOT NULL, primary keys, etc). You should think about putting constraint_type in the WHERE clause.
BEGIN
FOR c IN
(SELECT c.owner, c.table_name, c.constraint_name
FROM user_constraints c, user_tables t
WHERE c.table_name = t.table_name
AND c.status = 'ENABLED'
AND NOT (t.iot_type IS NOT NULL AND c.constraint_type = 'P')
ORDER BY c.constraint_type DESC)
LOOP
dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" disable constraint ' || c.constraint_name);
END LOOP;
END;
/
Enabling the constraints again is a bit tricker - you need to enable primary key constraints before you can reference them in a foreign key constraint. This can be done using an ORDER BY on constraint_type. 'P' = primary key, 'R' = foreign key.
BEGIN
FOR c IN
(SELECT c.owner, c.table_name, c.constraint_name
FROM user_constraints c, user_tables t
WHERE c.table_name = t.table_name
AND c.status = 'DISABLED'
ORDER BY c.constraint_type)
LOOP
dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" enable constraint ' || c.constraint_name);
END LOOP;
END;
/
To take in count the dependencies between the constraints:
SET Serveroutput ON
BEGIN
FOR c IN
(SELECT c.owner,c.table_name,c.constraint_name
FROM user_constraints c,user_tables t
WHERE c.table_name=t.table_name
AND c.status='ENABLED'
ORDER BY c.constraint_type DESC,c.last_change DESC
)
LOOP
FOR D IN
(SELECT P.Table_Name Parent_Table,C1.Table_Name Child_Table,C1.Owner,P.Constraint_Name Parent_Constraint,
c1.constraint_name Child_Constraint
FROM user_constraints p
JOIN user_constraints c1 ON(p.constraint_name=c1.r_constraint_name)
WHERE(p.constraint_type='P'
OR p.constraint_type='U')
AND c1.constraint_type='R'
AND p.table_name=UPPER(c.table_name)
)
LOOP
dbms_output.put_line('. Disable the constraint ' || d.Child_Constraint ||' (on table '||d.owner || '.' ||
d.Child_Table || ')') ;
dbms_utility.exec_ddl_statement('alter table ' || d.owner || '.' ||d.Child_Table || ' disable constraint ' ||
d.Child_Constraint) ;
END LOOP;
END LOOP;
END;
/
It's not a single command, but here's how I do it. The following script has been designed to run in SQL*Plus. Note, I've purposely written this to only work within the current schema.
set heading off
spool drop_constraints.out
select
'alter table ' ||
owner || '.' ||
table_name ||
' disable constraint ' || -- or 'drop' if you want to permanently remove
constraint_name || ';'
from
user_constraints;
spool off
set heading on
#drop_constraints.out
To restrict what you drop, filter add a where clause to the select statement:-
filter on constraint_type to drop only particular types of constraints
filter on table_name to do it only for one or a few tables.
To run on more than the current schema, modify the select statement to select from all_constraints rather than user_constraints.
Note - for some reason I can't get the underscore to NOT act like an italicization in the previous paragraph. If someone knows how to fix it, please feel free to edit this answer.
Use following cursor to disable all constraint.. And alter query for enable constraints...
DECLARE
cursor r1 is select * from user_constraints;
cursor r2 is select * from user_tables;
BEGIN
FOR c1 IN r1
loop
for c2 in r2
loop
if c1.table_name = c2.table_name and c1.status = 'ENABLED' THEN
dbms_utility.exec_ddl_statement('alter table ' || c1.owner || '.' || c1.table_name || ' disable constraint ' || c1.constraint_name);
end if;
end loop;
END LOOP;
END;
/
This can be scripted in PL/SQL pretty simply based on the DBA/ALL/USER_CONSTRAINTS system view, but various details make not as trivial as it sounds. You have to be careful about the order in which it is done and you also have to take account of the presence of unique indexes.
The order is important because you cannot drop a unique or primary key that is referenced by a foreign key, and there could be foreign keys on tables in other schemas that reference primary keys in your own, so unless you have ALTER ANY TABLE privilege then you cannot drop those PKs and UKs. Also you cannot switch a unique index to being a non-unique index so you have to drop it in order to drop the constraint (for this reason it's almost always better to implement unique constraints as a "real" constraint that is supported by a non-unique index).
SELECT 'ALTER TABLE '||substr(c.table_name,1,35)||
' DISABLE CONSTRAINT '||constraint_name||' ;'
FROM user_constraints c, user_tables u
WHERE c.table_name = u.table_name;
This statement returns the commands which turn off all the constraints including primary key, foreign keys, and another constraints.
It doesn't look like you can do this with a single command, but here's the closest thing to it that I could find.
This is another way for disabling constraints (it came from https://asktom.oracle.com/pls/asktom/f?p=100:11:2402577774283132::::P11_QUESTION_ID:399218963817)
WITH qry0 AS
(SELECT 'ALTER TABLE '
|| child_tname
|| ' DISABLE CONSTRAINT '
|| child_cons_name
disable_fk
, 'ALTER TABLE '
|| parent_tname
|| ' DISABLE CONSTRAINT '
|| parent.parent_cons_name
disable_pk
FROM (SELECT a.table_name child_tname
,a.constraint_name child_cons_name
,b.r_constraint_name parent_cons_name
,LISTAGG ( column_name, ',') WITHIN GROUP (ORDER BY position) child_columns
FROM user_cons_columns a
,user_constraints b
WHERE a.constraint_name = b.constraint_name AND b.constraint_type = 'R'
GROUP BY a.table_name, a.constraint_name
,b.r_constraint_name) child
,(SELECT a.constraint_name parent_cons_name
,a.table_name parent_tname
,LISTAGG ( column_name, ',') WITHIN GROUP (ORDER BY position) parent_columns
FROM user_cons_columns a
,user_constraints b
WHERE a.constraint_name = b.constraint_name AND b.constraint_type IN ('P', 'U')
GROUP BY a.table_name, a.constraint_name) parent
WHERE child.parent_cons_name = parent.parent_cons_name
AND (parent.parent_tname LIKE 'V2_%' OR child.child_tname LIKE 'V2_%'))
SELECT DISTINCT disable_pk
FROM qry0
UNION
SELECT DISTINCT disable_fk
FROM qry0;
works like a charm
In the "disable" script, the order by clause should be that:
ORDER BY c.constraint_type DESC, c.last_change DESC
The goal of this clause is disable the constraints in the right order.
with cursor for loop (user = 'TRANEE', table = 'D')
declare
constr all_constraints.constraint_name%TYPE;
begin
for constr in
(select constraint_name from all_constraints
where table_name = 'D'
and owner = 'TRANEE')
loop
execute immediate 'alter table D disable constraint '||constr.constraint_name;
end loop;
end;
/
(If you change disable to enable, you can make all constraints enable)
You can execute all the commands returned by the following query :
select 'ALTER TABLE '||substr(c.table_name,1,35)||
' DISABLE CONSTRAINT '||constraint_name||' ;'
from user_constraints c
--where c.table_name = 'TABLE_NAME' ;