<?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: M Shahid</title>
    <description>The latest articles on DEV Community by M Shahid (@algopro).</description>
    <link>https://dev.to/algopro</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3487582%2F02cc4ad9-6f25-429c-9df3-dddd000fdeb7.png</url>
      <title>DEV Community: M Shahid</title>
      <link>https://dev.to/algopro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/algopro"/>
    <language>en</language>
    <item>
      <title>One Annotation Cut Our SQL Write Amplification by 90%</title>
      <dc:creator>M Shahid</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:20:26 +0000</pubDate>
      <link>https://dev.to/algopro/one-annotation-cut-our-sql-write-amplification-by-90-35jp</link>
      <guid>https://dev.to/algopro/one-annotation-cut-our-sql-write-amplification-by-90-35jp</guid>
      <description>&lt;p&gt;I added one annotation to a Java entity class. Two lines of code. The PR&lt;br&gt;
diff was so small that our reviewer almost skipped it.&lt;/p&gt;

&lt;p&gt;That change cut the bytes written to our SQL transaction log on every UPDATE&lt;br&gt;
by roughly 90%, eliminated unnecessary index maintenance on 17 nonclustered&lt;br&gt;
indexes, and brought our peak database wait times from minutes down to near&lt;br&gt;
zero during the busiest job of the day.&lt;/p&gt;

&lt;p&gt;Here's what happened, why it happened, and how you can check whether the&lt;br&gt;
same thing is quietly hurting your service right now.&lt;/p&gt;


&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;We run a Spring Boot service backed by Azure SQL (SQL Server). One of our&lt;br&gt;
core tables — let's call it &lt;code&gt;MEMBER_PROFILE&lt;/code&gt; — stores member profile and&lt;br&gt;
loyalty data. It has &lt;strong&gt;22 columns&lt;/strong&gt;: ids, names, statuses, timestamps, JSON&lt;br&gt;
blobs, and an optimistic-lock version.&lt;/p&gt;

&lt;p&gt;Every day a scheduled cron job processes metric-update events. During peak&lt;br&gt;
hours this job handles tens of thousands of events per minute. Each event&lt;br&gt;
typically changes &lt;strong&gt;2–3 columns&lt;/strong&gt; on a &lt;code&gt;MEMBER_PROFILE&lt;/code&gt; row — say,&lt;br&gt;
&lt;code&gt;RANK_LEVEL&lt;/code&gt;, &lt;code&gt;NEXT_RANK_LABEL&lt;/code&gt;, and &lt;code&gt;LAST_MODIFIED_AT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The service ran fine for months. Then the user base grew, traffic climbed,&lt;br&gt;
and we started seeing something ugly in our database dashboards.&lt;/p&gt;


&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;During the daily cron window our Azure SQL metrics showed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;What we expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg transaction log writes per UPDATE&lt;/td&gt;
&lt;td&gt;~3.2 KB&lt;/td&gt;
&lt;td&gt;~0.3 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonclustered index pages touched per UPDATE&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WRITELOG&lt;/code&gt; waits during cron&lt;/td&gt;
&lt;td&gt;elevated, periodic spikes&lt;/td&gt;
&lt;td&gt;near zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PAGELATCH&lt;/code&gt; waits&lt;/td&gt;
&lt;td&gt;noticeable&lt;/td&gt;
&lt;td&gt;minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger firing overhead&lt;/td&gt;
&lt;td&gt;all 22 columns evaluated&lt;/td&gt;
&lt;td&gt;only changed columns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The database was doing roughly &lt;strong&gt;10× more write work&lt;/strong&gt; than the actual data&lt;br&gt;
changes justified.&lt;/p&gt;

