<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Postgres Pro</title>
    <description>The latest articles on DEV Community by Postgres Pro (@postgres_pro).</description>
    <link>https://dev.to/postgres_pro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3280831%2Fa4de8d11-8f30-474b-aef0-097c86588158.jpg</url>
      <title>DEV Community: Postgres Pro</title>
      <link>https://dev.to/postgres_pro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/postgres_pro"/>
    <language>en</language>
    <item>
      <title>We’ve learned how to migrate databases from Oracle to Postgres Pro at 41 TB/day</title>
      <dc:creator>Postgres Pro</dc:creator>
      <pubDate>Fri, 22 Aug 2025 09:35:57 +0000</pubDate>
      <link>https://dev.to/postgres_pro/weve-learned-how-to-migrate-databases-from-oracle-to-postgres-pro-at-41-tbday-1kd4</link>
      <guid>https://dev.to/postgres_pro/weve-learned-how-to-migrate-databases-from-oracle-to-postgres-pro-at-41-tbday-1kd4</guid>
      <description>&lt;p&gt;Imagine a typical scenario: your company runs on Oracle Database, and over the years, you’ve accumulated tens of terabytes of data. Now you want to move to Postgres Pro — but the migration process quickly turns into a nightmare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It takes forever to move all the data.&lt;/li&gt;
&lt;li&gt;The source system can’t stop — it has to keep running, even though the data is constantly changing.&lt;/li&gt;
&lt;li&gt;There’s a real risk of data loss or corruption — from transmission errors to type mismatches and other gremlins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We decided to tackle these problems head-on and created ProGate, a toolkit that makes life a lot easier for DBAs and speeds up the migration to Postgres Pro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F293zc7v0yyaooj8ck4yb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F293zc7v0yyaooj8ck4yb.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s ProGate?
&lt;/h2&gt;

&lt;p&gt;ProGate is an all-in-one solution for moving data into Postgres Pro. It’s a set of specialized tools that cover every step of the migration process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ProCopy — high-speed initial data load.&lt;/li&gt;
&lt;li&gt;ProSync — continuous change synchronization (CDC — Change Data Capture).&lt;/li&gt;
&lt;li&gt;ProCheck — post-migration data quality and integrity checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s go through them one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Initial data load with ProCopy
&lt;/h2&gt;

&lt;p&gt;ProCopy is a console utility for blasting large amounts of data into Postgres Pro as fast as possible — without having to stop the source system. That means minimal downtime and minimal impact on running applications.&lt;/p&gt;

&lt;p&gt;In our recent synthetic tests, we reached 200–500 MB/sec for Oracle → Postgres Pro migrations, which works out to about 41 TB/day. For PostgreSQL → Postgres Pro, we hit around 1 GB/sec.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work
&lt;/h2&gt;

&lt;p&gt;ProCopy is written in Go, so it’s lightweight, highly concurrent, and ready to chew through databases of any size. Under the hood, it uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pool of parallel read/write processes.&lt;/li&gt;
&lt;li&gt;An internal data bus for fast process communication.&lt;/li&gt;
&lt;li&gt;Robust error handling with retries for problematic records.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ProCopy use case
&lt;/h2&gt;

