<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[My Oracle DBA & Sysadmin Blog]]></title><description><![CDATA[Notes on Oracle internals, SE2 workarounds, ASM, and performance tuning — written by an Oracle ACE who prefers the problems that aren't in the manual.]]></description><link>https://blog.srecnik.info</link><generator>RSS for Node</generator><lastBuildDate>Thu, 14 May 2026 00:45:29 GMT</lastBuildDate><atom:link href="https://blog.srecnik.info/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Migrating LOBs on Standard Edition with Materialized View Logs]]></title><description><![CDATA[LOBs can be stored as either BasicFiles or as SecureFiles. While both storage types are still supported in 19c, let me start by a quote from 12.1 documentation:

SecureFiles is the default storage mec]]></description><link>https://blog.srecnik.info/migrating-lobs-on-standard-edition-with-materialized-view-logs</link><guid isPermaLink="true">https://blog.srecnik.info/migrating-lobs-on-standard-edition-with-materialized-view-logs</guid><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 06 Apr 2026 21:36:45 GMT</pubDate><content:encoded><![CDATA[<p>LOBs can be stored as either <em>BasicFiles</em> or as <em>SecureFiles</em>. While both storage types are still supported in 19c, let me start by a quote from <a href="https://docs.oracle.com/database/121/ADLOB/adlob_intro.htm#ADLOB45131">12.1 documentation</a>:</p>
<blockquote>
<p>SecureFiles is the default storage mechanism for LOBs starting with Oracle Database 12c, and Oracle strongly recommends SecureFiles for storing and managing LOBs, rather than BasicFiles. BasicFiles will be deprecated in a future release.</p>
</blockquote>
<p>Besides the mentioned warning, there are also performance considerations that drive us toward moving to SecureFiles. We won't delve into those - this post will focus on how to migrate from <em>BasicFile</em> to <em>SecureFile</em>. Which is simple with relatively small amount of rows:</p>
<pre><code class="language-sql">ALTER TABLE my_table MOVE LOB (lob_column) STORE AS SECUREFILE;
</code></pre>
<p>But.. this can take significant amount of time. In my case, it took 15 hours for 6TB of LOBs. And while this is running, the table is locked - users can't issue DMLs on it.</p>
<p>If we were on Enterprise Edition, we could think of <code>ONLINE</code> move option or even better, <code>dbms_redefinition</code>; but those are specifically Enterprise Edition features according to <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/dblic/Licensing-Information.html#GUID-B6113390-9586-46D7-9008-DCC9EDA45AB4">Licensing Guide</a>.</p>
<p>So, here's an idea on approach somewhat similar to the use of <code>dbms_redefinition</code> which works on Standard Edition. Be warned though, this idea focuses solely on <em>cloning the table data</em> - everything else (explained at the end of this post) is up to the developers/DBAs and not covered by this post.</p>
<h2>Preparation</h2>
<p>First, we create a new, empty table, with the same columns as our <em>BasicFile</em> source table. We can use <code>dbms_metadata.get_ddl</code> to obtain original DDL and modify it to specify <em>SecureFile</em> instead of <em>BasicFile</em>.</p>
<p>So, thus, suppose that now we have two tables:</p>
<ul>
<li><code>my_basicfile</code> with millions of rows    </li>
<li><code>my_securefile</code> which is empty for now</li>
</ul>
<p>You could also specify <code>NOLOGGING</code> on the new table - just don't forget that in most cases you'll also want to change this back to <code>LOGGING</code> and take appropriate backup before letting users use the new table. It should also go without saying that <code>NOLOGGING</code> operations won't propagate to your physical standby.</p>
<p>Anyway, for most of actions described after this point, it's best to execute them <strong>while there are no users online</strong> (perhaps using <code>restricted session</code>?).</p>
<p>This example requires that the <em>BasicFiles</em> table have a primary key. The same concept might work <code>WITH ROWID</code> clause for <code>CREATE MATERIALIZED VIEW LOG</code>, but this isn't the case in this example.</p>
<h2>Initial Clone</h2>
<p>Let's start by creating <strong>Materialized View</strong> and a <strong>MV Log</strong>:</p>
<pre><code class="language-plaintext">CREATE MATERIALIZED VIEW LOG ON my_basicfile
    WITH PRIMARY KEY
    INCLUDING NEW VALUES;

CREATE MATERIALIZED VIEW my_securefile
	ON PREBUILT TABLE
	REFRESH FAST
	AS SELECT * FROM my_basicfile;
</code></pre>
<p>The <em>log</em> is used to track all changed rows (ins/upd/del) on the source table since its creation. We'll need it in the next step for fast refresh. This log must be created before <code>dbms_mview.refresh()</code> is called and before users are allowed to make changes to the <code>my_basicfile</code>.</p>
<p>Note that the MV log only stores the primary keys of changed rows - the actual LOB data is read from the source table during refresh.</p>
<p>The <code>MATERIALIZED VIEW</code> is created on <em>prebuilt</em> table, which is actually empty. So, we need to populate it:</p>
<pre><code class="language-sql">begin
    dbms_mview.refresh(
        list =&gt; 'my_securefile',
        method =&gt; 'C', 
        atomic_refresh =&gt; FALSE);
end;
</code></pre>
<p>The <code>method =&gt; 'C'</code> indicates complete refresh (copying <em>all rows</em> from the <code>my_basicfile</code> source).</p>
<p><code>atomic_refresh=&gt;FALSE</code> doesn't seem needed at first glance, but it's there so that the underlying method uses <code>/*+ APPEND */</code> hint when inserting the data - which makes this complete refresh a bit faster. It also uses <code>TRUNCATE</code> before insert this way, but that doesn't matter since <code>my_securefile</code> is empty at this stage.</p>
<p>This <code>dbms_mview.refresh</code> is still likely to take significant amount of time - but the users can be online while it is running; remember, any changes they make to the underlying <code>my_basicfile</code> is recorded in a materialized view log.</p>
<p>Under the hood, according to my tracing on 19c, following is executed:</p>
<pre><code class="language-sql">LOCK TABLE "LOB_TEST"."MY_SECUREFILE" IN EXCLUSIVE MODE  NOWAIT;
/* MV_REFRESH (DEL) */ truncate table "LOB_TEST"."MY_SECUREFILE" purge snapshot log;
/* MV_REFRESH (INS) */INSERT /*+ BYPASS_RECURSIVE_CHECK APPEND SKIP_UNQ_UNUSABLE_IDX */ INTO "LOB_TEST"."MY_SECUREFILE"("COL_PK","COL_CLOB") SELECT "MY_BASICFILE"."COL_PK","MY_BASICFILE"."COL_CLOB" FROM "LOB_TEST"."MY_BASICFILE" "MY_BASICFILE";
</code></pre>
<h2>Fast Refresh</h2>
<p>After previously mentioned complete refresh finishes (this could be, say, the next evening), we only need to add changes that gathered in <em>materialized view log</em> since we ran the full refresh. Like this:</p>
<pre><code class="language-sql">exec dbms_mview.refresh('my_securefile', method =&gt; 'F');
</code></pre>
<p><code>F</code> stands for <code>Fast</code> refresh. This means, that Oracle will use the <em>materialized view log</em>, which is just a table (automatically populated by Oracle as changes occur) named <code>mlog$_my_basicfile</code> - you'll find list of all primary keys that were touched in there.</p>
<p>Under the hood, according to my sql tracing on 19c, it runs:</p>
<pre><code>DELETE FROM "LOB_TEST"."MY_SECUREFILE" SNAP\( WHERE "COL_PK" IN (SELECT * FROM (SELECT MLOG\)."COL_PK" FROM "LOB_TEST"."MLOG\(_MY_BASICFILE" MLOG\) WHERE "SNAPTIME$$" &gt; :1 AND ("DMLTYPE$$" != 'I')) AS OF SNAPSHOT(:B_SCN) );

/* MV_REFRESH (MRG) */ MERGE INTO "LOB_TEST"."MY_SECUREFILE" "SNA\(" USING (SELECT * FROM (SELECT CURRENT\)."COL_PK",CURRENT\(."COL_CLOB" FROM (SELECT "MY_BASICFILE"."COL_PK" "COL_PK","MY_BASICFILE"."COL_CLOB" "COL_CLOB" FROM "LOB_TEST"."MY_BASICFILE" "MY_BASICFILE") CURRENT\), (SELECT MLOG\(."COL_PK" FROM "LOB_TEST"."MLOG\)_MY_BASICFILE" MLOG$ WHERE "SNAPTIME$$" &gt; :1 AND ("DMLTYPE$$" != 'D') GROUP BY MLOG\(."COL_PK") LOG\) WHERE CURRENT\(."COL_PK" = LOG\)."COL_PK") AS OF SNAPSHOT(:B_SCN) )"AV\(" ON ("SNA\)"."COL_PK" = "AV\("."COL_PK") WHEN MATCHED THEN UPDATE  SET "SNA\)"."COL_CLOB" = "AV\("."COL_CLOB" WHEN NOT MATCHED THEN INSERT  (SNA\)."COL_PK",SNA\(."COL_CLOB") VALUES (AV\)."COL_PK",AV$."COL_CLOB");
</code></pre>
<h2>Cutover &amp; Cleanup</h2>
<p>For the final cutover, users should be offline again (<code>restricted session</code>?). Fast Refresh, as explained previously, should finish once again, just to make sure the mview log is all applied to the <code>my_securefile</code>.</p>
<p>So, once we're sure both our tables, <code>my_securefile</code> and <code>my_basicfile</code> have the same contents, we can drop the materialized views, the MV log and finally, also the <code>my_basicfile</code> table. Like so:</p>
<pre><code class="language-plaintext">DROP MATERIALIZED VIEW my_securefile PRESERVE TABLE;
DROP MATERIALIZED VIEW LOG ON my_basicfile;
DROP TABLE my_basicfile PURGE;
</code></pre>
<p>but.. herein lies the problem, what if there were foreign keys referencing <code>my_basicfile</code>? What about permissions? Triggers? All those and more must be manually changed (this could mean altering a huge amount of database objects/tables), so that it points to our new table before we're ready to make the final rename:</p>
<pre><code class="language-plaintext">ALTER TABLE my_securefile RENAME TO my_basicfile;
</code></pre>
<h2>Possible Alternative (adding a column)</h2>
<p>There is an alternative idea - what if, instead of creating a new table and copying everything over, we'd simply add another column? e.g.:</p>
<pre><code class="language-sql">alter table my_basicfile
    add (col_securelob clob)
    lob (col_securelob) store as securefile;
</code></pre>
<p>then, simply update new column to the value of the old. There is no free lunch though - this approach might be much lighter regarding metadata (permissions, foreign keys, etc), but...</p>
<p>You'd need to consider the following challenges:</p>
<ul>
<li>Row Lock Contention</li>
<li>Undo &amp; Redo Generation</li>
</ul>
<p>Those challenges can be addressed to certain extent by:</p>
<ul>
<li>updating in chunks</li>
<li>setting nologging (temporarily) to the new lob column (e.g. <code>store as securefile (nocache nologging)</code>)</li>
<li>creating a trigger to update the new column when the old one is updated/inserted</li>
</ul>
<p>A quick and naive comparison of this alternative on a slow demo server with generated demo data proved to be about 7x slower than a MV approach. YMMV, of course.</p>
<h2>Conclusion</h2>
<p>There are many (other) methods on how to migrate LOBs from <code>basicfile</code> to <code>securefile</code> - a simple blog post such as this one cannot possibly address all of them; This is why I focused on one specific approach which I found specifically interesting. It is by no means "the best" approach - it's simply one of the options.</p>
<p>I've tested this approach on 19c SE and 26ai EE.</p>
]]></content:encoded></item><item><title><![CDATA[The Hidden Cost of Login (Redo)]]></title><description><![CDATA[I was investigating an issue where there were a lot of user logins to Oracle database via listener due to misconfigured connection pool. This post explains why many logins could be a problem.
Most would first think of the cost of process creation, me...]]></description><link>https://blog.srecnik.info/the-hidden-cost-of-login-redo</link><guid isPermaLink="true">https://blog.srecnik.info/the-hidden-cost-of-login-redo</guid><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Tue, 17 Feb 2026 15:03:25 GMT</pubDate><content:encoded><![CDATA[<p>I was investigating an issue where there were a lot of user logins to Oracle database via listener due to misconfigured connection pool. This post explains why many logins could be a problem.</p>
<p>Most would first think of the cost of process creation, memory structures allocation and initialization etc. Which is all true, except that there is another important thing that goes on - and it has the potential to affect all the other sessions (those already logged in) - the REDO!</p>
<p>This post will focus on what is going on with REDO at the time of new user login.</p>
<h1 id="heading-the-test-case">The Test Case</h1>
<p>The original story was that "the database is 'slow' because of a lot of logins!". And I thought "that sounds unlikely". So I made a quick test case - a program which takes three parameters:</p>
<ul>
<li><p>connect string (e.g. user/pass@host:1521/service )</p>
</li>
<li><p>number of minutes to run the test</p>
</li>
<li><p>number of parallel threads in which to run the test, but we'll always use only 1 in this blog post.</p>
</li>
</ul>
<p>Each thread does only the following:</p>
<ul>
<li><p>connect</p>
</li>
<li><p>select * from dual;</p>
</li>
<li><p>disconnect</p>
</li>
</ul>
<p>... as many times as possible. The code of this test case is available as <a target="_blank" href="https://gist.github.com/usrecnik/72a18bf1c816d25cf53f0ce600dd57fd">GitHub Gist</a>.</p>
<p>Note that this test case was run on fairly "slow" vm; which is intentional - you can spot performance issues faster on slower machines :)</p>
<h2 id="heading-ash">ASH</h2>
<p>Here's the 1 hour ASH AAS (average active sessions per minute) for the period where the only load on the database was the given test case:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771318827007/30b74988-1817-4c97-91f4-1c970781b683.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771318868480/d0232b70-78ac-4007-87fe-7d023e9347bf.png" alt class="image--center mx-auto" /></p>
<p>Notice the "orange" and “blue” colors? Those are <code>log file parallel write</code> and<code>log file sync</code> in this case. So, what was being written to redo?</p>
<p>(btw, the ASH images are screenshots from the free <a target="_blank" href="https://appm.abakus.si/">APPM tool</a>).</p>
<h2 id="heading-oracle-log-miner">Oracle Log Miner</h2>
<p>To find out what was written to redo logs I decided to "craft" an archivelog by:</p>
<ul>
<li><p>alter system archive log current</p>
</li>
<li><p>run the test for 1 minute</p>
</li>
<li><p>alter system archive log current</p>
</li>
</ul>
<p>My test case said:</p>
<pre><code class="lang-plaintext">Threads: 1
Total time: 60.03 seconds
Total successful connections: 770
Total errors: 0                
Total connections per second: 12.83
</code></pre>
<p>Now let's see what LogMiner has to say regarding operations on <code>SYS.USER$</code> segment:</p>
<pre><code class="lang-plaintext">with
   user_ops as (
   select *
      from v$logmnr_contents
      where seg_owner='SYS' and seg_name='USER$'   
   )
select c.operation, count(*) 
   from user_ops u
   join v$logmnr_contents c on c.xid=u.xid
   group by c.operation;