&lt;p&gt;Here's what the database wait dashboard looked like during peak cron hours —&lt;br&gt;
&lt;code&gt;WRITELOG&lt;/code&gt; at over 2,000 seconds total:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyn2qg2mfp4p6d9vx2jk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyn2qg2mfp4p6d9vx2jk.png" alt=" " width="800" height="1205"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;Hibernate's default behaviour is to generate a &lt;strong&gt;single static UPDATE&lt;br&gt;
statement&lt;/strong&gt; for each entity class at startup and reuse it for every save. That&lt;br&gt;
statement sets &lt;strong&gt;every column&lt;/strong&gt;, every time, regardless of which fields&lt;br&gt;
actually changed.&lt;/p&gt;

&lt;p&gt;Here's what Hibernate was sending to SQL Server on every &lt;code&gt;save()&lt;/code&gt; call&lt;br&gt;
(simplified):&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="c1"&gt;-- Hibernate's DEFAULT behaviour: static UPDATE with all 22 columns&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;MEMBER_PROFILE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;ACCOUNT_ID&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EMAIL_ADDR&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;REGION_CODE&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TZ_NAME&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_TZ&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BILLING_PERIOD&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RANK_LEVEL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;-- ← actually changed&lt;/span&gt;
    &lt;span class="n"&gt;RANK_LABEL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NEXT_RANK_LEVEL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;-- ← actually changed&lt;/span&gt;
    &lt;span class="n"&gt;NEXT_RANK_LABEL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ACCT_STATUS&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CONSENT_STATUS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CONSENT_ACCEPTED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CONSENT_VERSION&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;STATS_JSON&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- a JSON blob, ~2 KB&lt;/span&gt;
    &lt;span class="n"&gt;RANK_HISTORY_JSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- another JSON blob&lt;/span&gt;
    &lt;span class="n"&gt;UPDATED_AT&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CREATED_AT&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAST_MODIFIED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- ← actually changed&lt;/span&gt;
    &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ORG_ID&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UPDATED_BY&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p22&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p23&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We only changed 3 values. Hibernate sent all 22.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters to the database
&lt;/h3&gt;