&lt;p&gt;Say you’ve got a CUSTOMERS table in Oracle with billions of rows. With ProCopy, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the migration in parallel, tuning the number of reader/writer processes and batch sizes.&lt;/li&gt;
&lt;li&gt;Exclude unnecessary columns.&lt;/li&gt;
&lt;li&gt;Rename columns or change their data types.&lt;/li&gt;
&lt;li&gt;Transform NULLs into default values on the fly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blazing fast data transfer.&lt;/li&gt;
&lt;li&gt;Flexible configuration (YAML/JSON).&lt;/li&gt;
&lt;li&gt;Resume from where you left off.&lt;/li&gt;
&lt;li&gt;Support for complex data types (LOB, XML, JSON, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw15loyrfw0pvi2sujz5e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw15loyrfw0pvi2sujz5e.png" alt=" " width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Change synchronization with ProSync (CDC)
&lt;/h2&gt;

&lt;p&gt;ProSync keeps Oracle and Postgres Pro in sync by continuously capturing changes (CDC) and applying them to the target database. This makes it possible to migrate with very short downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work
&lt;/h2&gt;

&lt;p&gt;Oracle stores every data change — inserts, updates, deletes — in redo logs. ProSync tails these logs in real time, detects each change, and immediately applies it to Postgres Pro.&lt;/p&gt;

&lt;p&gt;As a result, your Postgres Pro instance is always up-to-date with Oracle, down to the last committed transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world example
&lt;/h2&gt;

&lt;p&gt;A bank can’t afford even an hour of downtime. With ProSync, they can migrate data and apps gradually, while the old Oracle system stays live. Customers keep working with Oracle, and every transaction is instantly mirrored to Postgres Pro. Once everything’s in sync, switching over takes minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Minimal load on the source DB.&lt;/li&gt;
&lt;li&gt;Reliable error handling and replication health monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3. Data quality check with ProCheck
&lt;/h2&gt;

&lt;p&gt;ProCheck verifies that the migration went smoothly and that the data in Postgres Pro is exactly the same as in Oracle.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work
&lt;/h2&gt;

&lt;p&gt;It compares tables, rows, and columns between the two databases. It catches discrepancies, conversion errors, and missing data — and gives you a detailed report.&lt;/p&gt;

&lt;h2&gt;
  
  
  ProGate use case
&lt;/h2&gt;

&lt;p&gt;After migrating a financial system, you want to make sure every balance matches to the last cent. ProCheck will go row-by-row to confirm that nothing’s been lost or altered.&lt;/p&gt;

&lt;p&gt;ProGate is perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Huge databases (terabytes and up).&lt;/li&gt;
&lt;li&gt;Hot migrations with minimal downtime.&lt;/li&gt;
&lt;li&gt;Strict data consistency requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Known Limitations
&lt;/h2&gt;

&lt;p&gt;While ProGate solves a lot of headaches, you should keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema changes during migration may require manual handling.&lt;/li&gt;
&lt;li&gt;Custom data types may need special mapping.&lt;/li&gt;
&lt;li&gt;Tables without primary keys — ProSync works best with unique identifiers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2inhm7eklxi67sryfvbz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2inhm7eklxi67sryfvbz.png" alt=" " width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;The public release of ProGate is planned for this fall. Our roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A graphical UI, backend, and API.&lt;/li&gt;
&lt;li&gt;New sources/targets (MS SQL Server, MySQL, Shardman).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: ProGate will let you move from Oracle to Postgres Pro fast, safe, and with zero drama — no endless downtime, no missing data, no nasty surprises.&lt;/p&gt;

&lt;p&gt;We’ll share more about the technical internals after the public launch.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>oracle</category>
      <category>migration</category>
    </item>
    <item>
      <title>On reordering expressions in Postgres</title>
      <dc:creator>Postgres Pro</dc:creator>
      <pubDate>Fri, 01 Aug 2025 10:28:11 +0000</pubDate>
      <link>https://dev.to/postgres_pro/on-reordering-expressions-in-postgres-1bhf</link>
      <guid>https://dev.to/postgres_pro/on-reordering-expressions-in-postgres-1bhf</guid>
      <description>&lt;p&gt;Today, I want to talk about one of those sneaky tricks that can help speed up query execution. Specifically, this is about reordering conditions in WHERE clauses, JOINs, HAVING clauses, and so on.&lt;/p&gt;

&lt;p&gt;The idea is simple: if a condition in an AND chain turns out to be false, or if one in an OR chain turns out to be true, there's no need to evaluate the rest. That means saved CPU cycles — and sometimes, a lot of them. Let’s break this down.&lt;/p&gt;




&lt;p&gt;From time to time, I come across queries with complex filters like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM table
WHERE
 date &amp;gt; min_date AND
 date &amp;lt; now() - interval '1 day' AND
 value IN Subplan AND
 id = 42;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in real life, just changing the order of these conditions can noticeably improve performance. Why? Individually, each condition is cheap. But when you're applying them to millions of rows, that "cheap" quickly adds up. Especially once you've dealt with the usual suspects — like making sure the table fits into shared buffers.&lt;/p&gt;

&lt;p&gt;This effect is easiest to spot in wide tables — the kind with dozens of variable-length columns. I sometimes see sluggish IndexScans, and when you dig in, it turns out the performance hit comes from filtering on a column that's the 20th field in the tuple. Just figuring out the offset of that column takes CPU time.&lt;/p&gt;

&lt;p&gt;Looking at Postgres source code, it’s clear the community thought about this long ago. Back in 2002, Tom Lane reluctantly committed &lt;a href="https://github.com/postgres/postgres/commit/3779f7f" rel="noopener noreferrer"&gt;3779f7f&lt;/a&gt;, which added a basic reordering of expressions (see order_qual_clauses) to push subplans to the end of the list. That made sense — subplans can depend on runtime parameters and are generally expensive to evaluate.&lt;/p&gt;

&lt;p&gt;Later, in 2007, commit &lt;a href="https://github.com/postgres/postgres/commit/5a7471c" rel="noopener noreferrer"&gt;5a7471c&lt;/a&gt; changed the logic. From that point on, expressions were ordered strictly by their estimated cost. This still holds today, with a small tweak from &lt;a href="https://github.com/postgres/postgres/commit/215b43c" rel="noopener noreferrer"&gt;215b43c&lt;/a&gt; for row-level security (RLS), where evaluation order needed more control within each plan node.&lt;/p&gt;

&lt;p&gt;Let’s see what we get in upstream Postgres right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE test (
  x integer, y numeric, w timestamp DEFAULT CURRENT_TIMESTAMP, z integer
);
INSERT INTO test (x, y) SELECT gs, gs FROM generate_series(1, 1E3) AS gs;
VACUUM ANALYZE test;

EXPLAIN (COSTS ON)
SELECT * FROM test
WHERE
 z &amp;gt; 0 AND
 w &amp;gt; now() AND
 x &amp;lt; (SELECT avg(y) FROM generate_series(1,1E2) y WHERE y % 2 = x % 3) AND
 x NOT IN (SELECT avg(y) FROM generate_series(1,1E2) y OFFSET 0) AND
 w IS NOT NULL AND
 x = 42;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we check the Filter line in the plan, we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filter: ((w IS NOT NULL) AND (z &amp;gt; 0) AND (x = 42) AND (w &amp;gt; now()) AND
         ((x)::numeric = (InitPlan 2).col1) AND ((x)::numeric &amp;lt; (SubPlan 1)))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres evaluates them in this exact order, left to right.&lt;/p&gt;

&lt;p&gt;Here are the estimated costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;z &amp;gt; 0: 0.0025&lt;/li&gt;
&lt;li&gt;w &amp;gt; now(): 0.005&lt;/li&gt;
&lt;li&gt;x &amp;lt; SubPlan 1: 2.0225&lt;/li&gt;
&lt;li&gt;x NOT IN SubPlan 2: 0.005&lt;/li&gt;
&lt;li&gt;w IS NOT NULL: 0.0&lt;/li&gt;
&lt;li&gt;x = 42: 0.0025&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ordering looks mostly reasonable. But could we do better?&lt;/p&gt;

&lt;p&gt;There are at least two low-hanging fruits here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;1. Column position cost. If the column is far to the right in the tuple layout, accessing it costs more. We could add a tiny cost factor based on ordinal position — enough to help the planner choose x = 42 over z = 42, for example, all other things equal.&lt;/li&gt;
&lt;li&gt;Selectivity-based ordering. When two expressions have similar cost — say, x = 42 and z &amp;lt; 50 — it's better to put the more selective one first. If x = 42 is true less often, the planner should evaluate it before the rest.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s test how much performance gain we could actually get from this. First, we’ll build a table where some columns are far apart but have the same selectivity, and others are close together but differ in selectivity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TEMP TABLE test_2 (x1 numeric, x2 numeric, x3 numeric, x4 numeric);
INSERT INTO test_2 (x1, x2, x3, x4)
  SELECT x, (x::integer)%2, (x::integer)%100,x FROM 
    (SELECT random()*1E7 FROM generate_series(1,1E7) AS x) AS q(x);
ANALYZE;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s check what happens when we search for values in this "wide" tuple. The columns x1 and x4 are identical in every way, except that the offset of x1 within the tuple is known in advance, while for x4, the system has to calculate it for each row:&lt;/p&gt;

&lt;p&gt;EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF)&lt;br&gt;
SELECT * FROM test_2 WHERE x1 = 42 AND x4 = 42;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF)
SELECT * FROM test_2 WHERE x4 = 42 AND x1 = 42;

