<?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>Forem: Mateus Rauli </title>
    <description>The latest articles on Forem by Mateus Rauli  (@mateus-rauli).</description>
    <link>https://forem.com/mateus-rauli</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%2F864333%2F22f9e2a3-8a12-40a2-b2b9-b31cfb887ac5.png</url>
      <title>Forem: Mateus Rauli </title>
      <link>https://forem.com/mateus-rauli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mateus-rauli"/>
    <language>en</language>
    <item>
      <title>How fsync and synchronous_commit Affect PostgreSQL Performance</title>
      <dc:creator>Mateus Rauli </dc:creator>
      <pubDate>Mon, 05 May 2025 19:19:57 +0000</pubDate>
      <link>https://forem.com/mateus-rauli/how-fsync-and-synchronouscommit-affect-postgresql-performance-22di</link>
      <guid>https://forem.com/mateus-rauli/how-fsync-and-synchronouscommit-affect-postgresql-performance-22di</guid>
      <description>&lt;p&gt;While diving into PostgreSQL's configuration options, two settings caught my attention: &lt;code&gt;fsync&lt;/code&gt; and &lt;code&gt;synchronous_commit&lt;/code&gt;. At first glance, they might seem like minor toggles, but a closer look reveals their critical role in balancing database performance and data durability. Through research and experimentation, I discovered how these settings can significantly affect not just the performance of your PostgreSQL instance, but also its ability to recover from failures. In this post, I’ll guide you through the inner workings of these settings, their trade-offs, and how to make informed decisions based on your use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is fsync
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;fsync&lt;/code&gt; parameter in PostgreSQL determines whether all changes to the database are physically written to the disk before a transaction is confirmed. When &lt;code&gt;fsync&lt;/code&gt;is enabled, PostgreSQL issues &lt;code&gt;fsync()&lt;/code&gt; system calls (or equivalent methods) to ensure that data is securely flushed to disk. This guarantees that the database cluster can recover to a consistent state in the event of an operating system or hardware crash.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;fsync&lt;/code&gt; is disabled, PostgreSQL skips this step, which can dramatically improve performance but introduces significant risks. In the event of a crash or unexpected shutdown, data that was not yet written to disk can be lost or corrupted. Worse, because both the Write-Ahead Log (WAL) and checkpoints are critical for maintaining the consistency of the entire cluster, a crash with &lt;code&gt;fsync&lt;/code&gt; disabled could render the &lt;strong&gt;entire cluster unusable&lt;/strong&gt;, requiring a full restore from backups.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is synchronous_commit
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;synchronous_commit&lt;/code&gt; determines how PostgreSQL handles transaction commits in relation to the WAL. When enabled, PostgreSQL ensures that the WAL records for a transaction are flushed to durable storage before the transaction is reported as committed. This provides a high level of data safety, ensuring that committed transactions are not lost even in the case of a crash.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;synchronous_commit&lt;/code&gt; is disabled, PostgreSQL does not wait for the WAL to be flushed to disk. Instead, it relies on the operating system to handle the flushing asynchronously. This can significantly improve transaction throughput but comes with a trade-off: in the event of a crash, some transactions reported as committed might not actually be saved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing performance with fsync on/off
&lt;/h3&gt;

&lt;p&gt;To benchmark the performance impact, I utilized &lt;a href="https://www.postgresql.org/docs/current/pgbench.html" rel="noopener noreferrer"&gt;pgbench &lt;/a&gt; with 10 connections and 2 threads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pgbench -c 10 -j 2 -T 30&lt;/code&gt;&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%2Fmljy3diybh61f9u8ylzj.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%2Fmljy3diybh61f9u8ylzj.png" alt="fsync benchmark" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With fsync disabled, we observed a performance improvement of approximately 58% in TPS and a latency reduction of about 37% when compared to having it enabled. &lt;/p&gt;

&lt;h3&gt;
  
  
  Testing performance with synchronous_commit on/off
&lt;/h3&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%2Fno212q38lgk0uazwin7i.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%2Fno212q38lgk0uazwin7i.png" alt="synchronous_commit benchmark" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With synchronous_commit disabled, we observed a performance improvement of approximately 3.5% in TPS and a latency reduction of about 3.4% when compared to having it enabled. When both fsync and synchronous_commit were disabled, there was an additional performance boost, resulting in a 10.7% improvement in TPS and a 6.5% reduction in latency compared to having only synchronous_commit disabled.&lt;/p&gt;

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