&lt;p&gt;When SQL Server receives this UPDATE, it doesn't know that 19 of those&lt;br&gt;
column values are identical to what's already on disk. It has to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│              What SQL Server does                   │
├─────────────────────────────────────────────────────┤
│                                                     │
│  1. Write the FULL before-image of the row          │
│     to the transaction log        (~3.2 KB)         │
│                                                     │
│  2. Write the FULL after-image of the row           │
│     to the transaction log        (~3.2 KB)         │
│                                                     │
│  3. For EACH nonclustered index that covers         │
│     a column in the SET clause:                     │
│     • Read the old index entry                      │
│     • Delete it                                     │
│     • Insert the new entry                          │
│     (Even if the indexed value didn't change!)      │
│                                                     │
│  4. Fire any UPDATE triggers, passing ALL 22        │
│     columns in the INSERTED / DELETED pseudo-tables │
│                                                     │
│  5. Update the row version for CDC / replication    │
│                                                     │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 17 nonclustered indexes on the table, every UPDATE touched up to &lt;strong&gt;17&lt;br&gt;
index B-trees&lt;/strong&gt; even when the indexed columns hadn't changed. That's a lot&lt;br&gt;
of page latches, a lot of log writes, and a lot of wasted I/O.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At tens of thousands of events per minute, this added up fast.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The fix: two lines of code
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.hibernate.annotations.DynamicUpdate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@DynamicUpdate&lt;/span&gt;   &lt;span class="c1"&gt;// ← this is the fix&lt;/span&gt;
&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MEMBER_PROFILE"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MemberProfileEntity&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseEntity&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ACCOUNT_ID"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"RANK_LEVEL"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;rankLevel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ... 19 more columns ...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. One import, one annotation.&lt;/p&gt;
&lt;h3&gt;
  
  
  What &lt;code&gt;@DynamicUpdate&lt;/code&gt; does
&lt;/h3&gt;

&lt;p&gt;Instead of generating a static UPDATE at startup, Hibernate now &lt;strong&gt;compares&lt;br&gt;
the entity's current state with its loaded snapshot&lt;/strong&gt; (the "dirty check" it&lt;br&gt;
already does anyway) and builds the SQL at runtime with only the changed&lt;br&gt;
columns:&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="c1"&gt;-- With @DynamicUpdate: only the 3 columns that actually changed&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;MEMBER_PROFILE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;RANK_LEVEL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NEXT_RANK_LEVEL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAST_MODIFIED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p5&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4 columns instead of 22. (The &lt;code&gt;OPT_LOCK_VER&lt;/code&gt; increment is always&lt;br&gt;
included for optimistic locking.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Before vs. after — the actual SQL
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; (&lt;code&gt;save()&lt;/code&gt; after changing 3 fields — Hibernate sends all 22):&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;MEMBER_PROFILE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;ACCOUNT_ID&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EMAIL_ADDR&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;REGION_CODE&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TZ_NAME&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_TZ&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BILLING_PERIOD&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RANK_LEVEL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;-- ← actually changed&lt;/span&gt;
    &lt;span class="n"&gt;RANK_LABEL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NEXT_RANK_LEVEL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;-- ← actually changed&lt;/span&gt;
    &lt;span class="n"&gt;NEXT_RANK_LABEL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ACCT_STATUS&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CONSENT_STATUS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CONSENT_ACCEPTED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CONSENT_VERSION&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;STATS_JSON&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RANK_HISTORY_JSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UPDATED_AT&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CREATED_AT&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAST_MODIFIED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- ← actually changed&lt;/span&gt;
    &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ORG_ID&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UPDATED_BY&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p22&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p23&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt; (&lt;code&gt;@DynamicUpdate&lt;/code&gt; — Hibernate sends only what changed):&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;MEMBER_PROFILE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;RANK_LEVEL&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NEXT_RANK_LEVEL&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAST_MODIFIED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p5&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;OPT_LOCK_VER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;22 columns → 4. Same result, ~90% less I/O.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cascade of savings
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────┐
│                                                                  │
│   Before: UPDATE sets 22 columns                                 │
│   ─────────────────────────────                                  │
│   • Transaction log:  ~6.4 KB per UPDATE (before + after image)  │
│   • Index maintenance: 17 indexes touched                        │
│   • Page latches: high contention                                │
│   • Trigger cost: all columns evaluated                          │
│                                                                  │
│   After: UPDATE sets 3–4 columns                                 │
│   ──────────────────────────────                                 │
│   • Transaction log:  ~0.6 KB per UPDATE                         │
│   • Index maintenance: 2–3 indexes touched                       │
│   • Page latches: minimal contention                             │
│   • Trigger cost: only changed columns evaluated                 │
│                                                                  │
│   Result                                                         │
│   ──────                                                         │
│   • ~90% reduction in log bytes per UPDATE                       │
│   • ~85% reduction in index page writes                          │
│   • WRITELOG wait type: elevated → near zero                     │
│   • PAGELATCH_EX wait type: noticeable → negligible              │
│   • Peak cron throughput headroom: significantly increased        │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the same dashboard after deploying the fix — &lt;code&gt;WRITELOG&lt;/code&gt; dropped&lt;br&gt;
from 2,020s to 403s:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm49h2h6y427zoun3mgpr.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm49h2h6y427zoun3mgpr.png" alt=" " width="800" height="1205"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  How to check if this affects you
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Find your wide entities
&lt;/h3&gt;

&lt;p&gt;Any JPA entity with &lt;strong&gt;10+ columns&lt;/strong&gt; where typical updates touch fewer than&lt;br&gt;
half of them is a candidate. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON/text blob columns (&lt;code&gt;STATS_JSON&lt;/code&gt;, &lt;code&gt;PAYLOAD&lt;/code&gt;, &lt;code&gt;CONFIG&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Audit columns (&lt;code&gt;CREATED_BY&lt;/code&gt;, &lt;code&gt;CREATED_AT&lt;/code&gt;) that never change after insert&lt;/li&gt;
&lt;li&gt;Status fields that change independently (&lt;code&gt;ACCT_STATUS&lt;/code&gt; vs. &lt;code&gt;RANK_LEVEL&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: Look at your SQL Server wait stats
&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;-- Check current waits (run during your peak load)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;wait_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_time_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;waiting_tasks_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_os_wait_stats&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;wait_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WRITELOG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'PAGELATCH_EX'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'PAGELATCH_SH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s1"&gt;'PAGEIOLATCH_EX'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LOGBUFFER'&lt;/span&gt;&lt;span class="p"&gt;)&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;wait_time_ms&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;If &lt;code&gt;WRITELOG&lt;/code&gt; or &lt;code&gt;PAGELATCH_EX&lt;/code&gt; are high during your write-heavy windows,&lt;br&gt;
you may be doing unnecessary work.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Count your indexes
&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;-- How many nonclustered indexes does your table have?&lt;/span&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;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object_id&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;object_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&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;type_desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NONCLUSTERED'&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;index_count&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;More indexes = more pain from full-row UPDATEs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: See what Hibernate is actually sending
&lt;/h3&gt;

&lt;p&gt;Add this to your &lt;code&gt;application.yml&lt;/code&gt; temporarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jpa&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;show-sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hibernate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;format_sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your update flow and look at the generated SQL. If you see all columns&lt;br&gt;
in the SET clause when you only changed two, &lt;code&gt;@DynamicUpdate&lt;/code&gt; will help.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tradeoffs — when NOT to use it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@DynamicUpdate&lt;/code&gt; isn't free. Here's what you're trading:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Default (static)&lt;/th&gt;
&lt;th&gt;@DynamicUpdate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Once at startup, cached&lt;/td&gt;
&lt;td&gt;Every dirty &lt;code&gt;save()&lt;/code&gt; call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PreparedStatement caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One statement per entity, highly cacheable&lt;/td&gt;
&lt;td&gt;Many statement variations, lower hit rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower (no per-save SQL build)&lt;/td&gt;
&lt;td&gt;Slightly higher (dirty-check diff + SQL build)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network payload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Larger (all columns)&lt;/td&gt;
&lt;td&gt;Smaller (only changed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB write I/O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higher (all columns written)&lt;/td&gt;
&lt;td&gt;Lower (only changed columns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Narrow tables, bulk inserts&lt;/td&gt;
&lt;td&gt;Wide tables with partial updates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;@DynamicUpdate&lt;/code&gt; when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your table has &lt;strong&gt;10+ columns&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Typical updates touch &lt;strong&gt;less than half&lt;/strong&gt; the columns&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;multiple nonclustered indexes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You're doing &lt;strong&gt;high-throughput writes&lt;/strong&gt; (batch jobs, event processing)&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;large columns&lt;/strong&gt; (JSON blobs, text) that rarely change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your table has 5 columns and every update changes all of them&lt;/li&gt;
&lt;li&gt;You're doing mostly inserts (not updates)&lt;/li&gt;
&lt;li&gt;PreparedStatement plan-cache pressure is already a problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case the math was obvious: 22 columns, 17 indexes, typically 3&lt;br&gt;
columns changing, tens of thousands of writes per minute. The CPU cost of&lt;br&gt;
building SQL at runtime was negligible compared to the I/O savings.&lt;/p&gt;




&lt;h2&gt;
  
  
  A note on &lt;code&gt;@DynamicInsert&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Hibernate also offers &lt;code&gt;@DynamicInsert&lt;/code&gt;, which excludes null columns from&lt;br&gt;
INSERT statements. This is useful when your database has column defaults&lt;br&gt;
(like &lt;code&gt;GETDATE()&lt;/code&gt; for timestamps) that you want the DB to fill in rather&lt;br&gt;
than Hibernate sending explicit NULLs.&lt;/p&gt;

&lt;p&gt;We didn't need it here — our &lt;code&gt;@PrePersist&lt;/code&gt; callback sets all required&lt;br&gt;
defaults in Java — but it's worth knowing about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;The real bug wasn't technical. It was &lt;strong&gt;assuming that the ORM was doing&lt;br&gt;
something sensible by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Hibernate's static UPDATE is a perfectly reasonable default for most&lt;br&gt;
applications. It reduces startup cost, maximises statement caching, and&lt;br&gt;
simplifies the runtime path. For a CRUD app with 8-column tables and&lt;br&gt;
moderate traffic, you'd never notice the difference.&lt;/p&gt;

&lt;p&gt;But defaults don't know about your table shape, your index count, or your&lt;br&gt;
traffic pattern. When those change — when the table grows wider, when you&lt;br&gt;
add more indexes, when traffic scales up — the same default that was fine&lt;br&gt;
last quarter becomes the bottleneck this quarter.&lt;/p&gt;

&lt;p&gt;The fix took two lines. Finding it took staring at Azure Monitor dashboards&lt;br&gt;
wondering why &lt;code&gt;WRITELOG&lt;/code&gt; waits were spiking during a job that "only updates&lt;br&gt;
a couple of fields."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your wide entities. The annotation is right there.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. If you've run into similar ORM-vs-database surprises,&lt;br&gt;
I'd love to hear about them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>hibernate</category>
      <category>springboot</category>
      <category>sql</category>
    </item>
    <item>
      <title>Streamline Your JSON Workflow: A Free Tool &amp; Tips for API Debugging</title>
      <dc:creator>M Shahid</dc:creator>
      <pubDate>Mon, 08 Sep 2025 16:40:33 +0000</pubDate>
      <link>https://dev.to/algopro/streamline-your-json-workflow-a-free-tool-tips-for-api-debugging-3f3k</link>
      <guid>https://dev.to/algopro/streamline-your-json-workflow-a-free-tool-tips-for-api-debugging-3f3k</guid>
      <description>&lt;p&gt;Hey Dev.to community,&lt;/p&gt;

&lt;p&gt;As developers, we’ve all faced the chaos of unformatted JSON—whether it’s debugging an API response, cleaning up config files, or converting data for analysis. I’ve been tinkering with ways to make JSON handling less painful and wanted to share a tool and some tips that have helped me out.&lt;/p&gt;

&lt;p&gt;I built a &lt;a href="https://json-formatter.utilia.dev" rel="noopener noreferrer"&gt;free JSON Formatter tool&lt;/a&gt; that formats JSON with clean indentation, validates syntax in real-time with clear error messages, and converts to formats like CSV, XML, or YAML. It’s browser-based, privacy-first (no data leaves your device), and works on any platform. I use it for quick API debugging or prepping data for reports, and it’s been a time-saver.&lt;/p&gt;

&lt;p&gt;Here are a few tips for working with JSON:&lt;/p&gt;

&lt;p&gt;Validate Early: Always check JSON syntax before sending to an API to avoid 400 errors. Tools like this highlight issues like trailing commas or missing quotes.&lt;br&gt;
Use JSON Schema: For complex APIs, define schemas to enforce data structure.&lt;/p&gt;

&lt;p&gt;Chunk Large Files: For huge JSON datasets, process in smaller chunks to avoid browser lag.&lt;/p&gt;

&lt;p&gt;What’s your go-to setup for JSON formatting or debugging? Any favorite tools, scripts, or workflows for handling JSON in your projects? I’d love to hear your thoughts, and if you try the tool, let me know what features would make it even better!&lt;br&gt;
Happy coding! 🚀&lt;/p&gt;

&lt;p&gt;Tags: #javascript #webdev #json #apitools #developertools&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