/*
 Seq Scan on test_2  (actual rows=0.00 loops=1)
   Filter: ((x1 = '42'::numeric) AND (x4 = '42'::numeric))
   Buffers: local read=94357
  Execution Time: 2372.032 ms

 Seq Scan on test_2  (actual rows=0.00 loops=1)
   Filter: ((x4 = '42'::numeric) AND (x1 = '42'::numeric))
   Buffers: local read=94357
 Execution Time: 2413.633 ms
*/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, all other things being equal, even a moderately wide tuple can cause a 2–3% difference in execution time. That’s roughly the same impact you'd expect from enabling JIT in typical cases.&lt;/p&gt;

&lt;p&gt;Now let’s look at how selectivity affects performance. The columns x1 and x2 are located close to each other in the tuple, but there's a key difference: x1 holds almost unique values, while x2 is filled with near-duplicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF)
SELECT * FROM test_2 WHERE x2 = 1 AND x1 = 42;

EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF)
SELECT * FROM test_2 WHERE x1 = 42 AND x2 = 1;
/*
 Seq Scan on test_2  (actual rows=0.00 loops=1)
   Filter: ((x2 = '1'::numeric) AND (x1 = '42'::numeric))
   Buffers: local read=74596
 Execution Time: 2363.903 ms

 Seq Scan on test_2  (actual rows=0.00 loops=1)
   Filter: ((x1 = '42'::numeric) AND (x2 = '1'::numeric))
   Buffers: local read=74596
 Execution Time: 2034.873 ms
*/
That’s a 10% difference.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you assume that these small effects can stack across the entire plan tree — with scans, joins, groupings, etc. — then yes, reordering conditions might actually be worth doing, even if it adds a little planning overhead.&lt;/p&gt;

&lt;p&gt;So let’s try implementing it. As an extension, this won’t work — there’s no hook in the planner to intercept final plan creation. We could really use a create_plan_hook() in create_plan(), but the community hasn’t discussed it seriously yet.&lt;/p&gt;

&lt;p&gt;So I went ahead and made a core patch. You need to touch two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cost_qual_eval() — where Postgres estimates the cost of conditions&lt;/li&gt;
&lt;li&gt;order_qual_clauses — the function that sorts them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the final code in a branch on GitHub.&lt;/p&gt;

&lt;p&gt;Running the earlier examples on that branch, expressions are now ordered better — taking both column order and selectivity into account. No extra planning overhead observed so far.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>dbms</category>
      <category>performance</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