</code></pre>
<pre><code class="lang-plaintext">OPERATION         COUNT(*)
--------------- ----------
START                  770
UNSUPPORTED            770
COMMIT                 770
</code></pre>
<p>The query first finds all the changes on <code>SYS.USER$</code> segment and then joins all other operations from the same transactions as the changes (<code>XID</code> is the transaction id). This means we can confirm that the START, UNSUPPORTED, and COMMIT operations are part of the same transaction triggered by each login, not unrelated activity.</p>
<p>Notice how there is exactly as many transactions on <code>SYS.USER$</code> as there are logins (770)? So, according to this, each login does "update" on <code>SYS.USER$</code> and a COMMIT. In our "connection storm", we managed to perform 12 commits per second just by logging in 12 times per second. And those numbers can get even higher when users are logging in concurrently.</p>
<h2 id="heading-unsupported-operation">Unsupported Operation</h2>
<p>Official documentation says regarding such operations: "<em>Change was caused by operations not currently supported by LogMiner</em>", so, we need to resort to redo dump next:</p>
<pre><code class="lang-plaintext">ALTER SYSTEM DUMP LOGFILE '/path/to/arch1_112_1223914063.dbf' SCN MIN 6365975 SCN MAX 6365975;
</code></pre>
<h3 id="heading-26ai">26ai</h3>
<p>All 770 of redo records contain this change on <code>SYS.USER$</code>:</p>
<pre><code class="lang-plaintext">CHANGE #3 ... OP:11.5 ...
ncol: 30 nnew: 1 size: 0
col 18: [ 1]  80
</code></pre>
<p>Column 18, if I'm not mistaken is <code>SPARE1</code> column of <code>SYS.USER$</code> table. Value <code>0x80</code> is Oracle's internal representation of NUMBER <code>0</code>. In <code>DBA_USERS</code>, this column is used with <code>bitand()</code> to check for many different flags.</p>
<p>But - of those 770 redo records, 122 of them, besides the described change, also contain an update of column 23, which is <code>SPARE6</code> column of <code>SYS.USER$</code>, which is of datatype <code>DATE</code> and refers to <code>DBA_USERS.LAST_LOGIN</code>:</p>
<pre><code class="lang-plaintext">ncol: 30 nnew: 2 size: 0
col 18: [ 1]  80
col 23: [ 7]  78 7e 02 10 0b 35 0f
</code></pre>
<p>This tells us, that even though my test case was doing about 12 logins per second, the <code>LAST_LOGIN</code> column was updated two times per second.</p>
<h3 id="heading-19c">19c</h3>
<p>On <code>19.20.0</code>, the situation is pretty similar for each login:</p>
<pre><code class="lang-plaintext">CHANGE #3 ... OP:11.19 ...
Array Update of 1 rows: 
ncol: 28 nnew: 1 size: 0
col 23: [ 7]  78 7e 02 10 10 26 1e
</code></pre>
<p>So, instead of changing column 18, it either applies an empty change array or updates the last login date. Regardless, it still makes and commits a transaction for each login, just like 26ai does.</p>
<h3 id="heading-older-versions">Older versions</h3>
<p>I didn't test older versions, but it might be interesting to know that according <a target="_blank" href="https://docs.oracle.com/database/121/REFRN/GUID-309FCCB2-2E8D-4371-9FC5-7F3B10E2A8C0.htm#REFRN23302">DBA_USERS 12c documentation</a>, the <code>DBA_USERS.LAST_LOGIN</code> column was introduced in version 12c.</p>
<h1 id="heading-disablelastsuccessfullogintime">_disable_last_successful_login_time</h1>
<p>This is the undocumented parameter which greatly reduces redo generation on logins. However, as it is with all undocumented parameters, you should only use them in accordance with Oracle Support guidance.</p>
<p>Here's the same test case with <code>_disable_last_successful_login_time</code> set:</p>
<pre><code class="lang-plaintext">select count(*) as update_count
   from v$logmnr_contents
   where seg_owner='SYS' and seg_name='USER$';

select count(*) as commit_count
   from v$logmnr_contents
   where operation='COMMIT';
</code></pre>
<pre><code class="lang-plaintext">UPDATE_COUNT
------------
           0


COMMIT_COUNT
------------
           9
</code></pre>
<p>So, truly no commit per login anymore. There are 9 commits, yes, but those were unrelated to logins. This not only disables last successful login time update, but also the update on <code>SPARE1</code> - after enabling the parameter, no <code>USER$</code> transaction per login was observed in LogMiner output.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In tested 19c/26ai environments with default settings, each regular user login resulted in a recursive transaction and commit. During a connection storm, these login-driven commits add to the LGWR workload alongside commits from actual application activity. So the hidden cost isn't just the login itself - it's the commit latency impact on every other session. This kind of issue is quite rare, but I find it valuable to know that, by default, there is a transaction after each regular user login.</p>
]]></content:encoded></item><item><title><![CDATA[Reading Oracle ASM Files Directly Using x$kffxp]]></title><description><![CDATA[In a previous post, I explored implementing asmfs using dbms_diskgroup.read(). Since then, asmfs (my open source GitHub project) has gained a new capability: reading files stored in Oracle ASM by directly accessing block devices.
Rather than needing ...]]></description><link>https://blog.srecnik.info/reading-oracle-asm-files-directly-using-xkffxp</link><guid isPermaLink="true">https://blog.srecnik.info/reading-oracle-asm-files-directly-using-xkffxp</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sun, 11 Jan 2026 19:29:43 GMT</pubDate><content:encoded><![CDATA[<p>In a <a target="_blank" href="https://blog.srecnik.info/asmfs-and-dbmsdiskgroupread">previous post</a>, I explored implementing asmfs using <code>dbms_diskgroup.read()</code>. Since then, <a target="_blank" href="https://github.com/usrecnik/asmfs/">asmfs</a> (my open source GitHub project) has gained a new capability: reading files stored in Oracle ASM by directly accessing block devices.</p>
<p>Rather than needing deep knowledge of ASM's internal structures, asmfs can rely on the <code>x$kffxp</code> view, which exposes nearly all the metadata required. In this post, I'll walk through how asmfs leverages this view to read ASM files.</p>
<h2 id="heading-disk-groups-and-allocation-units-au">Disk Groups and Allocation Units (AU)</h2>
<p>Here are the basics: an ASM diskgroup is made of one or more disks. Space for each file is allocated in uniformly sized "chunks" known as <em>extents</em>. The same <em>extent</em> can be written to one (aka <em>external redundancy</em>), two (<em>normal redundancy</em>) or three (<em>high redundancy</em>) different disks.
So, for example, in a <em>high redundancy</em> diskgroup, each <em>extent</em> needs 3 <em>allocation units</em> (AU).</p>
<p>Each ASM diskgroup can have different AU size, but strictly all <em>allocation units</em> (AU) have the same size within one diskgroup. You can define your preferred AU size (1, 2, 4, 8, 16, 32 or 64 MB)  when you create a diskgroup. </p>
<p>We can figure out our current AU size and <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/ostmg/mirroring-diskgroup-redundancy.html">ASM redundancy level</a> (<code>v$asm_diskgroup.type</code>) by querying <code>v$asm_diskgroup</code> like this:</p>
<pre><code>SQL&gt; select group_number, name, type, allocation_unit_size / <span class="hljs-number">1024</span> / <span class="hljs-number">1024</span> <span class="hljs-keyword">as</span> au_size_mb <span class="hljs-keyword">from</span> v$asm_diskgroup;

GROUP_NUMBER NAME     TYPE     AU_SIZE_MB
------------ -------- -------- ----------
           <span class="hljs-number">1</span> DATA     NORMAL            <span class="hljs-number">4</span>

SQL&gt; select disk_number, path <span class="hljs-keyword">from</span> v$asm_disk where group_number=<span class="hljs-number">1</span>;

DISK_NUMBER PATH
----------- ---------------
          <span class="hljs-number">0</span> AFD:DATA1
          <span class="hljs-number">1</span> AFD:DATA2
          <span class="hljs-number">2</span> AFD:DATA3
</code></pre><p>If you're using <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/ostmg/administer-filter-driver.html">AFD</a> then you'll also need to resolve AFD labels to actual block devices like this:</p>
<pre><code>$ afdtool -getdevlist
--------------------------------------------------------------------------------
Label                     Path
================================================================================
DATA1                     /dev/sdd
DATA2                     /dev/sdb
DATA3                     /dev/sde
</code></pre><h2 id="heading-file-number">File Number</h2>
<p>In order to obtain the list of all allocation units (AU) that belong to a given file, we need to obtain the <code>file_number</code> of the file we want to read. It can be obtained by simply querying <code>v$asm_alias</code> like this:</p>
<pre><code>SQL&gt; select name, file_number <span class="hljs-keyword">from</span> v$asm_alias where name = <span class="hljs-string">'SYSTEM.261.1206729191'</span> and group_number=<span class="hljs-number">1</span>;

NAME                           FILE_NUMBER
------------------------------ -----------
SYSTEM<span class="hljs-number">.261</span><span class="hljs-number">.1206729191</span>                  <span class="hljs-number">261</span>
</code></pre><h2 id="heading-xkffxp-extent-map">x$kffxp (Extent Map)</h2>
<p>Now the fun part:</p>
<pre><code>SQL&gt; SELECT
    x.disk_kffxp AS disk_number,
    x.au_kffxp AS allocation_unit
FROM x$kffxp x  
WHERE x.group_kffxp = <span class="hljs-number">1</span>        <span class="hljs-comment">/* :group_number */</span>
    AND x.number_kffxp = <span class="hljs-number">261</span>   <span class="hljs-comment">/* :file_number */</span>
    AND x.lxn_kffxp = <span class="hljs-number">0</span>        <span class="hljs-comment">/* :mirror */</span>
ORDER BY x.xnum_kffxp <span class="hljs-comment">/* extent number */</span>;

DISK_NUMBER ALLOCATION_UNIT
----------- ---------------
          <span class="hljs-number">2</span>             <span class="hljs-number">148</span>
          <span class="hljs-number">0</span>             <span class="hljs-number">147</span>
          <span class="hljs-number">1</span>             <span class="hljs-number">152</span>
...
</code></pre><p>By using first two where conditions (<code>:group_number</code> and <code>:file_number</code>) we select only those allocation units that belong to our file. But because our example diskgroup has <em>normal redundancy</em>, we get two allocation units for each <em>extent</em>.
So, we filter by <code>:mirror</code>, which can be:</p>
<ul>
<li><code>0</code> for primary copy</li>
<li><code>1</code> for secondary copy (available in normal and high redundancy diskgroups)</li>
<li><code>2</code> for tertiary copy (available in high redundancy diskgroups)</li>
</ul>
<p>In asmfs, you can select which <code>:mirror</code> copy you want to use by specifying <code>--mirror</code> parameter to <code>asmfs</code> command (e.g. <code>asmfs --mirror 1</code> for always reading only from secondary copy).</p>
<p>Order is also important, <code>xnum_kffxp</code> is extent number. First extent starts with <code>0</code>, second one is  <code>1</code> etc. A constant value of <code>2147483648</code> refers to the triple-mirrored file metadata.</p>
<p>So, how can we use disk number (<code>x.disk_kffxp</code>) and allocation unit number <code>x.au_kffxp</code>? </p>
<h2 id="heading-running-dd">Running dd</h2>
<p>Based on the example three rows returned by our example query, we can run three <code>dd</code> commands like this:</p>
<pre><code>dd <span class="hljs-keyword">if</span>=<span class="hljs-regexp">/dev/</span>sde bs=<span class="hljs-number">4194304</span> skip=<span class="hljs-number">148</span> count=<span class="hljs-number">1</span> &gt;&gt; <span class="hljs-regexp">/tmp/</span>SYSTEM.DBF
dd <span class="hljs-keyword">if</span>=<span class="hljs-regexp">/dev/</span>sdd bs=<span class="hljs-number">4194304</span> skip=<span class="hljs-number">147</span> count=<span class="hljs-number">1</span> &gt;&gt; <span class="hljs-regexp">/tmp/</span>SYSTEM.DBF
dd <span class="hljs-keyword">if</span>=<span class="hljs-regexp">/dev/</span>sdb bs=<span class="hljs-number">4194304</span> skip=<span class="hljs-number">152</span> count=<span class="hljs-number">1</span> &gt;&gt; <span class="hljs-regexp">/tmp/</span>SYSTEM.DBF
...
</code></pre><p>Let's explain the first command in detail:</p>
<ul>
<li><code>if=/dev/sde</code> because <code>DISK_NUMBER=2</code> from <code>x$kffxp</code> refers to <code>PATH=AFD:DATA3</code> (according to <code>v$asm_disk</code>) which refers to <code>/dev/sde</code> (according to <code>afdtool</code>).</li>
<li><code>bs=4194304</code> because AU size is 4 MB (according to <code>v$asm_diskgroup.allocation_unit_size</code>)</li>
<li><code>skip=148</code> because  allocation unit from <code>x$kffxp.au_kffxp</code> is <code>148</code></li>
<li><code>count=1</code> because we're reading only one AU at a time</li>
</ul>
<p>btw, if you're running AFD, you can only do such <code>dd</code> commands as root because one of the AFD features is to deny non-oracle non-root software direct access to block devices.</p>
<h2 id="heading-file-size">File size</h2>
<p>Note that the file we've created by running all those <code>dd</code> commands <em>must</em> be multiple of AU size (4 MB in our example). But actual Oracle files (e.g. datafiles) can be any multiple of any valid block size (e.g. 8 KB).
In other words, there is a high chance that our file contains garbage at the end of file, which we can get rid of by running <code>truncate</code>:</p>
<pre><code>truncate -s <span class="hljs-number">3659538432</span> /tmp/SYSTEM.DBF
</code></pre><p>The number of bytes to which we need to truncate is obtained simply from <code>v$asm_file</code>:</p>
<pre><code>SQL&gt; select bytes <span class="hljs-keyword">from</span> v$asm_file where file_number=<span class="hljs-number">261</span>;

     BYTES
