Oracle 11g online redefinition table to view - oracle

This may be impossible, but I'd hoped to see if there is an approachable way to run an online replacement of a table with a view.
For online table restructuring like partitioning, etc. DBMS_REDEFINITION works great. But I'd like to replace a table with a (materialized) view, so DBMS_REDEFINITION appears to be unsuitable.
I have no constraints, dependencies or mutating dml, etc. to worry about during the rename; I would only like to keep the target SELECTable when replacing the table with a view. A trumped-up example is below.
CREATE TABLE SCI_FI_MOVIE (
SCI_FI_MOVIE_ID NUMBER(10, 0) NOT NULL PRIMARY KEY,
NAME VARCHAR2(100) UNIQUE NOT NULL,
DIRECTOR VARCHAR2(100) NOT NULL,
REVIEW_SCORE NUMBER(1, 0) CHECK ( REVIEW_SCORE IN (1, 2, 3, 4, 5))
);
CREATE TABLE NO_SCORES_SCI_FI_MOVIE (
SCI_FI_MOVIE_ID NUMBER(10, 0) NOT NULL PRIMARY KEY,
NAME VARCHAR2(100) UNIQUE NOT NULL,
DIRECTOR VARCHAR2(100) NOT NULL
);
CREATE MATERIALIZED VIEW KUBRICK_SABOTAGE
(SCI_FI_MOVIE_ID, NAME, DIRECTOR, REVIEW_SCORE)
REFRESH COMPLETE ON COMMIT
AS
SELECT
SCI_FI_MOVIE_ID,
NAME,
DIRECTOR,
CASE WHEN DIRECTOR = 'KUBRICK'
THEN 5
ELSE 2 END AS REVIEW_SCORE
FROM NO_SCORES_SCI_FI_MOVIE;
INSERT INTO SCI_FI_MOVIE VALUES (1, 'Apollo 13', 'HOWARD', 5);
INSERT INTO SCI_FI_MOVIE VALUES (2, '2001: A Space Odyssey', 'KUBRICK', 4);
INSERT INTO NO_SCORES_SCI_FI_MOVIE VALUES (1, 'Apollo 13', 'HOWARD');
INSERT INTO NO_SCORES_SCI_FI_MOVIE VALUES (2, '2001: A Space Odyssey', 'KUBRICK');
COMMIT;
-- THEN WHAT STEPS TO REPLACE TABLE WITH VIEW?
In this example I'd like to end up with the MV named SCI_FI_MOVIE and the TABLE SCI_FI_MOVIE renamed to SCI_FI_MOVIE_TEMP or whatever pending its removal. There isn't any requirement for the MV to exist prior to replacing the original table, if the replacement can be done atomically
I'd like to avoid any interruption or compromise to object name resolution (CREATE PUBLIC SYNONYM then renaming original won't work here)
Is there a clean no-downtime way to do this?
I am free to disable logging, read-only etc. anything as needed; the only goal is to prevent "ORA-00942: table or view does not exist" during the dictionary switch. I'm on 11gR2 but would welcome in 12c solutions as well.
Thanks so much for your thoughts