&lt;p&gt;These results highlight the need to carefully evaluate the trade-offs between performance and data safety based on the application requirements. For production environments, it's generally recommended to keep fsync enabled and consider disabling synchronous_commit only if the risk of losing recent transactions is acceptable. &lt;br&gt;
However, for high-throughput applications where performance is critical and occasional data loss is tolerable, such as analytics pipelines, caching layers, or systems with external redundancy, relaxing durability guarantees can lead to significant throughput improvements. Ultimately, tuning these settings should be guided by thorough benchmarking and a deep understanding of your workload's tolerance for failure.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>performance</category>
      <category>postgressql</category>
      <category>database</category>
    </item>
    <item>
      <title>How TOAST and Tombstones Work in PostgreSQL</title>
      <dc:creator>Mateus Rauli </dc:creator>
      <pubDate>Mon, 14 Apr 2025 19:31:03 +0000</pubDate>
      <link>https://forem.com/mateus-rauli/how-toast-and-tombstones-work-in-postgresql-1cg6</link>
      <guid>https://forem.com/mateus-rauli/how-toast-and-tombstones-work-in-postgresql-1cg6</guid>
      <description>&lt;p&gt;In relational databases like PostgreSQL, seemingly simple operations — such as updating JSON field — can hide significant complexity. Behind the scenes, mechanisms like &lt;strong&gt;TOAST&lt;/strong&gt; and &lt;strong&gt;tombstones&lt;/strong&gt; come into play, impacting performance, disk usage and even query efficiency. &lt;/p&gt;

&lt;p&gt;Depending on how data is structured, even a tiny modification can trigger disk rewrites, compression processes or silent fragmentation. In this article we'll explore: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What TOAST is&lt;/strong&gt;, when it activates, and why it matters for large data types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The role of tombstones&lt;/strong&gt; and how they affect read/write operations&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;practical benchmark&lt;/strong&gt;, exposing how tiny updates can cause surprising overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you work with types like text[], jsonb, or dynamically growing data, understading these concepts is key to avoiding decisions that might compromise your database's scalability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TOAST is
&lt;/h2&gt;

&lt;p&gt;TOAST (The Oversized-Attribute Storage Technique) is PostgreSQL's clever solution for handling large data values that exceed the database's default page size (commonly 8 kB). When a column's data — like a lengthy text field, a hefty jsonb object or an array — would otherwise bloat a table row and degrade performance, TOAST steps in. It automatically compresses, slices, or even moves the data out-of-line into a secondary storage area, leaving behind only a compact reference in the main table. This optimization keeps frequent operations (like full-table scans) efficient, but it's not free: updates to TOASTed data can introduce overhead, as PostgreSQL may need to rewrite or recompress chunks behind the scenes. &lt;/p&gt;

&lt;p&gt;TOAST targets variable-length or potentially large data types, including: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON/JSONB: Especially when storing deeply nested or verbose documents.&lt;/li&gt;
&lt;li&gt;Text data: text, varchar (if values exceed ~2KB, even with varchar(n)’s length limit).&lt;/li&gt;
&lt;li&gt;Binary data: bytea (e.g., images, files).&lt;/li&gt;
&lt;li&gt;Geometric types: PostgreSQL’s built-in path, polygon, or spatial types like PostGIS geometry/geography.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Toast example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: Create a table with a JSONB column (toastable)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;toast_demo&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="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;small_data&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- Will NOT be toasted&lt;/span&gt;
  &lt;span class="n"&gt;large_data&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;           &lt;span class="c1"&gt;-- Will be toasted&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Insert a small record (no TOAST)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;toast_demo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;large_data&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="s1"&gt;'short text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"key": "small_value"}'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Insert a large JSONB payload (triggers TOAST)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;toast_demo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;small_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;large_data&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="s1"&gt;'short text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s1"&gt;'key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s1"&gt;'nested'&lt;/span&gt;&lt;span class="p"&gt;,&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;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&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;generate_series&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;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
        &lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="c1"&gt;-- This query should return the TOAST table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltoastrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&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;'toast_demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   