----------
<span class="hljs-number">3659538432</span>
</code></pre><h2 id="heading-file-headers">File Headers</h2>
<p>By using the approach described above, you'll get file contents as it is written in the ASM. The problem is that if you copy a file using, say <code>rman</code> or <code>asmcmd</code>, then <code>md5sum</code> will be different than what you'll get from described <code>dd</code> &amp; <code>truncate</code> commands.
The difference comes from first block only. Not really the complete first block, but just two 4-byte regions:</p>
<ul>
<li>The 4 bytes from <code>0x20</code> to <code>0x23</code> seems to be the <strong>magic constant</strong>, which is different for files in ASM and for files in a local filesystem. For files in local filesystem, this always seems to be the same magic value <code>0x000081a0</code> in little-endian format.</li>
<li>The 4 bytes from <code>0x10</code> to <code>0x13</code> seems to be the <strong>checksum</strong>. Due to the way this checksum is calculated, we can simply read the existing checksum and XOR it with the previously mentioned magic value.</li>
</ul>
<p>For more details, feel free to read the function <code>fix_header_block</code> in file <a target="_blank" href="https://github.com/usrecnik/asmfs/blob/8bb65295c54a161d522133d4b4d456d5c70559cb/src/fuse.rs#L389">fuse.rs</a>.</p>
<p>Not every file type follows this rule, but most do - datafiles, tempfiles, and archivelogs all behave this way. I stumbled upon this magic constant by doing a simple byte-by-byte comparison of files and noticed it kept showing up with the same value.</p>
<p>Since we're deep in undocumented territory here, there's always a chance I've got something slightly wrong. But so far, it seems to work! :)</p>
<h2 id="heading-getting-started-with-asmfs">Getting started with ASMFS</h2>
<p>Now that you understand how it works, feel free to grab a ready-made <code>asmfs.rpm</code> files compatible with Oracle Linux versions 10, 9, 8 from <a target="_blank" href="https://github.com/usrecnik/asmfs/releases">asmfs releases</a>.
Installation instructions and examples are all in the <a target="_blank" href="https://github.com/usrecnik/asmfs/">README.md</a> of the github project.</p>
<p>If you run into issues or discover edge cases, feel free to open an issue on GitHub.</p>
<h2 id="heading-references">References</h2>
<p>A lot of useful additional information regarding <strong>ASM Metadata and Internals</strong> is also available on <a target="_blank" href="https://twiki.cern.ch/twiki/bin/view/PDBService/ASM_Internals">twiki.cern.ch</a>.</p>
]]></content:encoded></item><item><title><![CDATA[utl_recomp and Automatic Statistics Gathering]]></title><description><![CDATA[When recompiling invalid objects—often after patches, upgrades, or invalid dependency chains—you might call utl_recomp.recomp_serial. Since it's a serial procedure, one could reasonably expect it to avoid any parallel or background job interaction. B...]]></description><link>https://blog.srecnik.info/utlrecomp-and-automatic-statistics-gathering</link><guid isPermaLink="true">https://blog.srecnik.info/utlrecomp-and-automatic-statistics-gathering</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Wed, 03 Dec 2025 10:09:29 GMT</pubDate><content:encoded><![CDATA[<p>When recompiling invalid objects—often after patches, upgrades, or invalid dependency chains—you might call <code>utl_recomp.recomp_serial</code>. Since it's a serial procedure, one could reasonably expect it to avoid any parallel or background job interaction. But Oracle can be persuaded to behave a bit differently…</p>
<pre><code class="lang-plaintext">SQL&gt; exec utl_recomp.recomp_serial('HR');
BEGIN utl_recomp.recomp_serial('HR'); END;

*
ERROR at line 1:
ORA-20000: Unable to gather statistics concurrently: the job_queue_processes parameter is less than 4
</code></pre>
<p>How does this make sense—<em>serial</em> recompilation wants to do <em>concurrent</em> statistics gathering? Also, the <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/UTL_RECOMP.html">UTL_RECOMP documentation</a> does not mention anything regarding statistics (I tried searching for the 'stat' keyword in this doc). So, what and why is it being gathered?</p>
<p>A quick spoiler, this happens if you set both of these:</p>
<pre><code class="lang-plaintext">alter system set job_queue_processes=0;
exec dbms_stats.set_global_prefs('concurrent', 'true');
</code></pre>
<p>Changing either one will make <code>utl_recomp.recomp_serial()</code> succeed.</p>
<p>Also, perhaps worth mentioning, this test case can be reproduced even if the example <code>HR</code> schema is empty.</p>
<h2 id="heading-stats-gathering">Stats Gathering</h2>
<p>This is the full stack trace:</p>
<pre><code class="lang-plaintext">SQL&gt; exec utl_recomp.recomp_serial('HR');
BEGIN utl_recomp.recomp_serial('HR'); END;

*
ERROR at line 1:
ORA-20000: Unable to gather statistics concurrently: the job_queue_processes
parameter is less than 4
ORA-06512: at "SYS.UTL_RECOMP", line 927
ORA-06512: at "SYS.DBMS_STATS", line 40799
ORA-06512: at "SYS.DBMS_STATS", line 5065
ORA-06512: at "SYS.DBMS_STATS", line 40725
ORA-06512: at "SYS.UTL_RECOMP", line 260
ORA-06512: at "SYS.UTL_RECOMP", line 830
ORA-06512: at "SYS.UTL_RECOMP", line 940
ORA-06512: at line 1
</code></pre>
<p>According to the unwrapped <code>utl_recomp</code> source, those lines refer to the following functions:</p>
<pre><code class="lang-plaintext">recomp_serial() 
-&gt; recomp_parallel()                 line 940
-&gt; select_invalid_parallel_objs()    line 830
-&gt; dbms_stats.gather_table_stats()   line 260
</code></pre>
<p>Where, at line 260, there is:</p>
<pre><code class="lang-plaintext">  260      DBMS_STATS.GATHER_TABLE_STATS('SYS', 'UTL_RECOMP_COMPILED',
  261         ESTIMATE_PERCENT =&gt; DBMS_STATS.AUTO_SAMPLE_SIZE,
  262         NO_INVALIDATE =&gt; FALSE);
</code></pre>
<p>So, <code>utl_recomp.recomp_serial</code> truly invokes <code>dbms_stats.gather_table_stats</code> with the intention of gathering statistics for a table called <code>SYS.UTL_RECOMP_COMPILED</code>:</p>
<pre><code class="lang-plaintext">SQL&gt; desc SYS.UTL_RECOMP_COMPILED;
 Name              Null?    Type
 ----------------- -------- ------------
 OBJ#              NOT NULL NUMBER
 BATCH#                     NUMBER
 COMPILED_AT                TIMESTAMP(6)
 COMPLETED_AT               TIMESTAMP(6)
 COMPILED_BY                VARCHAR2(64)
</code></pre>
<p>which contains the list of compiled objects; it tracks which objects were compiled and in which batch.</p>
<p>Anyway, the <code>dbms_stats.gather_table_stats</code> must honor its global preferences, which can be set to use concurrency.</p>
<h2 id="heading-references">References</h2>
<p>This blog post was tested on 19.29.0-EE.</p>
<p>Links to the official Oracle documentation:</p>
<ul>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/UTL_RECOMP.html">UTL_RECOMP</a></p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_STATS.html">DBMS_STATS</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Legacy Clients on 19c]]></title><description><![CDATA[We've just migrated one of the last databases to 19c. The main area of concern for the client was that they were using rather old embedded devices, which had 11g client libraries and connect strings baked into their firmware.
Here's the list of the m...]]></description><link>https://blog.srecnik.info/legacy-clients-on-19c</link><guid isPermaLink="true">https://blog.srecnik.info/legacy-clients-on-19c</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 10 Nov 2025 20:58:47 GMT</pubDate><content:encoded><![CDATA[<p>We've just migrated one of the last databases to 19c. The main area of concern for the client was that they were using rather old embedded devices, which had 11g client libraries and connect strings baked into their firmware.</p>
<p>Here's the list of the main “tricks” we used to make it work.</p>
<h2 id="heading-allowed-logon-version">Allowed Logon Version</h2>
<p>These are the settings from <code>sqlnet.ora</code>:</p>
<pre><code class="lang-plaintext">SQLNET.ALLOWED_LOGON_VERSION_CLIENT=11
SQLNET.ALLOWED_LOGON_VERSION_SERVER=11
</code></pre>
<p>The <code>_CLIENT</code> one sets the minimum authentication protocol version that clients connecting to this database must use. And the <code>_SERVER</code> one sets the minimum authentication protocol that the database will use when acting as a client.</p>
<p>So, now the old clients are allowed to connect.</p>
<p>Just be also aware that allowing older protocols also means allowing <em>weaker</em> protocols.</p>
<h2 id="heading-usesidasservice">USE_SID_AS_SERVICE</h2>
<p>In most cases, you want clients to use <em>service name</em> rather than <em>SID</em> when connecting to a specific PDB. This is because if you connect using <code>SID=</code> to <code>$ORACLE_SID</code>, then you’ll be connected to <code>CDB$ROOT</code> and not to the <code>PDB</code> (which is usually the intention).</p>
<p>But in this case, the client was an embedded device with a hardcoded connect string - so changing the <code>tnsnames.ora</code> or the connect string itself to use <em>service name</em> instead of <em>SID</em> was not an option.</p>
<p>We can convince the listener to consider <em>SIDs</em> as <em>service names</em> by setting the following parameter in <code>listener.ora</code>:</p>
<pre><code class="lang-plaintext">USE_SID_AS_SERVICE_LISTENER = ON
</code></pre>
<h2 id="heading-static-listener-service">Static Listener Service</h2>
<p>However, there is a complication when the database is also using <code>db_domain</code>. This domain becomes the suffix to every service name that our database is using.</p>
<p>One way around this is to create a static <code>listener.ora</code> entry like this:</p>
<pre><code class="lang-plaintext">SID_LIST_LISTENER =
  (SID_LIST =
    (SID_DESC =
      (GLOBAL_DBNAME = sfx)
      (ORACLE_HOME = /oracle/db_se/19.28.0/dbhome_1)
      (SID_NAME = sfxc)
      (SERVICE_NAME = sfx.example.com)
    )
  )
</code></pre>
<p>This will create a new service in the listener called <code>sfx</code>. It is also the <em>SID</em> used by our embedded client.</p>
<p><code>(SID_NAME = sfxc)</code> is the actual <code>$ORACLE_SID</code> that is running from <code>$ORACLE_HOME</code>. And since we want to connect to a specific PDB, we can also specify the default service name for this particular PDB, which in this example is <code>(SERVICE_NAME = sfx.example.com)</code>.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/netrf/parameters-for-the-sqlnet.ora.html#GUID-B2908ADF-0973-44A9-9B34-587A3D605BED">ALLOW_LOGON_VERSION_CLIENT</a></p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/netrf/oracle-net-listener-parameters-in-listener-ora.html#GUID-5055BBB9-26E5-465D-B79A-A712FADF3595">USE_SID_AS_SERVICE_listener</a></p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/spmsu/adding-static-service-to-listener.html">SID_LIST_listener</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Oracle Flashback Database Automatic Deactivation]]></title><description><![CDATA[Today I learned that Oracle can disable the Flashback Database feature by itself automatically. Here's the reason - an excerpt from the alert log:
ORA-38886: WARNING: Flashback database was disabled due to error when writing flashback database logs.
...]]></description><link>https://blog.srecnik.info/oracle-flashback-database-automatic-deactivation</link><guid isPermaLink="true">https://blog.srecnik.info/oracle-flashback-database-automatic-deactivation</guid><category><![CDATA[Oracle Database]]></category><category><![CDATA[Oracle]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Fri, 26 Sep 2025 19:58:29 GMT</pubDate><content:encoded><![CDATA[<p>Today I learned that Oracle can disable the Flashback Database feature by itself automatically. Here's the reason - an excerpt from the alert log:</p>
<pre><code class="lang-plaintext">ORA-38886: WARNING: Flashback database was disabled due to error when writing flashback database logs.
</code></pre>
<p>Apparently, this is not a new behavior, as there's a <a target="_blank" href="https://jhdba.wordpress.com/2010/01/06/flashback-disabled-automatically-a-minor-rant/">blog post about it</a> from the year 2010 by John Hallas.</p>
<p>But there have been improvements since he wrote this post. In 19c, the flashback logging (<code>alter database flashback on/off</code>) can be executed while the database is in <code>READ WRITE</code> state (no need to bounce it anymore).</p>
<p>I also agree with John that a parameter, which would cause flashback to behave similarly to a <code>MANDATORY</code> archivelog destination, would be a welcome addition. To my knowledge, such a parameter does not exist.</p>
<h2 id="heading-official-docs">Official Docs</h2>
<p>Here's what the <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/bradv/using-flasback-database-restore-points.html#GUID-E90F7FBD-CCE2-4234-A3B4-FDFEC3BC2E90">official docs</a> say about that:</p>
<p><em>The following rules govern creating, retaining, overwriting and deleting of flashback logs in the fast recovery area:</em></p>
<ul>
<li><p><em>If the fast recovery area has enough space, then a flashback log is created whenever necessary to satisfy the flashback retention target.</em></p>
</li>
<li><p><em>If a flashback log is old enough that it is no longer needed to satisfy the flashback retention target, then the flashback log may be reused or deleted.</em></p>
</li>
<li><p><em>If the database must create a flashback log and the fast recovery area is full or</em> <strong><em>there is no disk space, then the oldest flashback log is reused instead.</em></strong></p>
</li>
</ul>
<h2 id="heading-test-cases">Test Cases</h2>
<p>I decided to create a test case to replicate the issue of flashback disabling itself. According to the docs, it shouldn’t easily happen due to space pressure, except, it seems, if there are actual I/O errors due to out-of-space conditions.</p>
<p>In both test cases, there were no Restore Points in use (empty <code>v$restore_point</code>).</p>
<h3 id="heading-case-1-mountpoint-2g-smaller-than-dbrecoveryfiledestsize-10g">Case 1: mountpoint (2G) smaller than db_recovery_file_dest_size (10G)</h3>
<pre><code class="lang-plaintext">NAME                           VALUE              COMMENT
------------------------------ --------------- ----------
db_recovery_file_dest          /fra                  2 GB
db_recovery_file_dest_size     10737418240          10 GB
db_flashback_retention_target  1440                 24 h

FLASHBACK_ON       LOG_MODE
------------------ ------------
YES                ARCHIVELOG
</code></pre>
<p>On the first try, when the space ran out, I got this in the alert log:</p>
<pre><code class="lang-plaintext">2025-09-25T23:01:41.098262+02:00                                       
Errors in file /oracle/diag/rdbms/orcl/orcl/trace/orcl_gen0_1207793.trc:                                                                      
ORA-38701: Flashback database log 10 seq 1 thread 1: "/fra/ORCL/flashback/o1_mf_nfccfnv4_.flb"                                                                                                                                                                                              
ORA-27072: File I/O error                                              
Additional information: 4                                              
Additional information: 1153                                           
Additional information: 90112                                          
2025-09-25T23:01:41.098454+02:00                                       
Deleted Oracle managed file /fra/ORCL/flashback/o1_mf_nfccfnv4_.flb
</code></pre>
<p>But it did not stop the database from continuing to work. The only side effect was, as documented, <code>V$FLASHBACK_DATABASE_LOG.OLDEST_FLASHBACK_TIME</code> shifted forward as the oldest flb log(s) got deleted (the docs say “reuse,” but according to this test case, old ones are deleted so that new ones can appear).</p>
<p>Note that <code>df</code> reported a little bit of space still left (~9mb):</p>
<pre><code class="lang-plaintext">[root@db-server fra]# df -h .
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdg        2.0G  1.8G  9.1M 100% /fra
</code></pre>
<p>So, I decided to use <code>dd</code> to ensure there is absolutely no space left (by creating dummy file(s)):</p>
<pre><code class="lang-plaintext">[root@db-server fra]# df -h /fra
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdg        2.0G  1.9G     0 100% /fra
</code></pre>
<p>Finally I got:</p>
<pre><code class="lang-plaintext">Errors in file /oracle/diag/rdbms/orcl/orcl/trace/orcl_gen0_1207793.trc:
ORA-38701: Flashback database log 10 seq 1 thread 1: "/fra/ORCL/flashback/o1_mf_%u_.flb"
ORA-27044: unable to write the header block of file
Linux-x86_64 Error: 28: No space left on device
</code></pre>
<p>And a bit later, finally:</p>
<pre><code class="lang-plaintext">ORA-38886: WARNING: Flashback database was disabled due to error when writing flashback database logs.
ORA-38701: Flashback database log 10 seq 12 thread 1: "/fra/ORCL/flashback/o1_mf_%u_.flb"
ORA-27044: unable to write the header block of file
Linux-x86_64 Error: 28: No space left on device
</code></pre>
<p>And, to confirm that it was actually disabled:</p>
<pre><code class="lang-plaintext">SQL&gt; select flashback_on from v$database;

FLASHBACK_ON
------------------
NO
</code></pre>
<h3 id="heading-case-2-mountpoint-2g-gt-dbrecoveryfiledestsize-1g">Case 2: mountpoint (2G) &gt; db_recovery_file_dest_size (1G)</h3>
<p>Now, given the observations from the first case, I would imagine that this only happens on an OS-level <strong><em>error</em></strong>. The chance of an out-of-space OS error occurring is much smaller if <code>db_recovery_file_dest_size</code> is correctly configured—not like I did in the previous case just to prove a point.</p>
<pre><code class="lang-plaintext">NAME                           VALUE             GB_VALUE
------------------------------ --------------- ----------
db_recovery_file_dest          /fra
db_recovery_file_dest_size     1073741824               1
db_flashback_retention_target  1440
</code></pre>
<p>Now, we do get a warning like this, but nothing is disabled because of space pressure:</p>
<pre><code class="lang-plaintext">ORA-19815: WARNING: db_recovery_file_dest_size of 1073741824 bytes is 93.69% used, and has 67731456 remaining bytes available.
2025-09-25T23:48:47.961667+02:00
************************************************************************
You have following choices to free up space from recovery area:
1. Consider changing RMAN RETENTION POLICY. If you are using Data Guard,
   then consider changing RMAN ARCHIVELOG DELETION POLICY.
2. Back up files to tertiary device such as tape using RMAN
   BACKUP RECOVERY AREA command.
3. Add disk space and increase db_recovery_file_dest_size parameter to
   reflect the new space.
4. Delete unnecessary files using RMAN DELETE command. If an operating
   system command was used to delete files, then use RMAN CROSSCHECK and
   DELETE EXPIRED commands.
