<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Shadab Mohammad on Medium]]></title>
        <description><![CDATA[Stories by Shadab Mohammad on Medium]]></description>
        <link>https://medium.com/@shadabshaukat?source=rss-51b8ef0ed6f4------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*QeBQiP5xtEGGl0NvdGr2EQ.png</url>
            <title>Stories by Shadab Mohammad on Medium</title>
            <link>https://medium.com/@shadabshaukat?source=rss-51b8ef0ed6f4------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 23:35:16 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@shadabshaukat/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[25 Oracle Database Tricks Every Backend Engineer Should Know]]></title>
            <link>https://shadabshaukat.medium.com/25-oracle-database-tricks-every-backend-engineer-should-know-9684f95e8b5f?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/9684f95e8b5f</guid>
            <category><![CDATA[sql-queries]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[oracle]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Tue, 27 Oct 2026 12:51:14 GMT</pubDate>
            <atom:updated>2026-10-27T12:51:14.182Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Practical SQL and PL/SQL patterns for faster queries, safer writes, and easier production debugging.</em></p><p>A slow endpoint can start with a small database decision: fetching unnecessary columns, binding the wrong data type, issuing hundreds of individual updates, or keeping a transaction open while waiting for another service.</p><p>Oracle Database gives backend engineers useful tools for addressing these problems. The challenge is knowing which tool fits — and checking whether it improves the actual workload.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qloFdqwStJJLoiUD.png" /></figure><p>Here are 25 techniques worth keeping close.</p><p><strong>Compatibility:</strong> Examples target Oracle Database 19c. Tables and columns are illustrative, and :name denotes a bind variable supplied by your application or SQL client.</p><h3>1. Fetch Only the Columns You Need</h3><p>If an endpoint needs a customer’s name and email, make that explicit:</p><pre>SELECT customer_name, email<br>FROM customers<br>WHERE customer_id = :customer_id;</pre><p>A smaller projection reduces the result payload and can make an index sufficient to answer a query without accessing the table. Whether that happens depends on the selected columns, index definition, and execution plan.</p><p>Avoid assuming every extra column causes another physical read; Oracle retrieves table data in blocks. The reliable benefit is avoiding unnecessary data retrieval and transfer. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/optimizer-access-paths.html">Oracle access paths</a></p><h3>2. Inspect the Executed Plan</h3><p>An estimated plan is useful. Runtime statistics show what happened during execution.</p><p>For a targeted investigation:</p><pre>SELECT /*+ gather_plan_statistics */<br>       order_id, total_amount<br>FROM orders<br>WHERE customer_id = :customer_id;</pre><p>Fetch the complete result, then inspect the cursor:</p><pre>SELECT *<br>FROM TABLE(<br>  DBMS_XPLAN.DISPLAY_CURSOR(<br>    NULL,<br>    NULL,<br>    &#39;ALLSTATS LAST&#39;<br>  )<br>);</pre><p>Compare estimated rows with actual rows, and examine buffer gets and repeated operations. Large differences can expose poor cardinality estimates.</p><p>Use a specific SQL ID and child cursor when client activity makes “the last statement” ambiguous. DISPLAY_CURSOR also requires access to Oracle’s relevant dynamic performance views. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_XPLAN.html">DBMS_XPLAN documentation</a></p><h3>3. Bind Values Instead of Building SQL Strings</h3><p>Keep statement text stable:</p><pre>SELECT order_id, status<br>FROM orders<br>WHERE customer_id = :customer_id;</pre><p>Generating a different SQL string for every customer can increase hard parsing. Bind variables enable cursor sharing and keep values separate from SQL syntax.</p><p>Bind with the appropriate data type, too. A numeric customer ID should arrive as a number.</p><p>Binds do not guarantee one ideal plan for every value; skewed data can require different execution strategies. Oracle’s adaptive cursor sharing helps address that distinction. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/improving-rwp-cursor-sharing.html">Oracle cursor sharing</a></p><h3>4. Design Composite Indexes Around Queries</h3><p>Consider this access pattern:</p><pre>SELECT order_id, created_at<br>FROM orders<br>WHERE customer_id = :customer_id<br>  AND status = :status<br>ORDER BY created_at DESC;</pre><p>A candidate index is:</p><pre>CREATE INDEX orders_customer_status_dt_ix<br>ON orders(customer_id, status, created_at DESC);</pre><p>Its leading columns match equality predicates, followed by the requested ordering.</p><p>Treat this as a candidate to measure. Index order should reflect the workload; “put the most selective column first” is not a universal rule. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/optimizer-access-paths.html">Oracle access paths</a></p><h3>5. Express Date Filters as Ranges</h3><p>For a DATE column, a day filter can be written as:</p><pre>SELECT order_id, created_at<br>FROM orders<br>WHERE created_at &gt;= DATE &#39;2026-10-27&#39;<br>  AND created_at &lt;  DATE &#39;2026-10-28&#39;;</pre><p>This keeps the indexed column unwrapped and includes every time within the day.</p><p>A predicate such as TRUNC(created_at) = ... may need a matching function-based index for efficient indexed access. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/optimizer-access-paths.html">Oracle index access paths</a></p><h3>6. Index Expressions You Regularly Search</h3><p>If case-insensitive email lookup is part of the application’s rules:</p><pre>CREATE INDEX customers_email_lower_ix<br>ON customers(LOWER(email));</pre><p>Use the matching expression:</p><pre>SELECT customer_id<br>FROM customers<br>WHERE LOWER(email) = LOWER(:email);</pre><p>A function-based index gives Oracle an access path for the expression itself.</p><p>Choose normalization rules deliberately, and remember that every additional index adds storage and write-maintenance work. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html">Function-based indexes</a></p><h3>7. Treat NULL as a Separate State</h3><p>This predicate does not find missing values:</p><pre>WHERE cancelled_at = NULL</pre><p>Use:</p><pre>WHERE cancelled_at IS NULL</pre><p>In Oracle 19c, a zero-length character string is also treated as null. That matters when application code distinguishes &quot;&quot; from a missing value.</p><p>For example, email = &#39;&#39; does not find empty emails. Define missing-value behavior explicitly at the application boundary. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Nulls.html">Oracle NULL semantics</a></p><h3>8. Use EXISTS When the Question Is About Existence</h3><p>To find customers who have orders:</p><pre>SELECT c.customer_id, c.customer_name<br>FROM customers c<br>WHERE EXISTS (<br>  SELECT 1<br>  FROM orders o<br>  WHERE o.customer_id = c.customer_id<br>);</pre><p>This expresses the requirement directly and avoids generating duplicate customer rows that later require DISTINCT.</p><p>It does not guarantee a faster plan than every alternative. It gives the optimizer — and the next engineer — a clear statement of intent. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/EXISTS-Condition.html">EXISTS condition</a></p><h3>9. Use Keyset Pagination for Sequential Browsing</h3><p>For a feed ordered by a unique, increasing ID:</p><pre>SELECT order_id, created_at, total_amount<br>FROM orders<br>WHERE order_id &gt; :last_seen_order_id<br>ORDER BY order_id<br>FETCH FIRST 50 ROWS ONLY;</pre><p>This lets the next page start after the previous page’s final key.</p><p>Use a unique, stable ordering. If the business order uses a timestamp, include a unique tie-breaker in both the ordering and continuation predicate.</p><p>Keyset pagination suits next-page navigation; arbitrary page-number jumps need a different design. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html">Oracle row limiting</a></p><h3>10. Avoid the ROWNUM Top-N Trap</h3><p>This does not reliably return the ten highest-paid employees:</p><pre>SELECT employee_id, salary<br>FROM employees<br>WHERE ROWNUM &lt;= 10<br>ORDER BY salary DESC;</pre><p>The row restriction can happen before the final sort.</p><p>Write the requirement directly:</p><pre>SELECT employee_id, salary<br>FROM employees<br>ORDER BY salary DESC, employee_id<br>FETCH FIRST 10 ROWS ONLY;</pre><p>The employee ID makes ties deterministic. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ROWNUM-Pseudocolumn.html">ROWNUM behavior</a></p><h3>11. Find the Latest Row per Group with ROW_NUMBER</h3><p>To retrieve each customer’s latest order:</p><pre>SELECT customer_id, order_id, created_at<br>FROM (<br>  SELECT customer_id,<br>         order_id,<br>         created_at,<br>         ROW_NUMBER() OVER (<br>           PARTITION BY customer_id<br>           ORDER BY created_at DESC, order_id DESC<br>         ) AS rn<br>  FROM orders<br>)<br>WHERE rn = 1;</pre><p>The unique tie-breaker matters when two orders share a timestamp.</p><p>This pattern also works for the newest event per device, the latest subscription per account, or the top three products per category. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ROW_NUMBER.html">ROW_NUMBER documentation</a></p><h3>12. Use MERGE for Insert-or-Update Workflows</h3><p>Oracle’s MERGE combines matched updates and unmatched inserts:</p><pre>MERGE INTO customer_preferences p<br>USING (<br>  SELECT :customer_id AS customer_id,<br>         :theme AS theme<br>  FROM dual<br>) s<br>ON (p.customer_id = s.customer_id)<br>WHEN MATCHED THEN<br>  UPDATE SET p.theme = s.theme<br>WHEN NOT MATCHED THEN<br>  INSERT (customer_id, theme)<br>  VALUES (s.customer_id, s.theme);</pre><p>Ensure the source has at most one applicable row per target key.</p><p>Also retain a primary or unique constraint. Concurrent sessions can race to insert the same absent key; MERGE does not remove the need to handle uniqueness conflicts. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/MERGE.html">MERGE documentation</a></p><h3>13. Return Inserted Values Without Another Query</h3><p>Retrieve an inserted identifier through an output bind:</p><pre>INSERT INTO orders (<br>  order_id,<br>  customer_id,<br>  total_amount<br>)<br>VALUES (<br>  orders_seq.NEXTVAL,<br>  :customer_id,<br>  :total_amount<br>)<br>RETURNING order_id INTO :new_order_id;</pre><p>This avoids a follow-up query just to discover the inserted key.</p><p>Your driver must support the output binding used by the statement. Never use MAX(order_id) to identify your own insert; another session may have inserted a larger value. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html">INSERT and RETURNING</a></p><h3>14. Put Data Invariants in Constraints</h3><p>If an inventory record must always have a nonnegative quantity:</p><pre>CREATE TABLE inventory (<br>  product_id NUMBER PRIMARY KEY,<br>  quantity   NUMBER NOT NULL,<br>  CONSTRAINT inventory_quantity_ck<br>    CHECK (quantity &gt;= 0)<br>);</pre><p>Application validation provides helpful feedback. Database constraints enforce the rule across every writer.</p><p>Use both NOT NULL and CHECK here: a check constraint alone does not reject null merely because its condition evaluates to unknown. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/constraint.html">Oracle constraints</a></p><h3>15. Review Foreign-Key Indexes</h3><p>For a frequently accessed child relationship:</p><pre>CREATE INDEX order_items_order_ix<br>ON order_items(order_id);</pre><p>Foreign-key indexing affects more than joins. Unindexed child foreign keys can cause broader locking during parent-key changes or deletes.</p><p>Check existing indexes before creating another one. An appropriate composite index may already cover the foreign-key columns. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/data-concurrency-and-consistency.html">Foreign keys and locking</a></p><h3>16. Replace Procedural Updates with Set-Based SQL</h3><p>If every qualifying row receives the same transformation, express it once:</p><pre>UPDATE subscriptions<br>SET status = &#39;EXPIRED&#39;<br>WHERE status = &#39;ACTIVE&#39;<br>  AND expires_at &lt; :cutoff;</pre><p>Fetching IDs into application code and issuing one update per ID adds round trips and repeated statement execution.</p><p>A single statement is a useful starting point. Large maintenance jobs may still need bounded batches based on undo, lock duration, restartability, and business requirements. Oracle’s bulk-processing guidance explains the overhead of repeated SQL calls. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/bulk-sql-and-bulk-binding.html">Bulk SQL documentation</a></p><h3>17. Use FORALL When Procedural Batching Is Necessary</h3><p>When PL/SQL already has a collection of IDs:</p><pre>DECLARE<br>  TYPE id_list IS TABLE OF NUMBER;<br>  l_ids id_list := id_list(101, 102, 103);<br>BEGIN<br>  FORALL i IN 1 .. l_ids.COUNT<br>    UPDATE orders<br>    SET status = &#39;ARCHIVED&#39;<br>    WHERE order_id = l_ids(i);<br>END;<br>/</pre><p>FORALL batches communication between PL/SQL and SQL.</p><p>Prefer a single set-based statement when possible. When using collections, bound their size and define whether failures should stop processing or be collected with SAVE EXCEPTIONS. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/bulk-sql-and-bulk-binding.html">FORALL and bulk binding</a></p><h3>18. Commit at the Business Transaction Boundary</h3><p>A transfer requires both balance changes to succeed together:</p><pre>UPDATE accounts<br>SET balance = balance - :amount<br>WHERE account_id = :source_id;</pre><pre>UPDATE accounts<br>SET balance = balance + :amount<br>WHERE account_id = :destination_id;</pre><pre>COMMIT;</pre><p>Application code must validate the amount, affected rows, and funds policy, then roll back on failure.</p><p>Committing after the first update breaks atomicity. Keeping the transaction open while calling a remote service prolongs lock ownership.</p><p>Also remember that Oracle DDL can implicitly commit surrounding work. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/COMMIT.html">COMMIT semantics</a></p><h3>19. Use SKIP LOCKED for Competing Workers</h3><p>Queue consumers can skip rows another transaction has locked:</p><pre>SELECT job_id<br>FROM job_queue<br>WHERE status = &#39;READY&#39;<br>FOR UPDATE SKIP LOCKED;</pre><p>Fetch a bounded batch through a cursor, mark those jobs claimed in the same transaction, and commit promptly.</p><p>In 19c, do not combine FETCH FIRST with FOR UPDATE in the same query block.</p><p>A complete worker design also needs retries, recovery of abandoned claims, and idempotent processing. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html">Oracle locking clauses</a></p><h3>20. Detect Lost Updates with a Version Column</h3><p>For records edited across separate requests:</p><pre>UPDATE customer_profiles<br>SET display_name = :display_name,<br>    version_no = version_no + 1<br>WHERE customer_id = :customer_id<br>  AND version_no = :expected_version;</pre><p>Check the affected-row count immediately.</p><p>One updated row means the expected version matched. Zero rows means the record was changed or removed, so the application must handle the conflict.</p><p>Initialize version_no as non-null, and require every writer to follow the same convention. This applies optimistic concurrency control without holding a lock across user interaction. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/data-concurrency-and-consistency.html">Oracle concurrency concepts</a></p><h3>21. Check Statistics Before Adding Hints</h3><p>Inspect the statistics available for a table:</p><pre>SELECT table_name,<br>       num_rows,<br>       last_analyzed,<br>       stale_stats<br>FROM user_tab_statistics<br>WHERE table_name = &#39;ORDERS&#39;<br>  AND partition_name IS NULL;</pre><p>A major data load or distribution change can make existing statistics less representative.</p><p>Where appropriate, gather statistics through the established operational process:</p><pre>BEGIN<br>  DBMS_STATS.GATHER_TABLE_STATS(<br>    ownname =&gt; USER,<br>    tabname =&gt; &#39;ORDERS&#39;<br>  );<br>END;<br>/</pre><p>Oracle already supports automatic collection. Investigate whether it is meeting the workload’s needs before adding routine manual gathers. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/tgsql/gathering-optimizer-statistics.html">Optimizer statistics collection</a></p><h3>22. Keep Data Types Aligned</h3><p>Suppose external_reference is a VARCHAR2 column containing values such as &#39;00123&#39;.</p><p>Bind it as text:</p><pre>SELECT order_id<br>FROM orders<br>WHERE external_reference = :reference_text;</pre><p>Binding a number can trigger implicit conversion, change matching semantics, cause conversion errors, or prevent an expected index access path.</p><p>Dates deserve the same care. Use typed date or timestamp binds, or explicit literals, instead of depending on a session’s default date format. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Type-Comparison-Rules.html">Oracle conversion rules</a></p><h3>23. Extract JSON Values Inside SQL</h3><p>For JSON documents stored in payload:</p><pre>SELECT event_id,<br>       JSON_VALUE(<br>         payload,<br>         &#39;$.customerId&#39;<br>         RETURNING NUMBER<br>         ERROR ON ERROR<br>       ) AS customer_id<br>FROM events;</pre><p>This retrieves a scalar without transferring the entire document for application-side parsing.</p><p>Choose error handling deliberately. JSON_VALUE defaults to returning null on errors; ERROR ON ERROR makes malformed values visible, while missing-field behavior can be controlled separately.</p><p>Frequently searched JSON expressions can also be indexed. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/adjsn/function-JSON_VALUE.html">JSON_VALUE documentation</a></p><h3>24. Use Flashback Query to Investigate Recent Changes</h3><p>To inspect an earlier committed state:</p><pre>SELECT order_id, status, total_amount<br>FROM orders AS OF TIMESTAMP (<br>  SYSTIMESTAMP - INTERVAL &#39;10&#39; MINUTE<br>)<br>WHERE order_id = :order_id;</pre><p>This can help answer: “What did this order contain before the update?”</p><p>Historical availability depends on retained undo, privileges, and intervening operations. An AS OF query reads past data; it does not restore the current row. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/flashback.html">Oracle Flashback Query</a></p><h3>25. Label Database Work with Application Context</h3><p>Make sessions recognizable during production investigations:</p><pre>BEGIN<br>  DBMS_APPLICATION_INFO.SET_MODULE(<br>    module_name =&gt; &#39;orders-api&#39;,<br>    action_name =&gt; &#39;create-order&#39;<br>  );<br>END;<br>/</pre><p>The module and action help connect database activity to application operations.</p><p>With connection pools, set the context when borrowing a connection and clear or replace it before reuse:</p><pre>BEGIN<br>  DBMS_APPLICATION_INFO.SET_MODULE(NULL, NULL);<br>END;<br>/</pre><p>Use stable operation names. Keep secrets and customer payloads out of diagnostic labels. <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_APPLICATION_INFO.html">DBMS_APPLICATION_INFO documentation</a></p><p>Backend performance improves when database work is deliberate: retrieve the required data, express operations clearly, keep transactions bounded, and inspect execution evidence before tuning.</p><p>Start with one expensive endpoint. Measure its SQL, change one thing, and measure again.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9684f95e8b5f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Lakehouse Querying OCI Object Storage from OCI PostgreSQL using Oracle Autonomous 26ai Database]]></title>
            <link>https://shadabshaukat.medium.com/oci-postgresql-oracle-autonomous-26ai-and-oci-object-storage-querying-lakehouse-data-across-oci-a371a00a6d50?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/a371a00a6d50</guid>
            <category><![CDATA[oci-object-storage]]></category>
            <category><![CDATA[oracle-26ai]]></category>
            <category><![CDATA[lake-house]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <category><![CDATA[oci-postgresql]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Sat, 03 Oct 2026 12:44:49 GMT</pubDate>
            <atom:updated>2026-10-06T03:48:56.241Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>It is a brave new world, and every system talks to every system. The database wars are highly exaggerated. Postgres and Oracle talk to each other and share the tables and data amongst each other. So why all the hate and competition :)</blockquote><p>Postgres and Oracle are often considered polar opposites where it is either one of the other. Both databases have their strengths and drawbacks. Two is better than one, ask anyone, it is fun fun fun :)</p><p>Alrighty, now that we have got all the cute puns out of the way; lets dive deep into how would a solution look like if you want to query OCI Object Storage from OCI PostgreSQL. For this recipe we will use an intermittent layer of self-managed Postgres in OCI VM and Oracle Autonomous 26ai.</p><p>In this walkthrough, OCI Database with PostgreSQL queries orders stored in a Parquet file in OCI Object Storage. Oracle Autonomous AI Database 26ai reads the file as an external table, and a self-managed PostgreSQL server on an OCI Ubuntu VM bridges the two databases. While there is extra layers and hops. Currently this is the only way to achieve something like this.</p><p>But there is so much more you can do with the self-managed Postgres in the VM and the Oracle Autonomous 26ai Database.</p><h4>Some other use-cases :</h4><ul><li><strong>Join Operation and Relational data. </strong>From your OCI PostgreSQL database and Historical Data in your Autonomous Lakehouse being queried together. No need for any reverse ETL</li><li><strong>Query Oracle tables and views from OCI PostgreSQL.</strong> The same bridge can expose selected ADB operational data without first copying whole tables into PostgreSQL.</li><li><strong>Join application and lake data.</strong> An OCI PostgreSQL application can join its local orders with selected rows from an ADB external table over Parquet files.</li><li><strong>Validate a migration in stages.</strong> Keep Oracle and PostgreSQL data accessible during a transition, then compare counts, keys, and business aggregates before changing an application’s data source.</li><li><strong>Explore other external sources.</strong> ADB can also expose supported Apache Iceberg tables through SQL. The bridge lets you test those results from OCI PostgreSQL, while ADB remains responsible for accessing the underlying lake data</li></ul><h3>Data Flow for the Solution</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sAl5EwunkhMMbivlYWQjHw.png" /><figcaption>OCI PostreSQL → Self-Managed Postgres → ADB26ai → OCI Object Storage</figcaption></figure><h3>Set Up the Two-Hop FDW Bridge: OCI PostgreSQL → Ubuntu PostgreSQL → Autonomous Database 26ai</h3><p>Install <a href="https://www.server-world.info/en/note?os=Ubuntu_22.04&amp;p=postgresql&amp;f=1">PostgreSQL 14 Server on OCI VM with Ubuntu 22</a>, and then install Oracle FDW extension on it.</p><p>The bridge is necessary for this particular design because managed OCI Database with PostgreSQL supports postgres_fdw but does not list oracle_fdw among its supported extensions.</p><p>The Ubuntu server runs oracle_fdw; OCI PostgreSQL reaches that server through postgres_fdw. We will verify the data at each hop and then query it alongside a local PostgreSQL table. The Parquet file stays in Object Storage while each query retrieves the rows it needs through the chain.</p><p>This method can be used to query both external tables and relational tables in Oracle Autonomous Database 26ai from OCI PostgreSQL.</p><pre>sudo apt-get update<br>sudo apt-get install postgresql-14-oracle-fdw<br>sudo apt-get install libaio1<br><br>postgres=# CREATE EXTENSION oracle_fdw;<br>CREATE EXTENSION<br><br>TNS entry for ADB 26ai from OCI Console :<br>(description= (retry_count=20)(retry_delay=3)(address=(protocol=tcps)(port=1521)(host=hr4mqmvq.adb.ap-melbourne-1.oraclecloud.com))(connect_data=(service_name=g9b8049aad9c64c_demoadw_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=yes)))<br><br> -- Connect to ADB to test via sqlcl --<br>sql admin@&#39;(description= (retry_count=20)(retry_delay=3)(address=(protocol=tcps)(port=1521)(host=hr4mqmvq.adb.ap-melbourne-1.oraclecloud.com))(connect_data=(service_name=g9b8049aad9c64c_demoadw_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=yes)))&#39;<br><br>-- Create a sample table called PAYMENT --<br>  CREATE TABLE &quot;ADMIN&quot;.&quot;PAYMENT&quot;<br>   ( &quot;PAYMENT_ID&quot; NUMBER(19,0),<br> &quot;INVOICE_ID&quot; NUMBER(19,0) NOT NULL ENABLE,<br> &quot;PAYMENT_REF&quot; VARCHAR2(40 BYTE) NOT NULL ENABLE,<br> &quot;PAYMENT_DATE&quot; TIMESTAMP (3) NOT NULL ENABLE,<br> &quot;AMOUNT_NZD&quot; NUMBER(14,2) NOT NULL ENABLE,<br> &quot;PAYMENT_METHOD&quot; VARCHAR2(30 BYTE) NOT NULL ENABLE,<br> &quot;PAYMENT_STATUS&quot; VARCHAR2(30 BYTE) NOT NULL ENABLE<br>   ) ;<br>ALTER TABLE &quot;ADMIN&quot;.&quot;PAYMENT&quot; ADD PRIMARY KEY (&quot;PAYMENT_ID&quot;)<br>  USING INDEX  ENABLE;<br>ALTER TABLE &quot;ADMIN&quot;.&quot;PAYMENT&quot; ADD UNIQUE (&quot;PAYMENT_REF&quot;)<br>  USING INDEX  ENABLE;<br><br>INSERT INTO &quot;ADMIN&quot;.&quot;PAYMENT&quot;<br>(&quot;PAYMENT_ID&quot;, &quot;INVOICE_ID&quot;, &quot;PAYMENT_REF&quot;, &quot;PAYMENT_DATE&quot;, &quot;AMOUNT_NZD&quot;, &quot;PAYMENT_METHOD&quot;, &quot;PAYMENT_STATUS&quot;)<br>VALUES (900001, 501, &#39;PAY-NZ-900001&#39;, TIMESTAMP &#39;2026-10-01 09:15:00.123&#39;, 125.50, &#39;CARD&#39;, &#39;COMPLETED&#39;);<br><br>INSERT INTO &quot;ADMIN&quot;.&quot;PAYMENT&quot;<br>(&quot;PAYMENT_ID&quot;, &quot;INVOICE_ID&quot;, &quot;PAYMENT_REF&quot;, &quot;PAYMENT_DATE&quot;, &quot;AMOUNT_NZD&quot;, &quot;PAYMENT_METHOD&quot;, &quot;PAYMENT_STATUS&quot;)<br>VALUES (900002, 502, &#39;PAY-NZ-900002&#39;, TIMESTAMP &#39;2026-10-01 11:30:00.000&#39;, 89.99, &#39;BANK_TRANSFER&#39;, &#39;COMPLETED&#39;);<br><br>INSERT INTO &quot;ADMIN&quot;.&quot;PAYMENT&quot;<br>(&quot;PAYMENT_ID&quot;, &quot;INVOICE_ID&quot;, &quot;PAYMENT_REF&quot;, &quot;PAYMENT_DATE&quot;, &quot;AMOUNT_NZD&quot;, &quot;PAYMENT_METHOD&quot;, &quot;PAYMENT_STATUS&quot;)<br>VALUES (900003, 503, &#39;PAY-NZ-900003&#39;, TIMESTAMP &#39;2026-10-02 14:05:30.250&#39;, 240.00, &#39;CARD&#39;, &#39;PENDING&#39;);<br><br>INSERT INTO &quot;ADMIN&quot;.&quot;PAYMENT&quot;<br>(&quot;PAYMENT_ID&quot;, &quot;INVOICE_ID&quot;, &quot;PAYMENT_REF&quot;, &quot;PAYMENT_DATE&quot;, &quot;AMOUNT_NZD&quot;, &quot;PAYMENT_METHOD&quot;, &quot;PAYMENT_STATUS&quot;)<br>VALUES (900004, 504, &#39;PAY-NZ-900004&#39;, TIMESTAMP &#39;2026-10-02 16:45:10.500&#39;, 55.75, &#39;DIGITAL_WALLET&#39;, &#39;COMPLETED&#39;);<br><br>INSERT INTO &quot;ADMIN&quot;.&quot;PAYMENT&quot;<br>(&quot;PAYMENT_ID&quot;, &quot;INVOICE_ID&quot;, &quot;PAYMENT_REF&quot;, &quot;PAYMENT_DATE&quot;, &quot;AMOUNT_NZD&quot;, &quot;PAYMENT_METHOD&quot;, &quot;PAYMENT_STATUS&quot;)<br>VALUES (900005, 505, &#39;PAY-NZ-900005&#39;, TIMESTAMP &#39;2026-10-03 08:20:00.000&#39;, 310.25, &#39;BANK_TRANSFER&#39;, &#39;FAILED&#39;);</pre><h4>A. Create Self-Managed PostgreSQL 14 on Ubuntu VM FDW to Connect to ADB 26ai</h4><pre>psql -h 127.0.0.1 -U postgres -d postgres<br><br> -- Create Remote Server for Oracle ADB 26ai using the TNS entry (Can only be done for TLS with ADB in a subnet with VCN and DNS connectivity) - <br><br>CREATE SERVER adb<br> FOREIGN DATA WRAPPER oracle_fdw<br> OPTIONS (dbserver &#39;(description= (retry_count=20)(retry_delay=3)(address=(protocol=tcps)(port=1521)(host=hrxxx.adb.ap-melbourne-1.oraclecloud.com))(connect_data=(service_name=gxxxx_demoadw_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=yes)))&#39;);<br><br>-- Map Current User to ADMIN user in Autonomous 26ai -- <br><br>CREATE USER MAPPING FOR CURRENT_USER<br> SERVER adb<br> OPTIONS (user &#39;ADMIN&#39;, password &#39;AdminUserPWD1234##&#39;);<br> <br>-- ADMIN is Oracle user and it&#39;s password -- <br>-- Create a local Postgres schema to import Oracle schema into --<br><br>CREATE SCHEMA oracle_remote;<br> - Import Oracle Schema into Postgres Schema - <br>IMPORT FOREIGN SCHEMA &quot;ADMIN&quot;<br> FROM SERVER adb<br> INTO oracle_remote;<br><br> -- Query Remote Oracle Table --<br>SELECT * FROM oracle_remote.some_table LIMIT 10;<br><br> -- List all remote tables in Oracle -- <br>\det oracle_remote.*<br><br>OR<br><br>SELECT foreign_table_schema, foreign_table_name<br>FROM information_schema.foreign_tables<br>WHERE foreign_table_schema = &#39;oracle_remote&#39;<br>ORDER BY foreign_table_name;</pre><h4>B. Create a new bridge user in the same self-managed PostgreSQL 14 on Ubuntu to connect from OCI PostgreSQL</h4><pre>psql -h 127.0.0.1 -U postgres -d postgres <br><br>CREATE ROLE bridge_reader LOGIN;<br><br>-- In psql, set its password interactively:<br>\password bridge_reader<br><br>GRANT CONNECT ON DATABASE postgres TO bridge_reader;<br>GRANT USAGE ON SCHEMA oracle_remote TO bridge_reader;<br>GRANT SELECT ON ALL TABLES IN SCHEMA oracle_remote TO bridge_reader;<br>GRANT USAGE ON FOREIGN SERVER adb TO bridge_reader;<br><br>CREATE USER MAPPING FOR bridge_reader<br>  SERVER adb<br>  OPTIONS (user &#39;ADMIN&#39;, password &#39;YourPWD1234##&#39;);<br>  <br>-- ADMIN is Oracle user and it&#39;s password --<br><br>Note : A dedicated read-only Oracle user is preferable to ADMIN. Test the full second hop while logged in to Ubuntu PostgreSQL as bridge_reader:<br><br>SELECT * FROM oracle_remote.payment LIMIT 1;</pre><h4>C. Allow OCI PostgreSQL to reach the PostgreSQL 14 on Ubuntu VM</h4><p>Use the VM’s FQDN. In OCI networking, allow outbound TCP 5432 from the managed PostgreSQL system and inbound TCP 5432 to the VM, limited to the relevant subnet or security group.</p><p>On Ubuntu, make PostgreSQL listen on its private interface and add a pg_hba.conf entry for bridge_reader from the managed PostgreSQL subnet. For a TLS-enabled VM PostgreSQL server, that entry would be:</p><pre>hostssl  postgres  bridge_reader  10.180.2/24  scram-sha-256</pre><p>Restart PostgreSQL after changing listen_addresses or pg_hba.conf. Test that the VM accepts a connection from within the OCI PostgreSQL network before creating the foreign server.</p><pre>10.180.2.168 --&gt; IP for OCI PostgreSQL DBSystem</pre><pre>psql -h 10.180.2.168 -U postgres -D postgres<br><br>CREATE EXTENSION postgres_fdw;<br><br>-- Create Postgres Remote FDW from OCI Postgres to Self-Managed Postgres --<br>CREATE SERVER ubuntu_bridge<br>  FOREIGN DATA WRAPPER postgres_fdw<br>  OPTIONS (<br>    host &#39;postgres-14.sub06091142401.demovcn.oraclevcn.com&#39;,<br>    port &#39;5432&#39;,<br>    dbname &#39;postgres&#39;,<br>    sslmode &#39;prefer&#39;,<br>    updatable &#39;true&#39;<br>  );<br>  <br>postgres-14.sub06091142401.demovcn.oraclevcn.com -&gt; FQDN of Ubuntu VM where Postgres 14 is installed</pre><pre><br>-- Create User Mapping from OCI PostgreSQL to Self-managed Postgres User--<br>CREATE USER MAPPING FOR CURRENT_USER<br>  SERVER ubuntu_bridge<br>  OPTIONS (user &#39;bridge_reader&#39;, password &#39;BridgeReaderPWD1234##&#39;);<br><br>## bridge_reader is the user on Ubuntu VM PostgreSQL ##</pre><pre>-- Create OCI PostgreSQL Schema --<br>CREATE SCHEMA adb_via_vm;<br><br>-- Import the Remote Self-Managed Postgres Schema into OCI PostgreSQL schema created above --<br>IMPORT FOREIGN SCHEMA oracle_remote<br>  FROM SERVER ubuntu_bridge<br>  INTO adb_via_vm;<br>  <br>-- Incase you get this error , make sure you use FQDN and not IP address --<br>ERROR:  could not connect to server &quot;ubuntu_bridge&quot;<br>DETAIL:  connection to server at &quot;10.180.2.27&quot;, port 5432 failed (for outgoing connections - you must specify FQDN, not ip number): Connection timed out<br> Is the server running on that host and accepting TCP/IP connections?<br> drop server ubuntu_bridge cascade;<br><br>SELECT * FROM adb_via_vm.payment LIMIT 10;</pre><p>The query flow for the about output is like below :</p><pre>Application queries OCI PostgreSQL<br>  → postgres_fdw connects to PostgreSQL 14 on the Ubuntu VM<br>  → the VM queries its oracle_fdw foreign table<br>  → oracle_fdw connects to Autonomous Database 26ai<br>  → results return through the VM to OCI PostgreSQL and the application</pre><blockquote>Note : The two FDWs can add latency, and a stopped VM will make the OCI ADB26ai foreign table unavailable.</blockquote><h3>Refresh Schemas in case schema new tables are added to ADB 26ai</h3><p>When a new ADB table is added</p><pre>eg :<br>CREATE TABLE orders (<br>  order_id NUMBER(38) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,<br>  customer_id NUMBER(38),<br>  product_id NUMBER(38),<br>  product_description VARCHAR2(500),<br>  order_delivery_address VARCHAR2(500),<br>  order_date_taken DATE,<br>  order_misc_notes VARCHAR2(500)<br>);<br><br>Import it at both hops, in order:<br><br>-- On Ubuntu PostgreSQL --<br>IMPORT FOREIGN SCHEMA &quot;ADMIN&quot;<br>  LIMIT TO (orders)<br>  FROM SERVER adb INTO oracle_remote;<br><br>GRANT SELECT ON oracle_remote.orders TO bridge_reader;<br><br>-- On OCI PostgreSQL --<br>IMPORT FOREIGN SCHEMA oracle_remote<br>  LIMIT TO (orders)<br>  FROM SERVER ubuntu_bridge INTO adb_via_vm;<br>  <br> \det adb_via_vm.*<br> <br>SELECT * FROM adb_via_vm.orders LIMIT 10;</pre><h3>Schema change like addition, deletion of columns in ADB 26ai. Drop and re-import the table</h3><p>Eg : table productsin ADB 26ai changed, so we will drop and re-create the tabels in Ubuntu VM PostgreSQL and OCI PostgreSQL</p><blockquote>Important Note : These are separate database transactions, so do this during a quiet period if applications query the table.</blockquote><pre>-- On the Ubuntu PostgreSQL database:<br>BEGIN;<br><br>DROP FOREIGN TABLE oracle_remote.product;<br><br>IMPORT FOREIGN SCHEMA &quot;ADMIN&quot;<br>  LIMIT TO (product)<br>  FROM SERVER adb<br>  INTO oracle_remote;<br><br>GRANT SELECT ON oracle_remote.product TO bridge_reader;<br><br>COMMIT;<br><br>SELECT * FROM oracle_remote.product LIMIT 1;<br><br>-- On OCI PostgreSQL:<br>BEGIN;<br><br>DROP FOREIGN TABLE adb_via_vm.product;<br><br>IMPORT FOREIGN SCHEMA oracle_remote<br>  LIMIT TO (product)<br>  FROM SERVER ubuntu_bridge<br>  INTO adb_via_vm;<br><br>-- Reapply any grants your application needs:<br>GRANT SELECT ON adb_via_vm.product TO your_app_role;<br><br>COMMIT;<br><br>SELECT * FROM adb_via_vm.product LIMIT 1;</pre><blockquote><strong>Note : </strong>Check the actual table spelling with \det oracle_remote.*; if it appears as “PRODUCT”, use that quoted name instead. Dropping may fail if views or other objects depend on the foreign table. Do not add CASCADE blindly — identify those dependencies and plan to recreate them.</blockquote><h3>Join local OCI PostgreSQL table order and Oracle tables order table</h3><pre>SELECT<br>    pg.order_id,<br>    pg.customer_id,<br>    pg.product_description AS pg_description,<br>    ora.product_description AS oracle_description,<br>    pg.order_date_taken AS pg_date,<br>    ora.order_date_taken AS oracle_date<br>FROM public.orders AS pg<br>JOIN adb_via_vm.orders AS ora<br>  ON pg.order_id::numeric = ora.order_id<br>ORDER BY pg.order_id<br>LIMIT 20;<br><br>-- Complex Join <br>-- Explain the SQL Query Path --<br>EXPLAIN (VERBOSE)<br>WITH pg_counts AS (<br>    SELECT customer_id, product_id, count(*) AS pg_orders<br>    FROM public.orders<br>    GROUP BY customer_id, product_id<br>),<br>oracle_counts AS (<br>    SELECT customer_id, product_id, count(*) AS oracle_orders<br>    FROM adb_via_vm.orders<br>    GROUP BY customer_id, product_id<br>)<br>SELECT<br>    coalesce(pg.customer_id::numeric, ora.customer_id) AS customer_id,<br>    coalesce(pg.product_id::numeric, ora.product_id) AS product_id,<br>    coalesce(pg.pg_orders, 0) AS pg_orders,<br>    coalesce(ora.oracle_orders, 0) AS oracle_orders<br>FROM pg_counts AS pg<br>FULL JOIN oracle_counts AS ora<br>  ON pg.customer_id::numeric = ora.customer_id<br> AND pg.product_id::numeric = ora.product_id<br>ORDER BY customer_id, product_id;<br><br>-- Run the Complex SQL --<br>WITH pg_counts AS (<br>    SELECT customer_id, product_id, count(*) AS pg_orders<br>    FROM public.orders<br>    GROUP BY customer_id, product_id<br>),<br>oracle_counts AS (<br>    SELECT customer_id, product_id, count(*) AS oracle_orders<br>    FROM adb_via_vm.orders<br>    GROUP BY customer_id, product_id<br>)<br>SELECT<br>    coalesce(pg.customer_id::numeric, ora.customer_id) AS customer_id,<br>    coalesce(pg.product_id::numeric, ora.product_id) AS product_id,<br>    coalesce(pg.pg_orders, 0) AS pg_orders,<br>    coalesce(ora.oracle_orders, 0) AS oracle_orders<br>FROM pg_counts AS pg<br>FULL JOIN oracle_counts AS ora<br>  ON pg.customer_id::numeric = ora.customer_id<br> AND pg.product_id::numeric = ora.product_id<br>ORDER BY customer_id, product_id;</pre><blockquote>Ref :<br>PostgreSQL Orders table : <a href="https://github.com/shadabshaukat/STAND/blob/main/postgres.sql">https://github.com/shadabshaukat/STAND/blob/main/postgres.sql</a><br>Oracle Orders table : <a href="https://github.com/shadabshaukat/STAND/blob/main/oracle.sql">https://github.com/shadabshaukat/STAND/blob/main/oracle.sql</a></blockquote><h3>Call External Table with Parquet in ADB from OCI PostgreSQL</h3><h4>1. Generate a parquet file which has structure similar to the Oracle orders table</h4><pre>&quot;&quot;&quot;Generate a 1,000-row Parquet orders fixture.<br>Requires pyarrow. Run: python3 generate_orders_parquet.py<br>&quot;&quot;&quot;<br><br>from datetime import date, timedelta<br>from pathlib import Path<br><br>import pyarrow as pa<br>import pyarrow.parquet as pq<br><br><br>OUTPUT = Path(__file__).with_name(&quot;orders_1000.parquet&quot;)<br>ROW_COUNT = 1_000<br>START_DATE = date(2026, 1, 1)<br><br><br>def main() -&gt; None:<br>    order_ids = list(range(1, ROW_COUNT + 1))<br>    customer_ids = [((order_id - 1) % 120) + 1 for order_id in order_ids]<br>    product_ids = [((order_id * 7 - 1) % 80) + 1 for order_id in order_ids]<br><br>    schema = pa.schema(<br>        [<br>            pa.field(&quot;ORDER_ID&quot;, pa.int64(), nullable=False),<br>            pa.field(&quot;CUSTOMER_ID&quot;, pa.int32(), nullable=False),<br>            pa.field(&quot;PRODUCT_ID&quot;, pa.int32(), nullable=False),<br>            pa.field(&quot;PRODUCT_DESCRIPTION&quot;, pa.string(), nullable=False),<br>            pa.field(&quot;ORDER_DELIVERY_ADDRESS&quot;, pa.string(), nullable=False),<br>            pa.field(&quot;ORDER_DATE_TAKEN&quot;, pa.date32(), nullable=False),<br>            pa.field(&quot;ORDER_MISC_NOTES&quot;, pa.string(), nullable=True),<br>        ]<br>    )<br><br>    columns = [<br>        order_ids,<br>        customer_ids,<br>        product_ids,<br>        [f&quot;Demo product {product_id:03d}&quot; for product_id in product_ids],<br>        [<br>            f&quot;Unit {((order_id - 1) % 25) + 1}, &quot;<br>            f&quot;{((order_id - 1) % 200) + 1} Example Street, Melbourne VIC 3000&quot;<br>            for order_id in order_ids<br>        ],<br>        [START_DATE + timedelta(days=(order_id - 1) % 273) for order_id in order_ids],<br>        [<br>            None if order_id % 10 == 0 else f&quot;Synthetic Parquet order {order_id:04d}&quot;<br>            for order_id in order_ids<br>        ],<br>    ]<br><br>    table = pa.Table.from_arrays(<br>        [pa.array(values, type=field.type) for values, field in zip(columns, schema)],<br>        schema=schema,<br>    )<br>    pq.write_table(table, OUTPUT, compression=&quot;snappy&quot;, row_group_size=250)<br><br>    read_back = pq.read_table(OUTPUT)<br>    assert read_back.schema.equals(schema)<br>    assert read_back.num_rows == ROW_COUNT<br>    assert read_back.column(&quot;ORDER_ID&quot;).to_pylist() == order_ids<br>    print(f&quot;Created {OUTPUT} with {read_back.num_rows} rows&quot;)<br><br><br>if __name__ == &quot;__main__&quot;:<br>    main()</pre><h4>2. Upload the Parquet file to your OCI Object Storage bucket</h4><p>Fill in your OCI username, auth token, namespace, and bucket in the ADB external table SQL below, and run in ADB 26ai</p><pre>BEGIN<br>  DBMS_CLOUD.CREATE_CREDENTIAL(<br>    credential_name =&gt; &#39;ORDERS_PARQUET_CRED&#39;,<br>    username        =&gt; &#39;OracleIdentityCloudService/yourname@email.com&#39;,<br>    password        =&gt; &#39;*******&#39;<br>  );<br>END;<br>/<br><br>BEGIN<br>  DBMS_CLOUD.CREATE_EXTERNAL_TABLE(<br>    table_name      =&gt; &#39;ORDERS_PARQUET_EXT&#39;,<br>    credential_name =&gt; &#39;ORDERS_PARQUET_CRED&#39;,<br>    file_uri_list   =&gt; &#39;https://objectstorage.ap-melbourne-1.oraclecloud.com/n/&lt;namespace&gt;/b/&lt;bucket&gt;/o/orders_1000.parquet&#39;,<br>    column_list     =&gt; &#39;ORDER_ID NUMBER(38), CUSTOMER_ID NUMBER(38), PRODUCT_ID NUMBER(38), PRODUCT_DESCRIPTION VARCHAR2(500), ORDER_DELIVERY_ADDRESS VARCHAR2(500), ORDER_DATE_TAKEN DATE, ORDER_MISC_NOTES VARCHAR2(500)&#39;,<br>    format          =&gt; &#39;{&quot;type&quot;:&quot;parquet&quot;}&#39;<br>  );<br>END;<br>/<br><br><br>-- Expected: one row, 1000 / 1 / 1000.<br>SELECT COUNT(*) AS ROW_COUNT, MIN(ORDER_ID) AS FIRST_ID, MAX(ORDER_ID) AS LAST_ID<br>FROM ORDERS_PARQUET_EXT;<br><br>SELECT ORDER_ID, CUSTOMER_ID, PRODUCT_ID, ORDER_DATE_TAKEN<br>FROM ORDERS_PARQUET_EXT<br>ORDER BY ORDER_ID<br>FETCH FIRST 5 ROWS ONLY;<br><br>-- If the Ubuntu Oracle mapping uses another Oracle user, grant it SELECT here.<br>-- GRANT SELECT ON ORDERS_PARQUET_EXT TO &lt;ORACLE_READ_USER&gt;;</pre><h4>3. Run the Ubuntu SQL to import it as oracle_remote.orders_parquet_ext</h4><pre>-- IMPORT FOREIGN SCHEMA creates the PostgreSQL foreign table.<br>-- The default oracle_fdw case folding makes ORDERS_PARQUET_EXT lowercase.<br>IMPORT FOREIGN SCHEMA &quot;ADMIN&quot;<br>  LIMIT TO (orders_parquet_ext)<br>  FROM SERVER adb<br>  INTO oracle_remote<br>  OPTIONS (readonly &#39;true&#39;);<br><br>GRANT USAGE ON SCHEMA oracle_remote TO bridge_reader;<br>GRANT SELECT ON oracle_remote.orders_parquet_ext TO bridge_reader;<br><br>-- Expected: 1000 / 1 / 1000.<br>SELECT COUNT(*) AS row_count, MIN(order_id) AS first_id, MAX(order_id) AS last_id<br>FROM oracle_remote.orders_parquet_ext;<br><br>SELECT order_id, customer_id, product_id, order_date_taken<br>FROM oracle_remote.orders_parquet_ext<br>ORDER BY order_id<br>LIMIT 5;</pre><h4>4. Run the OCI PostgreSQL to import it as adb_via_vm.orders_parquet_ext</h4><pre>-- This imports the Ubuntu foreign table as another foreign table in OCI PG.<br>IMPORT FOREIGN SCHEMA oracle_remote<br>  LIMIT TO (orders_parquet_ext)<br>  FROM SERVER ubuntu_bridge<br>  INTO adb_via_vm;<br><br>-- Expected: 1000 / 1 / 1000.<br>SELECT COUNT(*) AS row_count, MIN(order_id) AS first_id, MAX(order_id) AS last_id<br>FROM adb_via_vm.orders_parquet_ext;<br><br>SELECT order_id, customer_id, product_id, product_description, order_date_taken<br>FROM adb_via_vm.orders_parquet_ext<br>ORDER BY order_id<br>LIMIT 10;<br><br>-- Example join to your local OCI PostgreSQL orders table. This is meaningful<br>-- only when both sources use the same customer_id and product_id values.<br>SELECT<br>  local_order.order_id AS local_order_id,<br>  parquet_order.order_id AS parquet_order_id,<br>  local_order.customer_id,<br>  local_order.product_id,<br>  parquet_order.product_description AS parquet_description<br>FROM public.orders AS local_order<br>JOIN adb_via_vm.orders_parquet_ext AS parquet_order<br>  ON local_order.customer_id::numeric = parquet_order.customer_id<br> AND local_order.product_id::numeric = parquet_order.product_id<br>ORDER BY local_order.order_id, parquet_order.order_id<br>LIMIT 20;<br><br>-- Inspect the first hop&#39;s SQL without executing the query:<br>EXPLAIN (VERBOSE)<br>SELECT order_id, customer_id, product_id<br>FROM adb_via_vm.orders_parquet_ext<br>WHERE order_id BETWEEN 1 AND 10;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TCWu-sJKKTtprCbLkJdlcw.png" /><figcaption>External Parquet File Read Workflow from OCI PostgreSQL</figcaption></figure><h3>Write Data from OCI PostgreSQL back to OCI Object Storage using the same flow</h3><p>In Oracle Autonomous Database :</p><pre>CREATE TABLE PG_ORDERS_STAGE (<br>    ORDER_ID               NUMBER(10,0) PRIMARY KEY,<br>    CUSTOMER_ID            NUMBER(10,0),<br>    PRODUCT_ID             NUMBER(10,0),<br>    PRODUCT_DESCRIPTION    VARCHAR2(500 CHAR),<br>    ORDER_DELIVERY_ADDRESS VARCHAR2(500 CHAR),<br>    ORDER_DATE_TAKEN       DATE,<br>    ORDER_MISC_NOTES       VARCHAR2(500 CHAR)<br>);</pre><pre>Import it at both hops, in order:<br><br>-- On Ubuntu PostgreSQL --<br>IMPORT FOREIGN SCHEMA &quot;ADMIN&quot;<br>  LIMIT TO (PG_ORDERS_STAGE)<br>  FROM SERVER adb<br>  INTO oracle_remote<br>  OPTIONS (readonly &#39;false&#39;);<br><br>GRANT SELECT ON oracle_remote.PG_ORDERS_STAGE TO bridge_reader;<br>GRANT INSERT ON oracle_remote.PG_ORDERS_STAGE TO bridge_reader;<br>GRANT UPDATE ON oracle_remote.PG_ORDERS_STAGE TO bridge_reader;<br><br>--Check INSERT privilige --<br>SELECT has_table_privilege(<br>  &#39;bridge_reader&#39;,<br>  &#39;oracle_remote.pg_orders_stage&#39;,<br>  &#39;INSERT&#39;<br>);<br><br>ALTER SERVER adb<br>  OPTIONS (ADD isolation_level &#39;read_committed&#39;);<br><br>-- On OCI PostgreSQL --<br>IMPORT FOREIGN SCHEMA oracle_remote<br>  LIMIT TO (PG_ORDERS_STAGE)<br>  FROM SERVER ubuntu_bridge INTO adb_via_vm;<br>  <br> \det adb_via_vm.*<br> <br>SELECT * FROM adb_via_vm.PG_ORDERS_STAGE LIMIT 10;</pre><blockquote><strong>Important Note : </strong><a href="https://github.com/laurenz/oracle_fdw/blob/master/README.oracle_fdw">oracle_fdw documentation</a> specifically describes read_committed as a workaround for ORA-08177 on inserts.</blockquote><blockquote>For a production setup, I’d use a separate Oracle FDW server for the staging load and keep the existing read path at its current isolation level. Because changing serveradb affects every foreign table using that server. READ COMMITTED can produce inconsistent results if a query scans the same Oracle table more than once while data changes.</blockquote><p><strong>In OCI PostgreSQL</strong>, copy the rows across the two hops :</p><blockquote>Table public.orders in OCI PostgreSQL can be created from here : <a href="https://github.com/shadabshaukat/STAND/blob/main/postgres.sql">https://github.com/shadabshaukat/STAND/blob/main/postgres.sql</a></blockquote><pre>psql -h 10.180.2.168 -U postgres -d postgres<br><br> INSERT INTO adb_via_vm.pg_orders_stage (<br>  order_id, customer_id, product_id,<br>  product_description, order_delivery_address,<br>  order_date_taken, order_misc_notes<br>)<br>SELECT<br>  order_id, customer_id, product_id,<br>  product_description, order_delivery_address,<br>  order_date_taken, order_misc_notes<br>FROM public.orders;<br><br>SELECT * FROM adb_via_vm.PG_ORDERS_STAGE LIMIT 10;</pre><p>In ADB check the row count and export the table to OCI Object storage :</p><pre>SQL&gt; select count(*) from pg_orders_stage;<br><br>   COUNT(*)<br>___________<br>      10000<br><br><br>BEGIN<br>  DBMS_CLOUD.EXPORT_DATA(<br>    credential_name =&gt; &#39;ORDERS_PARQUET_CRED&#39;,<br>    file_uri_list   =&gt; &#39;https://objectstorage.ap-melbourne-1.oraclecloud.com/n/&lt;namespace&gt;/b/&lt;bucket&gt;/o/pg_orders_export&#39;,<br>    query           =&gt; &#39;SELECT * FROM ADMIN.PG_ORDERS_STAGE&#39;,<br>    format          =&gt; JSON_OBJECT(&#39;type&#39; VALUE &#39;parquet&#39;,<br>                                   &#39;compression&#39; VALUE &#39;snappy&#39;)<br>  );<br>END;<br>/</pre><p>You can see the exported Parquet files for the table in OCI Object storage bucket</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*deT6aQB2ItOb8ZgWVBv-QA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7M92vNZPj_7Dyb6TAY_g3Q.png" /><figcaption>External Parquet File Write Workflow from OCI PostgreSQL</figcaption></figure><h3><strong>Where could this pattern help?</strong></h3><ol><li><strong>Order reconciliation:</strong> Compare recent data in OCI PostgreSQL with historical or partner data exposed through ADB.</li><li><strong>Archive exploration:</strong> Keep older Parquet data in Object Storage while allowing occasional SQL access from an existing PostgreSQL application or reporting tool.</li><li><strong>Application enrichment:</strong> Combine a small set of external Oracle attributes with local PostgreSQL data for a specific workflow.</li><li><strong>Phased modernization:</strong> Let teams test Oracle and lake data from a PostgreSQL-facing application before deciding what should be replicated, cached, or migrated permanently.</li></ol><h3>Conclusion</h3><p>This example shows that OCI PostgreSQL can read Parquet data in Object Storage through two foreign data wrappers and an ADB external table. Each component has a clear role: ADB reads the file, the Ubuntu PostgreSQL server bridges Oracle and PostgreSQL, and OCI PostgreSQL remains the application’s query endpoint. The design is most useful for selective queries, exploration, and transition work.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a371a00a6d50" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating On-Premises PostgreSQL to OCI PostgreSQL with Near-Zero Downtime Using OCI GoldenGate]]></title>
            <link>https://shadabshaukat.medium.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/60801fbaf657</guid>
            <category><![CDATA[migration]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[oracle-goldengate]]></category>
            <category><![CDATA[zero-downtime]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Tue, 29 Sep 2026 13:28:24 GMT</pubDate>
            <atom:updated>2026-10-02T13:17:20.935Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Migrating a large PostgreSQL database becomes significantly more challenging when production transactions cannot stop. A 10 TB database generating 100 GB of WAL per hour demands careful planning for data transfer, change capture, replication lag, and cutover. In this article, we walk through a tested lab implementation using OCI GoldenGate and explore how we would plan and operate the same migration approach at production scale.</em></p><h3>Introduction</h3><p>Recently we were planning to move a busy PostgreSQL 14 cluster from on-premises to OCI Database with PostgreSQL for a customer with a very large and busy PostgreSQL cluster.</p><p>The database was 10 TB and produces approximately 100+ GB of write-ahead log (WAL) every hour. A dump and restore alone would leave an unacceptable outage: even an ideal 1 Gbps link takes more than 22 hours to transmit 10 TB, before compression, indexes, restore work and network overhead. During a 48-hour migration, the source might generate another 4.8 TB of WAL</p><p>We need the application to continue writing while we copy the database. OCI GoldenGate gives us an enterprise replication service with managed deployments, connection management, Extract and Replicat processes, durable trails, checkpoints and operational monitoring. We combined these capabilities of Goldengate with a PostgreSQL snapshot-based initial load</p><p>pg_dump establishes the baseline, GoldenGate captures the subsequent transactions, and Replicat applies them after the restore. We reserve a short, controlled write pause for the final cutover. this pattern does not require HANDLECOLLISIONS which can lead to potential lost or duplicate transactions.</p><h3>Setup</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jEy_4UX7iCmk8cigGyp7bg.png" /><figcaption>OCI Goldengate PostgreSQL Data Flow Diagram</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z00fRFm2YLrDUSuD181sFg.png" /><figcaption>OGG Replication. Initial Load</figcaption></figure><blockquote><strong>Important Note : </strong>Everything documented here can be run on not just a self-managed Postgres on-premise, but also PostgreSQL running on a managed service like Digital Ocean, RDS, Aurora, Azure or GCP. As long as the source database has native Postgres logical replication available and a replication role user which can create a replication slot, this should work on any other source platform.</blockquote><p>Our source was PostgreSQL 14 on an Ubuntu VM at <em>10.180.2.27</em>, our target was managed OCI PostgreSQL <em>10.180.2.199</em>, and our OCI GoldenGate deployment was <em>postgres-ogg </em>at <em>10.180.2.176:443</em></p><pre><br>On-prem PostgreSQL 14                      OCI GoldenGate                       OCI PostgreSQL<br>source 10.180.2.27                         postgres-ogg                         target 10.180.2.199<br>         │                                        │                                      ▲<br>         ├── pgoutput slot ───────────────────&gt; PGEXT ── dv trail ── PGRP ─────────────┤<br>         │                                                                               │<br>         └── exported snapshot ── pg_dump ── migration host ── pg_restore ───────────────┘<br></pre><p><strong>The execution order is very critical </strong>: We create the publication, register the permanent `pgoutput` slot, export a temporary snapshot and its matching LSN, launch the dump against that snapshot, start Extract at that LSN, release the temporary slot after the dump imports the snapshot, restore, and finally start Replicat. We keep source writes flowing during the dump and restore.</p><h3>High-Level Migration Sequence</h3><p>We divide the migration into four phases: <strong>prepare, copy and capture, catch up, and cut over</strong>.</p><h3>Phase 1 — Prepare the Environment</h3><ul><li>📐 Step 1 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#c508">Plan Migration Capacity and Network Connectivity</a></li><li>☁️ Step 2 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#234c">Create the OCI GoldenGate Deployment and Database Connections</a></li><li>🛠️ Step 3 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#a59b">Build the Source Schema, Load Data, and Validate Replication Readiness</a></li><li>🔑 Step 4 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#03bf">Configure GoldenGate TRANDATA and Verify Table Keys</a></li><li>📋 Step 5 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#fe1a">Create the Publication with Explicit Table Names</a></li></ul><h3>Phase 2 — Copy the Baseline and Capture Changes</h3><ul><li>📸 Step 6 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#2958">Register the Permanent Slot and Export a Snapshot with Its Matching LSN</a></li><li>💾 Step 7 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#fa42">Run the Snapshot Backup While Source Writes Continue</a></li><li>▶️ Step 8 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#97f2">Start GoldenGate Extract at the Matching LSN</a></li><li>📥 Step 9 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#e559">Restore the Initial Backup to OCI PostgreSQL</a></li></ul><h3>Phase 3 — Apply Changes and Validate</h3><ul><li>🔄 Step 10 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#dc12">Start Replicat from the Beginning of the New Trail</a></li><li>📊 Step 11 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#8b68">Monitor WAL, Replication Slots, Apply Progress, and Data Consistency</a></li><li>🧪 Step 12 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#2123">Validate CDC with Batched Updates, Deletes, and Upserts</a></li></ul><p><strong>Throughout the online migration</strong>, starting with slot creation, we monitor retained WAL, disk space, slot activity, long transactions, vacuum pressure, trail growth, replication lag, and target performance.</p><h3>Phase 4 — Cut Over and Complete</h3><ul><li>⏸️ Step 13 — Pause Source Writes, Drain Replication, and Reconcile Data</li><li>🔢 Step 14 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#5348">Synchronize and Verify All 13 Sequences</a></li><li>🚀 Step 15 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#611b">Switch Application Connections and Enable Target Writes</a></li><li>🧹 Step 16 — <a href="https://blog.shadabmohammad.com/migrating-on-premises-postgresql-to-oci-postgresql-with-near-zero-downtime-using-oci-goldengate-60801fbaf657#94ca">Clean Up the GoldenGate and Database Lab Resources</a></li></ul><p>Stop writers, resolve in-flight transactions, drain Replicat, reconcile data, synchronize sequences, and switch applications. Sequence synchronization is essential</p><h3>Prerequisites and Steps for Setting Up the OCI Environment</h3><h3>1. Size the migration and its network paths</h3><p>For an on-premises data centre, we would use private connectivity such as redundant FastConnect circuits, plan a VPN contingency, test return routes and DNS, and allow TCP/5432 only from the required private endpoints.</p><p>The 100 GB/hour WAL rate is about 27.8 MB/s, or 222 Mbps, of source WAL generation, which is approximately 2.4 TB per day. It is not a measured GoldenGate trail rate. We would collect peak WAL rate, Extract output rate, trail bytes per hour, Replicat apply rate and restore throughput in a rehearsal. The network link must carry the copy plus ongoing CDC with headroom; our target must apply faster than the new transactions arrive.</p><p>We would also reserve storage for the base copy, restored data and indexes, source WAL, GoldenGate trails and a recovery interval. If Extract stopped for 48 hours, source WAL exposure at the stated rate would approach 4.8 TB before any overhead. If Extract keeps pace but Replicat falls behind, the backlog moves into GoldenGate trails instead. OCI Golgengate currently allocates 1 Gbps of deployment bandwidth, and 500 GB of storage per consumed GoldenGate OCPU.</p><h3>2. Create the GoldenGate deployment and both database connections</h3><p>For a fresh environment, we use the OCI Console for OCI setup. In *GoldenGate → Deployments → Create deployment*, we select a “Data replication” deployment with PostgreSQL technology, the appropriate compartment, a private subnet and enough OCPU/storage for the measured CDC backlog.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zoXHqI8NNI7umZifgSMOmg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LnRCGK2ey44MOK85ZlpZow.png" /></figure><p>For a fresh deployment, we create two connection resources in GoldenGate → Connections → Create connection.</p><p><strong>Create 2 connections :</strong></p><ol><li>Source OnPremise Database to which you have connectivity from OCI to OnPremise via private network with high bandwidth and low latency</li><li>Target OCI PostgreSQL Database</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gUHADTIV1yFeBZwMReFpQA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9eehuxraCRFyx7e72n6CFQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M4h2dlLX3LuXAczl-pyRAw.png" /></figure><p>Assign to the Postgres Goldengate Deployment</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n90ZnCoxcl24SkYdmfmFGQ.png" /></figure><pre>-- Source DB Connectivity Check --<br>psql -h 10.180.2.27 -U postgres -d postgres<br><br>-- TargetDB Connectivity Check --<br>psql -h 10.180.2.199 -U postgres -d postgres</pre><p>SSH forward maps local `localhost:8443` to the private GoldenGate private endpoint `10.180.2.176:443`</p><pre>ssh -fNT -v -L 8443:10.180.2.176:443 \<br>  opc@&lt;bastion-host-ip&gt; -i ~/.ssh/mydemo_vcn.priv &amp;</pre><p>We will create a Goldgentgate 26ai container to provide the local AdminClient, while the actual replication deployment remains in OCI.</p><pre>docker run -d \<br>  --name ogg26ai \<br>  --hostname ogg26ai \<br>  --restart unless-stopped \<br>  -p 443:443 \<br>  --env-file &quot;$HOME/goldengate/26ai/.env&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u02:/u02&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u03:/u03&quot; \<br>  -v &quot;$HOME/goldengate/26ai/cert:/etc/nginx/cert&quot; \<br>  --log-opt max-size=50m \<br>  --log-opt max-file=5 \<br>  container-registry.oracle.com/goldengate/goldengate-oracle-free:latest</pre><pre>docker exec -it ogg26ai bash<br>adminclient</pre><pre>CONNECT https://host.docker.internal:8443 DEPLOYMENT postgres-ogg AS oggadmin PASSWORD YourPassword1234#_!<br><br>OGG&gt; INFO CREDENTIALSTORE<br><br>Default domain: OracleGoldenGate<br><br>  Alias: postgres14ubuntuvm<br>  Userid: postgres@ip-10-180-2-27.ociggsvc.oraclevcn.com:5432/postgres;sslMode=disable<br><br>  Alias: pggoldengatedemo<br>  Userid: postgres@ip-10-180-2-199.ociggsvc.oraclevcn.com:5432/postgres;sslMode=prefer;validateServerCertificate=0<br><br>OGG&gt; DBLOGIN USERIDALIAS postgres14ubuntuvm DOMAIN OracleGoldenGate<br><br>OGG&gt; DBLOGIN USERIDALIAS pggoldengatedemo DOMAIN OracleGoldenGate</pre><h3>3. Build and validate the PostgreSQL Source DB</h3><p>To simulate the customers schema we will use a slightly different version of DVDRental schema in our source database</p><h4>SQL files which will be our Schema :</h4><ul><li><a href="https://gist.github.com/shadabshaukat/9500bc7458a16e1733f8681aad10775a.js"><strong>00_create_schema.sql</strong></a><strong> </strong>creates schemas, types, 15 tables, constraints, indexes, 13 sequences, and the dvd_lab.generate_data(integer) procedure. It never generates rows.</li><li><strong>0</strong><a href="https://gist.github.com/shadabshaukat/66e8c7ecc1222a6faff6d86d8510c982.js"><strong>1_load_source.sql</strong> </a>validates -v rows, sets UTC for the transaction, and calls the generator. We run this script only on the source.</li></ul><p>Refer to Appendix at the end of the article for both code files : <strong>00_create_schema.sql 01_load_source.sql</strong></p><p>The schema Gist defines dvdrental.year as a domain for film.release_year.GoldenGate’s PostgreSQL support list excludes domain types. We preserve the 1901–2155 rule with an integer column and a check constraint before loading data, publishing tables or starting CDC.</p><p>The schema also has a `text` column, an enum and a tsvector in `dvdrental.film`; Oracle Goldengate documents PostgreSQL array capture/delivery beginning with GoldenGate 26ai, and our OCI Goldengate identified build is 23.26.2.0.1.</p><p>Our 15-table comparison confirmed baseline and final data parity, while the live CDC test changed rental, payment and country rows. For production, we would also exercise representative CDC updates to the array, enum and `tsvector`</p><blockquote>To Check what <strong>Postgres data types are supported</strong> by Oracle Goldengate refer this document : <a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/prepare-postgresql-understanding-whats-supported.html">https://docs.oracle.com/en/database/goldengate/core/26/coredoc/prepare-postgresql-understanding-whats-supported.html</a></blockquote><p>The Gist leaves four foreign-key columns without a leading index. Oracle requires indexes on foreign-key columns for Parallel Replicat. We include the four indexes before the dump in this repeatable runbook, so the restore carries them. In the actual lab we added them to both databases after the restore when the final audit found the omission.</p><blockquote><strong>Oracle Parallel Replicat guidance :</strong> <a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/replicat-parallel-replicat.html">https://docs.oracle.com/en/database/goldengate/core/26/coredoc/replicat-parallel-replicat.html</a></blockquote><p>On a busy production source, we would schedule any missing index builds with the appropriate online PostgreSQL method and measure the extra I/O before migration day.</p><pre><br># On the bastion as opc, connected to the source over TCP<br><br>git clone https://github.com/shadabshaukat/ogg_dvd_lab/<br><br>cd ogg_dvd_lab/<br><br>psql -X -v ON_ERROR_STOP=1 -h 10.180.2.27 -U postgres -d postgres \<br>  -f /home/opc/ogg_dvd_lab/00_create_schema.sql<br><br>psql -X -v ON_ERROR_STOP=1 -h 10.180.2.27 -U postgres -d postgres &lt;&lt;&#39;SQL&#39;<br>BEGIN;<br>ALTER TABLE dvdrental.film<br>  ALTER COLUMN release_year TYPE integer USING release_year::integer;<br>ALTER TABLE dvdrental.film<br>  ADD CONSTRAINT film_release_year_range<br>  CHECK (release_year BETWEEN 1901 AND 2155);<br>DROP DOMAIN dvdrental.year;<br>COMMIT;<br>SQL<br><br>psql -X -v ON_ERROR_STOP=1 -h 10.180.2.27 -U postgres -d postgres &lt;&lt;&#39;SQL&#39;<br>CREATE INDEX IF NOT EXISTS film_original_language_idx<br>  ON dvdrental.film (original_language_id);<br>CREATE INDEX IF NOT EXISTS inventory_store_idx<br>  ON dvdrental.inventory (store_id);<br>CREATE INDEX IF NOT EXISTS staff_address_idx<br>  ON dvdrental.staff (address_id);<br>CREATE INDEX IF NOT EXISTS store_address_idx<br>  ON dvdrental.store (address_id);<br>SQL<br><br>-- Initial Load of Data --<br>psql -X -v ON_ERROR_STOP=1 -v rows=100000 \<br>  -h 10.180.2.27 -U postgres -d postgres \<br>  -f /home/opc/ogg_dvd_lab/01_load_source.sql</pre><p>We verify that both ends use database/user ‘postgres’, that all 15 replicated tables have primary keys, and that source logical decoding is ready:</p><pre>psql -h 10.180.2.27 -U postgres -d postgres<br><br>SELECT current_database(), current_user, version();<br>SHOW wal_level;<br>SHOW max_replication_slots;<br>SHOW max_wal_senders;<br>SHOW max_slot_wal_keep_size;<br>SHOW track_commit_timestamp;<br><br>-- Check Table with no PK --<br>SELECT<br>    n.nspname AS schema_name,<br>    c.relname AS table_name<br>FROM pg_catalog.pg_class c<br>JOIN pg_catalog.pg_namespace n<br>    ON n.oid = c.relnamespace<br>WHERE c.relkind IN (&#39;r&#39;, &#39;p&#39;)<br>  AND n.nspname NOT LIKE &#39;pg_%&#39;<br>  AND n.nspname &lt;&gt; &#39;information_schema&#39;<br>  AND NOT EXISTS (<br>      SELECT 1<br>      FROM pg_catalog.pg_constraint con<br>      WHERE con.conrelid = c.oid<br>        AND con.contype = &#39;p&#39;<br>  )<br>ORDER BY schema_name, table_name;<br><br>-- Check Replica identity --<br>SELECT n.nspname, c.relname, c.relreplident,<br>       pg_get_userbyid(c.relowner) AS owner<br>FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace<br>WHERE c.relkind = &#39;r&#39; AND n.nspname NOT LIKE &#39;pg_%&#39;<br>  AND n.nspname &lt;&gt; &#39;information_schema&#39;<br>ORDER BY c.relname;</pre><p>We require <strong>wal_level=logical</strong> , two available logical slots during the snapshot handoff plus recovery headroom, enough WAL senders and direct PostgreSQL connections rather than a transaction-pooling proxy.</p><p>In production, we would have to inventory the DDL, extensions, large objects, unsupported types, triggers and scheduled jobs before the migration freeze.</p><p><a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/prepare-postgresql-understanding-whats-supported.html">Goldengate PostgreSQL: Supported Data Types, Objects, and Operations</a></p><h3>4. Goldengate Source Database Setup</h3><p>Add Trandata on Source</p><pre>DBLOGIN USERIDALIAS postgres14ubuntuvm DOMAIN OracleGoldenGate<br><br>ADD TRANDATA dvdrental.actor<br>ADD TRANDATA dvdrental.address<br>ADD TRANDATA dvdrental.category<br>ADD TRANDATA dvdrental.city<br>ADD TRANDATA dvdrental.country<br>ADD TRANDATA dvdrental.customer<br>ADD TRANDATA dvdrental.film<br>ADD TRANDATA dvdrental.film_actor<br>ADD TRANDATA dvdrental.film_category<br>ADD TRANDATA dvdrental.inventory<br>ADD TRANDATA dvdrental.language<br>ADD TRANDATA dvdrental.payment<br>ADD TRANDATA dvdrental.rental<br>ADD TRANDATA dvdrental.staff<br>ADD TRANDATA dvdrental.store<br><br>INFO TRANDATA dvdrental.actor<br>INFO TRANDATA dvdrental.address<br>INFO TRANDATA dvdrental.category<br>INFO TRANDATA dvdrental.city<br>INFO TRANDATA dvdrental.country<br>INFO TRANDATA dvdrental.customer<br>INFO TRANDATA dvdrental.film<br>INFO TRANDATA dvdrental.film_actor<br>INFO TRANDATA dvdrental.film_category<br>INFO TRANDATA dvdrental.inventory<br>INFO TRANDATA dvdrental.language<br>INFO TRANDATA dvdrental.payment<br>INFO TRANDATA dvdrental.rental<br>INFO TRANDATA dvdrental.staff<br>INFO TRANDATA dvdrental.store</pre><p>Before publishing a table, ensure UPDATE and DELETE operations have an unambiguous key.</p><p><strong>Preferred order:</strong><br>1. Primary key.<br>2. Non-null unique index used as replica identity.<br>3. GoldenGate KEYCOLS backed by a genuinely unique target index.<br>4. REPLICA IDENTITY FULL only as a temporary last resort.</p><pre>eg :<br>ALTER TABLE public.example<br>  REPLICA IDENTITY USING INDEX example_replica_key;<br>  <br>If not then :<br><br>ALTER TABLE public.example<br>  REPLICA IDENTITY FULL;</pre><p>For your no-primary-key tables, do not use FULL where duplicate rows can exist and UPDATE/DELETE occurs. The same key or lookup index must exist on OCI before Replicat starts.</p><h3>5. Publish the tables we will use for the dump and replicate</h3><p>We will create the publication on the source before registering the pgoutput Extract. This order is mandatory for GoldenGate pgoutput.</p><p>The publication contains 15 tables and enables insert, update, delete and truncate operations.</p><pre>psql -h 10.180.2.27 -U postgres -d postgres<br><br>CREATE PUBLICATION ogg_migration_pub FOR TABLE<br>  dvdrental.actor,<br>  dvdrental.address,<br>  dvdrental.category,<br>  dvdrental.city,<br>  dvdrental.country,<br>  dvdrental.customer,<br>  dvdrental.film,<br>  dvdrental.film_actor,<br>  dvdrental.film_category,<br>  dvdrental.inventory,<br>  dvdrental.language,<br>  dvdrental.payment,<br>  dvdrental.rental,<br>  dvdrental.staff,<br>  dvdrental.store<br>WITH (publish = &#39;insert, update, delete, truncate&#39;);<br><br>SELECT schemaname, tablename<br>FROM pg_publication_tables<br>WHERE pubname = &#39;ogg_migration_pub&#39;<br>ORDER BY schemaname, tablename;<br><br>SELECT pubname, pubinsert, pubupdate, pubdelete, pubtruncate<br>FROM pg_publication<br>WHERE pubname = &#39;ogg_migration_pub&#39;;</pre><h3>6. Register the permanent slot, then export the snapshot and its LSN</h3><p>We return to the AdminClient, log into the source alias and register PGEXT explicitly with pgoutput.</p><p>Registration creates the permanent logical replication slot; it does not add or start the Extract process. We will inspect the slot on the source before the temporary snapshot begins:</p><pre>DBLOGIN USERIDALIAS postgres14ubuntuvm DOMAIN OracleGoldenGate<br><br>REGISTER EXTRACT PGEXT PGPLUGINTYPE pgoutput</pre><pre>SELECT slot_name, plugin, slot_type, temporary, active,<br>       restart_lsn, confirmed_flush_lsn<br>FROM pg_replication_slots<br>WHERE database = current_database()<br>ORDER BY slot_name;<br><br>       slot_name        |  plugin  | slot_type | temporary | active | restart_lsn | confirmed_flush_lsn<br>------------------------+----------+-----------+-----------+--------+-------------+---------------------<br> pgext_e8f172784cad044e | pgoutput | logical   | f         | f      | 0/8C4BD8B0  | 0/8C4BD8E8</pre><p><strong>Expected:</strong> one new permanent logical slot using pgoutput, initially inactive. In our lab, GoldenGate reported the slot name pgext_e8f172784cad044e</p><p>We open a <strong>separate replication-protocol connection from the bastion</strong> to the source in a persistent terminal session. It stays open and idle after it returns a temporary slot, snapshot_name and consistent_point; closing that session invalidates the exported snapshot name:</p><pre>tmux new -s pgdump<br><br>psql -X &#39;host=10.180.2.27 port=5432 dbname=postgres user=postgres replication=database&#39;<br><br>CREATE_REPLICATION_SLOT gg_snap_tmp TEMPORARY LOGICAL pgoutput EXPORT_SNAPSHOT;<br><br>  slot_name  | consistent_point |    snapshot_name    | output_plugin<br>-------------+------------------+---------------------+---------------<br> gg_snap_tmp | 0/8C4BDA08       | 00000003-0000B7E7-1 | pgoutput</pre><blockquote>Important Note : We record the two returned values <strong>as a pair</strong>. We give the snapshot name to pg_dump --snapshot; we position Extract at the matching consistent_point. We do not run another command in this exporter connection. PostgreSQL removes the temporary slot when we close the connection.</blockquote><p>snapshot_name will be used in the pg_dump</p><p>consistent_point will be used to steer the Goldengate extract to start at that LSN</p><h3>7. pgdump of Snapshot while writes continue on source</h3><p>The below command is to simulate writes happening on the source database while we run the pg_dump and pg_restore</p><p>Paste this into the <strong>bastion prompt</strong>. It asks for the password once:</p><pre>(<br>set -Eeuo pipefail<br>read -rsp &quot;PostgreSQL source password: &quot; PGPASSWORD<br>echo<br>export PGPASSWORD<br><br>control=/home/opc/ogg_dvd_lab/ogg_dvd_writer.run<br>touch &quot;$control&quot;<br>batch=0<br><br>while test -e &quot;$control&quot; &amp;&amp; test &quot;$batch&quot; -lt 600; do<br>  psql -X -w -v ON_ERROR_STOP=1 -v rows=100 \<br>    -h 10.180.2.27 -U postgres -d postgres \<br>    -f /home/opc/ogg_dvd_lab/01_load_source.sql \<br>    &gt;&gt; /home/opc/ogg_dvd_lab/ogg_dvd_writer.log 2&gt;&amp;1<br>  batch=$((batch + 1))<br>  sleep 2<br>done<br>)</pre><p>If you open another session and check the slots size, you will see it incrementing due to the writes happening on the database :</p><pre>psql -h 10.180.2.27 -U postgres -d postgres<br><br>SELECT slot_name,<br>       plugin,<br>       temporary,<br>       active,<br>       restart_lsn,<br>       confirmed_flush_lsn,<br>       pg_size_pretty(<br>         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)<br>       ) AS retained_wal,<br>       wal_status,<br>       pg_size_pretty(safe_wal_size) AS safe_wal_remaining<br>FROM pg_replication_slots<br>WHERE database = current_database();<br><br> slot_name              |  plugin  | temporary | active | restart_lsn | confirmed_flush_lsn | retained_wal | wal_status | <br>------------------------+----------+-----------+--------+-----------------+---------------------+--------------+---------<br> pgext_e8f172784cad044e | pgoutput | f         | f      | 0/8C4BD8B0  | 0/8C4BD8E8          | 18 MB        | reserved   |<br> gg_snap_tmp            | pgoutput | t         | t      | 0/8C4BD9D0  | 0/8C4BDA08          | 18 MB        | reserved   |</pre><p>Take a snapshot with id00000003–0000B7E7–1 as in<strong> Step 6</strong></p><pre>PGAPPNAME=ogg_lab_dump pg_dump \<br>  --host=10.180.2.27 --port=5432 --username=postgres --dbname=postgres \<br>  --format=directory --jobs=4 --compress=6 --lock-wait-timeout=5s \<br>  --verbose \<br>  --snapshot=&#39;00000003-0000B7E7-1&#39; \<br>  --schema=dvdrental --schema=dvd_lab \<br>  --quote-all-identifiers --no-owner --no-acl \<br>  --no-publications --no-subscriptions \<br>  --file=/home/opc/ogg_dvd_lab/dump &gt; /home/opc/ogg_dvd_lab/dump.log 2&gt;&amp;1</pre><p>Keep the snapshot-exporting session open until the dump finishes. If the dump directory already exists from an earlier attempt, choose a new output directory.</p><blockquote>We confirm that <em>pg_dump</em> exits successfully, review its log, and inspect the archive before closing the snapshot-exporting session.</blockquote><p>To monitor from a separate session run :</p><pre>-- Check Active Sessions for the Dump --<br>SELECT pid,<br>       application_name,<br>       state,<br>       xact_start,<br>       backend_xmin,<br>       wait_event_type,<br>       wait_event,<br>       left(query, 120) AS query<br>FROM pg_stat_activity<br>WHERE application_name = &#39;ogg_lab_dump&#39;<br>ORDER BY pid;<br><br>-- No rows for above query means pg_dump backup has completed --<br><br>-- Check for Retained WAL continously -- <br>SELECT slot_name,<br>       plugin,<br>       temporary,<br>       active,<br>       restart_lsn,<br>       confirmed_flush_lsn,<br>       pg_size_pretty(<br>         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)<br>       ) AS retained_wal,<br>       wal_status,<br>       pg_size_pretty(safe_wal_size) AS safe_wal_remaining<br>FROM pg_replication_slots<br>WHERE database = current_database();</pre><p>We set a five-second dump lock-wait limit for the rehearsal; a production db lock timeout needs its own review. We run this job in a persistent bastion session, then use another AdminClient session for Extract.</p><p><strong>You can add the Extract while the </strong><strong>pg_dump is running.</strong></p><h3>8. Add and start Extract at the matching LSN</h3><p>We will use the consistent_point LSN <strong>0/8C4BDA08</strong> from Step 6.</p><pre>DBLOGIN USERIDALIAS postgres14ubuntuvm DOMAIN OracleGoldenGate<br><br>ADD EXTRACT PGEXT, TRANLOG, LSN 0/8C4BDA08<br><br>ADD EXTTRAIL dv, EXTRACT PGEXT<br><br>EDIT PARAMS PGEXT</pre><pre>EXTRACT PGEXT<br>USERIDALIAS postgres14ubuntuvm DOMAIN OracleGoldenGate<br>EXTTRAIL dv<br>TRANLOGOPTIONS STREAMINGOPTIONS (publication_names=&quot;ogg_migration_pub&quot;;)<br>GETTRUNCATES<br>TABLE dvdrental.actor;<br>TABLE dvdrental.address;<br>TABLE dvdrental.category;<br>TABLE dvdrental.city;<br>TABLE dvdrental.country;<br>TABLE dvdrental.customer;<br>TABLE dvdrental.film;<br>TABLE dvdrental.film_actor;<br>TABLE dvdrental.film_category;<br>TABLE dvdrental.inventory;<br>TABLE dvdrental.language;<br>TABLE dvdrental.payment;<br>TABLE dvdrental.rental;<br>TABLE dvdrental.staff;<br>TABLE dvdrental.store;</pre><pre>START EXTRACT PGEXT<br>INFO EXTRACT PGEXT, DETAIL<br>VIEW REPORT PGEXT<br>STATS EXTRACT PGEXT</pre><blockquote><strong>Important Note : </strong>Once the extract is started the permanent slot created by Goldengate will lower the WAL retention.</blockquote><p>For a long production dump, we would start Extract during the dump and release the exporter after snapshot import is completed. The gg_snap_tmp slot must disappear; the GoldenGate permanent slot must remain active and advancing.</p><h3>9. Restore the dump to OCI PostgreSQL</h3><pre>PGAPPNAME=ogg_lab_restore pg_restore \<br>  --host=10.180.2.199 --port=5432 \<br>  --username=postgres --dbname=postgres \<br>  --jobs=4 --exit-on-error --verbose \<br>  --no-owner --no-acl --no-tablespaces \<br>  /home/opc/ogg_dvd_lab/dump &gt; /home/opc/ogg_dvd_lab/restore.log 2&gt;&amp;1</pre><p>To monitor from a separate session run :</p><pre>psql -h 10.180.2.199 -U postgres -d postgres<br><br>SELECT pid,<br>       application_name,<br>       state,<br>       xact_start,<br>       backend_xmin,<br>       wait_event_type,<br>       wait_event,<br>       left(query, 120) AS query<br>FROM pg_stat_activity<br>WHERE application_name = &#39;ogg_lab_restore&#39;<br>ORDER BY pid;</pre><p>In case if any extensions are not available on OCI PostgreSQL as compared to Source then create a list</p><pre>pg_restore -l /home/opc/ogg_dvd_lab/dump &gt; restore-list.txt</pre><p>Edit the<strong> </strong><strong>restore-list.txt </strong>file using a text editor (like nano or vim). Find the lines that correspond to CREATE EXTENSION or the specific extensions you want to skip, and delete or comment out those lines (by placing a ; at the start of the line)</p><p>Run<strong> </strong>pg_restore<strong> </strong>with the modified list using the -L flag</p><pre><br>PGAPPNAME=ogg_lab_restore pg_restore \<br>  --host=10.180.2.199 --port=5432 \<br>  --username=postgres --dbname=postgres \<br>  --jobs=4 --exit-on-error --verbose \<br>  -L restore-list.txt \<br>  --no-owner --no-acl --no-tablespaces \<br>  /home/opc/ogg_dvd_lab/dump &gt; /home/opc/ogg_dvd_lab/restore.log 2&gt;&amp;1</pre><h3>10. Add Replicat from the beginning of the new trail</h3><pre>DBLOGIN USERIDALIAS pggoldengatedemo DOMAIN OracleGoldenGate<br><br>ADD REPLICAT PGRP, PARALLEL, EXTTRAIL dv, CHECKPOINTTABLE dvdrental.dvd_chkpt<br><br>EDIT PARAMS PGRP</pre><pre>REPLICAT PGRP<br>USERIDALIAS pggoldengatedemo DOMAIN OracleGoldenGate<br>MAP_PARALLELISM 4<br>APPLY_PARALLELISM 8<br>REPERROR (DEFAULT, ABEND)<br>GETTRUNCATES<br>MAP dvdrental.actor, TARGET dvdrental.actor;<br>MAP dvdrental.address, TARGET dvdrental.address;<br>MAP dvdrental.category, TARGET dvdrental.category;<br>MAP dvdrental.city, TARGET dvdrental.city;<br>MAP dvdrental.country, TARGET dvdrental.country;<br>MAP dvdrental.customer, TARGET dvdrental.customer;<br>MAP dvdrental.film, TARGET dvdrental.film;<br>MAP dvdrental.film_actor, TARGET dvdrental.film_actor;<br>MAP dvdrental.film_category, TARGET dvdrental.film_category;<br>MAP dvdrental.inventory, TARGET dvdrental.inventory;<br>MAP dvdrental.language, TARGET dvdrental.language;<br>MAP dvdrental.payment, TARGET dvdrental.payment;<br>MAP dvdrental.rental, TARGET dvdrental.rental;<br>MAP dvdrental.staff, TARGET dvdrental.staff;<br>MAP dvdrental.store, TARGET dvdrental.store;</pre><pre>VIEW PARAMS PGRP<br><br>START REPLICAT PGRP<br><br>INFO ALL<br><br>VIEW REPORT PGRP<br><br>STATS REPLICAT PGRP<br><br>LAG REPLICAT PGRP</pre><p>We start at the <strong>beginning of the fresh trail</strong>. We omit BEGIN NOW, HANDLECOLLISIONS The snapshot/LSN pair gives us an exact instantiation boundary, and REPERROR (DEFAULT, ABEND) stops a conflict for diagnosis. We can tune Parallel Replicat from measured target apply rate and foreign-key behavior later.</p><h3>11. Monitor the publication, slots, WAL and apply stream</h3><p>Some important SQL queries to run at source</p><h4>Source SQL Queries :</h4><pre>psql -h 10.180.2.27 -U postgres -d postgres<br><br>-- 1. Count the source tables included in our migration publication.<br>-- Expected: 15 tables for this lab.<br>SELECT count(*) AS published_tables<br>FROM pg_publication_tables<br>WHERE pubname = &#39;ogg_migration_pub&#39;;<br><br><br>-- 2. Check replication slots for the current source database.<br>-- restart_lsn determines the oldest WAL position each slot still needs.<br>-- confirmed_flush_lsn shows capture acknowledgement, not target apply progress.<br>-- retained_wal estimates the WAL span retained by each slot; these spans can overlap.<br>-- safe_wal_remaining is NULL when retention is unlimited or the slot is lost.<br>SELECT slot_name,<br>       plugin,<br>       temporary,<br>       active,<br>       restart_lsn,<br>       confirmed_flush_lsn,<br>       pg_size_pretty(<br>         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)<br>       ) AS retained_wal,<br>       wal_status,<br>       pg_size_pretty(safe_wal_size) AS safe_wal_remaining<br>FROM pg_replication_slots<br>WHERE database = current_database();<br><br><br>-- 3. Sample the source cluster&#39;s current WAL position and cumulative WAL bytes.<br>-- Compare samples over time to measure WAL generation.<br>-- Check stats_reset before calculating differences; cumulative statistics can lag.<br>SELECT pg_current_wal_lsn() AS current_lsn,<br>       wal_bytes,<br>       stats_reset<br>FROM pg_stat_wal;<br><br><br>-- 4. Measure the physical size of files in the source pg_wal directory.<br>-- This is WAL-file usage, not available filesystem space or just slot-retained WAL.<br>SELECT pg_size_pretty(coalesce(sum(size), 0)) AS physical_pg_wal_files<br>FROM pg_ls_waldir();<br><br><br>-- 5. Check whether logical decoding spills transaction data from memory to disk.<br>-- Rising spill counters can help explain capture I/O pressure or slower decoding.<br>-- Use the permanent Extract slot name from the current migration run.<br>SELECT slot_name,<br>       spill_txns,<br>       spill_count,<br>       spill_bytes,<br>       stats_reset<br>FROM pg_stat_replication_slots<br>WHERE slot_name = &#39;pgext_e8f172784cad044e&#39;;<br><br><br>-- 6. Inspect open source transactions, including idle-in-transaction sessions.<br>-- Transaction age and backend_xmin help identify sessions delaying vacuum cleanup.<br>-- Wait details and blocking_pids help identify lock contention during the dump.<br>SELECT pid,<br>       application_name,<br>       state,<br>       extract(epoch FROM clock_timestamp() - xact_start)::bigint<br>         AS xact_age_seconds,<br>       backend_xmin,<br>       wait_event_type,<br>       wait_event,<br>       pg_blocking_pids(pid) AS blocking_pids<br>FROM pg_stat_activity<br>WHERE datname = current_database()<br>  AND pid &lt;&gt; pg_backend_pid()<br>  AND xact_start IS NOT NULL<br>ORDER BY xact_start;<br><br><br>-- 7. Monitor estimated dead tuples and autovacuum activity in our source schema.<br>-- Increasing dead tuples can indicate that cleanup is falling behind.<br>-- n_dead_tup is an estimate, not a direct measurement of table bloat.<br>SELECT relname,<br>       n_dead_tup,<br>       last_autovacuum,<br>       autovacuum_count<br>FROM pg_stat_user_tables<br>WHERE schemaname = &#39;dvdrental&#39;<br>ORDER BY n_dead_tup DESC;<br><br><br>-- 8. Check the cumulative deadlock count for the current source database.<br>-- Compare with our baseline to detect new deadlocks during the migration.<br>SELECT deadlocks<br>FROM pg_stat_database<br>WHERE datname = current_database();<br><br>-- 9. Run this same query on both source and target to get exact row counts:<br>SELECT &#39;actor&#39; AS table_name, count(*) AS row_count FROM dvdrental.actor<br>UNION ALL<br>SELECT &#39;address&#39;, count(*) FROM dvdrental.address<br>UNION ALL<br>SELECT &#39;category&#39;, count(*) FROM dvdrental.category<br>UNION ALL<br>SELECT &#39;city&#39;, count(*) FROM dvdrental.city<br>UNION ALL<br>SELECT &#39;country&#39;, count(*) FROM dvdrental.country<br>UNION ALL<br>SELECT &#39;customer&#39;, count(*) FROM dvdrental.customer<br>UNION ALL<br>SELECT &#39;film&#39;, count(*) FROM dvdrental.film<br>UNION ALL<br>SELECT &#39;film_actor&#39;, count(*) FROM dvdrental.film_actor<br>UNION ALL<br>SELECT &#39;film_category&#39;, count(*) FROM dvdrental.film_category<br>UNION ALL<br>SELECT &#39;inventory&#39;, count(*) FROM dvdrental.inventory<br>UNION ALL<br>SELECT &#39;language&#39;, count(*) FROM dvdrental.language<br>UNION ALL<br>SELECT &#39;payment&#39;, count(*) FROM dvdrental.payment<br>UNION ALL<br>SELECT &#39;rental&#39;, count(*) FROM dvdrental.rental<br>UNION ALL<br>SELECT &#39;staff&#39;, count(*) FROM dvdrental.staff<br>UNION ALL<br>SELECT &#39;store&#39;, count(*) FROM dvdrental.store<br>ORDER BY table_name;</pre><h4>Target Database SQL queries :</h4><pre>psql -h 10.180.2.199 -U postgres -d postgres<br><br>-- 1. Monitor active restore and replication sessions, open transactions and waits.<br>-- blocking_pids identifies sessions preventing another session from progressing.<br>-- GoldenGate sessions may use a generic application name, so we do not filter by it.<br>SELECT pid,<br>       application_name,<br>       usename,<br>       state,<br>       extract(epoch FROM clock_timestamp() - xact_start)::bigint<br>         AS xact_age_seconds,<br>       wait_event_type,<br>       wait_event,<br>       pg_blocking_pids(pid) AS blocking_pids,<br>       left(query, 200) AS query<br>FROM pg_stat_activity<br>WHERE datname = current_database()<br>  AND pid &lt;&gt; pg_backend_pid()<br>  AND state &lt;&gt; &#39;idle&#39;<br>ORDER BY xact_start NULLS LAST;<br><br><br>-- 2. Monitor table data currently being loaded through COPY during pg_restore.<br>-- bytes_total can be zero when PostgreSQL does not know the incoming stream size.<br>-- An empty result means no COPY is currently visible, not that restore succeeded.<br>SELECT pid,<br>       relid::regclass AS table_name,<br>       command,<br>       type,<br>       tuples_processed,<br>       pg_size_pretty(bytes_processed) AS bytes_processed,<br>       pg_size_pretty(bytes_total) AS bytes_total<br>FROM pg_stat_progress_copy<br>WHERE datname = current_database()<br>ORDER BY pid;<br><br><br>-- 3. Monitor index creation during the restore&#39;s post-data phase.<br>-- These counters describe the current index-build phase, not the entire restore.<br>SELECT pid,<br>       relid::regclass AS table_name,<br>       index_relid::regclass AS index_name,<br>       command,<br>       phase,<br>       blocks_done,<br>       blocks_total,<br>       tuples_done,<br>       tuples_total<br>FROM pg_stat_progress_create_index<br>WHERE datname = current_database()<br>ORDER BY pid;<br><br><br>-- 4. Monitor table activity and storage growth during restore and CDC apply.<br>-- DML counters include activity from all sessions and are cumulative.<br>-- estimated_rows is approximate; use COUNT(*) for final row-count reconciliation.<br>-- total_size includes the table, its indexes and TOAST storage.<br>SELECT relname AS table_name,<br>       n_live_tup AS estimated_rows,<br>       n_tup_ins AS inserts,<br>       n_tup_upd AS updates,<br>       n_tup_del AS deletes,<br>       pg_size_pretty(pg_total_relation_size(relid)) AS total_size<br>FROM pg_stat_user_tables<br>WHERE schemaname = &#39;dvdrental&#39;<br>ORDER BY relname;<br><br><br>-- 5. Monitor database size, connections, rollbacks, deadlocks and temporary files.<br>-- Compare cumulative counters between samples and check stats_reset.<br>-- Temporary-file growth can indicate sorting or other work spilling to disk.<br>-- database_size does not report available or provisioned storage.<br>SELECT datname,<br>       pg_size_pretty(pg_database_size(datname)) AS database_size,<br>       numbackends AS connections,<br>       xact_commit,<br>       xact_rollback,<br>       deadlocks,<br>       temp_files,<br>       pg_size_pretty(temp_bytes) AS temp_data_written,<br>       stats_reset<br>FROM pg_stat_database<br>WHERE datname = current_database();<br><br><br>-- 6. Monitor dead tuples, vacuum cleanup and planner-statistics maintenance.<br>-- After the initial restore, ANALYZE should populate statistics for restored tables.<br>-- last_analyze records manual ANALYZE; last_autoanalyze records automatic ANALYZE.<br>SELECT relname AS table_name,<br>       n_dead_tup,<br>       last_vacuum,<br>       last_autovacuum,<br>       last_analyze,<br>       last_autoanalyze<br>FROM pg_stat_user_tables<br>WHERE schemaname = &#39;dvdrental&#39;<br>ORDER BY relname;<br><br><br>-- 7. Check that foreign keys exist and have been validated after the full restore.<br>-- Expected for our complete lab schema: 22 foreign keys and zero unvalidated.<br>-- A custom restore list that omits constraints will require completing them later.<br>SELECT count(*) AS foreign_keys,<br>       count(*) FILTER (WHERE NOT c.convalidated) AS unvalidated_foreign_keys<br>FROM pg_constraint c<br>JOIN pg_namespace n ON n.oid = c.connamespace<br>WHERE n.nspname = &#39;dvdrental&#39;<br>  AND c.contype = &#39;f&#39;;<br><br><br>-- 8. Find indexes that are not valid or not ready to receive changes.<br>-- Review any returned rows after index creation and restore have finished.<br>-- An empty result does not prove that every expected index was restored.<br>SELECT t.relname AS table_name,<br>       i.indexrelid::regclass AS index_name,<br>       i.indisprimary AS is_primary_key,<br>       i.indisunique AS is_unique,<br>       i.indisvalid AS is_valid,<br>       i.indisready AS is_ready<br>FROM pg_index i<br>JOIN pg_class t ON t.oid = i.indrelid<br>JOIN pg_namespace n ON n.oid = t.relnamespace<br>WHERE n.nspname = &#39;dvdrental&#39;<br>  AND (NOT i.indisvalid OR NOT i.indisready)<br>ORDER BY t.relname, i.indexrelid;<br><br><br>-- 9. Inspect target sequence values without advancing them.<br>-- last_value can include values reserved by sequence caching.<br>-- This is an overview; final sequence validation also needs is_called,<br>-- source sequence state and target maximum IDs before enabling target writes.<br>SELECT sequencename,<br>       last_value,<br>       increment_by,<br>       cache_size<br>FROM pg_sequences<br>WHERE schemaname = &#39;dvdrental&#39;<br>ORDER BY sequencename;</pre><p><strong>After Parallel Replicat creates its checkpoint tables</strong>, this additional query works with the checkpoint layout verified in our lab:</p><pre>-- 10. Inspect the source positions recorded by each Parallel Replicat applier.<br>-- Compare successive samples to see whether the saved checkpoints advance.<br>-- For cutover, verify every applier against the final source barrier.<br>-- This is a GoldenGate implementation-specific table, not a PostgreSQL system view.<br>SELECT applier_id,<br>       lwm_csn AS applied_source_position,<br>       seqno AS trail_sequence,<br>       rba AS trail_byte_position,<br>       trail_name<br>FROM dvdrental.dvd_chkpt_pgrp<br>ORDER BY applier_id;</pre><p>Some important Goldengate commands :</p><pre>INFO EXTRACT PGEXT, DETAIL<br>STATS EXTRACT PGEXT, TOTAL<br>VIEW REPORT PGEXT<br><br>INFO REPLICAT PGRP, DETAIL<br>STATS REPLICAT PGRP, TOTAL<br>LAG REPLICAT PGRP<br>VIEW REPORT PGRP</pre><h3>Conclusion</h3><p>What We Validated:</p><blockquote>1. Initial and final row counts<br>2. CDC operations tested<br>3. Reconciliation results<br>4. Sequence verification<br>5. Any measured timings.</blockquote><p>We make a large PostgreSQL migration manageable by coupling the initial copy and the CDC stream to one PostgreSQL snapshot boundary. The exported snapshot gives the dump a consistent baseline; its paired LSN tells GoldenGate where to start. Source applications continue to write while we copy and restore, and a measured final fence gives us a clean point for barrier verification and sequence synchronization.</p><h3>Appendix A — Sequences Synchronization</h3><p>Sequences are not handled by PostgreSQL logical replication and has to be handled outside. Please read : <a href="https://www.postgresql.org/docs/18/logical-replication-restrictions.html">https://www.postgresql.org/docs/18/logical-replication-restrictions.html</a></p><p>For our dvdrental schema, we need to synchronize <strong>13 sequences</strong>. film_actor and film_category use composite primary keys and have no sequences.</p><h4>Where this step fits</h4><pre>Restore the initial backup<br>        ↓<br>Start Replicat and let it catch up<br>        ↓<br>Stop source application writes, jobs and the test writer<br>        ↓<br>Wait for in-flight transactions and final changes to reach the target<br>        ↓<br>SYNCHRONIZE THE 13 SEQUENCES<br>        ↓<br>Verify sequence values<br>        ↓<br>Switch applications and enable target writes</pre><p>We perform this during the <strong>final cutover window</strong>. Replicat can remain running; target application writers must remain stopped.</p><p>The replicated rows contain their generated IDs, but those inserts do not advance the corresponding target sequences.</p><h4>1. Export the final source sequence states</h4><p>Run this <strong>from the bastion</strong>, after source writes have stopped and Replicat has applied the final changes:</p><pre># Export only the current states of our 13 source sequences.<br>pg_dump \<br>  --host=10.180.2.27 --port=5432 \<br>  --username=postgres --dbname=postgres \<br>  --data-only --no-owner --no-acl \<br>  --table=dvdrental.actor_actor_id_seq \<br>  --table=dvdrental.address_address_id_seq \<br>  --table=dvdrental.category_category_id_seq \<br>  --table=dvdrental.city_city_id_seq \<br>  --table=dvdrental.country_country_id_seq \<br>  --table=dvdrental.customer_customer_id_seq \<br>  --table=dvdrental.film_film_id_seq \<br>  --table=dvdrental.inventory_inventory_id_seq \<br>  --table=dvdrental.language_language_id_seq \<br>  --table=dvdrental.payment_payment_id_seq \<br>  --table=dvdrental.rental_rental_id_seq \<br>  --table=dvdrental.staff_staff_id_seq \<br>  --table=dvdrental.store_store_id_seq \<br>  --file=/home/opc/ogg_dvd_lab/final_sequences.sql</pre><p>This produces setval() statements containing each sequence’s <strong>last_value and </strong><strong>is_called</strong>. It does not export table rows.</p><p>We generate this file freshly at cutover because source sequence values continue advancing during the migration.</p><h4>2. Compare sequence states before applying</h4><p>Run the following <strong>in </strong><strong>psql on both source and target</strong>. It discovers the sequences owned by columns in dvdrental and displays their states alongside each table’s maximum ID:</p><pre>-- Inspect every owned sequence in our schema without advancing it.<br>-- Expected: 13 sequence results.<br>SELECT format(<br>  &#39;SELECT %L AS sequence_name, &#39;<br>  || &#39;(SELECT max(%I) FROM %I.%I) AS max_id, &#39;<br>  || &#39;last_value, is_called FROM %s;&#39;,<br>  pg_get_serial_sequence(<br>    format(&#39;%I.%I&#39;, n.nspname, c.relname), a.attname<br>  ),<br>  a.attname,<br>  n.nspname,<br>  c.relname,<br>  pg_get_serial_sequence(<br>    format(&#39;%I.%I&#39;, n.nspname, c.relname), a.attname<br>  )<br>)<br>FROM pg_class c<br>JOIN pg_namespace n ON n.oid = c.relnamespace<br>JOIN pg_attribute a ON a.attrelid = c.oid<br>WHERE n.nspname = &#39;dvdrental&#39;<br>  AND c.relkind = &#39;r&#39;<br>  AND a.attnum &gt; 0<br>  AND NOT a.attisdropped<br>  AND pg_get_serial_sequence(<br>        format(&#39;%I.%I&#39;, n.nspname, c.relname), a.attname<br>      ) IS NOT NULL<br>ORDER BY c.relname<br>\gexec</pre><p>For our sequences, which increment by one:</p><ul><li>is_called = true: the next generated value is last_value + 1.</li><li>is_called = false: the next generated value is last_value.</li><li>The <strong>planned next value must exceed the target table’s maximum ID</strong>.</li><li>If a target sequence has already advanced beyond the source, we resolve that difference before applying the file; we do not move it backward.</li></ul><p>We preserve source sequence state rather than simply setting every sequence to MAX(id), because unsuccessful inserts and rolled-back transactions can consume sequence values.</p><h4>3. Apply the reviewed states to the target</h4><p>After the export succeeds and the comparisons above are satisfactory, run this <strong>from the bastion</strong>:</p><pre># Apply the final source sequence states to OCI PostgreSQL.<br>psql -X -v ON_ERROR_STOP=1 \<br>  --host=10.180.2.199 --port=5432 \<br>  --username=postgres --dbname=postgres \<br>  --file=/home/opc/ogg_dvd_lab/final_sequences.sql</pre><h4>4. Verify, then enable target writes</h4><p>Rerun the inspection query on both databases. For this fresh target, we expect all <strong>13 </strong><strong>last_value / </strong><strong>is_called pairs to match</strong>, with safe next values.</p><p>Keep source writers stopped throughout. Reconnect any target application sessions that previously consumed sequence values before enabling writes, so they cannot reuse old cached allocations.</p><p>This sequence synchronization is the final database preparation step before applications start writing to OCI PostgreSQL.</p><h3>Appendix B — Test Update, Delete, Upserts</h3><p>The script operates on <strong>dvdrental.payment</strong>, which has no referencing child tables in your schema, so deleting payments preserves the foreign keys.</p><pre>/home/opc/ogg_dvd_lab/02_dml_source.sql</pre><p>Upserts use fresh sequence values for new rows and <a href="https://www.postgresql.org/docs/14/sql-insert.html">ON CONFLICT … DO UPDATE</a> for existng rows.</p><p>Run this command from the bastion. Change the first three variables as required. The password is requested <strong>once</strong>.</p><pre>(<br>set -Eeuo pipefail<br><br>operation=update<br>rows=100<br>batches=600<br><br>read -rsp &quot;PostgreSQL source password: &quot; PGPASSWORD<br>echo<br>export PGPASSWORD<br><br>control=&quot;/home/opc/ogg_dvd_lab/ogg_dvd_${operation}.run&quot;<br>log=&quot;/home/opc/ogg_dvd_lab/ogg_dvd_${operation}.log&quot;<br><br>touch &quot;$control&quot;<br>batch=0<br><br>while test -e &quot;$control&quot; &amp;&amp; test &quot;$batch&quot; -lt &quot;$batches&quot;; do<br>  printf &#39;\nBatch %s: %s\n&#39; &quot;$((batch + 1))&quot; &quot;$operation&quot; &gt;&gt; &quot;$log&quot;<br><br>  PGAPPNAME=&quot;ogg_lab_${operation}&quot; \<br>  psql -X -w -v ON_ERROR_STOP=1 \<br>    -v operation=&quot;$operation&quot; -v rows=&quot;$rows&quot; \<br>    -h 10.180.2.27 -U postgres -d postgres \<br>    -f /home/opc/ogg_dvd_lab/02_dml_source.sql \<br>    &gt;&gt; &quot;$log&quot; 2&gt;&amp;1<br><br>  batch=$((batch + 1))<br>  sleep 2<br>done<br>)</pre><p>Each batch commits separately. If fewer than the requested number of unlocked payments remain, the script reports an error and the loop stops.</p><p>Use delete or upsert in these filenames when running those modes.</p><p><strong>Performance note:</strong> Random selection uses ORDER BY random(), which scans the payment table. Logged timings include selection and source execution; monitor GoldenGate lag and operation counters separately to assess replication performance.</p><p>Upsert advances the source payment sequence, so retain the <strong>final sequence synchronization before target writes begin</strong>.</p><h3>Appendix C — Workstation setup and lab cleanup</h3><p>We use the following <strong>one-time workstation setup</strong> for the management forward and Docker AdminClient. During the rebuild, we reused the existing container and forward. The forward later closed and was not relaunched; our database checks continued from the bastion.</p><pre>ssh -fNT -v -L 8443:10.180.2.176:443 opc@&lt;bastion-host-ip&gt; \<br>  -i ~/.ssh/mydemo_vcn.priv</pre><pre>docker run -d \<br>  --name ogg26ai \<br>  --hostname ogg26ai \<br>  --restart unless-stopped \<br>  -p 443:443 \<br>  --env-file &quot;$HOME/goldengate/26ai/.env&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u02:/u02&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u03:/u03&quot; \<br>  -v &quot;$HOME/goldengate/26ai/cert:/etc/nginx/cert&quot; \<br>  --log-opt max-size=50m \<br>  --log-opt max-file=5 \<br>  container-registry.oracle.com/goldengate/goldengate-oracle-free:latest</pre><h3>Cleanup for Goldengate :</h3><pre>STOP REPLICAT PGRP<br>STOP EXTRACT PGEXT<br>DBLOGIN USERIDALIAS pggoldengatedemo DOMAIN OracleGoldenGate<br>DELETE REPLICAT PGRP<br>DELETE CHECKPOINTTABLE dvdrental.dvd_chkpt !<br>DBLOGIN USERIDALIAS postgres14ubuntuvm DOMAIN OracleGoldenGate<br>DELETE EXTRACT PGEXT<br>UNREGISTER EXTRACT PGEXT<br>PURGE EXTTRAIL dv<br>PURGE EXTTRAIL pg<br>INFO ALL</pre><p>We used ! only to suppress confirmation on DELETE CHECKPOINTTABLE. We did not use it on DELETE REPLICAT, where it can bypass database cleanup. We then removed the old publication, fixture schemas and the retired public.orders example from both lab databases:</p><pre><br>psql -X -v ON_ERROR_STOP=1 -h 10.180.2.27 -U postgres -d postgres &lt;&lt;&#39;SQL&#39;<br>DROP PUBLICATION IF EXISTS ogg_migration_pub;<br>DROP SCHEMA IF EXISTS dvd_lab CASCADE;<br>DROP SCHEMA IF EXISTS dvdrental CASCADE;<br>SQL<br><br>psql -X -v ON_ERROR_STOP=1 -h 10.180.2.199 -U postgres -d postgres &lt;&lt;&#39;SQL&#39;<br>DROP PUBLICATION IF EXISTS ogg_migration_pub;<br>DROP SCHEMA IF EXISTS dvd_lab CASCADE;<br>DROP SCHEMA IF EXISTS dvdrental CASCADE;<br>SQL</pre><p>Our post-cleanup checks showed no source logical slots, no old target checkpoint tables and no fixture tables or sequences. The new run could therefore register a new slot, create an empty trail and restore into empty fixture schemas.</p><h3>References</h3><ul><li><a href="https://docs.oracle.com/en-us/iaas/goldengate/">OCI GoldenGate service overview</a> and <a href="https://docs.oracle.com/en/cloud/paas/goldengate-service/ocigg/manage-deployments.html">deployment management</a></li><li><a href="https://docs.oracle.com/en/cloud/paas/goldengate-service/ocigg/whats-supported.html">Oracle Goldengate Support Matrix</a></li><li><a href="https://docs.oracle.com/en/cloud/paas/goldengate-service/ocigg/create-data-replication-resources.html">Create a GoldenGate data-replication deployment</a>, <a href="https://docs.oracle.com/en/cloud/paas/goldengate-service/ocigg/connections/connect-to-postgresql-server.html">PostgreSQL Server connection</a>, <a href="https://docs.oracle.com/en/cloud/paas/goldengate-service/ocigg/manage-connections.html">assign and test connections</a></li><li><a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/instantiate-add-initial-load-extract-postgresql-ma-1.html">Oracle precise instantiation with </a><a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/instantiate-add-initial-load-extract-postgresql-ma-1.html">pg_dump</a>, <a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/extract-add-primary-extract.html">pgoutput Extract requirements</a>, <a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/prepare-postgresql-understanding-whats-supported.html">PostgreSQL type and operation support</a></li><li><a href="https://docs.oracle.com/en-us/iaas/Content/postgresql/import-export-migrate.htm">OCI PostgreSQL migration guidance</a> and <a href="https://docs.oracle.com/en-us/iaas/Content/postgresql/cross-region-replication.htm">Warm Standby</a></li><li><a href="https://www.postgresql.org/docs/14/protocol-replication.html">PostgreSQL replication protocol</a>, <a href="https://www.postgresql.org/docs/14/app-pgdump.html">pg_dump</a>, <a href="https://www.postgresql.org/docs/14/app-pgrestore.html">pg_restore</a>, <a href="https://www.postgresql.org/docs/14/view-pg-replication-slots.html">replication-slot monitoring</a>, <a href="https://www.postgresql.org/docs/14/functions-sequence.html">sequence functions</a></li><li>Our <a href="https://gist.github.com/shadabshaukat/9500bc7458a16e1733f8681aad10775a">DVD rental schema and generator</a> and <a href="https://gist.github.com/shadabshaukat/66e8c7ecc1222a6faff6d86d8510c982">source load script</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=60801fbaf657" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Multi-Region Replication with OCI PostgreSQL using pglogical]]></title>
            <link>https://shadabshaukat.medium.com/building-multi-region-replication-with-oci-postgresql-using-pglogical-23fdf5c024fe?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/23fdf5c024fe</guid>
            <category><![CDATA[replication]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <category><![CDATA[cross-region]]></category>
            <category><![CDATA[pglogical]]></category>
            <category><![CDATA[postgresql]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Sun, 27 Sep 2026 10:33:46 GMT</pubDate>
            <atom:updated>2026-10-11T04:48:15.867Z</atom:updated>
            <content:encoded><![CDATA[<p>OCI Database with PostgreSQL provides a managed PostgreSQL platform, pglogical extension is available on OCI PostgreSQL which provides flexible publish-and-subscribe replication at the logical level.</p><p>With pglogical, we can explicitly select the tables and sequences to replicate, build a one-to-many topology from multiple regions, and retain options for cross-version migrations or replication beyond a single managed-service boundary. This makes it well suited to an architecture where the regional databases are deliberately prepared and operated as independent PostgreSQL systems.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VVT_HnPctbGSfwVQz3MAqw.png" /><figcaption>OCI PostgreSQL Logical Replicaiion from SYD → MEL &amp; SGP</figcaption></figure><p>While OCI PostgreSQL Warm Standby is best suited for service-managed disaster recovery: it asynchronously streams WAL for the complete database system. pglogical is selective logical distribution rather than a complete physical DR copy : pglogical does not replace backups or managed failover, and DDL changes, monitoring, sequence handling, and promotion must be planned separately.</p><p>Ref : <a href="https://blogs.oracle.com/cloud-infrastructure/reducing-postgresql-replication-latency">https://blogs.oracle.com/cloud-infrastructure/reducing-postgresql-replication-latency</a></p><p>In this blog, we’ll build a one-way, one-to-many logical replication topology on Oracle Cloud Infrastructure (OCI) Database with PostgreSQL. Sydney will be the only writable source, while Melbourne and Singapore will receive changes as application-read-only subscribers.</p><pre>                    publication-syd <br>                         --------<br>                        | Sydney |<br>                         --------<br>                          Writer<br>                    |               |<br>                    |               |<br>                    v               v<br>        subscription_mel      subscription_sgp<br>            ---------             -----------<br>           | Melbourne|          | Singapore|<br>            ---------             ----------<br>              Reader                 Reader<br><br></pre><pre>The replication flow is one-way:<br><br>   - Applications write in Sydney.<br>   - Melbourne and Singapore are read-only for the application role.<br>   - Each target pulls from the Sydney provider over a private, TLS-verified connection.<br>   - We initialize Melbourne completely before starting Singapore.<br>   - The default replication set contains both the application tables and their sequences.</pre><h3><strong>Prerequisites and Steps for Setting up OCI Environment</strong></h3><h4><strong>1. SQL files which will be our Schema :</strong></h4><ul><li><strong>00_create_schema.sql </strong>creates schemas, types, 15 tables, constraints, indexes, 13 sequences, and the dvd_lab.generate_data(integer) procedure. It never generates rows.</li></ul><p><a href="https://gist.github.com/shadabshaukat/9500bc7458a16e1733f8681aad10775a">https://gist.github.com/shadabshaukat/9500bc7458a16e1733f8681aad10775a</a></p><ul><li><strong>01_load_source.sql</strong> validates -v rows, sets UTC for the transaction, and calls the generator. <strong>This will be run in Sydney only</strong></li></ul><p><a href="https://gist.github.com/shadabshaukat/66e8c7ecc1222a6faff6d86d8510c982">https://gist.github.com/shadabshaukat/66e8c7ecc1222a6faff6d86d8510c982</a></p><blockquote>Refer to Appendix at the end of the article to download code files for <strong>00_create_schema.sql <br>01_load_source.sql</strong></blockquote><h4>1.1 OCI PostgreSQL Setup</h4><p>The below are the 3 OCI PostgreSQL DBSystems.<strong> Sydney is primary writer</strong> node and <strong>Melbourne</strong> and <strong>Singapore</strong> are the <strong>Reader target nodes</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NHZJ4kb849ig_7KG1hQHaA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UEfUtkIO1CS-Qy6iHdxcLg.png" /></figure><h4><strong>1.2 Other prerequisites:</strong></h4><ul><li>non-overlapping VCN CIDRs;</li><li>a bastion or equivalent private administrative path in each region;</li><li>OCI permissions for database configuration, networking, DNS, logging, and Network Path Analyzer;</li><li>the OCI CA certificate for each client connection;</li><li>a maintenance window for settings that require a restart.</li><li>Copy 00_create_schema.sql to /home/opc on all three bastions.</li><li>Copy 01_load_source.sql to Sydney only; do not distribute it to MEL or SGP. This makes accidental target-side data generation less likely.</li></ul><h4><strong>2. OCI configuration, Routing, Security, and Private DNS Setup</strong></h4><h4><strong>2.1 PostgreSQL configuration on all three DB systems</strong></h4><p>Apply an OCI PostgreSQL configuration that permits pglogical, then wait for every DB system to return to ACTIVE.</p><pre>oci.admin_enabled_extensions includes pglogical<br>shared_preload_libraries includes pglogical<br>wal_level = logical<br>track_commit_timestamp = on<br>max_replication_slots &gt;= 2 on SYD, plus headroom<br>max_wal_senders &gt;= 2 on SYD, plus headroom<br>max_worker_processes: target at least 16 if the OCI shape permits it</pre><p><strong>Run on every database:</strong></p><pre>SELECT name, setting, unit, pending_restart<br>FROM pg_settings<br>WHERE name IN (<br>  &#39;wal_level&#39;,<br>  &#39;track_commit_timestamp&#39;,<br>  &#39;shared_preload_libraries&#39;,<br>  &#39;max_worker_processes&#39;,<br>  &#39;max_replication_slots&#39;,<br>  &#39;max_wal_senders&#39;<br>)<br>ORDER BY name;</pre><p>Expected: <strong>pglogical</strong> is allowed and preloaded, <strong>wal_level</strong> is logical, <strong>track_commit_timestamp</strong> is on, and <strong>pending_restart</strong> is false after the maintenance action.</p><h4><strong>2.2 Cross-region network</strong><br> <br> Build and verify two independent peerings:</h4><ul><li><strong>SYD VCN ↔ MEL VCN</strong></li><li><strong>SYD VCN ↔ SGP VCN</strong></li></ul><p>Each path needs:</p><ol><li>DRGs and a distinct remote peering connection on each side.</li><li>Reciprocal subnet routes, including the return route from Sydney.</li><li>Stateful TCP/5432 ingress to the Sydney database subnet from the MEL and SGP private subnet CIDRs.</li><li>Target-side TCP/5432 egress to the Sydney subnet if the existing egress policy does not already permit it.</li><li>Non-overlapping VCN CIDRs.</li><li>Review of the route table and every security list/NSG attached to the actual database subnets.</li><li>DNS resolution does not create the network path. If DNS works but psql times out, use OCI Network Path Analyzer and VCN flow logs.</li></ol><h4><strong>2.3 Private DNS on OCI</strong></h4><p>For one-way replication, only MEL and SGP need the Sydney private zone:</p><pre>Zone: 7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com<br>Record: primary<br>Type: A<br>Address: 10.150.2.229</pre><p>Attach the private view to each target VCN resolver and publish it. Sydney does not need custom MEL/SGP records for this one-way design, but it is good to get across and add DNS for all zones to all private resolvers, in-case you need to create multi-master replication in the future.</p><p><strong>DNS Source setup in Sydney</strong></p><pre>10.150.2.229<br>primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com<br><br>## Add Private Zone for Melbourne in Private DNS View of Sydney VCN as &#39;mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com&#39; and add A-record &#39;primary&#39; with IP of DBSystem of Melbourne &#39;10.180.2.168&#39;<br>## Add Private Zone for Singapore in Private DNS View of Sydney VCN as &#39;bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com&#39; and add A-record &#39;primary&#39; with IP of DBSystem of Singapore &#39;10.140.2.64&#39;</pre><p><strong>DNS Target setup in Melbourne</strong></p><pre>10.180.2.168<br>primary.mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com<br><br>## Add Private Zone for Sydney in Private DNS View of Melbourne VCN as &#39;7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com&#39; and add A-record &#39;primary&#39; with IP of DBSystem of Sydney &#39;10.150.2.229&#39;<br>## Add Private Zone for Singapore in Private DNS View of Melbourne VCN as &#39;bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com&#39; and add A-record &#39;primary&#39; with IP of DBSystem of Singapore &#39;10.140.2.64&#39;</pre><p><strong>DNS Target setup in Singapore</strong></p><pre>10.140.2.64<br>primary.bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com<br><br>## Add Private Zone for Sydney in Private DNS View of Singapore VCN as &#39;7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com&#39; and add A-record &#39;primary&#39; with IP of DBSystem of Sydney &#39;10.150.2.229&#39;<br>## Add Private Zone for Melbourne in Private DNS View of Singapore VCN as &#39;mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com&#39; and add A-record &#39;primary&#39; with IP of DBSystem of Melbourne &#39;10.180.2.168&#39;</pre><p>For this article : Run from both target bastions (MEL and SGP):</p><pre>getent hosts primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com<br><br>psql -X \<br>  &quot;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=postgres user=postgres sslmode=require connect_timeout=10&quot; \<br>  -v ON_ERROR_STOP=1 \<br>  -c &quot;SELECT current_database(), current_user, inet_server_addr();&quot;<br></pre><p>Expected: the hostname resolves to <strong>10.150.2.229 </strong>(Sydney DBSystem IP) and the TLS connection succeeds.</p><h4><strong>3. Common database setup on SYD, MEL, and SGP</strong></h4><p>Run this section separately on all three DB systems.</p><p>Use the provisioned OCI administrator, not <em>dvdrental_user</em>.</p><pre>-- Sydney --<br>psql -X \<br>  &quot;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=postgres user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1<br><br>-- Melbourne --<br>psql -X \<br>  &quot;host=primary.mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com port=5432 dbname=postgres user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1<br><br>-- Singapore --<br>psql -X \<br>  &quot;host=primary.bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com port=5432 dbname=postgres user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1</pre><p>Run once on all 3 DB system in SYD, MEL, SGP :</p><pre>SELECT rolname, rolreplication<br>FROM pg_roles<br>WHERE rolname = &#39;postgres&#39;;<br><br>-- Run only if the preceding result rolreplication is false.<br>ALTER ROLE postgres WITH REPLICATION;<br><br>CREATE ROLE dvdrental_user LOGIN NOREPLICATION;<br>\password dvdrental_user<br><br>CREATE DATABASE dvdrental_lab;<br>\connect dvdrental_lab<br><br>SELECT current_database(), current_user;<br>-- Must be: dvdrental_lab | postgres<br><br>CREATE EXTENSION pglogical;<br><br>GRANT EXECUTE<br>ON FUNCTION pg_catalog.pg_replication_origin_session_setup(text)<br>TO postgres;<br><br>GRANT EXECUTE<br>ON FUNCTION pg_catalog.pg_replication_origin_session_reset()<br>TO postgres;<br><br>-- Verify while still connected to dvdrental_lab:<br><br>SELECT<br>  current_database(),<br>  current_user,<br>  has_function_privilege(<br>    &#39;postgres&#39;,<br>    &#39;pg_catalog.pg_replication_origin_session_setup(text)&#39;,<br>    &#39;EXECUTE&#39;<br>  ) AS can_origin_setup,<br>  has_function_privilege(<br>    &#39;postgres&#39;,<br>    &#39;pg_catalog.pg_replication_origin_session_reset()&#39;,<br>    &#39;EXECUTE&#39;<br>  ) AS can_origin_reset;<br><br>SELECT extname, extversion<br>FROM pg_extension<br>WHERE extname = &#39;pglogical&#39;;</pre><p>Expected: database dvdrental_lab, user postgres, both booleans true, and one pglogical extension row.</p><blockquote>Do not install pglogical in database postgres. Extensions and the function grants are database-local.</blockquote><h4><strong>4. Initial schema and data load on the Sydney source</strong></h4><p>Run on the Sydney DB:</p><pre>psql -X \<br>  &quot;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1 \<br>  -f /home/opc/00_create_schema.sql<br><br>psql -X \<br>  &quot;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1 \<br>  -v rows=100000 \<br>  -f /home/opc/01_load_source.sql</pre><p>The first command creates objects only. <br>The second validates rows before any cast, opens one transaction, sets UTC with SET LOCAL TIME ZONE ‘UTC’, generates the source rows, and commits atomically.</p><p>Grant the source application role access to existing objects and set prospective defaults:</p><pre>GRANT CONNECT ON DATABASE dvdrental_lab TO dvdrental_user;<br>GRANT USAGE ON SCHEMA dvdrental TO dvdrental_user;<br><br>GRANT SELECT, INSERT, UPDATE, DELETE<br>ON ALL TABLES IN SCHEMA dvdrental<br>TO dvdrental_user;<br><br>GRANT USAGE, SELECT<br>ON ALL SEQUENCES IN SCHEMA dvdrental<br>TO dvdrental_user;<br><br>ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA dvdrental<br>  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dvdrental_user;<br><br>ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA dvdrental<br>  GRANT USAGE, SELECT ON SEQUENCES TO dvdrental_user;</pre><p>Keep the generator owner-only. Do not grant dvdrental_user access to dvd_lab or the pglogical schema.</p><p><strong>Verify source object counts:</strong></p><pre>SELECT count(*) AS tables<br>FROM pg_class c<br>JOIN pg_namespace n ON n.oid = c.relnamespace<br>WHERE n.nspname = &#39;dvdrental&#39;<br>  AND c.relkind IN (&#39;r&#39;, &#39;p&#39;);<br><br>SELECT count(*) AS owned_sequences<br>FROM pg_class s<br>JOIN pg_namespace n ON n.oid = s.relnamespace<br>WHERE n.nspname = &#39;dvdrental&#39;<br>  AND s.relkind = &#39;S&#39;;</pre><p>Expected: 15 tables and 13 sequences.</p><h4><strong>5. Schema-only setup on the Melbourne target</strong></h4><p>Run on the Melbourne bastion. Do not pass rows and do not run 01_load_source.sql.</p><pre>psql -X \<br>  &quot;host=primary.mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1 \<br>  -f /home/opc/00_create_schema.sql</pre><p>Grant read-only application access:</p><pre>psql -X \<br>  &quot;host=primary.mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1<br><br>GRANT CONNECT ON DATABASE dvdrental_lab TO dvdrental_user;<br>GRANT USAGE ON SCHEMA dvdrental TO dvdrental_user;<br>GRANT SELECT ON ALL TABLES IN SCHEMA dvdrental TO dvdrental_user;<br><br>ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA dvdrental<br>  GRANT SELECT ON TABLES TO dvdrental_user;</pre><p>Do not grant table DML, sequence use, generator execution, REPLICATION, replication-origin functions, or pglogical catalog access to the target application user.</p><p>Run the empty-target assertion in dvdrental_lab:</p><pre>DO $empty_check$<br>DECLARE<br>  r record;<br>  v_table_count integer;<br>  v_has_rows boolean;<br>BEGIN<br>  SELECT count(*)<br>  INTO v_table_count<br>  FROM pg_class c<br>  JOIN pg_namespace n ON n.oid = c.relnamespace<br>  WHERE n.nspname = &#39;dvdrental&#39;<br>    AND c.relkind IN (&#39;r&#39;, &#39;p&#39;);<br><br>  IF v_table_count &lt;&gt; 15 THEN<br>    RAISE EXCEPTION &#39;Expected 15 dvdrental tables, found %&#39;, v_table_count;<br>  END IF;<br><br>  FOR r IN<br>    SELECT n.nspname, c.relname<br>    FROM pg_class c<br>    JOIN pg_namespace n ON n.oid = c.relnamespace<br>    WHERE n.nspname = &#39;dvdrental&#39;<br>      AND c.relkind IN (&#39;r&#39;, &#39;p&#39;)<br>    ORDER BY c.relname<br>  LOOP<br>    EXECUTE format(<br>      &#39;SELECT EXISTS (SELECT 1 FROM %I.%I LIMIT 1)&#39;,<br>      r.nspname,<br>      r.relname<br>    ) INTO v_has_rows;<br><br>    IF v_has_rows THEN<br>      RAISE EXCEPTION &#39;Target table %.% is not empty&#39;, r.nspname, r.relname;<br>    END IF;<br>  END LOOP;<br>END<br>$empty_check$;</pre><p>Expected: DO completes without an exception.</p><h4><strong>6. Schema-only setup on the Singapore target</strong></h4><pre>psql -X \<br>  &quot;host=primary.bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1 \<br>  -f /home/opc/00_create_schema.sql<br><br>psql -X \<br>  &quot;host=primary.bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1<br><br>GRANT CONNECT ON DATABASE dvdrental_lab TO dvdrental_user;<br>GRANT USAGE ON SCHEMA dvdrental TO dvdrental_user;<br>GRANT SELECT ON ALL TABLES IN SCHEMA dvdrental TO dvdrental_user;<br><br>ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA dvdrental<br> GRANT SELECT ON TABLES TO dvdrental_user;</pre><p>Apply the same read-only grants and the same 15-table empty-target assertion from section 5.</p><p><strong>Expected: all 15 tables exist and all are empty.</strong></p><h4>7. Some Important Assumptions</h4><ul><li><strong>pglogical runs in the application database.</strong> We install the extension in dvdrental_lab not in the postgres maintenance database.</li><li><strong>The OCI administrator owns the replication workflow.</strong> In this managed-service design, pglogical copy and apply need privileges such as changing session_replication_role. PostgreSQL restricts that setting to superusers or roles with the appropriate SET privilege.</li><li><strong>The application login is not the replication identity. </strong>dvdrental_user remains NOREPLICATION and receives no access to pglogical catalogs.</li><li><strong>Only Sydney receives generated data.</strong> Melbourne and Singapore get the same schema but remain empty until pglogical performs the initial copy.</li><li><strong>Schema and data loading are separate artifacts.</strong> This prevents a target operator from accidentally generating an independent dataset.</li><li><strong>Existing-object and future-object privileges are handled separately.</strong> ALTER DEFAULT PRIVILEGES affects future objects; it does not update tables or sequences that already exist.</li><li><strong>Subscribers are initialized sequentially.</strong> Melbourne must be healthy before Singapore begins. This reduces worker pressure and makes failures easier to isolate.</li><li><strong>Monitoring queries never print stored DSNs.</strong> A pglogical provider DSN contains credentials, so I project only safe status columns.</li></ul><p><a href="https://docs.oracle.com/en/learn/oci-pglogical-extension/index.html">OCI pglogical guide</a> and <a href="https://github.com/2ndQuadrant/pglogical">upstream pglogical documentation</a> are useful companion references.</p><h3>pglogical Setup</h3><h4><strong>1. Create the Sydney provider</strong></h4><p>Connect to Sydney dvdrental_lab as postgres.</p><blockquote>pglogical persists working DSNs in protected catalogs. Use a controlled interactive psql session, disable its history before entering a real DSN, and do not save a substituted copy of this template:</blockquote><pre>\set HISTFILE /dev/null<br><br>SELECT pglogical.create_node(<br>  node_name := &#39;publication-syd&#39;,<br>  dsn := &#39;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres password=YourPassword1234## sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem connect_timeout=60&#39;<br>);<br><br>SELECT pglogical.replication_set_add_all_tables(<br>  &#39;default&#39;,<br>  ARRAY[&#39;dvdrental&#39;],<br>  true<br>);<br><br>SELECT pglogical.replication_set_add_all_sequences(<br>  &#39;default&#39;,<br>  ARRAY[&#39;dvdrental&#39;],<br>  true<br>);</pre><blockquote>A real password must be escaped for both the surrounding SQL literal and libpq keyword/value syntax. Do not blindly paste a password containing quotes, backslashes, or spaces into the template.</blockquote><p>Verify membership without printing DSNs:</p><pre>SELECT count(*) AS table_members<br>FROM pglogical.replication_set_table rst<br>JOIN pglogical.replication_set rs USING (set_id)<br>WHERE rs.set_name = &#39;default&#39;;<br><br>SELECT count(*) AS sequence_members<br>FROM pglogical.replication_set_seq rss<br>JOIN pglogical.replication_set rs USING (set_id)<br>WHERE rs.set_name = &#39;default&#39;;</pre><p>Expected: <strong>15 tables</strong> and <strong>13 sequences.</strong></p><h4>2. <strong>Create and finish Melbourne Node and Subscriber Setup before starting Singapore</strong></h4><p>Connect to Melbourne dvdrental_lab as postgres:</p><pre>\set HISTFILE /dev/null<br><br>SELECT pglogical.create_node(<br>  node_name := &#39;subscriber-mel&#39;,<br>  dsn := &#39;host=primary.mniso4nt6sfnz3de4ysxauopiel5hq.postgresql.ap-melbourne-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres password=YourPassword1234## sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem connect_timeout=60&#39;<br>);<br><br>SELECT pglogical.create_subscription(<br>  subscription_name := &#39;subscription_mel&#39;,<br>  provider_dsn := &#39;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres password=YourPassword1234## sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem connect_timeout=60&#39;,<br>  replication_sets := ARRAY[&#39;default&#39;],<br>  synchronize_structure := false,<br>  synchronize_data := true,<br>  forward_origins := ARRAY[]::text[]<br>);</pre><p>Poll the following safe status query:</p><pre>SELECT<br>  subscription_name,<br>  status,<br>  provider_node,<br>  slot_name,<br>  replication_sets<br>FROM pglogical.show_subscription_status();</pre><blockquote>Never use SELECT * here; provider_dsn contains the stored password.</blockquote><p>Check sync state:</p><pre>SELECT<br>  sync_kind,<br>  sync_nspname,<br>  sync_relname,<br>  sync_status,<br>  sync_statuslsn<br>FROM pglogical.local_sync_status<br>ORDER BY sync_kind, sync_nspname, sync_relname;<br><br>SELECT sync_status, count(*)<br>FROM pglogical.local_sync_status<br>GROUP BY sync_status<br>ORDER BY sync_status;</pre><p>Melbourne is accepted only when:</p><ul><li>subscription_mel is replicating;</li><li>local_sync_status has 16 rows total — one database-level row and 15 table rows — and every row is r;</li><li>the exact row counts match Sydney;</li><li>Sydney shows one active pglogical slot and one streaming sender for MEL.</li></ul><blockquote>pglogical.wait_for_subscription_sync_complete(…) is only an observer. A statement_timeout on that function does not control or repair the background sync.</blockquote><blockquote>Do not continue to section 9 until all Melbourne acceptance checks pass.</blockquote><h4>3. Create and finish Singapore Node and Subscriber Setup</h4><p>After Melbourne passes, connect to Singapore dvdrental_lab as postgres:</p><pre>\set HISTFILE /dev/null<br><br>SELECT pglogical.create_node(<br>  node_name := &#39;subscriber-sgp&#39;,<br>  dsn := &#39;host=primary.bts5ctx56gtoofohtkqcc33dinc2uq.postgresql.ap-singapore-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres password=YourPassword1234## sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem connect_timeout=60&#39;<br>);<br><br>SELECT pglogical.create_subscription(<br>  subscription_name := &#39;subscription_sgp&#39;,<br>  provider_dsn := &#39;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres password=YourPassword1234## sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem connect_timeout=60&#39;,<br>  replication_sets := ARRAY[&#39;default&#39;],<br>  synchronize_structure := false,<br>  synchronize_data := true,<br>  forward_origins := ARRAY[]::text[]<br>);</pre><p>Use the same safe status and sync-state queries from section 8.</p><p>Singapore is accepted only when subscription_sgp is replicating, all 16 sync rows are r, counts match Sydney, and Sydney now shows two active pglogical slots and two streaming senders.</p><h4>4. <strong>Full Replication topology validation</strong></h4><p><strong>4.1 Provider slots and senders : Run on Sydney</strong></p><pre>SELECT<br>  slot_name,<br>  plugin,<br>  database,<br>  active,<br>  active_pid,<br>  restart_lsn,<br>  confirmed_flush_lsn,<br>  pg_size_pretty(<br>    pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)<br>  ) AS confirmed_gap,<br>  pg_size_pretty(<br>    pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)<br>  ) AS retained_wal<br>FROM pg_replication_slots<br>WHERE slot_name LIKE &#39;pgl\_%&#39; ESCAPE &#39;\&#39;<br>  AND plugin = &#39;pglogical_output&#39;<br>  AND database = &#39;dvdrental_lab&#39;<br>ORDER BY slot_name;<br><br>SELECT<br>  application_name,<br>  usename,<br>  client_addr,<br>  state,<br>  sent_lsn,<br>  write_lsn,<br>  flush_lsn,<br>  replay_lsn,<br>  write_lag,<br>  flush_lag,<br>  replay_lag<br>FROM pg_stat_replication<br>WHERE application_name IN (&#39;subscription_mel&#39;, &#39;subscription_sgp&#39;)<br>ORDER BY application_name;</pre><blockquote>Expected steady state: two active pglogical_output slots and two streaming sender rows.</blockquote><p><strong>4.2 Exact row counts : Run on all three databases</strong></p><pre>SELECT &#39;actor&#39; AS table_name, count(*) AS rows FROM dvdrental.actor<br>UNION ALL SELECT &#39;address&#39;, count(*) FROM dvdrental.address<br>UNION ALL SELECT &#39;category&#39;, count(*) FROM dvdrental.category<br>UNION ALL SELECT &#39;city&#39;, count(*) FROM dvdrental.city<br>UNION ALL SELECT &#39;country&#39;, count(*) FROM dvdrental.country<br>UNION ALL SELECT &#39;customer&#39;, count(*) FROM dvdrental.customer<br>UNION ALL SELECT &#39;film&#39;, count(*) FROM dvdrental.film<br>UNION ALL SELECT &#39;film_actor&#39;, count(*) FROM dvdrental.film_actor<br>UNION ALL SELECT &#39;film_category&#39;, count(*) FROM dvdrental.film_category<br>UNION ALL SELECT &#39;inventory&#39;, count(*) FROM dvdrental.inventory<br>UNION ALL SELECT &#39;language&#39;, count(*) FROM dvdrental.language<br>UNION ALL SELECT &#39;payment&#39;, count(*) FROM dvdrental.payment<br>UNION ALL SELECT &#39;rental&#39;, count(*) FROM dvdrental.rental<br>UNION ALL SELECT &#39;staff&#39;, count(*) FROM dvdrental.staff<br>UNION ALL SELECT &#39;store&#39;, count(*) FROM dvdrental.store<br>ORDER BY table_name;</pre><p>Expected after the <strong>first rows=100,000 </strong>load</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/558/1*vtuyiow-x7xA3h4xJ9gzMA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/546/1*g8stACmGuy_pS4lL0htqdw.png" /></figure><p><strong>4.3 Self-cleaning streaming test</strong></p><p>Choose an unused negative bigint and a short unique change ID. First confirm neither value exists:</p><pre>SELECT country_id, country<br>FROM dvdrental.country<br>WHERE country_id = &lt;UNUSED_NEGATIVE_BIGINT&gt;<br>   OR country = &#39;__pglogical_probe_&lt;SHORT_CHANGE_ID&gt;__&#39;;</pre><p>Expected: <strong>zero</strong> rows.</p><p>Insert on Sydney:</p><pre>INSERT INTO dvdrental.country(country_id, country)<br>VALUES (&lt;UNUSED_NEGATIVE_BIGINT&gt;, &#39;__pglogical_probe_&lt;SHORT_CHANGE_ID&gt;__&#39;)<br>RETURNING country_id;</pre><p>Confirm it appears on MEL and SGP, then delete it on Sydney:</p><pre>DELETE FROM dvdrental.country<br>WHERE country_id = &lt;UNUSED_NEGATIVE_BIGINT&gt;<br>  AND country = &#39;__pglogical_probe_&lt;SHORT_CHANGE_ID&gt;__&#39;;</pre><p>Confirm the delete reaches both targets. A negative key prevents the probe from advancing the normal positive sequence.</p><h4><strong>5. Loading another generated batch on Sydney</strong></h4><p>Run only on Sydney and as the generator owner:</p><pre>psql -X \<br>  &quot;host=primary.7gtysdaouav7agdgmh3mdeekdz3fwq.postgresql.ap-sydney-1.oci.oraclecloud.com port=5432 dbname=dvdrental_lab user=postgres sslmode=require&quot; \<br>  -v ON_ERROR_STOP=1 \<br>  -v rows=10000 \<br>  -f /home/opc/01_load_source.sql</pre><p>The value must be an unsigned canonical integer from 1 through 1,000,000. Repeat the command for larger controlled loads rather than increasing the limit. Monitor both provider slots during the load and verify the new counts on both targets.</p><h4><strong>6. Stop and restart one subscription</strong></h4><p>Run only on the relevant target.</p><pre>-- On MEL:<br>SELECT pglogical.alter_subscription_disable(&#39;subscription_mel&#39;, true);<br>SELECT pglogical.alter_subscription_enable(&#39;subscription_mel&#39;, true);<br><br>-- On SGP:<br>SELECT pglogical.alter_subscription_disable(&#39;subscription_sgp&#39;, true);<br>SELECT pglogical.alter_subscription_enable(&#39;subscription_sgp&#39;, true);</pre><p>After enabling, verify replicating and check slot lag. Disabling a subscription does not make its target writable and does not remove the provider slot; prolonged disablement can retain WAL on Sydney.</p><h4><strong>7. Recover a failed initial sync stuck at down /d</strong></h4><p>Use this only when all are true:</p><ul><li>subscription status is down;</li><li>local_sync_status is stuck at d;</li><li>there is no matching copy/apply initialization worker;</li><li>pg_stat_progress_copy shows no copy for the subscription;</li><li>the target data is disposable, or an approved backup exists.</li></ul><p>If target data is valuable, stop and reconcile it or build a new subscriber database. Do not truncate it.</p><p><strong>7.1 Prove that initialization is abandoned</strong></p><p>Run on the affected target:</p><pre>SELECT subscription_name, status, slot_name<br>FROM pglogical.show_subscription_status()<br>WHERE subscription_name = &#39;&lt;SUBSCRIPTION_NAME&gt;&#39;;<br><br>SELECT<br>  pid,<br>  usename,<br>  application_name,<br>  backend_type,<br>  state,<br>  wait_event_type,<br>  wait_event<br>FROM pg_stat_activity<br>WHERE datname = &#39;dvdrental_lab&#39;<br>  AND (<br>    application_name LIKE &#39;&lt;SUBSCRIPTION_NAME&gt;%&#39;<br>    OR application_name ILIKE &#39;pglogical apply%&#39;<br>    OR backend_type ILIKE &#39;pglogical apply%&#39;<br>    OR backend_type ILIKE &#39;pglogical sync%&#39;<br>  )<br>ORDER BY pid;<br><br>SELECT * FROM pg_stat_progress_copy;</pre><p>Record the exact slot name before dropping metadata.</p><p><strong>7.2 Drop failed target metadata</strong></p><p>Run on the affected target:</p><pre>SELECT pglogical.drop_subscription(&#39;&lt;SUBSCRIPTION_NAME&gt;&#39;, true);<br><br>SELECT subscription_name, status<br>FROM pglogical.show_subscription_status()<br>WHERE subscription_name = &#39;&lt;SUBSCRIPTION_NAME&gt;&#39;;<br><br>SELECT pid, application_name, backend_type<br>FROM pg_stat_activity<br>WHERE datname = &#39;dvdrental_lab&#39;<br>  AND (<br>    application_name LIKE &#39;&lt;SUBSCRIPTION_NAME&gt;%&#39;<br>    OR backend_type ILIKE &#39;pglogical apply%&#39;<br>    OR backend_type ILIKE &#39;pglogical sync%&#39;<br>  );<br><br>SELECT pglogical.drop_node(&#39;&lt;SUBSCRIBER_NODE_NAME&gt;&#39;, true);</pre><blockquote>The two post-condition queries must return zero rows before continuing. The `true` argument means “if exists”; it is not a force flag.</blockquote><p><strong>7.3 Check the exact provider slot</strong></p><p>Run on Sydney:</p><pre>SELECT slot_name, active, active_pid<br>FROM pg_replication_slots<br>WHERE slot_name = &#39;&lt;EXACT_SLOT_NAME&gt;&#39;;</pre><p>Normally drop_subscription removes the remote slot. If the exact slot remains, drop it only after the subscription is absent and active = false:</p><pre>SELECT pg_drop_replication_slot(&#39;&lt;EXACT_SLOT_NAME&gt;&#39;);</pre><blockquote>Never create a pglogical slot manually.</blockquote><p><strong>7.4 Empty only the approved disposable target</strong></p><p>Run on the affected target only:</p><pre>TRUNCATE TABLE<br>  dvdrental.payment,<br>  dvdrental.rental,<br>  dvdrental.inventory,<br>  dvdrental.film_actor,<br>  dvdrental.film_category,<br>  dvdrental.customer,<br>  dvdrental.staff,<br>  dvdrental.store,<br>  dvdrental.film,<br>  dvdrental.actor,<br>  dvdrental.category,<br>  dvdrental.language,<br>  dvdrental.address,<br>  dvdrental.city,<br>  dvdrental.country<br>RESTART IDENTITY;</pre><p>Re-run the empty-target assertion, then recreate that target node and subscription. Complete and validate it before making any change to the other subscriber.</p><h4><strong>8. Planned promotion and sequence safety</strong></h4><p>Sequence replication is not a guarantee that every target last_value always equals Sydney. PostgreSQL sequences are non-transactional and may cache or advance values.</p><p><strong>Before a planned promotion:</strong></p><ol><li>Fence all writes to Sydney.</li></ol><p>2. Wait on Sydney for every pglogical slot:</p><pre>BEGIN; <br>SET LOCAL statement_timeout = &#39;30min&#39;; <br>SELECT pglogical.wait_slot_confirm_lsn(NULL, NULL); <br>COMMIT;</pre><p>3. If the wait times out, roll back, diagnose the lagging subscriber, and abort the cutover.</p><p>4. Confirm both slots are active and caught up.</p><p>5. Stop subscriber apply according to the cutover plan.</p><p>6. With both application writes and apply fenced, advance any sequence whose next value could collide with stored keys:</p><pre>DO $block$<br>DECLARE<br>r record;<br>v_max bigint;<br>v_last bigint;<br>v_is_called boolean;<br>BEGIN<br>FOR r IN<br>SELECT<br>n.nspname AS schema_name,<br>c.relname AS table_name,<br>a.attname AS column_name,<br>pg_get_serial_sequence(<br>format(&#39;%I.%I&#39;, n.nspname, c.relname),<br>a.attname<br>) AS sequence_name<br>FROM pg_class c<br>JOIN pg_namespace n ON n.oid = c.relnamespace<br>JOIN pg_attribute a ON a.attrelid = c.oid<br>WHERE n.nspname = &#39;dvdrental&#39;<br>AND c.relkind IN (&#39;r&#39;, &#39;p&#39;)<br>AND a.attnum &gt; 0<br>AND NOT a.attisdropped<br>AND pg_get_serial_sequence(<br>format(&#39;%I.%I&#39;, n.nspname, c.relname),<br>a.attname<br>) IS NOT NULL<br>LOOP<br>EXECUTE format(<br>&#39;SELECT max(%I)::bigint FROM %I.%I&#39;,<br>r.column_name,<br>r.schema_name,<br>r.table_name<br>) INTO v_max;<br>EXECUTE format( &#39;SELECT last_value::bigint, is_called FROM %s&#39;, r.sequence_name ) INTO v_last, v_is_called; IF v_max IS NOT NULL AND ( v_last &lt; v_max OR (v_last = v_max AND NOT v_is_called) ) THEN PERFORM setval(r.sequence_name::regclass, v_max, true); END IF; END LOOP;<br>END<br>$block$;</pre><p>7. Enable required application DML only on the promoted target.</p><p>8. Redirect the application and keep the old source write-fenced until a reverse topology is deliberately designed.</p><p>9. setval is not transactional. Test the promotion procedure in a clone before production use.</p><h4><strong>9. DDL and new tables</strong></h4><p>pglogical does not automatically replicate arbitrary DDL or auto-enroll future objects.</p><p>For a new table:</p><ol><li>Apply compatible DDL on MEL and SGP first.</li><li>Apply it on Sydney.</li><li>Apply explicit current-object grants and correct owner-scoped default privileges.</li></ol><p>4. Add the table on Sydney:</p><pre>SELECT pglogical.replication_set_add_table( &#39;default&#39;, &#39;dvdrental.&lt;NEW_TABLE&gt;&#39;::regclass, true );</pre><p>5. Add its sequence separately, if present:</p><pre>SELECT pglogical.replication_set_add_sequence( &#39;default&#39;, &#39;dvdrental.&lt;NEW_SEQUENCE&gt;&#39;::regclass, true );</pre><p>6. Wait for both provider slots on Sydney:</p><pre>BEGIN; <br>SET LOCAL statement_timeout = &#39;30min&#39;; <br>SELECT pglogical.wait_slot_confirm_lsn(NULL, NULL); <br>COMMIT;</pre><p>7. Wait for the new table separately on MEL and SGP, using bounded timeouts. Run on MEL only:</p><pre>BEGIN; <br>SET LOCAL statement_timeout = &#39;30min&#39;;<br>SELECT pglogical.wait_for_table_sync_complete( &#39;subscription_mel&#39;, &#39;dvdrental.&lt;NEW_TABLE&gt;&#39;::regclass );<br>COMMIT;</pre><p>8. Run in a separate session on SGP only:</p><pre>BEGIN; <br>SET LOCAL statement_timeout = &#39;30min&#39;; <br>SELECT pglogical.wait_for_table_sync_complete( &#39;subscription_sgp&#39;, &#39;dvdrental.&lt;NEW_TABLE&gt;&#39;::regclass ); <br>COMMIT;</pre><p>If either wait times out, roll back that session, inspect local_sync_status, and treat the DDL deployment gate as failed.</p><p>All replicated tables need a suitable primary key or replica identity for UPDATE and DELETE. The current 15 tables have primary keys.</p><h4><strong>10. Credential and privilege rules</strong></h4><ol><li>Use separate strong secrets for the administrator and application login.</li><li>Keep dvdrental_user NOREPLICATION.</li><li>Do not grant the application role access to pglogical catalogs or the replication-origin functions.</li><li>Do not log or share full pglogical DSNs.</li><li>Use the projected status query; never SELECT * from show_subscription_status().</li></ol><h4><strong>11. Troubleshooting Guide</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qEa7_5Mw1w7JyuoxKWIT6A.png" /></figure><p>When diagnosing a target, use:</p><pre>SELECT<br>  subscription_name,<br>  status,<br>  provider_node,<br>  slot_name,<br>  replication_sets<br>FROM pglogical.show_subscription_status();<br><br>SELECT<br>  sync_kind,<br>  sync_nspname,<br>  sync_relname,<br>  sync_status,<br>  sync_statuslsn<br>FROM pglogical.local_sync_status<br>ORDER BY sync_kind, sync_nspname, sync_relname;<br><br>SELECT pid, application_name, backend_type, state, wait_event_type, wait_event<br>FROM pg_stat_activity<br>WHERE datname = &#39;dvdrental_lab&#39;<br>ORDER BY pid;<br><br>SELECT * FROM pg_stat_progress_copy;</pre><blockquote>In OCI Logging, search the current DB-system log resource for `pglogical`, subscription name, `permission denied`, `session_replication_role`, `table copy failed`, `could not connect`, certificate failures, and background-worker errors.</blockquote><h4><strong>12. Controlled teardown to remove the pglogical setup</strong></h4><p>Remove subscribers before the provider. Run each block in a separately verified regional session.</p><pre>-- On MEL:<br>SELECT pglogical.alter_subscription_disable(&#39;subscription_mel&#39;, true);<br>SELECT pglogical.drop_subscription(&#39;subscription_mel&#39;, true);<br>SELECT pglogical.drop_node(&#39;subscriber-mel&#39;, true);<br><br>-- On SGP:<br>SELECT pglogical.alter_subscription_disable(&#39;subscription_sgp&#39;, true);<br>SELECT pglogical.drop_subscription(&#39;subscription_sgp&#39;, true);<br>SELECT pglogical.drop_node(&#39;subscriber-sgp&#39;, true);<br><br>-- On Sydney, first verify no pglogical slots remain:<br>SELECT slot_name, active<br>FROM pg_replication_slots<br>WHERE slot_name LIKE &#39;pgl\_%&#39; ESCAPE &#39;\&#39;<br>  AND plugin = &#39;pglogical_output&#39;<br>  AND database = &#39;dvdrental_lab&#39;;<br><br>-- Only when the topology is intentionally being dismantled:<br>SELECT pglogical.drop_node(&#39;publication-syd&#39;, true);<br><br>-- Do not drop nodes in an unlabeled all-regions script; connection context and order matter.</pre><h3>Appendix</h3><h3><strong>A. References</strong></h3><ul><li><a href="https://docs.oracle.com/en/learn/oci-pglogical-extension/index.html">Oracle: Cross-Region Synchronization of OCI Database with PostgreSQL using pglogical</a></li><li><a href="https://docs.oracle.com/en/learn/ocipg-dr-pglogical-sqn/index.html">Oracle: Manage Sequences in pglogical Replication for DR</a></li><li><a href="https://www.postgresql.org/docs/16/app-psql.html">PostgreSQL 16: psql</a></li><li><a href="https://www.postgresql.org/docs/16/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE">PostgreSQL 16: session_replication_role</a></li><li><a href="https://www.postgresql.org/docs/16/sql-alterdefaultprivileges.html">PostgreSQL 16: ALTER DEFAULT PRIVILEGES</a></li><li><a href="https://github.com/2ndQuadrant/pglogical">pglogical upstream documentation</a></li></ul><h3>B. Code</h3><h4>00_create_schema.sql</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1db83ee0ecea133eeee70f59872a43fc/href">https://medium.com/media/1db83ee0ecea133eeee70f59872a43fc/href</a></iframe><h4>01_load_source.sql</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fa684f096204997712e47d9852c59197/href">https://medium.com/media/fa684f096204997712e47d9852c59197/href</a></iframe><h3>C. DVDRental Entity Relationship Diagram</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PGOeUXtknzzB9OjxPIJd9w.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=23fdf5c024fe" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Production-Grade Containers for Oracle GoldenGate 26ai for Migrations]]></title>
            <link>https://shadabshaukat.medium.com/building-production-grade-containers-for-oracle-goldengate-26ai-for-migrations-0a11790a03d5?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/0a11790a03d5</guid>
            <category><![CDATA[containers]]></category>
            <category><![CDATA[golden-gate]]></category>
            <category><![CDATA[oracle]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[change-data-capture]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Fri, 21 Aug 2026 07:03:59 GMT</pubDate>
            <atom:updated>2026-09-18T14:56:42.215Z</atom:updated>
            <content:encoded><![CDATA[<p>Oracle GoldenGate remains one of the most widely used technologies for real-time data replication, zero-downtime database migrations, and heterogeneous data integration. With <strong>Oracle GoldenGate 26ai</strong>, containerization makes it considerably easier to create repeatable GoldenGate environments for migration testing, development, proof-of-concepts, and controlled production deployments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BqKHgB6iLwBg8hQAtG7uRA.png" /></figure><p>In this post, I will walk you through a Docker-based (Podman can also be used) for Oracle GoldenGate 26ai deployment which :</p><ul><li>Loads the Oracle GoldenGate container image</li><li>Runs a quick validation container</li><li>Builds a persistent Docker filesystem layout</li><li>Keeps credentials out of the docker run command</li><li>Starts GoldenGate with operationally useful Docker options</li><li>Locates the trail files</li><li>Accesses the container</li><li>Checks GoldenGate Microservices inside the container</li><li>Connects to the Admin Client</li><li>Inspects trails with Logdump</li><li>Monitors trail growth from the Docker host</li><li>Useful troubleshooting and operational commands</li></ul><p>The examples use the Oracle GoldenGate 26ai container image. This image is for migrations and can be downloaded using OCI Marketplace image. Please see section <em>Oracle GoldenGate on Podman </em>in the below document :</p><p>Ref : <a href="https://docs.oracle.com/en/database/oracle/zero-downtime-migration/21.5/zdmea/#ZDM%E2%80%93LogicalOnlineMigrationtoADBDonOracleDatabase@AWS-OracleGoldenGateonPodman">https://docs.oracle.com/en/database/oracle/zero-downtime-migration/21.5/zdmea/#ZDM%E2%80%93LogicalOnlineMigrationtoADBDonOracleDatabase@AWS-OracleGoldenGateonPodman</a></p><blockquote><strong><em>Security note:</em></strong><em> All passwords, IP addresses, and similar values shown in this article should be treated as examples. Use your organization’s secret-management and certificate-management standards for real environments.</em></blockquote><h4>A Typical Goldengate flow for Source to Target Database Replication:</h4><pre>Source Database<br>      |<br>      v<br>   Extract (Transaction Capture from Source)<br>      |<br>      v<br> Local Trail (Transactions stored)<br>      |<br>      v<br>Distribution Path (if Heterogenous or Multiple goldengate deployments)<br>      |<br>      v<br> Remote Trail (Transactions stored)<br>      |<br>      v<br>   Replicat (Transaction Apply to Target)<br>      |<br>      v<br>Target Database</pre><h3>Architecture at a Glance</h3><p>The deployment we are building looks conceptually like this:</p><pre>Docker Host<br>│<br>├── $HOME/goldengate/26ai/<br>│   │<br>│   ├── u02/  ────────────────&gt; /u02<br>│   │                            Persistent GoldenGate data<br>│   │                            Deployments, trails, configuration<br>│   │<br>│   ├── u03/  ────────────────&gt; /u03<br>│   │                            Temporary GoldenGate data<br>│   │<br>│   └── cert/ ────────────────&gt; /etc/nginx/cert<br>│                                HTTPS certificate material<br>│<br>└── Docker Container: ogg26ai<br>    │<br>    ├── /u01/ogg               GoldenGate binaries<br>    ├── /u02                   Persistent deployment data<br>    ├── /u03                   Temporary data<br>    │<br>    └── GoldenGate Microservices<br>        ├── Administration Server<br>        ├── Distribution Server<br>        ├── Receiver Server<br>        └── Performance Metrics Server</pre><p>That separation becomes particularly important during database migrations, where trail continuity and recoverability matter considerably more than the lifecycle of an individual Docker container.</p><pre>                       [~~ Docker Host ~~]<br>                              |<br>                +-------------+-------------+<br>                |             |             |<br>                v             v             v<br>              /u02          /u03          cert<br>           Persistent      Temporary       TLS<br>                |<br>                v<br>   +---------------------------+<br>   | Oracle GoldenGate 26ai    |<br>   | Docker Container          |<br>   |                           |<br>   | /u01/ogg -&gt; Software      |<br>   | /u02     -&gt; Deployment    |<br>   | /u03     -&gt; Temporary     |<br>   +---------------------------+<br>                |<br>                v<br>   +---------------------------+<br>   | GoldenGate Microservices  |<br>   |                           |<br>   | Administration Server     |<br>   | Distribution Server       |<br>   | Receiver Server           |<br>   | Performance Metrics       |<br>   +---------------------------+<br>                |<br>                v<br>   Extract -&gt; Trail -&gt; Distribution -&gt; Trail -&gt; Replicat</pre><h3>Step 1 — Load the Oracle GoldenGate Container Image</h3><p>After downloading the Oracle GoldenGate container image from OCI Marketplace Image for Database Migrations, load the image into the local Docker repository.</p><p>For example:</p><pre>sudo docker load -i ora23ai-2326300.tar</pre><p>Verify that Docker can see the image:</p><pre>sudo docker images<br><br>REPOSITORY                       TAG                 IMAGE ID       CREATED        SIZE<br>localhost/oracle/goldengate      23.26.3.0.0         5bb91f52a79a   4 weeks ago    3.1GB</pre><p>At this point, Docker has the image locally and we can perform an initial validation.</p><h3>Step 2 — Perform a Quick Container Validation</h3><p>Before building the persistent configuration, it can be useful to verify that the image starts successfully.</p><p>A minimal invocation looks like:</p><pre>docker run -p 443:443 \<br>  -e OGG_ADMIN=oggadmin \<br>  -e OGG_ADMIN_PWD=&#39;&lt;temporary-password&gt;&#39; \<br>  localhost/oracle/goldengate:23.26.3.0.0</pre><p>This is useful for confirming that:</p><ul><li>The image starts</li><li>GoldenGate initializes</li><li>HTTPS is exposed</li><li>The administration account works</li><li>The host can reach the GoldenGate UI</li></ul><p>In case you want to use the Oracle Container for registry for the free Goldengate image, you can run the below command directly without downloading the image from the Marketplace image.</p><pre>docker run -p 443:443 \<br>  -e OGG_ADMIN=oggadmin \<br>  -e OGG_ADMIN_PWD=&#39;&lt;temporary-password&gt;&#39; \<br>container-registry.oracle.com/goldengate/goldengate-oracle-free:latest</pre><p>However, <strong>we will not use this as the long-term deployment model</strong>.</p><p>The container lifecycle should not determine the lifecycle of your GoldenGate deployment data. We need persistent storage for that.</p><h3>Step 3 — Create a Persistent GoldenGate Filesystem Layout</h3><p>Create a dedicated directory on the Docker host:</p><pre>~/goldengate/26ai/<br>├── u02/        # Persistent GoldenGate deployments, trails, config and logs<br>├── u03/        # Temporary GoldenGate data<br>└── cert/       # HTTPS certificate material</pre><p>Create the directories:</p><pre>mkdir -p &quot;$HOME/goldengate/26ai/u02&quot;<br>mkdir -p &quot;$HOME/goldengate/26ai/u03&quot;<br>mkdir -p &quot;$HOME/goldengate/26ai/cert&quot;</pre><pre>chmod -R 777 $HOME/goldengate/26ai/u02</pre><pre>chmod -R 777 $HOME/goldengate/26ai/u03</pre><pre>chmod -R 777 $HOME/goldengate/26ai/cert</pre><p>Oracle GoldenGate’s container layout is important to understand:</p><pre>/u01                GoldenGate installation<br>/u02                Persistent GoldenGate data<br>/u03                Temporary GoldenGate data<br>/etc/nginx/cert     HTTPS certificate material</pre><p>The important design decision here is that <strong>don’t bind-mount </strong><strong>/u01</strong>.</p><p>/u01 contains the GoldenGate software delivered by the container image. Keeping it within the image maintains a clean separation between:</p><pre>Immutable software  -&gt; Container image<br>Persistent state    -&gt; Host-mounted storage</pre><p>This also makes container upgrades and replacements considerably easier.</p><h3>Step 4 — Define a Named GoldenGate Deployment</h3><p>Rather than relying entirely on defaults, define the deployment explicitly:</p><pre>export OGG_DEPLOYMENT=Local</pre><p>In this example, the deployment will be called:</p><pre>Local</pre><p>Using an explicit deployment name makes scripting, troubleshooting, and Admin Client connections easier to understand.</p><h3>Step 5 — Keep the Password Out of docker run</h3><p>Putting credentials directly on a docker run command is easy for quick testing but should not be used for a persistent environment.</p><p>Instead, create an environment file:</p><pre>cat &gt; &quot;$HOME/goldengate/26ai/.env&quot; &lt;&lt;&#39;EOF&#39;<br>OGG_ADMIN=oggadmin<br>OGG_ADMIN_PWD=&lt;strong-password&gt;<br>OGG_DEPLOYMENT=Local<br>OGG_DOMAIN=localhost<br>EOF</pre><p>Restrict access:</p><pre>chmod 600 &quot;$HOME/goldengate/26ai/.env&quot;</pre><blockquote><strong>GoldenGate passwords must comply with Oracle’s documented password requirements, including the required combination of uppercase, lowercase, numeric, and permitted special characters.</strong></blockquote><p>For a production environment, I would take this one step further and integrate the deployment with the organization’s established secrets-management mechanism rather than maintaining long-lived credentials in plaintext files.</p><h3>Step 6 — Start the GoldenGate Container</h3><p>Now start the container with persistent storage and some useful operational settings:</p><pre>docker run -d \<br>  --name ogg26ai \<br>  --hostname ogg26ai \<br>  --restart unless-stopped \<br>  -p 443:443 \<br>  --env-file &quot;$HOME/goldengate/26ai/.env&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u02:/u02&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u03:/u03&quot; \<br>  -v &quot;$HOME/goldengate/26ai/cert:/etc/nginx/cert&quot; \<br>  --log-opt max-size=50m \<br>  --log-opt max-file=5 \<br>  localhost/oracle/goldengate:23.26.3.0.0</pre><p>In Case of using the Oracle container registry with free Goldengate image, just run the below :</p><pre>docker run -d \<br>  --name ogg26ai \<br>  --hostname ogg26ai \<br>  --restart unless-stopped \<br>  -p 443:443 \<br>  --env-file &quot;$HOME/goldengate/26ai/.env&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u02:/u02&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u03:/u03&quot; \<br>  -v &quot;$HOME/goldengate/26ai/cert:/etc/nginx/cert&quot; \<br>  --log-opt max-size=50m \<br>  --log-opt max-file=5 \<br>container-registry.oracle.com/goldengate/goldengate-oracle-free:latest</pre><p>If you’re using Podman on RHEL or Oracle Linux, run the command as below :</p><pre>docker run -d \<br>  --name ogg26ai \<br>  --hostname ogg26ai \<br>  --restart unless-stopped \<br>  -p 8443:443 \<br>  --env-file &quot;$HOME/goldengate/26ai/.env&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u02:/u02:z&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u03:/u03:z&quot; \<br>  -v &quot;$HOME/goldengate/26ai/cert:/etc/nginx/cert:z&quot; \<br>  --log-opt max-size=50m \<br>  --log-opt max-file=5 \<br>container-registry.oracle.com/goldengate/goldengate-oracle-free:latest</pre><h3>Step 7 — Verify the Container</h3><p>First verify that the container is running:</p><pre>docker ps --filter name=ogg26ai</pre><p>Check the most recent logs:</p><pre>docker logs --tail 100 ogg26ai</pre><p>To follow startup or runtime messages continuously:</p><pre>docker logs -f ogg26ai</pre><p>Next, verify the actual Docker mounts:</p><pre>docker inspect ogg26ai \<br>  --format &#39;{{range .Mounts}}{{println .Source &quot;-&gt;&quot; .Destination}}{{end}}&#39;</pre><p>On my test host, the result looked similar to:</p><pre>/home/ec2-user/goldengate/26ai/u02 -&gt; /u02<br>/home/ec2-user/goldengate/26ai/u03 -&gt; /u03<br>/home/ec2-user/goldengate/26ai/cert -&gt; /etc/nginx/cert<br>/var/lib/docker/volumes/&lt;volume-id&gt;/_data -&gt; /u01/ogg/scripts</pre><p>This is an important validation.</p><p>Do not assume that Docker mounted what you intended. Verify it.</p><p>For migration infrastructure, knowing exactly which host filesystem contains your GoldenGate state is critical.</p><h3>Step 8 — Enter the GoldenGate Container</h3><p>Open an interactive shell:</p><pre>docker exec -it ogg26ai bash</pre><p>Check the runtime identity and working directory:</p><pre>whoami<br>pwd</pre><p>Now inspect the GoldenGate-related environment:</p><pre>env | sort | grep -E &#39;^(OGG|DEPLOYMENT|TNS)&#39;</pre><p>In my environment, this returned:</p><pre>OGG_ADMIN=oggadmin<br>OGG_ADMIN_PWD=&lt;redacted&gt;<br>OGG_DEPLOYMENT=Local<br>OGG_DEPLOYMENT_HOME=/u02<br>OGG_DEPLOYMENT_SCRIPTS=/u01/ogg/scripts<br>OGG_DOMAIN=localhost<br>OGG_HOME=/u01/ogg<br>OGG_TEMPORARY_FILES=/u03</pre><p>This tells us several useful things immediately:</p><pre>GoldenGate software     -&gt; /u01/ogg<br>Deployment root         -&gt; /u02<br>Deployment scripts      -&gt; /u01/ogg/scripts<br>Temporary files         -&gt; /u03<br>Deployment              -&gt; Local</pre><p>You can also inspect the standard GoldenGate variables individually:</p><pre>echo &quot;$OGG_HOME&quot;<br>echo &quot;$OGG_ETC_HOME&quot;<br>echo &quot;$OGG_VAR_HOME&quot;<br>echo &quot;$OGG_DATA_HOME&quot;<br>echo &quot;$OGG_SSL_HOME&quot;</pre><p>For example:</p><pre>echo &quot;$OGG_HOME&quot;</pre><p>returns:</p><pre>/u01/ogg</pre><p>Oracle documents the GoldenGate deployment directories and the role of the Data Home here:</p><p><a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/deployment-directories.html">https://docs.oracle.com/en/database/goldengate/core/26/coredoc/deployment-directories.html</a></p><h3>Step 9 — Locate the GoldenGate Trail Files</h3><p>Trail location is one of the first things I want to establish when troubleshooting any GoldenGate environment.</p><p>Inside the container, search for deployment data directories:</p><pre>find /u02 -maxdepth 6 -type d -name data -print</pre><p>Then list files underneath GoldenGate data directories:</p><pre>find /u02 -maxdepth 8 -path &#39;*/var/lib/data/*&#39; -type f -print</pre><p>Another useful exploratory command is:</p><pre>find /u02 -type f \( -name &#39;*000000*&#39; -o -name &#39;??*&#39; \) 2&gt;/dev/null | head -100</pre><p>A deployment will conceptually resemble:</p><pre>/u02/Deployment/<br>├── etc/<br>│   ├── conf/<br>│   └── ssl/<br>└── var/<br>    └── lib/<br>        ├── data/<br>        │   ├── ea000000001<br>        │   ├── ea000000002<br>        │   └── ...<br>        └── archive/</pre><p>The important part is:</p><pre>var/lib/data</pre><p>This is where GoldenGate trail data is stored for the deployment.</p><p>From an operational perspective, I want to be able to answer three questions immediately:</p><ol><li>Which Extract is writing the trail?</li><li>Where is that trail physically stored?</li><li>Which downstream process has consumed it?</li></ol><p>Those three questions resolve a surprising percentage of GoldenGate migration troubleshooting scenarios.</p><h3>Step 10 — Check the GoldenGate Processes</h3><p>Inside the container:</p><pre>ps -ef | grep -i &#39;[o]gg&#39;</pre><p>For the Microservices components:</p><pre>ps -ef | grep -Ei &#39;[s]ervice|[a]dminsrvr|[d]istsrvr|[r]eceiversrvr|[p]msrvr&#39;</pre><p>Also inspect listening ports:</p><pre>ss -lntp</pre><p>With GoldenGate Microservices Architecture, the major services to understand are:</p><pre>Administration Server<br>Distribution Server<br>Receiver Server<br>Performance Metrics Server<br>Service Manager</pre><p>At this stage, we are validating two different layers:</p><pre>Docker says container is running<br>             AND<br>GoldenGate services are actually running</pre><p>A healthy Docker container does not automatically mean every GoldenGate service inside it is healthy. Check &amp; Verify</p><h3>Step 11 — Access GoldenGate from a Browser</h3><p>If running locally:</p><pre>https://localhost</pre><p>If the Docker host is remotely accessible:</p><pre>https://&lt;host-ip-or-dns-name&gt;</pre><p>The reason no additional port is required is our Docker mapping:</p><pre>-p 443:443</pre><p>This maps:</p><pre>Host TCP/443 -&gt; Container TCP/443</pre><p>If the environment uses the default or a self-signed HTTPS certificate, expect the browser to present a certificate warning.</p><p>For anything beyond a lab environment, replace this with certificate material that complies with your organization’s PKI and TLS standards.</p><h3>Step 12 — Connect Using GoldenGate Admin Client</h3><p>For database migration work, Admin Client is one of the most useful tools to become comfortable with.</p><p>Inside the container, locate it:</p><pre>find /u01/ogg -type f -name adminclient -print 2&gt;/dev/null</pre><p>Or:</p><pre>which adminclient</pre><p>Start it:</p><pre>adminclient</pre><p>Connect to the deployment:</p><pre>CONNECT https://localhost:443 DEPLOYMENT Local AS oggadmin !</pre><p>Enter the password when prompted.</p><p>Once connected:</p><pre>INFO ALL</pre><p>In a newly initialized environment, output may resemble:</p><pre>Program     Status      Group       Type                   Lag at Chkpt       Time Since Chkpt<br>ADMINSRVR   RUNNING<br>DISTSRVR    RUNNING<br>PMSRVR      RUNNING<br>RECVSRVR    RUNNING</pre><p>Then inspect Extracts:</p><pre>INFO EXTRACT ALL</pre><p>Replicats:</p><pre>INFO REPLICAT ALL</pre><p>Distribution Paths:</p><pre>INFO DISTPATH ALL</pre><p>For detailed information about an individual Extract:</p><pre>INFO EXTRACT &lt;extract_name&gt;, DETAIL</pre><p>For an individual Replicat:</p><pre>INFO REPLICAT &lt;replicat_name&gt;, DETAIL</pre><p>And for a specific Distribution Path:</p><pre>INFO DISTPATH &lt;path_name&gt;, DETAIL</pre><p>During a migration incident, I generally work through this pipeline in order rather than jumping directly to whichever component happens to report an error.</p><h3>Step 13 — Inspect Trail Records with Logdump</h3><p>Sometimes process status isn’t enough. You need to know what is actually inside the trail. GoldenGate’s <strong>Logdump</strong> utility allows us to inspect trail records directly.</p><p>REF : <a href="https://docs.oracle.com/en/database/goldengate/core/26/logdump-ref/using-logdump-utility.html">https://docs.oracle.com/en/database/goldengate/core/26/logdump-ref/using-logdump-utility.html</a></p><p>Inside the container:</p><pre>which logdump</pre><p>Alternately :</p><pre>find /u01/ogg -type f -name logdump -print 2&gt;/dev/null</pre><p>Start it:</p><pre>logdump</pre><p>Open a trail:</p><pre>open /u02/Deployment/var/lib/data/&lt;trail-file&gt;</pre><p>For example:</p><pre>open /u02/Deployment/var/lib/data/ea000000001</pre><p>Enable useful record information:</p><pre>ghdr on<br>detail on<br>nldetail on</pre><p>Then move through records:</p><pre>next</pre><p>Logdump is particularly useful when investigating questions such as:</p><ul><li>Is Extract actually writing operations into the trail?</li><li>Does the trail contain the expected table?</li><li>What operation type was captured?</li><li>What timestamp does the record carry?</li><li>Is the issue upstream or downstream of trail creation?</li></ul><p>That makes it one of the most valuable low-level troubleshooting utilities in the GoldenGate tool kit.</p><h3>Step 14 — Inspect Deployment Metadata</h3><p>Another useful troubleshooting technique is simply understanding what GoldenGate created beneath /u02.</p><p>Inside the container:</p><pre>find /u02 -maxdepth 4 -type f | sort | head -200</pre><p>Inspect configuration and variable areas:</p><pre>find /u02 -type f | grep -E &#39;/etc/(conf|ssl)|/var/&#39;</pre><p>GoldenGate deployments logically expose locations such as:</p><pre>GoldenGate Home<br>Configuration Home<br>SSL Home<br>Variable Home<br>Data Home<br>Archive Home</pre><p>Knowing these locations is extremely useful when troubleshooting configuration, logging, certificate, and trail-related issues.</p><h3>Step 15 — Inspect Trails Directly from the Docker Host</h3><p>This is where the bind-mounted architecture pays off.</p><p>We don’t need to enter the container simply to inspect the filesystem.</p><p>From the Docker host:</p><pre>find &quot;$HOME/goldengate/26ai/u02&quot; \<br>  -path &#39;*/var/lib/data/*&#39; \<br>  -type f \<br>  -ls</pre><p>Check total persistent storage usage:</p><pre>du -sh &quot;$HOME/goldengate/26ai/u02&quot;</pre><p>On Linux, watch it continuously:</p><pre>watch -n 2 &#39;du -sh &quot;$HOME/goldengate/26ai/u02&quot;&#39;</pre><p>If watch is unavailable, for example on a default macOS installation:</p><pre>while true; do<br>  du -sh &quot;$HOME/goldengate/26ai/u02&quot;<br>  sleep 2<br>done</pre><p>During high-volume migrations, trail growth is something I would actively monitor.</p><p>An abended (stopped) Replicat or Distribution Path can turn what initially looks like a replication problem into a filesystem-capacity problem surprisingly quickly.</p><h3>Step 16 — Inspect the Container Itself</h3><p>This command provides a useful high-level Docker summary:</p><pre>docker inspect ogg26ai \<br>  --format &#39;<br>Name: {{.Name}}<br>Image: {{.Config.Image}}<br>Status: {{.State.Status}}<br>Started: {{.State.StartedAt}}<br>RestartCount: {{.RestartCount}}<br>&#39;</pre><p>Also monitor runtime resource consumption:</p><pre>docker stats ogg26ai</pre><p>This helps separate infrastructure symptoms from GoldenGate symptoms.</p><p>For example:</p><pre>High CPU                    -&gt; Extract/Replicat workload or host contention<br>High memory                 -&gt; Process/container pressure<br>Rapid /u02 growth           -&gt; Trail accumulation<br>Container restart count     -&gt; Infrastructure/container instability<br>GoldenGate lag increasing   -&gt; Replication pipeline issue</pre><p>Looking at these signals together is much more useful than troubleshooting GoldenGate entirely in isolation.</p><h3>Step 17 — Consider Using a Non-Default Host Port</h3><p>Port 443 is convenient:</p><pre>-p 443:443</pre><p>but it may conflict with another HTTPS service on the host.</p><p>For development and test environments, I often prefer:</p><pre>-p 8443:443</pre><p>The mapping becomes:</p><pre>Host TCP/8443 -&gt; Container TCP/443</pre><p>Access GoldenGate using:</p><pre>https://localhost:8443</pre><p>The container still listens on its expected HTTPS port while avoiding a collision with the host’s standard HTTPS endpoint.</p><h3>Step 18 — My GoldenGate Container Health-Check Cheat Sheet</h3><p>These are the commands I find myself using repeatedly.</p><h3>Enter the container</h3><pre>docker exec -it ogg26ai bash</pre><h3>Check Docker status</h3><pre>docker ps --filter name=ogg26ai</pre><h3>Check logs</h3><pre>docker logs --tail 100 ogg26ai</pre><h3>Follow logs</h3><pre>docker logs -f ogg26ai</pre><h3>Verify persistent mounts</h3><pre>docker inspect ogg26ai \<br>  --format &#39;{{range .Mounts}}{{println .Source &quot;-&gt;&quot; .Destination}}{{end}}&#39;</pre><h3>Check GoldenGate environment</h3><pre>env | sort | grep -E &#39;^(OGG|DEPLOYMENT|TNS)&#39;</pre><h3>Check the installation home</h3><pre>echo &quot;$OGG_HOME&quot;</pre><h3>Locate deployment data</h3><pre>find /u02 -maxdepth 6 -type d -name data -print</pre><h3>Find trail files</h3><pre>find /u02 -maxdepth 8 \<br>  -path &#39;*/var/lib/data/*&#39; \<br>  -type f \<br>  -print</pre><h3>Check GoldenGate processes</h3><pre>ps -ef | grep -Ei &#39;[o]gg|[s]ervice|[a]dminsrvr|[d]istsrvr|[r]eceiversrvr&#39;</pre><h3>Check filesystem capacity</h3><pre>df -h</pre><h3>Check GoldenGate persistent storage consumption</h3><pre>du -sh /u02/*</pre><h3>Start Admin Client</h3><pre>adminclient</pre><h3>Connect</h3><pre>CONNECT https://localhost:443 DEPLOYMENT Local AS oggadmin !</pre><h3>Operational Considerations for Real Migration Environments</h3><p>The Docker command is only one part of running GoldenGate reliably.</p><p>For an enterprise database migration, I would additionally consider the following.</p><h3>1. Trail Storage Capacity</h3><p>Trail storage should be sized based on:</p><pre>Source transaction volume<br>×<br>Maximum expected outage/recovery window<br>+<br>Operational safety margin</pre><p>Do not size /u02 simply based on the current database size.</p><p>The relevant question is how much change data GoldenGate may need to retain while downstream processing is unavailable.</p><h3>2. Trail Purging</h3><p>Avoid treating old GoldenGate trails like ordinary temporary files.</p><p>Do not build a cron job such as:</p><pre>find ... -mtime +N -delete</pre><p>without understanding GoldenGate checkpoints and downstream consumers. Trail retention is part of the recoverability of the replication pipeline.</p><h3>3. Monitor Lag and Checkpoints</h3><p>Filesystem monitoring tells us whether trails are accumulating.</p><p>During a migration, monitor:</p><pre>Extract status<br>Extract lag<br>Replicat status<br>Replicat lag<br>Checkpoint progression<br>Distribution Path status<br>Trail filesystem growth<br>Host filesystem utilization</pre><p>Together, these provide a much more complete view of replication health.</p><h3>4. Protect /u02</h3><p>Once /u02 contains active deployment state and trails, treat it as persistent application data.</p><p>The useful containerization model is:</p><pre>Container      = Replaceable<br>Image          = Replaceable<br>/u01 software  = Replaceable</pre><pre>/u02 data      = Persistent<br>Trails         = Operationally critical<br>Configuration  = Operationally critical<br>Credentials    = Sensitive<br>Certificates   = Sensitive</pre><p>That distinction makes container lifecycle management significantly safer.</p><h3>5. Replace Self-Signed Certificates</h3><p>Self-signed certificates are convenient for initial validation.</p><p>For an enterprise deployment, use certificates issued according to the organization’s PKI standards and validate:</p><ul><li>Certificate chain</li><li>Hostname/SAN configuration</li><li>Expiration monitoring</li><li>Private-key permissions</li><li>Certificate rotation procedures</li></ul><h3>6. Don’t Expose GoldenGate Administration to the Internet</h3><p>A public IP may technically make:</p><pre>https://&lt;public-ip&gt;</pre><p>reachable, but that doesn’t mean GoldenGate’s administration interface should be broadly internet-accessible.</p><p>For a real migration environment, restrict connectivity using the relevant network controls: security groups, firewall rules, private subnets/networks, VPN, bastion access, or equivalent enterprise controls.</p><h3>The Final Docker Configuration</h3><p>Putting everything together, this is the baseline configuration used in this walkthrough:</p><pre>docker run -d \<br>  --name ogg26ai \<br>  --hostname ogg26ai \<br>  --restart unless-stopped \<br>  -p 443:443 \<br>  --env-file &quot;$HOME/goldengate/26ai/.env&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u02:/u02&quot; \<br>  -v &quot;$HOME/goldengate/26ai/u03:/u03&quot; \<br>  -v &quot;$HOME/goldengate/26ai/cert:/etc/nginx/cert&quot; \<br>  --log-opt max-size=50m \<br>  --log-opt max-file=5 \<br>  localhost/oracle/goldengate:23.26.3.0.0</pre><h3>Conclusion</h3><p>Containerizing Oracle GoldenGate 26ai is relatively straightforward. The more important decision is determining <strong>what belongs to the container and what must survive the container restarts.</strong></p><p>Some of the Key principles are:</p><p><strong>1. Keep GoldenGate software in the image.</strong><br>Don’t unnecessarily externalize /u01.</p><p><strong>2. Persist </strong><strong>/u02.</strong><br>Your deployment configuration and trail data should not disappear because a container was replaced.</p><p><strong>3. Know exactly where the trails are.</strong><br>During migration troubleshooting, trail location, growth, and consumer checkpoints are fundamental signals.</p><p><strong>4. Validate both Docker and GoldenGate.</strong><br>docker ps tells you the container is running. INFO ALL tells you considerably more about GoldenGate.</p><p><strong>5. Learn Admin Client and Logdump.</strong><br>The UI is useful, but these tools become invaluable when diagnosing real replication problems.</p><p><strong>6. Monitor disk consumption.</strong><br>An interrupted downstream consumer can translate directly into accumulating trails and eventually filesystem exhaustion.</p><p><strong>7. Treat the container as disposable but not the system of record.</strong></p><p>If I can remove and recreate the GoldenGate container while retaining the deployment state I intentionally chose to persist, I have a much cleaner operational boundary.</p><p>This separation makes the environment easier to operate, troubleshoot, upgrade, and the most importantly easier to troubleshoot during a high-pressure database migration.</p><h3>References</h3><p><strong>[1] Oracle GoldenGate 26ai — Deployment Directories</strong></p><p><a href="https://docs.oracle.com/en/database/goldengate/core/26/coredoc/deployment-directories.html">https://docs.oracle.com/en/database/goldengate/core/26/coredoc/deployment-directories.html</a></p><p><strong>[2] Oracle GoldenGate 26ai — Logdump Utility</strong></p><p><a href="https://docs.oracle.com/en/database/goldengate/core/26/logdump-ref/using-logdump-utility.html">https://docs.oracle.com/en/database/goldengate/core/26/logdump-ref/using-logdump-utility.html</a></p><p><em>The commands and configuration in this post are based on a working Oracle GoldenGate 26ai Docker deployment. Adapt filesystem paths, network exposure, authentication, TLS configuration, resource limits, storage design, and security controls to the requirements of your environment before using the approach for production workloads.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0a11790a03d5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OCI PostgreSQL vs. Amazon Aurora PostgreSQL for Geospatial Workloads — A Benchmarking Comparison]]></title>
            <link>https://towardsdev.com/oci-postgresql-vs-amazon-aurora-postgresql-for-geospatial-workloads-a-benchmarking-comparison-ea244ac2fbbb?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/ea244ac2fbbb</guid>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[amazon]]></category>
            <category><![CDATA[aurora]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <category><![CDATA[postgis]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Sat, 27 Jun 2026 12:32:04 GMT</pubDate>
            <atom:updated>2026-07-23T12:02:31.348Z</atom:updated>
            <content:encoded><![CDATA[<p>OCI PostgreSQL delivered 300.8 requests/sec with 439 ms P95 end-to-end HTTP latency on a 4-OCPU shape (8 vCPU equivalent). The Aurora 4-vCPU baseline delivered 88.4 requests/sec with 1,374 ms P95; Aurora scaled to 377.0 requests/sec at 16 vCPUs, with a higher directional monthly estimate.</p><p>The useful conclusion is not that one platform is universally faster. For this particular end-to-end, read-heavy geospatial API path, OCI supplied the strongest throughput-per-dollar result under the stated assumptions. Teams should validate that result with their own data distribution, query mix, index design, network topology, repeat runs, and complete pricing model.</p><p>## FAQs</p><p>### Was this a direct database benchmark?</p><p>No. JMeter called an HTTPS endpoint on Apache; Apache/PHP then connected to the selected PostgreSQL backend. The result is closer to a small web-service path than a direct SQL microbenchmark.</p><p>### Why compare OCI 4 OCPUs with Aurora 4, 8, and 16 vCPUs?</p><p>The 4-vCPU Aurora run provides a baseline. The larger Aurora shapes show how the same workload responds to more compute. Because one OCPU equals two vCPUs, OCI’s 4-OCPU result is also shown as an 8-vCPU equivalent.</p><p>### Does this prove OCI PostgreSQL is always faster than Aurora PostgreSQL?</p><p>No. It shows OCI PostgreSQL performed better and more cost efficiently for this specific end-to-end PostGIS read workload under the stated topology and pricing assumptions. Other schemas, indexes, query plans, regions, data sizes, I/O profiles, and write patterns should be tested separately.</p><p>### Why are PostGIS and geospatial workloads so sensitive to indexes, query shape, and data distribution?</p><p>Spatial predicates first depend on how effectively the index narrows the search area. The radius, geometry/geography type, ordering, returned-row limit, and density of points near the searched location determine how many candidates must be evaluated and returned. Small changes can therefore affect both database time and JSON response size substantially.</p><p>### What should someone replicate before making a platform decision?</p><p>Use the same schema, seed process, indexes, query parameters, application path, network placement, concurrency profile, and pricing model. Repeat the runs and inspect both client-side metrics and database-side behaviour.</p><p>## Methodology and source notes</p><p>- Benchmark source repository: <a href="https://github.com/shadabshaukat/jmeter-postgis-e2e-benchmark">https://github.com/shadabshaukat/jmeter-postgis-e2e-benchmark</a><br>- JMeter artifacts used for metrics: `web_read_public`, `web_read_public_aurora`, `web_read_public_aurora_8vcpu`, and `web_read_public_aurora_16vcpu`.<br>- Effective run evidence: each JTL contains 48,000 samples and a maximum observed thread count of 120. The repository sources `.env`, so the artifacts are the source of truth for the executed profile.<br>- AWS pricing source: Amazon RDS public price list for `ap-southeast-2`, publication date 2026–06–19T20:39:21Z. Rates used: Aurora PostgreSQL `db.r6gd.xlarge` US$0.751/hour, `db.r5.2xlarge` US$1.40/hour, `db.r5.4xlarge` US$2.80/hour, and Aurora storage US$0.11/GB-month.<br>- Oracle pricing source: OCI public price list, last updated 2026–06–11T14:42:17.843Z. Rates used: Database with PostgreSQL — X86 US$0.098/OCPU-hour and Database Optimized Storage US$0.072/GB-month.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea244ac2fbbb" width="1" height="1" alt=""><hr><p><a href="https://towardsdev.com/oci-postgresql-vs-amazon-aurora-postgresql-for-geospatial-workloads-a-benchmarking-comparison-ea244ac2fbbb">OCI PostgreSQL vs. Amazon Aurora PostgreSQL for Geospatial Workloads — A Benchmarking Comparison</a> was originally published in <a href="https://towardsdev.com/">Towards Dev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating On-Premise PostgreSQL to OCI PostgreSQL using a Hybrid Replication Architecture]]></title>
            <link>https://shadabshaukat.medium.com/migrating-on-premise-postgresql-to-oci-postgresql-using-a-hybrid-replication-architecture-f875d248321b?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/f875d248321b</guid>
            <category><![CDATA[golden-gate]]></category>
            <category><![CDATA[hybrid]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[migration]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Sat, 11 Apr 2026 14:37:13 GMT</pubDate>
            <atom:updated>2026-04-30T14:53:20.855Z</atom:updated>
            <content:encoded><![CDATA[<p>Most zero downtime migrations from on-prem PostgreSQL to OCI PostgreSQL rely on logical replication directly from the source. But what if you want to reduce load on your production system, add a control layer, or deal with network constraints?</p><p>I recently explored an approach using an intermediary PostgreSQL instance on OCI Compute.</p><h4><strong>Architecture in brief</strong></h4><pre>** On-prem PostgreSQL (A) **<br>    |<br>    |<br>    |<br>(Physical replication)<br>    |<br>    |<br>    V<br>** OCI Instance PostgreSQL (B)  **<br>    |<br>    |<br>    |<br>(Logical replication)<br>    |<br>    | <br>    V                                                              <br> ** OCI PostgreSQL (C) **</pre><p>This architecture <strong>is used in complex migrations</strong>, especially when you want to decouple source load, add transformation control, or work around network/security constraints.</p><p>We’re essentially proposing a <strong>hybrid replication chain</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*in8bpmnuNOO-4J0bdcxxHQ.png" /></figure><ul><li><strong>A (on-prem PostgreSQL)</strong> → <strong>B (OCI Instance PostgreSQL)</strong> via <em>physical replication</em></li><li><strong>B (OCI Instance PostgreSQL)</strong> → <strong>C (OCI PostgreSQL)</strong> via <em>logical replication / OCI Goldengate</em></li></ul><blockquote><strong>Important Note :</strong> You can use native postgres logical replication, pglogical or any other logical tool for the replication from B → C.<br>Becuase the target is on OCI PostgreSQL on Oracle Cloud Infrastructure, we’re using OCI Goldengate; which is an industry leading real-time replication solution.</blockquote><h3><strong>Why this is interesting</strong></h3><p>• Physical replication ensures a low-impact, byte-level copy from the source<br>• Logical replication from the intermediary allows selective migration and transformation<br>• The OCI compute layer acts as a buffer, isolating production from migration tooling</p><h4><strong>Key benefits</strong></h4><p>• Zero logical decoding overhead on production<br>• Better control over migration scope<br>• Improved fault isolation<br>• Flexible transformation using OCI Goldengate</p><h4><strong>Trade-offs</strong></h4><p>• Increased replication lag due to chaining<br>• More operational complexity<br>• WAL retention needs careful monitoring<br>• Logical decoding on standby requires newer PostgreSQL versions</p><p><strong>Pre-Requisities on both A and B</strong></p><p>1. Ensure Port 5432 is added to Firewall whitelist on both hosts for ingress and also on the Network Security Group of the instance</p><pre>sudo firewall-cmd --zone=public --permanent --add-port=5432/tcp<br>sudo firewall-cmd --reload</pre><p>2. listen_address in postgresql.conf is set to <strong>‘*’</strong></p><p>3. pg_hba.confis set to allow all hosts</p><pre>host    all             all             0.0.0.0/0               scram-sha-256</pre><h3><strong>Step-by-Step Implementation</strong></h3><h3>Phase 1: Setup Physical Replication (A → B)</h3><h3>1. Prepare On-Premise Source (A)</h3><p>Update postgresql.conf:</p><pre>wal_level = replica<br>max_wal_senders = 10<br>wal_keep_size = 2048 #2GB<br>hot_standby = on</pre><p>Update pg_hba.conf:</p><pre>host    replication     replicator      &lt;Server-B-IP&gt;/32         scram-sha-256</pre><p>Create replication user:</p><pre>CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD &#39;password&#39;;</pre><h3>2. Take Base Backup to OCI Instance with Postgres -Instance B</h3><p>On OCI instance (B) install a fresh PostgreSQL engine or disable the previous Postgresql database for a fresh restore :</p><pre>mv /var/lib/pgsql/17/data/ /var/lib/pgsql/17/data_old/<br><br>sudo systemctl stop postgresql-17</pre><p>Take backup on OCI instance postgres (B) :</p><pre>pg_basebackup \<br>  -h &lt;Server-A-IP&gt; \<br>  -D /var/lib/pgsql/17/data/ \<br>  -U replicator \<br>  -P \<br>  -R \<br>  -X stream</pre><p>-h &lt;Server-A-IP&gt;<strong>Host (source database server)</strong></p><ul><li>Specifies the <strong>primary server (A)</strong> to connect to</li><li>Can be:</li><li>IP address</li><li>DNS name</li></ul><p>-D /var/lib/pgsql/17/data/<strong>Destination directory (data directory on B)</strong></p><ul><li>Where the <strong>base backup will be written</strong></li><li>This becomes the <strong>entire PostgreSQL data directory</strong> on B</li></ul><p>⚠️ Important:</p><ul><li>Must be <strong>empty or non-existent</strong></li><li>Will contain:</li><li>base/, pg_wal/, global/, etc.</li></ul><p>👉 This is effectively:</p><blockquote><em>“Make this directory an exact clone of A”</em></blockquote><p>-U replicator<strong>Replication user</strong></p><ul><li>PostgreSQL user used to connect</li><li>Must have:</li><li>REPLICATION LOGIN privilige</li></ul><p>-P<strong>Progress output</strong></p><ul><li>Shows real-time progress of backup:</li><li>Total size</li><li>Data transferred</li><li>Percentage</li></ul><p>-R<strong>Write replication configuration automatically</strong></p><p>This is a very important flag 👇</p><p>It does two things:</p><p>1. Creates standby.signal</p><ul><li>Tells PostgreSQL:</li><li>“Start in standby (replica) mode”</li></ul><p>2. Writes connection info into:</p><pre>postgresql.auto.conf</pre><pre>primary_conninfo = &#39;host=Server-A-IP user=replicator password=xxx&#39;</pre><p>👉 Without -R, you would have to:</p><ul><li>Manually create standby.signal</li><li>Manually configure primary_conninfo</li></ul><p>-X stream<strong>WAL (Write-Ahead Log) handling method</strong></p><p>This ensures backup consistency.</p><p>What it does:</p><ul><li>Streams WAL files in parallel with the base backup</li><li>Ensures backup is transactionally consistent</li></ul><p>Alternatives:</p><p>-X streamStream WAL during backup (most common)<br>-X fetchFetch WAL after backup<br>-X noneNo WAL (⚠️ unsafe for replication)</p><p>👉 Always use stream for replication setups</p><h3>⚡What this Does :</h3><ul><li>Copies data</li><li>Creates standby.signal</li><li>Configures replication automatically</li></ul><h3>🔄 What Happens End-to-End</h3><p>When you run this command:</p><ol><li>Connects to A using replication protocol</li><li>Starts a consistent snapshot</li><li>Copies all data files</li><li>Streams WAL simultaneously (-X stream)</li><li>Writes standby config (-R)</li><li>Prepares B to start as a replica</li></ol><h3>📦 Result on B</h3><p>After execution, your directory contains:</p><pre>/var/lib/pgsql/17/data/<br>├── base/<br>├── global/<br>├── pg_wal/<br>├── postgresql.auto.conf   &lt;-- contains primary_conninfo<br>├── standby.signal         &lt;-- enables replica mode</pre><h3>✅ Pro Tips (Real-World)</h3><p>Add compression (faster over network)</p><pre>--compress=server-gzip</pre><p>Limit bandwidth (avoid saturating link)</p><pre>--max-rate=100MB</pre><p>Use replication slot (prevent WAL loss)</p><pre>--slot=my_slot --create-slot</pre><h3>⚠️ Common Mistakes</h3><ul><li>Forgetting -R → replica won’t start automatically</li><li>Using non-empty -D directory</li><li>Not allowing replication in pg_hba.conf</li><li>Not enough WAL retention on A</li></ul><h4><strong>Start B as Standby</strong></h4><pre>sudo systemctl start postgresql-17</pre><h4>Verify:</h4><pre>SELECT * FROM pg_stat_wal_receiver;</pre><h3>Phase 2: Prepare B for Logical Replication / OCI Goldengate</h3><p>This is the <strong>critical step</strong></p><p>By default, a physical replica <strong>cannot act as a logical replication source</strong> unless configured properly.</p><h3>1. Enable Logical Decoding on B</h3><p>Update on B:</p><pre>wal_level = logical<br>max_replication_slots = 10<br>max_wal_senders = 10</pre><p>Important:<br>–&gt; Requires restart<br>–&gt; In PostgreSQL 14+, logical decoding on standby is supported</p><p>Create Logical Replication Slot (if using native or pglogical. Goldengate creates it’s own replication slot)</p><pre>SELECT * FROM pg_create_logical_replication_slot(&#39;goldengate_slot&#39;, &#39;test_decoding&#39;);</pre><h3>Phase 3: Setup OCI Goldengate (B → C)</h3><p>For detailed steps refer these articles :</p><p>[1] <a href="https://medium.com/oracledevs/oci-goldengate-to-perform-cross-region-replication-for-oci-postgresql-dbsystems-b14d4b7623d5">https://medium.com/oracledevs/oci-goldengate-to-perform-cross-region-replication-for-oci-postgresql-dbsystems-b14d4b7623d5</a></p><p>[2] <a href="https://medium.com/@shadabshaukat/zero-downtime-migration-to-oci-postgresql-from-amazon-rds-postgresql-using-oci-goldengate-76c80c631c45">https://medium.com/@shadabshaukat/zero-downtime-migration-to-oci-postgresql-from-amazon-rds-postgresql-using-oci-goldengate-76c80c631c45</a></p><h3>1. Using OCI Goldengate:</h3><p>a. Create GoldenGate deployment in OCI</p><p>b. Configure Extract on B :</p><ul><li>Capture changes using logical decoding</li></ul><p>c. Configure Replicat to Target:</p><ul><li>OCI PostgreSQL</li></ul><h3>2. Perform Initial Load + CDC:</h3><p>a. Perform initial load using:</p><ul><li>Native dump/restore OR</li><li>GoldenGate initial load</li></ul><p>b. Start CDC pipeline:</p><p>GoldenGate streams ongoing changes from B → C</p><h3>Advantages</h3><h4>Zero Impact on Source (A)</h4><p>No logical decoding or CDC load on production</p><h4>High Control Over Migration</h4><p>GoldenGate provides:</p><ul><li>Filtering</li><li>Transformation</li><li>Conflict handling</li></ul><h4>Cloud-Native Integration</h4><p>Seamless integration within OCI ecosystem</p><h4>Better Failure Isolation</h4><p>Failures in replication do not impact production</p><h3>Trade-offs</h3><h4>Increased Complexity</h4><p>Two replication layers instead of one</p><h4>Replication Lag</h4><p>Lag compounds:</p><ul><li>A → B</li><li>B → C</li></ul><h4>WAL Retention Management</h4><p>Logical replication slots on B must be monitored</p><h4>Operational Overhead</h4><p>Requires monitoring:</p><ul><li>Physical replication health</li><li>GoldenGate processes</li></ul><h3>When Should You Use This Pattern?</h3><p>This approach is ideal when:</p><ul><li>Production systems cannot handle additional load especially with replication slots which is an overhead of logical replication</li><li>Strict network/security boundaries exist</li><li>Advanced transformation or filtering is required</li><li>Migration must be highly controlled and observable</li></ul><h3>Final Thoughts</h3><p>This hybrid architecture is not the simplest path, but it is a powerful one. By combining PostgreSQL physical replication with OCI GoldenGate’s logical capabilities, you gain a robust and flexible migration pipeline that minimizes production risk while maximizing control.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f875d248321b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Zero-Downtime Blue-Green Deployments for OCI MySQL HeatWave]]></title>
            <link>https://medium.com/oracledevs/zero-downtime-blue-green-deployments-for-oci-mysql-heatwave-3088ab5b6ea9?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/3088ab5b6ea9</guid>
            <category><![CDATA[mysql]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <category><![CDATA[blue-green-deployment]]></category>
            <category><![CDATA[zero-downtime]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 03:43:05 GMT</pubDate>
            <atom:updated>2026-03-17T17:41:05.578Z</atom:updated>
            <content:encoded><![CDATA[<h4>Orchestrating safe and fast cutovers on OCI MySQL HeatWave using native replication or Oracle GoldenGate</h4><h3>Key Takeaways</h3><ul><li>Blue-green for databases, simplified on OCI: Run parallel Blue (prod) and Green (staging) MySQL HeatWave environments, keep them in sync, and switch traffic with near-zero downtime.</li><li>Two replication paths: Use MySQL Channel Replication for simpler, cost-effective rollouts; use Oracle GoldenGate for enterprise-grade CDC and major-version upgrades.</li><li>Smooth cutovers with OCI networking: Private DNS plus OCI Network Load Balancer keep app endpoints stable, enabling quick, reversible switchovers.</li><li>Practical, repeatable flow: Provision two DB Systems, seed Green from Blue, start replication, validate, then flip NLB backends — keeping risk low during upgrades, tests, and schema changes.</li></ul><h3>Introduction</h3><p>If you have been following my work on Medium, you may have read my previous article describing how to orchestrate a blue-green deployment for OCI PostgreSQL.</p><p>In this article, we will explore how to implement the same deployment strategy for <strong>MySQL HeatWave on </strong>Oracle Cloud Infrastructure (OCI).</p><p>Blue-green deployment is a well-established strategy used by DBAs and DevOps teams to safely introduce changes to production systems. The idea is simple but powerful: maintain two environments — <strong>Blue (Production)</strong> and <strong>Green (Staging)</strong> — that remain synchronized through replication. This allows new versions, schema changes, or application updates to be tested in a production-like environment before switching live traffic.</p><p>In the context of databases, blue-green deployment involves:</p><ul><li>A <strong>Blue environment</strong> serving live production workloads</li><li>A <strong>Green environment</strong> acting as a synchronized staging copy</li><li>Continuous <strong>replication from Blue → Green</strong></li></ul><p>Once testing is complete, traffic can be switched to the Green environment with minimal disruption.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vpY8LfH-l-_tTh321YM6Ag.png" /><figcaption>Blue-Green Deployment</figcaption></figure><p>With <strong>OCI MySQL HeatWave</strong>, implementing this pattern is easier than many expect. It requires only a few OCI native services combined with MySQL replication technologies such as:</p><ul><li><strong>MySQL Channel Replication (binlog replication)</strong></li><li><strong>Oracle GoldenGate</strong></li></ul><p>In this article, we will walk through how to build a blue-green deployment architecture for <strong>OCI MySQL Database Service</strong> to support application testing, regression validation, and database upgrades.</p><h3>High-level steps to orchestrate a Blue-Green deployment strategy with OCI MySQL HeatWave</h3><p>A typical OCI MySQL blue-green deployment consists of:</p><h4><strong>2 MySQL DB Systems</strong></h4><ul><li>Blue (Production)</li><li>Green (Staging)</li></ul><h4><strong>Replication</strong></h4><ul><li>MySQL Channel Replication <strong>or</strong></li><li>Oracle GoldenGate</li></ul><h4><strong>Network orchestration</strong></h4><ul><li>Oracle Cloud Infrastructure Network Load Balancer</li></ul><h4><strong>Private DNS</strong></h4><ul><li>Ensures application connectivity remains constant during switchovers</li></ul><h3><strong>Method 1: </strong>Using OCI MySQL Channel Replication — <strong>Primary (Preferred)</strong></h3><h4><strong>Use cases: Minor version upgrades, app performance &amp; regression testing</strong></h4><p><strong>Pro:</strong> Easy to setup using OCI Native Tooling.<br><strong>Con</strong>: Needs downtime of the source to keep backup and restore consistently, before creating channel replication. A major version upgrade is possible, but will incur a longer downtime of the source.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/652/1*1p22lMPomaBgKN7_XdKjPg.png" /><figcaption>Phased deployment diagram</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vpY8LfH-l-_tTh321YM6Ag.png" /><figcaption>Deployment diagram</figcaption></figure><h4>Prerequisites</h4><p><strong>1. OCI Tenancy</strong> with a user with appropriate IAM permissions : <a href="https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/overview.htm">https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/overview.htm</a></p><p><strong>2. OCI Tenancy API key</strong>: <a href="https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm">https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm</a></p><p><strong>3. OCI command line</strong> installed and configured on your machine from where you’re running this : <a href="https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm">https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm</a></p><h4>Step 1</h4><p>Use the below Github repository to provision OCI MySQL Infrastructure, or use your own instances:</p><p><strong>Code Repo: </strong><a href="https://github.com/shadabshaukat/oci-mysql-blue-green-deployment.git">https://github.com/shadabshaukat/oci-mysql-blue-green-deployment.git</a></p><p>Edit the terraform.tfvars as per your environment. Once done, run terraform plan and terraform apply .</p><h4>Step 2</h4><p>If you have used the above to create the stack, then it provisions the below compoments:</p><p>2 x OCI MySQL HA DB Systems</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/971/1*qutFL1MrAcfZAt8AyV3_wg.png" /><figcaption>DB Systems</figcaption></figure><p>1 x MySQL Bastion VM</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B13DOy6g8NNmsYGJPp87xQ.png" /><figcaption>Bastion VM</figcaption></figure><p>1 x NLB</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_Rpm_fAVXZoLB1zWftdpUg.png" /><figcaption>NLB</figcaption></figure><p>1 X VCN with 2 subnets, along with network security list and routing tables</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZGor7m1EIi92h-LISeKb_g.png" /><figcaption>VCN</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jnaJ1Ma2NLLegJBG_eaukQ.png" /><figcaption>Subnets</figcaption></figure><p>3 x DNS Records in Private Zone for the VCN created above</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/943/1*ym_5NK_BSIDdEihPd-mtKg.png" /><figcaption>DNS Records</figcaption></figure><p>1 x OCI GoldenGate for MySQL Deployment</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qeg6_gL1UMERJrgusy9UAA.png" /><figcaption>OCI GoldenGate</figcaption></figure><h4><strong>Step 3</strong></h4><p>Create a backup of the Blue Database (8.0.45), and restore to a higher version (8.4.8).</p><blockquote>You can also use MySQL Channel replication to clone the Blue (8.0.45) to Green (8.4.8) , or even the 9.x DBSystem, using the MySQL Shell util.copyInstance utility.</blockquote><pre>mysqlsh -h mysql-blue.private.mysqlvcn.oraclevcn.com -u admin -D mysql<br><br>\js<br><br> --Dry Run--<br>util.copyInstance(&#39;mysql://admin@mysql-green.private.mysqlvcn.oraclevcn.com&#39;, {&quot;compatibility&quot;:[&quot;force_innodb&quot;, &quot;skip_invalid_accounts&quot;, &quot;strip_definers&quot;,&quot;strip_restricted_grants&quot;,&quot;strip_tablespaces&quot;,&quot;ignore_wildcard_grants&quot;,&quot;strip_invalid_grants&quot;,&quot;create_invisible_pks&quot;], updateGtidSet: &quot;append&quot;, users: &quot;true&quot;, threads: 8, consistent: &quot;true&quot; , dryRun:&quot;true&quot;})<br><br> --Actual Run--<br>util.copyInstance(&#39;mysql://admin@mysql-green.private.mysqlvcn.oraclevcn.com&#39;, {&quot;compatibility&quot;:[&quot;force_innodb&quot;, &quot;skip_invalid_accounts&quot;, &quot;strip_definers&quot;,&quot;strip_restricted_grants&quot;,&quot;strip_tablespaces&quot;,&quot;ignore_wildcard_grants&quot;,&quot;strip_invalid_grants&quot;,&quot;create_invisible_pks&quot;], updateGtidSet: &quot;append&quot;, users: &quot;true&quot;, threads: 8, consistent: &quot;true&quot; , dryRun:&quot;false&quot;})<br><br>---<br>Dump_metadata:<br>  Binlog_file: binary-log.002478<br>  Binlog_position: 197<br>  Executed_GTID_set: 8880f0e7-a14a-400c-8eda-1c9fd5c969be:1-15</pre><p>To keep a consistent backup, stop the Blue DB System during non-business hours for some time, to ensure no writes are happening on the Source DBSystem.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*60acl0C1P0od_oYKD3qB4g.png" /><figcaption>Stopping Blue</figcaption></figure><pre>oci mysql backup create \<br>--db-system-id ocid1.mysqldbsystem.oc1.ap-tokyo-1.aaaaaaaa5phic3km677ppklnlzmp6et7i44tbsacae5xdusxgo4hwzzpb77a \<br>--backup-type FULL \<br>--display-name Full_Backup_MySQL_Blue_Source \<br>--retention-in-days 7 \<br>--region ap-tokyo-1 \<br>--profile DEFAULT</pre><p>Once the backup is created successfully, check in the OCI Console.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N0JqOhcYSje8Ez1hvOHnJA.png" /><figcaption>Backup</figcaption></figure><p>Copy the Backup OCI: ocid1.mysqlbackup.oc1.ap-tokyo-1.aaaaaaaakjzryp4l77vv3bhgiosjrb7hgqi23q4nkkmf77lovpo4oj7aewaq</p><p>Then, create a clone of this DBSystem with a higher version.</p><pre>oci mysql db-system clone \<br>--admin-username admin \<br>--admin-password RAbbithole1234## \<br>--availability-domain Whux:AP-TOKYO-1-AD-1 \<br>--shape-name MySQL.2 \<br>--is-highly-available 1 \<br>--mysql-version 8.4.8 \<br>--source-backup-id ocid1.mysqlbackup.oc1.ap-tokyo-1.aaaaaaaakjzryp4l77vv3bhgiosjrb7hgqi23q4nkkmf77lovpo4oj7aewaq \<br>--subnet-id ocid1.subnet.oc1.ap-tokyo-1.aaaaaaaaazmu66kfwu5fwhtn45thir3wox2kqkqsvk6fc32lkhrdzdfpkvca \<br>--data-storage-size-in-gbs 50 \<br>--display-name MySQL_Green_Staging \<br>--region ap-tokyo-1 \<br>--profile DEFAULT</pre><blockquote>admin-username : Username of the new DB System<br>admin-password : Password of the new DB System<br>availability-domain : Get the AD details from the Blue DB System<br>source-backup-id : Source manual Backup OCID<br>subnet-id : OCID of the subnet where you want to provision the new staging database</blockquote><p>Wait for the DBSystem to complete, then copy the OCID and IP.</p><blockquote>ocid1.mysqldbsystem.oc1.ap-tokyo-1.aaaaaaaajlwl23yuczci637xey5xv3rfmqlkwg2x66rzbbcciw6nbko33ama</blockquote><blockquote>IP — 10.30.20.59</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7KVw5_hWui_C8VqZ3iKeeQ.png" /><figcaption>OCID</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qt_B2ZSiQpl33oQb47qCGw.png" /><figcaption>IP address</figcaption></figure><p>Start up the Blue Source DB System.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fhNkNwec7N6PDXaRfsydLQ.png" /></figure><p>Create channel replication from the Target (Green Staging) to the Source (Blue).</p><pre>oci mysql channel create-from-mysql \<br>--source-hostname 10.30.20.78 \<br>--source-port 3306 \<br>--source-username admin \<br>--source-password RAbbithole1234## \<br>--source-ssl-mode REQUIRED \<br>--target-db-system-id ocid1.mysqldbsystem.oc1.ap-tokyo-1.aaaaaaaa72t2etr2tdpyw47g2b4ukr22xkynlsw7g6nhjdtyub7amtpsae3q \<br>--display-name channel-rep-blue-to-green \<br>--is-enabled true \<br>--target-applier-username admin \<br>--target-channel-name replication_channel \<br>--target-delay-in-seconds 0 \<br>--target-tables-without-primary-key-handling ALLOW \<br>--region ap-tokyo-1 \<br>--profile DEFAULT</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/655/1*g6QAao_qE_1fWKX2Pax0MA.png" /><figcaption>Channel information</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/655/1*38pJOChZj2LP5jEFf9LE2Q.png" /><figcaption>Source information</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/655/1*OYkSs91psdOomzMAYPvmdA.png" /><figcaption>Target information</figcaption></figure><blockquote><strong>Important: </strong>You cannot upgrade from 8.0.X to 9.X You would need to first upgrade 8.0.X to 8.4 , and then to 9.X— hence you can only restore your 8.0.X backup to 8.4. Once you restored to 8.4, you can then upgrade to 9.X.</blockquote><blockquote>Refer to the official documentation for more information:</blockquote><blockquote><a href="https://docs.oracle.com/en-us/iaas/mysql-database/doc/major-version-upgrade.html">https://docs.oracle.com/en-us/iaas/mysql-database/doc/major-version-upgrade.html</a></blockquote><blockquote><a href="https://docs.oracle.com/en-us/iaas/mysql-database/doc/restoring-backup.html">https://docs.oracle.com/en-us/iaas/mysql-database/doc/restoring-backup.html</a></blockquote><h4>Upgrading 8.0 → 8.4 → 9.x</h4><p>To upgrade the DB System from 8.4.x to 9.x, run the below OCI CLI:</p><pre>oci mysql db-system update \<br>--db-system-id ocid1.mysqldbsystem.oc1.ap-tokyo-1.aaaaaaaa72t2etr2tdpyw47g2b4ukr22xkynlsw7g6nhjdtyub7amtpsae3q \<br>--mysql-version 9.6.1 \<br>--region ap-tokyo-1 \<br>--profile DEFAULT</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gBEusdP3sG4PDU8qsUMUdQ.png" /><figcaption>Green Staging</figcaption></figure><p>The DB System is upgraded:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JUo0RM3yEsRy7FHYcfhV2Q.png" /><figcaption>Green Staging upgraded</figcaption></figure><p>After this, you can create the channel replication, or start it after upgrade is completed if you wish.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JIJJn8P9ZUvy7L3BHcyf9g.png" /></figure><h4><strong>Step 4</strong></h4><p>Now, let’s add the new DBSystem MySQL_Green_Staging IP to the NLB backend set, and keep it offline.</p><p>First, make sure that the header preservation of source IP is set to <strong>off</strong> on the backend set of the Network Load Balancer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n0rcI7ogUcsmJlbP86DcnQ.png" /><figcaption>Backend set</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*79hiMOqv2mWRxVkordhhDw.png" /><figcaption>Editing the backend set to preserve source IP</figcaption></figure><p>Add the IP of the new Staging DB to the NLB:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AWJ1yn2pj8Vn1_mrtbitiw.png" /><figcaption>Adding additional backend</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mueNwyqFi7lwRTVaTmcZKQ.png" /><figcaption>Backend settings</figcaption></figure><p>Ensure all your production apps always use the NLB’s DNS Endpoint. Your Test App environment can use the Green Staging DB endpoint.</p><p>So, every time the Production app connects to the NLB DB endpoint mysql-nlb.private.mysqlvcn.oraclevcn.com , it is directed to the Blue Environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KMfOGc4IzeCBwLKcmiGErw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rc1a0Gq2qXTiSgBTf17nfA.png" /></figure><pre>MySQL  mysql-nlb.private.mysqlvcn.oraclevcn.com:3306 ssl  mysql<br><br>SQL &gt; SELECT VERSION();<br><br>+--------------+<br>| VERSION()    |<br>+--------------+<br>| 8.0.45-cloud |<br>+--------------+<br><br>1 row in set (0.0006 sec)</pre><p>Once you’ve completed testing, it is a simple matter of draining the blue-db backend, take it offline, and bring the green-db backend online.</p><p>Take the blue backend offline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o7Hd0yQdClfquRauLvl8Mw.png" /><figcaption>Editing the backend</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UduO-OpeJYcorQ05j1hsNg.png" /><figcaption>Backends</figcaption></figure><p>Bring the Green backend online:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4zl_71kDxd3C_HjVOeaH3w.png" /><figcaption>Backends</figcaption></figure><p>App now connects back, and this time it goes to the new Green DB.</p><pre>MySQL  mysql-nlb.private.mysqlvcn.oraclevcn.com:3306 ssl  mysql  <br><br>SQL &gt; select version();<br>+----------------+<br>| version()      |<br>+----------------+<br>| 8.4.8-u1-cloud |<br>+----------------+<br>1 row in set (0.0007 sec)</pre><blockquote>Important Note: You can remove the channel replication after this activity.</blockquote><h3>Method 2: Using OCI GoldenGate Replication</h3><h4>Use cases: Minor and major version upgrades, app performance and regression testing</h4><p><strong>Pros</strong>: Enterprise-grade, cloud-native replication technology which can be used for testing major and minor versions both. <br><strong>Cons</strong>: Additional cost; requires basic knowledge of GoldenGate.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/653/1*iRUcp9elymySrLOuKEYpAA.png" /><figcaption>Phased deployment diagram</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2MWXmEPeCDY2DvLHTHcOIg.png" /><figcaption>Deployment diagram</figcaption></figure><p>If you’ve used the previous code repository to provision the stack, this creates an OCI GoldenGate for MySQL deployment in the same VCN. Also, it creates the OCI Network Load Balancer, along with the private DNS zones.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H6qcDzIr6VAvH_I0a-eTxw.png" /><figcaption>Deployments</figcaption></figure><p>To use OCI Goldengate for Blue-Green deployment, we will use MySQL Shell for the initial load, and then GoldenGate to enable change data capture from the Blue to Green environment.</p><p>2 x MySQL DB Systems — one is Blue (Prod), and other is Green (Staging).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gyOL9hgQuoiRjnh2nPex_w.png" /><figcaption>DB systems</figcaption></figure><p>3 Records in DNS Private Zone for Blue, Green and NLB.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ePUBZQyuCQG2LcKzw7685w.png" /><figcaption>DNS records</figcaption></figure><h4>Prerequisites</h4><p>In the Blue Database:</p><pre>mysqlsh -h mysql-blue.private.mysqlvcn.oraclevcn.com -u admin -D mysql<br><br>select version();<br><br>+--------------+<br>| version()    |<br>+--------------+<br>| 8.0.45-cloud |<br>+--------------+<br><br>CREATE DATABASE app;<br><br>CREATE TABLE app.orders (<br>  order_id bigint PRIMARY KEY,<br>  customer_id bigint,<br>  product_id bigint,<br>  product_description VARCHAR(500),<br>  order_delivery_address VARCHAR(500),<br>  order_date_taken date,<br>  order_misc_notes VARCHAR(500)<br>);<br><br><br>INSERT into app.orders  values (1,1000,2048,&quot;Chair&quot;,&quot;30/2 Acme Street, Pin : 9000, AZ-US&quot;,&quot;2022-12-25&quot;,&quot;Deliver to Doorstep&quot;);<br><br>INSERT into app.orders  values (2,1000,2049,&quot;Table&quot;,&quot;30/2 Acme Street, Pin : 9000, AZ-US&quot;,&quot;2022-12-25&quot;,&quot;Deliver to Doorstep&quot;);<br><br>INSERT into app.orders  values (3,1000,2050,&quot;Lounge&quot;,&quot;30/2 Acme Street, Pin : 9000, AZ-US&quot;,&quot;2022-12-25&quot;,&quot;Deliver to Doorstep&quot;);<br><br>INSERT into app.orders  values (4,1000,2051,&quot;TV&quot;,&quot;30/2 Acme Street, Pin : 9000, AZ-US&quot;,&quot;2022-12-25&quot;,&quot;Deliver to Doorstep&quot;);<br><br>INSERT into app.orders  values (5,1000,2052,&quot;Fridge&quot;,&quot;30/2 Acme Street, Pin : 9000, AZ-US&quot;,&quot;2022-12-25&quot;,&quot;Deliver to Doorstep&quot;);<br><br>INSERT into app.orders  values (6,1000,2053,&quot;Mattress&quot;,&quot;30/2 Acme Street, Pin : 9000, AZ-US&quot;,&quot;2022-12-31&quot;,&quot;Deliver to Doorstep&quot;);</pre><p>In Green Database</p><pre>mysqlsh -h mysql-green.private.mysqlvcn.oraclevcn.com -u admin -D mysql<br><br>CREATE DATABASE app;<br><br>select version();<br><br>+----------------+<br>| version()      |<br>+----------------+<br>| 8.4.8-u1-cloud |<br>+----------------+</pre><h4>Step 1</h4><p>Now, we will add Connections between Blue/Green MySQL DB and the OCI GoldenGate Deployment.</p><p>Go to OCI Console &gt; AI Database &gt; GoldenGate:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HZ_Ic2CIyBGY0-qicF2C4g.png" /><figcaption>Accessing GoldenGate</figcaption></figure><p>Create a connection:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zxSv950Zf8jaQWz94r43dA.png" /><figcaption>Connection creation</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0QzupA90Uyu0aVUhvXKrBw.png" /><figcaption>Connection creation (cont.)</figcaption></figure><p>Select <strong>Use Oracle-managed encryption key</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3wITn0bTQaF2YRMrncp3ig.png" /><figcaption>Advanced options</figcaption></figure><p>Assign to the MySQL Deployment:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q7LzpF5B7tOnPjDArLwceg.png" /><figcaption>Deployment assignment</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OJLQFTaNlGK7LZnnpGIe7A.png" /><figcaption>Deployment assignment (cont.)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*skmVTi7ScrbMM-n0vcTevg.png" /><figcaption>Assigned deployments</figcaption></figure><p>Do the same for the Green environment, and you will have 2 connections assigned to the OCI Goldengate instance:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zDcsVZhk2oDEcfNtdEwY0Q.png" /><figcaption>Assigned connections</figcaption></figure><h4>Step 2</h4><p>Login to OCI Goldengate MySQL Console URL:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f_FfsjwQI0J_ACXHgjPowQ.png" /><figcaption>GoldenGate</figcaption></figure><p>Open SSH tunnel, via bastion host for the OCI Goldengate console URL:</p><pre> ssh -fNT -L 8443:10.30.20.42:443 opc@141.147.162.126 -i ~/Download</pre><p>Open in browser: <a href="https://localhost:8443/">https://localhost:8443/</a></p><p>Then, check the connection to both Databases.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LPZ699dh78SEKImS8FBv9g.png" /><figcaption>DB Connections</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AafDGnfxOG9ir4k4XDynRw.png" /><figcaption>DB Connections (cont.)</figcaption></figure><p>Add the checkpoint table to the Green database:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*loGQAJmRc1wmmNxBGbkQkQ.png" /><figcaption>Checkpoint Table</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HLWFauFDkSmBnkbWIRRmlg.png" /><figcaption>Checkpoint Table (cont.)</figcaption></figure><h4>Step 3</h4><p>Add a Change Data Capture Extract (Capture) onto the Blue Database:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*caywb_3MPsp-vzhOLAO_vA.png" /><figcaption>Extracts menu</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vq5IIldYmC2MMuzZdGSZKg.png" /><figcaption>Adding an extract</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tGLW0DKlfuqDXcvjYYhX3w.png" /><figcaption>Parameter file for extracts</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5e49psTKCN7WAQ4pedXIOA.png" /><figcaption>Extracts</figcaption></figure><h4>Step 4</h4><p>Perform an initial Load with MySQL Shell from Blue to Green:</p><pre>mysqlsh -h mysql-blue.private.mysqlvcn.oraclevcn.com -u admin -D mysql<br><br>\js<br><br> --Dry Run--<br>util.copyInstance(&#39;mysql://admin@mysql-green.private.mysqlvcn.oraclevcn.com&#39;, {&quot;compatibility&quot;:[&quot;force_innodb&quot;, &quot;skip_invalid_accounts&quot;, &quot;strip_definers&quot;,&quot;strip_restricted_grants&quot;,&quot;strip_tablespaces&quot;,&quot;ignore_wildcard_grants&quot;,&quot;strip_invalid_grants&quot;,&quot;create_invisible_pks&quot;], updateGtidSet: &quot;append&quot;, users: &quot;true&quot;, threads: 8, consistent: &quot;true&quot; , dryRun:&quot;true&quot;})<br><br> --Actual Run--<br>util.copyInstance(&#39;mysql://admin@mysql-green.private.mysqlvcn.oraclevcn.com&#39;, {&quot;compatibility&quot;:[&quot;force_innodb&quot;, &quot;skip_invalid_accounts&quot;, &quot;strip_definers&quot;,&quot;strip_restricted_grants&quot;,&quot;strip_tablespaces&quot;,&quot;ignore_wildcard_grants&quot;,&quot;strip_invalid_grants&quot;,&quot;create_invisible_pks&quot;], updateGtidSet: &quot;append&quot;, users: &quot;true&quot;, threads: 8, consistent: &quot;true&quot; , dryRun:&quot;false&quot;})<br><br>---<br>Dump_metadata:<br>  Binlog_file: binary-log.002478<br>  Binlog_position: 197<br>  Executed_GTID_set: 8880f0e7-a14a-400c-8eda-1c9fd5c969be:1-15</pre><p>binary-log.002478 is the last position of the bin log, and we will start the CDC Replicat in GoldenGate after this position, to ensure the initial load and CDC replication is consistent and avoid duplicates.</p><h4>Step 5</h4><p>Create a Replicat for ongoing CDC:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gWScMWZJw2hfvmcXSvbI0g.png" /><figcaption>Replicats menu</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f4XB0vSoPSbM_wRc2YCQjA.png" /><figcaption>Adding Replicat</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NRplNamLNB3ufD9yg2ZvIg.png" /><figcaption>Adding Replicat (cont.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IgGnjzfLngWevk4puuUAtA.png" /><figcaption>Replicat parameter file</figcaption></figure><p>Alter Replicat and position to GTID:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VBUxis15tfmHluqLTwBeRg.png" /><figcaption>Start wiht Options</figcaption></figure><p>We will position the replicat to start after CSN, which is the bin log position of the completion of initial load — binary-log.002478</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fi9qfA0LlSpJuEA32jZAJQ.png" /><figcaption>Starting Replicat</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XXj2u785uAqWC6PDIZwKrg.png" /><figcaption>Replicats</figcaption></figure><p>Now we have initial load and CDC via Goldengate, we can discuss how the OCI Network Load Balancer can be used to connect productions apps and then failover to Blue environment seamlessly.</p><h4>Step 6</h4><p>Now, let’s look at orchestrating application connectivity via the OCI Load Balancer.</p><p>If you connect through the NLB endpoint mysql-nlb.private.mysqlvcn.oraclevcn.com, you connect to the Blue database. Your test application continues to connect, and you can test the green database while the replication is happening.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nVjvkhoRzLCLyg5V4KNIdg.png" /><figcaption>Backends</figcaption></figure><pre>mysqlsh -h mysql-nlb.private.mysqlvcn.oraclevcn.com -u admin -D mysql<br><br><br>select version();<br>+--------------+<br>| version()    |<br>+--------------+<br>| 8.0.45-cloud |<br>+--------------+</pre><p>When you’re ready to switch the green environment to Production, just flip the offline status of the Green environment to false and set the offline status of Blue to true .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i6ZB-ufVkxoYYWuZZRIeCg.png" /><figcaption>Setting status</figcaption></figure><pre>mysqlsh -h mysql-nlb.private.mysqlvcn.oraclevcn.com -u admin -D mysql<br><br>select version();<br>+ — — — — — — — — +<br>| version() |<br>+ — — — — — — — — +<br>| 8.4.8-u1-cloud |<br>+ — — — — — — — — +</pre><p>Once the switch-over is completed, you can stop the Replicat, extract from OCI GoldenGate and delete them.</p><h3>Final Thoughts</h3><p>Blue-green deployment is an effective strategy for minimizing risk during:</p><ul><li>Database upgrades</li><li>Schema changes</li><li>Application testing</li></ul><p>Using <strong>OCI MySQL HeatWave</strong>, you can implement this pattern using either:</p><ul><li><strong>MySQL Channel Replication</strong> for simpler environments</li><li><strong>Oracle GoldenGate</strong> for enterprise-grade replication and major upgrades</li></ul><p>Combined with <strong>OCI Network Load Balancer</strong>, these approaches allow seamless cutover between environments with minimal disruption to production workloads.</p><h3>Frequently Asked Questions (FAQs)</h3><h4>What problem does blue-green solve for MySQL HeatWave on OCI?</h4><p>It reduces upgrade and change risk by testing on a production-like Green copy and switching traffic only when ready.</p><h4>When should I choose MySQL Channel Replication vs. Oracle GoldenGate?</h4><p>Pick Channel Replication for minor upgrades and straightforward testing with minimal setup. Choose GoldenGate for major-version moves, enterprise resilience, and fine-grained CDC.</p><h4>How is downtime minimized during setup?</h4><p>You may briefly pause writes or stop the Blue system to take a consistent backup or use MySQL Shell copyInstance for initial load. After that, replication keeps Green current until cutover.</p><h4>How do applications avoid changing connection strings?</h4><p>Route all production traffic through an OCI Network Load Balancer and Private DNS. During cutover, toggle backend health/online status to point to Blue or Green without app changes.</p><h4>Can I upgrade from MySQL 8.0 directly to 9.x?</h4><p>No. Follow the supported path: 8.0 → 8.4 → 9.x. You can restore an 8.0 backup to 8.4, then upgrade to 9.x.</p><h4>What happens after the cutover?</h4><p>Disable or remove replication (Channel or GoldenGate), monitor the new production Green environment, and optionally repurpose the old Blue as the next staging target.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3088ab5b6ea9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/oracledevs/zero-downtime-blue-green-deployments-for-oci-mysql-heatwave-3088ab5b6ea9">Zero-Downtime Blue-Green Deployments for OCI MySQL HeatWave</a> was originally published in <a href="https://medium.com/oracledevs">Oracle Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Blue-Green Deployment for OCI PostgreSQL]]></title>
            <link>https://medium.com/oracledevs/blue-green-deployment-for-oci-postgresql-1068faa8ce1c?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/1068faa8ce1c</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[blue-green-deployment]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[oracle-cloud]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Sun, 21 Dec 2025 15:22:54 GMT</pubDate>
            <atom:updated>2026-01-06T15:40:12.412Z</atom:updated>
            <content:encoded><![CDATA[<p>A Blue-Green deployment for a Database service involves using a <strong>Blue</strong> environment (<strong>Production</strong>), as well as creating a <strong>Green</strong> environment alongside it (<strong>Staging</strong>), then facilitating ongoing replication from the Production Blue environment to the staging environment.</p><p>Blue/Green deployments work by creating a fully synchronized copy of your production database and running that copy as a separate staging environment.</p><p>In this article, I will show how you can build a Blue-Green deployment for your OCI PostgreSQL, for performing apps-related testing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_cdKOKYlwsjHpqHa32FyyA.jpeg" /><figcaption>Image Credit : <a href="https://unsplash.com/photos/blue-sky-with-white-clouds-yHWvPxLadRE">https://unsplash.com/photos/blue-sky-with-white-clouds-yHWvPxLadRE</a></figcaption></figure><h3>High-level steps to Orchestrate a Blue-Green deployment strategy with OCI PostgreSQL</h3><p><strong>Via Logical Replication for Major Version Upgrades or App Performance/Regression Testing</strong></p><p>Most DBAs &amp; DevOps people are already familiar with the Blue-Green deployment methodology when deploying a new version of a service. Setting up Blue-Green deployment in PostgreSQL is easier than one would expect, requiring a few OCI native components and logical replication with pglogical (or OCI Goldengate).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W3empk3h18akaKLFgJ5BJA.png" /><figcaption>Diagram 1 — Blue-Green Workflow</figcaption></figure><ol><li><strong>Initial and one-time setup:</strong></li></ol><ul><li>Create an additional <strong>DB2 OCI PostgreSQL cluster.</strong></li><li>Create<strong> OCI DNS Private Zone</strong> for the application, for your VCN’s DNS resolver. This zone will be used by the local applications to connect to OCI PostgreSQL via the OCI Load Balancer. If you have an on-premise DNS and need to extend your on-premise DNS to resolve this private zone, please refer to this documentation: <a href="https://docs.oracle.com/en/solutions/oci-best-practices-networking/private-dns-oci-and-premises-or-third-party-cloud.html">https://docs.oracle.com/en/solutions/oci-best-practices-networking/private-dns-oci-and-premises-or-third-party-cloud.html</a></li><li>Create an <strong>OCI Network Load Balancer</strong> for their applications to connect to. This LB will act as a proxy to the actual database system.</li><li>Have the Load Balancer backend point to the primary endpoint IP address of the database system (say DB1).</li></ul><p><strong>2. </strong>When we have new changes, do initial load for <strong>OCI Postgres </strong>between DB1 to DB2 using any of the logical data migration utilities like <strong>pg_dump, pg_restore.</strong></p><p><strong>3. </strong>Establish publication and subscription from DB1 to DB2 using <strong>pglogical</strong> (or <strong>OCI Goldengate</strong>).</p><p><strong>4. </strong>Have the app tests hit DB2 endpoint to perform read queries, validate and certify the changes.</p><p><strong>5. </strong>When DB2 appears ready for production consumption, orchestrate:</p><ul><li>Pause any app activity and pause pglogical replication (optional). Since pglogical is a logical replication tool, the DB2 is always available in Read-Write mode. Just for app testing, we are using read-only mode, to avoid conflicts and complex scenario of reverse replication from DB2 to DB1.</li><li>Update load balancer backend to the primary endpoint IP address of DB2.</li><li>Stop the replication setup between DB1 and DB2, by stopping the publisher and subscribers.</li></ul><p><strong>6. </strong>Production client apps are now connecting to the new Production environment (Green).</p><h3>Step-By-Step Orchestration of Blue Green Deployment:</h3><h4>Setup</h4><ol><li><strong>OCI PostgreSQL Database v15</strong>, which is the Blue environment aka Current Production.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QZJb1xM7teLFXrd9EgQbkg.png" /></figure><p><strong>IP address: 10.150.2.105</strong></p><p>2. <strong>OCI PostgreSQL Database v16</strong> which is the Green environment aka Staging.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RAe6Nb1LGg3xiCrnbEcPCQ.png" /></figure><p><strong>IP address: 10.150.2.62</strong></p><p>3. <strong>OCI Network Load Balancer,</strong> with the 2 database endpoints added as a backend set, fronted with a listener on <strong>tcp/5432.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NPyLJ8zcP_G07mUDG-2aNA.png" /></figure><p><strong>Listener</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FXnfogrnJiEyHLZMjsI2Kw.png" /></figure><p><strong>Backends</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9lxLvVRK4QLTLiUqngM27w.png" /></figure><p>Make sure the <strong>Blue</strong> (Production) environment is <strong>active,</strong> and the <strong>Green</strong> environment backend set is <strong>offline</strong> and drained.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-p6Nmwh4TvGjWxcb-MZwKA.png" /></figure><p>4. Create an OCI DNS Private Zone in your VCN’s DNS resolver.</p><p>In this case, I call my private zone <strong><em>postgres.local.</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W8j-ey0Cnlmj6FRVKjc7Ng.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kOIFCe9MycIb6HXG5WQVgQ.png" /></figure><p><strong>nlb-app.postgres.local</strong> is the main FQDN all apps will use to connect the backend database.</p><p><strong>nlb-app-blue.postgres.local</strong> is the FQDN for Blue Database, and this is not used by App connections.</p><p><strong>nlb-app-green.postgres.local</strong> is the FQDN for the Green Database and this is used by the App stack, which will perform the Read-only queries for validating the application performance.</p><p>We will use the OCI Network Load Balancers backend, set to make the blue backend IP offline, and activate the green IP. This incurs a small outage (in seconds depending on TTL of the DNS resolver) where the App moves from connecting to Blue Database to the Final Green Database, which is promoted as production.</p><h4>Create pglogical replication from DB1 to DB2</h4><p>Reference: <a href="https://docs.oracle.com/en/learn/oci-pglogical-extension/index.html#task-5-configure-the-source-database">https://docs.oracle.com/en/learn/oci-pglogical-extension/index.html#task-5-configure-the-source-database</a></p><p>In this example, we are demonstrating using a simple table: <a href="https://github.com/shadabshaukat/STAND/blob/main/postgres.sql">https://github.com/shadabshaukat/STAND/blob/main/postgres.sql</a></p><p><strong>#################<br> — Source Blue v15 — <br>#################</strong></p><p>Hostname: primary.umiokgnhd4hlpov7xncfwymgxv4pgq.postgresql.us-sanjose-1.oci.oraclecloud.com<br>Version: 15.12</p><ul><li>Run the following query to grant permissions on the source database, to enable logical replication.</li></ul><pre>CREATE EXTENSION pglogical;<br>show oci.admin_enabled_extensions ;</pre><pre>alter role postgres with replication;<br>grant EXECUTE on FUNCTION pg_catalog.pg_replication_origin_session_reset() to postgres ;<br>grant EXECUTE on FUNCTION pg_catalog.pg_replication_origin_session_setup to postgres ;<br>grant all on FUNCTION pg_catalog.pg_replication_origin_session_setup to postgres;</pre><p><em>Note: postgres is the admin user created during the database setup process.</em></p><ul><li>Create the publisher node on the source database.</li></ul><pre>SELECT pglogical.create_node(node_name := &#39;provider1&#39;,dsn :=&#39;host=primary.umiokgnhd4hlpov7xncfwymgxv4pgq.postgresql.us-sanjose-1.oci.oraclecloud.com port=5432 user=postgres password=RAbbithole1234## dbname=postgres&#39;);</pre><pre>node_name: Specify the name of the publisher to be created on the source database.<br>host: Enter the fully qualified domain name (FQDN) of the source database.<br>port_number: Provide the port on which the source database is running.<br>database_name: Specify the database where the publication will be created.</pre><ul><li>Include all tables in the public schema to the default replication set.</li></ul><pre>SELECT pglogical.replication_set_add_all_tables(&#39;default&#39;, ARRAY[&#39;public&#39;]);</pre><pre>replication_set_add_all_tables<br> - - - - - - - - - - - - - - - - <br> t<br>(1 row)</pre><p><strong>#################<br> — Target Green v16 — <br>#################</strong></p><p>Hostname: primary.46mtfkxsj6337nqvx2de6gq3a57m4a.postgresql.us-sanjose-1.oci.oraclecloud.com<br>Version: 16.8</p><ul><li>Run the following query to grant permissions on the target database to enable logical replication.</li></ul><pre>CREATE EXTENSION pglogical;<br>show oci.admin_enabled_extensions ;</pre><pre>alter role postgres with replication;<br>grant EXECUTE on FUNCTION pg_catalog.pg_replication_origin_session_reset() to postgres ;<br>grant EXECUTE on FUNCTION pg_catalog.pg_replication_origin_session_setup to postgres ;<br>grant all on FUNCTION pg_catalog.pg_replication_origin_session_setup to postgres;</pre><p><em>Note: postgres is the admin user created during the database setup process.</em></p><ul><li>Create the subscriber node on the target database.</li></ul><pre>SELECT pglogical.create_node(node_name := &#39;subscriber1&#39;,dsn :=&#39;host=primary.46mtfkxsj6337nqvx2de6gq3a57m4a.postgresql.us-sanjose-1.oci.oraclecloud.com port=5432 user=postgres password=RAbbithole1234## dbname=postgres&#39;);</pre><pre>node_name: Define the name of the subscriber on the target database.<br>host: Enter the fully qualified domain name (FQDN) of the target database.<br>port_number: Enter the port on which the target database is running.<br>database_name: Provide the name of the database where the subscription will be created.</pre><ul><li>Set Schema-only on the target database. This can also be done with pg_dump and pg_restore or psql.</li></ul><pre>CREATE TABLE orders (<br> order_id SERIAL PRIMARY KEY,<br> customer_id INTEGER,<br> product_id INTEGER,<br> product_description VARCHAR(500),<br> order_delivery_address VARCHAR(500),<br> order_date_taken DATE,<br> order_misc_notes VARCHAR(500)<br>);</pre><pre>CREATE OR REPLACE FUNCTION add_random_orders(n INTEGER) RETURNS TEXT AS $$<br>DECLARE<br> i INTEGER := 1;<br> v_customer_id INTEGER;<br> v_product_id INTEGER;<br> v_product_description VARCHAR(500);<br> v_order_delivery_address VARCHAR(500);<br> v_order_date_taken DATE;<br> v_order_misc_notes VARCHAR(500);<br>BEGIN<br> WHILE i &lt;= n LOOP<br> v_customer_id := floor(random() * 100) + 1;<br> v_product_id := floor(random() * 50) + 1;<br> v_product_description := CONCAT(&#39;Product &#39;, floor(random() * 10) + 1);<br> v_order_delivery_address := CONCAT(&#39;Address &#39;, floor(random() * 10) + 1);<br> v_order_date_taken := CURRENT_DATE - (floor(random() * 30) || &#39; days&#39;)::INTERVAL;<br> v_order_misc_notes := CONCAT(&#39;Note &#39;, floor(random() * 10) + 1);<br>INSERT INTO orders (customer_id, product_id, product_description, order_delivery_address, order_date_taken, order_misc_notes)<br> VALUES (v_customer_id, v_product_id, v_product_description, v_order_delivery_address, v_order_date_taken, v_order_misc_notes);<br>i := i + 1;<br> END LOOP;<br>RETURN n || &#39; random orders added.&#39;;<br>EXCEPTION<br> WHEN OTHERS THEN<br> RAISE EXCEPTION &#39;Error: %&#39;, SQLERRM;<br>END;<br>$$ LANGUAGE plpgsql;</pre><ul><li>Create the subscription on the subscriber node, which will initiate the background synchronization and replication processes.</li></ul><pre>SELECT pglogical.create_subscription(subscription_name := &#39;subscription1&#39;,provider_dsn := &#39;host=primary.umiokgnhd4hlpov7xncfwymgxv4pgq.postgresql.us-sanjose-1.oci.oraclecloud.com port=5432 user=postgres password=RAbbithole1234## dbname=postgres sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem&#39;);</pre><pre>subscription_name: Provide the name of the subscription.<br>host: Provide the FQDN of the source database.<br>port_number: Provide the port on which the target database is running.<br>database_name: Provide the name of the source database.</pre><p><em>Note: Be sure to use sslmode=verify-full and sslrootcert = /etc/opt/postgresql/ca-bundle.pem in subscription creation string to prevent any connection failures.</em></p><pre>SELECT pglogical.wait_for_subscription_sync_complete(&#39;subscription1&#39;);</pre><p><strong>#################<br> — Target Green v16 — <br>#################</strong></p><ul><li>Run the following statement to check the status of your subscription on your target database.</li></ul><pre>select * from pglogical.show_subscription_status();</pre><pre>subscription_name | status | provider_node |<br> provider_dsn |<br> slot_name | replication_sets | forward_origins<br> - - - - - - - - - -+ - - - - - - -+ - - - - - - - -+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <br> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+<br> - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - -+ - - - - - - - - -<br> subscription1 | replicating | provider1 | host=primary.umiokgnhd4hlpov7xncfwymgxv4pgq.postgresql.us-sanjose-1.oci.oraclecloud.c<br>om port=5432 user=postgres password=RAbbithole1234## dbname=postgres sslmode=verify-full sslrootcert=/etc/opt/postgresql/ca-bundle.pem |<br> pgl_postgres_provider1_subscription1 | {default,default_insert_only,ddl_sql} | {all}<br>(1 row)</pre><p><strong>#################<br> — Source Blue v15 — <br>#################</strong></p><ul><li>Run the following statement to check the status of your replication on your source database.</li></ul><pre>SELECT * FROM pg_stat_replication;</pre><pre>pid | usesysid | usename | application_name | client_addr | client_hostname | client_port | backend_start | backend<br>_xmin | state | sent_lsn | write_lsn | flush_lsn | replay_lsn | write_lag | flush_lag | replay_lag | sync_priority | sync_state |<br> reply_time<br> - - - -+ - - - - - + - - - - - + - - - - - - - - - + - - - - - - - + - - - - - - - - -+ - - - - - - -+ - - - - - - - - - - - - - - - -+ - - - - <br> - - - + - - - - - -+ - - - - - -+ - - - - - -+ - - - - - -+ - - - - - - + - - - - - -+ - - - - - -+ - - - - - - + - - - - - - - -+ - - - - - - + - <br> - - - - - - - - - - - - - - -<br> 18569 | 16387 | postgres | subscription1 | 10.150.2.196 | | 1247 | 2025–12–02 04:50:07.242335+00 |<br> | streaming | 0/16BAB50 | 0/16BAB50 | 0/16BAB50 | 0/16BAB50 | | | | 0 | async | 2<br>025–12–02 05:09:09.626248+00<br>(1 row)</pre><p><strong>#################<br> — Target Green v16 — <br> #################</strong><br> <strong>A. Stop or Start the Replication</strong></p><ul><li>You can disable the subscription using the following command on your target database.</li></ul><pre>select pglogical.alter_subscription_disable(&#39;subscription_name&#39;);</pre><pre>-- Target --<br>select pglogical.alter_subscription_disable(&#39;subscription1&#39;);</pre><ul><li>You can enable the subscription using the following command on your target database.</li></ul><pre>select pglogical.alter_subscription_enable(&#39;subscription_name&#39;);</pre><pre>-- Target --<br>select pglogical.alter_subscription_enable(&#39;subscription1&#39;);</pre><p><em>Note: In subscription_name, enter the name of the subscription created at the target.</em></p><p><strong>B. Drop the Subscription</strong></p><pre>select pglogical.drop_subscription(&#39;subscription_name&#39;);</pre><pre>-- Target --<br>select pglogical.drop_subscription(&#39;subscription1&#39;);</pre><p><em>Note: In subscription_name, enter the name of the subscription created at the target.</em></p><p><strong>C. Drop the Nodes</strong></p><ul><li>To drop the node from your Source or Target database, execute the following command :</li></ul><pre>select pglogical.drop_node(&#39;node_name&#39;);</pre><pre>Note: In node_name, enter the node name created in source/target database.</pre><pre>-- Source --<br>select pglogical.drop_node(&#39;provider1&#39;);</pre><pre>-- Target --<br>select pglogical.drop_node(&#39;subscriber1&#39;);</pre><h3>Orchestration of Blue-Green Deployment by Updating OCI Network Load Balancer Backend</h3><h4>Scenario 1 — Everything is business as usual (Refer Diagram 1 — Blue-Green Workflow above)</h4><p>All the production clients are connected to Blue (Production) DB environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dKkpEO4TEe0qjm9BWbA3ug.png" /></figure><p>NLB has the Blue environment active in the backend set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GBisGuZJa3t3Qvf5ziDGSw.png" /></figure><p>We will use psql for the testing. Let’s add aliases to the app host to make the testing a bit simpler.</p><pre>alias pgblue=&#39;PGPASSWORD=YourPasswor@123# psql -h nlb-app-blue.postgres.local -U postgres -d postgres&#39;<br>alias pggreen=&#39;PGPASSWORD=YourPasswor@123# psql -h nlb-app-green.postgres.local -U postgres -d postgres&#39;<br>alias pgnlb=&#39;PGPASSWORD=YourPasswor@123# psql -h nlb-app.postgres.local -U postgres -d postgres&#39;</pre><p>Your apps are connecting to the v15 Blue database via this endpoint.</p><pre>$ alias | grep pgnlb<br>alias pgnlb=&#39;PGPASSWORD=YourPasswor@123# psql -h nlb-app.postgres.local -U postgres -d postgres&#39;<br><br>$ pgnlb<br><br>psql (17.7, server 15.12)<br>SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off, ALPN: none)<br>Type &quot;help&quot; for help.<br>## The Server we&#39;re connecting to is the v15.12 which is Blue</pre><h4>Scenario 2 — The replication is ongoing to DB2 and Your Testing Clients connect to DB2</h4><p>Your testing apps are now connecting to the v16 Green database via this endpoint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QD37DzmK_J8hmYBAPnHI-g.png" /></figure><pre>$ alias | grep pggreen<br>alias pggreen=&#39;PGPASSWORD=YourPasswor@123# psql -h nlb-app-green.postgres.local -U postgres -d postgres&#39;<br><br>$ pggreen<br>psql (17.7, server 16.8)<br>SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off, ALPN: none)<br>Type &quot;help&quot; for help.</pre><h4>Scenario 3 — The replication is stopped to Green (DB2), Green is promoted as Production and your Production clients connect to the Green environment</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IQSQ4wB5Yek-uBnNYUNO8w.png" /></figure><p>Flip the OCI Network Load Balancer backend set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6jG-167BdUhPlwxI0rq6gg.png" /></figure><p>Make the current Blue IP backend offline, and drain it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AHy4LDhaR4PHvQVCJe754w.png" /></figure><p>Save the changes.</p><p>For a brief moment, there is no connectivity from the app to the database, and your business users should be notified that there will be a brief outage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PhW8oQ7sdluc-o3Q6r9yrg.png" /></figure><p>If you run pgnlb, it will hang, as there is no backend IP to connect to.</p><pre>[opc@linux-bastion ~]$ pgnlb<br></pre><p>Now, let’s make the Green environment as online from the backend set, and re-connect the apps.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fzrY3GAOrXq8ZEt05dl7bw.png" /></figure><p>Select Save Changes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8N634jFiTczydWWPh0riFA.png" /></figure><p>Now, connect with pgnlb.</p><pre>[opc@linux-bastion ~]$ pgnlb<br>psql (17.7, server 16.8)<br>SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off, ALPN: none)<br>Type &quot;help&quot; for help.</pre><p>You can see that pgnlb is now connecting to the new upgraded v16 version, which is the Green environment.</p><h4>Advantages of Blue-Green testing</h4><p>· Perform testing of major version upgrades (e.g. pgsql 13 -&gt; 15)</p><p>· Major patching/testing with a production-like copy</p><p>· Validate application behavior against new database versions/configs</p><p>· Support phased testing (read-only validation, performance testing)</p><p>· Decouple app endpoint from database endpoint using a Load Balancer</p><h4>Conclusion</h4><p>We’ve successfully created an end-to-end Blue-Green Testing Methodolgy for OCI PostgreSQL.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1068faa8ce1c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/oracledevs/blue-green-deployment-for-oci-postgresql-1068faa8ce1c">Blue-Green Deployment for OCI PostgreSQL</a> was originally published in <a href="https://medium.com/oracledevs">Oracle Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why PostgreSQL Full-Text Search is all you need in 2025]]></title>
            <link>https://shadabshaukat.medium.com/why-postgresql-full-text-search-is-all-you-need-in-2025-7cb1f09a05e1?source=rss-51b8ef0ed6f4------2</link>
            <guid isPermaLink="false">https://medium.com/p/7cb1f09a05e1</guid>
            <category><![CDATA[vector-database]]></category>
            <category><![CDATA[elasticsearch]]></category>
            <category><![CDATA[ts-vector]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[full-text-search]]></category>
            <dc:creator><![CDATA[Shadab Mohammad]]></dc:creator>
            <pubDate>Wed, 27 Aug 2025 03:31:37 GMT</pubDate>
            <atom:updated>2025-08-28T06:22:25.131Z</atom:updated>
            <content:encoded><![CDATA[<h3>Why PostgreSQL Full Text Search is all you need in 2025</h3><p>Why PostgreSQL Is All You Need for Search in 2025 (No Elasticsearch, Solr OpenSearch Required)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x-rTLqoTxSN6jq7u8VZyRA.png" /></figure><p>PostgreSQL has quietly become the backbone of some of the world’s most effective search experiences. Starting with version 16, its Full-Text Search (FTS) engine is smarter, faster, and more flexible than ever.</p><p>In this guide, we’ll build a search-centric online shop and demonstrate how PostgreSQL can deliver advanced search, typo tolerance, ranking, and highlighting — all <strong>natively in SQL</strong>, without relying on external services.</p><h3>🔎 Introduction</h3><p>Search defines the user experience in any online store. Customers expect results that are fast, accurate, typo-tolerant, and ranked by relevance. With PostgreSQL 16+, you get <strong>Google-like search capabilities out-of-the-box</strong>:</p><ul><li>Natural language understanding</li><li>Tolerance for misspellings</li><li>Flexible ranking and weighting</li><li>Sub-second response times</li></ul><p>And the best part? It’s <strong>all inside PostgreSQL</strong> — no extra SaaS costs, no additional latency.</p><p>This tutorial walks through <strong>production-ready SQL patterns</strong> you can use today in your e-commerce or SaaS product.</p><h3>🚀 Why PostgreSQL Full-Text Search?</h3><ul><li><strong>No external search engine</strong>: Everything runs in SQL — no Elasticsearch, no SaaS bills.</li><li><strong>Powerful features</strong>: Boolean queries, ranked results, snippets, and phrase search.</li><li><strong>Fuzzy matching</strong>: Catch typos like <em>iphne → iPhone</em>.</li><li><strong>Flexible schema support</strong>: Works across text, arrays, and multilingual content.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I0QCkBjCkHbEUJz49ZZy7g.png" /></figure><h3>🛒 Designing the Online Shop Schema</h3><p>We’ll keep it simple with one table, but the same design applies to large catalogs.</p><pre>-- 1) Create the products table<br>CREATE TABLE products (<br>id SERIAL PRIMARY KEY,<br>title TEXT NOT NULL,<br>description TEXT,<br>brand TEXT,<br>tags TEXT[],<br>price NUMERIC(10,2)<br>);<br><br>-- 2) Add the column (no generated expression)<br>ALTER TABLE products<br>  ADD COLUMN search_vector tsvector;<br><br>-- 3) Create a trigger function that builds the weighted tsvector<br>CREATE OR REPLACE FUNCTION products_tsvector_update()<br>RETURNS trigger<br>LANGUAGE plpgsql<br>AS $$<br>BEGIN<br>  NEW.search_vector :=<br>      setweight(to_tsvector(&#39;pg_catalog.english&#39;, coalesce(NEW.title,&#39;&#39;)), &#39;A&#39;)<br>   || setweight(to_tsvector(&#39;pg_catalog.english&#39;, coalesce(NEW.brand,&#39;&#39;)), &#39;B&#39;)<br>   || setweight(to_tsvector(&#39;pg_catalog.english&#39;, coalesce(NEW.description,&#39;&#39;)), &#39;C&#39;)<br>   || setweight(to_tsvector(&#39;pg_catalog.english&#39;, array_to_string(NEW.tags,&#39; &#39;)), &#39;D&#39;);<br>  RETURN NEW;<br>END;<br>$$;<br><br>-- 4) Hook it up for INSERT/UPDATE<br>CREATE TRIGGER t_products_tsvector_update<br>BEFORE INSERT OR UPDATE ON products<br>FOR EACH ROW<br>EXECUTE FUNCTION products_tsvector_update();<br></pre><h4>🌱 Seeding Example Data</h4><p>A realistic dataset makes the queries more accurate.</p><p>The below INSERT sql query inserts <strong>108</strong> unique electronics product into the productstable.</p><pre>-- If Data already exists in the table, truncate the table and reset the identity column sequence<br>--TRUNCATE TABLE products RESTART IDENTITY;<br><br>INSERT INTO products (title, description, brand, tags, price) VALUES<br>(&#39;Apple iPhone 15&#39;, &#39;Latest Apple iPhone with improved camera and battery.&#39;, &#39;Apple&#39;, ARRAY[&#39;smartphone&#39;,&#39;electronics&#39;,&#39;flagship&#39;], 1200.00),<br>(&#39;Samsung Galaxy S24&#39;, &#39;Premium Android phone with powerful features.&#39;, &#39;Samsung&#39;, ARRAY[&#39;smartphone&#39;,&#39;android&#39;,&#39;flagship&#39;], 1100.00),<br>(&#39;Sony WH-1000XM5 Headphones&#39;, &#39;Top-rated noise-cancelling headphones for music lovers.&#39;, &#39;Sony&#39;, ARRAY[&#39;headphones&#39;,&#39;audio&#39;,&#39;wireless&#39;], 400.00),<br>(&#39;Dell XPS 13 Laptop&#39;, &#39;Sleek ultrabook, ideal for productivity and travel.&#39;, &#39;Dell&#39;, ARRAY[&#39;laptop&#39;,&#39;ultrabook&#39;,&#39;windows&#39;], 1800.00),<br>(&#39;Apple MacBook Air M3&#39;, &#39;Apple silicon, fast and lightweight. Perfect for students.&#39;, &#39;Apple&#39;, ARRAY[&#39;laptop&#39;,&#39;macos&#39;,&#39;apple silicon&#39;], 1800.00),<br>(&#39;Google Pixel Buds Pro&#39;, &#39;Google&#39;&#39;s premium wireless earbuds with ANC.&#39;, &#39;Google&#39;, ARRAY[&#39;earbuds&#39;,&#39;audio&#39;,&#39;wireless&#39;], 250.00),<br>(&#39;Apple iPad Air M2&#39;, &#39;Powerful and portable tablet with Apple silicon and vibrant display.&#39;, &#39;Apple&#39;, ARRAY[&#39;tablet&#39;,&#39;electronics&#39;], 699.00),<br>(&#39;Apple MacBook Pro 14&#39;, &#39;Professional-grade laptop with Apple silicon and Liquid Retina display.&#39;, &#39;Apple&#39;, ARRAY[&#39;laptop&#39;,&#39;macos&#39;,&#39;pro&#39;], 2299.00),<br>(&#39;Apple Watch Series 9&#39;, &#39;Advanced smartwatch with health and fitness tracking.&#39;, &#39;Apple&#39;, ARRAY[&#39;smartwatch&#39;,&#39;wearable&#39;,&#39;fitness&#39;], 399.00),<br>(&#39;Apple AirPods Pro 2&#39;, &#39;Premium wireless earbuds with active noise cancellation.&#39;, &#39;Apple&#39;, ARRAY[&#39;earbuds&#39;,&#39;audio&#39;,&#39;wireless&#39;], 249.00),<br>(&#39;Apple HomePod mini&#39;, &#39;Smart speaker with Siri and powerful sound in a compact size.&#39;, &#39;Apple&#39;, ARRAY[&#39;speaker&#39;,&#39;smart home&#39;,&#39;audio&#39;], 129.00),<br>(&#39;Apple TV 4K&#39;, &#39;Streaming device with 4K HDR and Dolby Atmos support.&#39;, &#39;Apple&#39;, ARRAY[&#39;streaming&#39;,&#39;tv&#39;,&#39;entertainment&#39;], 179.00),<br>(&#39;Samsung Galaxy S24 Ultra&#39;, &#39;Flagship Android phone with advanced camera and display.&#39;, &#39;Samsung&#39;, ARRAY[&#39;smartphone&#39;,&#39;android&#39;,&#39;flagship&#39;], 1299.00),<br>(&#39;Samsung Galaxy Tab S9&#39;, &#39;High-performance Android tablet with AMOLED display.&#39;, &#39;Samsung&#39;, ARRAY[&#39;tablet&#39;,&#39;android&#39;], 899.00),<br>(&#39;Samsung Galaxy Book3&#39;, &#39;Powerful Windows ultrabook with sleek design.&#39;, &#39;Samsung&#39;, ARRAY[&#39;laptop&#39;,&#39;windows&#39;,&#39;ultrabook&#39;], 1499.00),<br>(&#39;Samsung Galaxy Watch6&#39;, &#39;Smartwatch with advanced health tracking and AMOLED display.&#39;, &#39;Samsung&#39;, ARRAY[&#39;smartwatch&#39;,&#39;wearable&#39;], 349.00),<br>(&#39;Samsung Galaxy Buds2 Pro&#39;, &#39;Wireless earbuds with Hi-Fi sound and ANC.&#39;, &#39;Samsung&#39;, ARRAY[&#39;earbuds&#39;,&#39;audio&#39;,&#39;wireless&#39;], 229.00),<br>(&#39;Samsung Neo QLED TV&#39;, &#39;High-end QLED 4K TV with HDR and vivid colors.&#39;, &#39;Samsung&#39;, ARRAY[&#39;tv&#39;,&#39;display&#39;,&#39;home theater&#39;], 1999.00),<br>(&#39;Google Pixel 8&#39;, &#39;Next-gen Google smartphone with AI-powered features.&#39;, &#39;Google&#39;, ARRAY[&#39;smartphone&#39;,&#39;android&#39;,&#39;flagship&#39;], 899.00),<br>(&#39;Google Pixel 8 Pro&#39;, &#39;Premium Pixel with advanced camera and AI tools.&#39;, &#39;Google&#39;, ARRAY[&#39;smartphone&#39;,&#39;android&#39;,&#39;flagship&#39;], 1099.00),<br>(&#39;Google Pixel Tablet&#39;, &#39;Versatile Android tablet designed for home and productivity.&#39;, &#39;Google&#39;, ARRAY[&#39;tablet&#39;,&#39;android&#39;], 599.00),<br>(&#39;Google Nest Hub 2&#39;, &#39;Smart display with Google Assistant integration.&#39;, &#39;Google&#39;, ARRAY[&#39;smart home&#39;,&#39;display&#39;,&#39;assistant&#39;], 129.00),<br>(&#39;Google Nest WiFi Pro&#39;, &#39;Mesh Wi-Fi 6E system for whole-home coverage.&#39;, &#39;Google&#39;, ARRAY[&#39;wifi&#39;,&#39;mesh&#39;,&#39;networking&#39;], 399.00),<br>(&#39;Google Chromecast 4K&#39;, &#39;Streaming device with Google TV support.&#39;, &#39;Google&#39;, ARRAY[&#39;streaming&#39;,&#39;entertainment&#39;,&#39;tv&#39;], 49.00),<br>(&#39;Sony WF-1000XM5&#39;, &#39;Premium earbuds with market-leading noise cancellation.&#39;, &#39;Sony&#39;, ARRAY[&#39;earbuds&#39;,&#39;audio&#39;,&#39;wireless&#39;], 279.00),<br>(&#39;Sony PlayStation 5&#39;, &#39;Next-gen gaming console with immersive graphics.&#39;, &#39;Sony&#39;, ARRAY[&#39;gaming&#39;,&#39;console&#39;], 499.00),<br>(&#39;Sony A7 IV&#39;, &#39;Mirrorless full-frame camera with 4K recording.&#39;, &#39;Sony&#39;, ARRAY[&#39;camera&#39;,&#39;mirrorless&#39;,&#39;photography&#39;], 2499.00),<br>(&#39;Sony ZV-E10&#39;, &#39;Vlogger-friendly mirrorless camera with interchangeable lenses.&#39;, &#39;Sony&#39;, ARRAY[&#39;camera&#39;,&#39;mirrorless&#39;,&#39;video&#39;], 799.00),<br>(&#39;Sony Bravia XR OLED&#39;, &#39;High-end OLED TV with XR processor for cinematic quality.&#39;, &#39;Sony&#39;, ARRAY[&#39;tv&#39;,&#39;display&#39;,&#39;home theater&#39;], 2299.00),<br>(&#39;Sony HT-A7000&#39;, &#39;Dolby Atmos soundbar with immersive surround sound.&#39;, &#39;Sony&#39;, ARRAY[&#39;audio&#39;,&#39;soundbar&#39;,&#39;home theater&#39;], 1199.00),<br>(&#39;Dell XPS 15&#39;, &#39;High-performance laptop for creators and professionals.&#39;, &#39;Dell&#39;, ARRAY[&#39;laptop&#39;,&#39;windows&#39;,&#39;ultrabook&#39;], 1999.00),<br>(&#39;Dell Alienware m16&#39;, &#39;Gaming laptop with powerful GPU and cooling system.&#39;, &#39;Dell&#39;, ARRAY[&#39;laptop&#39;,&#39;gaming&#39;,&#39;windows&#39;], 2499.00),<br>(&#39;Dell UltraSharp 27 4K&#39;, &#39;Professional-grade 4K monitor with excellent color accuracy.&#39;, &#39;Dell&#39;, ARRAY[&#39;monitor&#39;,&#39;display&#39;,&#39;4k&#39;], 699.00),<br>(&#39;Dell Inspiron 14&#39;, &#39;Affordable and reliable laptop for everyday tasks.&#39;, &#39;Dell&#39;, ARRAY[&#39;laptop&#39;,&#39;windows&#39;,&#39;budget&#39;], 799.00),<br>(&#39;HP Spectre x360 14&#39;, &#39;Premium convertible laptop with OLED display.&#39;, &#39;HP&#39;, ARRAY[&#39;laptop&#39;,&#39;windows&#39;,&#39;2-in-1&#39;], 1499.00),<br>(&#39;HP Omen 16&#39;, &#39;Gaming laptop with dedicated graphics and RGB keyboard.&#39;, &#39;HP&#39;, ARRAY[&#39;laptop&#39;,&#39;gaming&#39;,&#39;windows&#39;], 1699.00),<br>(&#39;HP Envy 15&#39;, &#39;Stylish and powerful laptop for creators.&#39;, &#39;HP&#39;, ARRAY[&#39;laptop&#39;,&#39;windows&#39;,&#39;creative&#39;], 1399.00),<br>(&#39;HP Dragonfly G4&#39;, &#39;Ultra-light business laptop with long battery life.&#39;, &#39;HP&#39;, ARRAY[&#39;laptop&#39;,&#39;business&#39;,&#39;windows&#39;], 1899.00),<br>(&#39;Lenovo ThinkPad X1 Carbon&#39;, &#39;Business-class ultrabook with durability and speed.&#39;, &#39;Lenovo&#39;, ARRAY[&#39;laptop&#39;,&#39;business&#39;,&#39;windows&#39;], 1799.00),<br>(&#39;Lenovo Legion 7&#39;, &#39;High-performance gaming laptop with RGB keyboard.&#39;, &#39;Lenovo&#39;, ARRAY[&#39;laptop&#39;,&#39;gaming&#39;,&#39;windows&#39;], 2199.00),<br>(&#39;Lenovo Yoga 9i&#39;, &#39;Convertible 2-in-1 laptop with touch display.&#39;, &#39;Lenovo&#39;, ARRAY[&#39;laptop&#39;,&#39;2-in-1&#39;,&#39;windows&#39;], 1399.00),<br>(&#39;Lenovo IdeaPad 5&#39;, &#39;Affordable laptop for students and casual use.&#39;, &#39;Lenovo&#39;, ARRAY[&#39;laptop&#39;,&#39;budget&#39;,&#39;windows&#39;], 699.00),<br>(&#39;ASUS ROG Zephyrus G14&#39;, &#39;Gaming laptop with powerful GPU and lightweight design.&#39;, &#39;ASUS&#39;, ARRAY[&#39;laptop&#39;,&#39;gaming&#39;,&#39;windows&#39;], 1899.00),<br>(&#39;ASUS ZenBook 14 OLED&#39;, &#39;Slim ultrabook with OLED display and long battery.&#39;, &#39;ASUS&#39;, ARRAY[&#39;laptop&#39;,&#39;ultrabook&#39;,&#39;windows&#39;], 1399.00),<br>(&#39;ASUS ProArt PA279CV Monitor&#39;, &#39;Professional 4K monitor for creators.&#39;, &#39;ASUS&#39;, ARRAY[&#39;monitor&#39;,&#39;display&#39;,&#39;4k&#39;], 499.00),<br>(&#39;ASUS RT-AX88U Router&#39;, &#39;Wi-Fi 6 router with high throughput.&#39;, &#39;ASUS&#39;, ARRAY[&#39;router&#39;,&#39;wifi&#39;,&#39;networking&#39;], 299.00),<br>(&#39;Acer Swift 5&#39;, &#39;Lightweight ultrabook for mobility.&#39;, &#39;Acer&#39;, ARRAY[&#39;laptop&#39;,&#39;ultrabook&#39;,&#39;windows&#39;], 1199.00),<br>(&#39;Acer Predator Helios 16&#39;, &#39;Gaming laptop with high refresh display.&#39;, &#39;Acer&#39;, ARRAY[&#39;laptop&#39;,&#39;gaming&#39;,&#39;windows&#39;], 1799.00),<br>(&#39;Acer Nitro 5&#39;, &#39;Budget-friendly gaming laptop.&#39;, &#39;Acer&#39;, ARRAY[&#39;laptop&#39;,&#39;gaming&#39;,&#39;windows&#39;], 999.00),<br>(&#39;Bose QuietComfort Ultra&#39;, &#39;Noise-cancelling headphones with immersive audio.&#39;, &#39;Bose&#39;, ARRAY[&#39;headphones&#39;,&#39;audio&#39;,&#39;wireless&#39;], 399.00),<br>(&#39;Bose SoundLink Flex&#39;, &#39;Portable Bluetooth speaker with waterproof design.&#39;, &#39;Bose&#39;, ARRAY[&#39;speaker&#39;,&#39;audio&#39;,&#39;wireless&#39;], 149.00),<br>(&#39;Sennheiser Momentum 4&#39;, &#39;High-end wireless headphones with long battery life.&#39;, &#39;Sennheiser&#39;, ARRAY[&#39;headphones&#39;,&#39;audio&#39;,&#39;wireless&#39;], 379.00),<br>(&#39;Sennheiser IE 600&#39;, &#39;Premium in-ear monitors for audiophiles.&#39;, &#39;Sennheiser&#39;, ARRAY[&#39;earbuds&#39;,&#39;audio&#39;,&#39;wired&#39;], 699.00),<br>(&#39;JBL Charge 5&#39;, &#39;Portable Bluetooth speaker with long playtime.&#39;, &#39;JBL&#39;, ARRAY[&#39;speaker&#39;,&#39;audio&#39;,&#39;wireless&#39;], 179.00),<br>(&#39;JBL Flip 6&#39;, &#39;Compact waterproof Bluetooth speaker.&#39;, &#39;JBL&#39;, ARRAY[&#39;speaker&#39;,&#39;audio&#39;,&#39;wireless&#39;], 129.00),<br>(&#39;JBL Bar 5.1&#39;, &#39;Soundbar with surround sound and wireless subwoofer.&#39;, &#39;JBL&#39;, ARRAY[&#39;soundbar&#39;,&#39;audio&#39;,&#39;home theater&#39;], 499.00),<br>(&#39;DJI Mini 4 Pro&#39;, &#39;Compact drone with 4K camera and obstacle avoidance.&#39;, &#39;DJI&#39;, ARRAY[&#39;drone&#39;,&#39;camera&#39;,&#39;aerial&#39;], 999.00),<br>(&#39;DJI Air 3&#39;, &#39;Drone with dual camera system for professional use.&#39;, &#39;DJI&#39;, ARRAY[&#39;drone&#39;,&#39;camera&#39;,&#39;aerial&#39;], 1299.00),<br>(&#39;DJI Osmo Pocket 3&#39;, &#39;Handheld camera with 4K stabilization.&#39;, &#39;DJI&#39;, ARRAY[&#39;camera&#39;,&#39;handheld&#39;,&#39;video&#39;], 499.00),<br>(&#39;Canon EOS R6 Mark II&#39;, &#39;Mirrorless full-frame camera with excellent autofocus.&#39;, &#39;Canon&#39;, ARRAY[&#39;camera&#39;,&#39;mirrorless&#39;,&#39;photography&#39;], 2499.00),<br>(&#39;Canon PIXMA G6020&#39;, &#39;All-in-one inkjet printer with refillable tanks.&#39;, &#39;Canon&#39;, ARRAY[&#39;printer&#39;,&#39;inkjet&#39;,&#39;office&#39;], 299.00),<br>(&#39;Nikon Z6 II&#39;, &#39;Mirrorless camera with 24MP sensor and 4K video.&#39;, &#39;Nikon&#39;, ARRAY[&#39;camera&#39;,&#39;mirrorless&#39;,&#39;photography&#39;], 1999.00),<br>(&#39;Nikon Z8&#39;, &#39;Professional mirrorless camera with 8K recording.&#39;, &#39;Nikon&#39;, ARRAY[&#39;camera&#39;,&#39;mirrorless&#39;,&#39;photography&#39;], 3999.00),<br>(&#39;Logitech MX Master 3S&#39;, &#39;Ergonomic wireless mouse with advanced features.&#39;, &#39;Logitech&#39;, ARRAY[&#39;mouse&#39;,&#39;accessory&#39;,&#39;wireless&#39;], 129.00),<br>(&#39;Logitech MX Keys&#39;, &#39;Wireless keyboard with smart illumination.&#39;, &#39;Logitech&#39;, ARRAY[&#39;keyboard&#39;,&#39;accessory&#39;,&#39;wireless&#39;], 119.00),<br>(&#39;Logitech G Pro X Superlight&#39;, &#39;Ultra-lightweight wireless gaming mouse.&#39;, &#39;Logitech&#39;, ARRAY[&#39;mouse&#39;,&#39;gaming&#39;,&#39;wireless&#39;], 149.00),<br>(&#39;Razer DeathAdder V3&#39;, &#39;Gaming mouse with ergonomic design.&#39;, &#39;Razer&#39;, ARRAY[&#39;mouse&#39;,&#39;gaming&#39;,&#39;wired&#39;], 79.00),<br>(&#39;Razer BlackWidow V4 Pro&#39;, &#39;Mechanical gaming keyboard with RGB.&#39;, &#39;Razer&#39;, ARRAY[&#39;keyboard&#39;,&#39;gaming&#39;,&#39;mechanical&#39;], 229.00),<br>(&#39;Razer Kiyo Pro Ultra&#39;, &#39;High-quality webcam for streaming.&#39;, &#39;Razer&#39;, ARRAY[&#39;camera&#39;,&#39;webcam&#39;,&#39;streaming&#39;], 299.00),<br>(&#39;Microsoft Surface Laptop 7&#39;, &#39;Powerful Windows laptop with sleek design.&#39;, &#39;Microsoft&#39;, ARRAY[&#39;laptop&#39;,&#39;windows&#39;,&#39;ultrabook&#39;], 1599.00),<br>(&#39;Microsoft Surface Pro 11&#39;, &#39;2-in-1 tablet PC with detachable keyboard.&#39;, &#39;Microsoft&#39;, ARRAY[&#39;tablet&#39;,&#39;windows&#39;,&#39;2-in-1&#39;], 1299.00),<br>(&#39;Microsoft Xbox Series X&#39;, &#39;Next-gen gaming console with 4K HDR gaming.&#39;, &#39;Microsoft&#39;, ARRAY[&#39;gaming&#39;,&#39;console&#39;], 499.00),<br>(&#39;Nintendo Switch OLED&#39;, &#39;Popular hybrid gaming console with OLED display.&#39;, &#39;Nintendo&#39;, ARRAY[&#39;gaming&#39;,&#39;console&#39;,&#39;handheld&#39;], 349.00),<br>(&#39;AMD Radeon RX 7900 XTX&#39;, &#39;High-end GPU for gaming and creators.&#39;, &#39;AMD&#39;, ARRAY[&#39;gpu&#39;,&#39;graphics&#39;,&#39;pc&#39;], 999.00),<br>(&#39;NVIDIA GeForce RTX 4090&#39;, &#39;Flagship GPU with massive performance for gaming and AI.&#39;, &#39;NVIDIA&#39;, ARRAY[&#39;gpu&#39;,&#39;graphics&#39;,&#39;pc&#39;], 1599.00),<br>(&#39;Seagate FireCuda 530 2TB&#39;, &#39;High-speed NVMe SSD for gaming PCs.&#39;, &#39;Seagate&#39;, ARRAY[&#39;storage&#39;,&#39;ssd&#39;,&#39;nvme&#39;], 299.00),<br>(&#39;WD Black SN850X 2TB&#39;, &#39;High-performance NVMe SSD for gamers.&#39;, &#39;WD&#39;, ARRAY[&#39;storage&#39;,&#39;ssd&#39;,&#39;nvme&#39;], 289.00),<br>(&#39;WD My Cloud EX2 Ultra&#39;, &#39;2-bay NAS for home storage and backups.&#39;, &#39;WD&#39;, ARRAY[&#39;storage&#39;,&#39;nas&#39;,&#39;backup&#39;], 349.00),<br>(&#39;Crucial T700 2TB&#39;, &#39;PCIe Gen5 NVMe SSD with blazing speed.&#39;, &#39;Crucial&#39;, ARRAY[&#39;storage&#39;,&#39;ssd&#39;,&#39;nvme&#39;], 399.00),<br>(&#39;TP-Link Archer AXE75&#39;, &#39;Wi-Fi 6E router with fast speeds.&#39;, &#39;TP-Link&#39;, ARRAY[&#39;router&#39;,&#39;wifi&#39;,&#39;networking&#39;], 199.00),<br>(&#39;TP-Link Deco XE75&#39;, &#39;Mesh Wi-Fi system with Wi-Fi 6E support.&#39;, &#39;TP-Link&#39;, ARRAY[&#39;wifi&#39;,&#39;mesh&#39;,&#39;networking&#39;], 399.00),<br>(&#39;Netgear Nighthawk RAXE500&#39;, &#39;Wi-Fi 6E router with tri-band performance.&#39;, &#39;Netgear&#39;, ARRAY[&#39;router&#39;,&#39;wifi&#39;,&#39;networking&#39;], 499.00),<br>(&#39;Netgear Orbi RBKE963&#39;, &#39;Premium mesh Wi-Fi system for large homes.&#39;, &#39;Netgear&#39;, ARRAY[&#39;wifi&#39;,&#39;mesh&#39;,&#39;networking&#39;], 799.00),<br>(&#39;Philips Hue Starter Kit&#39;, &#39;Smart lighting kit with hub and bulbs.&#39;, &#39;Philips&#39;, ARRAY[&#39;lighting&#39;,&#39;smart home&#39;], 199.00),<br>(&#39;Ring Video Doorbell Pro 2&#39;, &#39;Smart doorbell with motion detection and HDR video.&#39;, &#39;Ring&#39;, ARRAY[&#39;smart home&#39;,&#39;security&#39;,&#39;camera&#39;], 249.00),<br>(&#39;Epson EcoTank ET-3850&#39;, &#39;All-in-one printer with refillable ink tanks.&#39;, &#39;Epson&#39;, ARRAY[&#39;printer&#39;,&#39;inkjet&#39;,&#39;office&#39;], 329.00),<br>(&#39;Brother HL-L3270CDW&#39;, &#39;Compact color laser printer with wireless support.&#39;, &#39;Brother&#39;, ARRAY[&#39;printer&#39;,&#39;laser&#39;,&#39;office&#39;], 249.00),<br>(&#39;Kindle Paperwhite 11th Gen&#39;, &#39;E-reader with warm light and waterproof design.&#39;, &#39;Kindle&#39;, ARRAY[&#39;ereader&#39;,&#39;reading&#39;], 179.00),<br>(&#39;Meta Quest 3&#39;, &#39;VR headset with advanced graphics and comfort.&#39;, &#39;Meta&#39;, ARRAY[&#39;vr&#39;,&#39;headset&#39;,&#39;gaming&#39;], 499.00),<br>(&#39;Oculus Quest 2&#39;, &#39;Affordable standalone VR headset.&#39;, &#39;Oculus&#39;, ARRAY[&#39;vr&#39;,&#39;headset&#39;,&#39;gaming&#39;], 299.00),<br>(&#39;Anker Soundcore Motion X600&#39;, &#39;Portable Bluetooth speaker with spatial audio.&#39;, &#39;Anker&#39;, ARRAY[&#39;speaker&#39;,&#39;audio&#39;,&#39;wireless&#39;], 199.00),<br>(&#39;Anker PowerCore 20K&#39;, &#39;High-capacity portable charger with USB-C.&#39;, &#39;Anker&#39;, ARRAY[&#39;charger&#39;,&#39;powerbank&#39;,&#39;accessory&#39;], 79.00),<br>(&#39;Sonos Beam Gen 2&#39;, &#39;Compact soundbar with Dolby Atmos.&#39;, &#39;Sonos&#39;, ARRAY[&#39;soundbar&#39;,&#39;audio&#39;,&#39;home theater&#39;], 499.00),<br>(&#39;Sonos Sub Mini&#39;, &#39;Wireless subwoofer for Sonos systems.&#39;, &#39;Sonos&#39;, ARRAY[&#39;speaker&#39;,&#39;audio&#39;,&#39;wireless&#39;], 429.00),<br>(&#39;Yamaha YAS-209&#39;, &#39;Soundbar with built-in subwoofer and Alexa.&#39;, &#39;Yamaha&#39;, ARRAY[&#39;soundbar&#39;,&#39;audio&#39;,&#39;home theater&#39;], 349.00),<br>(&#39;LG C3 OLED 55&#39;, &#39;OLED TV with stunning picture and webOS.&#39;, &#39;LG&#39;, ARRAY[&#39;tv&#39;,&#39;display&#39;,&#39;home theater&#39;], 1799.00),<br>(&#39;LG UltraFine 5K&#39;, &#39;High-resolution 5K monitor for Mac users.&#39;, &#39;LG&#39;, ARRAY[&#39;monitor&#39;,&#39;display&#39;,&#39;5k&#39;], 1299.00),<br>(&#39;Hisense U8K Mini-LED 65&#39;, &#39;Affordable Mini-LED TV with HDR support.&#39;, &#39;Hisense&#39;, ARRAY[&#39;tv&#39;,&#39;display&#39;,&#39;home theater&#39;], 1399.00),<br>(&#39;GoPro Hero12 Black&#39;, &#39;Action camera with 5.3K video and stabilization.&#39;, &#39;GoPro&#39;, ARRAY[&#39;camera&#39;,&#39;action&#39;,&#39;4k&#39;], 399.00),<br>(&#39;Garmin Fenix 7 Pro&#39;, &#39;Premium multisport smartwatch with GPS.&#39;, &#39;Garmin&#39;, ARRAY[&#39;smartwatch&#39;,&#39;fitness&#39;,&#39;gps&#39;], 799.00),<br>(&#39;Fitbit Versa 4&#39;, &#39;Fitness smartwatch with health tracking.&#39;, &#39;Fitbit&#39;, ARRAY[&#39;smartwatch&#39;,&#39;fitness&#39;,&#39;wearable&#39;], 229.00),<br>(&#39;Tile Pro 2024&#39;, &#39;Bluetooth tracker with long range and replaceable battery.&#39;, &#39;Tile&#39;, ARRAY[&#39;tracker&#39;,&#39;accessory&#39;,&#39;smart home&#39;], 49.00),<br>(&#39;Synology DS923+&#39;, &#39;Powerful 4-bay NAS for small businesses.&#39;, &#39;Synology&#39;, ARRAY[&#39;storage&#39;,&#39;nas&#39;,&#39;backup&#39;], 649.00),<br>(&#39;QNAP TS-464&#39;, &#39;Compact 4-bay NAS with expandable storage.&#39;, &#39;QNAP&#39;, ARRAY[&#39;storage&#39;,&#39;nas&#39;,&#39;backup&#39;], 699.00),<br>(&#39;Corsair K70 RGB Pro&#39;, &#39;Mechanical gaming keyboard with RGB lighting.&#39;, &#39;Corsair&#39;, ARRAY[&#39;keyboard&#39;,&#39;gaming&#39;,&#39;mechanical&#39;], 169.00),<br>(&#39;SteelSeries Apex Pro TKL&#39;, &#39;Gaming keyboard with adjustable switches.&#39;, &#39;SteelSeries&#39;, ARRAY[&#39;keyboard&#39;,&#39;gaming&#39;,&#39;mechanical&#39;], 199.00),<br>(&#39;SanDisk Extreme Pro 256GB&#39;, &#39;High-performance microSD card for phones and cameras.&#39;, &#39;SanDisk&#39;, ARRAY[&#39;storage&#39;,&#39;microsd&#39;,&#39;memory&#39;], 59.00),<br>(&#39;SanDisk Extreme Pro 512GB&#39;, &#39;High-capacity microSD card for professional use.&#39;, &#39;SanDisk&#39;, ARRAY[&#39;storage&#39;,&#39;microsd&#39;,&#39;memory&#39;], 99.00);<br><br>select count(*) from products;<br></pre><h3>⚙️ Setting Up Full-Text Search</h3><h4>Create Indexes</h4><p>To prevent slow searches as your catalog grows, create these indexes. Indexes keep searches sub-second even at scale.</p><pre>CREATE INDEX idx_products_search_vector ON products USING GIN (search_vector);<br><br>CREATE EXTENSION IF NOT EXISTS pg_trgm;<br>CREATE INDEX idx_products_title_trgm ON products USING GIN (title gin_trgm_ops);<br><br>CREATE EXTENSION IF NOT EXISTS unaccent;<br>CREATE INDEX IF NOT EXISTS idx_products_title_trgm_ci<br>ON products USING GIN ((lower(title)) gin_trgm_ops);</pre><h3>🔥 Search in Action</h3><h4>[1] Basic Full-Text Search with Ranking</h4><pre>SELECT<br>  id, title, brand, price,<br>  ts_rank(search_vector, plainto_tsquery(&#39;english&#39;, &#39;apple laptop&#39;)) AS rank<br>FROM products<br>WHERE search_vector @@ plainto_tsquery(&#39;english&#39;, &#39;apple laptop&#39;)<br>ORDER BY rank DESC;<br><br>----+----------------------+-------+---------+------------<br> id |        title         | brand |  price  |    rank<br>----+----------------------+-------+---------+------------<br>  8 | Apple MacBook Pro 14 | Apple | 2299.00 |  0.6552824<br>  5 | Apple MacBook Air M3 | Apple | 1800.00 | 0.30206525<br>(2 rows)</pre><p><strong><em>plainto_tsquery</em></strong>: Converts user-friendly queries (“apple laptop”)</p><p><strong><em>@@</em></strong>: Matches documents against the query</p><p><strong><em>ts_rank</em></strong>: Computes relevance score</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xvl-ajRQhdfomlHCUtm9qA.png" /></figure><h4>[2] Boolean, Phrase, and Prefix Search</h4><pre>-- Boolean: apple OR samsung<br>SELECT * FROM products<br>WHERE search_vector @@ to_tsquery(&#39;english&#39;, &#39;apple | samsung&#39;);<br><br>----+----------------------+------------------------------------------------------------+---------+-----------------------------------+---------+------------------------------------------------------------------------------------------------------------------------------------------------<br> id |        title         |                        description                         |  brand  |               tags                |  price  |                                                                 search_vector<br>----+----------------------+------------------------------------------------------------+---------+-----------------------------------+---------+------------------------------------------------------------------------------------------------------------------------------------------------<br>  7 | Apple iPhone 15      | Latest Apple iPhone with improved camera and battery.      | Apple   | {smartphone,electronics,flagship} | 1200.00 | &#39;15&#39;:3A &#39;appl&#39;:1A,4B,6C &#39;batteri&#39;:12C &#39;camera&#39;:10C &#39;electron&#39;:14 &#39;flagship&#39;:15 &#39;improv&#39;:9C &#39;iphon&#39;:2A,7C &#39;latest&#39;:5C &#39;smartphon&#39;:13<br>  8 | Samsung Galaxy S24   | Premium Android phone with powerful features.              | Samsung | {smartphone,android,flagship}     | 1100.00 | &#39;android&#39;:6C,12 &#39;featur&#39;:10C &#39;flagship&#39;:13 &#39;galaxi&#39;:2A &#39;phone&#39;:7C &#39;power&#39;:9C &#39;premium&#39;:5C &#39;s24&#39;:3A &#39;samsung&#39;:1A,4B &#39;smartphon&#39;:11<br> 11 | Apple MacBook Air M3 | Apple silicon, fast and lightweight. Perfect for students. | Apple   | {laptop,macos,&quot;apple silicon&quot;}    | 1800.00 | &#39;air&#39;:3A &#39;appl&#39;:1A,5B,6C,16 &#39;fast&#39;:8C &#39;laptop&#39;:14 &#39;lightweight&#39;:10C &#39;m3&#39;:4A &#39;macbook&#39;:2A &#39;maco&#39;:15 &#39;perfect&#39;:11C &#39;silicon&#39;:7C,17 &#39;student&#39;:13C<br>(3 rows)<br><br>-- Phrase match: &quot;noise cancelling&quot;<br>SELECT * FROM products<br>WHERE search_vector @@ phraseto_tsquery(&#39;english&#39;, &#39;noise cancelling&#39;);<br><br>----+----------------------------+---------------------------------------------------------+-------+-----------------------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------<br> id |           title            |                       description                       | brand |            tags             | price  |                                                                                           search_vector<br>----+----------------------------+---------------------------------------------------------+-------+-----------------------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------<br>  9 | Sony WH-1000XM5 Headphones | Top-rated noise-cancelling headphones for music lovers. | Sony  | {headphones,audio,wireless} | 400.00 | &#39;1000xm5&#39;:4A &#39;audio&#39;:18 &#39;cancel&#39;:12C &#39;headphon&#39;:5A,13C,17 &#39;lover&#39;:16C &#39;music&#39;:15C &#39;nois&#39;:11C &#39;noise-cancel&#39;:10C &#39;rate&#39;:9C &#39;soni&#39;:1A,6B &#39;top&#39;:8C &#39;top-rat&#39;:7C &#39;wh&#39;:3A &#39;wh-1000xm5&#39;:2A &#39;wireless&#39;:19<br>(1 row)<br><br><br>-- Prefix match: words starting with &quot;head&quot;<br>SELECT * FROM products<br>WHERE search_vector @@ to_tsquery(&#39;english&#39;, &#39;head:*&#39;);<br><br>----+----------------------------+---------------------------------------------------------+-------+-----------------------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------<br> id |           title            |                       description                       | brand |            tags             | price  |                                                                                           search_vector<br>----+----------------------------+---------------------------------------------------------+-------+-----------------------------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------<br>  9 | Sony WH-1000XM5 Headphones | Top-rated noise-cancelling headphones for music lovers. | Sony  | {headphones,audio,wireless} | 400.00 | &#39;1000xm5&#39;:4A &#39;audio&#39;:18 &#39;cancel&#39;:12C &#39;headphon&#39;:5A,13C,17 &#39;lover&#39;:16C &#39;music&#39;:15C &#39;nois&#39;:11C &#39;noise-cancel&#39;:10C &#39;rate&#39;:9C &#39;soni&#39;:1A,6B &#39;top&#39;:8C &#39;top-rat&#39;:7C &#39;wh&#39;:3A &#39;wh-1000xm5&#39;:2A &#39;wireless&#39;:19<br>(1 row)</pre><h4>[3] Highlighting Matches</h4><pre>SELECT<br>title,<br>ts_headline(&#39;english&#39;, description, plainto_tsquery(&#39;english&#39;, &#39;wireless&#39;), &#39;StartSel=&lt;b&gt;, StopSel=&lt;/b&gt;&#39;)<br>FROM<br>products<br>WHERE<br>search_vector @@ plainto_tsquery(&#39;english&#39;, &#39;wireless&#39;);<br><br>----------------------------+---------------------------------------------------------<br>           title            |                       ts_headline<br>----------------------------+---------------------------------------------------------<br> Sony WH-1000XM5 Headphones | Top-rated noise-cancelling headphones for music lovers.<br> Pixel Buds Pro             | Google&#39;s premium &lt;b&gt;wireless&lt;/b&gt; earbuds with ANC.<br>(2 rows)</pre><h4>[4] Fuzzy / Typo-Tolerant Search with Trigrams</h4><p>Powerful for catching “iphne”, “galaxi”, “headphnes” etc</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QM20PwN5B5Y65-Isgq3QYg.png" /></figure><p><em>Confirm data + extension and lower similarity threshold for smaller words</em></p><pre>SELECT COUNT(*) FROM products;<br>SELECT title FROM products;<br>SELECT extname FROM pg_extension WHERE extname=&#39;pg_trgm&#39;;</pre><pre>SHOW pg_trgm.similarity_threshold;      -- likely 0.3<br>SET pg_trgm.similarity_threshold = 0.15;  -- try 0.15–0.2 for short typos</pre><pre>SELECT title, similarity(lower(title), &#39;iphne&#39;) AS sim<br>FROM products<br>WHERE lower(title) % &#39;iphne&#39;<br>ORDER BY sim DESC<br>LIMIT 5;</pre><p>If the result returns 0 rows, avoid changing it at all and filter explicitly:</p><pre>SELECT title, similarity(lower(title), &#39;iphne&#39;) AS sim<br>FROM products<br>WHERE similarity(lower(title), &#39;iphne&#39;) &gt; 0.12<br>ORDER BY sim DESC<br>LIMIT 5;<br><br>-----------------+------------<br>      title      |    sim<br>-----------------+------------<br> Apple iPhone 15 | 0.22222222<br>(1 row)</pre><pre>SELECT title, similarity(lower(title), &#39;iphne&#39;) AS sim<br>FROM products<br>WHERE similarity(lower(title), &#39;iphne&#39;) &gt; 0.18<br>ORDER BY sim DESC<br>LIMIT 5;<br><br>-----------------+------------<br>      title      |    sim<br>-----------------+------------<br> Apple iPhone 15 | 0.22222222<br>(1 row)</pre><pre>SELECT title, similarity(lower(title), q) AS sim<br>FROM products, (VALUES (&#39;iphne&#39;), (&#39;galaxi&#39;), (&#39;macbok&#39;)) AS v(q)<br>WHERE similarity(lower(title), q) &gt; 0.18<br>ORDER BY q, sim DESC;<br><br>--------------------------+------------<br>          title           |    sim<br>--------------------------+------------<br> Samsung Galaxy S24       |       0.25<br> Samsung Galaxy Tab S9    |  0.2173913<br> Samsung Galaxy Book3     |  0.2173913<br> Samsung Galaxy Watch6    | 0.20833333<br> Samsung Galaxy S24 Ultra |  0.1923077<br> Samsung Galaxy Buds2 Pro | 0.18518518<br> Apple iPhone 15          | 0.22222222<br> Apple MacBook Air M3     |        0.3<br> Apple MacBook Pro 14     | 0.27272728<br>(9 rows)</pre><h4><strong>[5] Hybrid search: FTS + Fuzzy</strong></h4><pre>SELECT *,<br>  ts_rank(search_vector, plainto_tsquery(&#39;english&#39;, &#39;iphone&#39;)) AS rank,<br>  similarity(title, &#39;iphne&#39;) AS sim<br>FROM products<br>WHERE search_vector @@ plainto_tsquery(&#39;english&#39;, &#39;iphone&#39;)<br>   OR title % &#39;iphne&#39;<br>ORDER BY rank DESC, sim DESC;<br><br>----+-----------------+-------------------------------------------------------+-------+-----------------------------------+---------+-------------------------------------------------------------------------------------------------------------------------------------+-----------+------------<br> id |      title      |                      description                      | brand |               tags                |  price  |                                                            search_vector                                                            |   rank    |    sim<br>----+-----------------+-------------------------------------------------------+-------+-----------------------------------+---------+-------------------------------------------------------------------------------------------------------------------------------------+-----------+------------<br>  7 | Apple iPhone 15 | Latest Apple iPhone with improved camera and battery. | Apple | {smartphone,electronics,flagship} | 1200.00 | &#39;15&#39;:3A &#39;appl&#39;:1A,4B,6C &#39;batteri&#39;:12C &#39;camera&#39;:10C &#39;electron&#39;:14 &#39;flagship&#39;:15 &#39;improv&#39;:9C &#39;iphon&#39;:2A,7C &#39;latest&#39;:5C &#39;smartphon&#39;:13 | 0.6383234 | 0.22222222<br>(1 row)<br><br>--Alternate<br><br>WITH q AS (<br>  SELECT &#39;iphone&#39;::text AS fts_query, &#39;iphne&#39;::text AS typo<br>)<br>SELECT p.title,<br>       ts_rank(<br>         to_tsvector(&#39;pg_catalog.english&#39;, coalesce(p.title,&#39;&#39;) || &#39; &#39; || coalesce(p.description,&#39;&#39;)),<br>         plainto_tsquery(&#39;pg_catalog.english&#39;, (SELECT fts_query FROM q))<br>       ) AS rank,<br>       similarity(lower(p.title), (SELECT typo FROM q)) AS sim<br>FROM products p<br>WHERE to_tsvector(&#39;pg_catalog.english&#39;, coalesce(p.title,&#39;&#39;) || &#39; &#39; || coalesce(p.description,&#39;&#39;)) @@<br>      plainto_tsquery(&#39;pg_catalog.english&#39;, (SELECT fts_query FROM q))<br>   OR lower(p.title) % (SELECT typo FROM q)<br>ORDER BY rank DESC NULLS LAST, sim DESC NULLS LAST<br>LIMIT 10;<br><br>-----------------+-------------+------------<br>      title      |    rank     |    sim<br>-----------------+-------------+------------<br> Apple iPhone 15 | 0.075990885 | 0.22222222<br>(1 row)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ngNgMC6hozY4c0uDhe_-dw.png" /></figure><h4>⚡ Performance Best Practices</h4><ul><li>Always add a <strong>GIN index</strong> on search_vector</li><li>Use <strong>trigram indexes</strong> for typo-prone columns (title, brand)</li><li>Tune <strong>ranking weights</strong> with setweight</li><li>Run VACUUM ANALYZE regularly for accurate query planning</li></ul><h4>✅ Conclusion &amp; Next Steps</h4><p>With PostgreSQL 16+ FTS:</p><ul><li>You get <strong>fast, typo-resistant, ranked search</strong> with zero added servers.</li><li>Advanced features rival dedicated search engines — without the SaaS bill.</li><li>Your search logic lives <strong>inside the database</strong>, not in scattered microservices.</li></ul><p>If you like this blog post please press the clap button 👏</p><p><strong>What’s next?</strong></p><ul><li>Add <strong>multilingual support</strong>: swap english for your audience’s language.</li><li>Expand search to customer reviews, blog posts, and FAQs.</li><li>Incorporate <strong>behavioral ranking</strong> (clicks, purchases, personalization).</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7cb1f09a05e1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>