&lt;span class="c1"&gt;-- Step 4: Verify TOAST usage&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'toast_demo'&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;main_table_size&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_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pg_toast.pg_toast_32803'&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;toast_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&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;'toast_demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 5: Observe UPDATE overhead&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;toast_demo&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;large_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;large_data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'{"new_key": "value"}'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&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;/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%2Fu8p5d58j03ekl59bqbqn.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%2Fu8p5d58j03ekl59bqbqn.png" alt="Update query plan" width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key observations&lt;/strong&gt;: For a single-row update, the execution time of 26.5ms is slightly slower than typical non-TOASTed updates (which usually take 1-5ms). This overhead occurs because PostgreSQL must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch and decompress the TOASTed large_data value.&lt;/li&gt;
&lt;li&gt;Modify it (appending {"new_key": "value"}).
&lt;/li&gt;
&lt;li&gt;Recompress and potentially relocate the data in the TOAST table.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  When this becomes a problem ?
&lt;/h4&gt;

&lt;p&gt;The 26.5ms latency is acceptable if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates are infrequent (e.g., background jobs rather than user-facing operations).&lt;/li&gt;
&lt;li&gt;The JSONB payload is very large (&amp;gt;10KB) and requires compression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Investigate further if you see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates exceeding &amp;gt;100ms (indicates severe TOAST fragmentation or bloat).&lt;/li&gt;
&lt;li&gt;Concurrent updates causing lock contention (check for blocked queries in pg_stat_activity).&lt;/li&gt;
&lt;li&gt;Autovacuum falling behind on TOAST table maintenance (monitor with pg_stat_user_tables).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The role of Tombstones and their impact on Read/Write Operations
&lt;/h2&gt;

&lt;p&gt;In PostgreSQL, tombstones (often called "dead tuples") are remnants of rows that have been deleted or updated but not yet physically removed from disk. They play crucial role in PostgreSQL's MVCC (Multi-Version Concurrency Control) system, but if left unchecked, they can degrade performance and bloat storage. &lt;/p&gt;