************************************************************************
Reclaimable space = 0 
Available space = 67731456 
Disk space limit = 1073741824 
See trace file /oracle/diag/rdbms/orcl/orcl/trace/orcl_m000_1211049.trc for usage information from V$RECOVERY_AREA_USAGE
</code></pre>
<p>I’ve let redo be generated for about 24h straight, and even after RVWR wrote about 50GB of data, the FRA (Fast Recovery Area) did not get disabled and no out-of-space errors occurred.</p>
<h2 id="heading-how-to-avoid">How to Avoid</h2>
<p>Having sufficient space for your FRA and maybe adding an additional check to your monitoring system (which is what we just did) is probably the safest bet.</p>
<p>Also, as demonstrated, having a dedicated mount point (or ASM disk group) for <code>db_recovery_file_dest</code> and having a correctly configured <code>db_recovery_file_dest_size</code> can significantly reduce the chance of this happening.</p>
<p>For monitoring purposes, you might want to distinguish between databases where Flashback was previously enabled (and later disabled) versus databases where it was never enabled (nor intended to). Based on my limited testing on 19.28.0, I observed an interesting pattern: when Flashback Database is disabled, Oracle removes the oracle-managed files but leaves behind an empty <code>./flashback/</code> directory within the FRA. In contrast, databases that never had Flashback enabled don't have this directory at all.</p>
<p><strong>Important caveat:</strong> This is my preliminary observation based on a quick test and should be validated more thoroughly before relying on it for monitoring.</p>
]]></content:encoded></item><item><title><![CDATA[ASMFS & dbms_diskgroup.read]]></title><description><![CDATA[Here's what I've learned while trying to implement an ASMFS (which is an open-source GitHub project) and where my implementation fell short of my initial expectations.
Initial Expectations
I can access v$asm_file and v$asm_alias, which provide a comp...]]></description><link>https://blog.srecnik.info/asmfs-and-dbmsdiskgroupread</link><guid isPermaLink="true">https://blog.srecnik.info/asmfs-and-dbmsdiskgroupread</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sun, 17 Aug 2025 18:00:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755451222453/0cbf8982-dbd0-41e5-857e-0c4f75764d97.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's what I've learned while trying to implement an <a target="_blank" href="https://github.com/usrecnik/asmfs">ASMFS</a> (which is an open-source <a target="_blank" href="https://github.com/usrecnik/asmfs">GitHub project</a>) and where my implementation fell short of my initial expectations.</p>
<h2 id="heading-initial-expectations">Initial Expectations</h2>
<p>I can access <code>v$asm_file</code> and <code>v$asm_alias</code>, which provide a complete directory structure in a documented way. So, I can render those as a real filesystem, and I can treat aliases as symlinks.</p>
<p>But can I read the contents of those files? Well, according to a quick Google search, apparently, I can do that also, using the <code>dbms_diskgroup.read()</code> call.</p>
<p>So, I thought, awesome, let's go! The idea needed a bit of fiddling with how to map each file/symlink to a specific inode number and how to resolve a specific inode number back to the actual <code>v$asm_file</code>. But the official Oracle documentation is solid, so this part worked as envisioned.</p>
<p>However, about a weekend later, I discovered a few limitations of this initial idea.</p>
<h2 id="heading-the-results">The Results</h2>
<p>Since a picture is said to be worth a thousand words, here’s a screenshot. But do read on, as there are limits to what it can do.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755285450866/8c0c560a-ec91-4730-80a9-8ab849c8951d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-limitations-of-dbmsdiskgroupread">Limitations of <code>dbms_diskgroup.read</code></h2>
<p>While accessing performance views and rendering the directory structure worked more or less flawlessly, reading the files using <code>dbms_diskgroup.read</code> comes with a few nuances I did not expect.</p>
<p>Before describing them, please note that this is an <strong>undocumented</strong> procedure. So, everything I describe here could be wrong or could be different in different versions of Oracle ASM. What I'm describing comes from my trial and error, so ymmv.</p>
<p>Here's the procedure signature:</p>
<pre><code class="lang-plaintext">begin dbms_diskgroup.read(:b_handle, :b_offset, :b_length, :b_buffer); end;
</code></pre>
<h3 id="heading-parameter-bhandle">Parameter <code>:b_handle</code></h3>
<p>In order to obtain <code>:b_handle</code> and other needed parameters, you need to call this:</p>
<pre><code class="lang-plaintext">dbms_diskgroup.getfileattr(:b_target, :b_filetype, :b_filesize, :b_blksize);
dbms_diskgroup.open(:b_target, :b_mode, :b_filetype, :b_blksize, :b_handle, :b_pblksize, :b_filesize);
</code></pre>
<p>And when you finish, you need to call:</p>
<pre><code class="lang-plaintext">dbms_diskgroup.close(:b_handle);
</code></pre>
<h3 id="heading-parameters-boffset-and-blength">Parameters <code>:b_offset</code> and <code>:b_length</code></h3>
<p>My trial and error show that <code>:b_offset</code> is expected to be given in the number of <strong>blocks</strong>, while <code>:b_length</code> is expected to be given in the number of <strong>bytes</strong>.</p>
<p>Also, the <code>:b_length</code> must be a multiple of <code>block_size</code> (e.g., a multiple of 8192 for 8k datafiles and 512 bytes for archive logs, etc. - you get the expected block size from the previously mentioned <code>dbms_diskgroup.getfileattr</code>).</p>
<p><code>:b_offset</code> seems to be an <code>IN</code> parameter, and <code>:b_length</code> seems to be an <code>IN OUT</code> parameter, which makes sense: IN is the number of bytes expected to be read, OUT is the number of bytes actually read.</p>
<h3 id="heading-parameter-bbuffer">Parameter <code>:b_buffer</code></h3>
<p>This seems to be <code>OUT RAW(32767)</code> and holds raw data returned by the read call.</p>
<p>Notice how Oracle's max size for the <code>RAW</code> datatype is one byte less than 32K. That means reading datafiles with a 32K block size is likely not possible using this procedure.</p>
<h3 id="heading-block-zero-file-headers">Block zero (file headers)</h3>
<p>Does <code>:b_offset</code> start with 0 or with 1 to return the first block? :)</p>
<p>Well, I tried reading a block with <code>:b_offset=0</code>, and here are the results.</p>
<ul>
<li><p>On 19.7, it reports an error if we try to read one block at offset 0 for datafiles and archivelogs.</p>
</li>
<li><p>On 19.27 and 19.28, it returns data.</p>
</li>
</ul>
<p>So, on versions where data is returned, what is returned?</p>
<p>I tried copying the file using <code>asmcmd cp</code> and <code>rman backup as copy</code>. Those two produced identical results. But what <code>dbms_diskgroup.read</code> returned was a bit different. The first block was a bit different, and the file size was slightly off. All other blocks (<code>:b_offset&gt;=1</code>) were identical.</p>
<p>At first, I thought I made some kind of coding error, which may still be the case, but so far, I think the reason for this lies elsewhere.</p>
<p>Here's the hexdump of block zero of a random archive log, as copied using <code>asmfs/dbms_read</code>:</p>
<pre><code class="lang-plaintext">┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 00 22 00 00 00 00 c0 ff ┊ 00 00 00 00 00 00 00 04 │⋄"⋄⋄⋄⋄××┊⋄⋄⋄⋄⋄⋄⋄•│
│00000010│ 55 27 00 00 00 02 00 00 ┊ 91 fa 02 00 7d 7c 7b 7a │U'⋄⋄⋄•⋄⋄┊××•⋄}|{z│
│00000020│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         │        ┊        │
│00000200│                         ┊                         │        ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
</code></pre>
<p>And here's the one of the same file using <code>asmcmd cp</code>:</p>
<pre><code class="lang-plaintext">┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 00 22 00 00 00 00 c0 ff ┊ 00 00 00 00 00 00 00 04 │⋄"⋄⋄⋄⋄××┊⋄⋄⋄⋄⋄⋄⋄•│
│00000010│ f5 a6 00 00 00 02 00 00 ┊ 91 fa 02 00 7d 7c 7b 7a │××⋄⋄⋄•⋄⋄┊××•⋄}|{z│
│00000020│ a0 81 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │××⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│00000030│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │⋄⋄⋄⋄⋄⋄⋄⋄┊⋄⋄⋄⋄⋄⋄⋄⋄│
│*       │                         ┊                         │        ┊        │
│00000200│                         ┊                         │        ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
</code></pre>
<p>Notice how they're a bit different? It seems to me, that headers in ASM might be stored a bit differently than on filesystem.</p>
<h3 id="heading-file-size">File Size</h3>
<p>The following are copies of the same archivelog; <code>100.arch.1</code> was obtained using <code>asmfs/dbms_diskgroup.read</code>, and the other one, <code>100.arch.2</code>, was obtained using <code>asmcmd cp</code>.</p>
<pre><code class="lang-plaintext">$ ls -l /tmp/100.arch.*
-rwxr-xr-x. 1 oracle oinstall 99951104 Aug 15 13:43 /tmp/100.arch.1
-rw-r-----. 1 oracle oinstall 99951616 Aug 15 13:43 /tmp/100.arch.2
</code></pre>
<p>Notice the size in bytes, a 512-byte difference. Let's compare this to what <code>v$asm_file</code> says:</p>
<pre><code class="lang-plaintext">select a.file_number, f.bytes, f.blocks, f.block_size, f.blocks*f.block_size as bb_check
    from v$asm_alias a
    join v$asm_file f on f.file_number = a.file_number 
    where a.name='thread_1_seq_100.288.1207485831';

FILE_NUMBER      BYTES     BLOCKS BLOCK_SIZE   BB_CHECK
----------- ---------- ---------- ---------- ----------
        288   99951616     195218        512   99951616
</code></pre>
<p>All OK, but, look at what <code>dbms_diskgroup.getfileattr</code> (which provides arguments for open() and read() calls) returns:</p>
<pre><code class="lang-plaintext">set serveroutput on;
declare
    b_target varchar2(500) := '+DATA/DBSE/ARCHIVELOG/2025_07_26/thread_1_seq_100.288.1207485831';
    b_filetype number;
    b_filesize number;
    b_blksize number;
begin
    dbms_diskgroup.getfileattr(b_target, b_filetype, b_filesize, b_blksize);
    dbms_output.put_line('file_type=' || b_filetype);
    dbms_output.put_line('file_size=' || b_filesize);
    dbms_output.put_line('blksize=' || b_blksize);
end;
/
</code></pre>
<pre><code class="lang-plaintext">file_type=4
file_size=195217
blksize=512
</code></pre>
<p>It's one block less than what <code>v$asm_file</code> reports! That's why <a target="_blank" href="https://github.com/usrecnik/asmfs">asmfs</a> files are one block shorter than those copied using <code>asmcmd cp</code>.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>If anyone has a hint on how to "persuade" <code>dbms_diskgroup.read()</code> to return header blocks as they should be on a regular file system, then this filesystem could become much more than it currently is.</p>
<p>Consider how awesome it would be to access complete ASM diskgroups from a remote server as if the files were on a local filesystem. Well, I look forward to technical discussions at the next *OUG events. Also, feel free to reach out to me with ideas.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/V-ASM_FILE.html">v$asm_file</a></p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/V-ASM_ALIAS.html">v$asm_alias</a></p>
</li>
<li><p>How to Dump or Extract a Raw Block From a File Stored in an ASM Diskgroup (Doc ID 603962.1)</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Navigating Through Incarnations on a Physical Standby Database]]></title><description><![CDATA[We deal a lot with physical standby databases on Standard Edition. Some may call it "poor man's Data Guard." We call it Deja-Vu, and it's capable of much more than just keeping a database consistently recovered. The main purpose of Deja-Vu is to quic...]]></description><link>https://blog.srecnik.info/navigating-through-incarnations-on-a-physical-standby-database</link><guid isPermaLink="true">https://blog.srecnik.info/navigating-through-incarnations-on-a-physical-standby-database</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sun, 20 Jul 2025 22:40:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752790137387/6aa6a721-ff71-41ea-8d84-b7968cce6932.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We deal a lot with physical standby databases on Standard Edition. Some may call it "poor man's Data Guard." We call it <a target="_blank" href="https://www.abakus.si/en/products/software/dejavu">Deja-Vu</a>, and it's capable of much more than just keeping a database consistently recovered. The main purpose of <a target="_blank" href="https://www.abakus.si/en/products/software/dejavu">Deja-Vu</a> is to quickly provision clones of production databases.</p>
<p>One thing we haven't quite automated yet is recovery through resetlogs, mostly because it's virtually impossible to <em>automatically</em> choose the correct incarnation when it doesn't even exist yet.</p>
<p>Regardless of how you recover your databases, this blog might provide insight on how to handle incarnations on standby databases.</p>
<p>OK, let's go slow. What is an incarnation?</p>
<h2 id="heading-incarnation">Incarnation</h2>
<p>A new incarnation is created each time a production database is opened using resetlogs (thus not completely recovered). Consider a scenario where a production database needs to be restored to the state it was in at, say, 20:00. But the decision to restore was made at, say, 22:00. So, we need to "discard" the last two hours of data.</p>
<p>On Enterprise Edition one might simply perform <code>flashback database</code>. On SE though, we can, by-the-book, do a complete restore and then <code>recover until 20:00</code>. Regardless, once the restore/flashback is completed, a resetlogs must occur.</p>
<p>And this creates a new incarnation.</p>
<p>Let's also suppose that a new archived log was created every 15 minutes (which can be achieved using the <code>ARCHIVE_LAG_TARGET</code> parameter on a database without many changes).</p>
<p>In the following image, incarnation #1 is shown in green, and incarnation #2 is shown in blue. The numbers that appear at 15-minute intervals are sequence numbers of archived logs:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752773046880/aafc3bb8-0a4b-4b9d-9252-9cba489a9303.png" alt class="image--center mx-auto" /></p>
<p>One thing we can observe from this image is that a wall time (e.g., 21:00) determines the exact incarnation (notice how the "blue" one starts only after the "green" one has ended?). This is always true because only one incarnation can be active at a time on the primary database.</p>
<h2 id="heading-standby">Standby</h2>
<p>Suppose that our standby database is currently recovered up to 21:00. So, first of all, we need to go "back in time," to before the "fork"; that is, to at most 19:59 (because the primary was recovered until 20:00 before resetlogs occurred). We can do a simple restore/recover in a classical SE scenario or maybe use the functionality of underlying software/hardware (such as <a target="_blank" href="https://www.abakus.si/en/products/software/dejavu">Deja-Vu</a>) to revert physical files to a state as they were at some time in the past (before 20:00). Anyway, we’re focused on the incarnations, rather than backup/restore operations in this post.</p>
<p>OK, now we’re at 19:59. If we check the incarnations known to the standby database, we only see incarnation #1 (the green one):</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">select</span> controlfile_time, current_scn <span class="hljs-keyword">from</span> v$<span class="hljs-keyword">database</span>;

CONTROLFILE_TIME    CURRENT_SCN
<span class="hljs-comment">------------------- -----------</span>
2025-07-17 19:59:00    968507

SQL&gt; <span class="hljs-keyword">select</span> <span class="hljs-keyword">status</span>, incarnation<span class="hljs-comment">#, resetlogs_time, resetlogs_change# from v$database_incarnation;</span>

<span class="hljs-keyword">STATUS</span>  INCARNATION<span class="hljs-comment"># RESETLOGS_TIME      RESETLOGS_CHANGE#</span>
<span class="hljs-comment">------- ------------ ------------------- -----------------</span>
<span class="hljs-keyword">CURRENT</span>            <span class="hljs-number">1</span> <span class="hljs-number">2025</span><span class="hljs-number">-07</span><span class="hljs-number">-17</span> <span class="hljs-number">18</span>:<span class="hljs-number">32</span>:<span class="hljs-number">55</span>                 <span class="hljs-number">1</span>
</code></pre>
<p>So how do we make incarnation #2 (blue one) known to this standby database?</p>
<p>One obvious (although usually unnecessary) solution is to make another backup of production controlfile and use it to replace the current standby controlfile.</p>
<p>Another one is to simply <strong>catalog the first archive log of the (currently) unknown incarnation</strong>.</p>
<pre><code class="lang-sql">RMAN&gt; catalog <span class="hljs-keyword">start</span> <span class="hljs-keyword">with</span> <span class="hljs-string">'/path/to/1_1_1206745458.dbf'</span> <span class="hljs-keyword">noprompt</span>;
</code></pre>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">select</span> <span class="hljs-keyword">status</span>, incarnation<span class="hljs-comment">#, resetlogs_time, resetlogs_change# from v$database_incarnation;</span>

<span class="hljs-keyword">STATUS</span>  INCARNATION<span class="hljs-comment"># RESETLOGS_TIME      RESETLOGS_CHANGE#</span>
<span class="hljs-comment">------- ------------ ------------------- -----------------</span>
<span class="hljs-keyword">PARENT</span>             <span class="hljs-number">1</span> <span class="hljs-number">2025</span><span class="hljs-number">-07</span><span class="hljs-number">-17</span> <span class="hljs-number">18</span>:<span class="hljs-number">32</span>:<span class="hljs-number">55</span>                 <span class="hljs-number">1</span>
<span class="hljs-keyword">CURRENT</span>            <span class="hljs-number">2</span> <span class="hljs-number">2025</span><span class="hljs-number">-07</span><span class="hljs-number">-17</span> <span class="hljs-number">21</span>:<span class="hljs-number">45</span>:<span class="hljs-number">00</span>            <span class="hljs-number">968776</span>
</code></pre>
<p>Great, now our standby controlfile knows about both incarnations and is currently at a time where we can choose either one. Note that instead of querying <code>v$database_incarnation</code> we could also use RMAN’s <code>list incarnations</code> command to display the same data.</p>
<p>Note that column <code>RESETLOGS_TIME</code> tells us when resetlogs occurred (it has nothing to do with <code>until time</code> to which incomplete recovery was set). So, according to our image above, this happened at about 21:45.</p>
<p>But how do we make a choice?</p>
<h2 id="heading-selecting-the-incarnation">Selecting the Incarnation</h2>
<pre><code class="lang-sql">RMAN&gt; <span class="hljs-keyword">reset</span> <span class="hljs-keyword">database</span> <span class="hljs-keyword">to</span> incarnation &lt;incarnation<span class="hljs-comment">#&gt;;</span>
</code></pre>
<p>So, if we’ve reset the incarnation to #1, then:</p>
<ul>
<li><p>Oracle will apply archive logs [ … 36, 37, 38, <strong>39, 40, 41, 42,</strong> …]</p>
</li>
<li><p>Incarnation #1 becomes <code>CURRENT</code> while Incarnation #2 becomes <code>ORPHAN</code> (not used)</p>
</li>
</ul>
<p>and if we’ve reset to incarnation #2, then:</p>
<ul>
<li><p>Oracle will apply archive logs [ … 36, 37, 38, <strong>1, 2, 3, 4,</strong> … ].</p>
</li>
<li><p>Incarnation #1 becomes <code>PARENT</code> (to incarnation #2) while incarnation #2 becomes <code>CURRENT</code></p>
</li>
</ul>
<p>Note that we don’t need to use RMAN to recover the standby database once the incarnation is selected, we can simply use:</p>
<pre><code class="lang-sql">SQL&gt; recover standby database until cancel;
</code></pre>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/V-DATABASE_INCARNATION.html">v$database_incarnation</a> view documentation</p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/rcmrf/RESET-DATABASE.html">reset database</a> RMAN command documentation</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Why Are My Oracle Sessions Getting SIGSTOP’d?]]></title><description><![CDATA[Finally, I encountered a case that couldn't be solved using traditional Oracle Database tools. I've been experimenting with eBPF for some time, and this is a real-world scenario from an Exadata environment where I needed eBPF to help me truly underst...]]></description><link>https://blog.srecnik.info/why-are-my-oracle-sessions-getting-sigstopd</link><guid isPermaLink="true">https://blog.srecnik.info/why-are-my-oracle-sessions-getting-sigstopd</guid><category><![CDATA[bpftrace]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sun, 08 Jun 2025 18:56:40 GMT</pubDate><content:encoded><![CDATA[<p>Finally, I encountered a case that couldn't be solved using traditional Oracle Database tools. I've been experimenting with eBPF for some time, and this is a real-world scenario from an Exadata environment where I <em>needed</em> eBPF to help me truly understand the problem.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>Users complained that database sessions were occasionally "frozen." Interestingly, these "frozen" sessions could not be killed. When DBAs used <code>alter system kill session</code>, those sessions stayed in the <code>KILLED</code> state.</p>
<p>Also, the load average of the machine where this was happening was suspiciously high all the time, even though no one complained about things running "too slow."</p>
<h2 id="heading-the-investigation">The Investigation</h2>
<p>I'll keep this story brief: the sessions couldn't be killed because their dedicated server process was STOPPED. By STOPPED, I mean the Linux process was in a <code>T</code> state. You can replicate this by sending a SIGSTOP signal to any Linux process, like this:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">kill</span> -SIGSTOP 12345
</code></pre>
<p>So, why is Oracle stopping those sessions? Initially, I suspected it might be related to Oracle Resource Manager and <code>cpu_count</code> settings because the load was high. However, as far as I know, Oracle doesn't use STOP/CONTINUE signals for resmgr. I even ran a quick test in my lab using resmgr under heavy load and did not see any such signals being sent.</p>
<h2 id="heading-observing-the-signals">Observing the Signals</h2>
<p>This is where the <a target="_blank" href="https://bpftrace.org/"><code>bpftrace</code></a> comes into play. We can use it to detect when the signal is sent (or received) and who sent it. And it isn't really a voodoo, all you need is a simple <a target="_blank" href="https://bpftrace.org/"><code>bpftrace</code></a> script:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/usr/bin/env -S bpftrace </span>