You don't need to do on-line redefinition or any clever; what you're trying to do is built in via the ON PREBUILT TABLE clause:
The ON PREBUILT TABLE clause lets you register an existing table as a preinitialized materialized view. This clause is particularly useful for registering large materialized views in a data warehousing environment. The table must have the same name and be in the same schema as the resulting materialized view.
If the materialized view is dropped, then the preexisting table reverts to its identity as a table.
So you can just do:
CREATE MATERIALIZED VIEW SCI_FI_MOVIE
(SCI_FI_MOVIE_ID, NAME, DIRECTOR, REVIEW_SCORE)
ON PREBUILT TABLE
REFRESH COMPLETE ON COMMIT
AS
SELECT
SCI_FI_MOVIE_ID,
NAME,
DIRECTOR,
CAST(CASE WHEN DIRECTOR = 'KUBRICK'
THEN 5
ELSE 2 END AS NUMBER(1,0)) AS REVIEW_SCORE
FROM NO_SCORES_SCI_FI_MOVIE;
Materialized view SCI_FI_MOVIE created.
The CAST(... AS NUMBER(1,0)) is needed to make the generated data type match the underlying table.
The table is locked while the MV is built (which is pretty much instant anyway as there is no data collection or creation) so queries against it while that is happening will just block briefly.
The view will have the original table values:
select * from SCI_FI_MOVIE;
SCI_FI_MOVIE_ID NAME DIRECTOR REVIEW_SCORE
--------------- ------------------------------ ---------- ------------
1 Apollo 13 HOWARD 5
2 2001: A Space Odyssey KUBRICK 4
... until it's refreshed, which (in this example) will be on next commit:
INSERT INTO NO_SCORES_SCI_FI_MOVIE VALUES (3, 'Star Wars', 'LUCAS');
1 row inserted.
COMMIT;
select * from SCI_FI_MOVIE;
SCI_FI_MOVIE_ID NAME DIRECTOR REVIEW_SCORE
--------------- ------------------------------ ---------- ------------
1 Apollo 13 HOWARD 2
2 2001: A Space Odyssey KUBRICK 5
3 Star Wars LUCAS 2
For a normal (non-materialized view) you could do a bit of shuffling to achieve the same thing, as long as you can create a public synonym:
CREATE TABLE SCI_FI_MOVIE_TMP_TAB AS SELECT * FROM SCI_FI_MOVIE;
CREATE VIEW SCI_FI_MOVIE_TMP_VIEW AS SELECT * FROM SCI_FI_MOVIE_TMP_TAB;
CREATE PUBLIC SYNONYM SCI_FI_MOVIE FOR SCI_FI_MOVIE_TMP_VIEW;
ALTER TABLE SCI_FI_MOVIE RENAME TO SCI_FI_MOVIE_OLD;
CREATE VIEW SCI_FI_MOVIE AS
SELECT
SCI_FI_MOVIE_ID,
NAME,
DIRECTOR,
CAST(CASE WHEN DIRECTOR = 'KUBRICK'
THEN 5
ELSE 2 END AS NUMBER(1,0)) AS REVIEW_SCORE
FROM NO_SCORES_SCI_FI_MOVIE;
DROP PUBLIC SYNONYM SCI_FI_MOVIE;
DROP VIEW SCI_FI_MOVIE_TMP_VIEW;
DROP TABLE SCI_FI_MOVIE_TMP_TAB;
DROP TABLE SCI_FI_MOVIE_OLD;
This relies on how Oracle resolves schema object references. When you rename the original table it can no longer find an object with that name in the current schema (namespace), and looks for a public synonym, and happily uses that. When the view is created that takes precedence over the public synonym again.

Related

Oracle DB. Insert Trigger with Merge statament inside. Table is mutating