&lt;p&gt;Tombstones are created on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;delete&lt;/strong&gt; - When a row is deleted, it's not immediately erased — instead, it's marked as a "dead" (a tombstone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;update&lt;/strong&gt; - PostgreSQL treats updates as a delete + insert, leaving the old row version as a tombstone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vacuum&lt;/strong&gt; - PostgreSQL's autovacuum daemon (or manual vacuum) eventually cleans up these tombstones, reclaiming space.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How tombstones affect performance
&lt;/h3&gt;

&lt;p&gt;On the read side, they contribute to table bloat by forcing the database to scan through dead rows that remain in heap pages, while index scans must still check MVCC visibility for these obsolete entries, adding CPU overhead. The presence of excessive tombstones can also prevent PostgreSQL from using visibility map optimizations, slowing down sequential scans. For write operations, update-heavy workloads suffer from write amplification as each modification generates new tombstones, requiring addition I/O for the same logical changes. This accumulation poses serious risks, including transaction ID wraparound if VACUUM can't keep pace with tombstone generation, potentially leading to database shutdowns. Storage efficiency takes a hit as well, with dead tuples occupying disk space until vacuumed — sometimes doubling a table's footprint — while leaving behind fragmented pages that degrade storage utilization. These compounding effects make proper tombstone management crucial for maintaining database performance and stability. &lt;/p&gt;

&lt;h3&gt;
  
  
  Tombstone Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: Create a table and disable autovacuum (for demo purposes)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tombstone_demo&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="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="nb"&gt;TEXT&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;tombstone_demo&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;span class="c1"&gt;-- Step 2: Insert initial data&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tombstone_demo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;data&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="s1"&gt;'original_value'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Check dead tuples (should be 0)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&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;'tombstone_demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4: Run 1000 updates (each creates a dead tuple)&lt;/span&gt;
&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;IN&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;1000&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;
    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tombstone_demo&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'updated_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&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="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;span class="c1"&gt;-- Step 5: Verify dead tuples accumulated&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&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;'tombstone_demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 6: Show table bloat&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tombstone_demo'&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;size_with_bloat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 7: Manual VACUUM to clean tombstones&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tombstone_demo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 8: Confirm if dead tuples are gone&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&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;'tombstone_demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when repeatedly updating the same row, PostgreSQL doesn't overwrite the original data. Instead, it creates a new version of the row and marks the previous one as a dead tuple. This means that with every UPDATE, the table accumulates older versions of the data. By disabling autovacuum, we can observe how these dead tuples accumulate without automatic cleanup. The result? The table begins to bloat on disk, even though it logically contains just one row.&lt;/p&gt;

&lt;p&gt;The VACUUM command removes these old versions and makes the space reusable, but doesn't physically reduce the table's size on disk.&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%2F6iwe4qwry0ods0msnf6w.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%2F6iwe4qwry0ods0msnf6w.png" alt="size with bloat" width="744" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VACUUM FULL goes further: it creates a compacted copy of the table, eliminating all empty space and returning it to the operating system. However, this process is blocking (LOCK TABLE) and resource-intensive, as it must completely rewrite the table from scratch. For production systems where downtime isn't an option, the pg_repack extension is the superior alternative. It delivers all the benefits of VACUUM FULL (actual space recovery) without table locking, operating in parallel with normal database operations.&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%2F1sd8her7x5z5g0vgb2kz.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%2F1sd8her7x5z5g0vgb2kz.png" alt="Vacuum full" width="340" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;While TOAST and tombstones are powerful mechanisms that support PostgreSQL’s flexibility and MVCC architecture, they also introduce performance trade-offs — especially in high-write or JSON-heavy applications.&lt;/p&gt;

&lt;p&gt;Monitoring tools like pg_stat_user_tables, using VACUUM VERBOSE, and periodically checking bloat via pgstattuple or pg_repack can go a long way in keeping your database healthy and efficient.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>database</category>
      <category>postgres</category>
      <category>performance</category>
    </item>
    <item>
      <title>POSTGRESQL - ÍNDICE GIN NA PRÁTICA</title>
      <dc:creator>Mateus Rauli </dc:creator>
      <pubDate>Thu, 09 Jan 2025 20:53:44 +0000</pubDate>
      <link>https://forem.com/mateus-rauli/postgresql-indice-gin-na-pratica-2c91</link>
      <guid>https://forem.com/mateus-rauli/postgresql-indice-gin-na-pratica-2c91</guid>
      <description>&lt;p&gt;Hoje vim relatar minha experiência com o índice GIN, que resultou numa melhora muito positiva na performance de algumas das consultas mais lentas do sistema que atuei. &lt;/p&gt;

&lt;h2&gt;
  
  
  Introdução ao GIN INDEX
&lt;/h2&gt;

&lt;p&gt;O GIN (Generalized Inverted iNdex) é um índice designado para lidar com tipos de dados que são subdivisíveis, ou seja, podem ser "divididos" em partes menores, permitindo a pesquisa de valores individuais. É muito utilizado para consultas em dados estruturados e não estruturados como array, jsonb e campos de texto para busca full-text.&lt;/p&gt;

&lt;p&gt;Introduzido na versão 8.2 do PostgreSQL, rapidamente se tornou uma solução indispensável para cenários onde se precisa de buscas rápidas em dados complexos. Diferente do índice padrão B-TREE que é mais adequado para buscas de valores únicos, o GIN é otimizado para casos onde o mesmo valor pode estar associado a vários registros, assim como os valores de um array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como eu utilizei o GIN INDEX
&lt;/h2&gt;

&lt;p&gt;Certo dia, me deparei com um problema crítico no sistema: uma consulta estava causando uma lentidão significativa e prejudicando o desempenho geral da aplicação. Após analisar, percebi que ela fazia uso de uma coluna do tipo ARRAY em seu filtro.&lt;/p&gt;

&lt;p&gt;Consultando a documentação oficial do PostgreSQL, encontrei o índice GIN como uma possível solução. Decidi implementá-lo na coluna problemática para avaliar o impacto. O resultado foi impressionante: o tempo de execução da consulta caiu drasticamente e, com isso, o sistema se tornou visivelmente mais ágil.&lt;/p&gt;

&lt;p&gt;O impacto foi ainda maior porque a coluna onde apliquei o índice GIN era amplamente utilizada em diversas partes do sistema, tornando essa melhoria um divisor de águas para a performance geral. Essa experiência reforçou minha percepção de por que o GIN é frequentemente chamado de "índice mágico".&lt;/p&gt;

&lt;h2&gt;
  
  
  Desvantagens da utilização do GIN Index
&lt;/h2&gt;

&lt;p&gt;Como visto até então, o índice traz sim diversas vantagens quando o assunto é lidar com dados complexos, porém nem tudo é perfeito e ele possui desvantagens que devem ser consideradas e analisadas caso você esteja pensando em implementar ele na sua tabela. &lt;/p&gt;

&lt;p&gt;Uma grande desvantagem são as operações de escrita que se tornam mais custosas já que o índice deve se atualizar para refletir as mudanças dos dados, ou seja, cada operação que altera o dado exige a atualização do índice também, o que é mais custoso em tabelas que possuem alta frequência de escrita.&lt;/p&gt;

&lt;p&gt;Além disso, o GIN consome mais memória do que os outros índices, tanto de armazenamento quanto de consulta, especialmente se a coluna indexada contiver muitos valores únicos ou complexos. &lt;/p&gt;

&lt;h2&gt;
  
  
  Exemplo de criação do índice
&lt;/h2&gt;

&lt;p&gt;Para poder exemplificar o uso do índice, criei uma tabela chamada users contendo apenas id e data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    data JSONB
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para criarmos o índice, basta usar o USING indicando qual o tipo de índice que será utilizado&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE INDEX idx_data_gin ON users USING gin (data);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Pronto! Índice criado, agora ao analisar a consulta será possível notar que ele utilizara o índice criado para buscar a informação.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query plan
"Bitmap Heap Scan on users u  (cost=12.00..16.01 rows=1 width=68) (actual time=0.022..0.023 rows=1 loops=1)"
"  Recheck Cond: (data @&amp;gt; '{""name"": ""Mateus""}'::jsonb)"
"  Heap Blocks: exact=1"
"  -&amp;gt;  Bitmap Index Scan on idx_data_gin  (cost=0.00..12.00 rows=1 width=0) (actual time=0.011..0.011 rows=1 loops=1)"
"        Index Cond: (data @&amp;gt; '{""name"": ""Mateus""}'::jsonb)"
"Planning Time: 0.120 ms"
"Execution Time: 0.061 ms"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note que você sempre verá Bitmap Index Scan no plano de execução pois este é o plano de varredura compatível com o índice GIN. Ou seja, isso não significa que seu índice está sendo usado de maneira ineficiente, apenas que é o comportamento esperado. &lt;/p&gt;

&lt;h2&gt;
  
  
  GIN Multicoluna, uma alternativa para B-TREE x GIN
&lt;/h2&gt;

&lt;p&gt;Em consultas que envolvem colunas com características distintas, pode ser necessário combinar índices B-TREE e GIN. Uma abordagem inicial seria criar índices separados, com um índice B-TREE para colunas que lidam bem com valores únicos e um índice GIN para colunas com dados mais complexos. Embora essa solução funcione, ela pode não ser eficiente em termos de desempenho, pois cada índice é avaliado separadamente.&lt;/p&gt;

&lt;p&gt;Uma alternativa interessante é o uso de índices GIN multicoluna. Com essa abordagem, é possível indexar várias colunas em um único índice, cobrindo diferentes tipos de dados. No entanto, para colunas que não são subdivisíveis (como integer ou timestamp), é necessário habilitar a extensão btree_gin do PostgreSQL. Essa extensão permite que esses tipos sejam indexados de maneira compatível dentro de um índice GIN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE EXTENSION btree_gin;
CREATE INDEX ON records USING gin (data, customer_id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com esses comandos você consegue criar um índice GIN multicoluna.&lt;/p&gt;

&lt;p&gt;Embora seja uma solução menos comum, ela pode ser útil em cenários específicos. No entanto, é importante considerar que índices maiores resultam em maior uso de I/O e custos adicionais em operações de escrita, como inserções e atualizações.&lt;/p&gt;

&lt;h2&gt;
  
  
  Considerações finais
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Com grandes poderes, vem grandes responsabilidades.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Embora o GIN seja extremamente eficiente em cenários específicos, ele não é uma solução universal para todos os problemas relacionados à performance de consultas. Em muitos casos, o tradicional B-TREE continua sendo a escolha mais adequada, especialmente para consultas simples ou quando se busca por valores únicos. É essencial avaliar cuidadosamente o problema em questão para determinar se a inclusão de um índice é realmente necessária e, caso seja, qual o tipo de índice que melhor atenderá às necessidades do sistema. &lt;/p&gt;

&lt;h2&gt;
  
  
  Referências
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/gin.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/gin.html&lt;/a&gt;&lt;br&gt;
&lt;a href="https://pganalyze.com/blog/gin-index" rel="noopener noreferrer"&gt;https://pganalyze.com/blog/gin-index&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>database</category>
      <category>learning</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