tracepoint:signal:signal_generate /args-&gt;sig == 19/ {
    time(<span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>);
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">" SIGSTOP sent from %d (%s) to %d (%s)\n"</span>, pid, comm, args-&gt;pid, args-&gt;comm);
}
</code></pre>
<p>I was hoping to let this script run for a week or so and then see which processes were sending SIGSTOP signals. But to my surprise, there were tons of such signals, many of them within the same second.</p>
<p>When I tried to see details of the process that sent the signal (the pid is printed using the script above), I noticed that such a process no longer existed. Even when I added printing info about the pid into the <code>bpftrace</code> script, like this:</p>
<pre><code class="lang-c">system(<span class="hljs-string">"cat /proc/%d/stack"</span>, pid);
</code></pre>
<p>I noticed that most of the time the process did not exist anymore when my script reached this call. So, they were very <em>short-lived</em> processes. Using a similar approach (reading <code>PPid</code> from <code>/proc/pid/status</code>), I found the parent process that was spawning them. And lo and behold, the parent process was the Oracle database session process itself!</p>
<p>Turns out, the Oracle database process was spawning a child process every second or so to send a STOP signal, do something, then send a CONTINUE signal again.</p>
<p>Why would Oracle do that?!</p>
<h2 id="heading-the-why">The Why</h2>
<p>To understand why, I tried profiling a database session, which was waiting on <code>SQL*Net message from client</code> for a while now but was still receiving STOP/CONTINUE signals.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749143976659/b5b615d0-9d36-4962-b3bb-b7a9ebe4e0b7.png" alt class="image--center mx-auto" /></p>
<p>I’ve blurred out functions that were named like <code>Something::doSomething</code>. Notice how it looks like C++ code? If I remember correctly, most of Oracle's core is written in C. Could this really be Oracle code?!</p>
<p>Turns out that it is <em>not</em>. Inspecting shared libraries used by this process, like this:</p>
<pre><code class="lang-sql">cat /proc/12345/maps
</code></pre>
<p>proved, that Oracle process was using <code>.so</code> libraries from <code>/opt/some-other-product</code>.</p>
<p>So, it's not even an Oracle code; it's code from a product, that hooked Oracle database sessions in order to extract session-related info in a consistent manner.</p>
]]></content:encoded></item><item><title><![CDATA[Proxy Users & Schema Only Accounts]]></title><description><![CDATA[Remember those MYAPP/MYAPP@MYDB logons? With all the security buzz and auditors out there, I thought of those as a thing of the past. This blog won't preach about the importance of strong passwords. Instead, I'll explain which capabilities of Oracle ...]]></description><link>https://blog.srecnik.info/proxy-users-schema-only-accounts</link><guid isPermaLink="true">https://blog.srecnik.info/proxy-users-schema-only-accounts</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 20 Jan 2025 18:44:11 GMT</pubDate><content:encoded><![CDATA[<p>Remember those <code>MYAPP/MYAPP@MYDB</code> logons? With all the security buzz and auditors out there, I thought of those as a thing of the past. This blog won't preach about the importance of strong passwords. Instead, I'll explain which capabilities of Oracle Database we can use to avoid the need for such logons and what I consider to be the best practice for them.</p>
<h2 id="heading-schema-only-accounts">Schema Only Accounts</h2>
<p>They were introduced in 18c, and they allow us to create a user (who is the owner of objects, such as tables or views) <em>without a password</em>. Such a user cannot log on to the database.</p>
<p>It's simple to create such an account:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> MYAPP <span class="hljs-keyword">no</span> <span class="hljs-keyword">authentication</span>;
</code></pre>
<p>Or, if you already have an existing user, you can modify it:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">alter</span> <span class="hljs-keyword">user</span> MYAPP <span class="hljs-keyword">no</span> <span class="hljs-keyword">authentication</span>;
</code></pre>
<p>On older databases, we could achieve a similar result by setting an “impossible” password hash, as explained on <a target="_blank" href="http://www.petefinnigan.com/weblog/archives/00001469.htm">Pete Finnigan's Oracle Security Weblog</a>.</p>
<p>Note that traditionally, on older databases, simply revoking <code>CREATE SESSION</code> from the <code>MYAPP</code> user and then locking the account doesn’t really work because then you cannot log in to this schema via a proxy user, as explained in the next chapter.</p>
<p>So, now we have a user who has access to all the objects it owns - but can't log on.</p>
<h2 id="heading-oracle-proxy-users">Oracle Proxy Users</h2>
<p>These are the answer to how developers can create/modify the objects owned by <code>MYAPP</code> user. Every developer should have his/her own account on the database. If there are many developers/users working on many databases, then maybe consider using <em>externally authenticated</em> users (such as those from MS Active Directory/Kerberos, Radius, etc). The point here is that, regardless of how developers' accounts are authenticated, there should be a database user for each developer.</p>
<p>Let's suppose that we have a developer named <code>SCOTT</code>. So we create a traditional database account for him:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> SCOTT <span class="hljs-keyword">identified</span> <span class="hljs-keyword">by</span> <span class="hljs-string">"tiger"</span>;
</code></pre>
<p>Notice how we didn't grant <em>anything</em> to user <code>SCOTT</code>? At this point, he cannot even connect to the database, because he's lacking the <code>CREATE SESSION</code> privilege. Which we don't need to grant. Now, for the fun part:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">alter</span> <span class="hljs-keyword">user</span> MYAPP <span class="hljs-keyword">grant</span> <span class="hljs-keyword">connect</span> <span class="hljs-keyword">through</span> SCOTT;
SQL&gt; <span class="hljs-keyword">grant</span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SESSION</span> <span class="hljs-keyword">to</span> MYAPP;
</code></pre>
<p>We only allowed the schema owner, <code>MYAPP</code>, to connect <em>through</em> <code>SCOTT</code>. So, if <code>SCOTT</code> wants to use <code>sqlplus</code> to work on the <code>MYAPP</code> schema, he would connect like this:</p>
<pre><code class="lang-plaintext">$ sqlplus scott[myapp]/tiger@MYDB
SQL&gt; select user from dual;

