{"id":111142,"date":"2026-06-03T12:00:00","date_gmt":"2026-06-03T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=111142"},"modified":"2026-06-02T14:02:46","modified_gmt":"2026-06-02T14:02:46","slug":"why-migrating-from-oracle-is-harder-than-anyone-admits-and-what-you-should-do-instead","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/oracle-databases\/why-migrating-from-oracle-is-harder-than-anyone-admits-and-what-you-should-do-instead\/","title":{"rendered":"Why migrating from Oracle is harder than anyone admits &#8211; and what you should do instead"},"content":{"rendered":"\n<p><strong>Oracle-to-Postgres migration seems simple at first, but everything changes when you hit the DBMS_* packages. The 200,000 lines of procedural logic your application has built on Oracle&#8217;s built-in package ecosystem \u2014 job scheduling, optimizer statistics, transactional messaging, file I\/O, and more \u2014 don&#8217;t migrate automatically, and no tool will tell you that until you&#8217;re already over budget.<\/strong> <strong>Replacing them means reconstructing decades of kernel-level behavior from scratch.<\/strong><\/p>\n\n\n\n<p><strong>This guide covers which packages cause the most damage, what replacement actually looks like in practice, and how to build a migration plan with honest numbers in it.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-introduction-the-common-scenario-i-ve-seen-all-too-often\">Introduction: the common scenario I&#8217;ve seen all too often<\/h2>\n\n\n\n<p>Everyone obsesses over table schemas and indexes\u00a0and, once\u00a0they\u2019re\u00a0finished, they announce their success in <a href=\"https:\/\/www.red-gate.com\/blog\/database-migration\/\" target=\"_blank\" rel=\"noreferrer noopener\">migrating the database to the new database platform<\/a>.\u00a0That&#8217;s great! However, they fail to mention the 200,000 lines of PL\/SQL wrapped inside DBMS packages that make your database\u00a0work &#8211; or how it&#8217;ll\u00a0absolutely <em>break<\/em> you\u00a0in the &#8216;race&#8217; to migrate\u00a0from <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/oracle-databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">Oracle<\/a>.\u00a0<\/p>\n\n\n\n<p>I&#8217;ll begin by describing a scene that plays out in\u00a0conference rooms\u00a0across the industry. A scene\u00a0that\u00a0I\u2019ve\u00a0watched for over a decade now.<\/p>\n\n\n\n<p>It&#8217;s quite simple:\u00a0a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Chief_technology_officer\" target=\"_blank\" rel=\"noreferrer noopener\">CTO<\/a> walks in armed with a spreadsheet. On the left column of said spreadsheet are Oracle licensing costs, which are at least\u00a0seven figures, growing annually, and negotiated under duress every renewal cycle.<\/p>\n\n\n\n<p>On the other side of the spreadsheet: a projected cloud-native <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/\" target=\"_blank\" rel=\"noreferrer noopener\">PostgreSQL<\/a>\u00a0bill, which is dramatically lower. So, of course, the room agrees this is great. The costs of the upcoming <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/migrating-from-on-prem-to-the-cloud-dba-stories\/\" target=\"_blank\" rel=\"noreferrer noopener\">cloud migration<\/a> and planned AI initiatives will be slashed. And just like that, the organization is given the green light to migrate\u00a0away from\u00a0Oracle.\u00a0<\/p>\n\n\n\n<p>Fast-forward 18 months to 2 years and it&#8217;s a different story. The project is massively over budget, the team is exhausted, half the business-critical functionality is broken in ways that only show up under production load, and the organization still has <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/oracle-databases\/oracles-limited-cloud-vendor-licensing\/\" target=\"_blank\" rel=\"noreferrer noopener\">vendor lock-in<\/a>. <\/p>\n\n\n\n<p>The spreadsheet didn&#8217;t lie per se &#8211; the numbers it gave were right &#8211; but it&#8217;s <em>what<\/em> it was measuring that was wrong.<\/p>\n\n\n\n<div id=\"callout-block_2988dc60cbba56f3a8bcd697ce002795\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p><strong>You may also be interested in&#8230;<\/strong><\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/oracle-databases\/introduction-to-oracle-database-for-database-professionals\/\" target=\"_blank\" rel=\"noreferrer noopener\">Kellyn&#8217;s introduction guide to Oracle (for database professionals<\/a>)<\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/podcasts\/how-is-oracle-different-from-other-data-platforms\/\" target=\"_blank\" rel=\"noreferrer noopener\">Podcast: How is Oracle different from other data platforms?<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-this-article-is-not-about\">What this article is <em>not<\/em> about<\/h2>\n\n\n\n<p>I&#8217;m not here to talk about table migration, because that part\u00a0was solved a long time ago. Tools like <a href=\"https:\/\/ora2pg.darold.net\/\" target=\"_blank\" rel=\"noreferrer noopener\">ora2pg<\/a>, <a href=\"https:\/\/docs.aws.amazon.com\/SchemaConversionTool\/latest\/userguide\/CHAP_Welcome.html\" target=\"_blank\" rel=\"noreferrer noopener\">AWS Schema Conversion Tool<\/a>, and others can get your DDL (data definition language) across the wire with\u00a0easy\u00a0fidelity. The indexes,\u00a0<a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/working-within-constraints\/\" target=\"_blank\" rel=\"noreferrer noopener\">constraints<\/a>,\u00a0and even <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/uniqueness-keys-and-identity\/\" target=\"_blank\" rel=\"noreferrer noopener\">foreign keys<\/a>\u00a0come over without a hitch.<\/p>\n\n\n\n<p><strong>The problem is,\u00a0there\u2019s\u00a0going to be those that signal the\u00a0celebration without realizing what\u00a0hasn\u2019t\u00a0come over and what\u00a0can\u2019t\u00a0come over automatically.\u00a0Oracle has spent four decades baking into its procedural\u00a0DNA\u00a0a very powerful\u00a0and\u00a0complex category\u00a0called\u00a0the\u00a0DBMS_*\u202fpackage ecosystem.\u00a0<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-code-you-can-t-read\">The code you can&#8217;t read<\/h2>\n\n\n\n<p>Oracle ships its built-in packages as wrapped, compiled <a href=\"https:\/\/www.techtarget.com\/whatis\/definition\/bytecode\" target=\"_blank\" rel=\"noreferrer noopener\">bytecode<\/a> &#8211; meaning you <em>can<\/em> call <code>DBMS_SCHEDULER.CREATE_JOB<\/code> and read its documented API, but\u00a0you <em>can\u2019t<\/em> read &#8211;\u00a0under any circumstances &#8211; its implementation. Effectively, it&#8217;s a very beneficial &#8216;black box&#8217; delivered with your license.\u00a0<\/p>\n\n\n\n<p>This matters enormously during migration because,\u00a0if your organization has PL\/SQL developers, it&#8217;s almost certainly\u00a0built years of\u00a0logic\u202fon top of\u202fthese packages. Every call\u00a0to a\u202fDBMS_*\u202fpackage is a dependency on behavior that Oracle doesn&#8217;t document fully,\u00a0with\u00a0performance\u00a0tuned over decades and behavior that your developers have come to rely on in ways they\u00a0don&#8217;t\u00a0even consciously recognize.<\/p>\n\n\n\n<p><strong>There are over 200 DBMS_* &#8211; and another 40 or so UTL_*\u00a0packages &#8211; available for use in Oracle version 19c\u00a0or newer.\u00a0This article will touch on only a few of these as examples of what challenges a migration may face due to their use.\u00a0<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_df6b72a1fcdec54431cc47bfa3a40d83\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Simple Talk is brought to you by Redgate Software<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/solutions\/overview\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Discover how Redgate can help you: Simple Talk is brought to you by Redgate Software\">Discover how Redgate can help you<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-first-challenge-the-code-is-wrapped\">The first challenge: the code is wrapped<\/h2>\n\n\n\n<p><strong>The code is wrapped,\u00a0so developers simply can&#8217;t see the code &#8216;under the covers.&#8217;\u00a0This is challenge number one.<\/strong><\/p>\n\n\n\n<p>Here&#8217;s an Oracle DBMS\u00a0metadata\u00a0view wrapped package body (and what\u00a0you\u2019ll\u00a0see):<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >SQL&gt; SELECT text FROM dba_source WHERE name = 'DBMS_SCHEDULER' AND type = 'PACKAGE BODY'; \n\n \n\nDBMS_SCHEDULER wrapped \n\na000000 \n\n369 \n\nabcd \n\n... \n\n[Binary wrapped content    completely unreadable] \n\n0000000000000000000000000000000000000000 <\/pre><\/div>\n\n\n\n<p>You\u00a0can\u2019t\u00a0reverse-engineer it\u00a0or\u00a0diff it against a\u00a0PostgreSQL equivalent. You can only read the documentation,\u00a0observe\u00a0the behavior, and\u00a0attempt\u00a0to reproduce it\u00a0in a different platform environment. The\u00a0code is\u00a0functionally, semantically, and performatively\u00a0hidden from anyone outside of\u00a0those internal at Oracle.<\/p>\n\n\n\n<p>The core problem\u00a0is that\u00a0Oracle&#8217;s\u00a0DBMS packages are not just <a href=\"https:\/\/aws.amazon.com\/what-is\/api\/#:~:text=API%20stands%20for%20Application%20Programming,of%20service%20between%20two%20applications.\" target=\"_blank\" rel=\"noreferrer noopener\">APIs<\/a>, but are\u00a0opaque, compiled executables that interact deeply with the Oracle kernel: the shared memory pool, the\u00a0undo tablespace, the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/oracle-databases\/oracle-archived-redo-size-research\/\" target=\"_blank\" rel=\"noreferrer noopener\">redo log<\/a>, and the scheduler. Replacing them means replacing not just function signatures, but decades of kernel-level integration.\u00a0<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-dbms-universe-a-taxonomy-of-pain\">The DBMS_* universe: a taxonomy of pain<\/h2>\n\n\n\n<p>Specifically, here\u00a0are the most commonly relied-upon Oracle built-in packages. I&#8217;ll\u00a0mention what they\u00a0actually do, and why replacing them is genuinely\u00a0hard work.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-scheduler\">DBMS_SCHEDULER\u00a0<\/h3>\n\n\n\n<p id=\"h-dbms-scheduler-enterprise-job-scheduling-with-calendaring-syntax-job-chains-windows-resource-groups-and-distributed-execution\">Enterprise job scheduling with calendaring syntax, job chains, windows, resource groups, and distributed execution.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<a href=\"https:\/\/access.crunchydata.com\/documentation\/pg_cron\/latest\/\" target=\"_blank\" rel=\"noreferrer noopener\">pg_cron<\/a>\u00a0covers\u00a0maybe 30%\u00a0of this.\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-stats\">DBMS_STATS\u00a0<\/h3>\n\n\n\n<p>Optimizer statistics gathering, histogram management, pending stats, extended stats, and stat locking.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<code>ANALYZE<\/code> is\u00a0still in its infancy.\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-sql\">DBMS_SQL\u00a0<\/h3>\n\n\n\n<p>Dynamic <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/what-is-sql\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL<\/a> execution with fine-grained cursor control, bulk binds, and describe capability.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<code>EXECUTE IMMEDIATE<\/code> gaps are non-trivial.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-pipe\">DBMS_PIPE\u00a0<\/h3>\n\n\n\n<p id=\"h-dbms-pipe-synchronous-and-asynchronous-inter-session-messaging-within-the-database\">Synchronous and asynchronous inter-session messaging within the database.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0no direct PostgreSQL equivalent exists.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-alert\">DBMS_ALERT\u00a0<\/h3>\n\n\n\n<p>Transactional event notification alerts fire only when the triggering transaction commits.\u00a0<\/p>\n\n\n\n<p>The facts:<code>\u00a0LISTEN<\/code>\/<code>NOTIFY<\/code> lacks transactional binding.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-lob\">DBMS_LOB\u00a0<\/h3>\n\n\n\n<p>Fine-grained <a href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/26\/adlob\/LOB-classifications.html\" target=\"_blank\" rel=\"noreferrer noopener\">LOB<\/a> manipulation: chunked reads, writes, appends,\u00a0substr,\u00a0instr\u00a0on BLOBs and CLOBs.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0large objects in PostgreSQL differ <em>semantically<\/em>, so this is an apples-to-oranges comparison.\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-crypto\">DBMS_CRYPTO\u00a0<\/h3>\n\n\n\n<p>Encryption, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/postgresql-is-removing-md5-authentication-for-passwords-heres-everything-you-need-to-know\/#:~:text=Hashing%20is%20the%20process%20of%20transforming%20data%20into%20a%20hash%20value%2C%20or%2C%20in%20other%20words%2C%20a%20string%20of%20characters%2C%20using%20a%20hash%20function%20(sort%20of%20a%20formula%20in%20math.)\" target=\"_blank\" rel=\"noreferrer noopener\">hashing<\/a>, MAC generation using AES, 3DES, RSA, SHA variants.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<a href=\"https:\/\/www.postgresql.org\/docs\/current\/pgcrypto.html\" target=\"_blank\" rel=\"noreferrer noopener\">pgcrypto<\/a>\u00a0maps well, but not identically. However, it can compete!\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-output\">DBMS_OUTPUT\u00a0<\/h3>\n\n\n\n<p>Buffer-based text output from PL\/SQL. Ubiquitous in debugging and reporting procedures.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<code>RAISE NOTICE<\/code> is not semantically equivalent.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-utility\">DBMS_UTILITY\u00a0<\/h3>\n\n\n\n<p>Compilation, dependency analysis, name resolution, comma-string parsing, call stack introspection.&nbsp;<\/p>\n\n\n\n<p>The facts:\u00a0no single PostgreSQL substitute exists.\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-xmlgen\">DBMS_XMLGEN\u00a0<\/h3>\n\n\n\n<p>SQL-to-XML conversion with full control over element naming, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/empty-thoughts-working-with-null\/\" target=\"_blank\" rel=\"noreferrer noopener\">null<\/a> handling, and row encapsulation.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<code>xmlforest<\/code>\/<code>xmlelement<\/code>\u00a0behavior differs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-dbms-parallel-execute\">DBMS_PARALLEL_EXECUTE\u00a0<\/h3>\n\n\n\n<p>Chunk-based parallel DML (data manipulation language) for large table operations with restart capability.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0no native equivalent; requires custom design.\u00a0<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-utl-file\">UTL_FILE\u00a0<\/h3>\n\n\n\n<p>Server-side file I\/O, reading and writing OS files from within PL\/SQL.\u00a0<\/p>\n\n\n\n<p>The facts:\u00a0<code>pg_read_file<\/code>\u00a0is read-only,\u00a0and\u00a0restricted.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-cases-where-it-gets-brutal-case-a-dbms-scheduler\">Cases where it gets brutal (case A: DBMS_SCHEDULER)<\/h2>\n\n\n\n<p>On the surface, <code><a href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/21\/arpls\/DBMS_SCHEDULER.html\" target=\"_blank\" rel=\"noreferrer noopener\">DBMS_SCHEDULER<\/a><\/code> looks replaceable. You have jobs?\u00a0Just use pg_cron\u00a0and\u00a0you\u2019re\u00a0done. Except\u00a0it&#8217;s\u00a0not done &#8211; and experienced teams know that within the first week.\u00a0<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-oracle-pl-sql-a-dbms-scheduler-job-chain-simplified\">Oracle PL\/SQL\u00a0&#8211; a DBMS_SCHEDULER job chain (simplified)\u00a0<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >BEGIN \n\n  DBMS_SCHEDULER.CREATE_PROGRAM( \n\n    program_name   =&gt; 'LOAD_DAILY_POSITIONS', \n\n    program_type   =&gt; 'STORED_PROCEDURE', \n\n    program_action =&gt; 'pkg_etl.load_positions', \n\n    enabled        =&gt; TRUE \n\n  ); \n\n \n\n  DBMS_SCHEDULER.CREATE_SCHEDULE( \n\n    schedule_name   =&gt; 'TRADING_DAYS_ONLY', \n\n    repeat_interval =&gt; 'FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI; \n\n                        BYHOUR=18;BYMINUTE=0; \n\n                        EXCLUDE=NAMED_HOLIDAY_SET:\"NYSE_HOLIDAYS\"' \n\n  ); \n\n \n\n  DBMS_SCHEDULER.CREATE_JOB( \n\n    job_name      =&gt; 'POSITION_LOAD_JOB', \n\n    program_name  =&gt; 'LOAD_DAILY_POSITIONS', \n\n    schedule_name =&gt; 'TRADING_DAYS_ONLY', \n\n    job_class     =&gt; 'HIGH_PRIORITY_CLASS', \n\n    enabled       =&gt; TRUE \n\n  ); \n\n \n\n  -- Chain: positions must complete before risk calcs run \n\n  DBMS_SCHEDULER.CREATE_CHAIN('RISK_CHAIN'); \n\n  DBMS_SCHEDULER.DEFINE_CHAIN_STEP('RISK_CHAIN','STEP1','LOAD_DAILY_POSITIONS'); \n\n  DBMS_SCHEDULER.DEFINE_CHAIN_STEP('RISK_CHAIN','STEP2','CALC_MARKET_RISK'); \n\n  DBMS_SCHEDULER.DEFINE_CHAIN_RULE( \n\n    chain_name =&gt; 'RISK_CHAIN', \n\n    condition  =&gt; 'STEP1 COMPLETED', \n\n    action     =&gt; 'START STEP2' \n\n  ); \n\nEND; \n\n\/ <\/pre><\/div>\n\n\n\n<p>Notice what this one job does? It uses a calendaring expression that explicitly excludes NYSE holidays by name. It runs in a named job class with resource allocation, and\u00a0is part of a dependency chain where step 2\u00a0can\u2019t\u00a0begin until step 1 completes.\u00a0That dependency is managed <em>inside<\/em> the database.\u00a0<\/p>\n\n\n\n<p>With\u202fpg_cron\u202fyou get:\u202f<code>SELECT\u00a0cron.schedule('0 18 * * 1-5', $$CALL\u00a0load_positions()$$);\u00a0<\/code><\/p>\n\n\n\n<p>That&#8217;s\u00a0Monday through Friday at\u00a06pm. It has no concept of market holidays, no job chaining, and no resource classes. To replicate the Oracle behavior fully, you need an external orchestration layer (meaning\u00a0<a href=\"https:\/\/airflow.apache.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Apache Airflow<\/a>, <a href=\"https:\/\/www.prefect.io\/\" target=\"_blank\" rel=\"noreferrer noopener\">Prefect<\/a>, <a href=\"https:\/\/temporal.io\/\" target=\"_blank\" rel=\"noreferrer noopener\">Temporal<\/a>)&#8230;which\u00a0also\u00a0means your scheduling logic leaves the database.<\/p>\n\n\n\n<p>This creates problems further down the line, as the scheduling logic now crosses a network boundary and becomes\u00a0 new infrastructure dependency. Dependency that falls onto your team &#8211; who now has to operate, monitor, and maintain it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-case-b-dbms-stats\">Case B: DBMS_STATS<\/h2>\n\n\n\n<p>We&#8217;ll call this one the &#8216;silent killer.&#8217; It breaks slowly, through query plan degradation that only starts many weeks after go-live, as the data distribution begins to drift from what the optimizer saw at the time of migration.<\/p>\n\n\n\n<div id=\"callout-block_2988dc60cbba56f3a8bcd697ce002795\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p><strong>You may also be interested in:<\/strong><\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/hub\/product-learning\/redgate-monitor\/six-things-to-monitor-with-postgresql\/\" target=\"_blank\" rel=\"noreferrer noopener\">6 things to monitor with PostgreSQL<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<h4 class=\"wp-block-heading\" id=\"h-oracle-pl-sql-fine-grained-statistics-management\">Oracle PL\/SQL &#8211; fine-grained statistics management\u00a0<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >-- Lock stats on a stable reference table, gather with sampling on a volatile one, \n\n-- then publish as a pending set for review before they affect the optimizer \n\nBEGIN \n\n  DBMS_STATS.GATHER_TABLE_STATS( \n\n    ownname          =&gt; 'TRADING', \n\n    tabname          =&gt; 'MARKET_PRICES', \n\n    estimate_percent =&gt; 15, \n\n    method_opt       =&gt; 'FOR ALL COLUMNS SIZE SKEWONLY', \n\n    degree           =&gt; 8, \n\n    no_invalidate    =&gt; FALSE, \n\n    stattype         =&gt; 'DATA' \n\n  ); \n\n \n\n  -- Extended stats on correlated columns the optimizer must understand together \n\n  DBMS_STATS.CREATE_EXTENDED_STATS( \n\n    ownname    =&gt; 'TRADING', \n\n    tabname    =&gt; 'MARKET_PRICES', \n\n    extension  =&gt; '(ASSET_CLASS, INSTRUMENT_TYPE, CURRENCY)' \n\n  ); \n\n \n\n  -- Pending stats: gather without publishing    review plan changes first \n\n  DBMS_STATS.SET_TABLE_PREFS( \n\n    ownname  =&gt; 'TRADING', \n\n    tabname  =&gt; 'MARKET_PRICES', \n\n    pname    =&gt; 'PUBLISH', \n\n    pvalue   =&gt; 'FALSE' \n\n  ); \n\nEND; \n\n\/<\/pre><\/div>\n\n\n\n<p>PostgreSQL&#8217;s\u202f<code><a href=\"https:\/\/www.postgresql.org\/docs\/current\/sql-analyze.html\" target=\"_blank\" rel=\"noreferrer noopener\">ANALYZE<\/a><\/code>\u202fis\u00a0coming along nicely,\u00a0but has the following shortfalls:<\/p>\n\n\n\n<p><strong>No concept of pending statistics.<\/strong><\/p>\n\n\n\n<p><strong>Lack of column group (extended) statistics configuration of this granularity.<\/strong><\/p>\n\n\n\n<p><strong>No built-in sampling percentage control per column.<\/strong><\/p>\n\n\n\n<p><strong>And finally, no mechanism to lock statistics on stable tables while aggressively refreshing volatile ones.<\/strong><\/p>\n\n\n\n<p>DBAs who spent years tuning Oracle&#8217;s optimizer through <code>DBMS_STATS<\/code> will find themselves starting over. And they very often\u00a0won&#8217;t\u00a0understand why certain queries that ran in milliseconds in Oracle are now scanning millions of rows in PostgreSQL.\u00a0\u00a0\u00a0<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-case-c-dbms-pipe-and-dbms-alert\">Case C: DBMS_PIPE and DBMS_ALERT<\/h2>\n\n\n\n<p>These two packages\u00a0represent\u00a0something that simply does not have a PostgreSQL native equivalent: transactional inter-session communication that lives entirely inside the database.\u00a0<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-oracle-pl-sql-transactional-alert-pattern\">Oracle PL\/SQL &#8211; transactional alert pattern\u00a0<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >-- Session A: inside a transaction, register an alert \n\nBEGIN \n\n  -- This alert fires only if THIS transaction commits \n\n  -- If the transaction rolls back, the alert is never sent \n\n  INSERT INTO orders (order_id, status) VALUES (seq_orders.NEXTVAL, 'PENDING'); \n\n \n\n  DBMS_ALERT.SIGNAL( \n\n    name    =&gt; 'NEW_ORDER_ALERT', \n\n    message =&gt; 'order_id=' || seq_orders.CURRVAL \n\n  ); \n\n  COMMIT;  -- Alert fires HERE, only because commit succeeded \n\nEND; \n\n\/ \n\n \n\n-- Session B: waiting listener (blocks until alert or timeout) \n\nDECLARE \n\n  l_message VARCHAR2(1800); \n\n  l_status  INTEGER; \n\nBEGIN \n\n  DBMS_ALERT.WAITONE( \n\n    name    =&gt; 'NEW_ORDER_ALERT', \n\n    message =&gt; l_message, \n\n    status  =&gt; l_status, \n\n    timeout =&gt; 30 \n\n  ); \n\n  IF l_status = 0 THEN \n\n    process_new_order(l_message); \n\n  END IF; \n\nEND; \n\n\/<\/pre><\/div>\n\n\n\n<p>The critical property here\u00a0is one\u00a0that\u2019s\u00a0nearly invisible\u00a0until it breaks. The alert is\u00a0transactional\u00a0and it\u00a0fires if &#8211; and ONLY if &#8211; the<em>\u00a0<\/em><a href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/21\/cncpt\/transactions.html\" target=\"_blank\" rel=\"noreferrer noopener\">transaction<\/a> commits. Otherwise, a\u00a0<a href=\"https:\/\/www.red-gate.com\/simple-talk\/podcasts\/rollback-vs-roll-forward-in-databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">rollback<\/a> suppresses it.<\/p>\n\n\n\n<p>PostgreSQL\u2019s <code>LISTEN<\/code>\/<code>NOTIFY<\/code>\u202fdon&#8217;t have this\u00a0guarantee, however, so a\u00a0notification sent inside a transaction <em>still<\/em> gets delivered to listeners even if it&#8217;s rolled back. Replicating this behavior is awkward, non-trivial, and most certainly was not part of your migration plan. <\/p>\n\n\n\n<p>Specifically, it requires building a custom transactional outbox pattern:\u00a0inserting notifications into a table within the transaction, then having a background worker read and dispatch them post-commit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-pl-sql-is-not-pl-pgsql\">PL\/SQL is <em>not<\/em> PL\/pgSQL\u00a0<\/h2>\n\n\n\n<p>Beyond the DBMS packages, the procedural language itself introduces a category of migration problems that automated tools handle poorly at best.\u00a0<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-oracle-pl-sql-patterns-that-have-no-direct-pl-pgsql-equivalent\">Oracle PL\/SQL\u00a0&#8211; patterns that have no direct PL\/pgSQL\u00a0equivalent\u00a0<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >-- 1.  Autonomous transactions (commit independently of the calling transaction) \n\nCREATE OR REPLACE PROCEDURE log_audit_event(p_event VARCHAR2) AS \n\n  PRAGMA AUTONOMOUS_TRANSACTION; \n\nBEGIN \n\n  INSERT INTO audit_log VALUES (SYSDATE, p_event, SYS_CONTEXT('USERENV','SESSION_USER')); \n\n  COMMIT;  -- Commits WITHOUT committing the outer transaction \n\nEND; \n\n \n\n-- 2.  Package-level state (persists for session lifetime) \n\nCREATE OR REPLACE PACKAGE session_context AS \n\n  g_user_id     NUMBER; \n\n  g_permissions VARCHAR2(4000); \n\n  g_cache       SYS.ODCIVARCHAR2LIST;  -- session-scoped collection \n\n \n\n  PROCEDURE initialize(p_user_id NUMBER); \n\n  FUNCTION  has_permission(p_code VARCHAR2) RETURN BOOLEAN; \n\nEND session_context; \n\n \n\n-- 3.  Bulk collect with LIMIT clause for streaming large result sets \n\nDECLARE \n\n  TYPE t_orders IS TABLE OF orders%ROWTYPE; \n\n  l_orders t_orders; \n\n  CURSOR c_orders IS SELECT * FROM orders WHERE status = 'PENDING'; \n\nBEGIN \n\n  OPEN c_orders; \n\n  LOOP \n\n    FETCH c_orders BULK COLLECT INTO l_orders LIMIT 1000; \n\n    EXIT WHEN l_orders.COUNT = 0; \n\n    FORALL i IN 1..l_orders.COUNT \n\n      INSERT INTO orders_archive VALUES l_orders(i); \n\n    COMMIT;  -- Commit each batch \n\n  END LOOP; \n\nEND; \n\n \n\n-- 4.  Conditional compilation \n\n$IF $$enable_debug $THEN \n\n  DBMS_OUTPUT.PUT_LINE('Debug: entering calc_risk for portfolio ' || p_id); \n\n$END<\/pre><\/div>\n\n\n\n<p>Each of these patterns, unfortunately, require more than just syntax translation. <\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/autonomous-databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">Autonomous<\/a> transactions in PostgreSQL\u00a0require\u00a0a\u202f<code>dblink<\/code>\u202for\u202f<code>postgres_fdw<\/code> &#8211; a network hop to a connection back to the <em>same database<\/em>. This is both architecturally odd <em>and<\/em> has performance implications.\u00a0\u00a0<\/p>\n\n\n\n<p>Meanwhile, Package-level session state requires rearchitecting around\u202f<code>pg_temp<\/code>\u202fschemas or application-layer <a href=\"https:\/\/www.red-gate.com\/simple-talk\/development\/dotnet-development\/caching-the-good-the-bad-and-the-hype\/\" target=\"_blank\" rel=\"noreferrer noopener\">caching<\/a>. <code>BULK COLLECT<\/code> \/ <code>FORALL<\/code>\u202f maps imperfectly to PL\/pgSQL&#8217;s\u00a0array handling, and also loses some of its performance characteristics in the translation.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<h3 class=\"wp-block-heading\" id=\"h-in-summary\">In summary&#8230;<\/h3>\n\n\n\n<p>The migration tools will confidently convert your PL\/SQL to PL\/pgSQL. What they won&#8217;t tell you is that the converted code will compile, run, and\u00a0produce wrong answers in production under conditions the automated\u00a0tests never exercised.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-congratulations-you-now-belong-to-aws-google-or-azure\">Congratulations!\u00a0You now belong to AWS,\u00a0Google,\u00a0or Azure<\/h2>\n\n\n\n<p><strong>Here&#8217;s where the migration story gets a second,\u00a0underappreciated\u00a0chapter. Organizations rightly worry about Oracle licensing, so they\u00a0run toward\u00a0PostgreSQL because\u00a0it&#8217;s <a href=\"https:\/\/db-engines.com\/en\/ranking_osvsc\" target=\"_blank\" rel=\"noreferrer noopener\">open-source<\/a>. This is\u00a0a true statement. <\/strong><\/p>\n\n\n\n<p><strong>However, &#8220;open-source\u00a0database&#8221; and &#8220;no vendor lock-in&#8221; are <em>not<\/em> the same thing &#8211; especially\u00a0when your destination is a <a href=\"https:\/\/www.ibm.com\/think\/topics\/managed-cloud\" target=\"_blank\" rel=\"noreferrer noopener\">managed cloud service<\/a><\/strong>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-what-open-source-postgresql-actually-looks-like-at-scale-on-the-cloud\">What &#8220;open-source\u00a0PostgreSQL&#8221;\u00a0actually looks\u00a0like at scale on the cloud<\/h4>\n<\/blockquote>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >-- AWS Aurora PostgreSQL: uses Aurora's distributed storage layer \n\n-- Snapshots, backups, failover all managed by and dependent on AWS \n\n \n\n-- Extensions you've come to rely on: \n\nCREATE EXTENSION aws_s3;          -- AWS-specific.  Binds you to S3. \n\nCREATE EXTENSION aws_lambda;      -- AWS-specific.  Binds you to Lambda. \n\n \n\n-- Google AlloyDB: Postgres-compatible, not Postgres \n\n-- Proprietary columnar engine underneath, custom memory management \n\n-- Performance characteristics differ from vanilla Postgres \n\n \n\n-- Azure Flexible Server: closest to vanilla, but: \n\n--   - Azure AD integration for auth \n\n--   - Private endpoints tied to Azure Virtual Network \n\n--   - Backup\/PITR through Azure Recovery Services \n\n \n\n-- The moment you use ANY of these managed features at scale, \n\n-- your migration cost OUT of that cloud equals your old Oracle exit cost.<\/pre><\/div>\n\n\n\n<p><a href=\"https:\/\/aws.amazon.com\/rds\/aurora\/\" target=\"_blank\" rel=\"noreferrer noopener\">Amazon Aurora\u00a0PostgreSQL<\/a>\u00a0is\u00a0<em>not<\/em>\u00a0the same as\u00a0open-source PostgreSQL. Instead, it&#8217;s\u00a0a PostgreSQL-compatible interface sitting on top of a proprietary distributed storage engine,\u00a0deeply integrated with <a href=\"https:\/\/aws.amazon.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">AWS<\/a> infrastructure.\u00a0\u00a0<\/p>\n\n\n\n<p>Its performance profile, failure modes, scaling\u00a0behavior&#8230;these\u00a0are AWS&#8217;s, not the\u00a0PostgreSQL\u00a0community&#8217;s. The same is true of <a href=\"https:\/\/cloud.google.com\/products\/alloydb\" target=\"_blank\" rel=\"noreferrer noopener\">Google&#8217;s\u00a0AlloyDB<\/a>, which adds a proprietary columnar acceleration layer. These are excellent products but <em>also<\/em> have\u00a0vendor lock-in,\u00a0just\u00a0dressed in the clothing of open-source.\u00a0<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-extensions-trap\">The extensions trap<\/h2>\n\n\n\n<p><strong>The moment your team starts using\u202faws_s3, Aurora-specific global database features,\u00a0AlloyDB&#8217;s\u00a0columnar store hints, or Azure-specific authentication integrations, you have rebuilt your lock-in dependency.\u00a0You just moved it from an Oracle Enterprise License Agreement to an AWS Enterprise Discount Program negotiation.\u00a0<\/strong><\/p>\n\n\n\n<p>The\u00a0<a href=\"https:\/\/www.redhat.com\/en\/topics\/cloud-computing\/what-is-a-hyperscaler\" target=\"_blank\" rel=\"noreferrer noopener\">hyperscalers<\/a>\u00a0are not naive about\u00a0this: their\u00a0managed database services are deliberately designed to be excellent, and to accumulate switching costs naturally through network effects, deep integrations, and proprietary extensions.\u00a0\u00a0<\/p>\n\n\n\n<p><strong>They win when you migrate to their platform,\u00a0and\u00a0there\u2019s\u00a0a reason\u00a0migration <em>away<\/em> from their platform becomes\u00a0difficult like it is from other platforms, such as Oracle.\u00a0<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-spreadsheet-that-lied\">The spreadsheet that lied<\/h2>\n\n\n\n<p>Let&#8217;s\u00a0revisit that boardroom spreadsheet we mentioned at the start.\u00a0The licensing cost comparison is real &#8211; sure,\u00a0Oracle is expensive.\u00a0But the column labeled &#8220;savings&#8221; is systematically missing several entries:\u00a0<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Cost Category<\/strong>&nbsp;<\/td><td><strong>Oracle (On-Prem)<\/strong>&nbsp;<\/td><td><strong>Cloud Managed Postgres<\/strong>&nbsp;<\/td><\/tr><tr><td>Database license \/ subscription&nbsp;<\/td><td><s>$1,200,000\/yr<\/s>&nbsp;<\/td><td>$180,000\/yr&nbsp;<\/td><\/tr><tr><td>DBA labor (Oracle specialists)&nbsp;<\/td><td><s>$420,000\/yr (3 FTE)<\/s>&nbsp;<\/td><td>$420,000\/yr (same people, new skills needed)&nbsp;<\/td><\/tr><tr><td>Migration engineering labor&nbsp;<\/td><td>&nbsp;&nbsp;&nbsp;<\/td><td><strong>$800,000\u20132,400,000 (one-time)<\/strong>&nbsp;<\/td><\/tr><tr><td>DBMS_* package replacement&nbsp;<\/td><td>Included in license&nbsp;<\/td><td><strong>$300,000\u2013900,000 in custom code<\/strong>&nbsp;<\/td><\/tr><tr><td>External orchestration (Airflow, etc.)&nbsp;<\/td><td>&nbsp;&nbsp;&nbsp;<\/td><td><strong>$60,000\u2013200,000\/yr + engineering<\/strong>&nbsp;<\/td><\/tr><tr><td>Incident recovery (plan degradation, bugs)&nbsp;<\/td><td>Low (mature platform)&nbsp;<\/td><td><strong>High (first 2\u20133 years)<\/strong>&nbsp;<\/td><\/tr><tr><td>Cloud compute + storage at scale&nbsp;<\/td><td>Sunk (existing hardware)&nbsp;<\/td><td><strong>$400,000\u20131,800,000\/yr<\/strong>&nbsp;<\/td><\/tr><tr><td>Cloud egress (data leaving the cloud)&nbsp;<\/td><td>&nbsp;&nbsp;&nbsp;<\/td><td><strong>Variable, sometimes&nbsp;substantial<\/strong>&nbsp;<\/td><\/tr><tr><td>Future exit costs (cloud lock-in)&nbsp;<\/td><td>Known Oracle exit cost&nbsp;<\/td><td><strong>Unknown, but growing annually<\/strong>&nbsp;<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>This is not an argument against\u00a0migration.\u00a0Indeed, there are times where\u00a0migration genuinely makes sense for teams with smaller Oracle footprints &#8211;\u00a0or\u00a0for organizations whose Oracle usage is simple enough that the DBMS_* dependency surface is manageable. Instead, the argument is against doing the math wrong and calling the result a victory.\u00a0<\/p>\n\n\n\n<p><strong>Shifting Oracle licensing cost to cloud infrastructure cost is not saving money.\u00a0It&#8217;s simply moving a number from the left column to the right column of a budget spreadsheet, then presenting the left column to the CFO as evidence of savings&#8230;while hoping nobody notices that the right column grew.\u00a0<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_6a0c7d3a883bf3bc82cdf762b201892c\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Enjoying this article? Subscribe to the Simple Talk newsletter<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Get selected articles, event information, podcasts and other industry content delivered straight to your inbox.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/simple-talk\/subscribe\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Subscribe now: Enjoying this article? Subscribe to the Simple Talk newsletter\">Subscribe now<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-a-more-honest-migration-framework-what-you-should-do\">A more honest migration framework (what you <em>should<\/em> do)<\/h2>\n\n\n\n<p>If your organization is evaluating or mid-stream in an Oracle migration, here is a more grounded approach to the decision:&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-step-1-audit-your-dbms-dependency-first\">Step 1:\u00a0Audit your DBMS_*\u00a0dependency\u00a0<em>first\u00a0<\/em><\/h3>\n\n\n\n<p>Before any migration decision is\u00a0finalized, run a full dependency audit of every DBMS_* call in your codebase. Not just <em>which<\/em> packages are used, but also\u00a0which specific procedures, which behaviors, and which edge cases your developers have come to rely on. This is not a two-hour query, but more like\u00a0weeks of analysis.\u00a0<\/p>\n\n\n\n<div id=\"callout-block_2988dc60cbba56f3a8bcd697ce002795\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p><strong>You may also be interested in&#8230;<\/strong><\/p>\n\n\n\n<p><a href=\"http:\/\/red-gate.com\/simple-talk\/featured\/oracle-asm-monitoring-and-managing-part-2\/\" target=\"_blank\" rel=\"noreferrer noopener\">Oracle ASM: Monitoring and Managing (Part 2)<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/hub\/product-learning\/flyway\/how-can-redgate-tools-help-with-a-cloud-migration\/\" target=\"_blank\" rel=\"noreferrer noopener\">How Redgate tools can help with a cloud migration<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<h4 class=\"wp-block-heading\" id=\"h-oracle-dba-query-to-surface-dbms-dependencies-across-all-schema-objects\">Oracle DBA\u00a0&#8211; query to surface DBMS_* dependencies across all schema objects\u00a0<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >SELECT \n\n  d.owner, \n\n  d.name         AS object_name, \n\n  d.type         AS object_type, \n\n  d.referenced_name, \n\n  s.line, \n\n  s.text \n\nFROM \n\n  dba_dependencies  d \n\n  JOIN dba_source   s ON s.owner = d.owner \n\n                      AND s.name = d.name \n\n                      AND s.type = d.type \n\nWHERE \n\n  d.referenced_owner = 'SYS' \n\n  AND d.referenced_name LIKE 'DBMS_%' \n\n  AND d.owner NOT IN ('SYS','SYSTEM','OUTLN','DBSNMP') \n\nORDER BY \n\n  d.owner, d.name, s.line;<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-step-2-classify-by-replaceability\">Step 2:\u00a0Classify by &#8216;replaceability&#8217;\u00a0<\/h2>\n\n\n\n<p>Not all DBMS_* usage is equally\u00a0difficult.\u00a0<code>DBMS_OUTPUT.PUT_LINE<\/code>\u202fis trivially replaced, whereas\u00a0<code>DBMS_SCHEDULER<\/code> job chains with holiday calendars are not. <\/p>\n\n\n\n<p><strong>So, build a\u00a0tiered inventory: trivial replacements, hard replacements requiring architectural decisions, and functionally irreplaceable behaviors that would require rebuilding application logic from scratch.\u00a0<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-step-3-model-the-full-cost-including-time-to-risk\">Step 3:\u00a0Model the full cost (including time-to-risk)\u00a0<\/h2>\n\n\n\n<p>The risks from an incomplete DBMS_* replacement only surface later down the line &#8211; usually in production and under load, in edge cases.\u00a0Budget for a sustained <a href=\"https:\/\/www.red-gate.com\/simple-talk\/cloud\/how-to-ensure-success-following-a-cloud-migration\/\" target=\"_blank\" rel=\"noreferrer noopener\">post-migration<\/a> engineering stabilization period\u00a0must be\u00a0realistically\u00a0built,\u00a0including\u00a0two to three years for a complex Oracle workload. <\/p>\n\n\n\n<p><strong>If that cost makes the business case weaker, so be it. It&#8217;s the correct, honest information, and doesn&#8217;t need any more explanation than that.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-step-4-evaluate-cloud-independent-deployment\">Step 4:\u00a0Evaluate cloud-independent deployment\u00a0<\/h2>\n\n\n\n<p>If you migrate to PostgreSQL, consider what option gives you better long-term positioning than a full managed, cloud-native service. Would a self-managed PostgreSQL on cloud\u00a0<a href=\"https:\/\/www.vmware.com\/topics\/virtual-machine\" target=\"_blank\" rel=\"noreferrer noopener\">virtual machines (VMs)<\/a> do the job, or does a simple\u00a0on-premises approach suffice?<\/p>\n\n\n\n<p><strong>I know this may be\u00a0unpopular but, before you swap one lock-in for another,\u00a0make sure you&#8217;ve only lost <em>some<\/em> operational convenience.\u00a0If you gain portability and predictable cost, then you can decide if the cloud makes sense after proving it out. For large, stable\u00a0workloads,\u00a0the math often favors the latter.\u00a0<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-step-5-consider-oracle-alternatives-that-don-t-require-full-migration\">Step 5:\u00a0Consider Oracle alternatives that <em>don&#8217;t<\/em> require full migration\u00a0<\/h2>\n\n\n\n<p>License cost negotiation with Oracle is not fixed. Oracle&#8217;s licensing practices are aggressive, but so is\u00a0competition\u00a0and organizations with leverage\u00a0can be persuasive if they are actively\u00a0evaluating alternatives.\u00a0\u00a0<\/p>\n\n\n\n<p><strong>Most\u00a0find that Oracle&#8217;s negotiating floor is significantly lower than their current contract\u00a0and a\u00a0credible migration threat, even a partial one, changes the conversation completely. This sometimes even leads to discounts upwards of 80% for large enterprise customers.\u00a0<\/strong><\/p>\n\n\n\n<p>The goal is not loyalty to Oracle, but in\u00a0making decisions with accurate data about true total cost. This includes the cost of what you&#8217;d have to rebuild, and the cost of the new lock-in\u00a0you\u2019d\u00a0acquire\u00a0on the way out.\u00a0<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-migration-that-actually-works\">The migration that <em>actually works<\/em><\/h2>\n\n\n\n<p><strong>Successful Oracle migrations\u00a0do\u00a0exist<\/strong>, <strong>and they all look quite similar. They took longer than initially planned, were led by engineers with familiarity of both platforms, and there was heavy investment in understanding and replacing DBMS_* behavior before cutting over. And they were honest with leadership about the <em>real<\/em> costs throughout.<\/strong><\/p>\n\n\n\n<p>What they did <em>not<\/em> do\u00a0is\u00a0treat the database schema as the hard part, because the\u00a0schema is, in fact, the <em>easy<\/em> part.<\/p>\n\n\n\n<p>The hard part is the 200,000 lines of accumulated behavioral logic that Oracle has made fast, reliable, and invisible over 40 years. The very logic that your application has silently learned to trust.\u00a0<\/p>\n\n\n\n<p>The DBMS packages are wrapped because Oracle&#8217;s IP\u00a0requires\u00a0it. But they are also, in a sense, wrapped in your organization&#8217;s institutional knowledge, production incident history, and in the edge cases your DBA patched at 2am. That knowledge is what\u00a0<em>doesn&#8217;t<\/em>\u00a0migrate &#8211; it\u00a0has to\u00a0be reconstructed, tested, and earned again on the new platform.\u00a0<\/p>\n\n\n\n<p><strong>Know what\u00a0you&#8217;re\u00a0buying when you sign the migration project plan,\u00a0and make sure the right column of that spreadsheet has honest numbers in it.\u00a0<\/strong><\/p>\n\n\n\n<p><strong>And\u00a0remember:\u00a0there is no such thing as\u00a0a free\u00a0migration. There are only migrations where the true costs show up later, and migrations where you accounted for them honestly from the start.<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_988207e5f1bea7fbe3260121dcf98559\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">How to use Redgate Flyway as a multi-database migration system<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Learn how to use Flyway to do a single-batch, multi-database migration, comprising SQL Server, Oracle Cloud, PostgreSQL, MySQL and SQLite databases.<br \/>\r\n                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/hub\/product-learning\/flyway\/using-flyway-as-a-multi-database-migration-system\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Read the guide: How to use Redgate Flyway as a multi-database migration system\">Read the guide<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: Why migrating from Oracle is harder than anyone admits<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. Why do Oracle-to-PostgreSQL migrations take longer and cost more than projected?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Most migration estimates focus on schema conversion \u2014 tables, indexes, constraints \u2014 which tooling handles well. The real time sink is Oracle&#8217;s DBMS_* package ecosystem: over 200 built-in packages containing job scheduling, statistics management, cryptography, inter-session messaging, and more. These are compiled as unreadable bytecode, deeply integrated with Oracle&#8217;s kernel, and have no direct PostgreSQL equivalents. Replacing them requires custom engineering, not just syntax conversion.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Can automated migration tools like ora2pg handle PL\/SQL conversion?<\/h3>\n            <div class=\"faq-answer\">\n                <p>They handle DDL well and will convert most PL\/SQL syntax to PL\/pgSQL. The risk is that converted code often compiles and runs, but produces wrong results under production conditions that automated tests don&#8217;t cover \u2014 particularly around package-level session state, autonomous transactions, BULK COLLECT behaviour, and any logic that depends on DBMS_* packages whose semantics differ in PostgreSQL.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Isn&#039;t migrating to cloud PostgreSQL a way to avoid vendor lock-in?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Not automatically. &#8220;Open-source database&#8221; and &#8220;no vendor lock-in&#8221; are different things. AWS Aurora PostgreSQL runs on a proprietary distributed storage engine. Google AlloyDB adds a proprietary columnar layer. Once your team adopts cloud-native extensions like aws_s3 or aws_lambda, Aurora global database features, or Azure AD authentication integrations, you have rebuilt lock-in \u2014 just with a different vendor and a different contract to renegotiate.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. How should an organization prepare for an Oracle migration honestly?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Start with a full DBMS_* dependency audit before any migration decision is finalised \u2014 not which packages are used, but which specific behaviours your developers rely on. Classify each dependency by replaceability: trivial, hard, or architecturally irreplaceable. Model the full cost including external orchestration tooling, post-migration stabilisation (realistically 2\u20133 years for complex workloads), and the new cloud lock-in you&#8217;ll accumulate. Only then does the spreadsheet comparison become honest.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. Is staying on Oracle ever the right call?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Sometimes, yes \u2014 at least as a negotiating position. Oracle&#8217;s published licensing costs are not fixed; organizations actively evaluating alternatives frequently achieve significant discounts, sometimes 60\u201380% off, simply by presenting a credible migration plan. The goal is not loyalty to Oracle but making the decision with accurate total-cost data, including what you&#8217;d have to rebuild and what new lock-in you&#8217;d acquire on the way out.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Oracle-to-Postgres migrations routinely go over budget \u2014 not because of schemas, but because of DBMS_* packages. Learn what breaks, why automated tools miss it, and how to build an honest migration plan.&hellip;<\/p>\n","protected":false},"author":316206,"featured_media":108164,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143533,143534],"tags":[4168,4309,4459,158978],"coauthors":[48576],"class_list":["post-111142","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-oracle-databases","category-postgresql","tag-database","tag-migration","tag-oracle","tag-postgresql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111142","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/316206"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=111142"}],"version-history":[{"count":6,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111142\/revisions"}],"predecessor-version":[{"id":111188,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111142\/revisions\/111188"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/108164"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=111142"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=111142"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=111142"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=111142"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}