I have two back-end systems (the old one and the new one) that shares an Oracle DB.
In the older system, to save customers data, there are two tables
customers_A
ID NAME ETC
1 PETE ....
customers_B
ID NAME ETC
1 JOSH ...
2 ROSS ...
In the new system I've created a new table called All_Costumer, to join those tables.
This new table contains customer ID's of type A and B respectively.
All_Customers
ID ID_CUSTOMER_A ID_CUSTOMER_B
A19E----D2B0 1 null
A19E----D2B1 null 1
A19E----D2B2 null 2
So, when the new system creates a new customer of type A, data are inserted on customer_A and All_Customers tables, with customer of type B as well.
Currently, the old system is working too, and when a new customer of type A is created, data is inserted only on customer_A table, but I need that data in All_Customers too.
To solve this, I've created a TRIGGER with a MERGE INTO statement inside, to insert a row in All_Customers if doesn't exist on this table (when a new customer of type A are created by the older system)
CREATE OR REPLACE TRIGGER customers_trg
AFTER INSERT
ON customer_A
FOR EACH ROW
DECLARE
variables that doesn't matters
BEGIN
MERGE INTO all_customers
USING (SELECT :new.id id FROM customer_A where id = :new.id) customer
ON (all_customers.id_customer_a = customer.id)
WHEN NOT MATCHED THEN
INSERT (id, id_customer_a)
VALUES (SYS_GUID(), :new.id, null);
COMMIT;
END;
But when I try to create a new customer from the older system, I get this error:
ORA-04091: table **customer_A** is mutating, trigger/function may not see it
Any idea to solve this?
I've tried adding PRAGMA AUTONOMOUS_TRANSACTION; on DECLARE section, but didn't work.
Note: I can't modify the old system
The immediate issue is that you're querying table_a in a trigger against that table; but you don't need to. Your merge query
SELECT :new.id id FROM customer_A where id = :new.id
can simply do
SELECT :new.id id FROM dual
i.e. the clause becomes:
...
USING (SELECT :new.id id FROM dual) customer
ON (all_customers.id_customer_a = customer.id)
...
You also can't commit in a trigger - unless it's autonomous, which this shouldn't be. You said you'd tried that, but it breaks if the insert is rolled back, since the merged row will still exist. So hopefully that commit is just a hang-over from trying and rejecting that approach.
But it works in this db<>fiddle, anyway.
If you weren't adding the GUID you could get the same effect with a view:
create or replace view all_customers (id_customer_a, id_customer_b) as
select id, null from customers_a
union all
select null, id from customers_b;
db<>fiddle

Oracle trigger to update a table when another table insert or update

I have two tables (master-detail) I use to record orders, I need to create a trigger that allows me to update the "TOTAL_GENERAL" field that is in the master table with the sum of subtotals in the "SUBTOTAL" field the detail table that are related to the foreign key "ID_ORDEN" but I get an error with the trigger.
tables:
CREATE TABLE "ENCABEZADO_ORDEN"
("ID_ENCABEZADO" NUMBER(10,0),
"NUMERO_ORDEN" NUMBER(10,0),
"FECHA" DATE,
"NOMBRE_CLIENTE" VARCHAR2(50),
"DIRECCION" VARCHAR2(50),
"TOTAL_GENERAL" NUMBER(10,0),
"LUGAR_VENTA" VARCHAR2(50),
CONSTRAINT "ENCABEZADO_ORDEN_PK" PRIMARY KEY ("ID_ENCABEZADO")
USING INDEX ENABLE
)
CREATE TABLE "DETALLE_ORDEN"
("ID_DETALLE" NUMBER(10,0),
"PRODUCTO" VARCHAR2(50),
"PRECIO_UNITARIO" NUMBER(10,2),
"CANTIDAD" NUMBER(10,0),
"SUBTOTAL" NUMBER(10,2),
"ID_ENCABEZADO" NUMBER(10,0),
CONSTRAINT "DETALLE_ORDEN_PK" PRIMARY KEY ("ID_DETALLE")
USING INDEX ENABLE
)
/
ALTER TABLE "DETALLE_ORDEN" ADD CONSTRAINT "DETALLE_ORDEN_FK" FOREIGN KEY ("ID_ENCABEZADO")
REFERENCES "ENCABEZADO_ORDEN" ("ID_ENCABEZADO") ENABLE
/
trigger:
create or replace TRIGGER "CALCULAR_TOTAL_GENERAL"
BEFORE INSERT OR UPDATE ON "DETALLE_ORDEN"
FOR EACH ROW
DECLARE
V_ID_ENCABEZADO NUMBER(10,0);
BEGIN
SELECT "ID_ENCABEZADO"
INTO V_ID_ENCABEZADO
FROM "ENCABEZADO_ORDEN"
WHERE "ID_ENCABEZADO" = :NEW."ID_ENCABEZADO";
UPDATE "ENCABEZADO_ORDEN"
SET "TOTAL_GENERAL" = (SELECT SUM("SUBTOTAL") FROM "DETALLE_ORDEN"
WHERE "ID_ENCABEZADO" = V_ID_ENCABEZADO)
WHERE "ID_ENCABEZADO" = V_ID_ENCABEZADO;
END;
This is the error message I get when I insert or update the table "DETALLE_ORDEN":
1 error has occurred
ORA-04091: table CARLOSM.DETALLE_ORDEN is mutating, trigger/function may not see it
ORA-06512: at "CARLOSM.CALCULAR_TOTAL_GENERAL", line 9
ORA-04088: error during execution of trigger 'CARLOSM.CALCULAR_TOTAL_GENERAL'
Don't use triggers for this kind of logic (for that matter, don't use triggers ever; there's almost always a better way). Also, avoid storing redundant information in base tables whenever possible.
Far better design, with minimal impact to existing code is to
1) rename table "ENCABEZADO_ORDEN" (i.e. to "ENCABEZADO_ORDEN_TAB") and 2) disable/drop "TOTAL_GENERAL" field, and then 3) create a view with original name "ENCABEZADO_ORDEN" as:
CREATE OR REPLACE VIEW ENCABEZADO_ORDEN AS
SELECT O.*, (SELECT SUM(D.SUBTOTAL) FROM DETALLE_ORDEN D
WHERE D.ID_ENCABEZADO = O.ID_ENCABEZADO) TOTAL_GENERAL
FROM ENCABEZADO_ORDEN_TAB O;
This will ensure TOTAL_GENERAL is always correct (in fact, any efforts to set it directly to some other value via update of ENCABEZADO_ORDEN will result in immediate syntax error).
If performance is an issue (i.e. users frequently query TOTAL_GENERAL field in ENCABEZADO_ORDEN table for orders with large numbers of detail records in DETALLE_ORDEN, causing Oracle to repeatedly fetch&sum multitudes of SUBTOTALS) then use a materialized view instead of a basic view.