USER
-----------
MYAPP
</code></pre>
<p>So, he used his personal password to connect to the database, and yet he is working as the <code>MYAPP</code> user with all the permissions that the <code>MYAPP</code> user has.</p>
<h2 id="heading-database-design-and-api-schemas">Database Design and "API Schemas"</h2>
<p>We already have <code>MYAPP</code>, which is the main schema owner with all the data, that is, all the tables. It can also contain packages and views with logic that is common to all API schemas. However, only developers, using proxy users, can connect. What about application servers and other end-user applications that need data from this schema?</p>
<p>Most importantly, no end-user should connect to <code>MYAPP</code> directly. Instead, create API schemas, such as, say <code>MYAPP_GUI</code>. There could be others, e.g., <code>MYAPP_CLI</code>. This API schema should have views and packages that are essentially wrappers for what <code>MYAPP</code> provides. This allows access to only the parts of <code>MYAPP</code> that are relevant for the interface and, equally important, exposes the interface so that its users can easily access it. For example, some libraries in certain languages have difficulty specifying <code>RECORDs</code> as parameters to package functions.</p>
<p>If clients (that is, applications) connect to those API schemas directly, we have already made a step forward from where this blog started. But we should still go a step further.</p>
<p>We can create those API schemas with <code>no authentication</code> in the same way we did with our main <code>MYAPP</code> schema. Then we can let end users connect using their own usernames and grant each of them roles, which allow them to execute actions on API schemas. After logging on, they would do something like:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">alter</span> <span class="hljs-keyword">session</span> <span class="hljs-keyword">set</span> current_schema=MYAPP_GUI;
</code></pre>
<h2 id="heading-application-servers-amp-connection-pools">Application Servers &amp; Connection Pools</h2>
<p>Traditional applications, such as Oracle Forms, tended to allow login using an end-user database account. And everything described above "just works." However, today, most web-based applications prefer to have a connection pool.</p>
<p>The problem with a connection pool is that it establishes a fixed number of connections, e.g., 30 connections. All of them are established with the <em>same</em> database user. So far, this is how we’ve tackled this problem:</p>
<p>About ten years ago, I wrote a connection pool compatible with the WildFly application server, which establishes connections with a user that has no privileges at all. Let's call it <code>POOL_USER</code>. The only privilege that this <code>POOL_USER</code> has is <code>CREATE SESSION</code>. We let it establish all the connections with this user. However, whenever we take a connection from the pool, we call:</p>
<p><code>oracle.jdbc.OracleConnection.openProxySession()</code>, which essentially does the same as we showed with the <code>sqlplus</code> example earlier in the post. And whenever we return a connection to the pool, we call <code>conn.close(OracleConnection.PROXY_SESSION)</code>, which returns to the unauthenticated context of being logged in as our <code>POOL_USER</code>, no longer as the real end-user. For more info, check <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#openProxySession\(int,java.util.Properties\)">Java Doc</a>.</p>
<p>While this worked, it had a drawback - most Java applications would release database connections as quickly as possible (according to Java best practices). But that means developers can't use PL/SQL session state because it is lost after every page load. So, a few years later, I wrote H2DPool, which matches an HTTP session with one or many database sessions. As long as the user is logged in, the user has a database session established. That way, we're logging in as end-users and utilizing all the power of PL/SQL. And yes, this is no longer really a "pool," even though I named it so, because connections are established on application logon and closed on logout (or on session timeout).</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>This year, my colleague took my work on H2DPool even further and added support for OIDC, which may be the topic for another blog post. We never made those pool implementations publicly available; I just wanted to illustrate one of the possible ways to stay true to the guidelines presented in this blog in today’s world where 2FA, central user management and similar requirements are expected from most applications.</p>
<p>You can also read about topics from this post on <a target="_blank" href="https://oracle-base.com/articles/misc/introduction-to-plsql#my-utopian-development-environment">oracle-base.com</a>. Maybe the principles discussed here were considered utopian there back in 2005, but by sharing those ideas, evidently, they can eventually become reality.</p>
]]></content:encoded></item><item><title><![CDATA[Oracle Native Network Encryption]]></title><description><![CDATA[As I come across environments where SQL*Net is not encrypted I sometimes also notice that there are two common misconceptions about it:

First, some are afraid that certificates must be involved and consequently maintaining a PKI infrastructure is de...]]></description><link>https://blog.srecnik.info/oracle-native-network-encryption</link><guid isPermaLink="true">https://blog.srecnik.info/oracle-native-network-encryption</guid><category><![CDATA[Oracle]]></category><category><![CDATA[encryption]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sun, 22 Dec 2024 18:03:13 GMT</pubDate><content:encoded><![CDATA[<p>As I come across environments where SQL*Net is not encrypted I sometimes also notice that there are two common misconceptions about it:</p>
<ul>
<li>First, some are afraid that certificates <em>must</em> be involved and consequently maintaining a PKI infrastructure is deemed too complicated.</li>
<li>Second, some are afraid of licensing issues, especially in respect to Standard Edition</li>
</ul>
<p>Let me show you how none of those are true (certs certainly <em>can</em> be involved, but they're not necessary for simple setups).</p>
<h2 id="heading-license-requirements">License Requirements</h2>
<p>To quote <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/23/dblic/Licensing-Information.html#GUID-0F9EB85D-4610-4EDF-89C2-4916A0E7AC87">Database Licensing Information User Manual, Release 23</a>:</p>
<p><em>Note: Network encryption (native network encryption, network data integrity, and SSL/TLS) and strong authentication services (Kerberos, PKI, and RADIUS) are</em> <strong><em>no longer</em></strong> <em>part of Oracle Advanced Security and are available in all licensed editions of all supported releases of Oracle Database.</em></p>
<p>I've also checked the same document for versions 19c and 12.1 and they both say the same thing.</p>
<p>Quick disclaimer: this blog simply reflects my understanding of Oracle's licensing documentation, so keep in mind that it is Oracle License Management Services (LMS) that always has the final authority regarding licensing matters.</p>
<p>That being said, it is also certainly a good idea to double check licensing restrictions when you hear about encryption or compression in regard to Standard Edition.</p>
<h2 id="heading-server-configuration">Server Configuration</h2>
<p>So, let's setup <em>Native Network Encryption</em> as this is the simplest way of enabling encryption over SQL*Net. This method does not require any certificates and, in most cases, no client configuration. To enable it, we simply need to modify our <code>sqlnet.ora</code> file on the server. Like this:</p>
<pre><code>SQLNET.ENCRYPTION_SERVER = REQUESTED
SQLNET.CRYPTO_CHECKSUM_SERVER = REQUESTED
</code></pre><p>Or maybe like this if we want to be really strict about it:</p>
<pre><code class="lang-plaintext">SQLNET.ENCRYPTION_SERVER = REQUIRED             # default=ACCEPTED
SQLNET.ENCRYPTION_TYPES_SERVER = (AES256)       # default=all available algorithms
SQLNET.CRYPTO_CHECKSUM_SERVER = REQUIRED        # default=ACCEPTED
SQLNET.CRYPTO_CHECKSUM_TYPES_SERVER = (SHA512)  # default=all available algorithms
SQLNET.ALLOW_WEAK_CRYPTO_CLIENTS = FALSE        # default=TRUE
</code></pre>
<p>Defaults for <code>SQLNET.ENCRYPTION_SERVER</code> and <code>SQLNET.CRYPTO_CHECKSUM_SERVER</code> is <code>ACCEPTED</code>. Which would mean that if a client <code>REQUEST</code>s or <code>REQUIRE</code>s encryption then the server will be happy to encrypt the traffic. In our example though, as recommended for maximum security in official documentation, we only allow to establish encrypted connections, thus we specified <code>REQUIRED</code>. This is also why client configuration is not required in most cases: client also has default value for those parameters as <code>ACCEPTED</code>.</p>
<p><code>SQLNET.ALLOW_WEAK_CRYPTO_CLIENTS</code> is more of a backward compatibility setting. If you need to support older clients then you may probably need to keep it on default value (<code>TRUE</code>).</p>
<p>Regarding algorithms, there is not much choice really. On 19c, there is <code>AES</code> for encryption and <code>SHA</code> for checksum. You can only require specific key lengths (e.g <code>AES256</code>) and hash sizes (e.g. <code>SHA512</code>). Well, to be exact, there are other algorithms (RC4, 3DES) supported, but they're also deprecated.</p>
<p>That's really, in essence, all there is to it.</p>
<h2 id="heading-client-configuration">Client Configuration</h2>
<p>Same settings can be used on client, just replace <code>_SERVER</code> with <code>_CLIENT</code>. So, if you're only in control of the client, you can still have connection encrypted, even if your DBA did not touch the <code>sqlnet.ora</code> file. Simply set <code>SQLNET.ENCRYPTION_CLIENT</code> and <code>SQLNET.CRYPTO_CHECKSUM_CLIENT</code> to either <code>REQUESTED</code> or <code>REQUIRED</code>.</p>
<h2 id="heading-is-it-working">Is it Working?</h2>
<p>Hackers will of course want to use tcpdump/Wireshark or similar to make it sure, but most of those people probably aren't reading this blog post as they already have some kind of encryption in place. For DBAs who trust performance views, you can simply do a following query:</p>
<pre><code class="lang-plaintext">SELECT network_service_banner
    FROM v$session_connect_info
    WHERE sid=sys_context('userenv','sid');
</code></pre>
<p>If enabled, you'll see (among other rows) those two:</p>
<pre><code>NETWORK_SERVICE_BANNER
------------------------------------------------------------------------------------------
...
AES256 Encryption service adapter <span class="hljs-keyword">for</span> Linux: Version <span class="hljs-number">19.0</span><span class="hljs-number">.1</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span> - Production
SHA1 Crypto-checksumming service adapter <span class="hljs-keyword">for</span> Linux: Version <span class="hljs-number">19.0</span><span class="hljs-number">.1</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span> - Production
...
</code></pre><h2 id="heading-what-about-certificates">What About Certificates?</h2>
<p>... What you seek is called <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-secure-sockets-layer-authentication.html#GUID-6AD89576-526F-4D6B-A539-ADF4B840819F">Transport Layer Security (TLS)</a> and it can be configured so that either only server has a certificate or both (client+server). You can also <strong>authenticate</strong> clients (and servers) this way.</p>
<p>As I prefer to keep my blog posts concise and on-topic, I'll only provide a link to the documentation instead of discussing TLS in this post.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-network-data-encryption-and-integrity.html">Configuring Oracle Database Native Network Encryption and Data Integrity</a></li>
<li><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-secure-sockets-layer-authentication.html#GUID-6AD89576-526F-4D6B-A539-ADF4B840819F">Transport Layer Security (TLS)</a></li>
<li><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/23/dblic/Licensing-Information.html#GUID-0F9EB85D-4610-4EDF-89C2-4916A0E7AC87">Database Licensing Information User Manual, Release 23</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Introduction to JOIN LATERAL on Oracle]]></title><description><![CDATA[Lateral join joins a subquery, which is executed as many times as there are rows in the leading table. Consider it as a kind of for-each loop. Most of, if not all, the problems it solves, can also be solved without lateral join (e.g. using analytic f...]]></description><link>https://blog.srecnik.info/introduction-to-join-lateral-on-oracle</link><guid isPermaLink="true">https://blog.srecnik.info/introduction-to-join-lateral-on-oracle</guid><category><![CDATA[Oracle]]></category><category><![CDATA[joins]]></category><category><![CDATA[execution plan]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sat, 07 Dec 2024 16:14:34 GMT</pubDate><content:encoded><![CDATA[<p>Lateral join joins a subquery, which is executed as many times as there are rows in the leading table. Consider it as a kind of for-each loop. Most of, if not all, the problems it solves, can also be solved without lateral join (e.g. using analytic functions).</p>
<p>Those alternatives can sometimes yield a better execution plan and lateral join can sometimes yield a more readable code. So, as with anything - it depends. Let's consider a few examples.</p>
<h2 id="heading-example">Example</h2>
<p>Suppose we'd like to get a list of all departments (<code>DEPT</code>) and for each one we'd also like to display salary and commission of an employee (<code>EMP</code>), who was the first hire in a given department:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> <span class="hljs-comment">/*+ GATHER_PLAN_STATISTICS */</span> d.*, x.sal, x.comm
   <span class="hljs-keyword">from</span> dept d
   <span class="hljs-keyword">join</span> <span class="hljs-keyword">lateral</span> (
      <span class="hljs-keyword">select</span> sal, comm
         <span class="hljs-keyword">from</span> emp e 
         <span class="hljs-keyword">where</span> e.deptno = d.deptno
         <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> e.hiredate <span class="hljs-keyword">asc</span>
         <span class="hljs-keyword">fetch</span> <span class="hljs-keyword">first</span> <span class="hljs-number">1</span> <span class="hljs-keyword">row</span> <span class="hljs-keyword">only</span>
   ) x <span class="hljs-keyword">on</span> <span class="hljs-number">1</span>=<span class="hljs-number">1</span>;
</code></pre>
<pre><code>    DEPTNO DNAME          LOC                SAL    COMM
---------- -------------- ------------- -------- -------
        <span class="hljs-number">10</span> ACCOUNTING     NEW YORK       <span class="hljs-number">2450.00</span>
        <span class="hljs-number">20</span> RESEARCH       DALLAS          <span class="hljs-number">800.00</span>
        <span class="hljs-number">30</span> SALES          CHICAGO        <span class="hljs-number">1600.00</span>  <span class="hljs-number">300.00</span>
</code></pre><h2 id="heading-explain-plan">Explain Plan</h2>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> <span class="hljs-keyword">table</span>(
    dbms_xplan.display_cursor(<span class="hljs-string">'20p9d49b4q2j7'</span>, <span class="hljs-number">0</span>, <span class="hljs-string">'ALLSTATS'</span>));

<span class="hljs-comment">-----------------------------------------------------------------------</span>
| Id  | Operation                 | Name            | Starts | A-Rows |
<span class="hljs-comment">-----------------------------------------------------------------------</span>
|   0 | <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">STATEMENT</span>          |                 |      <span class="hljs-number">1</span> |      <span class="hljs-number">3</span> |
|   <span class="hljs-number">1</span> |  <span class="hljs-keyword">NESTED</span> LOOPS             |                 |      <span class="hljs-number">1</span> |      <span class="hljs-number">3</span> |
|   <span class="hljs-number">2</span> |   <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">ACCESS</span> <span class="hljs-keyword">FULL</span>       | DEPT            |      <span class="hljs-number">1</span> |      <span class="hljs-number">4</span> |
|   <span class="hljs-number">3</span> |   <span class="hljs-keyword">VIEW</span>                    | VW_LAT_2D0B8FC8 |      <span class="hljs-number">4</span> |      <span class="hljs-number">3</span> |
|*  <span class="hljs-number">4</span> |    <span class="hljs-keyword">COUNT</span> STOPKEY          |                 |      <span class="hljs-number">4</span> |      <span class="hljs-number">3</span> |
|   <span class="hljs-number">5</span> |     <span class="hljs-keyword">VIEW</span>                  |                 |      <span class="hljs-number">4</span> |      <span class="hljs-number">3</span> |
|*  <span class="hljs-number">6</span> |      <span class="hljs-keyword">SORT</span> <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> STOPKEY|                 |      <span class="hljs-number">4</span> |      <span class="hljs-number">3</span> |
|*  <span class="hljs-number">7</span> |       <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">ACCESS</span> <span class="hljs-keyword">FULL</span>   | EMP             |      <span class="hljs-number">4</span> |     <span class="hljs-number">14</span> |
<span class="hljs-comment">-----------------------------------------------------------------------</span>
</code></pre>
<p><code>dbms_xplan.display_cursor</code> actually returns more columns - I'm only displaying <code>Starts</code> and <code>A-Rows</code> here to make a point. Those two rows are only available in execution plan output if you use <code>/*+ GATHER_PLAN_STATISTICS */</code> (as I did in this example) or set <code>statistics_level=ALL</code>.</p>
<p>Letter "A" in <code>A-Rows</code> stands for <em>Actual</em> - it is the <em>actual</em> number of rows returned by the operation.</p>
<p><code>Starts</code> tells us how many times the subquery started executing. So, according to line with <code>Id=2</code>, full table scan on <code>DEPT</code> ran once and returned 4 rows.
In line <code>Id=3</code> we see that our subquery was executed 4 times. All those executions together returned 3 rows (one of the departments has no employees, 
which is why line with <code>Id=1</code> says only 3 rows were returned after <code>JOIN</code>).</p>
<h2 id="heading-alternatives">Alternatives</h2>
<p>Let's consider a few alternative solutions to our <code>JOIN LATERAL</code> example, just to give you ideas on what you can try if you find yourself hunting for a different execution plan. None of those is <em>best</em>; you can only get the <em>best</em> one if you study your specific execution plans and maybe compare the amount of buffer gets required.</p>
<h3 id="heading-scalar-subqueries">Scalar Subqueries</h3>
<p>The query that sparked the initial debate which eventually lead to this blog post was something like this:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> d.*,
      (<span class="hljs-keyword">select</span> x.sal  <span class="hljs-keyword">from</span> emp x <span class="hljs-keyword">where</span> x.deptno = d.deptno <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> x.hiredate <span class="hljs-keyword">asc</span> <span class="hljs-keyword">fetch</span> <span class="hljs-keyword">first</span> <span class="hljs-number">1</span> <span class="hljs-keyword">row</span> <span class="hljs-keyword">only</span>) <span class="hljs-keyword">as</span> sal,
      (<span class="hljs-keyword">select</span> x.comm <span class="hljs-keyword">from</span> emp x <span class="hljs-keyword">where</span> x.deptno = d.deptno <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> x.hiredate <span class="hljs-keyword">asc</span> <span class="hljs-keyword">fetch</span> <span class="hljs-keyword">first</span> <span class="hljs-number">1</span> <span class="hljs-keyword">row</span> <span class="hljs-keyword">only</span>) <span class="hljs-keyword">as</span> comm,
      <span class="hljs-comment">/* ... */</span>
   <span class="hljs-keyword">from</span> dept d;
</code></pre>
<p>It can be "bad" (performance-wise) to execute subquery for each row, but it is twice as "bad" to do it twice for each row.
Notice that <code>JOIN LATERAL</code> version accesses <code>EMP</code> table half as many times. Also, using expressions such as this example can only return 1 row whereas join lateral subquery can return any number of rows.</p>
<p>Eagle-eyed observers will probably also notice that our initial example would require <strong>left</strong> join lateral to match results of this alternative.</p>
<h3 id="heading-analytic-functions">Analytic Functions</h3>
<p>Using analytic functions we can read both tables only once, like this:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">WITH</span> emp_w <span class="hljs-keyword">AS</span> (
   <span class="hljs-keyword">SELECT</span> deptno, sal, comm,
          ROW_NUMBER() <span class="hljs-keyword">OVER</span> (<span class="hljs-keyword">PARTITION</span> <span class="hljs-keyword">BY</span> deptno <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> hiredate <span class="hljs-keyword">ASC</span>) <span class="hljs-keyword">AS</span> rn
    <span class="hljs-keyword">FROM</span> emp
)
<span class="hljs-keyword">SELECT</span> d.*, e.sal, e.comm
<span class="hljs-keyword">FROM</span> dept d
<span class="hljs-keyword">LEFT</span> <span class="hljs-keyword">JOIN</span> emp_w e <span class="hljs-keyword">ON</span> d.deptno = e.deptno <span class="hljs-keyword">AND</span> e.rn = <span class="hljs-number">1</span>;
</code></pre>
<h3 id="heading-correlated-subquery">Correlated Subquery</h3>
<p>This one may be the simplest one to understand:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">SELECT</span> d.*, e.sal, e.comm
<span class="hljs-keyword">FROM</span> dept d
<span class="hljs-keyword">JOIN</span> emp e <span class="hljs-keyword">ON</span> d.deptno = e.deptno
<span class="hljs-keyword">AND</span> e.hiredate = (<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">MIN</span>(hiredate)
                  <span class="hljs-keyword">FROM</span> emp
                  <span class="hljs-keyword">WHERE</span> deptno = d.deptno);
</code></pre>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p><code>JOIN LATERAL</code> is a powerful feature supported by many relational databases as it is also part of ANSI standard. On Oracle, it is supported since version 12c. However, I do believe that it is important to be aware of basic performance aspects of its usage.</p>
]]></content:encoded></item><item><title><![CDATA[How to Keep Oracle Database Schema(s) Under Version Control (git, mercurial) Using DDLFS]]></title><description><![CDATA[I've just released new (beta) version of ddlfs, which now also supports running natively on Windows (thanks to Dokan). Originally it written for Linux/libfuse only. I made it so that I wouldn't need to use graphical interfaces when interacting with O...]]></description><link>https://blog.srecnik.info/how-to-keep-oracle-database-schemas-under-version-control-git-mercurial-using-ddlfs</link><guid isPermaLink="true">https://blog.srecnik.info/how-to-keep-oracle-database-schemas-under-version-control-git-mercurial-using-ddlfs</guid><category><![CDATA[ddlfs ]]></category><category><![CDATA[Oracle Database]]></category><category><![CDATA[Git]]></category><category><![CDATA[mercurial]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 18 Nov 2024 22:31:47 GMT</pubDate><content:encoded><![CDATA[<p>I've just <a target="_blank" href="https://github.com/usrecnik/ddlfs/releases/tag/3.0-RC1">released</a> new (beta) version of <a target="_blank" href="https://github.com/usrecnik/ddlfs/">ddlfs</a>, which now also supports running natively on Windows (thanks to <a target="_blank" href="http://dokan-dev.github.io/">Dokan</a>). Originally it written for Linux/<a target="_blank" href="https://github.com/libfuse/libfuse/">libfuse</a> only. I made it so that I wouldn't need to use graphical interfaces when interacting with Oracle databases, but since it was written, there came other interesting uses for it. Let's discuss one of them:</p>
<p>How can we keep DDL changes of production database in git/mercurial repositoy? Application schemas of production database can be migrated using tools such as <a target="_blank" href="https://github.com/flyway/flyway">FlyWay</a> or <a target="_blank" href="https://www.liquibase.com/">Liquibase</a> which generally keep their history under version control.</p>
<p>But what about ad-hoc changes that happen on production? Wouldn't it be nice to have those, too, <em>automatically</em> under version control?</p>
<p>This blog post will provide a recipe to achive that using <a target="_blank" href="https://github.com/usrecnik/ddlfs/">ddlfs</a>. I decided to write examples in this blog post in Windows syntax, just because I'm still considering Windows support experimental (until I get more feedback), and I think writing this kind of blog post is a nice way to maybe catch some bugs before I mark the release as production-ready.</p>
<h2 id="heading-what-is-ddlfs">What is DDLFS?</h2>
<p>It's an open-source filesystem which exposes every database object (such as packages, views, procedures) as an <code>.sql</code> file. Here is a screenshot on Windows of how it looks like in practice. Please visit ddlfs <a target="_blank" href="https://github.com/usrecnik/ddlfs/">GitHub</a> page for more detailed description.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731967290588/5a7edab6-3445-453c-bab8-6fe50e75c313.png" alt class="image--center mx-auto" /></p>
<p>Also, here is a <a target="_blank" href="https://github.com/usrecnik/ddlfs/raw/refs/heads/master/docs/ddlfs-windows.mp4">video</a> from which the screenshot was taken.</p>
<h2 id="heading-what-about-gitmercurial">What about git/mercurial</h2>
<p>They're both distributed version control systems, which, you, if you're reading blogs such as this one, should definitively be already familiar with. If not, here are the links to their manuals:</p>
<ul>
<li><a target="_blank" href="https://git-scm.com/book/en/v2">Git Book</a>    </li>
<li><a target="_blank" href="https://www.mercurial-scm.org/guide">Mercurial Guide</a></li>
</ul>
<p>We do need to address one important detail regarding those two; They keep all their data under <code>.git</code> and <code>.hg</code> folder. So, if you have a project in, say, <code>C:\Users\John\repos\my-project</code>, there will be a <code>.git</code> or <code>.hg</code> folder in there.</p>
<h2 id="heading-creating-a-project">Creating a project</h2>
<p>Let's start by creating a project folder, say, <code>F:\database\prod</code> which contains <code>.\ddlfs\</code> folder. Like this:</p>
<pre><code class="lang-plaintext">F:\&gt;mkdir F:\database\prod\ddlfs
F:\&gt;mkdir F:\temp
</code></pre>
<p>The <code>F:\database\prod</code> will contain <code>.hg</code> or <code>.git</code> folder and <code>.\ddlfs\</code> folder will reflect the current state of database objects in the database.</p>
<p>The <code>F:\temp</code> is optional and can be deleted at any time when ddlfs is not mounted. We'll use it as <code>temppath=</code> (see details in the following section).</p>
<h2 id="heading-mounting-database-as-a-filesystem">Mounting Database as a Filesystem</h2>
<pre><code class="lang-plaintext">ddlfs -o ro,dbro,keepcache,temppathusername=SCOTT,password=tiger,database=dbhost:1521/service.name D:\database\prod\ddlfs
</code></pre>
<p>Regarding the folder <code>D:\database\prod\ddlfs</code> - this is mountpoint, it does not need to be a letter, e.g. <code>X:\</code>. Well, to be exact with terminology, on Windows, this "folder" becomes a <a target="_blank" href="https://learn.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions">junction</a> after we successfully execute this ddlfs command:</p>
<pre><code class="lang-plaintext">F:\database\prod&gt;dir
 Volume in drive F is Data
 Volume Serial Number is E277-8A48

 Directory of F:\database\prod

