<?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: Pedro H Goncalves</title>
    <description>The latest articles on DEV Community by Pedro H Goncalves (@pedrohgoncalves).</description>
    <link>https://dev.to/pedrohgoncalves</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%2F970850%2F82d5bcb0-acb4-4a55-a46e-ca987c4131cf.png</url>
      <title>DEV Community: Pedro H Goncalves</title>
      <link>https://dev.to/pedrohgoncalves</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pedrohgoncalves"/>
    <language>en</language>
    <item>
      <title>Diagnosing and fixing critical PostgreSQL performance issues: A deep dive</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Mon, 16 Jun 2025 20:17:22 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/diagnosing-and-fixing-critical-postgresql-performance-issues-a-deep-dive-3jj</link>
      <guid>https://dev.to/pedrohgoncalves/diagnosing-and-fixing-critical-postgresql-performance-issues-a-deep-dive-3jj</guid>
      <description>&lt;p&gt;I recently worked on optimizing a PostgreSQL database that was facing serious performance issues. Some of the main complaints were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries were slow to return results, even when using indexed columns.&lt;/li&gt;
&lt;li&gt;Inserts, deletes, and updates were painfully slow.&lt;/li&gt;
&lt;li&gt;Maintenance tasks like reindexing, vacuuming, analyzing, and the like were nearly impossible to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I’m going to break down what was behind the poor performance. We’ll cover things like over-indexing, bloated tables, fragmented indexes, basic maintenance tasks, some modeling tips, the roadmap I used to diagnose the problem (which you can adapt to your own case), the solution I came up with, and what I’d do differently to prevent this kind of situation from happening again.&lt;/p&gt;

&lt;p&gt;From time to time, you’ll see text formatted like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Example of text.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are just technical side notes for certain technologies or features I mention—feel free to skip them.&lt;/p&gt;

&lt;p&gt;Text formatted like this:&lt;br&gt;
&lt;em&gt;Example of text.&lt;/em&gt;&lt;br&gt;
Means it's a side comment.&lt;/p&gt;

&lt;p&gt;Or like this:&lt;br&gt;
— &lt;em&gt;"Example of text."&lt;/em&gt;&lt;br&gt;
Are meant to simulate questions the reader might be thinking.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;**Note 1&lt;/em&gt;&lt;em&gt;: While I do explain many of the terms, causes, and consequences of most of the issues here, it’s best if you have at least a bit of background in databases beyond basic SELECTs.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;**Note 2&lt;/em&gt;&lt;em&gt;: This article isn’t meant to showcase clean or optimized code examples. The SQL and Scala snippets could definitely be improved. Think of it as a mental exercise—how would you improve their readability and performance?&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Table of Contents
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Diagnosing the Problem&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Possible causes&lt;/li&gt;
&lt;li&gt;Hardware resources and parameter tweaks&lt;/li&gt;
&lt;li&gt;Computational resources&lt;/li&gt;
&lt;li&gt;Locks&lt;/li&gt;
&lt;li&gt;Table locks&lt;/li&gt;
&lt;li&gt;Database and Tables&lt;/li&gt;
&lt;li&gt;Large volumes of data&lt;/li&gt;
&lt;li&gt;Bloated Tables &lt;em&gt;&amp;amp;&lt;/em&gt; Fragmented Indexes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fixing the problem&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rebuilding the table&lt;/li&gt;
&lt;li&gt;Reloading the data&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How can this be avoided?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Conclusion&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Diagnosing the problem.
&lt;/h1&gt;

&lt;p&gt;To kick things off, it’s important to mention that we didn’t have query metadata or anything similar—&lt;code&gt;pg_stat_statements&lt;/code&gt; wasn’t enabled (nor any analytics service like RDS’), and we had very little visibility into query history to identify areas for improvement.&lt;/p&gt;
&lt;h2&gt;
  
  
  Possible causes.
&lt;/h2&gt;

&lt;p&gt;To make our “investigation” a bit easier, I laid out a few areas to check. We’ll break each one down below in this order, but feel free to jump ahead—reading them in order isn’t necessary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lack of computational resources&lt;/li&gt;
&lt;li&gt;Table locks&lt;/li&gt;
&lt;li&gt;Too much data&lt;/li&gt;
&lt;li&gt;Bloated tables&lt;/li&gt;
&lt;li&gt;Fragmented tables&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Hardware resources and parameter tweaks.
&lt;/h2&gt;

&lt;p&gt;I started with a general check of the system. Others had already said the server wasn’t maxed out in any way, but I like to confirm things for myself. At the time, the server was showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~25% average CPU usage&lt;/li&gt;
&lt;li&gt;~65% average RAM usage&lt;/li&gt;
&lt;li&gt;~4.3k ops/s (NVMe SSD)&lt;/li&gt;
&lt;li&gt;Disk ~90% full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, you might be thinking:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“Only 10% free disk? That’s risky—ideally it should be at least 20%.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And you’d be right. I agree with that take. But in this case, that alone didn’t explain the massive performance drop. With all the other metrics well below danger zones, we ruled out resource bottlenecks.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;&lt;del&gt;Computational resources.&lt;/del&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Even though resource limitations weren’t the issue, I still suggested tweaking some PostgreSQL parameters. The config had never been touched, and many default settings are made for local setups where resources are shared—unlike our case, where the server was dedicated to the database.&lt;/p&gt;

&lt;p&gt;Some of the parameters we updated were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_buffers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;work_mem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;effective_cache_size&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;effective_io_concurrency&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maintenance_work_mem&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes made better use of the available hardware and did improve things like ORDER BY queries and some maintenance tasks (on smaller tables), but this clearly wasn’t the main issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  Locks.
&lt;/h2&gt;

&lt;p&gt;Certain types of long-lasting or widespread locks can definitely wreak havoc on read/write performance and even block maintenance tasks. As I said before, we didn’t have historic query or lock data, but we could monitor the locks currently active.&lt;/p&gt;

&lt;p&gt;During peak hours, I ran this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locktype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;granted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query_duration&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_locks&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_stat_all_tables&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relid&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s what we got:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pid&lt;/th&gt;
&lt;th&gt;table&lt;/th&gt;
&lt;th&gt;locktype&lt;/th&gt;
&lt;th&gt;mode&lt;/th&gt;
&lt;th&gt;granted&lt;/th&gt;
&lt;th&gt;usename&lt;/th&gt;
&lt;th&gt;query&lt;/th&gt;
&lt;th&gt;query_start&lt;/th&gt;
&lt;th&gt;duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;22956&lt;/td&gt;
&lt;td&gt;table_d&lt;/td&gt;
&lt;td&gt;relation&lt;/td&gt;
&lt;td&gt;AccessShareLock&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;2025-06-16 13:00:31.543569+00&lt;/td&gt;
&lt;td&gt;00:00.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24810&lt;/td&gt;
&lt;td&gt;table_e&lt;/td&gt;
&lt;td&gt;relation&lt;/td&gt;
&lt;td&gt;AccessShareLock&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;2025-06-16 11:39:29.805778+00&lt;/td&gt;
&lt;td&gt;21:02.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24985&lt;/td&gt;
&lt;td&gt;table_e&lt;/td&gt;
&lt;td&gt;relation&lt;/td&gt;
&lt;td&gt;ShareUpdateExclusiveLock&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;autovacuum: VACUUM ANALYZE public.table_e (to prevent wraparound)&lt;/td&gt;
&lt;td&gt;2025-06-16 11:39:32.468211+00&lt;/td&gt;
&lt;td&gt;20:59.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25102&lt;/td&gt;
&lt;td&gt;table_f&lt;/td&gt;
&lt;td&gt;relation&lt;/td&gt;
&lt;td&gt;AccessShareLock&lt;/td&gt;
&lt;td&gt;TRUE&lt;/td&gt;
&lt;td&gt;postgres&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;2025-06-16 11:39:29.805778+00&lt;/td&gt;
&lt;td&gt;21:02.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;There were dozens more rows like these showing AccessShareLocks on other tables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now maybe you’re thinking:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“Aha! It’s the locks! That’s what’s killing performance!”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sorry to disappoint—AccessShareLocks are super permissive. They mostly coexist with everything except &lt;code&gt;AccessExclusiveLock&lt;/code&gt; (which is used by DROP TABLE, CLUSTER, REINDEX, VACUUM FULL, table-altering commands, etc). So they’re not the problem.&lt;/p&gt;

&lt;p&gt;But then you ask:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“What about that ShareUpdateExclusiveLock?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good catch. It’s a bit more restrictive, blocking maintenance tasks and table structure changes. So maybe this is the bad guy preventing maintenance from running?&lt;/p&gt;

&lt;p&gt;Not really. That lock was taken by an autovacuum trying to run &lt;code&gt;VACUUM ANALYZE&lt;/code&gt;. In reality, this process probably never finishes and ends up just hanging there. Our move here was to disable autovacuum temporarily and kill the zombie process.&lt;/p&gt;