Materialized view data doesn't update

I want to create a materialized view with fast refresh. The view aggregates values from a single table:
CREATE TABLE N_INSP_DTSEDIF_PLANTAS (
IMPORTACION_ID NUMBER(*,0) NOT NULL,
ID NUMBER(10) NOT NULL,
INSPECCION_ID NUMBER(10) NOT NULL,
NOMBRE_PLANTA VARCHAR2(255 CHAR),
NUM_VIVIENDAS NUMBER(10),
SUP_CONSTRUIDA_VIVIENDAS DECIMAL(10,4),
-- Plus some other columns I don't need
CONSTRAINT N_INSP_DTSEDIF_PLANTAS_P PRIMARY KEY (
IMPORTACION_ID,
ID
) ENABLE,
CONSTRAINT N_INSP_DTSEDIF_PLANTAS_F FOREIGN KEY (IMPORTACION_ID)
REFERENCES IMPORTACION (IMPORTACION_ID)
ON DELETE CASCADE
ENABLE
);
CREATE INDEX N_INSP_DTSEDIF_PLANTAS_X ON N_INSP_DTSEDIF_PLANTAS (IMPORTACION_ID);
CREATE SEQUENCE N_INSP_DTSEDIF_PLANTAS_S
INCREMENT BY 1
START WITH 1
MINVALUE 1
CACHE 20;
CREATE OR REPLACE TRIGGER N_INSP_DTSEDIF_PLANTAS_T
BEFORE INSERT
ON N_INSP_DTSEDIF_PLANTAS
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
IF :NEW.ID IS NULL THEN
SELECT N_INSP_DTSEDIF_PLANTAS_S.NEXTVAL INTO :NEW.ID FROM DUAL;
END IF;
END N_INSP_DTSEDIF_PLANTAS_T;
/
ALTER TRIGGER N_INSP_DTSEDIF_PLANTAS_T ENABLE;
I've composed this through trial and error:
CREATE MATERIALIZED VIEW LOG ON N_INSP_DTSEDIF_PLANTAS
WITH ROWID, SEQUENCE (IMPORTACION_ID, INSPECCION_ID, NUM_VIVIENDAS, SUP_CONSTRUIDA_VIVIENDAS)
INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW V_PLANTAS
REFRESH FAST
AS
SELECT IMPORTACION_ID, INSPECCION_ID,
SUM(NUM_VIVIENDAS) AS NUM_VIVIENDAS, SUM(SUP_CONSTRUIDA_VIVIENDAS) AS SUP_CONSTRUIDA_VIVIENDAS
FROM N_INSP_DTSEDIF_PLANTAS
GROUP BY IMPORTACION_ID, INSPECCION_ID;
Objects get created without errors and SELECT * FROM V_PLANTAS returns data. However, the view is stalled. New rows added to N_INSP_DTSEDIF_PLANTAS don't show up at V_PLANTAS.
What did I misunderstand from the documentation?
In the mess of random changes that follow panic and despair I inadvertently dropped the ON COMMIT clause:
CREATE MATERIALIZED VIEW V_PLANTAS
REFRESH FAST ON COMMIT
AS
-- ...
The log itself is also invalid for fast refresh because I also omitted the PRIMARY KEY clause. It should be like:
CREATE MATERIALIZED VIEW LOG ON N_INSP_DTSEDIF_PLANTAS
WITH ROWID, PRIMARY KEY, SEQUENCE (INSPECCION_ID, NUM_VIVIENDAS, SUP_CONSTRUIDA_VIVIENDAS)
INCLUDING NEW VALUES;
(Said that, it's worth noting that materialized tables are not just a simple results cache but a fairly large and complex feature that requires careful planning and maintenance. In many situations is easier to just optimize the underlying query.)

Does updating a view affects the base table?

When I updated a view which was created using a base table, the updation affected the base table as well. How is that possible? If view is considered as just a 'window' through which we can see a set of data of the base table then how can the base table change when I try to change the data inside a view.
You can make changes to the state of underlying table using the view as long as the you are targeting the change in single table.
View is a security layer on top of table object and allows most of the DML operation as long as you do not violet the base rule.
Example:
CREATE TABLE T1
(ID INT IDENTITY(1,1), [Value] NVARCHAR(50))
CREATE TABLE T2
(ID INT IDENTITY(1,1), [Value] NVARCHAR(50))
--Dummy Insert
INSERT INTO T1 VALUES ('TestT1')
INSERT INTO T2 VALUES ('TestT2')
--Create View
CREATE VIEW V1
AS
SELECT T1.ID AS T1ID, T2.ID AS T2ID, T1.Value AS T1Value, T2.Value AS T2Value FROM T1 INNER JOIN T2
ON T2.ID = T1.ID
--Check the result
SELECT * FROM V1
--Insert is possible via view as long as it affects only one table
INSERT INTO V1 (T1Value) VALUES
('TestT1_T1')
INSERT INTO V1 (T2Value) VALUES
('TestT2_T2')
--Change is possible only if target is only one table
UPDATE V1
SET T1Value = 'Changed'--**
WHERE T2ID = 1
--This is not allowed
INSERT INTO V1 (T1Value, T2Value) VALUES
('TestT1_T1','TestT2_T2')
--Msg 4405, Level 16, State 1, Line 1
--View or function 'V1' is not updatable because the modification affects multiple base tables.
--Check T1 and T2 with each statement to see how it gets affected
--
In some databases it's possible to update the source table(s) for a view if there is a one-to-one relationship between the rows in the view and the rows in the underlying table, that is, you cant have derived columns, aggregate functions or a distinct clause in your view for example.
In Oracle, even if a view is not inherently updatable, updates may be allowed if an INSTEAD OF DML trigger is defined.
If you use mysql, you can read a detailed description about this feature Updatable and insertable views.
" If view is considered as just a 'window' through which we can see a set of data of the base table "
- Where did you get this definition?
What oracle says about views:
A view is a logical representation of another table or combination of
tables. A view derives its data from the tables on which it is based.
These tables are called base tables. Base tables might in turn be
actual tables or might be views themselves. All operations performed
on a view actually affect the base table of the view. You can use
views in almost the same way as tables. You can query, update, insert
into, and delete from views, just as you can standard tables.
Such a view into which you can update or insert are fondly named as "Updatable and Insertable Views". Oracle documentation about them is here.
Also, this is how the purpose of an "insert" statement is defined by Oracle:
Use the INSERT statement to add rows to a table, the base table of a
view, a partition of a partitioned table or a subpartition of a
composite-partitioned table, or an object table or the base table of
an object view.
Yes we can achieve the DML Operation in Views like belows:
Create or replace view emp_dept_join as Select d.department_id,
d.department_name, e.first_name, e.last_name from employees
e, departments d where e.department_id = d.department_id;
SQL>CREATE OR REPLACE TRIGGER insert_emp_dept
INSTEAD OF INSERT ON emp_dept_join DECLARE v_department_id departments.department_id%TYPE;
BEGIN
BEGIN
SELECT department_id INTO v_department_id
FROM departments
WHERE department_id = :new.department_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
INSERT INTO departments (department_id, department_name)
VALUES (dept_sequence.nextval, :new.department_name)
RETURNING ID INTO v_department_id;
END;
INSERT INTO employees (employee_id, first_name, last_name, department_id)
VALUES(emp_sequence.nextval, :new.first_name, :new.last_name, v_department_id);
END insert_emp_dept;
/
if the viwe is defined through a simple query involving single base relation and either containing primary key or candidate key, so there will be change in base relation if changing the view. ( however there is restriction)
And updates are not allowed through view if there is multiple base relations or grouping operations.

How to duplicate all data in a table except for a single column that should be changed

I have a question regarding a unified insert query against tables with different data
structures (Oracle). Let me elaborate with an example:
tb_customers (
id NUMBER(3), name VARCHAR2(40), archive_id NUMBER(3)
)
tb_suppliers (
id NUMBER(3), name VARCHAR2(40), contact VARCHAR2(40), xxx, xxx,
archive_id NUMBER(3)
)
The only column that is present in all tables is [archive_id]. The plan is to create a new archive of the dataset by copying (duplicating) all records to a different database partition and incrementing the archive_id for those records accordingly. [archive_id] is always part of the primary key.
My problem is with select statements to do the actual duplication of the data. Because the columns are variable, I am struggling to come up with a unified select statement that will copy the data and update the archive_id.
One solution (that works), is to iterate over all the tables in a stored procedure and do a:
CREATE TABLE temp as (SELECT * from ORIGINAL_TABLE);
UPDATE temp SET archive_id=something;
INSERT INTO ORIGINAL_TABLE (select * from temp);
DROP TABLE temp;
I do not like this solution very much as the DDL commands muck up all restore points.
Does anyone else have any solution?
How about creating a global temporary table for each base table?
create global temporary table tb_customers$ as select * from tb_customers;
create global temporary table tb_suppliers$ as select * from tb_suppliers;
You don't need to create and drop these each time, just leave them as-is.
You're archive process is then a single transaction...
insert into tb_customers$ as select * from tb_customers;
update tb_customers$ set archive_id = :v_new_archive_id;
insert into tb_customers select * from tb_customers$;
insert into tb_suppliers$ as select * from tb_suppliers;
update tb_suppliers$ set archive_id = :v_new_archive_id;
insert into tb_suppliers select * from tb_suppliers$;
commit; -- this will clear the global temporary tables
Hope this helps.
I would suggest not having a single sql statement for all tables and just use and insert.
insert into tb_customers_2
select id, name, 'new_archive_id' from tb_customers;
insert into tb_suppliers_2
select id, name, contact, xxx, xxx, 'new_archive_id' from tb_suppliers;
Or if you really need a single sql statement for all of them at least precreate all the temp tables (as temp tables) and leave them in place for next time. Then just use dynamic sql to refer to the temp table.
insert into ORIGINAL_TABLE_TEMP (SELECT * from ORIGINAL_TABLE);
UPDATE ORIGINAL_TABLE_TEMP SET archive_id=something;
INSERT INTO NEW_TABLE (select * from ORIGINAL_TABLE_TEMP);

Resources