17/11/2024  14:02    &lt;DIR&gt;          .
15/11/2024  17:00    &lt;DIR&gt;          ..
17/11/2024  14:02    &lt;JUNCTION&gt;     ddlfs [\??\Volume{d6cc17c5-176c-4085-bce7-964f1e9f5de9}\]
               0 File(s)              0 bytes
               3 Dir(s)  21.358.292.992 bytes free
</code></pre>
<p>The only mandatory options are <code>username</code>, <code>password</code> and <code>database</code>. And everything described in this blog post should work if you'd only use those three. All the other options are added merely for performance optimization. On Linux, all mount options are described if you say <code>man ddlfs</code>, and all the mount options are also documented on the ddlfs GitHub page.</p>
<p>Let's discuss them in the context of using ddlfs for hg/git:</p>
<ul>
<li><p><code>ro</code>: Do not allow changes to packages. Without this option, it is possible to open the .SQL files in <code>.\ddlfs</code> folder and save them. We intend to only read the current state of the database and, with this option, prohibit incidental changes.</p>
</li>
<li><p><code>dbro</code>: Let ddlfs <em>assume</em> your database is opened as <code>READ ONLY</code>. We need this assumption for better caching - once we read the contents of a package, we can trust that the state of this package didn't change even if we read it again after a while (until <code>umount</code>).</p>
</li>
<li><p><code>keepcache</code>: ddlfs keeps temporary files in which cached contents of database objects are kept. Those files can be reused between mounts, because cache validation is faster than reading all the objects again.</p>
</li>
<li><p><code>temppath</code>: Simply a path where previously mentioned cached contents are stored. Note that if you point it to a filesystem which relies on inodes - this folder can potentially have <em>a lot</em> of files, thus, very small <code>ext4</code> may not cut it. Default is <code>C:\Users\%USER%\AppData\Local\Temp\</code> on Windows and <code>/tmp</code> on Linux.</p>
</li>
<li><p><code>filesize=-1</code>: Probably the most important one; Using this option, ddlfs will always provide correct, <em>actual</em>, size of each file. Default is <code>0</code> - if this would be used, then files would have reported size=0 bytes until they're read. Once read, they report the correct size. Problem with correct size is that we need to export every database object in order to obtain every size. Which takes time. But we need to do that anyway in case of git/hg. It's also why we <code>keepcache</code> between the mounts.</p>
</li>
</ul>
<p>You can optionally also specify <code>schemas=MY_APP%</code> to only include database objects belonging to schemas with name starting with <code>MY_APP</code>. You can also include list of schemas, like <code>schemas=SCHEMA_1:SCHEMA_2:SCHEMA_3</code>.</p>
<p>If you're scripting this, note that on Windows, ddlfs will go to background before everything is mounted, so before proceeding, make sure file <code>D:\database\prod\ddlfs\ddlfs.log</code> exists.</p>
<h2 id="heading-git">Git</h2>
<p><code>git init</code> is needed only the first time to create <code>.git</code> repository.</p>
<pre><code class="lang-plaintext">F:\database\prod&gt;git init
Initialized empty Git repository in F:/database/prod/.git/

F:\database\prod&gt;git config core.autocrlf false

F:\database\prod&gt;git add ddlfs
</code></pre>
<p><code>core.autocrlf=false</code> is here because we don't want git to care about line endings - we want it to add files exactly as they are, without line feed conversions (note that we've mounted <code>.\ddlfs</code> as <em>read only</em>).</p>
<p>The add part will take quite some time if your schemas are huge (it needs to export every object). Especially if you chose to log in using <code>SYS</code> user, which has access to all the objects in the database.</p>
<p>It will take the first time the longest (to build up a cache in <code>temppath</code>). Once this is built, we'll only deal with changes, which is way faster. This is also one of the differences between using custom export scripts (such as datapump metadata export, <code>dbms_metadata.get_ddl</code> or any other similar method - those methods will <em>read</em> everything they're exporting each time they're exporting it. Using ddlfs <code>keepcache</code> we're reusing everything that hasn't changed yet (according to <code>all_objects.last_ddl_time</code>).</p>
<p>Waiting on command to complete without knowing what it is doing and how much progress it's already made usually leads to lacking trust in any given software. So here's how you can monitor what is going on if you'd like:</p>
<p>You can run ddlfs in foreground by specifying <code>-f</code> flag to <code>ddlfs</code> command on Linux or use <code>ddlfs-fg</code> command (instead of <code>ddlfs</code>) on Windows. Furthermore, you can add <code>loglevel=DEBUG</code> to mount options. This way you'll see each object that is accessed by git printed to the console.</p>
<p>Once this is complete, simply <code>commit</code> (and maybe also <code>push</code>) and umount. On Windows, you need to be Administrator to do umount:</p>
<pre><code class="lang-plaintext">F:\database\prod&gt;git commit -m "daily commit"

F:\database\prod&gt;ddlfs-umount
</code></pre>
<h2 id="heading-mercurial">Mercurial</h2>
<p>Now, let's do the same with Mercurial, this time as a Linux example:</p>
<pre><code class="lang-plaintext">$ ddlfs -o... ./ddlfs
$ hg init
$ hg add ddlfs
$ hg commit -m "daily commit"
$ fusermount -u ./ddlfs
</code></pre>
<p>Replace three dots in the first command with mount options as described for git.</p>
<p>Also note, on Linux, you can mount/umount as any user, whereas on Windows, you need to be Administrator to umount.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Everything described so far is free and open-source. It is, however, also <em>optionally</em> integrated in my company's commercial offering <a target="_blank" href="https://www.abakus.si/en/products/hardware/active_backup_server">Abakus Backup Server</a> which uses physical standby databases to maintain physical backup copies of Oracle Databases using deduplication to save space.</p>
<p>Also keep in mind that ddlfs 3.x is not yet considered production ready because there has been many changes in order to accommodate both, Linux and Windows with the same source. So, feel free to report any issues on ddlfs GitHub page.</p>
<p>And if anyone happens to "convert" this article into an open-sourced generic script (e.g. bash or powershell), let me know, I'd be happy to publish a link to it.</p>
]]></content:encoded></item><item><title><![CDATA[Reading Execution Plans]]></title><description><![CDATA[I routinely do a "rehearsal" of my presentations with our DBA team and any other willing participants (they tend to be developers) to receive feedback and to share knowledge. In the last one, I've got quite an interesting question, which requires und...]]></description><link>https://blog.srecnik.info/reading-execution-plans</link><guid isPermaLink="true">https://blog.srecnik.info/reading-execution-plans</guid><category><![CDATA[Oracle Database]]></category><category><![CDATA[execution plan]]></category><category><![CDATA[hash join]]></category><category><![CDATA[nestedloops]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Sun, 13 Oct 2024 21:55:20 GMT</pubDate><content:encoded><![CDATA[<p>I routinely do a "rehearsal" of my presentations with our DBA team and any other willing participants (they tend to be developers) to receive feedback and to share knowledge. In the last one, I've got quite an interesting question, which requires understanding of execution plans - specifically, the order in which operations are executed. This post is my attempt to explain the misconception.</p>
<h2 id="heading-the-query">The Query</h2>
<p>The query which raised the question doesn't need much explaining. It's a simple example for demonstration purposes:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> <span class="hljs-comment">/*+ ORDERED */</span> *
    <span class="hljs-keyword">from</span> tab_first tf
    <span class="hljs-keyword">join</span> tab_second ts <span class="hljs-keyword">on</span> ts.id_first=tf.id_first;

<span class="hljs-comment">---------------------------------------------------------------------------------</span>
| Id  | Operation          | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
<span class="hljs-comment">---------------------------------------------------------------------------------</span>
|   0 | <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">STATEMENT</span>   |            |       |       |     <span class="hljs-number">4</span> (<span class="hljs-number">100</span>)|          |
|*  <span class="hljs-number">1</span> |  <span class="hljs-keyword">HASH</span> <span class="hljs-keyword">JOIN</span>         |            |     <span class="hljs-number">6</span> |   <span class="hljs-number">978</span> |     <span class="hljs-number">4</span>   (<span class="hljs-number">0</span>)| <span class="hljs-number">00</span>:<span class="hljs-number">00</span>:<span class="hljs-number">01</span> |
|   <span class="hljs-number">2</span> |   <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">ACCESS</span> <span class="hljs-keyword">FULL</span>| TAB_FIRST  |     <span class="hljs-number">3</span> |   <span class="hljs-number">225</span> |     <span class="hljs-number">2</span>   (<span class="hljs-number">0</span>)| <span class="hljs-number">00</span>:<span class="hljs-number">00</span>:<span class="hljs-number">01</span> |
|   <span class="hljs-number">3</span> |   <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">ACCESS</span> <span class="hljs-keyword">FULL</span>| TAB_SECOND |     <span class="hljs-number">6</span> |   <span class="hljs-number">528</span> |     <span class="hljs-number">2</span>   (<span class="hljs-number">0</span>)| <span class="hljs-number">00</span>:<span class="hljs-number">00</span>:<span class="hljs-number">01</span> |
<span class="hljs-comment">---------------------------------------------------------------------------------</span>
</code></pre>
<h2 id="heading-the-question">The Question</h2>
<p>Which of the two tables is accessed first?</p>
<p>Is it <code>tab_first</code> because it is explicitly requested to be the <em>leading table</em> or is it <code>tab_second</code> because it is displayed as the last line in execution plan output?</p>
<p>Well, the correct answer is <code>tab_first</code>, but what prompted the question was actually the awareness of the general rule of the order in which execution plans are supposed to be read: from bottom up - starting with the deepest entries (leaf nodes). And, if we'd follow this rule to the letter, the <code>tab_second</code> would be accessed first.</p>
<p>This general rule comes from the way the execution plan is executed. Maybe it can help thinking of it this way: it all starts with root operation (id=0), but in order to complete this root operation it needs full or partial results from its children operations. And the same goes for all children that have children until we get to leaf nodes. That is why we're generally reading the plan as discussed earlier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728855188369/881858fd-69c3-448e-9201-54547505757d.png" alt class="image--center mx-auto" /></p>
<p>Let's first understand how Hash Join and Nested Loops operations work in order to better understand when is which table accessed.</p>
<h2 id="heading-hash-join">Hash Join</h2>
<p>This join method is generally used when joining two larger row sources (two tables in our example) using equality operator.</p>
<p>The optimizer will use statistics to determine which of the two row sources is smaller than the other and proclaim it to be the <em>build table</em>. We can also force specific table to be the <em>build table</em> by explicitly specifying the join order using hints (<code>ORDERED</code> or <code>LEADING</code>).</p>
<p>The other row source (generally the bigger one) is considered the <em>probe table</em>.</p>
<p>In our example, <code>first_table</code> is the <em>build table</em> and <code>second_table</code> is the <em>probe table</em>.</p>
<p>Hash Join will read the whole <em>build table</em> ("smaller one") first and calculate hash on joined columns for each row. The hashes are stored in PGA where each hash value has associated linked list of rows for which the same hash has been calculated.</p>
<p>The <em>probe table</em> (bigger one) is being read only after this <code>hash -&gt; linked list</code> memory structure is built in PGA. This memory structure is probed by each record in the <em>probe table</em>.</p>
<p>So, in our example, <code>tab_first</code> is read first, and after that the <code>tab_second</code> is read.</p>
<h2 id="heading-nested-loops">Nested Loops</h2>
<p>Let's now consider an execution plan with Nested Loops such as this one:</p>
<pre><code class="lang-plaintext">---------------------------------------------------------------------------------
| Id  | Operation          | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |            |     6 |   102 |     6   (0)| 00:00:01 |
|   1 |  NESTED LOOPS      |            |     6 |   102 |     6   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| TAB_FIRST  |     3 |    30 |     2   (0)| 00:00:01 |
|*  3 |   TABLE ACCESS FULL| TAB_SECOND |     2 |    26 |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------
</code></pre>
<p>Nested loops are probably the easiest to understand: for each row in <code>tab_first</code> loop over all rows in <code>tab_second</code> to find matching rows. <code>tab_first</code> would be considered <em>outer table</em> and <code>tab_second</code> would be considered the <em>inner table</em> in terms of Nested loops terminology.</p>
<p>In regards of when is which table accessed:</p>
<ol>
<li>first, a batch of rows is read from <code>tab_first</code></li>
<li>for each of the rows in this batch the matching rows in <code>tab_second</code> are found</li>
</ol>
<p>So, <code>tab_first</code> is accessed first, then <code>tab_second</code> is iterated as many times as there is rows in first batch, then <code>tab_first</code> is accessed again for the next batch.</p>
<p>To put it differently, full table scan of <code>tab_first</code> is done once. But by the time it is done, the full table scan of <code>tab_second</code> was already done as many times as there is rows in <code>tab_first</code>.</p>
<h2 id="heading-connecting-the-dots">Connecting the Dots</h2>
<p>In the similar manner, if we'd have a multi-table join with nested loops, then all three tables would be accessed in described alternating manner and the execution plan would look like this:</p>
<pre><code class="lang-plaintext">----------------------------------------------------------------------------------
| Id  | Operation           | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |            |     8 |  2008 |    12   (0)| 00:00:01 |
|   1 |  NESTED LOOPS       |            |     8 |  2008 |    12   (0)| 00:00:01 |
|   2 |   NESTED LOOPS      |            |     6 |   978 |     6   (0)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| TAB_FIRST  |     3 |   225 |     2   (0)| 00:00:01 |
|*  4 |    TABLE ACCESS FULL| TAB_SECOND |     2 |   176 |     1   (0)| 00:00:01 |
|*  5 |   TABLE ACCESS FULL | TAB_THIRD  |     1 |    88 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------
</code></pre>
<p>Hopefully, I managed to explain that <em>reading</em> the execution plan from bottom up, starting with the deepest entries (leaf nodes), is correct. But it does not mean that one operation must complete for the next to begin. And the operations displayed as siblings (<code>tab_first</code> and <code>tab_second</code> in our examples), don't necessarily execute in any specific order.</p>
<p>And if you need more food for thought on the subject - consider thinking about execution plans of parallel queries, pipelined functions and adaptive plans.</p>
<h2 id="heading-references">References</h2>
<ul>
<li>Official doc: <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/joins.html">Oracle Documentation - Joins</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[utlfixdirs.sql]]></title><description><![CDATA[It's a script that comes with every $ORACLE_HOME and it's located
in $ORACLE_HOME/rdbms/admin/ folder. It recreates all DBA_DIRECTORIES
that are created by Oracle software and should be referencing the current $ORACLE_HOME.
We should use it every tim...]]></description><link>https://blog.srecnik.info/utlfixdirssql</link><guid isPermaLink="true">https://blog.srecnik.info/utlfixdirssql</guid><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 16 Sep 2024 21:21:42 GMT</pubDate><content:encoded><![CDATA[<p>It's a script that comes with every <code>$ORACLE_HOME</code> and it's located
in <code>$ORACLE_HOME/rdbms/admin/</code> folder. It recreates all <code>DBA_DIRECTORIES</code>
that are created by Oracle software and should be referencing the current <code>$ORACLE_HOME</code>.</p>
<p>We should use it every time we change <code>$ORACLE_HOME</code>. That means after migrations and after out-of-place upgrades, which is generally the preferred method of upgrading. You can read more on <a target="_blank" href="https://mikedietrichde.com/2024/01/10/the-downsides-of-in-place-patching-and-a-patching-lab/">downsides of in-place patching</a> on Mike Dietrich's blog.</p>
<p>This script is available in versions &gt;= 19c.</p>
<h2 id="heading-opatch">OPatch</h2>
<p>Interestingly, OPatch directories are fixed automatically on instance startup without running any scripts or commands other than <code>startup</code>.
This applies only to <code>OPATCH_%_DIR</code> directories. So, it should be safe to run OPatch even before you run <code>utlfixdirs.sql</code>.</p>
<p>Regardless, I still believe it to be a good practice to fix directories before datapatch because .sql scripts that are run by datapatch could, in theory, need something
from other, non-opatch directories (e.g. maybe from <code>SDO_DIR_ADMIN</code> directory).</p>
<h2 id="heading-read-only-pdbs">READ ONLY PDBs</h2>
<p>All Oracle maintained directories that point to paths in <code>$ORACLE_HOME</code> are created/replaced by Oracle using <code>sharing=metadata</code>. So, after running <code>utlfixdirs.sql</code> you will have those directories fixed even in PDBs that are open as READ ONLY. That includes <code>PDB$SEED</code>.</p>
<h2 id="heading-example">Example:</h2>
<pre><code>SQL&gt; @?<span class="hljs-regexp">/rdbms/</span>admin/utlfixdirs.sql

<span class="hljs-attr">Container</span>: CDB$ROOT

Current  ORACLE_HOME: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2
Original ORACLE_HOME: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1