&lt;p&gt;You can cancel it like this (replace &lt;code&gt;24985&lt;/code&gt; with your PID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_cancel_backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24985&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that doesn’t work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_terminate_backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24985&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we disable autovacuum for that table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_e&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autovacuum_enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;table_e&lt;/code&gt; wasn’t super critical—only a few queries hit it—so this helped a bit but wasn’t a game changer.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;&lt;del&gt;Lock Tables.&lt;/del&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Database and tables.
&lt;/h2&gt;

&lt;p&gt;At this point, the best move was to stop looking at things too broadly and zoom in on something more specific—maybe a slow query or a concrete complaint like:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“The database is really slow.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That kind of vague complaint doesn’t help much. Ideally, you want to narrow things down until you find the actual pain point. Is it a specific query? Then start with the &lt;code&gt;EXPLAIN&lt;/code&gt; plan. That’s SQL tuning 101. But as I mentioned earlier, we didn’t have access to historical queries, and there wasn’t enough logging to get insights. That made the challenge more… let’s say, “fun.”&lt;/p&gt;

&lt;p&gt;We knew all operations—reads, writes, maintenance—were way too slow. So the problem wasn’t a lack of indexes (too many indexes usually slow down writes, but reads get faster), and we couldn’t blame bad query design either—we simply didn’t have the history or even access to the codebase using the database.&lt;/p&gt;

&lt;p&gt;So next step: maybe we’re dealing with too much data?&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking the data volume.
&lt;/h3&gt;

&lt;p&gt;Let’s start with the obvious: how many rows do these tables actually have?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;my_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yeah… that didn’t go well. It ran for 10 minutes before I killed it. Not helpful.&lt;/p&gt;

&lt;p&gt;So, plan B: table statistics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;n_live_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_analyze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_autoanalyze&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives an estimate of live rows based on the last time PostgreSQL collected stats, along with timestamps for the last &lt;code&gt;ANALYZE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Results (abridged):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;schema&lt;/th&gt;
&lt;th&gt;table&lt;/th&gt;
&lt;th&gt;live_tuples&lt;/th&gt;
&lt;th&gt;last_analyze&lt;/th&gt;
&lt;th&gt;last_autoanalyze&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;td&gt;table_a&lt;/td&gt;
&lt;td&gt;1.3 billion&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;2025-03-20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;td&gt;table_b&lt;/td&gt;
&lt;td&gt;500 million&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;2025-01-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;td&gt;table_c&lt;/td&gt;
&lt;td&gt;200 million&lt;/td&gt;
&lt;td&gt;2025-03-15&lt;/td&gt;
&lt;td&gt;2025-03-16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No surprise here—the stats were outdated by months. These numbers were basically useless. But we already had a clue: missing &lt;code&gt;ANALYZE&lt;/code&gt; runs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;code&gt;ANALYZE&lt;/code&gt; updates internal stats used by the query planner to figure out the best way to run a query. It’s critical maintenance.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these stats aren’t being updated—and it’s not because nobody tried—it’s probably a symptom of a deeper issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Estimating row count without stats.
&lt;/h3&gt;

&lt;p&gt;Still, I needed a ballpark number. Since every table had a sequential &lt;code&gt;id&lt;/code&gt;, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Getting the MAX of a sequential, indexed column like &lt;code&gt;id&lt;/code&gt; is usually very fast thanks to how b-tree indexes work—the DB can just peek at the last page.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;max(id)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9,846,091,813&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So yeah… nearly 10 billion inserts at some point. That doesn’t mean the table has 10 billion rows now—just that it’s had a lot of activity.&lt;/p&gt;

&lt;p&gt;To estimate how many rows were still there, I used external tooling. Since the DB couldn't handle a simple count or run &lt;code&gt;ANALYZE&lt;/code&gt;, I turned to Spark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"(SELECT * FROM table_a WHERE id &amp;lt; 1000000000) AS subquery"&lt;/span&gt;

&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;df&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;read&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jdbcUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectionProps&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;count&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea: If IDs under 1 billion return ~1 billion rows, we might really have close to 10 billion rows. If not, we extrapolate based on what we get.&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“But why use Spark to count instead of waiting it out in the DB?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good question. When I tried counting in Postgres, it took over 40 minutes and didn’t finish. Selecting a sample was way faster.&lt;/p&gt;

&lt;p&gt;After 30 minutes of Spark chugging away, I got the answer: &lt;strong&gt;14 million rows&lt;/strong&gt; for IDs under 1 billion. So I estimated roughly 125 million total rows (not linear, but close enough).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;&lt;del&gt;Too much data.&lt;/del&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Next question: what if we had massive delete or update operations in the past? That would create tons of dead tuples—especially if &lt;code&gt;VACUUM&lt;/code&gt; or &lt;code&gt;VACUUM FULL&lt;/code&gt; hadn’t been running.&lt;/p&gt;

&lt;p&gt;Even though we know PostgreSQL’s stats were way off, I still checked them just to confirm our suspicions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;n_tup_ins&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;inserts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_tup_upd&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_tup_del&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deletes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_vacuum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_autovacuum&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'table_a'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;inserts&lt;/th&gt;
&lt;th&gt;updates&lt;/th&gt;
&lt;th&gt;deletes&lt;/th&gt;
&lt;th&gt;last_vacuum&lt;/th&gt;
&lt;th&gt;last_autovacuum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;117 million&lt;/td&gt;
&lt;td&gt;659&lt;/td&gt;
&lt;td&gt;3.25 billion&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;2025-01-30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three. Billion. Deletes. That’s… a lot.&lt;/p&gt;

&lt;p&gt;Now let’s see how bloated the table is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;live_tuples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_dead_tup&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dead_tuples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; 
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_dead_tup&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_live_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; 
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dead_tuples_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (simplified):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;table&lt;/th&gt;
&lt;th&gt;live_tuples&lt;/th&gt;
&lt;th&gt;dead_tuples&lt;/th&gt;
&lt;th&gt;dead_tuples_pct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;table_a&lt;/td&gt;
&lt;td&gt;1.38B&lt;/td&gt;
&lt;td&gt;612M&lt;/td&gt;
&lt;td&gt;44.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Similar stats for other large tables too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This confirmed two things:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Bloated tables&lt;/strong&gt; &lt;em&gt;and probably&lt;/em&gt; &lt;strong&gt;Fragmented tables.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Even though PostgreSQL’s stats were outdated and some operations were failing, we could already see the pattern. Tons of deletes happened, and no follow-up &lt;code&gt;VACUUM&lt;/code&gt; to clean things up. The tables were bloated, and reads/writes were getting slower by the day.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;PostgreSQL doesn’t immediately remove deleted rows. Because of MVCC (Multi-Version Concurrency Control), it needs to keep old versions around for open transactions.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you may be wondering:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“Okay, but what makes a table fragmented?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“Aren’t bloated and fragmented tables the same thing?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not quite. Bloated tables usually are fragmented, but not always the other way around. &lt;code&gt;REINDEX&lt;/code&gt; can help fix both—but only when it can actually run. In our case, it was completely unusable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Fragmented tables have rows scattered across disk in a messy way. That kills sequential scans.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Bloated tables are ones where dead tuples and overhead take up a huge chunk of storage—sometimes 30–40% or more.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Fixing the problem.
&lt;/h1&gt;

&lt;p&gt;Alright, so we’ve (probably) identified the culprits: the most heavily used tables are bloated and fragmented beyond hope. We also know that basic maintenance operations like &lt;code&gt;VACUUM&lt;/code&gt;, &lt;code&gt;REINDEX&lt;/code&gt;, or even &lt;code&gt;ANALYZE&lt;/code&gt; don’t complete anymore. So, what options are left?&lt;/p&gt;

&lt;p&gt;The most practical and efficient solution? Rebuild the table outside the database.&lt;/p&gt;

&lt;p&gt;The idea is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the entire table’s data.&lt;/li&gt;
&lt;li&gt;Apply any necessary cleanup or transformation.&lt;/li&gt;
&lt;li&gt;Import it back into the database in a fresh, clean table.&lt;/li&gt;
&lt;li&gt;Swap the old and new tables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You might be wondering:&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;“But if the table has around 140 million rows, won’t the extract/load process eat up a ton of resources?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not if you use Spark. Of course, tools like DuckDB or Polars might be even faster, but I used Spark because I already had the environment and some code ready to go.&lt;/p&gt;

&lt;p&gt;To make life easier, I split the export into chunks (batches). This way I could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get feedback during the process&lt;/li&gt;
&lt;li&gt;Resume from checkpoints in case anything failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the Scala code I used to extract in 1-billion-row batches based on the primary key (a serial ID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;jdbcUrl&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jdbc:postgresql://hostname:5432/database_name"&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;jdbcUser&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;jdbcPassword&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;tableName&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"table_a"&lt;/span&gt;

&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;connectionProperties&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nv"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="nv"&gt;connectionProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jdbcUser&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;connectionProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jdbcPassword&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;connectionProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetchsize"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1000000"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;batchSize&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000000000L&lt;/span&gt; &lt;span class="c1"&gt;// 1 billion&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;totalRecords&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000000000L&lt;/span&gt; &lt;span class="c1"&gt;// 10 billion&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;numBatches&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalRecords&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;batchSize&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;toInt&lt;/span&gt;

&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;outputBasePath&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"data/path"&lt;/span&gt;

&lt;span class="nf"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batchIndex&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="n"&gt;numBatches&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;startId&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batchIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;batchSize&lt;/span&gt;
  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;endId&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batchIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;batchSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;batchDF&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;read&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;jdbcUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"(SELECT * FROM $tableName WHERE id BETWEEN $startId AND $endId) AS tmp"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;connectionProperties&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;batchOutputPath&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"${outputBasePath}batch_${startId}_to_${endId}.parquet"&lt;/span&gt;
  &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Working in export..."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;batchDF&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;write&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;SaveMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Overwrite&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batchOutputPath&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: This snippet isn’t complete—it’s missing Spark setup and imports, but you get the idea.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Export time averaged around 1 hour and 20 minutes per batch, plus 40 minutes to write each file as CSV. Interestingly, export times increased with each batch, likely due to DB caching. Restarting the DB between exports might’ve helped.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Oh—and fun fact: we ended up with **250 million rows&lt;/em&gt;&lt;em&gt;, which was 110 million more than estimated.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Rebuilding the table.
&lt;/h3&gt;

&lt;p&gt;Once export was done, we needed to recreate the table structure with all its indexes and constraints. Fortunately, PostgreSQL makes that easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;new_table_a&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; &lt;span class="k"&gt;INCLUDING&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This clones the entire table structure—columns, types, indexes, constraints, everything (except the data). Now we were ready to reload the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reloading the data.
&lt;/h3&gt;

&lt;p&gt;In our case, no data transformation was needed, so we went straight to reimporting.&lt;/p&gt;

&lt;p&gt;I benchmarked two methods:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Batch inserts (Spark)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nf"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batchIndex&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="n"&gt;numBatches&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;startId&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batchIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;batchSize&lt;/span&gt;
  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;endId&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batchIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;batchSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;csvPath&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"${inputBasePath}batch_${startId}_to_${endId}.csv"&lt;/span&gt;

  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;df&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;read&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csvPath&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;dfToWrite&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;repartition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;dfToWrite&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;write&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;SaveMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Append&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;option&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"isolationLevel"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NONE"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jdbcUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jdbcTable&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectionProperties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this method, we managed to import &lt;strong&gt;250 million rows in about 40 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. COPY command
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;numExecutors&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;FileSystem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;sparkContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;hadoopConfiguration&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;baseDir&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputBasePath&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;csvFiles&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;listStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseDir&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;endsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".csv"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;sorted&lt;/span&gt;

&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;executor&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;newFixedThreadPool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numExecutors&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;csvFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;foreach&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;csvPath&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nv"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Runnable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
      &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;BufferedReader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;DriverManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jdbcUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jdbcUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jdbcPassword&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;setAutoCommit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;hadoopPath&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csvPath&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;localPath&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;csvPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file:"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="nv"&gt;csvPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;tmpDir&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"java.io.tmpdir"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;localFile&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmpDir&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"import_${System.currentTimeMillis()}_${hadoopPath.getName}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
          &lt;span class="nv"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;copyToLocalFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hadoopPath&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;localFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getAbsolutePath&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
          &lt;span class="nv"&gt;localFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getAbsolutePath&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;copyManager&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CopyManager&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;asInstanceOf&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;BaseConnection&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;copySQL&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"""COPY $jdbcTable FROM STDIN WITH (FORMAT CSV, DELIMITER '$delimiter', HEADER)"""&lt;/span&gt;

        &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BufferedReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localPath&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;rowsCopied&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;copyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;copyIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copySQL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

        &lt;span class="nv"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"[${Thread.currentThread().getName}] Imported: $rowsCopied rows."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

      &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Try&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;rollback&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
          &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"[${Thread.currentThread().getName}] ERROR: ${e.getMessage}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
          &lt;span class="nv"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;printStackTrace&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Try&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;close&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Try&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;close&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Yes, I know both methods should be parallelized for a fairer comparison—but even running single-threaded, &lt;code&gt;COPY&lt;/code&gt; was much faster.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re on AWS RDS, you can still use &lt;code&gt;COPY&lt;/code&gt;, but the files go to an S3 bucket instead of the DB server. Check the AWS docs for more.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;COPY&lt;/code&gt;, we loaded &lt;strong&gt;250 million rows in just over 15 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;The &lt;code&gt;COPY&lt;/code&gt; command is inherently faster because it loads raw data into memory and defers constraint checks and trigger execution until after the load. One downside: it’s all-or-nothing—if something breaks, you can’t resume halfway. If you want a deeper dive into &lt;code&gt;COPY&lt;/code&gt;, check out this great article by a friend of mine: &lt;a href="https://dev.to/josethz00/speed-up-your-postgresql-bulk-inserts-with-copy-40pk"&gt;Speed up your PostgreSQL bulk inserts with COPY&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, to complete the switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;new_table_a&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: If any foreign keys referenced the original table, you’ll need to drop and recreate them manually. PostgreSQL doesn’t have a built-in way to do this automatically.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rebuild process was done during a service freeze—no new data was being written or queried during the whole extraction/import. Some smaller tables were fixed with just &lt;code&gt;VACUUM FULL&lt;/code&gt; and &lt;code&gt;REINDEX&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disk usage dropped by over &lt;strong&gt;60%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;More than &lt;strong&gt;1.5 TB&lt;/strong&gt; of space freed.&lt;/li&gt;
&lt;li&gt;Massive performance gains across the board.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  How can this be avoided?
&lt;/h1&gt;

&lt;p&gt;After solving the issue, I went digging to figure out what might’ve caused those massive delete operations that ultimately blocked PostgreSQL from running &lt;code&gt;VACUUM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I talked to a few devs who had direct access to the DB, and they told me a service was being called dozens (maybe hundreds) of times a day. And every time it ran, it inserted the incoming data into the database.&lt;/p&gt;

&lt;p&gt;Here’s the catch: those inserts were often unnecessary. Most of the time, the service was receiving duplicated data. But instead of checking for duplicates, the application just blindly inserted everything. There were no proper constraints on the table either. So... boom: we ended up with a mountain of redundant records.&lt;/p&gt;

&lt;p&gt;Eventually, someone realized the performance was tanking and decided to clean up the duplicated data. That’s where our villain enters the story: the way those deletes were done.&lt;/p&gt;

&lt;p&gt;Here’s the actual code that was used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;LOOP&lt;/span&gt;
    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; 
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; 
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; 
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; 
        &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; 
      &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; 
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;EXIT&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a pretty straightforward loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find duplicate rows based on name and age&lt;/li&gt;
&lt;li&gt;Store their &lt;code&gt;id&lt;/code&gt;s in a temp table (in batches of 100k)&lt;/li&gt;
&lt;li&gt;Delete them from the main table&lt;/li&gt;
&lt;li&gt;Repeat until there are no more duplicates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance-wise, this approach isn’t too bad. The real problem? No &lt;code&gt;VACUUM&lt;/code&gt; was run during or after the process. This table had &lt;strong&gt;billions&lt;/strong&gt; of rows. The loop ran for days, and eventually the table became so bloated that &lt;code&gt;VACUUM&lt;/code&gt; couldn’t finish anymore. That’s what really tanked the DB.&lt;/p&gt;

&lt;p&gt;Now, what if they had just added these three lines?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_advisory_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="s1"&gt;'VACUUM (ANALYZE) table_a'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_advisory_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With those lines, the cleanup block would’ve looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;LOOP&lt;/span&gt;
    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; 
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; 
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; 
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; 
        &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; 
      &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a&lt;/span&gt; 
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table_a_duplicates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;EXIT&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_advisory_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="s1"&gt;'VACUUM (ANALYZE) table_a'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_advisory_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entire problem could’ve been avoided by having the right &lt;strong&gt;constraints&lt;/strong&gt; in place to begin with. The issue here was the lack of a unique constraint to prevent duplicate rows. The app should’ve either rejected duplicates or used &lt;code&gt;UPSERT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is why it's crucial to test against real-world scenarios and take time to understand your schema. Well-designed constraints can save your DB from future disaster.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For more on designing scalable, robust PostgreSQL databases, I wrote a follow-up piece: &lt;a href="https://dev.to/pedrohgoncalves/designing-robust-and-scalable-relational-databases-a-series-of-best-practices-1i20"&gt;Designing robust and scalable relational databases: A series of best practices&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After the rebuild, performance improved massively—insert, update, delete, read, maintenance, everything. I didn’t have any performance-monitoring tools enabled at the time (which is on me), so I couldn’t get hard numbers, but based on sample queries, we saw &lt;strong&gt;an average 250% improvement&lt;/strong&gt; in response time. Oh, and we also freed up more than &lt;strong&gt;1.5 TB&lt;/strong&gt; of disk space.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;PostgreSQL has its quirks—but with solid modeling, those quirks rarely become a problem. Of course, surprises and edge cases are unavoidable, but the goal is to reduce how often they happen.&lt;/p&gt;

&lt;p&gt;Sometimes, a single operation executed without proper thought—especially in large-scale environments—can lead to catastrophic outcomes, potentially costing thousands of dollars in downtime, troubleshooting, and emergency fixes.&lt;/p&gt;

&lt;p&gt;Before you run anything that touches a large amount of data, take a moment to ask yourself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“How will the database handle this volume of information?”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And study the cost of each operation—some are cheap, others will absolutely wreck your performance if used carelessly.&lt;/p&gt;

&lt;p&gt;Also, don’t forget: optimization should always align with how your services interact with the database. That’s where real gains happen. Having monitoring in place—like &lt;strong&gt;AWS Performance Insights&lt;/strong&gt; or enabling &lt;strong&gt;pg_stat_statements&lt;/strong&gt;—gives you visibility into where the real bottlenecks are, so you’re not left guessing.&lt;/p&gt;

&lt;p&gt;(I'm planning to write a guide on using &lt;code&gt;pg_stat_statements&lt;/code&gt; soon, by the way.)&lt;/p&gt;




&lt;p&gt;I hope this article helped clarify the root of the problem and gave some insight into how we approached the fix.&lt;/p&gt;

&lt;p&gt;Thanks a lot for reading 🙌&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>advanced</category>
      <category>performance</category>
    </item>
    <item>
      <title>Designing robust and scalable relational databases: A series of best practices.</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Tue, 19 Nov 2024 18:10:58 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/designing-robust-and-scalable-relational-databases-a-series-of-best-practices-1i20</link>
      <guid>https://dev.to/pedrohgoncalves/designing-robust-and-scalable-relational-databases-a-series-of-best-practices-1i20</guid>
      <description>&lt;p&gt;Throughout my experience as a data engineer and software developer, I've had the pleasure (and displeasure) of creating and maintaining many databases. In this article, I'll list some best practices that I consider essential for relational databases to become truly scalable.&lt;/p&gt;

&lt;p&gt;Some practices, such as adding indexes, table partitioning, and studying normalization and denormalization applications, will require a slightly deeper level of knowledge to be used correctly and not generate future or immediate problems. I'll describe some of these points, but it's highly recommended that you study them more in-depth. Additionally, some of the practices I'll list are not possible to implement in existing, consolidated systems that primarily have many interface points (services that consume from them); changes in these cases can mean a lot of headaches. In any case, it's highly recommended that you use these practices in your new projects, even if you're the only developer. Your future self will thank your past self for implementing them.&lt;/p&gt;

&lt;p&gt;Although I'll list some practices, concepts, and tools, I don't intend to delve deeply into them. My aim is to provide a basic explanation of each topic so that you can analyze and see if it makes sense to apply them to your projects. Therefore, for some of the topics, I'll leave a link to an article/post/answer that I believe has good quality regarding that theme and goes into more depth.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;While this article covers several crucial aspects of database management and design, some advanced topics deserve a more in-depth exploration. Subjects like MPP, sharding, distributed processing and storage are complex enough to warrant their own dedicated article.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I plan to address these topics in a future piece where I can give them the careful consideration and detailed explanation they merit.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we will see:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Monitoring your database.&lt;/li&gt;
&lt;li&gt;Low-level configuration files and variables.&lt;/li&gt;
&lt;li&gt;Naming Convention Standardization.&lt;/li&gt;
&lt;li&gt;Correct Use of Data Types.&lt;/li&gt;
&lt;li&gt;Normalizing and Denormalizing: How Far to Go.&lt;/li&gt;
&lt;li&gt;Choosing a Tool for Versioning and Testing.&lt;/li&gt;
&lt;li&gt;Documenting Your Entities and Fields.&lt;/li&gt;
&lt;li&gt;Applying Indexes Correctly.&lt;/li&gt;
&lt;li&gt;Modern Backup and Restoration Pipelines.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Monitoring your database
&lt;/h2&gt;

&lt;p&gt;Monitoring your database is one of the tasks that should start in the early stages of your application. It's completely understandable that part of the product/service development team's focus is on making things work, but monitoring your database and its entities can deliver important insights about your application and infrastructure. Good database monitoring answers questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average query response time.&lt;/li&gt;
&lt;li&gt;Throughput (number of transactions per second).&lt;/li&gt;
&lt;li&gt;CPU and memory utilization.&lt;/li&gt;
&lt;li&gt;Disk I/O (reads/writes per second).&lt;/li&gt;
&lt;li&gt;Uptime (available time within a range).&lt;/li&gt;
&lt;li&gt;Error rates.&lt;/li&gt;
&lt;li&gt;Deadlocks.&lt;/li&gt;
&lt;li&gt;Locks.&lt;/li&gt;
&lt;li&gt;Failed login attempts.&lt;/li&gt;
&lt;li&gt;Backup metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these questions are not applied at the table level, but the more you can drill down, the better it will be for future investigations. By drilling down, I mean how far you can go, such as the average execution time of queries, down to the table level and having the average time of the table, down to the query level and understanding which types of queries take longer to execute on that table.&lt;/p&gt;

&lt;p&gt;As a recommendation, I suggest a very famous monitoring stack, which is &lt;a href="https://prometheus.io/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt;, a monitoring system/time-series database, with &lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt;, a tool for analyzing metrics through graphs. If for some reason you can't or don't want to use these tools, I'm sorry to say that answering all these questions with just one tool (especially OSS) will be a difficult task. So, the ideal approach is to study a series of tools and design some implementations, clearly dividing the functions so that they don't overlap and end up generating ambiguities, which could also possibly generate different data, leading to a big headache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Low-level configuration files and variables.
&lt;/h2&gt;

&lt;p&gt;I believe that, like me, many people are curious about what configuration files like &lt;code&gt;postgresql.conf&lt;/code&gt; or &lt;code&gt;my.cnf&lt;/code&gt; can change within the database, whether in performance, authorization, transactions, or anything that presents a visible improvement. To address part of the topic and answer many people's questions: yes, configuration files can and do dictate much of the DBMS behavior and how it manages transactions and manipulates data. Database systems like PostgreSQL and MySQL have default values for basically all manipulable values, and they are adjusted to fit local environments (most of these DBMSs are used in test environments and for small to medium-sized projects). This means that in environments with large volumes of data that have a dedicated server with high processing capacity, you can (and should) certainly make adjustments to these configurations so that they are better utilized.&lt;/p&gt;

&lt;p&gt;Using some PostgreSQL configurations as a basis, I'll mention some important variables that can be altered. Ideally, you should read the documentation about these variables, study your needs, and test the changes to understand how they affect performance and whether they create problems elsewhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;shared_buffers&lt;/strong&gt;: This is the shared memory for data caching. Increasing the memory that the DBMS can consume significantly increases the query speed of frequently accessed data.&lt;/p&gt;

&lt;p&gt;Values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OLTP environments: 25 - 40% RAM&lt;/li&gt;
&lt;li&gt;OLAP environments: 50 - 70% RAM&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;effective_cache_size&lt;/strong&gt;: This is how much disk caching is available from the operating system and PostgreSQL. It's not effectively used but helps the query planner perform some operations to understand, for example, if an index fits entirely in the cache.&lt;/p&gt;

&lt;p&gt;Values:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OLTP environments: 50 - 70% RAM&lt;/li&gt;
&lt;li&gt;OLAP environments: 80 - 90% RAM&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;work_mem&lt;/strong&gt;: This is the amount of memory PostgreSQL allocates for sorting operations (order by) and hash operations such as aggregations and joins.&lt;/p&gt;

&lt;p&gt;Values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OLTP environments: 4MB - 64MB&lt;/li&gt;
&lt;li&gt;OLAP environments: 64MB - 1GB&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;max_connections&lt;/strong&gt;: This is the maximum number of simultaneous connections. It's a great starting point for controlling concurrency; too many connections can overload the server.&lt;/p&gt;

&lt;p&gt;Values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OLTP environments: 200 - 500&lt;/li&gt;
&lt;li&gt;OLAP environments: 20 - 60&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;max_wal_size&lt;/strong&gt;: The Write-Ahead-Log (WAL) is the mechanism that ensures data integrity by recording changes before applying them to the database. A larger value decreases the frequency of checkpoints, which increases write performance but also increases recovery times after failure.&lt;/p&gt;

&lt;p&gt;Values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OLTP environments: 1GB - 4GB&lt;/li&gt;
&lt;li&gt;OLAP environments: 4GB - 16GB&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;There are numerous other configurations that will make a difference across all aspects of your database, including resource consumption, authentication, encryption, connections, and other different areas. I strongly recommend reading the documentation for your DBMS and adapting to your needs, always making a prior backup to be able to return in case of unexpected behaviors and analyzing your database performance through monitoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Naming Convention Standardization
&lt;/h2&gt;

&lt;p&gt;Naming convention standardization is one of the most powerful practices, but it's probably the most difficult to implement. It's easy to implement during the initial database modeling, but it's very easy to "lose" throughout development, and in many cases, it's impossible to implement in existing systems.&lt;/p&gt;

&lt;p&gt;With naming convention standardization, we can list a few points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduction of redundant names, such as the field &lt;code&gt;client_name&lt;/code&gt; in the &lt;code&gt;client&lt;/code&gt; table where it could simply be &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Standardization of abbreviations. In one table, you might have the field &lt;code&gt;sale_value&lt;/code&gt;, and in another, you have &lt;code&gt;vl_service&lt;/code&gt;, where the first could be &lt;code&gt;vl_sale&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Singular names for tables and fields. Instead of naming a table &lt;code&gt;clients&lt;/code&gt;, you would name it &lt;code&gt;client&lt;/code&gt;, and the same rule applies to fields, with the exception of fields that truly represent more than one value. An example could be a &lt;code&gt;tags&lt;/code&gt; field of type ARRAY of STRINGS.&lt;/li&gt;
&lt;li&gt;Schema naming. Not every database has the possibility of dividing entities by schemas, but the most famous one currently (PostgreSQL) does. One of the recommendations is to work with short names, such as the schema that organizes finance-related tables being abbreviated to &lt;code&gt;fin&lt;/code&gt;, &lt;code&gt;hr&lt;/code&gt; for human resources, or &lt;code&gt;mkt&lt;/code&gt; for marketing. &lt;em&gt;You might wonder why short names for schemas and not for tables and fields? Schemas are the most comprehensive hierarchical class; in most cases, the tables already explain what that schema means, so there's no real need for a long description.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.javatpoint.com/data-dictionary-storage" rel="noopener noreferrer"&gt;Data Dictionary in DBMS - javatpoint&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Correct Use of Data Types
&lt;/h2&gt;

&lt;p&gt;The correct use of data types seems quite obvious to mention, and you've surely read/heard about it in more than one course or article. The important point I'll list here is, if you have a prior opportunity to study the data, study and define well how it should be stored. The correct data types, besides generating more performance in inserting new values or reading, ensure security and implement a layer of data safety.&lt;/p&gt;

&lt;p&gt;It's also important to understand the difference between data types that &lt;strong&gt;seem&lt;/strong&gt; ambiguous like &lt;em&gt;varchar&lt;/em&gt; and &lt;em&gt;char&lt;/em&gt;, &lt;em&gt;tinyint&lt;/em&gt; and &lt;em&gt;bigint&lt;/em&gt;, &lt;em&gt;varchar&lt;/em&gt; and &lt;em&gt;text&lt;/em&gt;, &lt;em&gt;text&lt;/em&gt; and &lt;em&gt;blob&lt;/em&gt;, &lt;em&gt;numeric&lt;/em&gt; &amp;amp; &lt;em&gt;decimal&lt;/em&gt; and &lt;em&gt;float&lt;/em&gt;. The choices of these types may not appear to and really don't make a difference in small amounts of data, but when it comes to billions of rows with numerous fields and indexes, the correct choice of data type can bring a valuable performance difference to the application.&lt;/p&gt;

&lt;p&gt;I'll mention some examples of use cases of data types that &lt;em&gt;seemed&lt;/em&gt; correct but actually generated future problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The use of &lt;strong&gt;FLOAT&lt;/strong&gt; to store transaction values. But well, why would this be a problem? Float (and decimal too) uses &lt;strong&gt;floating point&lt;/strong&gt;, which is a storage and representation technique that, roughly speaking, allows for rounding in exchange for performance in read and write operations, unlike types like &lt;strong&gt;DECIMAL&lt;/strong&gt; that use the &lt;strong&gt;fixed point&lt;/strong&gt; technique. Fixed point stores the value exactly, without rounding. In financial transactions, rounding cannot occur because at some point the value of these roundings would be significant, and we already know the end of that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The use of &lt;strong&gt;CHAR(355)&lt;/strong&gt; to store product descriptions. The char type has a fixed size, which means that if the product description doesn't have 355 characters in length, the database will fill the missing characters with blank characters. The number of stored products scaled to various levels, and the difference in storage use from CHAR to &lt;strong&gt;VARCHAR&lt;/strong&gt; was considered substantial, in addition to negatively affecting the performance of indexes such as FULLTEXT (we'll talk about indexes later).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The use of &lt;strong&gt;VARCHAR&lt;/strong&gt; to store negotiation situations. The field would have (or at least should have) only 3 negotiation situations, &lt;em&gt;approved&lt;/em&gt;, &lt;em&gt;pending&lt;/em&gt;, &lt;em&gt;denied&lt;/em&gt;. In this case, the correct type to use would be &lt;strong&gt;ENUM&lt;/strong&gt;, which would limit the entry of other values, such as incorrect spellings or unmapped situations. You might argue that only your application interfaces with the database in manipulation operations and this type of verification is in code, but it's important to understand that data constraints are never too much, and you don't know if other applications that are beyond your control may arise in the future and end up interfacing with the database.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I strongly recommend that you study the differences between data types and how the database handles them in insertions, selections, updates, and deletions. You will find pertinent information in the documentation of the RDBMS you are using.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Normalizing and Denormalizing: How Far to Go
&lt;/h2&gt;

&lt;p&gt;Briefly contextualizing, normalization is a set of rules to be followed in entity modeling. These rules aim to reduce redundancies and spaces for inconsistencies.&lt;/p&gt;

&lt;p&gt;Getting ahead of much of the topic and being quite generalist, denormalization doesn't make sense in environments that aren't analytical, such as data warehouses or OLAP. You might even ask, &lt;em&gt;"but joins are very costly, wouldn't it be better to reduce the number of joins in the most common queries?"&lt;/em&gt; Actually, no. SQL is famous precisely because joins are quite performant; there are some best practices to make joins execute in the best possible way, and I'll list them later.&lt;/p&gt;

&lt;p&gt;It's important to remember that with normalization/denormalization, you're not just gaining/losing performance, but also giving up integrity and consistency. With normalization, you ensure referential integrity, and this prevents some inconsistent data insertions where to compensate, you end up opting to write triggers or the like, which exponentially increases the complexity of your entities.&lt;/p&gt;

&lt;p&gt;Does this mean that analytical environments have always been wrong in terms of denormalization? &lt;strong&gt;No&lt;/strong&gt;, analytical environments are designed already knowing the initial demands of what information will be needed, what is and isn't useful data. In these cases, many relationship keys don't make sense and/or little information from another entity is used, so denormalization occurs. Another point is that in the case of analytical environments, they are a set of entities designed for reading; the removal of join operations really makes a difference at large scales but is not a determining factor compared to other common practices in analytical environments.&lt;/p&gt;

&lt;p&gt;Points to optimize your join operations (or lack thereof):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary and foreign keys should be of the same type and in the "same manner". Doing a type cast or an expression like lower() to then be able to do the join. The database needs to do a full scan, apply the expression/cast, and then make the comparisons.&lt;/li&gt;
&lt;li&gt;Very small entity (a few dozen rows). In these cases, scanning the table will probably be more performant than doing a join.&lt;/li&gt;
&lt;li&gt;Primary and/or foreign key are not indexed, which means the database needs to fully scan the table, which obviously reduces performance (&lt;strong&gt;SIGNIFICANTLY&lt;/strong&gt;).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Choosing a Tool for Versioning and Testing
&lt;/h2&gt;

&lt;p&gt;Versioning and testing in the creation and modification of entities is not very common but makes a &lt;strong&gt;HUGE&lt;/strong&gt; difference in the maintenance and understanding of a system as a whole. It's rare to find codebases where you can see the entire history of the database with field alterations, insertion of new ones, and removal of old ones, such as the evolution of an E-Commerce or ERP system. The easiest way to version your database is with migration scripts. There are some tools like &lt;a href="https://documentation.red-gate.com/flyway" rel="noopener noreferrer"&gt;&lt;em&gt;Flyway&lt;/em&gt;&lt;/a&gt; for JVM that does this with .sql scripts like &lt;code&gt;V0__create_customer_table.sql&lt;/code&gt;, or &lt;a href="https://ollycope.com/software/yoyo/latest/" rel="noopener noreferrer"&gt;&lt;em&gt;YoYo Migrations&lt;/em&gt;&lt;/a&gt; for Python where you can even generate migrations with Python code or .sql scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't forget to create rollback scripts!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Migration management tools enable you to write migration tests that are extremely important for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No data is lost.&lt;/li&gt;
&lt;li&gt;No inconsistencies are created between data.&lt;/li&gt;
&lt;li&gt;There are no unnecessary duplications.&lt;/li&gt;
&lt;li&gt;Prevention of failures due to logical or syntax errors.&lt;/li&gt;
&lt;li&gt;Allows performance evaluation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before choosing the migration tool you'll use, it's important to know some aspects of it. The most important aspect I usually consider is whether my database will have more than one interface that will manipulate entities and make migrations. Why consider this? Tools like &lt;em&gt;Alembic&lt;/em&gt;, also from Python, do a very simple metadata management that's perfect for small and unique projects. That is, you won't have problems working with just one project performing migrations. This scenario changes when you have other applications also making migrations because it's very easy to lose the order and continuity of migrations. Tools like &lt;em&gt;Flyway&lt;/em&gt; and &lt;em&gt;YoYo&lt;/em&gt; do more complex but also more complete metadata management and usually serve better in these cases.&lt;/p&gt;

&lt;p&gt;It's important to create rollback scripts so that if any of the migrations are performed successfully but haven't generated the expected result (bug), it can easily be undone to return to the previous state of the database. It's understandable that not all migrations have a rollback script, especially those that affect large quantities of unmapped tuples. In these cases, it's extremely important to have a development database that copies the production database in smaller proportions.&lt;/p&gt;

&lt;p&gt;Additionally, migration tools are easily integrated into CI/CD environments, which facilitates bringing entity changes to production environments, unifying and exposing the results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.liquibase.com/resources/guides/database-schema-migration" rel="noopener noreferrer"&gt;Database Schema Migration: Understand, Optimize, Automate&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Documenting Your Entities and Fields
&lt;/h2&gt;

&lt;p&gt;This topic draws a bit from what the first topic (&lt;strong&gt;Naming Convention Standardization&lt;/strong&gt;) introduced, which is a data dictionary defining meanings for fields, relationships, functions, giving meaning to elements that might seem ambiguous.&lt;/p&gt;

&lt;p&gt;The creation of metadata (data about data) is extremely important, especially for consumption by other teams. It's a practice that comes from data engineering teams but is also valuable for development teams. First, it's important to briefly describe the cardinality and relationship of the tables. There are numerous tools that allow this type of work, but the most common ones are &lt;a href="https://www.lucidchart.com/" rel="noopener noreferrer"&gt;&lt;em&gt;Lucidchart&lt;/em&gt;&lt;/a&gt; and the new &lt;a href="https://github.com/drawdb-io/drawdb" rel="noopener noreferrer"&gt;&lt;em&gt;drawdb&lt;/em&gt;&lt;/a&gt;. It's a simple task that is usually done even before the materialization of the tables; you can feel free to use any tool you want. There are some tools that generate documentation in HTML/PDF format like &lt;em&gt;&lt;a href="https://dataedo.com/" rel="noopener noreferrer"&gt;Dataedo&lt;/a&gt;&lt;/em&gt; which I also recommend. The next tool I'll list is a bit more comprehensive and solves the problem more satisfactorily.&lt;/p&gt;

&lt;p&gt;Data engineering teams have been frequently using &lt;em&gt;&lt;a href="https://open-metadata.org/" rel="noopener noreferrer"&gt;Open Metadata&lt;/a&gt;&lt;/em&gt;, an OSS tool for creating metadata. Software engineering professionals might think it could be a bit much for documentation, but tools like this are perfect for all types of companies and should be included in the culture as early as possible. Briefly explaining how Open Metadata works: When plugged into the database, the tool "scans" it for all entities, functions, triggers, and so on, and brings this information into a very user-friendly Web UI. With this "recognition" step done, you can put metadata on the fields to say how they are calculated, in which system they originate, and in which services they are used. This, combined with a very useful text search, gives you a complete tool that describes your database from end to end and provides crucial information about the entities and their respective fields.&lt;/p&gt;

&lt;p&gt;In general, there are N ways to document your database entities. The point is how you will make it available to stakeholders, what work you will have to do to document/update, and what granularity of information your documentation will have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.red-gate.com/simple-talk/databases/sql-server/database-administration-sql-server/database-documentation-lands-of-trolls-why-and-how/" rel="noopener noreferrer"&gt;Database Documentation - Lands of Trolls: Why and How? - Simple Talk&lt;/a&gt; &lt;a href="https://atlan.com/what-is-metadata/" rel="noopener noreferrer"&gt;Metadata: Definition, Examples, Benefits &amp;amp; Use Cases&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Applying Indexes Correctly
&lt;/h2&gt;

&lt;p&gt;In your queries, regardless of the data they retrieve or how you execute them, a universal best practice is to always think about reducing the number of I/Os the database needs to perform. The most common way to do this is with indexes. Indexes, in a quick, succinct, and rough explanation, are metadata about the data stored in the database that point to where certain tuples with certain values are located. A quick contextualization about indexes with a practical example: You make the following query &lt;code&gt;select * from orders where order_dt = '2024-11-05'&lt;/code&gt;, the &lt;code&gt;order_dt&lt;/code&gt; column has a b+ tree index. Instead of traversing all the data pages, the query planner goes directly to the pages that have the value &lt;code&gt;2024-11-05&lt;/code&gt; because it knows this thanks to the index metadata. In general, you'll prefer &lt;em&gt;Index Scan&lt;/em&gt; queries because the query planner uses the index to reach the desired values, and a &lt;em&gt;Table Scan&lt;/em&gt; query scans the entire table looking for these values. Indexes have a mathematics called selectivity, which basically tells the ability of the index to filter a large portion of the data from a table. The calculation is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;1&lt;/code&gt; &lt;code&gt;Selectivity = (Number of distinct values) / (Total number of records)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The closer to 1, the more selective the index is, and the more selective it is, the smaller the percentage of records returned in a query. It's counterintuitive, but they are usually more effective because the database can scan these indexes more quickly.&lt;/p&gt;

&lt;p&gt;After multiple insertions, removals, and updates of the data, the indexes can and probably will become fragmented. This happens because the physical order (where they are stored) does not correspond to the logical order, and this will result in loss of performance in searches. Moreover, unfortunately, there's no magic, and indexes come with a cost that is paid mainly in storage (you have metadata and this needs to be stored somewhere) and also in the performance of data manipulation operations (INSERT, UPDATE, DELETE) (the database needs to rearrange the indexes in these cases). This already gives a clear warning that you can't simply go filling your database with indexes. Their implementation should be studied so that it's not more harmful than beneficial to the database. This also applies to analytical environments where many complex queries are executed and the database will naturally do a table scan, which makes the design of these indexes even more crucial.&lt;/p&gt;

&lt;p&gt;It's important to note that there are different types of indexes such as &lt;strong&gt;hash&lt;/strong&gt;, which is excellent for equality searches but not so good for range searches, or &lt;strong&gt;fulltext&lt;/strong&gt;, which is optimized for large volumes of text and supports keyword and phrase searches. If you want to optimize queries on your database entities, I strongly recommend that you deepen your studies on indexes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.databasestar.com/sql-indexes/" rel="noopener noreferrer"&gt;SQL Indexes - The Definitive Guide - Database Star&lt;/a&gt; &lt;br&gt;
&lt;a href="https://www.elitmus.com/blog/technology/an-in-depth-look-at-database-indexing/" rel="noopener noreferrer"&gt;An in-depth look at Database Indexing – eLitmus Blog&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Modern Backup and Restoration Pipelines
&lt;/h2&gt;

&lt;p&gt;Those who think backups are only for restoring data in case of a natural catastrophe or some invasion and data hijacking are mistaken. Of course, these points are important and should be considered in planning your backups, but there are other reasons which you can use as motivators to improve your processes to make them more efficient on all fronts and better documented. We'll include in our list of "Why have modern backup pipelines" the ease of testing new features and integrations that make massive use of the database (high number of insertions or data manipulation and new data types). Having a backup of the database state in a previous version of the application can save you (and probably will) from numerous scenarios where things didn't go as planned. Another clear use is for compliance needs; in cases of cybercrime or legal disputes, it's extremely important to have the entire history of data movement for use in investigations.&lt;/p&gt;

&lt;p&gt;It's extremely common to see database backups being done with a plain-text SQL dump and also with binary formats. SQL Dump is only an option because many DBMSs do it automatically where you configure few things and they are generated automatically, but they are far from ideal in scenarios where you have a medium/large amount of data. The format is quite ineffective both in storage (takes up a lot of space), restoration (medium/large volumes will take good hours if not days to be restored), take a significant amount of time to be written, and do not have integrability with other tools for reading (analysis/query). For these reasons, we disqualify SQL dump as an option for backups in robust databases. Binary files solve part of these problems such as storage and writing time, which are good reasons to be used despite the integrability being worse than SQL dumps since binaries are usually specific to a particular DBMS. Binaries are more difficult to manage because they usually require a bit more technical knowledge, but the gains are noticeable and in most cases worth it, but now, we'll talk about another type of architecture.&lt;/p&gt;

&lt;p&gt;With the modernization of areas such as data engineering, machine learning engineering, data science and related fields, new file formats and new tools for manipulating these files have emerged. Although they are directed to analytical environments and not exactly for backup purposes, some of these tools can be applied to this context to extract great value. Something that analytical environments and backups have in common is the low, or no, amount of data change operations, only large volumes of insertion. With this, we can use columnar format files that have advanced compression techniques that significantly reduce the size and time used to read and reinsert back into a transactional environment. &lt;br&gt;
&lt;em&gt;Read my article &lt;a href="https://dev.to/pedrohgoncalves/different-file-formats-a-benchmark-doing-basic-operations-jfj"&gt;Different file formats, a benchmark doing basic operations&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The parquet format is more optimized in terms of storage mainly for transactional environment data that has a large amount of duplicate values (parquet uses a compression technique that reduces duplication, decreasing size). Writing parquet files is also not time-consuming; the time is usually quite similar to binary files despite requiring a bit more computational resource. Parquet files can be queried without necessarily a restoration, which facilitates in some scenarios where it would be necessary to restore a complete version to query some specific information in the case of binary. Although it stores the schema in its metadata, this metadata is not compatible with most DBMSs, and this is a problem if you don't have your backups synchronized with migrations. This style of architecture called "modern data stack" in the data engineering area requires more specific technical knowledge in some tools and concepts, but it will probably generate great savings in the resources your backups consume and in the quality of processes. In all cases, changes of this type that affect such complex and delicate things as compliance should be widely discussed and studied before being adopted, no matter how much the numbers point to great improvements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Many of these topics could easily become an article of this size on their own or even larger, but the idea is not to explain in depth, understand the anatomy, or explain in micro details how they work, but rather to give a reliable and robust introduction. The necessary deepening for future implementations or changes can be achieved by reading the documentation of the DBMSs, the tools you will use for assistance, in books about software architecture and application design, or in more specific articles.&lt;/p&gt;

&lt;p&gt;If you want to know more about databases, I recommend reading my other article about the ACID principle. &lt;a href="https://dev.to/pedrohgoncalves/transactions-and-the-acid-principle-going-a-little-deeper-32ie"&gt;Transactions and the ACID principle, going a little deeper.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you for your attention.&lt;/p&gt;

</description>
      <category>database</category>
      <category>advanced</category>
      <category>optimization</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Improving your Python code, an initial series of best practices.</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Thu, 21 Mar 2024 01:38:20 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/improving-your-python-code-an-initial-series-of-best-practices-150j</link>
      <guid>https://dev.to/pedrohgoncalves/improving-your-python-code-an-initial-series-of-best-practices-150j</guid>
      <description>&lt;p&gt;This article gathers some of the practices I would like to see in the codebases I maintain and that I believe are not difficult to adopt. Obviously, it is impossible to demand that developers, or you demand of yourself, all good code practices. You can start with this initial set and gradually improve.&lt;/p&gt;

&lt;p&gt;If you are looking for ways to optimize your code in terms of asymptotic complexity or anything related, this article will be of little use to you. However, if you want to improve the readability and organization of your Python projects, I strongly recommend reading it. You can adopt these practices in any type of project, not just in a specific niche like web development (which has some other good practices), data science, or any other area. It is important to consider that these practices should not override the code standards that your company/project already has, whether it's naming conventions, indentation, comments, or anything else. The overall goal is to provide you with ideas on how to keep your code readable and organized; you can modify the practices to your liking. Ultimately, what matters are the standards you adopt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exception Handling
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This best practice might sound like a rule imposition, but it's really important that you take it into consideration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why did I include exception handling as a topic to be addressed in good Python coding practices when there are many others? Well, exception handling is particularly dangerous in many applications, and if not done correctly, you will likely infer unwanted behavior in your application. In short, it's much easier to mess up with exception handling than with other practices.&lt;/p&gt;

&lt;p&gt;It's quite common to find the following code snippet in various Python projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;something&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I was quite generous with the part of log.register; you're more likely to find a print(e).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is extremely problematic for two reasons:&lt;/p&gt;

&lt;p&gt;Firstly, it catches all exceptions that may occur in that block of code, and you probably don't want that because debugging errors will become infinitely more difficult. Moreover, it enables your code to do whatever it wants and still continue to execute. It also doesn't tell me anything about what might potentially happen. If you put this in your application, you did it thinking about a potential error, such as a UniqueViolation from psycopg2 or a TypeError, and therefore you should focus only on the errors you want to handle.&lt;/p&gt;

&lt;p&gt;Secondly, it does nothing after catching the exception. In this case, it still logs an error (which is far from ideal), but in many applications with this type of snippet, it serves only to "not break" the code. However, it's always good for the rest of your application to know the result of that operation. Propagating the exception to higher layers or changing the behavior after a failure is a good practice.&lt;/p&gt;

&lt;p&gt;To refactor this exception, we could do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;something&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UniqueViolation&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;duplicate_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duplicate_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ModifiedExceptionDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Value exists in the database.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;type_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# do something
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ModifiedExceptionType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Type mismatch.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's also important to remember that it's not good to have large code blocks or a very extensive flow inside try-except blocks. The larger the block, the more errors it catches, and the less visibility you have of your application.&lt;/p&gt;

&lt;p&gt;In summary, you will want to avoid catching generic &lt;code&gt;Exception&lt;/code&gt; and instead handle only specific exceptions. After handling them, share the result of the operation with the rest of your application so that something can be done in higher layers.&lt;/p&gt;

&lt;p&gt;To learn more about error handling: &lt;a href="https://peps.python.org/pep-0463/" rel="noopener noreferrer"&gt;PEP 463 – Exception-catching expressions | peps.python.org&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Styling and Standards
&lt;/h2&gt;

&lt;p&gt;For code standards, there are several points you can address, such as naming conventions, abbreviations, common methods, among others. Speaking a bit about naming styles, Python's recommended styles are widely disseminated; it's almost common knowledge that snake case convention is used for function, method, and variable names, and Pascal case for classes. Although they are widely adopted by the community, they are far from irreplaceable. Now, if your team is accustomed to or already writes differently, it's completely acceptable, and this shouldn't be a problem for your codebase. However, if your team doesn't follow any standardization, whether in styling, naming, or tool usage, it's important that you adopt one. I strongly recommend that you write a document that is easily accessible to your team, containing the code standards you choose to adopt. To assist you, I'll list some points you can mention and give examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default Python version: 3.10&lt;/li&gt;
&lt;li&gt;Abbreviations. Ex: dt = date, nb = number&lt;/li&gt;
&lt;li&gt;Functions: Snake Case&lt;/li&gt;
&lt;li&gt;Methods: Snake Case&lt;/li&gt;
&lt;li&gt;Variables: Snake Case&lt;/li&gt;
&lt;li&gt;Classes: Pascal Case&lt;/li&gt;
&lt;li&gt;Modules: Kebab Case&lt;/li&gt;
&lt;li&gt;Spacing and indentation: Refer to PEP 8 (or any other)&lt;/li&gt;
&lt;li&gt;Database connection: ...&lt;/li&gt;
&lt;li&gt;Database operations: ...&lt;/li&gt;
&lt;li&gt;Library for migrations: ...&lt;/li&gt;
&lt;li&gt;Credential management: ...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are many other points you can address with a code standard to facilitate maintenance, reduce errors, and increase the testability of your code. Obviously, it's impossible to standardize all your code, and I dare say that would be harmful. However, mapping the most repeated points and addressing them in a standardized way is a good software development practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type Hints
&lt;/h2&gt;

&lt;p&gt;Python is a language with dynamic typing, which means that data types are inferred at runtime. That is, you don't assign data types to each variable, class parameter, method, or function returns. Fortunately, it has type hints (available from Python 3.5) which are literally what the name suggests, hints about types. Type hints do not interfere with code execution but aid in readability and maintenance. Some tools like PyCharm and VSCode extensions provide highlights when you infer a type that does not match the type hint. To infer a type &lt;code&gt;int&lt;/code&gt; in your code, after the variable or parameter name, you put a colon &lt;code&gt;:&lt;/code&gt; followed by its type as &lt;code&gt;int&lt;/code&gt;. In function returns, you should use a dash followed by a right arrow &lt;code&gt;-&amp;gt;&lt;/code&gt; and the return type as &lt;code&gt;str&lt;/code&gt;. Let's illustrate with code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;persons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_legal_age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our code, we have a dictionary with a string key and an integer value, and the type hint &lt;code&gt;dict[str, int]&lt;/code&gt; exactly conveys this. You might think that in this case, a type hint could be more of a hindrance than a help since the variable's value is explicit in the code, and you are correct. But in cases where we can't see what value the variable assumes after code execution, type hints are extremely important to give context to your application.&lt;/p&gt;

&lt;p&gt;In ideal applications, all variables would have type hints regardless of how the value is assigned to them. However, it's understandable that updating them whenever there's a change in the code or importing and inferring their types could be complicated. Therefore, &lt;strong&gt;I&lt;/strong&gt; usually put type hints only on class constructors' parameters, functions, methods, and their returns.&lt;/p&gt;

&lt;p&gt;It's important to remember that there are some tools like MyPy that serve as a "compiler"; it detects type errors using type hints and prevents problem execution until the error is fixed. It's not widely used, but if you encounter many type problems in your codebase, you may consider it as an option.&lt;/p&gt;

&lt;p&gt;To learn more about type hints: &lt;a href="https://peps.python.org/pep-0484/" rel="noopener noreferrer"&gt;PEP 484 – Type Hints | peps.python.org&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Docstrings
&lt;/h2&gt;

&lt;p&gt;Not only in Python but also in other languages, it's very common to have documentation for functions, classes, and methods in the code itself, as if they were comments. In Python, we call this documentation "docstrings," which are present in all built-in methods (functions integrated into the compiler) to community-made frameworks. It consists of adding a string below the initialization of your function, class, or method that succinctly explains some fundamental points such as what that function does, its parameters and their types, what that snippet returns, which exceptions it raises, and how it handles them, among other points. Let's visualize with a code example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A function that sums two values

        Parameters:
            one: First value to sum
            two: Second value to sum

        Raises:
            TypeError: if one of the two values is not an integer

        Returns:
            The sum of the two values
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;two&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Both values must be integers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function in the example sums two integers. The docstring explains what the function does, the parameters it receives and what they do, explains which exception is handled, in which scenario, and summarizes what it returns. Of course, you can write the docstring to your liking; some people might disagree with how this one is written, but in the end, it fulfills its purpose of explaining the function's operation and also meets standardization criteria.&lt;/p&gt;

&lt;p&gt;If you want to follow this pattern, it's important that you write your docstring within 6 double quotes, 3 at the beginning, a line break, and 3 at the end. Explain briefly what the function does in the first line/sentence, list the parameters separated by line breaks, include considerations you consider important such as exception handling, and finally, mention the return value, of course, separating all topics by blank lines. If the function doesn't have parameters, return values, or exceptions, it's okay to omit this information. Another consideration to make is that it's crucial to keep it updated; if you make a modification and don't update the docstring, it's preferable to delete the docstring than to leave misleading information.&lt;/p&gt;

&lt;p&gt;Docstrings written in classes generate the &lt;code&gt;__help__&lt;/code&gt; method where, if you type &lt;code&gt;print(Class.__help__)&lt;/code&gt;, the docstring will be printed in the terminal. For functions and methods, you enable the built-in help, where if you type &lt;code&gt;print(help(sum))&lt;/code&gt;, it will also print the docstring. Additionally, it optimizes automatic documentation such as that of the Sphinx tool, which relies on docstrings for documentation generation.&lt;/p&gt;

&lt;p&gt;Although it's a common consensus in the community that docstrings should be written for all classes, methods, and functions, it's understandable that this may not be possible due to a lack of practice leading to difficulty in creating readable documentation or due to lack of time to create and modify it. Therefore, something &lt;strong&gt;I&lt;/strong&gt; have adopted is to write docstrings for functions that I consider too complex and would take too long to understand with just the code reading. But of course, this is something you should think about and consider before imposing it on your projects or team.&lt;/p&gt;

&lt;p&gt;In the example of our code, which has an extremely simple function, in a real project, it would be ideal to assign a single-line docstring because it's a very obvious case. I exemplified with a more complex docstring because I believe it covers most cases of real projects. Anyway, docstring is a quite complex subject; it could easily fill an entire article on its own.&lt;/p&gt;

&lt;p&gt;To learn more about docstrings: &lt;a href="https://peps.python.org/pep-0257/" rel="noopener noreferrer"&gt;PEP 257 – Docstring Conventions | peps.python.org&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Perhaps, like me, a good portion of the scripts you write are not influenced by others, and you may judge it unimportant to adopt these practices. However, in the future, when you write code for others, they will likely be very grateful to see these practices materialized in your code. Furthermore, the you of the future will thank the you of the past for writing readable code and adopting code standards. It's important to exercise these skills to become almost like muscle memory and be able to develop readable code more quickly. If you can apply this, you'll become a much better developer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: I didn't mention code linters because I consider it a step further. They are great and almost mandatory in serious Python projects, but they require external configuration and this can be a problem for some people.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>goodpractices</category>
      <category>improving</category>
    </item>
    <item>
      <title>Different file formats, a benchmark doing basic operations</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Sun, 10 Mar 2024 20:35:14 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/different-file-formats-a-benchmark-doing-basic-operations-jfj</link>
      <guid>https://dev.to/pedrohgoncalves/different-file-formats-a-benchmark-doing-basic-operations-jfj</guid>
      <description>&lt;p&gt;Recently, I've been designing a data lake to store different types of data from various sources, catering to diverse demands across different areas and levels. To determine the best file type for storing this data, I compiled points of interest, considering the needs and demands of different areas. These points include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Compatibility
&lt;/h3&gt;

&lt;p&gt;Tool compatibility refers to which tools can write and read a specific file type. No/low code tools are crucial, especially when tools like Excel/LibreOffice play a significant role in operational layers where collaborators may have less technical knowledge to use other tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;

&lt;p&gt;How much extra or less space will a particular file type cost in the data lake? While non-volatile memory is relatively cheap nowadays, both on-premise and in the cloud, with a large volume of data, any savings and storage optimization can make a difference in the final balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading
&lt;/h3&gt;

&lt;p&gt;How long do the tools that will consume the data take to open and read the file? In applications where reading seconds matter, sacrificing compatibility and storage for gains in processing time becomes crucial in the data pipeline architecture planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing
&lt;/h3&gt;

&lt;p&gt;How long will the tools used by our data team take to generate the file in the data lake? If immediate file availability is a priority, this is an attribute we would like to minimize as much as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query
&lt;/h3&gt;

&lt;p&gt;Some services will directly consume data from the file and perform grouping and filtering functions. Therefore, it's essential to consider how much time these operations will take to make the correct choice in our data solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Files
&lt;/h3&gt;

&lt;h5&gt;
  
  
  1 - &lt;a href="https://www.kaggle.com/datasets/ealtman2019/ibm-transactions-for-anti-money-laundering-aml?rvi=1" rel="noopener noreferrer"&gt;IBM Transactions for Anti Money Laundering (AML)&lt;/a&gt;
&lt;/h5&gt;

&lt;p&gt;Rows: 31 million &lt;br&gt;
Columns: 11 &lt;br&gt;
Types: Timestamp, String, Integer, Digital, Boolean&lt;/p&gt;

&lt;h5&gt;
  
  
  2 - &lt;a href="https://www.kaggle.com/datasets/agungpambudi/network-malware-detection-connection-analysis?select=CTU-IoT-Malware-Capture-9-1conn.log.labeled.csv" rel="noopener noreferrer"&gt;Malware Detection in Network Traffic Data&lt;/a&gt;
&lt;/h5&gt;

&lt;p&gt;Rows: 6 million &lt;br&gt;
Columns: 23 &lt;br&gt;
Types: String, Integer&lt;/p&gt;

&lt;h3&gt;
  
  
  Number of Tests
&lt;/h3&gt;

&lt;p&gt;15 tests were conducted for each operation on each file, and the results in the graphs represent the average of each test iteration's results. The only variable unaffected by the number of tests is the file size, which remains the same regardless of how many times it is written.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why 2 datasets?
&lt;/h3&gt;

&lt;p&gt;I chose two completely different datasets. The first is significantly larger than the second and contains many null values represented by "-" and many columns with duplicate values where the distinction is low. In contrast, the first dataset has few columns with little data variability and contains more complex types such as timestamps. These characteristics highlight the distinctions, strengths, and weaknesses of each format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Script
&lt;/h3&gt;

&lt;p&gt;The script used for benchmarking is open on GitHub for anyone who wants to check or conduct their benchmarks with their files, which I strongly recommend.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://github.com/pedrohgoncalvess/file-format-benchmark" rel="noopener noreferrer"&gt;file-format-benchmark: benchmark script of key operations between different file formats&lt;/a&gt;
&lt;/h4&gt;

&lt;h3&gt;
  
  
  Tools
&lt;/h3&gt;

&lt;p&gt;I will use Python with Spark for the benchmark. Spark allows native queries on different file types, unlike Pandas, which requires an extra library to achieve this. Additionally, Spark is more performant in handling larger datasets, and the datasets used in this benchmark are relatively large, where Pandas struggled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Env:&lt;/strong&gt; &lt;br&gt;
Python version: 3.11.7 &lt;br&gt;
Spark version: 3.5.0 &lt;br&gt;
Hadoop version: 3.4.1&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tool Compatibility
&lt;/h3&gt;

&lt;p&gt;Although I wanted to measure tool compatibility, I couldn't find a way to do it, so I'll share &lt;strong&gt;my opinion&lt;/strong&gt;. For pipelines with downstream stakeholders who have more technical knowledge (data scientists, machine learning engineers, etc.), the file format matters little. With a library/framework in any programming language, you can manipulate information from a file in any format. However, for non-technical stakeholders like business analysts, C-level executives, or other collaborators who work directly with product/service production, the scenario changes. These individuals often use tools like Excel, LibreOffice, PowerBI, or Tableau (which, despite having more native readers, do not support Avro or ORC). In cases where files are consumed "manually" by people, you will almost always opt for CSV or JSON. These formats, being plain-text, can be opened, read, and understood in any text editor. Additionally, all kinds of tools can read structured data in files in these formats. Parquet still has some compatibility, being the column storage type with the most support and attention from the community. On the other hand, ORC and Avro have very little support and can be challenging to find parsers and serializers in non-Apache tools.&lt;/p&gt;

&lt;p&gt;In summary, CSV and JSON have a significant advantage over the others, and you will likely choose them when your stakeholders are directly handling the files and lack technical knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dataset 1:&lt;/strong&gt;&lt;br&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%2Fls7ih24fhy7thg0l0vek.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%2Fls7ih24fhy7thg0l0vek.png" alt="Storage results graph" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset 2:&lt;/strong&gt;&lt;br&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%2F2qfhrzidjqpqa7p86pev.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%2F2qfhrzidjqpqa7p86pev.png" alt="Storage results graph" width="800" height="269"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;To calculate storage, we loaded the dataset in CSV format, rewrote it in all formats (including CSV itself), and listed the amount of space they occupy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The graphs show a significant advantage for JSON, which was three times larger than the second-largest file (CSV) in both cases. The difference is so pronounced due to the way JSON is written, following a schema of a list of objects with key-value pairs, where the key is the column, and the value is the column's value in that tuple. This results in unnecessary schema redundancy, always inferring the column name in all values. In addition to being plain-text without any compression, similar to CSV, JSON shows the two worst performances in terms of storage. Parquet, ORC, and Avro had very similar results, highlighting their efficiency in storage compared to more common types. The key reasons for this advantage are that Parquet and Avro are binary files, offering a storage advantage. Furthermore, Parquet and ORC are columnar format files that significantly reduce data redundancy, avoiding waste and optimizing space. All three formats have highly efficient compression methods.&lt;/p&gt;

&lt;p&gt;In summary, CSV and JSON are by no means the best for storage optimization, especially in cases like storing logs or data with no immediate importance but cannot be discarded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dataset 1:&lt;/strong&gt;&lt;br&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%2Fcrz05ctz20a2xigwqafc.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%2Fcrz05ctz20a2xigwqafc.png" alt="Reading results graph" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset 2:&lt;/strong&gt;&lt;br&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%2Fdmv6s0p39jtm5gdhznuy.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%2Fdmv6s0p39jtm5gdhznuy.png" alt="Reading results graph" width="800" height="280"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;In the reading operation, we timed the dataset loading and printed the first 5 rows.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In reading, there is a peculiar case: despite increasing the file size difference several times (3x), the only format with a visible and relevant difference was JSON. This occurs solely due to the way JSON is written, making it costly for the Spark parser to work with that amount of metadata (redundant schema). The growth in reading time is exponential with the file size. As for why CSV performed as well as ORC and Parquet, it is because CSV is extremely simple, lacking metadata like a schema with types or field names. It is quick for the Spark parser to read, separate, and assess the column types of CSV files, unlike ORC and, especially, Parquet, which have a large amount of metadata useful in cases of files with more fields, complex types, and a larger amount of data. The difference between Avro, Parquet, and ORC is minimal and varies depending on the state of the cluster/machine, simultaneous tasks, and the data file layout. In the case of these datasets in the reading operation, it is challenging to evaluate the difference; it becomes more evident when scaling these files to several times larger than the datasets we are working with.&lt;/p&gt;

&lt;p&gt;In summary, CSV, Parquet, ORC, and Avro had almost no difference in reading performance, while JSON cannot be considered as an option in cases where fast data reading is required. Few cases prioritize reading alone; it is generally evaluated along with another task like a query. If you are looking for the most performant file type for this operation, you should consider conducting your own tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dataset 1:&lt;/strong&gt;&lt;br&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%2Fjtn3hn87b1fmkkz6ptuk.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%2Fjtn3hn87b1fmkkz6ptuk.png" alt="Write results graph" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset 2:&lt;/strong&gt;&lt;br&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%2Ftdl4aexrgqmc6rzjiozs.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%2Ftdl4aexrgqmc6rzjiozs.png" alt="Write results graph" width="800" height="272"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;In the writing operation, we read a .csv file and rewrote it in the respective format, only counting the writing time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In writing, there was a surprise: JSON was not the slowest to be written in the first dataset; it was actually ORC. However, in the second dataset, JSON took the longest. This discrepancy is due to the second dataset having more columns, meaning more metadata to be written. While ORC is a binary file with static typing of data, similar to Parquet, the difference is that ORC applies "better" optimization and compression techniques, requiring more processing power and time. This justifies the query time (which we will see next) and the generated file size, which is smaller in almost all cases than Parquet files. CSV had good performance because it is a very simple format, lacking additional metadata such as schema and types or redundant metadata like JSON. On a larger scale, more complex files would have better performance than CSV. Avro also has its benefits and had a very positive result in dataset 1, outperforming Parquet and ORC with a significant advantage. This &lt;strong&gt;probably&lt;/strong&gt; happened due to the data layout favoring Avro's optimizations, which differ from Parquet and ORC.&lt;/p&gt;

&lt;p&gt;In summary, Avro, despite not being a format with much fame or community support, is a good choice in situations where you want the quick availability of your files for stakeholders to consume. It starts making a difference when scaling to several GBs of data, where the difference becomes 20-30 minutes instead of 30-40 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dataset 1:&lt;/strong&gt;&lt;br&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%2F6iujc2kk1bo698nilkit.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%2F6iujc2kk1bo698nilkit.png" alt="Query results graph" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataset 2:&lt;/strong&gt;&lt;br&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%2F3xv3vklkl28wl4d3btdk.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%2F3xv3vklkl28wl4d3btdk.png" alt="Query results graph" width="800" height="274"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;In the query operation, the dataset was loaded, and a query with only one WHERE clause filtering a unique value was performed, followed by printing the respective tuple.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the first file, all formats had good performance, and the graph scales give the impression that Parquet had poor performance. However, the differences are minimal. Since dataset 2 is much smaller, we believe the query results are very susceptible to external factors. Therefore, we will focus on explaining the results in the first dataset. As mentioned earlier, ORC performs well compared even to Avro, which had excellent performance in other operations. Still, Parquet leads this ranking as the fastest query result. Why? Parquet, being the default format for Spark, indicates much about how the framework works with this format. It incorporates various query optimization techniques, many consolidated in DBMSs. One of the most famous is &lt;strong&gt;predicate pushdown&lt;/strong&gt;, which essentially leaves the WHERE clauses at the end of the execution plan to reduce the amount of data read and examined on the disk. This is an optimization not present in ORC. Why do CSV and JSON lag so far behind? In this case, CSV and JSON are not the problem; the truth is that Parquet and ORC are very well optimized. All the benefits mentioned earlier, such as schema metadata, binary files, and columnar formats, give them a significant advantage. And where does Avro fit into this since it has many of these mentioned benefits? In terms of query optimization, Avro lags far behind ORC and Parquet. One of the points we can mention is &lt;strong&gt;column projection&lt;/strong&gt;, which essentially computes only the specific columns used in the query rather than the entire dataset. This is present in ORC and Parquet but not in Avro. Logically, this is not the only thing that makes ORC and Parquet differ so much from Avro in terms of query optimization, but overall, Avro falls far behind in query optimization.&lt;/p&gt;

&lt;p&gt;In summary, when working with large files, with both simple and complex queries, you will want to work with Parquet or ORC. Both have many query optimizations that will deliver results much faster compared to other formats. This difference is already evident in files slightly smaller than dataset 1 and becomes even more apparent in larger files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In a data engineering environment where you need to serve various stakeholders, consume from various sources, and store data in different storage systems, operations such as reading, writing, or queries are widely affected by the file format. Here, we see what issues certain formats may have, evaluating the main points raised in data environment constructions.&lt;/p&gt;

&lt;p&gt;Even though Parquet is the "darling" of the community, we were able to highlight some of its strengths, such as query performance, but also show that there are better options for certain scenarios, such as ORC in storage optimization.&lt;/p&gt;

&lt;p&gt;The performances of these operations for each format also depend heavily on the tool you are using and how you are using it (environment and available resources). The results from Spark &lt;strong&gt;probably&lt;/strong&gt; will not differ much from other more robust frameworks like Duckdb or Flink, but we recommend that you conduct your tests before making any decisions that will have a significant impact on other areas of the business.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>spark</category>
      <category>benchmark</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Transactions and the ACID principle, going a little deeper.</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Mon, 04 Sep 2023 21:59:46 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/transactions-and-the-acid-principle-going-a-little-deeper-32ie</link>
      <guid>https://dev.to/pedrohgoncalves/transactions-and-the-acid-principle-going-a-little-deeper-32ie</guid>
      <description>&lt;p&gt;Recently, I began to study how databases work under the hood, and this is the result of a small part of those studies. Today, I will share what I have learned about transactions and how Database Management Systems (DBMS) minimize errors, both in terms of data inconsistency and general errors that can affect the availability and performance of the database. I emphasize to everyone to read the documentation of the DBMS you are using to make better use of the tools it provides and to improve the performance of your applications. This article is a generalization of how DBMS implement these properties, and only by reading the documentation will you have concrete answers.&lt;/p&gt;

&lt;h1&gt;
  
  
  Transactions
&lt;/h1&gt;

&lt;p&gt;We can say that a transaction is a unit of work. Sometimes, it is difficult to perform all the data manipulation you want to do with just one query; it may even be impossible. That's where a transaction comes in – to execute a sequence of queries.&lt;/p&gt;

&lt;p&gt;Transactions are generally used for data manipulation (which does not prevent or prohibit read-only queries). For this purpose, they have a set of properties that make data manipulation and querying safer in terms of data quality. This set of properties is formed by the concepts of Atomicity, Consistency, Isolation, and Durability, which form the acronym ACID. Later, we will delve deeper into how they work and what they represent.&lt;/p&gt;

&lt;p&gt;Returning to transactions, there are various DBMSs that address the topic in their own way, with some prioritizing performance in certain areas and others prioritizing performance in other operations. But in general, the way they work is quite similar. We will start with some commands that initiate transactions, terminate them, and what they do exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transaction Handling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BEGIN&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we give the command &lt;strong&gt;BEGIN&lt;/strong&gt; to the database, we are asking it to start a new "branch." In this context, a "branch" is something like a checkpoint in a game. We start our branch here, can perform our SQL manipulation or queries, and have some "privileges," such as the ability to roll back without the database applying the changes we instructed within the branch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;COMMIT&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As I mentioned earlier, starting a transaction or, as we call it, a "branch" gives us some privileges, such as the ability to revert to the database's state at the point where we began that transaction. But how do we confirm these changes (understand, with changes, I refer to DML operations executed within the same branch)? With the &lt;strong&gt;COMMIT&lt;/strong&gt; command, we ask the database to save the changes we made. In other words, all DMLs we performed in the transaction will be permanently applied to the database and cannot be reverted to the previous state without another DML command.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ROLLBACK&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;ROLLBACK&lt;/strong&gt; command tells the database that we do not want any of the queries executed "within" that transaction to be applied. In other words, everything executed in the transaction will be undone. Some databases allow the use of the &lt;strong&gt;ROLLBACK&lt;/strong&gt; command on DDL operations (e.g., create table) and the like, while others do not. This depends on the DBMS you are using. I recommend reading the documentation of the DBMS you will use in your project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ERRORS&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As I mentioned earlier, DBMSs implement transactions in their own way, and error behavior can vary among them. But it is almost a standard that the database performs a &lt;strong&gt;ROLLBACK&lt;/strong&gt; to preserve data reliability whenever an error occurs. This clashes with the concept of atomicity, which we will see next.&lt;/p&gt;

&lt;h1&gt;
  
  
  ACID
&lt;/h1&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Atomicity&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Atomicity is one of the four concepts that dictate the behavior of a transaction. It mainly deals with cases of failure. The initial idea of atomicity is that a transaction is a unit of work where all queries must be executed successfully, or none of them is executed. This applies both when the transaction is "open," and you are performing queries or manipulations, and if there is a query error, a constraint violation, a database crash for any reason, or even external forces like a power outage, the database will perform a &lt;strong&gt;ROLLBACK&lt;/strong&gt;. Even in more extraordinary cases, such as a crash while the database is performing a &lt;strong&gt;COMMIT&lt;/strong&gt; (writing changes to disk), it will undo all the queries that were successful and return to the state before the transaction started.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's perform a bank transfer between João and Maria with two queries: one to withdraw money from João's account and another to deposit it into Maria's account.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Withdrawing from João's account&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE BANK_ACCOUNTS SET BALANCE - 100 WHERE NAME = 'JOAO'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Depositing into Maria's account&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;BANK_ACCOUNTS&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;BALANCE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'MARIA'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Committing changes&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;COMMIT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x]  João's Query: &lt;strong&gt;ERROR IN THE DATABASE&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ]  Maria's Query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our example, the database encountered some memory error and couldn't write both changes to disk. So, instead of deducting the amount from João's account and not crediting it to Maria's account (which would leave a hole of 100 dolars in the system, thus serving inconsistent data), the database didn't write any of the changes and reverted to the state before the transaction started.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Isolation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As most database management systems (DBMS) allow multiple connections from different sources, each executing various queries, there is a need for transactions executed by these different connections to be isolated, meaning they do not share their operations and changes until they are committed. The principle of isolation ensures that transactions within a database are executed separately, even when occurring concurrently.&lt;/p&gt;

&lt;p&gt;Within isolation, there is something we call &lt;strong&gt;read phenomena&lt;/strong&gt;, which demonstrates errors that can occur when operations are executed in different transactions that are happening simultaneously. There are four phenomena in total:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Dirty read&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A dirty read occurs when a query is executed within an isolation system that shares transaction states. When this happens, there is a possibility that the change will not be committed, and the query will return inconsistent data. To illustrate this, let's use the following table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We initiate the following query in Transaction 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result will be as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quantXprice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Next, Transaction 2 begins in another database connection and performs the following manipulation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;orders&lt;/code&gt; table will now look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Transaction 1 then performs another query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query result will be:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sum(quantXprice)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then Transaction 2 executes the &lt;strong&gt;rollback&lt;/strong&gt; command, undoing all the changes it made, and the &lt;code&gt;orders&lt;/code&gt; table returns to its initial state. Consequently, the result of the &lt;code&gt;sum(quant X price)&lt;/code&gt; operation is not 350 but rather 250.&lt;/p&gt;

&lt;p&gt;Your example illustrates the concept of "dirty read" very effectively and how it can lead to inconsistent data in a database system when transactions are not isolated. Dirty read occurs when one transaction can see data that has been modified by another transaction, but that other transaction has not yet committed its changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Non-repeatable Read&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A non-repeatable read is quite similar to a dirty read, with the difference being that Transaction 2 commits its change before Transaction 1 performs the second query. This leads to data inconsistency. You might think, "But if the change has been committed, shouldn't my report have the most up-to-date number?" However, this contradicts the principle of isolation, which aims to isolate the states and changes of each transaction.&lt;/p&gt;

&lt;p&gt;In a large system where the database experiences a high volume of changes, this can be problematic. Since transactions can observe changes made by other transactions, the results can be inconsistent or unexpected.&lt;/p&gt;

&lt;p&gt;To illustrate, let's use the same table from the dirty read example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Transaction 1 executes a query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result will be as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quantXprice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then Transaction 2 begins in another database connection and performs the following manipulation, followed by a commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="k"&gt;commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;orders&lt;/code&gt; table will now permanently look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Transaction 1 then performs another query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query result will be:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sum(quantXprice)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This example is quite similar because both cases share similarities. The key difference is that a dirty read doesn't require the change to be committed for other transactions to observe the result, whereas in a non-repeatable read, a change must be committed for transactions to observe it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Phantom Read&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A phantom read presents the same issue as the other two phenomena, which is data inconsistency. The difference is that a phantom read involves rows that were not initially read. Instead of transactions making changes to a set or a single row, a phantom read adds rows that were not read in the initial read but affect the second result. It's called a phantom read because there are rows that alter the result but were not read because the change occurred after the transaction's initial query.&lt;/p&gt;

&lt;p&gt;Let's illustrate with the same table example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Transaction 1 performs the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result will be as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quantXprice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then Transaction 2 begins in another database connection and performs the following manipulation, followed by a commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table will now look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now, Transaction 1 performs another query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="nv"&gt;` 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query result will be:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sum(quantXprice)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this case, the row with SKU 3 was added in a separate transaction but affected the consistency of Transaction 1's queries, adding a "phantom row."&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lost Updates&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Lost updates aren't exactly a read phenomenon, but we'll include them in this topic. Lost updates occur when two transactions attempt to make changes to the same row, and one of them ends up overwriting the changes made by the other. In this case, the changes are not shared between transactions, and since transactions don't share status (what they read/changed, when they finished, etc.), a write-write conflict occurs where one transaction "discards" its own change after another transaction commits.&lt;/p&gt;

&lt;p&gt;Let's illustrate this with the same table used in the previous examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Transaction 1 executes the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table will look like this (important to note that no commit has occurred):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Afterward, Transaction 2 begins, where it also executes a modification query and commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="k"&gt;commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table is now permanently updated as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Right after that, Transaction 1 performs the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quant&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quantXprice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;However, due to a lost update where Transaction 1 relied on the "committed table" (where Transaction 2 had just made changes), the result will be:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quantXprice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This means the uncommitted change made by Transaction 1 was discarded without an explicit command, resulting in an unexpected outcome and data inconsistency.&lt;/p&gt;

&lt;p&gt;These are the phenomena that the concept of database isolation aims to eliminate. So, what are isolation levels exactly?&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolation Levels
&lt;/h3&gt;

&lt;p&gt;Isolation levels are divided into different levels because each level of isolation aims to address specific issues (the read phenomena). Each DBMS implements its isolation model based on the problems and demands it seeks to address (in some DBMSs, you can choose the isolation level for your transaction). The isolation levels include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read Uncommitted&lt;/strong&gt;: No isolation; any change can be observed from outside the transaction, whether committed or not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read Committed&lt;/strong&gt;: Minimal isolation; any change made and committed within a transaction can be observed by another transaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repeatable Read&lt;/strong&gt;: Medium isolation; the transaction ensures that when a query reads a row, that row remains unchanged throughout the transaction's execution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Snapshot&lt;/strong&gt;: True isolation. Each query in the transaction is executed based on the tuples that were committed in the database before starting the transaction. It's like taking a "snapshot" of the current state of the database and executing queries based on it, completely isolating that transaction from any other changes another transaction may make.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serializable&lt;/strong&gt;: At the serializable isolation level, the DBMS defines the degree to which transactions are separated from each other, ensuring data consistency during the concurrent execution of multiple transactions. In other words, serializable isolation ensures that even if multiple transactions are happening simultaneously, the final result is the same as if they were executed sequentially, one after the other.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table below illustrates which problems each isolation level addresses.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Isolation Level&lt;/th&gt;
&lt;th&gt;Dirty Read&lt;/th&gt;
&lt;th&gt;Non-Repeatable Read&lt;/th&gt;
&lt;th&gt;Phantom Read&lt;/th&gt;
&lt;th&gt;Lost Updates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These isolation levels offer a range of trade-offs between data consistency and performance, allowing you to choose the level that best suits your application's requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistency
&lt;/h2&gt;

&lt;p&gt;Throughout the text, I mentioned the term "data inconsistency," which is basically when data does not assume the value it should after executing manipulation commands, and it ends up not matching real-world events. In this case, we will address data inconsistencies within the database.&lt;/p&gt;

&lt;p&gt;To illustrate this, let's use two example tables:&lt;/p&gt;

&lt;p&gt;Table "orders":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;quant&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Table "relat_orders":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sku&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this example, we have a relationship inconsistency. The "relat_orders" table has the "amount" column, which is the result of quant x price from the "orders" table. SKU 1 should have an "amount" of 6, but it actually has 5 because the tables do not have the proper relationship. To facilitate table modeling within the database, DBMSs have constraints, which are rules applied to columns to standardize behavior or relationships between them. Examples of constraints include unique, not null, and foreign key constraints.&lt;/p&gt;

&lt;p&gt;The concept of consistency in ACID refers to a database that maintains the integrity of all constraints. The term "integrity" refers to the accuracy or correctness of the data's state within a database, where the database cannot transition from a consistent state to an inconsistent state. Let's work with some scripts to better understand this concept.&lt;/p&gt;

&lt;p&gt;Table creation script (PostgreSQL):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tuple insertion script (PostgreSQL):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database cannot accept the second script because the "status" column must have a value in all tuples that are inserted (due to the "not null" constraint). If the insertion occurs, the database will transition from a consistent state to an inconsistent one because it violated a constraint, consequently violating the principle of consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Durability&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the last property in this set of properties that makes the database so secure throughout the process of creation, management, and data storage. Durability ensures that all changes made within a transaction are written to non-volatile memory after a commit and cannot be altered in any other way except through a subsequent transaction. In other words, durability ensures that data is stored and can be queried and manipulated even in the event of a power outage, errors, or any type of database failure, causing it to be inactive for a certain period.&lt;/p&gt;

&lt;p&gt;The trade-off of durability is quite clear. To establish the durability property, data must be written to non-volatile memory as mentioned earlier (usually a hard drive), and non-volatile memories are naturally slow (I will write about how databases store data on disk and why they are slow in certain operations in the future).&lt;/p&gt;

&lt;p&gt;In-memory databases like Redis, which use RAM (volatile memory), trade off the benefits of ACID properties in favor of performance for both write and read operations. In terms of durability, the trade-off is also quite clear. Continuing with Redis, the low-latency access to data comes with the risk of an unpredictable major force (power outage, errors, etc.) shutting down the service, resulting in data loss since it is not saved in non-volatile memory. Redis also implements some things quite similar to atomicity, consistency, isolation, and durability in its own way. It caters to very specific demands, and surprisingly, these demands are very different from what databases that use volatile memory aim to solve.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;With this, we have been able to understand how a database with ACID properties minimizes errors for both read and write operations. It's important to read about how the DBMS you are using in your project implements these "concepts." What I presented here is a generalization of how databases approach transaction management and data storage.&lt;/p&gt;

&lt;p&gt;Finally, I recommend the "Fundamentals of Database Engineering" course available on Udemy (&lt;em&gt;I'm not someone who usually pays for courses, but this one is really worth it&lt;/em&gt;). I'll leave the link below.&lt;/p&gt;

&lt;p&gt;Course: &lt;a href="https://www.udemy.com/course/database-engines-crash-course/" rel="noopener noreferrer"&gt;Fundamentals of Database Engineering&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you very much to everyone who read this!&lt;/p&gt;

</description>
      <category>database</category>
      <category>beginners</category>
      <category>acid</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>What is data engineering and a B.I architecture</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Fri, 30 Jun 2023 20:28:02 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/what-is-data-engineering-and-a-bi-architecture-kg4</link>
      <guid>https://dev.to/pedrohgoncalves/what-is-data-engineering-and-a-bi-architecture-kg4</guid>
      <description>&lt;p&gt;In this article, we will talk about data engineers, what they manage, what they develop, and other activities they perform. We will also explore what a BI architecture is and what role data engineers play in this vast world of data (which is still relatively new to many people).&lt;/p&gt;

&lt;p&gt;We will also discuss the skills typically possessed by data engineers. To avoid confusion regarding the positions we'll be referring to or if you're unsure about how a BI team is usually structured, I recommend reading my other article that explains the differences in skills and activities performed in various positions. You can find the link below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/pedrohgoncalves/data-roles-in-data-teams-and-your-skill-set-using-math-1332"&gt;&lt;em&gt;&lt;strong&gt;Data roles in data teams and your skill set. Using math.&lt;/strong&gt;&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Data Engineer
&lt;/h1&gt;

&lt;p&gt;A data engineer is responsible for designing, building, and managing data infrastructures that handle the processing, transformation, and storage of large volumes of data from various sources. There are different "types" of data engineers, such as those working with streaming data, messaging systems, big data engineers dealing with distributed systems, and many others. In this article, I will focus on those who work in BI teams.&lt;/p&gt;

&lt;p&gt;A data engineer (in BI teams) is the one who equips BI analysts/data analysts, data scientists, ML models, data products, managers, and the entire company with reliable data. They achieve this by using tools for large-scale data processing, creating, managing, and monitoring routines. They develop tooling such as APIs and applications that abstract user activities, making it easier for all departments to leverage the data and ensuring transparent and auditable processes. They employ techniques like data modeling, following all normalization rules, and utilize current tools to develop scalable, performant data warehouses, data lakes, and data lakehouses that use minimal storage and processing resources.&lt;/p&gt;

&lt;h1&gt;
  
  
  Dated Processes
&lt;/h1&gt;

&lt;p&gt;In the vast majority of companies, data analysis is carried out using Excel and Google Sheets. Typically, it's a repetitive task that consumes one's time, which could be spent on other tasks. Moreover, it has various weaknesses, such as the lack of visualization with charts, making it difficult for individuals to grasp the magnitude of data and make more informed decisions quickly. Given that Excel is prone to human errors, using it as the primary method for data analysis is a significant disadvantage.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm not against companies using Excel for their analysis; I'm against companies that have valuable data, which could be used as a growth pillar, but still treat it as a mere consequence of events.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  When do I know I need a data engineer?
&lt;/h1&gt;

&lt;p&gt;Speaking from my experience, it became evident that we needed one or more data engineers when the BI team's constructions (metrics tracking routines) started competing with production in terms of processing power, leading to exponential resource consumption. The routines we created started affecting overall sales processing, negatively impacting the user experience in our e-commerce platform. Additionally, we were faced with the daunting task of managing the costs of machines, processing, and storage, which were incurred without proper planning.&lt;/p&gt;

&lt;p&gt;To address these issues, we decided to separate the production environments from the analytics team almost entirely. We improved storage by adopting datalakehouse principles and compressing files, which significantly reduced our space requirements. By using incremental data updates instead of full processing of all data, we eliminated processing bottlenecks and improved delivery speed for analytics and data science teams. With many abstractions in place, the processes became transparent, and most team members understood how KPIs were calculated. This transparency encouraged the company to become even more data-driven.&lt;/p&gt;

&lt;p&gt;In general, you will want a data engineer when your operation starts to grow and the data-driven culture is well-established, or when you want to turn your data into a product, commercialize it, create data products from it, or simply need better performance and cost-effectiveness. A data engineer can address many of these challenges and provide guidance on optimizing the utilization of your resources for data processing, handling, storage, and management. They can help you make the most out of the data your company possesses.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In an ideal world, the data engineer is the pioneer of the entire data movement, but in reality, this role is relatively new in the market and might not always be the case.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Architecture Planning
&lt;/h1&gt;

&lt;p&gt;As companies, teams, and operations grow, it's natural for these outdated processes to fall behind, and a BI team starts to be structured (or at least it should). At this stage, we enter the realm of data engineering, starting with studying the type of architecture to be used. Will we implement a data warehouse, or perhaps a data lakehouse? Will we use a cloud or an on-premise solution? What does our budget allow us to develop? Which tools will we use for daily KPI monitoring, PowerBI, Tableau, or another option? All of these questions, among others, are answered in collaboration with other departments, taking into account the company's current state, historical and cultural context, and the skills possessed by the people directly involved. These are some of the "obvious" variables that must be considered in the planning process of a data engineering center of excellence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development and Tooling
&lt;/h2&gt;

&lt;p&gt;Once these pertinent questions are answered, the subsequent stages should be solid, metric-driven, well-documented, and well-architected to ensure reliable ETL/ELT processes. The development of pipelines mainly involves moving information from one system to another. You can perform an ETL (extract-transform-load) directly into a data warehouse on your RDS, or an ELT (extract-load-transform) process on your data lake. You may be aggregating data from your production database or consuming an FTP from a partner company to enrich your database. Developing APIs for other services to consume your transformed data or for data scientists to access it is quite common and not exclusive to back-end developers. The choice of tools can be determined during the development stage, but it generally aligns with common practices. For pipeline orchestration, a strong candidate is Airflow, a Python framework for managing routines. For distributed processing, you have PySpark and Spark at your disposal. For an on-premise data lake, you can use MinIO. For your data warehouse, PostgreSQL with a star schema modeling is a common choice, but if you scale up with many fact tables and numerous dimensions, making star schema impractical, you can opt for snowflake modeling. If you're performing data scraping and want to enrich your data, you can use low-code software like IBM RPA, or if you prefer to continue with Python, you can use scrapy, an excellent framework for web crawling.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I will write more articles in the future about MinIO and data lakes, Airflow and task orchestration, and distributed processing with Spark.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Metadata
&lt;/h1&gt;

&lt;p&gt;When developing data systems, it is essential to have documentation, not just for the code but also for metadata, which is data about data. A pipeline that consumes information from a daily API performs various transformations before storing it alongside data from other systems. But what transformations does it perform and why? In BI teams, datasets are often prepared daily with numerous transformations, aggregations, and abstractions. How are these aggregations done? How is the &lt;code&gt;gross_revenue&lt;/code&gt; column calculated? Why do many columns from the production table not appear in this dataset? These are common questions that analysts and data scientists will ask, highlighting the need for a robust knowledge base with this metadata.&lt;/p&gt;

&lt;h1&gt;
  
  
  Data management
&lt;/h1&gt;

&lt;p&gt;Data management is one of the tasks that data engineers handle, and it shares similarities with the activities performed by DBAs. Applying privacy guidelines to your data, providing appropriate access to the right users, and managing it continuously is a labor-intensive task, despite many DBMSs and S3 storage services having integrated permission controls. Additionally, it is necessary to develop robust logging and metrics systems to monitor the daily health of the data and pipelines, providing reports on routines that ran with errors, ran incompletely, or encountered any other type of inconsistency. The reliability of the data and the margin of error need to be measured and relentlessly communicated. The reliability of the data is often a subject of discussion and is usually linked to external data sources of the company, while the margin of error is due to rounding and updates that may be performed in the production environment, directly affecting OLAP systems.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In conclusion, we have explored what a data engineer does, how they can begin their journey, and when their efforts are needed. It is important to note that these insights are based on my experiences working across the three main data fronts. If you have any remaining questions about the positions within a data team or if you would like to learn more about the skill set required for a data engineer, I encourage you to read my article on the composition of a BI team and the skills typically sought after in this role.&lt;/p&gt;

&lt;p&gt;Thank you very much for reading&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/pedrohgoncalves/data-roles-in-data-teams-and-your-skill-set-using-math-1332"&gt;&lt;em&gt;&lt;strong&gt;Data roles in data teams and your skill set. Using math.&lt;/strong&gt;&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>datascience</category>
      <category>beginners</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Unsupervised Clustering with K-Means</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Mon, 26 Jun 2023 01:30:43 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/unsupervised-clustering-with-k-means-5h26</link>
      <guid>https://dev.to/pedrohgoncalves/unsupervised-clustering-with-k-means-5h26</guid>
      <description>&lt;p&gt;In the past few weeks, I have been studying about clustering and some of its models to apply in a project at the company I work for. When you study clustering, you quickly come across the centroid model, which leads you to K-Means, the most famous method for working with this type of clustering. We will use K-Means for our activity.&lt;/p&gt;

&lt;p&gt;Speaking a bit about how we will perform our activity, we will use a dataset containing sales information from an unspecified company, and we will cluster its customers taking into account some of their behaviors in the store. For this purpose, we will also utilize the RFM concept (recency, frequency, monetary), which is widely used in marketing teams.&lt;/p&gt;

&lt;p&gt;It's important to remember that there are various other types of clustering and centroid-based clustering algorithms. This article specifically focuses on K-Means and a practical application of its algorithm.&lt;/p&gt;

&lt;p&gt;In this article, we will not discuss data transformation or data visualization. If you would like to provide feedback on my code, you can visit the repository where the code used in this article is located, as there are some visualization and transformation aspects not shown here.&lt;/p&gt;

&lt;h2&gt;
  
  
  K-Means
&lt;/h2&gt;

&lt;p&gt;K-Means is an unsupervised algorithm, which means it does not require "labels" on the events, unlike supervised algorithms that need labels for training. Unsupervised algorithms are designed to learn from the data itself by autonomously identifying patterns (often not visible to the naked eye).&lt;/p&gt;

&lt;p&gt;The goal of the algorithm is to generate K clusters (where K is defined by the scientist), reducing the variance between clusters and increasing the similarity among points assigned to the same cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The algorithm randomly assigns K numbers in the feature space.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The distance is calculated by iterating over each point and each centroid, and the point is assigned to the centroid with the shortest distance (the calculation of distance uses the Euclidean distance formula).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fbfhygmkgedlr5wmxzum9.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%2Fbfhygmkgedlr5wmxzum9.png" alt="Image description" width="385" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Recalculate the position of the clusters based on the mean coordinates of each point assigned to the same cluster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Repeat steps 2 and 3 until the position of the clusters no longer undergoes significant changes or until a certain number of iterations is reached.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Determining the ideal number of K
&lt;/h2&gt;

&lt;p&gt;To determine the number of K (clusters), we will use the elbow method, which is the most commonly used method for this task. We will also use the distance point-line calculation to further refine and better define our number of clusters.&lt;/p&gt;

&lt;p&gt;The elbow method calculates the sum of squared distances between the points within a cluster. Its goal is to minimize the total inertia (variability of the points) of each cluster. The formula for this calculation is as follows:&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%2Fwzjvvomv9dxc3u2z8i7p.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%2Fwzjvvomv9dxc3u2z8i7p.png" alt="Image description" width="250" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where K is the number of clusters, x is the point within each cluster, and μ is the mean distance between each point.&lt;/p&gt;

&lt;p&gt;The distance point-line calculation is the perpendicular distance of each point along a line defined by two points. It is used to discover the greatest homogeneity within a cluster and the greatest difference between clusters.&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%2F5xit2p05567yevsv1v3c.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%2F5xit2p05567yevsv1v3c.png" alt="Image description" width="659" height="80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;P0 and P1 are our starting point (P0) and our last point (P1). y1 represents the y-value (in a Cartesian plane) at our last point (P1), and the same applies to y0 for the first point. The same logic applies to x0 and x1. In the equation, as we usually iterate for each number of clusters, x and y represent the x and y values of the cluster being calculated.&lt;/p&gt;

&lt;p&gt;We will start by defining two functions. &lt;code&gt;calculateWcss&lt;/code&gt; iterates from 1 to 9 clusters (we don't want to have too many customer clusters in our dataset, and this is generally determined and tested with the data and business teams). It calculates the total inertia for each number of clusters and returns a list with the cluster number and its inertia.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculateWcss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  
    &lt;span class="n"&gt;wcss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  
        &lt;span class="n"&gt;kmeans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KMeans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clusters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;labels_&lt;/span&gt;  
        &lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inertia_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wcss&lt;/span&gt;  

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plotFigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quadraticSum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;  
    &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quadraticSum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clusters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  

&lt;span class="n"&gt;dfRecencyModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dfOrderCostumer&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;  
&lt;span class="n"&gt;quadraticSum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateWcss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dfRecencyModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="nf"&gt;plotFigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quadraticSum&lt;/span&gt;&lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling the calculateWcss function using the column in the dataset that represents the number of days since the last purchase and plotting it in the plotFigure function, we get the following result:&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%2Fmpzet6yq6kztbeo371at.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%2Fmpzet6yq6kztbeo371at.png" alt="Image description" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interpreting this graph, we might think, "&lt;b&gt;Well, the number 8 of clusters is the best because it has the lowest inertia.&lt;/b&gt;" It's not entirely incorrect, but not entirely correct either. As mentioned earlier, we don't want too many clusters, so we're looking for the point where the inertia doesn't decrease drastically, always aiming to have the fewest clusters.&lt;/p&gt;

&lt;p&gt;Upon reevaluation, we could say that 2 and 3 are strong candidates. However, we will use the distance point-line calculation to ensure the number of clusters we will apply.&lt;/p&gt;

&lt;p&gt;Let's define the &lt;code&gt;distancePointLine&lt;/code&gt; function in code. It calculates the distance of the number of clusters to the points P0 and P1, which are 1 and 9 (our number of clusters defined in &lt;code&gt;calculateWcss&lt;/code&gt;). It returns the ideal number of clusters where we have the greatest perpendicular distance between the starting point and the ending point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;distancePointLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;  
    &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  
    &lt;span class="n"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  

    &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;  
        &lt;span class="n"&gt;x0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;  
        &lt;span class="n"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wcss&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  
        &lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;y1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="n"&gt;denominator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numerator&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;denominator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling the function with the return of calculateWcss we have the value 4 as the ideal number of clusters and we will use it in the rest of the tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clustering our dataset
&lt;/h2&gt;

&lt;p&gt;In our dataset, we have information such as recency (which we used to determine the ideal number of clusters and represents the number of days since the last purchase), frequency (which represents the number of times a particular customer has made purchases in our store), and monetary value (representing the amount the customer spent in our store). Typically, people would cluster using all the features (columns) together. However, we will perform separate clustering for each feature, specifically four clusters for each feature.&lt;/p&gt;

&lt;p&gt;Let's start by defining a function that takes parameters such as a new column name for the cluster, the name of the feature to be used as the basis for clustering, the multidimensional array of the separated feature from the DataFrame, the DataFrame itself to add the clustering, and whether the rating (cluster it belongs to) should be in ascending or descending order.&lt;/p&gt;

&lt;p&gt;We will use the cluster as the rating. As the cluster starts from 0 and goes up to 3, the cluster with a rating of 0 will represent customers who have spent the least money or have been inactive for the longest time on the platform.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;orderCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;target_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;featureColumn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dfAppend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  
    &lt;span class="n"&gt;kmeans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KMeans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;nmrCluster&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  

    &lt;span class="n"&gt;dfUse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dfAppend&lt;/span&gt;  
    &lt;span class="n"&gt;dfUse&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit_predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;featureColumn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  

    &lt;span class="n"&gt;groupbyCluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dfUse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;target_name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
    &lt;span class="n"&gt;groupbyCluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupbyCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;groupbyCluster&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupbyCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;  
    &lt;span class="n"&gt;groupbyCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target_name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;dfUsageMerged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dfUse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;groupbyCluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;dfUsageMerged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="n"&gt;dfUsageMerged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dfUsageMerged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we will call the orderCluster function for each feature and increment by in the dfMain (DataFrame that we performed some transformations after reading the .csv file)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;finalDataframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dfMain&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id_unique_costumer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;order_approved&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frequency_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;agg_value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;revenue_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;  
&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pontuation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frequency_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;revenue_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;segmentation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Inactive&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pontuation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;segmentation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Business&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  
&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pontuation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;segmentation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Master&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  
&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pontuation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;segmentation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Premium&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, we can plot a graph to visualize the distribution of each segmentation, using the features of agg_value (amount of money spent) and recency (number of days since the last purchase) as well.&lt;/p&gt;

&lt;p&gt;Here's the function to plot the graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plot_segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  
    &lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;palette&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;muted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;color_codes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;whitegrid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    
    &lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatterplot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;hue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;segmentation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="n"&gt;size_order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Premium&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Master&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Business&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Inativo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  
    &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;plot_segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;agg_value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plotted graph:&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%2Fnrbqniso7oblszqlrrfc.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%2Fnrbqniso7oblszqlrrfc.png" alt="Image description" width="581" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this graph, it becomes clear that our customers classified as Premium (obviously few) have spent higher amounts than the average and made more recent purchases, while the inactive ones have not spent much and haven't made purchases for some time. Based on this, our company can have more targeted communication by offering customized services to Premium customers and providing some type of discount coupon to encourage inactive customers to return and spend in our store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Digging deeper into RFM.
&lt;/h2&gt;

&lt;p&gt;Let's further analyze our recency cluster with the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;finalDataframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency_cluster&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ftdgjnmjosa6psdfw3oe8.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%2Ftdgjnmjosa6psdfw3oe8.png" alt="Image description" width="800" height="115"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we know that customers belonging to cluster 0 have an average of 490 days since their last purchase, making them the cluster with the lowest recency.&lt;/p&gt;

&lt;p&gt;The RFM concept creates more customer groups based on the clusters they belong to, taking into account the attributes of recency, frequency, and monetary value. For example, a customer who belongs to cluster 0 in recency, cluster 1 in frequency, and cluster 3 in monetary value means that they haven't made a recent purchase, have made a reasonable number of purchases in our store, and have spent a high amount of money. The RFM analysis would allocate this customer to a "dormant" or "hibernating" customer segment. We can implement this classification in our algorithm, but I present it here as a challenge. I recommend reading more about RFM and how to implement it in your business alongside unsupervised clustering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion.
&lt;/h2&gt;

&lt;p&gt;In this article, we have learned how to determine the ideal number of clusters and how to cluster our customers based on the widely used RFM concept. If you would like to explore more about other models, data visualization, data transformation, I suggest checking out my GitHub repository, where I frequently work on data engineering projects and related topics.&lt;/p&gt;

&lt;p&gt;Thank you very much for reading.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/pedrohgoncalvess/k-means-clustering" rel="noopener noreferrer"&gt;https://github.com/pedrohgoncalvess/k-means-clustering&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>datascience</category>
      <category>machinelearning</category>
      <category>kmeans</category>
    </item>
    <item>
      <title>Data roles in data teams and your skill set. Using math</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Mon, 12 Jun 2023 22:08:08 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/data-roles-in-data-teams-and-your-skill-set-using-math-1332</link>
      <guid>https://dev.to/pedrohgoncalves/data-roles-in-data-teams-and-your-skill-set-using-math-1332</guid>
      <description>&lt;h1&gt;Data roles and skill set&lt;/h1&gt;

&lt;p&gt;
With GPT, some data positions have become quite famous, especially data scientists, but there are others that deserve the same attention. In this article I will show what they are, what are the main activities they develop and the set of skills they exercise.
It is open for discussion, I'll try to keep it updated as I change my opinions (which I certainly will).
&lt;/p&gt;

&lt;h2&gt;
About the ""method"" I chose.
&lt;/h2&gt;

&lt;p&gt;
I listed the most used abilities in the data area and compressed them until they were in common use between at least 2 positions. 
It is important to point out that I did not focus only on hard skills and that soft skills are just as important.
I gave grades from 1 to 10 (based on my experience) according to the use and the necessary level of knowledge for that skill.
&lt;/p&gt;




&lt;h2&gt;Skill explanations&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Applied math.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
Applied mathematics is the area of mathematics that looks for ways to solve problems in companies especially in areas of finance, expansion, logistics and marketing, it is widely used (mandatory skill for people who deal with data) in data-oriented companies.
The grades increase as the position needs to be closer to business-related indicators such as revenue, operating costs, CHURN, retention, etc.
&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Advanced math.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
Firstly, with advanced mathematics focused on data I mean the use of some areas such as calculus, probability, advanced statistics, linear regression among many other areas, it is distinguished from applied mathematics because, unlike it, advanced mathematics does not pass by """lots of changes""" in functions due to business rules, as opposed to applied math which is basically modeled by business rules.
&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SQL&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
Although it is quite obvious that functions around data must have knowledge in SQL, this knowledge does not have the same levels in different positions. SQL subsets are responsible for this great differentiation of levels since many of the positions do not necessarily need to know DCL, DDL, DML commands.
The levels serve precisely to separate the subsets, following this line of reasoning I created a "straight line of wisdom".
&lt;/p&gt;

&lt;p&gt;&lt;b&gt;DQL &amp;gt; DTL &amp;gt; DQL ADVANCED &amp;gt; DML &amp;gt; DDL &amp;gt; DCL&lt;/b&gt;&lt;br&gt;
&lt;/p&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Programming&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
Programming is very present in the data areas, whether in languages such as python, scala, pl/sql for creating routines and pipelines or M, DAX for manipulating data in specific software.
The increase in grades in the various areas has to do with how much code the job requires you to write thoughtfully
along with the languages and the framework that you will use in your day to day.
&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Business&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
With business I took the liberty to add some soft skills like communication and general market knowledge in the (imaginary) equation.
Knowledge about the business in which the company you provide your services is basic for any position, be it development, commercial, marketing, etc., but how much business do you need to understand to be able to act minimally in the positions? I didn't use any mathematical formula or quantitative driver to define this, just my experience and knowledge in BI functions.
&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Infra&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
The infra skill refers to knowledge of deployment, monitoring pipe lines, identifying bottlenecks and needs, it is basically focused on software and frameworks such as docker, terraform, cloud architecture, metadata, among others, it is very similar to the set of devops practices.
Particularly it is a skill 8 or 80.
&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;No/low code softwares&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;
Some positions require you to use some no/low code software for developing dashboards, pipeline, documentation, deployment, etc. The different levels are related to how much knowledge you should have about these tools and how much you will use them in your day-to-day life.
&lt;/p&gt;


&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ponderation&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
&lt;b&gt;The weight of the skill is greater in relation to the difficulty in learning its basics.&lt;/b&gt;

Weighting is an attempt to say how complex a position is in relation to the activities it performs and which are listed in the method. the account is

((Applied math * 1) + (Advanced math * 4) + (SQL * 2) + (Programming * 3) + (Business * 1) + (Infra * 3) + (No/low code * 1) / 15)

The weights of the weights were based on experience learning certain things related to skills and on informal surveys of some developers and data professionals.

The higher the grade, the greater the difficulty. &lt;strong&gt;DON'T&lt;/strong&gt; take it as true.
&lt;/p&gt;





&lt;h2&gt;Positions&lt;/h2&gt;
_&lt;h3&gt;Data analytics.&lt;/h3&gt;
&lt;p&gt;
The data analyst works in the opposite direction of the data scientist, focusing on talking about how and why things happened rather than making hypotheses about what might happen.
&lt;/p&gt;

&lt;p&gt;
This position usually works by participating more actively in meetings and creating dashboards so that the company's activity points can consume or that it can help in decision making, either making the analyzes more accurate or delivering the data as chewed up as possible using a lot of visualization like tools.
It gives good points to no/low code software because knowledge of tools like power bi, tableau, excel, pentaho, etc.
they are in high demand for vacancies and are allocated good working hours. Applied mathematics and business follow the same path, generating insights with data and extracting information that generates value for the business requires knowledge about them.
&lt;/p&gt;

&lt;p&gt;
For SQL and advanced math you don't need in-depth knowledge, you often won't need to elaborate complex queries or do a linear regression calculation.
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;h4&gt;Data analytics skills set points.&lt;/h4&gt;&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%2F25grqrmo1uz4yec5pcu8.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%2F25grqrmo1uz4yec5pcu8.png" alt="data-analytics" width="611" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;_&lt;/p&gt;
&lt;h3&gt;Data engineer.&lt;/h3&gt;

&lt;p&gt;
Quoting the good article by Maxime Beauchemin, former data engineer at Facebook now Meta, data engineers are much closer to their older brother Software Engineer than to their younger brother Data Scientist that's why instead of creating machine learning models, deep learning, data visualization or anything related, first of all a data engineer he creates the necessary tooling to abstract as much as possible from technical activities related to modeling, mining and manipulation of data, creating pipelines, data lakes and data warehouses.
&lt;/p&gt;

&lt;p&gt;
Data engineers are responsible for database modeling, mining and data manipulation, this explains why I gave SQL a 10 in the skill along with 10 in programming like routines in Python with Airflow or creating pipelines in Scala due to the large amount of data. 
In the day to day of a data engineer at IDEAL, reports or visualizations are not created for use on commercial fronts, nor machine learning or deep learning models, which is why the low grade in applied and advanced mathematics. The high score on infra is due to the fact that data engineers are very responsible for deployment environments, therefore knowledge in docker, terraform, cloud, etc.
they are in great demand in vacancies and occupy good hours of work.
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;h4&gt;Data engineers skills set points.&lt;/h4&gt;&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%2Fqldddo3p2jqaezebm32m.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%2Fqldddo3p2jqaezebm32m.png" alt="data-eng" width="606" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;_&lt;/p&gt;
&lt;h3&gt;Data ops.&lt;/h3&gt;

&lt;p&gt;
Yes, I know that data ops is not a person occupying a vacancy, but a culture equal to devops, but there was a materialization of the culture in which a person was leading the fronts for data ops, as happened in devops, in fact, other positions were born derivatives of dataops like ml ops which is basically data ops focused only on machine learning, so I think it's fair to materialize the culture in this text.&lt;/p&gt;
&lt;p&gt;
A dataops mainly takes care of monitoring pipelines, studying business needs, new possibilities and improvements. He ensures data security, reliability and quality, but he also participates in budget meetings and architecture definitions, abstracting that responsibility from data engineers, so obviously he gives full marks in infrastructure and also in business. The reason for the high score in programming is due to the configuration of environments and knowledge in the most diverse software aimed at deployment and the software life cycle. Even though it seems counterintuitive to ensure data quality and I didn't give high marks in SQL, I don't think advanced knowledge of SQL is needed to do this. The grade in Advanced Mathematics is explanatory and the reasonable grade in Applied Mathematics is due to the participation in financial issues that are related to the operational costs of keeping the applications available and with a satisfactory level of use.
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;h4&gt;Data ops skills set points.&lt;/h4&gt;&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%2F63tfwrrz6851g6wzd1fw.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%2F63tfwrrz6851g6wzd1fw.png" alt="data-ops" width="603" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;_&lt;/p&gt;
&lt;h3&gt;Data scientist.&lt;/h3&gt;

&lt;p&gt;
Data science is probably the most famous position on data in recent times. Her reputation is equally equivalent to the difficulty of becoming one, because the activities that a data scientist performs are extremely complex, even with the tools abstracting most of the things that are more complex, such as complex calculations, collecting and cleaning data.
&lt;/p&gt;

&lt;p&gt;
The high marks in both math skills is self explanatory, you need to know both very well to develop artificial intelligence models that make sense for your business, this goes directly with business knowledge which is also needed but not at a very high level. In the ideal world, the data scientist is abstracted from functions that involve infra and even the collection and cleaning of data, a minimum knowledge is enough. With the popularization of AI, several no/low code software are emerging that abstract most of the complex tasks in the day-to-day of a data scientist, but knowledge in programming is still necessary, mainly in languages ​​such as R and Python and their frameworks which are references in the area and therefore the medium score in programming.
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;h4&gt;Data scientist skills set points.&lt;/h4&gt;&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%2Fanphxlc0qgu4ecckrakd.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%2Fanphxlc0qgu4ecckrakd.png" alt="data-sci" width="606" height="593"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;br&gt;


&lt;h3&gt; Some considerations &lt;/h3&gt;

&lt;p&gt;
Perhaps you have already heard of some other functions such as ML Engineer, BI Specialist or others, these functions exist but they are consistent with the business model in which it is mentioned, they are usually ramifications of the 4 mentioned above
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Repository with csv and code for images: &lt;a href="https://github.com/pedrohgoncalvess/dataroles-skill-set" rel="noopener noreferrer"&gt;https://github.com/pedrohgoncalvess/dataroles-skill-set&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you very much for reading&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>beginners</category>
      <category>management</category>
      <category>data</category>
    </item>
    <item>
      <title>A little about the Scala Language</title>
      <dc:creator>Pedro H Goncalves</dc:creator>
      <pubDate>Sat, 10 Jun 2023 18:03:20 +0000</pubDate>
      <link>https://dev.to/pedrohgoncalves/a-little-about-the-scala-language-5cm0</link>
      <guid>https://dev.to/pedrohgoncalves/a-little-about-the-scala-language-5cm0</guid>
      <description>&lt;p&gt;&lt;strong&gt;What is Scala?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scala is a compiled multiparadigm language that runs on the JVM. It was inspired by Java, Erlang, and Haskell. It has a static type system with first-class type inference, making it one of the "modern" languages with a sophisticated type system. By saying that Scala is multiparadigm, we emphasize that the language embraces different programming styles. Unlike other languages that tend to favor a specific paradigm, Scala gives developers the freedom to choose the programming style that best suits the problem at hand. Additionally, the language is committed to code conciseness and expressiveness. Surprisingly, Scala manages to deliver all these features. In this article, I will explain how.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional Programming in Scala&lt;/strong&gt;&lt;br&gt;
Scala has many functional programming features, such as first-class functions. First-class functions allow functions to be treated as values, assignable to variables, returned by other functions, and passed as parameters. This provides a high level of abstraction and modularity, especially for data manipulation. Functional programming in a static type system can be challenging, but Scala offers features that facilitate this task. Additionally, Scala supports lambda expressions, allowing the creation of anonymous functions in a concise and expressive way. Immutability is encouraged through the val and var keywords for declaring immutable and mutable variables, respectively. Immutability is useful for asynchronous and concurrent programming, topics that we will address later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object-Oriented Programming and JVM Interoperability&lt;/strong&gt;&lt;br&gt;
As mentioned earlier, Scala runs on the JVM and has seamless integration with Java as one of its main philosophies. It is possible to use Java libraries in Scala and vice versa. Interoperability between the two languages is simpler when it comes to basic data structures but can become more complex when dealing with more advanced data structures due to subtle differences in Java and Scala syntax. Scala incorporates important concepts of object-oriented programming, such as class and object creation, encapsulation, and useful abstractions like abstract, case, and final class, which simplify the declaration of operations. Inheritance and polymorphism are also supported by the language, allowing for more flexible manipulation of types according to the application context.&lt;/p&gt;

&lt;p&gt;All these data manipulation and new type creation features allow developers, even with a static type system, to have the freedom to define the desired behavior for their solution, avoiding type errors and facilitating the creation of comprehensive tests. Additionally, first-class type inference reduces the need for explicit type declarations, giving the language an appearance of dynamic typing while maintaining the safety of the static type system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;br&gt;
Building upon what was mentioned earlier, the concept of immutability present in the language enables concurrent programming using multiple threads, resulting in better performance and the construction of more robust and efficient solutions. Alongside immutability and concurrent programming, Scala offers support for asynchronous tasks that execute work differently but leverage many of the benefits of concurrent programming. Furthermore, Scala is known as the foundation of two famous frameworks: Spark and Akka.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Spark&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
Spark is widely used for distributed processing and data analysis. It has APIs in multiple languages such as Python, R, and Java, and is widely adopted in both academic and commercial environments. The Spark MLlib library is dedicated to machine learning and data visualization.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Akka&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
Akka is a library used for creating distributed and concurrent systems. It implements the actor model, an abstraction for concurrent programming based on message passing. The Akka ecosystem includes Akka HTTP for web application development, Akka Streams for stream processing, and Akka Cluster for handling actor clustering in distributed environments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Web Development&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
For web development in Scala, there are two popular options. The first one is the Play Framework, which follows the MVC (Model-View-Controller) pattern and is similar to other popular frameworks such as Rails, Django, and Laravel. The Play Framework provides development abstractions that make web application creation quite enjoyable. The second option is Akka HTTP, which offers greater scalability but requires knowledge of actors and asynchronous message passing. Although it may require a bit more experience, Akka HTTP is powerful and flexible. Additionally, there is Slick, which is a commonly used ORM/FRM in Scala web development. Slick provides efficient abstractions over JDBC, including support for result streaming. While Slick doesn't have built-in object migrations, it can be combined with tools like Flyway, which integrates well with Slick and the Play Framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;&lt;br&gt;
Initially, Scala was developed with an academic focus, and this emphasis can be observed in some parts of the documentation, where complex concepts are presented without proper prior explanation. An example of this is the creation of the Cats library, which provides abstractions of the functional paradigm to fill some gaps in the official documentation. This approach ended up alienating some people who preferred to start their studies in another language. Additionally, the Scala community has faced some issues that negatively affected the perception of the language. Controversies involving creators and maintainers of famous libraries are an example of this. We can also attribute some blame to the creation of Scala 3, which became a "distraction" since Scala 2 still receives constant updates and many people are not focused or at least interested in making a (often laborious) migration from Scala 2 to Scala 3. The already "small" community around the language, which has experienced reductions, has become "elitist," where only big tech companies (LinkedIn, Netflix, Twitter, Airbnb, Spotify) use its technologies since skilled professionals and experienced architects of distributed systems (its main application) have become increasingly expensive. As a consequence of the "elitization" of the language, job opportunities have been consistently scarce, which doesn't usually attract new learners. All these events have led to the loss of popularity of the language, but it will undoubtedly continue to be widely used in commercial settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Despite these weaknesses, Scala remains a powerful and widely used language in many environments. Its combination of object-oriented and functional programming, interoperability with Java, and scalability features make it a solid choice for a variety of applications.&lt;/p&gt;

&lt;p&gt;After discussing Scala and its (dis)success to some extent, I encourage everyone to write a few lines in the language. Despite criticism of the documentation, there is a significant amount of introductory content available on the internet, and I recommend the Rock The JVM channel for those interested in the language.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you very much for reading.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>scala</category>
      <category>distributedsystems</category>
      <category>beginners</category>
      <category>scalability</category>
    </item>
  </channel>
</rss>
