{"id":1951,"date":"2020-05-22T17:34:59","date_gmt":"2020-05-22T15:34:59","guid":{"rendered":"http:\/\/www.ludovicocaldara.net\/dba\/?p=1951"},"modified":"2020-08-18T15:56:55","modified_gmt":"2020-08-18T13:56:55","slug":"partial-pdb-cloning","status":"publish","type":"post","link":"https:\/\/www.ludovicocaldara.net\/dba\/partial-pdb-cloning\/","title":{"rendered":"Multitenant Pills: Partial PDB cloning (and cleanup)"},"content":{"rendered":"<p>When consolidating to multitenant, there are several consolidation patterns.<\/p>\n<ul>\n<li>Big, complex databases usually have special requirements for which it might be a good choice to go to single-tenant (a single PDB in one CDB)<\/li>\n<li>Small, relatively easy databases are the best candidate for consolidation to multitenant<\/li>\n<li>Schema consolidated databases require special attention, but in general there are several advantages to convert individual schemas (or group of schemas) to individual PDBs<\/li>\n<\/ul>\n<p>For the latter, there are some techniques to convert a schema in a PDB.<\/p>\n<ul>\n<li>export\/import (obviously), with eventually Golden Gate to do it online<\/li>\n<li>Transportable tablespaces (if the schemas follow strict 1-to-1 tablespace separation<\/li>\n<li>partial PDB cloning<\/li>\n<\/ul>\n<p>We will focus on the last one for this blog post.<\/p>\n<p><strong>Situation<\/strong><\/p>\n<p><a href=\"https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1952\" src=\"https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone-1024x533.png\" alt=\"\" width=\"584\" height=\"304\" srcset=\"https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone-1024x533.png 1024w, https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone-300x156.png 300w, https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone-768x400.png 768w, https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone-1536x800.png 1536w, https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone-500x260.png 500w, https:\/\/www.ludovicocaldara.net\/dba\/wp-content\/uploads\/2020\/05\/pdb_partial_clone.png 1651w\" sizes=\"auto, (max-width: 584px) 100vw, 584px\" \/><\/a><\/p>\n<p>Here we have a PDB with some schemas, each of them has a dedicated tablespace, but accidentally, two of them have also some objects on a common tablespace.<\/p>\n<p>This happens frequently when all the users have quota on the default database tablespace and they do not have necessarily a personal default tablespace.<\/p>\n<pre class=\"lang:plsql decode:true\">connect \/ as sysdba\r\nCREATE PLUGGABLE DATABASE PDB1 ADMIN USER pdbadmin IDENTIFIED BY manager;\r\nALTER PLUGGABLE DATABASE PDB1OPEN;\r\nALTER SESSION SET CONTAINER=PDB1;\r\n\r\nCREATE TABLESPACE ABC DATAFILE SIZE 50M;\r\nCREATE TABLESPACE DEF DATAFILE SIZE 50M;\r\nCREATE TABLESPACE GHI DATAFILE SIZE 50M;\r\n\r\nCREATE USER ABC IDENTIFIED BY ABC DEFAULT TABLESPACE ABC QUOTA UNLIMITED ON ABC;\r\nCREATE USER DEF IDENTIFIED BY DEF DEFAULT TABLESPACE DEF QUOTA UNLIMITED ON DEF;\r\nCREATE USER GHI IDENTIFIED BY GHI DEFAULT TABLESPACE GHI QUOTA UNLIMITED ON GHI;\r\n\r\nALTER USER ABC QUOTA UNLIMITED ON DATA;\r\nALTER USER DEF QUOTA UNLIMITED ON DATA;\r\nALTER USER GHI QUOTA UNLIMITED ON DATA;\r\n\r\nGRANT CREATE SESSION, ALTER SESSION, CREATE TABLE TO ABC, DEF, GHI;\r\n\r\n\r\nCONNECT abc\/abc@srcsrv:1521\/pdb1\r\n\r\nCREATE TABLE T1 (A VARCHAR2(50));\r\nINSERT INTO T1 VALUES ('TOTO');\r\nCOMMIT;\r\n\r\nCREATE TABLE T2 (A VARCHAR2(50)) TABLESPACE DATA;\r\nINSERT INTO T2 VALUES ('TOTO');\r\nCOMMIT;\r\n\r\n\r\nCONNECT def\/def@srcsrv:1521\/pdb1\r\n\r\nCREATE TABLE T1 (A VARCHAR2(50));\r\nINSERT INTO T1 VALUES ('TOTO');\r\nCOMMIT;\r\n\r\nCREATE TABLE T2 (A VARCHAR2(50)) TABLESPACE DATA;\r\nINSERT INTO T2 VALUES ('TOTO');\r\nCOMMIT;\r\n\r\n\r\nCONNECT ghi\/ghi@srcsrv:1521\/pdb1\r\n\r\nCREATE TABLE T1 (A VARCHAR2(50));\r\nINSERT INTO T1 VALUES ('TOTO');\r\nCOMMIT;<\/pre>\n<p>This is the typical situation where transportable tablespaces become hard to achieve without some upfront segment movement, as tablespaces are not self-contained.<\/p>\n<p>Thankfully, Oracle Multitenant allows us to clone a PDB from a remote one and specify only a subset of tablespaces.<\/p>\n<pre class=\"lang:plsql decode:true \">CREATE PLUGGABLE DATABASE ABC FROM PDB1.ORCL1_PDB1_tempclone \r\n  USER_TABLESPACES=('ABC','DATA');<\/pre>\n<p>Here is a full example script with some checks and fancy parameters:<\/p>\n<pre class=\"lang:plsql decode:true\">--------------------------\r\n-- Script: clone_pdb.sql\r\n-- Purpose: (partial) clone a PDB from remote PDB\r\n-- Author: Ludovico Caldara (Trivadis)\r\n--\r\n-- Execute as: DBA in CDB$ROOT\r\n--\r\n-- Requirements: The remote user for the DBlink must have SELECT privileges on a few fixed views:\r\n-- --\r\n-- create user c##remote_user identified by manager container=all;\r\n-- grant create session, sysoper, create pluggable database to c##remote_user container=all;\r\n-- -- granting extra privileges to get file names and total size\r\n-- grant select on sys.v_$pdbs to c##remote_user container=all;\r\n-- grant select on sys.v_$datafile to c##remote_user container=all;\r\n-- grant select on sys.v_$database to c##remote_user container=all;\r\n-- alter user c##remote_user set container_data=all for v_$pdbs container=current;\r\n-- alter user c##remote_user set container_data=all for v_$datafile container=current;\r\n-- alter user c##remote_user set container_data=all for v_$database container=current;\r\n--\r\n--\r\n--\r\n-- Script variables:\r\ndefine SourceCDB = 'ORCL1'\r\ndefine SourcePDB = 'PDB1'\r\ndefine SourceUser = 'C##REMOTE_USER'\r\ndefine SourcePassword = 'manager'\r\ndefine SourceAlias = 'srcsrv:1521\/PDB1'\r\ndefine TargetCDB = 'ORCL2'\r\ndefine TargetPDB = 'ABC'\r\ndefine UserTBSPClause = 'USER_TABLESPACES=(''ABC'',''DATA'')'\r\n-- in case of non-OMF\r\ndefine FileNameConvertClause = 'FILE_NAME_CONVERT=(''\/&amp;SourceCDB\/&amp;SourcePDB\/'',''\/&amp;TargetCDB\/&amp;TargetPDB\/'')'\r\n\r\nSET ECHO ON SERVEROUTPUT ON\r\n\r\n-- Temporary database link name. We try to drop it if it exists but nevermind if it gives error\r\nWHENEVER SQLERROR CONTINUE\r\nDROP DATABASE LINK &amp;SourceCDB._&amp;SourcePDB._tempclone;\r\n\r\n-- The other tasks shouls succeed, otherwise exit\r\nWHENEVER SQLERROR EXIT 1\r\nCREATE DATABASE LINK &amp;SourceCDB._&amp;SourcePDB._tempclone connect to &amp;SourceUser identified by &amp;SourcePassword using '&amp;SourceAlias';\r\n\r\n-- in this PL\/SQL block we make sure that we do not copy if\r\n-- the source is NOARCHIVELOG or not LOCAL UNDO and PDB is not READ ONLY\r\nDECLARE\r\n        l_log_mode sys.v_$database.log_mode%TYPE;\r\n        l_local_undo database_properties.property_value%TYPE;\r\n        l_open_mode sys.v_$pdbs.open_mode%TYPE;\r\n\r\nBEGIN\r\n        BEGIN\r\n                SELECT open_mode INTO l_open_mode FROM sys.v_$pdbs@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE name='&amp;SourcePDB';\r\n        EXCEPTION\r\n                WHEN no_data_found THEN raise_application_error(-20104,'Pluggable database not found.');\r\n        END;\r\n\r\n        -- Check if NOARCHIVELOG and not READ ONLY\r\n        SELECT log_mode INTO l_log_mode FROM sys.v_$database@&amp;SourceCDB._&amp;SourcePDB._tempclone ;\r\n        IF l_log_mode = 'NOARCHIVELOG' AND l_open_mode &lt;&gt; 'READ ONLY' THEN\r\n                raise_application_error(-20102,'The database is in NOARCHIVELOG mode and the PDB is not READ ONLY.');\r\n        END IF;\r\n\r\n        -- Check if not LOCAL_UNDO_ENABLED  and not READ ONLY\r\n        SELECT property_value INTO l_local_undo FROM database_properties@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE  property_name = 'LOCAL_UNDO_ENABLED';\r\n        IF l_local_undo &lt;&gt; 'TRUE' AND l_open_mode &lt;&gt; 'READ ONLY' THEN\r\n                raise_application_error(-20103,'The LOCAL UNDO is not enabled for the database and the PDB is not READ ONLY.');\r\n        END IF;\r\nEND;\r\n\/\r\n\r\n-- let's add some intelligence... Parallel degree depending on PDB size\r\nCOLUMN parallel new_value parallel\r\nSELECT CASE\r\n        WHEN ROUND(total_size\/POWER(1024,3)) &lt;= 1 THEN 1\r\n        WHEN ROUND(total_size\/POWER(1024,3)) &gt;= 5 THEN 4\r\n        ELSE 2 END AS parallel\r\nFROM sys.v_$pdbs@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE NAME='&amp;SourcePDB';\r\n\r\n-- Real clone PDB\r\nCREATE PLUGGABLE DATABASE &amp;TargetPDB FROM &amp;SourcePDB.@&amp;SourceCDB._&amp;SourcePDB._tempclone &amp;UserTBSPClause PARALLEL &amp;parallel &amp;FileNameConvertClause;\r\n\r\nALTER PLUGGABLE DATABASE &amp;TargetPDB OPEN;\r\nALTER PLUGGABLE DATABASE &amp;TargetPDB SAVE STATE;\r\n-- drop temporary db link: leaving it there exposes some risks\r\nDROP DATABASE LINK &amp;SourcePDB.@&amp;SourceCDB._&amp;SourcePDB._tempclone\r\n\r\nSET ECHO OFF SERVEROUTPUT OFF<\/pre>\n<p>This is an example output:<\/p>\n<pre class=\"lang:plsql decode:true\">SQL&gt; @\/tmp\/clone\r\nSQL&gt;\r\nSQL&gt; -- Temporary database link name. We try to drop it if it exists but nevermind if it gives error\r\nSQL&gt; WHENEVER SQLERROR CONTINUE\r\nSQL&gt; DROP DATABASE LINK &amp;SourceCDB._&amp;SourcePDB._tempclone;\r\nold   1: DROP DATABASE LINK &amp;SourceCDB._&amp;SourcePDB._tempclone\r\nnew   1: DROP DATABASE LINK ORCL1_PDB1_tempclone\r\n\r\nDatabase link dropped.\r\n\r\nSQL&gt;\r\nSQL&gt; -- The other tasks shouls succeed, otherwise exit\r\nSQL&gt; WHENEVER SQLERROR EXIT 1\r\nSQL&gt; CREATE DATABASE LINK &amp;SourceCDB._&amp;SourcePDB._tempclone connect to &amp;SourceUser identified by &amp;SourcePassword using '&amp;SourceAlias';\r\nold   1: CREATE DATABASE LINK &amp;SourceCDB._&amp;SourcePDB._tempclone connect to &amp;SourceUser identified by &amp;SourcePassword using '&amp;SourceAlias'\r\nnew   1: CREATE DATABASE LINK ORCL1_PDB1_tempclone connect to C##REMOTE_USER identified by manager using 'srcsrv:1521\/PDB1'\r\n\r\nDatabase link created.\r\n\r\nSQL&gt;\r\nSQL&gt; -- in this PL\/SQL block we make sure that we do not copy if\r\nSQL&gt; -- the source is NOARCHIVELOG or not LOCAL UNDO and PDB is not READ ONLY\r\nSQL&gt; DECLARE\r\n  2          l_log_mode sys.v_$database.log_mode%TYPE;\r\n  3          l_local_undo database_properties.property_value%TYPE;\r\n  4          l_open_mode sys.v_$pdbs.open_mode%TYPE;\r\n  5\r\n  6  BEGIN\r\n  7          BEGIN\r\n  8                  SELECT open_mode INTO l_open_mode FROM sys.v_$pdbs@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE name='&amp;SourcePDB';\r\n  9          EXCEPTION\r\n 10                  WHEN no_data_found THEN raise_application_error(-20104,'Pluggable database not found.');\r\n 11          END;\r\n 12\r\n 13          -- Check if NOARCHIVELOG and not READ ONLY\r\n 14          SELECT log_mode INTO l_log_mode FROM sys.v_$database@&amp;SourceCDB._&amp;SourcePDB._tempclone ;\r\n 15          IF l_log_mode = 'NOARCHIVELOG' AND l_open_mode &lt;&gt; 'READ ONLY' THEN\r\n 16                  raise_application_error(-20102,'The database is in NOARCHIVELOG mode and the PDB is not READ ONLY.');\r\n 17          END IF;\r\n 18\r\n 19          -- Check if not LOCAL_UNDO_ENABLED  and not READ ONLY\r\n 20          SELECT property_value INTO l_local_undo FROM database_properties@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE  property_name = 'LOCAL_UNDO_ENABLED';\r\n 21          IF l_local_undo &lt;&gt; 'TRUE' AND l_open_mode &lt;&gt; 'READ ONLY' THEN\r\n 22                  raise_application_error(-20103,'The LOCAL UNDO is not enabled for the database and the PDB is not READ ONLY.');\r\n 23          END IF;\r\n 24  END;\r\n 25  \/\r\nold   8:                SELECT open_mode INTO l_open_mode FROM sys.v_$pdbs@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE name='&amp;SourcePDB';\r\nnew   8:                SELECT open_mode INTO l_open_mode FROM sys.v_$pdbs@ORCL1_PDB1_tempclone WHERE name='PDB1';\r\nold  14:        SELECT log_mode INTO l_log_mode FROM sys.v_$database@&amp;SourceCDB._&amp;SourcePDB._tempclone ;\r\nnew  14:        SELECT log_mode INTO l_log_mode FROM sys.v_$database@ORCL1_PDB1_tempclone ;\r\nold  20:        SELECT property_value INTO l_local_undo FROM database_properties@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE  property_name = 'LOCAL_UNDO_ENABLED';\r\nnew  20:        SELECT property_value INTO l_local_undo FROM database_properties@ORCL1_PDB1_tempclone WHERE  property_name = 'LOCAL_UNDO_ENABLED';\r\n\r\nPL\/SQL procedure successfully completed.\r\n\r\nSQL&gt;\r\nSQL&gt; -- let's add some intelligence... Parallel degree depending on PDB size\r\nSQL&gt; COLUMN parallel new_value parallel\r\nSQL&gt; SELECT CASE\r\n  2          WHEN ROUND(total_size\/POWER(1024,3)) &lt;= 1 THEN 1\r\n  3          WHEN ROUND(total_size\/POWER(1024,3)) &gt;= 5 THEN 4\r\n  4          ELSE 2 END AS parallel\r\n  5  FROM sys.v_$pdbs@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE NAME='&amp;SourcePDB';\r\nold   5: FROM sys.v_$pdbs@&amp;SourceCDB._&amp;SourcePDB._tempclone WHERE NAME='&amp;SourcePDB'\r\nnew   5: FROM sys.v_$pdbs@ORCL1_PDB1_tempclone WHERE NAME='PDB1'\r\n\r\n  PARALLEL\r\n----------\r\n         2\r\n\r\nSQL&gt;\r\nSQL&gt; -- Real clone PDB\r\nSQL&gt; CREATE PLUGGABLE DATABASE &amp;TargetPDB FROM &amp;SourcePDB.@&amp;SourceCDB._&amp;SourcePDB._tempclone &amp;UserTBSPClause PARALLEL &amp;parallel &amp;FileNameConvertClause;\r\nold   1: CREATE PLUGGABLE DATABASE &amp;TargetPDB FROM &amp;SourcePDB.@&amp;SourceCDB._&amp;SourcePDB._tempclone &amp;UserTBSPClause PARALLEL &amp;parallel &amp;FileNameConvertClause\r\nnew   1: CREATE PLUGGABLE DATABASE ABC FROM PDB1@ORCL1_PDB1_tempclone USER_TABLESPACES=('ABC','DATA') PARALLEL          2 FILE_NAME_CONVERT=('\/ORCL1\/PDB1\/','\/ORCL2\/ABC\/')\r\n\r\nPluggable database created.\r\n\r\nSQL&gt; ALTER PLUGGABLE DATABASE &amp;TargetPDB OPEN;\r\nold   1: ALTER PLUGGABLE DATABASE &amp;TargetPDB OPEN;\r\nnew   1: ALTER PLUGGABLE DATABASE ABC OPEN;\r\n\r\nPluggable database altered.\r\n\r\nSQL&gt;ALTER PLUGGABLE DATABASE &amp;TargetPDB SAVE STATE; \r\nold   1: ALTER PLUGGABLE DATABASE &amp;TargetPDB SAVE STATE;\r\nnew   1: ALTER PLUGGABLE DATABASE ABC SAVE STATE;\r\n\r\nPluggable database altered.\r\n\r\n\r\nSQL&gt;\r\nSQL&gt; -- drop temporary db link: leaving it there exposes some risks\r\nSQL&gt; DROP DATABASE LINK &amp;SourcePDB.@&amp;SourceCDB._&amp;SourcePDB._tempclone\r\n  2\r\nSQL&gt; SET ECHO OFF SERVEROUTPUT OFF\r\n<\/pre>\n<p>If the clone process succeeds, at the end we should have the new ABC pluggable database with ABC and DATA tablespaces only.<\/p>\n<pre class=\"lang:plsql decode:true \">SQL&gt; select tablespace_name from dba_tablespaces;\r\n\r\nTABLESPACE_NAME\r\n------------------------------\r\nSYSTEM\r\nSYSAUX\r\nUNDOTBS1\r\nTEMP\r\nDATA\r\nABC\r\n<\/pre>\n<p>Yeah!<\/p>\n<p><strong>Any Cleanup needed?<\/strong><\/p>\n<p>What happened to the users? Actually, they are all still there:<\/p>\n<pre class=\"lang:plsql decode:true\">SQL&gt; select username from dba_users where oracle_maintained='N';\r\n\r\nUSERNAME\r\n--------------------------------------------------------------------------------\r\nPDBADMIN\r\nC##REMOTE_CLONE\r\nABC\r\nDEF\r\nGHI\r\n\r\n5 rows selected.\r\n<\/pre>\n<p>And the segments in the two skipped tablespaces are not there:<\/p>\n<pre class=\"lang:plsql decode:true\">SQL&gt; select owner, segment_name, tablespace_name from dba_segments \r\n2&gt; where owner in ('ABC','DEF','GHI');\r\n\r\nOWNER                SEGMENT_NAME         TABLESPACE_NAME\r\n-------------------- -------------------- ------------------------------\r\nABC                  T2                   DATA\r\nDEF                  T2                   DATA\r\nABC                  T1                   ABC<\/pre>\n<p>So the table definitions are also gone?<\/p>\n<pre class=\"lang:plsql decode:true\">SQL&gt; select owner, table_name, tablespace_name from dba_tables\r\n  2    where owner in ('ABC','DEF','GHI');\r\n\r\nOWNER                TABLE_NAME           TABLESPACE_NAME\r\n-------------------- -------------------- ------------------------------\r\nABC                  T2                   DATA\r\nABC                  T1                   ABC\r\nDEF                  T2                   DATA\r\nDEF                  T1                   DEF\r\nGHI                  T1                   GHI\r\n<\/pre>\n<p>Not at all! The tables are still there and reference to tablespaces that do not exist anymore. Possible?<\/p>\n<p>Actually, the tablespaces definition are still there if we look at v$tablespace:<\/p>\n<pre class=\"lang:plsql decode:true \">SQL&gt; select name from v$tablespace;\r\n\r\nNAME\r\n------------------------------\r\nSYSTEM\r\nSYSAUX\r\nUNDOTBS1\r\nTEMP\r\nDATA\r\nABC\r\nDEF\r\nGHI\r\n\r\n8 rows selected.\r\n<\/pre>\n<p>If we give a look at the DBA_TABLESPACES view definition, there are a few interesting filters:<\/p>\n<pre class=\"lang:plsql decode:true\">create or replace view DBA_TABLESPACES\r\n...\r\nwhere ts.online$ != 3\r\nand bitand(flags,2048) != 2048\r\nand bitand(ts.flags, 16777216) != 16777216\r\n...\r\n\/<\/pre>\n<p>What is their meaning?<\/p>\n<pre class=\"lang:plsql decode:true\"> online$     \/* status (see KTT.H): *\/\r\n             \/* 1 = ONLINE, 2 = OFFLINE, 3 = INVALID *\/\r\n\r\n  flags      \/* various flags: see ktt3.h *\/\r\n             \/* 0x800 = this actually describes a group *\/ -- 2048\r\n             \/* 0x1000000 = bad transport of ts in pdb *\/ -- 16777216<\/pre>\n<p>So the first WHERE clause skips all the INVALID TABLESPACES (when you drop a tablespace it is still stored in ts$ with this state), the second skips the definition of TEMPORARY TABLESPACE GROUPS, the third one is actually our candidate.<\/p>\n<p>Indeed, this is what we get from ts$ for these tablespaces:<\/p>\n<pre class=\"lang:plsql decode:true \">SQL&gt; select name, decode(online$,1,'ONLINE',2,'OFFLINE',3,'INVALID',4,'READ ONLY','UNDEFINED') as STATUS, bitand(flags,2048), bitand(flags,16777216) from ts$;\r\n\r\nNAME                           STATUS    BITAND(FLAGS,2048) BITAND(FLAGS,16777216)\r\n------------------------------ --------- ------------------ ----------------------\r\nSYSTEM                         ONLINE                     0                      0\r\nSYSAUX                         ONLINE                     0                      0\r\nUNDOTBS1                       ONLINE                     0                      0\r\nTEMP                           ONLINE                     0                      0\r\nDATA                           ONLINE                     0                      0\r\nABC                            ONLINE                     0                      0\r\nDEF                            OFFLINE                    0               16777216\r\nGHI                            OFFLINE                    0               16777216\r\n\r\n8 rows selected.\r\n<\/pre>\n<p>So the two tablespaces are filtered out because of this new multitenant flag.<\/p>\n<p>If we try to drop the tablespaces, it succeeds:<\/p>\n<pre class=\"lang:plsql decode:true \">SQL&gt; drop tablespace def including contents and datafiles;\r\n\r\nTablespace dropped.\r\n\r\nSQL&gt; drop tablespace ghi including contents and datafiles;\r\n\r\nTablespace dropped.\r\n<\/pre>\n<p>But the user GHI, who has no objects anymore, is still there.<\/p>\n<pre class=\"lang:plsql decode:true\">SQL&gt; select username from dba_users\r\n  2  where username  in ('ABC','DEF','GHI') ;\r\n\r\nUSERNAME\r\n-------------------------\r\nGHI\r\nDEF\r\nABC<\/pre>\n<p>So we need to drop it explicitly.<\/p>\n<pre class=\"lang:plsql decode:true\">SQL&gt; drop user ghi cascade;\r\n\r\nUser dropped.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p><strong>Automate the cleanup<\/strong><\/p>\n<p>This is an example PL\/SQL that is aimed to automate the cleanup.<\/p>\n<p>Actually:<\/p>\n<ul>\n<li>Users that had segments in one of the excluded tablespaces but do not have any segments left are just LOCKED (for security reasons, you can guess why).<\/li>\n<li>Tablespaces that meet the &#8220;excluded PDB&#8221; criteria, are just dropped<\/li>\n<\/ul>\n<pre class=\"lang:plsql decode:true\">set serveroutput on\r\nDECLARE\r\n\r\n        -- get the users that had tables in the non-cloned TBSP that do not have any other segments...\r\n        CURSOR c_users IS\r\n                SELECT DISTINCT owner as username FROM dba_tables t\r\n                WHERE t.tablespace_name IN (\r\n                        SELECT name FROM sys.ts$ WHERE online$=2 AND bitand(flags, 16777216)=16777216\r\n                ) AND NOT EXISTS (\r\n                        SELECT 1 FROM dba_segments s WHERE s.owner=t.owner\r\n                );\r\n\r\n        -- get the list of non-cloned TBSP\r\n        CURSOR c_tbsps IS\r\n                SELECT name FROM sys.ts$ WHERE online$=2 AND bitand(flags, 16777216)=16777216;\r\n\r\n        r_users c_users%ROWTYPE;\r\n        r_tbsps c_tbsps%ROWTYPE;\r\n\r\n        e_drop_def_tbsp EXCEPTION;\r\n        PRAGMA EXCEPTION_INIT (e_drop_def_tbsp, -12919);\r\n\r\n\r\nBEGIN\r\n        -- check if currently in a PDB\r\n        IF sys_context('userenv','con_name') = 'CDB$ROOT' THEN\r\n                raise_application_error(-20101,'The current container is CDB$ROOT. It must be a PDB.');\r\n        END IF;\r\n\r\n        -- loop all candidate users and lock them out\r\n        OPEN c_users;\r\n        LOOP\r\n                        FETCH  c_users  INTO r_users;\r\n                        EXIT WHEN c_users%NOTFOUND;\r\n                        dbms_output.put_line ('ALTER USER '||r_users.username||' ACCOUNT LOCK');\r\n                        execute immediate 'ALTER USER '||r_users.username||' ACCOUNT LOCK';\r\n                        dbms_output.put_line ('User '||r_users.username||' locked.');\r\n        END LOOP;\r\n\r\n        -- loop all candidate tbsps and drop them\r\n        OPEN c_tbsps;\r\n        LOOP\r\n                FETCH  c_tbsps  INTO r_tbsps;\r\n                EXIT WHEN c_tbsps%NOTFOUND;\r\n                dbms_output.put_line ('DROP TABLESPACE '||r_tbsps.name||' INCLUDING CONTENTS AND DATAFILES');\r\n                BEGIN\r\n                        execute immediate 'DROP TABLESPACE '||r_tbsps.name||' INCLUDING CONTENTS AND DATAFILES';\r\n                        dbms_output.put_line ('Tablespace '||r_tbsps.name||' dropped.');\r\n                EXCEPTION\r\n                        WHEN e_drop_def_tbsp THEN\r\n                                dbms_output.put_line ('Cannot drop default tablespace '||r_tbsps.name||'. Please change the default tablespace and drop it afterwards.');\r\n                END;\r\n        END LOOP;\r\nEND;\r\n\/\r\n<\/pre>\n<p>This is the output for the clone procedure we have just seen:<\/p>\n<pre class=\"lang:plsql decode:true \">SQL&gt; @cleanup_partial_pdb.sql\r\nALTER USER GHI ACCOUNT LOCK\r\nUser GHI locked.\r\nDROP TABLESPACE DEF INCLUDING CONTENTS AND DATAFILES\r\nTablespace DEF dropped.\r\nDROP TABLESPACE GHI INCLUDING CONTENTS AND DATAFILES\r\nTablespace GHI dropped.\r\n\r\nPL\/SQL procedure successfully completed.\r\n<\/pre>\n<p>The PL\/SQL block can be quite slow depending on the segments and tablespaces, so it might be a good idea to have a custom script instead of this automated one.<\/p>\n<p><strong>What about user DEF?<\/strong><\/p>\n<p>The automated procedure has not locked the account DEF. Why?<\/p>\n<p>Actually, the user DEF still has some segments in the DATA tablespace. Hence, the procedure cannot be sure what was the original intention: copy the user ABC ? The clone procedure allows only to specify the tablespaces, so this is the only possible result.<\/p>\n<p><strong>Promo: If you need to migrate to Multitenant and you need consulting\/training, <a href=\"https:\/\/www.linkedin.com\/in\/ludovicocaldara\/\">just contact me, I can help you \ud83d\ude42<\/a><\/strong><\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>When consolidating to multitenant, there are several consolidation patterns. Big, complex databases usually have special requirements for which it might be a good choice to go to single-tenant (a single PDB in one CDB) Small, relatively easy databases are the &hellip; <a href=\"https:\/\/www.ludovicocaldara.net\/dba\/partial-pdb-cloning\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[321,326,3,315,328,132],"tags":[211,324,289],"class_list":["post-1951","post","type-post","status-publish","format-standard","hentry","category-aced","category-oracle","category-oracledb","category-oracle-database-19c","category-oracle-mt","category-triblog","tag-cloning","tag-db19c","tag-multitenant"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/posts\/1951","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/comments?post=1951"}],"version-history":[{"count":4,"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/posts\/1951\/revisions"}],"predecessor-version":[{"id":1956,"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/posts\/1951\/revisions\/1956"}],"wp:attachment":[{"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/media?parent=1951"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/categories?post=1951"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ludovicocaldara.net\/dba\/wp-json\/wp\/v2\/tags?post=1951"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}