DBMS_OPTIM_ADMINDIR
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/rdbms/admin
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/rdbms/admin
DBMS_OPTIM_LOGDIR
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/cfgtoollogs
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/cfgtoollogs
ORACLE_HOME
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2
ORACLE_OCM_CONFIG_DIR
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/ccr/state
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/ccr/state
ORACLE_OCM_CONFIG_DIR2
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/ccr/state
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/ccr/state
SDO_DIR_ADMIN
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/md/admin
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/md/admin
XMLDIR
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/rdbms/xml
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/rdbms/xml
XSDDIR
...OLD: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_1/rdbms/xml/schema
...NEW: <span class="hljs-regexp">/oracle/</span>db_ee/<span class="hljs-number">19.24</span><span class="hljs-number">.0</span>/dbhome_2/rdbms/xml/schema

PL/SQL procedure successfully completed.
</code></pre><h2 id="heading-references">References</h2>
<ul>
<li>Official doc: <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/creating-additional-data-dictionary-structures.html#GUID-571DA1D1-5274-4763-99B4-B1FF60E79F9F">Creating Additional Data Dictionary Structures</a></li>
</ul>
<p>The contents of this blog post were tested on database version 19.24. Feel free to leave a comment if you find that described behavior changes with newer versions.</p>
]]></content:encoded></item><item><title><![CDATA[Collecting SQL Plan Baselines]]></title><description><![CDATA[Ever thought about collecting baselines for all queries on your database? Let's explore the benefits and the methods of doing so.
Before we begin -
What is an SQL Plan Baseline?
A baseline for a specific query (sql_text) stores one or more execution ...]]></description><link>https://blog.srecnik.info/collecting-sql-plan-baselines</link><guid isPermaLink="true">https://blog.srecnik.info/collecting-sql-plan-baselines</guid><category><![CDATA[sql plan baselines]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 09 Sep 2024 19:28:18 GMT</pubDate><content:encoded><![CDATA[<p>Ever thought about collecting baselines for <em>all</em> queries on your database? Let's explore the benefits and the methods of doing so.</p>
<p>Before we begin -</p>
<h3 id="heading-what-is-an-sql-plan-baseline">What is an SQL Plan Baseline?</h3>
<p>A baseline for a specific query (sql_text) stores one or more execution plans for this query. You can load execution plans into a baseline from <strong>the shared SQL area</strong>, AWR and other sources (see the documentation for package <a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/21/tgsql/managing-sql-plan-baselines.html#GUID-C79E7420-053B-4DCE-A219-072BDE488E21">dbms_spm</a> for more details). And you can persist those baselines in SMB (short for "SQL Plan Management Base"), that is, in your database dictionary or unpack ("export") them into a regular table. This table can be exported/moved using Data Pump etc later on.</p>
<p>The baselines in SMB can be <strong>enabled</strong> and/or <strong>fixed</strong>. Optimizer will first consider only enabled fixed plans. If no enabled fixed plans exists, then all enabled (but not fixed) plans are considered.</p>
<p>Regardless of how many plans for a statement are enabled, Oracle can still try to find a better plan than what is stored in the baseline. If it finds such a plan, then you will have the option of testing and accepting the new plan. One baseline can have many accepted plans - in such case, the optimizer will choose between all accepted plans.</p>
<h3 id="heading-is-this-feature-available-on-standard-edition">Is this feature available on Standard Edition?</h3>
<p>Since 18c, partially, yes. The main limitation is that SE only allows to store a single SQL plan baseline per SQL statement. This was announced on <a target="_blank" href="https://blogs.oracle.com/optimizer/post/plan-stability-in-oracle-database-18c-standard-edition">blogs.oracle.com</a>.</p>
<p>Single plan baseline also means no opportunity for plan evolvement and other fancy features which would require more than a single plan per statement. So, this blog post will only mention what is relevant for SE from here on.</p>
<h3 id="heading-1-use-case-upgrades">1. Use Case: Upgrades</h3>
<p>Have you ever successfully upgraded the database without even a warning, just to be awoken the next morning by the customer stating that their database is running very slow or even seems like it is "hang"?</p>
<p>There are many possible reasons for such experiences and the main one probably being insufficient or no testing at all. Nonetheless, at times, the reason happens to be that the execution plan changed for worse due to newer version of CBO (Cost Based Optimizer).</p>
<p>Sure, you may succeed within a minute by using something like <code>/*+ OPTIMIZER_FEATURES_ENABLE=19.1.0 */</code> and resume your morning coffee. If not, you likely need to figure out what the execution plan was on the previous version.</p>
<p>There are many options on how to obtain the old plan, even without baselines. Those options include ASH, AWR, our <a target="_blank" href="https://appm.abakus.si/">APPM</a> and other third party tools. You may also restore previous version of database and run query there, hoping to produce a better plan.</p>
<p>Anyway, none of this options is as fast as simply doing this:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">declare</span> 
   l_count <span class="hljs-built_in">number</span> := <span class="hljs-number">0</span>;
<span class="hljs-keyword">begin</span>
   l_count := dbms_spm.alter_sql_plan_baseline(
      sql_handle =&gt; <span class="hljs-string">'SQL_b85e793d85b59754'</span>,
      plan_name =&gt; <span class="hljs-string">'SQL_PLAN_bhrmt7q2vb5undd079936'</span>,
      attribute_name =&gt; <span class="hljs-string">'enabled'</span>,
      attribute_value =&gt; <span class="hljs-string">'YES'</span>);
   dbms_output.put_line('enabled [' || l_count || '] baseline plans');
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>So, see, with baselines, your coffee won't even get cold by the time you're done fixing the execution plan.</p>
<p>Regarding <code>sql_handle</code> and <code>plan_name</code> parameters... You can find those values in <code>dba_sql_plan_baselines</code> view by searching for your statement's sql_text:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> sql_handle, plan_name
   <span class="hljs-keyword">from</span> dba_sql_plan_baselines
   <span class="hljs-keyword">where</span> sql_text <span class="hljs-keyword">like</span> <span class="hljs-string">'select ... %'</span>;
</code></pre>
<p><code>sql_handle</code> identifies your SQL. Maybe we can also call this a <em>baseline</em>. Each can have one or more execution plans, identified by <code>plan_name</code>. And each execution plan can be <em>enabled</em> (though in SE, only one <code>plan_name</code> per <code>sql_hande</code> is allowed). You can see the exact plan like this:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> <span class="hljs-keyword">table</span>(dbms_xplan.DISPLAY_SQL_PLAN_BASELINE(
   sql_handle =&gt; <span class="hljs-string">'SQL_b85e793d85b59754'</span>,
   plan_name =&gt; <span class="hljs-string">'SQL_PLAN_bhrmt7q2vb5undd079936'</span>));
</code></pre>
<h3 id="heading-2-use-case-oltp-applications">2. Use Case: OLTP Applications</h3>
<p>Let's imagine an OLTP application, which is deployed to many customers. So, schema and queries are the same at every customer, but each customer has their own data.</p>
<p>Suppose the vendor provides collected baselines. Here are the use cases:</p>
<p>When application is first deployed, customer doesn't yet have any data or they have incomplete data. And so, they don't have the correct statistics. Initial execution plans may be off because of that.</p>
<p>So, on EE we can have baselines which provide a starting point. If database comes up with a better plan than what is in a baseline, we can evaluate/accept that.</p>
<p>On SE, we can have a regular table, containing all baselines. If users experience unexpected delays because of suboptimal execution plan for specific query, we can simply load (and mark as fixed) the vendor supplied, field tested, execution plan.</p>
<p>A quick note on why the title says "OLTP" applications; those are online transaction processing apps, which usually have live users who expect nearly real time response. They get frustrated if they click something and it doesn't respond within the time they deem necessary. More importantly, such workloads tend to use somewhat simpler queries, which generally should execute always the same way. It's not about providing the absolutely best plan possible, it's simply about providing good enough plans that users can rely on.</p>
<p>In contrast to OLTP, say, data warehouses, where it's generally expected to run background jobs which move or transform vast amounts of data and run highly complex queries which produce complex execution plans. Baselines can still be beneficial in such scenarios - just be aware that setting a fixed baseline in SE for such a complex query might do no good when, for example, one of the many referenced tables drastically changes data.</p>
<p>If you are a software vendor, I'd suggest that you think about deploying baselines as part of your software installation.</p>
<h3 id="heading-collecting-baselines">Collecting Baselines</h3>
<p>One, quite simple, way of collecting the baselines is spfile parameter <code>OPTIMIZER_CAPTURE_SQL_PLAN_BASELINES=true</code>. You can fine tune which plans to capture by using <code>DBMS_SPM.CONFIGURE</code> (e.g. to capture only queries with specific parsing schema). This will automatically add baselines for <em>repeatable</em> queries and will only happen at query parse time.</p>
<p>Another way of manually collecting baselines (which works on SE) is by using <code>DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE</code>, like this:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">set</span> serveroutput <span class="hljs-keyword">on</span>;
<span class="hljs-keyword">declare</span>
   l_plan_count <span class="hljs-built_in">number</span> := <span class="hljs-number">0</span>;
<span class="hljs-keyword">begin</span>
   <span class="hljs-keyword">for</span> l_plan <span class="hljs-keyword">in</span> (<span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> sql_id, plan_hash_value 
                     <span class="hljs-keyword">from</span> v$<span class="hljs-keyword">sql</span> 
                     <span class="hljs-keyword">where</span> parsing_schema_name=<span class="hljs-string">'MY_APP'</span>)
   <span class="hljs-keyword">loop</span>
         l_plan_count := l_plan_count + dbms_spm.load_plans_from_cursor_cache (
            sql_id =&gt; l_plan.sql_id,
            plan_hash_value =&gt; l_plan.plan_hash_value,
            <span class="hljs-keyword">fixed</span> =&gt; <span class="hljs-string">'NO'</span>,
            enabled =&gt; <span class="hljs-string">'NO'</span>);                     
   <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;
   dbms_output.put_line('Loaded [' || l_plan_count || '] plans.');
<span class="hljs-keyword">end</span>;
/
</code></pre>
<h3 id="heading-staging-baselines-exporting">Staging Baselines (= Exporting)</h3>
<p>Once you load baselines into SMB, they reside in the data dictionary and, if enabled, will be considered by the optimizer. You can export them from dictionary to a regular table by:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">declare</span>
   l_plan_count <span class="hljs-built_in">number</span> := <span class="hljs-number">0</span>;
<span class="hljs-keyword">begin</span>
   dbms_spm.create_stgtab_baseline (
      table_owner =&gt; <span class="hljs-string">'DEMO_OWNER'</span>,
      table_name =&gt; <span class="hljs-string">'DEMO_STGTAB'</span>);

   l_plan_count := l_plan_count + dbms_spm.pack_stgtab_baseline (
      table_owner =&gt; 'DEMO_OWNER',
      table_name =&gt; 'DEMO_STGTAB');
<span class="hljs-keyword">end</span>;
</code></pre>
<p>You can also verify if baselines were exported by querying the created table:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> <span class="hljs-keyword">count</span>(*) <span class="hljs-keyword">from</span> demo_owner.demo_stgtab;
</code></pre>
<p>If you prefer, you can remove loaded plans from dictionary. You can remove only those that were not enabled and/or only those belonging to specific parsing schema, etc:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">declare</span>
   l_plan_count <span class="hljs-built_in">number</span> := <span class="hljs-number">0</span>;
<span class="hljs-keyword">begin</span>
   <span class="hljs-keyword">for</span> l_baseline <span class="hljs-keyword">in</span> (<span class="hljs-keyword">select</span> sql_handle
                        <span class="hljs-keyword">from</span> dba_sql_plan_baselines
                        <span class="hljs-keyword">where</span> enabled=<span class="hljs-string">'NO'</span>
                           <span class="hljs-keyword">and</span> parsing_schema_name=<span class="hljs-string">'MY_APP'</span>)
   <span class="hljs-keyword">loop</span>
      l_plan_count := l_plan_count + 
         dbms_spm.drop_sql_plan_baseline(sql_handle =&gt; l_baseline.sql_handle);
   <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;
   dbms_output.put_line('Dropped [' || l_plan_count || '] baselines');
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>Now, you can even move those baselines to another database by using Data Pump to export DEMO_STGTAB into, say, baselines.dmp. Then import this in another database and from there into SMB of another database.</p>
<p>Specifically for Standard Edition, I recommend keeping only the baselines that you actually need in SMB and removing all others, before staging and exporting all of  them. Firstly, this will allow you to collect new plans and secondly, there is no restriction in SE on how many plans can be exported in a table - here's my example:</p>
<pre><code class="lang-SQL"><span class="hljs-keyword">select</span> sql_handle, obj_name, <span class="hljs-keyword">max</span>(optimizer_cost) <span class="hljs-keyword">as</span> <span class="hljs-keyword">cost</span>
   <span class="hljs-keyword">from</span> spm_test.spm_export_tab
   <span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> sql_handle, obj_name
   <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">max</span>(optimizer_cost) <span class="hljs-keyword">desc</span>
</code></pre>
<pre><code>SQL_HANDLE                     OBJ_NAME                             COST
------------------------------ ------------------------------ ----------
SQL_9832fdfb8497d4c2           SQL_PLAN_9hcrxzf29gp6202feb565      <span class="hljs-number">19089</span>
SQL_9832fdfb8497d4c2           SQL_PLAN_9hcrxzf29gp62cea8bf8c        <span class="hljs-number">759</span>
</code></pre><h3 id="heading-references">References</h3>
<p>You can find out more about Managing SQL Plan Baselines in:</p>
<ul>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/23/tgsql/managing-sql-plan-baselines.html">official documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_SPM.html">dbms_spm</a> pl/sql package for SPM management</p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/DBA_SQL_PLAN_BASELINES.html">dba_sql_plan_baselines</a> view which displays all available baselines in SMB</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Moving spfile & controlfile to ASM Using OMF]]></title><description><![CDATA[It is a bit tricky to strictly maintain OMF because the filename depends on asm client instance db_unique_name, rather than on what’s written in the files being moved.
It would generally make sense to move spfile first and controlfile after that as t...]]></description><link>https://blog.srecnik.info/moving-spfile-controlfile-to-asm-using-omf</link><guid isPermaLink="true">https://blog.srecnik.info/moving-spfile-controlfile-to-asm-using-omf</guid><category><![CDATA[Oracle Database]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[asm]]></category><dc:creator><![CDATA[Urh Srecnik]]></dc:creator><pubDate>Mon, 09 Sep 2024 19:14:30 GMT</pubDate><content:encoded><![CDATA[<p>It is a bit tricky to strictly maintain <a target="_blank" href="https://docs.oracle.com/database/121/ADMIN/omf.htm#ADMIN003">OMF</a> because the filename depends on asm client instance db_unique_name, rather than on what’s written in the files being moved.</p>
<p>It would generally make sense to move spfile first and controlfile after that as this is the order rdbms instance will access them. However, we intentionally won’t start with spfile as it is only possible to move spfile in OMF fashion if rdbms instance has already established connectivity with asm instance (rdbms instance is registered as rdbms client in asm instance).</p>
<h3 id="heading-controlfile">controlfile</h3>
<pre><code class="lang-plaintext">RMAN&gt; restore controlfile to '+SSD' from '/oradata/ssd/ORCL/controlfile/o1_mf_f1f8q9j1_.ctl';
SQL&gt; alter system set control_files='+SSD/ORCL/CONTROLFILE/current.260.960811081' scope=spfile;
</code></pre>
<p>So, we start with controlfile. First obvious thing here is to specify '+DG' location without full <a target="_blank" href="https://docs.oracle.com/database/121/ADMIN/omf.htm#ADMIN003">target</a> filename. Note that you can specify <em>current</em> contolfile in from clause (provided that your instance is mounted). If you specify backup location instead of current controlfile then your restored controlfile will be flagged as <em>backup</em> control file and thus you would need resetlogs to open the database (or recreate controlfile from source). You can use asmcmd to obtain the actual filename to which controlfile was restored to.</p>
<h3 id="heading-spfile">spfile</h3>
<pre><code class="lang-plaintext">RMAN&gt; backup as copy spfile format '/home/oracle/orcl.spfile';
RMAN&gt; restore spfile to '+HDD' from '/home/oracle/orcl.spfile';

$ rm $ORACLE_HOME/dbs/spfileorcl.ora
$ echo "SPFILE='+HDD/ORCL/PARAMETERFILE/spfile.257.960812067'" &gt; $ORACLE_HOME/dbs/initorcl.ora
</code></pre>
<p>This procedure seems to only work if ASM instance knows which database it is talking to. Specifically, if your rdbms instance is listed in v$asm_client (on asm instance). We achieved this by moving controlfile into ASM before attempting to move spfile. If this would not be the case, then your spfile would be created in +DG/DB_UNKNOWN/ folder.</p>
<p>While created asm <em>alias</em> may be correct, the actual path is determined by the asm client instance. So, when you put spfile or controlfile into asm, it does not matter what is stored in them (for example, spfile has db_unique_name written in itself). What does matter is db_unique_name of asm client which restored (copied) the spfile or controlfile to asm.</p>
]]></content:encoded></item></channel></rss>