<?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: Mircea Cadariu</title>
    <description>The latest articles on DEV Community by Mircea Cadariu (@mcadariu).</description>
    <link>https://dev.to/mcadariu</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%2F1224431%2F76874bab-571a-437b-abfa-864a9e1e2349.png</url>
      <title>DEV Community: Mircea Cadariu</title>
      <link>https://dev.to/mcadariu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mcadariu"/>
    <language>en</language>
    <item>
      <title>Postgres tuning using feedback loops</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Sat, 15 Nov 2025 11:50:36 +0000</pubDate>
      <link>https://dev.to/mcadariu/postgres-tuning-using-feedback-loops-2hmp</link>
      <guid>https://dev.to/mcadariu/postgres-tuning-using-feedback-loops-2hmp</guid>
      <description>&lt;p&gt;I wanted to gather in one place a selection of resources that have helped me learn how to monitor and tune Postgres effectively. For optimal database performance, there's a high chance we'll have to get our hands dirty with this topic, because the default settings are on the conservative side (it has to start even on a Raspberry Pi), but with proper tuning it is impressive how far can Postgres take us. &lt;/p&gt;

&lt;p&gt;Given the number of individual settings you can configure, the task might seem daunting at first sight, but luckily there are great guides out there, as well as I've found you can structure them into some categories. It will help a lot also to learn a bit how the Postgres internal components work - I suggest reading the &lt;a href="https://postgrespro.com/community/books/internals" rel="noopener noreferrer"&gt;Postgres internals&lt;/a&gt; book (free) for this purpose. &lt;/p&gt;

&lt;p&gt;Our workloads can change so we have to continuously track the internal health over time and adjust when needed, using &lt;strong&gt;feedback loops&lt;/strong&gt; that let us know if we have to take any action. For this purpose, a custom Grafana dashboard based on Postgres internal metrics have provided me with everything I need for doing a good job. For best practices on setting up actionable dashboards, I recommend &lt;a href="https://www.amazon.co.uk/Information-Dashboard-Design-Effective-Communication/dp/0596100167" rel="noopener noreferrer"&gt;this book&lt;/a&gt;. There are many readily available ones as well, and below I'll gather some options for you. Lastly, you have AI agents that check metrics themselves and take action. &lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres wiki
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wiki.postgresql.org/wiki/Monitoring" rel="noopener noreferrer"&gt;Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/postgres-ai/postgres_ai" rel="noopener noreferrer"&gt;postgres_ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pgwat.ch/latest/" rel="noopener noreferrer"&gt;pgwatch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/percona/pg_stat_monitor" rel="noopener noreferrer"&gt;pg_stat_monitor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/percona/pg_stat_monitor" rel="noopener noreferrer"&gt;pg_statviz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pganalyze.com/" rel="noopener noreferrer"&gt;pganalyze&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dalibo/temboard" rel="noopener noreferrer"&gt;temboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NikolayS/pg_ash" rel="noopener noreferrer"&gt;pg_ash&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Postgres FM podcast episode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://postgres.fm/episodes/monitoring-checklist" rel="noopener noreferrer"&gt;Monitoring checklist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://xata.io/database-agent" rel="noopener noreferrer"&gt;Xata Agent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Blog post selection
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.craigkerstiens.com/2012/10/01/understanding-postgres-performance/" rel="noopener noreferrer"&gt;cache hit ratio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;checkpoints

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.enterprisedb.com/blog/basics-tuning-checkpoints" rel="noopener noreferrer"&gt;1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.percona.com/blog/importance-of-tuning-checkpoint-in-postgresql/" rel="noopener noreferrer"&gt;2&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://legacy.tembo.io/blog/optimizing-memory-usage" rel="noopener noreferrer"&gt;work_mem&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/database/a-case-study-of-tuning-autovacuum-in-amazon-rds-for-postgresql/" rel="noopener noreferrer"&gt;autovacuum&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://dataegret.de/2017/03/deep-dive-into-postgres-stats-pg_stat_bgwriter/" rel="noopener noreferrer"&gt;bgwriter&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://vondra.me/posts/tuning-aio-in-postgresql-18/" rel="noopener noreferrer"&gt;AIO&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://www.craigkerstiens.com/2017/09/18/postgres-connection-management/" rel="noopener noreferrer"&gt;connections&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://www.enterprisedb.com/blog/general-configuration-and-tuning-recommendations-edb-postgres-advanced-server-and-postgresql" rel="noopener noreferrer"&gt;initial settings recommendations&lt;br&gt;
&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>programming</category>
      <category>resources</category>
    </item>
    <item>
      <title>Postgres Column Tetris</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Tue, 14 Oct 2025 19:42:22 +0000</pubDate>
      <link>https://dev.to/mcadariu/postgres-column-tetris-neatly-packing-your-tables-for-fun-and-profit-1j6g</link>
      <guid>https://dev.to/mcadariu/postgres-column-tetris-neatly-packing-your-tables-for-fun-and-profit-1j6g</guid>
      <description>&lt;p&gt;If you've been working with Postgres for a while, you have probably already learned how to write queries and tune them for performance. Today, I'd like to show you a lesser known optimization technique called &lt;code&gt;Column Tetris&lt;/code&gt;: the practice of ordering your columns to minimize storage overhead due to CPU alignment requirements. &lt;/p&gt;

&lt;p&gt;As far as I can tell, the name of this technique was coined by Erwin Brandstetter &lt;a href="https://stackoverflow.com/a/7431468" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Important&lt;/em&gt;: The best time to apply this is when you initially create a table, since there's no data to migrate. But as they say, better late than never! &lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother?
&lt;/h2&gt;

&lt;p&gt;Column Tetris delivers tangible benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More rows per page&lt;/strong&gt;: Postgres' 8kb pages can fit more rows, reducing I/O operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better cache utilization&lt;/strong&gt;: denser pages mean more data fits in RAM, reducing slow disk reads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster sequential scans&lt;/strong&gt;: Less data to read means faster table scans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower backup/restore times&lt;/strong&gt;: Smaller tables are faster to backup and restore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Convinced? Let's dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does column order matter?
&lt;/h2&gt;

&lt;p&gt;In Postgres, the order you define columns in your CREATE TABLE statement affects how much disk space your table consumes. This is because CPUs prefer to read data at memory addresses that are multiples of the data type's size. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An 4-byte integer wants to start at an address divisible by 4&lt;/li&gt;
&lt;li&gt;An 8-byte timestamp wants to start at an address divisible by 8&lt;/li&gt;
&lt;li&gt;etc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is called "alignment". Not &lt;a href="https://en.wikipedia.org/wiki/AI_alignment" rel="noopener noreferrer"&gt;&lt;em&gt;that&lt;/em&gt;&lt;/a&gt; alignment, but &lt;a href="https://en.wikipedia.org/wiki/Data_structure_alignment#:~:text=A%20memory%20access%20is%20said,memory%20accesses%20are%20always%20aligned." rel="noopener noreferrer"&gt;this one&lt;/a&gt;. &lt;br&gt;
When data isn't naturally aligned, Postgres inserts padding bytes to maintain proper alignment. These padding bytes waste space and bloat your storage. &lt;/p&gt;
&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Let's track user logins with a simple schema:&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;logins&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 4 bytes&lt;/span&gt;
    &lt;span class="n"&gt;is_success&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 1 byte (+ 3 bytes padding)&lt;/span&gt;
    &lt;span class="n"&gt;login_time&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;is_mobile&lt;/span&gt;  &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;         &lt;span class="c1"&gt;-- 1 byte&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? The login_time TIMESTAMP needs to start at an address divisible by 8. Since is_success (1 byte) comes right before it, PostgreSQL must insert 3 bytes of padding to align the TIMESTAMP properly. &lt;br&gt;
Let's verify this. We know the row header is &lt;a href="https://www.postgresql.org/docs/current/storage-page-layout.html" rel="noopener noreferrer"&gt;24 bytes&lt;/a&gt;. Add our data (4 + 1 + 3 padding + 8 + 1 = 17 bytes), and we should get 41 bytes total.&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;logins&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;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&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="k"&gt;false&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;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logins&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;row_size_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; row_size_bytes 
----------------
             41
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed! Now let's optimize by reordering the 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="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;logins_optimized&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;login_time&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 4 bytes&lt;/span&gt;
    &lt;span class="n"&gt;is_success&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 1 byte&lt;/span&gt;
    &lt;span class="n"&gt;is_mobile&lt;/span&gt;  &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;         &lt;span class="c1"&gt;-- 1 byte&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By placing the TIMESTAMP first, it's naturally aligned at the start. The INTEGER fits perfectly after it, and both BOOLEANs last.&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;logins_optimized&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&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;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logins_optimized&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;row_size_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins_optimized&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; row_size_bytes 
----------------
             38
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Savings: 3 bytes per row just from reordering!&lt;/p&gt;

&lt;h2&gt;
  
  
  Padding between rows
&lt;/h2&gt;

&lt;p&gt;There's more to the story. PostgreSQL also aligns entire tuples to 8-byte boundaries (MAXALIGN on 64-bit systems). This means padding is inserted not just within rows, but also between them.&lt;/p&gt;

&lt;p&gt;Let's see this effect at scale by inserting 1 million rows:&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;TRUNCATE&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logins_optimized&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert 1 million rows&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;logins&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;login_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_mobile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;random&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;0&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="n"&gt;NOW&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="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'365 days'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;random&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;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&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;1000000&lt;/span&gt;&lt;span class="p"&gt;);&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;logins_optimized&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;login_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_mobile&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's analyze the storage:&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_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logins'&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;original_bytes&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;'logins'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;original_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&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;'logins'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8192&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rows_per_page_original&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;'logins_optimized'&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;optimized_bytes&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;'logins_optimized'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;optimized_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&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;'logins_optimized'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8192&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rows_per_page_optimized&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logins_optimized&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&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;Results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; original_bytes | original_pages | rows_per_page_original  | optimized_bytes | optimized_pages | rows_per_page_optimized 
-------------+-------------+----------------------+-----------------+-----------------+-------------------------
    52183040 |        6370 | 156.9858712715855573 |        44285952 |            5406 |    184.9796522382537921
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking this down:&lt;/p&gt;

&lt;p&gt;Original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000,000 rows in 6,370 pages&lt;/li&gt;
&lt;li&gt;~157 rows per page&lt;/li&gt;
&lt;li&gt;52,183,040 bytes / 1,000,000 rows = 52.18 bytes per row (including all overhead)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000,000 rows in 5,406 pages&lt;/li&gt;
&lt;li&gt;~185 rows per page&lt;/li&gt;
&lt;li&gt;44,285,952 bytes / 1,000,000 rows = 44.29 bytes per row (including all overhead)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Difference: 52.18 - 44.29 = &lt;code&gt;7.89 bytes per row&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Notice the difference is larger than the 3 bytes we saved from column alignment alone. The extra savings come from inter-row padding—each tuple is padded to reach an 8-byte boundary, and poorly ordered columns require more padding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Total savings
&lt;/h2&gt;

&lt;p&gt;Let's see the overall impact:&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;-- Check the table sizes&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;'logins'&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;original_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;'logins_optimized'&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;optimized_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;'logins) - 
        pg_relation_size('&lt;/span&gt;&lt;span class="n"&gt;logins_optimized&lt;/span&gt;&lt;span class="s1"&gt;')
    ) as savings;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; original_size | optimized_size | savings 
------------+----------------+---------
 50 MB      | 42 MB          | 7712 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a 14% reduction in storage from simply reordering four columns. Not bad, I'll take it! &lt;/p&gt;

&lt;h2&gt;
  
  
  Guideline
&lt;/h2&gt;

&lt;p&gt;To minimize the padding I showed you in the sections above, order your columns by alignment requirements, largest to smallest:&lt;/p&gt;

&lt;p&gt;8-byte types: &lt;code&gt;BIGINT&lt;/code&gt;, &lt;code&gt;DOUBLE PRECISION&lt;/code&gt;, &lt;code&gt;TIMESTAMP&lt;/code&gt;, &lt;code&gt;TIMESTAMPTZ&lt;/code&gt;&lt;br&gt;
4-byte types: &lt;code&gt;INTEGER&lt;/code&gt;, &lt;code&gt;REAL&lt;/code&gt;, &lt;code&gt;DATE&lt;/code&gt;&lt;br&gt;
2-byte types: &lt;code&gt;SMALLINT&lt;/code&gt;&lt;br&gt;
1-byte types: &lt;code&gt;BOOLEAN&lt;/code&gt;&lt;br&gt;
Variable-width types last: &lt;code&gt;TEXT&lt;/code&gt;, &lt;code&gt;VARCHAR&lt;/code&gt;, &lt;code&gt;BYTEA&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When does this matter the most?
&lt;/h2&gt;

&lt;p&gt;Most impactful when you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables with millions or billions of rows&lt;/li&gt;
&lt;li&gt;Tables with many small fixed-width columns&lt;/li&gt;
&lt;li&gt;Tables that are frequently scanned in full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Low-impact scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small lookup tables (&amp;lt; 1000 rows)&lt;/li&gt;
&lt;li&gt;Tables with only a few columns&lt;/li&gt;
&lt;li&gt;Tables dominated by large TEXT/VARCHAR fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a table with 100 million rows, saving 8 bytes per row translates to ~800 MB less storage, faster scans, better cache utilization, and lower I/O costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Column Tetris&lt;/code&gt; is a simple technique that costs nothing at design time but can yield significant storage savings. Think of it like organizing your closet: arranging items thoughtfully takes the same effort as tossing them in randomly, but the results are much better.&lt;br&gt;
So next time you write a CREATE TABLE statement, take a moment to play Column Tetris. Your database will thank you.&lt;/p&gt;

&lt;p&gt;Thanks for reading! Until next time! &lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Postgres Range Types</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Tue, 07 Oct 2025 13:10:01 +0000</pubDate>
      <link>https://dev.to/mcadariu/the-unreasonable-effectiveness-of-postgres-range-types-1ine</link>
      <guid>https://dev.to/mcadariu/the-unreasonable-effectiveness-of-postgres-range-types-1ine</guid>
      <description>&lt;p&gt;When developing applications that track measurements over time, you'll often encounter scenarios where values remain constant across multiple readings. Consider a temperature monitoring system that takes daily measurements: if the temperature stays at 20.5°C for an entire month, storing 30 identical rows is wasteful and degrades query performance.  &lt;/p&gt;

&lt;p&gt;Postgres' &lt;a href="https://www.postgresql.org/docs/current/rangetypes.html" rel="noopener noreferrer"&gt;range types&lt;/a&gt; offer an elegant solution to this problem, potentially reducing storage requirements by orders of magnitude while maintaining data integrity. In this post, I'll demonstrate how range types work and show you the dramatic space savings they can deliver.&lt;/p&gt;

&lt;h2&gt;
  
  
  A straightforward approach: one row per day
&lt;/h2&gt;

&lt;p&gt;This is how you would store one reading per day.&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;temperature_readings_daily&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reading_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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;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;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reading_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Example data: same temperature for 30 days&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;temperature_readings_daily&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-30'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach creates 30 rows to represent a single temperature value that remained constant throughout January. &lt;/p&gt;

&lt;h2&gt;
  
  
  Date ranges
&lt;/h2&gt;

&lt;p&gt;To use date ranges, we will have to rename our column and define it as having type &lt;code&gt;daterange&lt;/code&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;temperature_readings_range&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valid_period&lt;/span&gt; &lt;span class="n"&gt;DATERANGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Same data: one row covers 30 days&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;temperature_readings_range&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;span class="s1"&gt;'[2025-01-01,2025-01-31)'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daterange type uses interval notation: &lt;code&gt;[2025-01-01,2025-01-31)&lt;/code&gt; means the range includes January 1st through January 30th (the closing parenthesis excludes January 31st).&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the impact
&lt;/h2&gt;

&lt;p&gt;Let's have a look at what savings we can expect if we start working with range types. Let's generate some test data for our experiment.&lt;/p&gt;

&lt;p&gt;To quantify the space savings, let's run an experiment with realistic data. We'll simulate 100 sensors tracking temperatures over six months, with values changing twice per month (on the 1st and 15th). You can find the SQL to generate this data at the end of this post [1].&lt;/p&gt;

&lt;p&gt;Alright, how much did we gain? Here's the query we'll use to interrogate the table sizes.&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_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="s1"&gt;'temperature_readings_daily'&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;daily_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_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temperature_readings_range'&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;range_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&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="s1"&gt;'temperature_readings_range'&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="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temperature_readings_daily'&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="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;as&lt;/span&gt; &lt;span class="n"&gt;space_savings_percent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  daily_size | range_size | space_savings_percent 
------------+------------+-----------------------
 1448 kB    | 128 kB     |                 91.16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see a difference of one order of magnitude. Not bad! &lt;/p&gt;

&lt;p&gt;The space savings depend entirely on your data distribution though. If values change every day, you'll see minimal benefit. But if values remain constant for weeks or months at a time, the gains can be dramatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying
&lt;/h2&gt;

&lt;p&gt;Here's how you'd write queries to retrieve results of interest.&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;-- Find the temperature on a specific date&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temperature_readings_range&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sensor_id&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;AND&lt;/span&gt; &lt;span class="n"&gt;valid_period&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-06-15'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="c1"&gt;-- Get temperature changes in a date range&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temperature_readings_range&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sensor_id&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;AND&lt;/span&gt; &lt;span class="n"&gt;valid_period&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;'[2025-11-01,2025-12-31)'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Alternative: start and end columns
&lt;/h2&gt;

&lt;p&gt;You might wonder: why use range types at all? Why not just add start_date and end_date columns?&lt;/p&gt;

&lt;p&gt;This is a valid approach and achieves similar storage savings. So what are the tradeoffs?&lt;/p&gt;

&lt;h3&gt;
  
  
  Start/end columns:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;More familiar and intuitive for most developers&lt;/li&gt;
&lt;li&gt;Works with any database system, not just PostgreSQL&lt;/li&gt;
&lt;li&gt;Easier to understand in query results&lt;/li&gt;
&lt;li&gt;No need to learn range-specific operators&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Range types:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Data integrity: range types enforce that the period is valid (start before end) at the type level&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Specialized operators: @&amp;gt; (contains), &amp;amp;&amp;amp; (overlaps), &amp;lt;@ (contained by) make queries more expressive&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GiST indexing: PostgreSQL can build efficient indexes specifically designed for range queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NULL handling: With start/end columns, you need to handle the case where end_date is NULL (for ongoing periods). Range types handle open-ended ranges naturally with the [2025-01-01,) notation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleaner semantics: A single valid_period column is conceptually clearer than two related columns&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Beyond date ranges: other Postgres range types
&lt;/h2&gt;

&lt;p&gt;While we've focused on daterange for this example, Postgres provides several built-in range types for different use cases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;int4range&lt;/code&gt; and &lt;code&gt;int8range&lt;/code&gt; – Integer ranges, useful for ID ranges, version numbers, or inventory levels&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;numrange&lt;/code&gt; – Numeric ranges for decimal values like prices or measurements&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsrange&lt;/code&gt; and &lt;code&gt;tstzrange&lt;/code&gt; – Timestamp ranges (with and without timezone) for precise event tracking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;daterange&lt;/code&gt; – Date ranges as we've used in this post&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Postgres's range types is an example of why it's such a feature-rich database. By representing continuous periods with a single row instead of many, you can achieve storage savings and cleaner data models. For applications dealing with time-series data that changes infrequently, always consider range types for your design.&lt;/p&gt;

&lt;p&gt;[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="c1"&gt;-- Generate readings that change twice per month (1st and 15th)&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;temp_changes&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;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;change_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&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="mi"&gt;5&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;as&lt;/span&gt; &lt;span class="n"&gt;temperature&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;100&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;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-06-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-12-31'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&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;day&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; 
    &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;day&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="mi"&gt;1&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="c1"&gt;-- Then, fill in all days with the temperature from the most recent change&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;temperature_readings_daily&lt;/span&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;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temperature&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;100&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;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-06-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-12-31'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&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;day&lt;/span&gt;
    &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&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;temperature&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temp_changes&lt;/span&gt; &lt;span class="n"&gt;tc2&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tc2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="o"&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;sensor_id&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tc2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&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;tc2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
        &lt;span class="k"&gt;LIMIT&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;tc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Populate the range table too&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;temperature_readings_range&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;change_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sensor_id&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;change_date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&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;valid_period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temp_changes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>postgres</category>
      <category>database</category>
      <category>data</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Links instead of repetition</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Thu, 11 Sep 2025 18:46:47 +0000</pubDate>
      <link>https://dev.to/mcadariu/links-instead-of-repetition-4pc7</link>
      <guid>https://dev.to/mcadariu/links-instead-of-repetition-4pc7</guid>
      <description>&lt;p&gt;Today I'm sharing with you a principle that I keep encountering in several systems I'm researching, appearing in different forms but always serving the same underlying goal: &lt;strong&gt;removing redundancy through indirection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I've structured this post into four separate patterns, let me walk you through them and you will see how all share the same substrate of favoring links instead of repetition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dictionary Encoding
&lt;/h2&gt;

&lt;p&gt;Dictionary encoding is perhaps the clearest expression. Instead of storing repeated values directly, we create a dictionary (lookup table) and store only references to entries in that dictionary.&lt;/p&gt;

&lt;p&gt;Let's consider this array:&lt;br&gt;
Fruits: ["apple", "banana", "apple", "cherry", "banana", "apple"]&lt;/p&gt;

&lt;p&gt;We can transform it to:&lt;br&gt;
Dictionary: {0: "apple", 1: "banana", 2: "cherry"}&lt;br&gt;
Fruits: [0, 1, 0, 2, 1, 0]&lt;/p&gt;

&lt;p&gt;The same information is stored, however this approach has the benefit of immediate reduction of storage space, but we've also  managed to establish a single source of truth for each unique value. Hmm, where have you heard this before..&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Normalization
&lt;/h2&gt;

&lt;p&gt;Database normalization takes this same principle but applies it to relational data structures. &lt;/p&gt;

&lt;p&gt;For example, instead of repeating customer information across every order record, we create separate tables and link them through foreign keys.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Denormalized:&lt;/em&gt;&lt;br&gt;
Orders: [order_id, customer_name, customer_email, product_name, quantity]&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Normalized:&lt;/em&gt; &lt;br&gt;
Customers: [customer_id, name, email]&lt;br&gt;
Products: [product_id, name, price]&lt;br&gt;
Orders: [order_id, customer_id, product_id, quantity]&lt;/p&gt;

&lt;p&gt;This isn't just about storage efficiency, but also about data integrity. When customer information changes, there's only one place to update it. We've eliminated the possibility of inconsistent data.&lt;/p&gt;

&lt;h2&gt;
  
  
  String Interning
&lt;/h2&gt;

&lt;p&gt;String interning is more of a programming language runtime feature, and it ensures that identical string literals share the same memory location. Instead of creating multiple string objects with the same content, the runtime maintains a pool of unique strings and returns references to existing instances. &lt;/p&gt;

&lt;p&gt;As a case study, see for example &lt;a href="https://docs.oracle.com/javase/specs/jls/se24/html/jls-3.html#jls-3.10.5" rel="noopener noreferrer"&gt;this&lt;/a&gt; entry from the Java Language Specification describing how it works in Java. We find this concept in &lt;a href="https://docs.python.org/3.2/library/sys.html?highlight=sys.intern#sys.intern" rel="noopener noreferrer"&gt;python&lt;/a&gt; as well. &lt;/p&gt;

&lt;p&gt;Outside of the programming languages domain, &lt;a href="https://victoriametrics.com/blog/tsdb-performance-techniques-strings-interning/" rel="noopener noreferrer"&gt;this&lt;/a&gt; is a post from Victoria Metrics on how they applied it in their solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  German Strings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cedardb.com/blog/german_strings/" rel="noopener noreferrer"&gt;"German strings"&lt;/a&gt; is a clever string optimization technique to know about when you're building a database. The approach stores a 4-character prefix directly within the string header, avoiding pointer dereferences for common string operations. The key insight is that most string operations only need to examine the beginning of a string.&lt;/p&gt;

&lt;p&gt;Let's consider this full string: "PostgreSQL is awesome".                 &lt;/p&gt;

&lt;p&gt;This is the German string structure:&lt;br&gt;
[length][prefix: "Post"][pointer] -&amp;gt; "greSQL is awesome"&lt;/p&gt;

&lt;p&gt;This creates an indirection pattern where the prefix enables fast string comparisons and filtering operations without dereferencing pointers, since most mismatches can be detected by comparing just the first few characters.&lt;/p&gt;

&lt;p&gt;However, German strings aren't always optimal. The overhead per string can be problematic for certain workloads. As the team at &lt;a href="https://www.polarsignals.com/blog/posts/2025/08/26/das-problem-mit-german-strings" rel="noopener noreferrer"&gt;Polar Signals&lt;/a&gt; described, for low-cardinality string columns (like airport codes or status enums), &lt;strong&gt;simple dictionary encoding provides a 75% memory reduction&lt;/strong&gt; compared to German strings.&lt;/p&gt;

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

&lt;p&gt;The pattern of creating links to canonical sources is prevalent because it addresses fundamental challenges: storage efficiency, data consistency, and maintainability.&lt;/p&gt;

&lt;p&gt;The next time you encounter repeated data in your systems, ask yourself: &lt;em&gt;"Could I consider a link here instead?"&lt;/em&gt; The answer might lead you to refactor towards more elegant, efficient, and maintainable solutions.&lt;/p&gt;

&lt;p&gt;Thanks for reading! Until next time!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>database</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Loading One-to-Many relationships efficiently using Spring Data JPA and Postgres</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Sun, 24 Aug 2025 16:45:48 +0000</pubDate>
      <link>https://dev.to/mcadariu/loading-one-to-many-relationships-efficiently-with-spring-data-jpa-and-postgres-1ik9</link>
      <guid>https://dev.to/mcadariu/loading-one-to-many-relationships-efficiently-with-spring-data-jpa-and-postgres-1ik9</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://en.wikipedia.org/wiki/One-to-many_(data_model)" rel="noopener noreferrer"&gt;One-to-Many relationship&lt;/a&gt;, or parent-child, is a common occurrence in application development. Off the cuff, we can name numerous instances, a sports team with its players, blog posts and their comments, etc. A natural solution for this use-case is to use a relational database and create foreign key constraints to enforce data integrity. &lt;/p&gt;

&lt;p&gt;This post focuses on the following task: how to efficiently load the &lt;strong&gt;list of parents, and all their corresponding children&lt;/strong&gt; in one go, with &lt;code&gt;Spring Data JPA&lt;/code&gt; and &lt;code&gt;Postgres&lt;/code&gt;. We'll start with the slowest approach (hello, N+1!) and show how to make it faster through successive refinements. The code is available in &lt;a href="https://github.com/mcadariu/one-to-many-relationships-demo" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authors and books
&lt;/h2&gt;

&lt;p&gt;Let's use a familiar scenario: &lt;code&gt;authors&lt;/code&gt; and their &lt;code&gt;books&lt;/code&gt;. This is how we'll create the tables.&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;authors&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;bigint&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&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;255&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;span class="n"&gt;bio&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;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;books&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;bigint&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&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;255&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;span class="n"&gt;isbn&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;13&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;published_year&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;author_id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&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;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;fk_books_author&lt;/span&gt; &lt;span class="k"&gt;foreign&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;author_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;authors&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="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;idx_books_author_id&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Populating the tables
&lt;/h2&gt;

&lt;p&gt;Let's insert some data to work with. We'll generate &lt;code&gt;1000&lt;/code&gt; authors, and every author wrote &lt;code&gt;30&lt;/code&gt; books each.&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;authors&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;bio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt;
    &lt;span class="s1"&gt;'Author '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Bio for author '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;seq&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;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&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;books&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isbn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_year&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;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;as&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Book '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;gs&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;isbn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;FLOOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;published_year&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;authors&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;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;30&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;gs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Entities
&lt;/h2&gt;

&lt;p&gt;We'll create two entity classes, &lt;code&gt;Author&lt;/code&gt; and &lt;code&gt;Book&lt;/code&gt;. In order to learn how to map them correctly with JPA/Hibernate, you can read &lt;a href="https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/" rel="noopener noreferrer"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the Book class, we'll reference Author like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LAZY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="nd"&gt;@JoinColumn&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;"author_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;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accordingly, in the Author class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"author"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LAZY&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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;books&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have the tables, the test data and the entities. So far so good! We're ready to look at ways we can query the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying
&lt;/h2&gt;

&lt;p&gt;We want to always keep a close eye on the queries Hibernate is generating for us under the hood in order to avoid surprises. We do it with the following setting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@DynamicPropertySource&lt;/span&gt; 
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;registerPgProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DynamicPropertyRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.jpa.show_sql"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&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;h3&gt;
  
  
  Iteration 1
&lt;/h3&gt;

&lt;p&gt;We'll start with a pure Java approach, which looks quite elegant actually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authorRepository&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
               &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBooks&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthorWithBooksDto&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                       &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                       &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                       &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBio&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                       &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BookDto&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                                  &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTitle&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                                  &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIsbn&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                                  &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPublishedYear&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toList&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="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toList&lt;/span&gt;&lt;span class="o"&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%2Frh014uow7tac1mrp1ieo.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%2Frh014uow7tac1mrp1ieo.png" alt="Nplus1" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But when running it, we immediately notice our console filling up with queries. You've just witnessed the &lt;a href="https://planetscale.com/blog/what-is-n-1-query-problem-and-how-to-solve-it" rel="noopener noreferrer"&gt;N+1 problem&lt;/a&gt;. You want to avoid this if you want a fast application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Iteration 2
&lt;/h3&gt;

&lt;p&gt;Alright, let's make this better. This is what we'll add in the repository class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT a FROM Author a JOIN FETCH a.books"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllWithBooks&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great stuff! Turns out, this cuts the time to approximately half. Hibernate generates one query only. You should always try to load all the data you need with a single query. It's this one:&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;a1_0&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="n"&gt;a1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b1_0&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="n"&gt;b1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isbn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;a1_0&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;from&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="n"&gt;a1_0&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="n"&gt;b1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;a1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;b1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's have a look at the &lt;a href="https://www.pgmustard.com/blog/2018/09/21/reading-postgres-query-plans-for-beginners" rel="noopener noreferrer"&gt;explain plan&lt;/a&gt; to learn the steps the database took to retrieve  our data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Hash Join  (cost=31.50..631.58 rows=30000 width=65) (actual time=0.476..8.912 rows=30000 loops=1)
   Hash Cond: (b1_0.author_id = a1_0.id)
   Buffers: shared hit=230
   -&amp;gt;  Seq Scan on books b1_0  (cost=0.00..521.00 rows=30000 width=29) (actual time=0.009..2.466 rows=30000 loops=1)
         Buffers: shared hit=221
   -&amp;gt;  Hash  (cost=19.00..19.00 rows=1000 width=36) (actual time=0.431..0.432 rows=1000 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 77kB
         Buffers: shared hit=9
         -&amp;gt;  Seq Scan on authors a1_0  (cost=0.00..19.00 rows=1000 width=36) (actual time=0.010..0.169 rows=1000 loops=1)
               Buffers: shared hit=9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing surprising, it joined two tables, authors and books, using the hash join algorithm. Note though the &lt;code&gt;rows=30000&lt;/code&gt; on the first line of the explain plan. This tells us that our final result set consists of 30000 rows. Let's have a look also at the layout of these rows. Below are the first 10 rows of the result set.&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%2Fnikrh8oj90wutbbzi2js.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%2Fnikrh8oj90wutbbzi2js.png" alt="duplication" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You're seeing where I'm going with this. Because of the join, we are fetching to our application code a result set that's larger than necessary and duplicated, with other words quite wasteful. &lt;/p&gt;

&lt;h3&gt;
  
  
  Iteration 3
&lt;/h3&gt;

&lt;p&gt;Let's try something else. We will construct the expected shape of the response fully database-side, using Postgres features. For this, we'll have to write a native query like the one below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
            SELECT
                a.id,
                a.name,
                a.bio,
                jsonb_agg(
                    jsonb_build_object(
                        'id', b.id,
                        'title', b.title,
                        'isbn', b.isbn,
                        'publishedYear', b.published_year
                    )
                ) AS books
            FROM authors a
            JOIN books b ON b.author_id = a.id
            GROUP BY a.id;
        """&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nativeQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;[]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllWithBooksAsJson&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how the result looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 652 | Author 652  | Bio for author 652  | [{"id": 652, "isbn": "0", "title": "Book 1 of author 652", "publishedYear": 2013}, {"id": 1652, "isbn": "1", "title": "Book 2 of author 652", "publishedYear": 2010}, {"id": 2652, "isbn": "1", "title": "Book 3 of author 652", "publishedYear": 2000}, {"id": 3652, "isbn": "0", "title": "Book 4 of author 652", "publishedYear": 2002}, {"id": 4652, "isbn": "1", "title": "Book 5 of author 652", "publishedYear": 2019}, {"id": 5652, "isbn": "1", "title": "Book 6 of author 652", "publishedYear": 2006}, {"id": 6652, "isbn": "1", "title": "Book 7 of author 652", "publishedYear": 2020}, {"id": 7652, "isbn": "1", "title": "Book 8 of author 652", "publishedYear": 2004}, {"id": 8652, "isbn": "1", "title": "Book 9 of author 652", "publishedYear": 2010}, {"id": 9652, "isbn": "1", "title": "Book 10 of author 652", "publishedYear": 2022}, {"id": 10652, "isbn": "1", "title": "Book 11 of author 652", "publishedYear": 2001}, {"id": 11652, "isbn": "1", "title": "Book 12 of author 652", "publishedYear": 2010}, {"id": 12652, "isbn": "1", "title": "Book 13 of author 652", "publishedYear": 2024}, {"id": 13652, "isbn": "1", "title": "Book 14 of author 652", "publishedYear": 2021}, {"id": 14652, "isbn": "0", "title": "Book 15 of author 652", "publishedYear": 2004}, {"id": 15652, "isbn": "1", "title": "Book 16 of author 652", "publishedYear": 2001}, {"id": 16652, "isbn": "1", "title": "Book 17 of author 652", "publishedYear": 2001}, {"id": 17652, "isbn": "0", "title": "Book 18 of author 652", "publishedYear": 2020}, {"id": 18652, "isbn": "0", "title": "Book 19 of author 652", "publishedYear": 2009}, {"id": 19652, "isbn": "1", "title": "Book 20 of author 652", "publishedYear": 2000}, {"id": 20652, "isbn": "1", "title": "Book 21 of author 652", "publishedYear": 2000}, {"id": 21652, "isbn": "1", "title": "Book 22 of author 652", "publishedYear": 2013}, {"id": 22652, "isbn": "1", "title": "Book 23 of author 652", "publishedYear": 2012}, {"id": 23652, "isbn": "0", "title": "Book 24 of author 652", "publishedYear": 2014}, {"id": 24652, "isbn": "0", "title": "Book 25 of author 652", "publishedYear": 2001}, {"id": 25652, "isbn": "1", "title": "Book 26 of author 652", "publishedYear": 2016}, {"id": 26652, "isbn": "1", "title": "Book 27 of author 652", "publishedYear": 2014}, {"id": 27652, "isbn": "0", "title": "Book 28 of author 652", "publishedYear": 2024}, {"id": 28652, "isbn": "0", "title": "Book 29 of author 652", "publishedYear": 2016}, {"id": 29652, "isbn": "0", "title": "Book 30 of author 652", "publishedYear": 2012}]
 273 | Author 273  | Bio for author 273  | [{"id": 273, "isbn": "1", "title": "Book 1 of author 273", "publishedYear": 2007}, {"id": 1273, "isbn": "1", "title": "Book 2 of author 273", "publishedYear": 2010}, {"id": 2273, "isbn": "1", "title": "Book 3 of author 273", "publishedYear": 2023}, {"id": 3273, "isbn": "0", "title": "Book 4 of author 273", "publishedYear": 2010}, {"id": 4273, "isbn": "1", "title": "Book 5 of author 273", "publishedYear": 2020}, {"id": 5273, "isbn": "0", "title": "Book 6 of author 273", "publishedYear": 2013}, {"id": 6273, "isbn": "0", "title": "Book 7 of author 273", "publishedYear": 2008}, {"id": 7273, "isbn": "1", "title": "Book 8 of author 273", "publishedYear": 2012}, {"id": 8273, "isbn": "1", "title": "Book 9 of author 273", "publishedYear": 2001}, {"id": 9273, "isbn": "0", "title": "Book 10 of author 273", "publishedYear": 2011}, {"id": 10273, "isbn": "0", "title": "Book 11 of author 273", "publishedYear": 2005}, {"id": 11273, "isbn": "1", "title": "Book 12 of author 273", "publishedYear": 2012}, {"id": 12273, "isbn": "1", "title": "Book 13 of author 273", "publishedYear": 2010}, {"id": 13273, "isbn": "1", "title": "Book 14 of author 273", "publishedYear": 2013}, {"id": 14273, "isbn": "1", "title": "Book 15 of author 273", "publishedYear": 2019}, {"id": 15273, "isbn": "1", "title": "Book 16 of author 273", "publishedYear": 2004}, {"id": 16273, "isbn": "1", "title": "Book 17 of author 273", "publishedYear": 2022}, {"id": 17273, "isbn": "0", "title": "Book 18 of author 273", "publishedYear": 2021}, {"id": 18273, "isbn": "0", "title": "Book 19 of author 273", "publishedYear": 2004}, {"id": 19273, "isbn": "1", "title": "Book 20 of author 273", "publishedYear": 2022}, {"id": 20273, "isbn": "1", "title": "Book 21 of author 273", "publishedYear": 2021}, {"id": 21273, "isbn": "1", "title": "Book 22 of author 273", "publishedYear": 2016}, {"id": 22273, "isbn": "1", "title": "Book 23 of author 273", "publishedYear": 2002}, {"id": 23273, "isbn": "0", "title": "Book 24 of author 273", "publishedYear": 2015}, {"id": 24273, "isbn": "1", "title": "Book 25 of author 273", "publishedYear": 2010}, {"id": 25273, "isbn": "1", "title": "Book 26 of author 273", "publishedYear": 2021}, {"id": 26273, "isbn": "1", "title": "Book 27 of author 273", "publishedYear": 2016}, {"id": 27273, "isbn": "0", "title": "Book 28 of author 273", "publishedYear": 2017}, {"id": 28273, "isbn": "0", "title": "Book 29 of author 273", "publishedYear": 2024}, {"id": 29273, "isbn": "1", "title": "Book 30 of author 273", "publishedYear": 2007}]
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's have a look at the explain plan as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; HashAggregate  (cost=856.58..869.08 rows=1000 width=68) (actual time=68.398..74.411 rows=1000 loops=1)
   Group Key: a.id
   Batches: 1  Memory Usage: 23361kB
   Buffers: shared hit=230
   -&amp;gt;  Hash Join  (cost=31.50..631.58 rows=30000 width=57) (actual time=0.385..7.345 rows=30000 loops=1)
         Hash Cond: (b.author_id = a.id)
         Buffers: shared hit=230
         -&amp;gt;  Seq Scan on books b  (cost=0.00..521.00 rows=30000 width=29) (actual time=0.012..1.821 rows=30000 loops=1)
               Buffers: shared hit=221
         -&amp;gt;  Hash  (cost=19.00..19.00 rows=1000 width=36) (actual time=0.356..0.357 rows=1000 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 77kB
               Buffers: shared hit=9
               -&amp;gt;  Seq Scan on authors a  (cost=0.00..19.00 rows=1000 width=36) (actual time=0.005..0.109 rows=1000 loops=1)
                     Buffers: shared hit=9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same hash join you saw before, plus the operations needed to provide the in-line JSON list of books for every author. Note how we're returning now &lt;strong&gt;only 1000 rows&lt;/strong&gt;, 30 times less than before. On my laptop, it turned out to run just a little bit faster than Option 2 above. However, in a typical 3 tier architecture, where the database is separated from the application by a network, choosing this approach will make all the difference, due to much less data needed to be transported over the network for every user request.&lt;/p&gt;

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

&lt;p&gt;We have seen three ways to retrieve the data we needed and looked closely at what makes one approach more performant than the other. Option 3 is the fastest but also the least portable, because it is based on a native query which leverages Postgres specific functionality. &lt;/p&gt;

&lt;p&gt;I hope you enjoyed reading it, and potentially even applied it in order to make your application faster. &lt;/p&gt;

&lt;p&gt;Thanks for reading! Until next time! &lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>spring</category>
      <category>java</category>
    </item>
    <item>
      <title>How to add rate limiting to your API using TigerBeetle</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Fri, 30 May 2025 19:03:22 +0000</pubDate>
      <link>https://dev.to/mcadariu/how-to-add-rate-limiting-to-a-spring-boot-app-using-tigerbeetle-16mm</link>
      <guid>https://dev.to/mcadariu/how-to-add-rate-limiting-to-a-spring-boot-app-using-tigerbeetle-16mm</guid>
      <description>&lt;p&gt;You should always consider having explicit limits in place when building software. For online services this ensures fair use and also prevents operational headaches. You witnessed the concept in the "real world" as well - in some more busy restaurants, you have only a limited time slot in which to enjoy being seated at a table. &lt;/p&gt;

&lt;p&gt;In this post, I'll show you in detail one solution for adding rate limiting to a Spring Boot API application. For the book-keeping required to make this work we will be using &lt;a href="https://tigerbeetle.com" rel="noopener noreferrer"&gt;TigerBeetle&lt;/a&gt;, a financial transactions OLTP database that recently caught my attention and wanted to try out. As a bonus, I'll show you how to capture and visualise your app's rate limiting capability using Prometheus and Grafana, a common open-source stack for application observability. This &lt;a href="https://github.com/mcadariu/tigerbeetle-spring-boot-ratelimiter" rel="noopener noreferrer"&gt;repo&lt;/a&gt; contains the code I'm about to show you, if you'd like to check it out. Onwards!&lt;/p&gt;

&lt;h2&gt;
  
  
  TigerBeetle
&lt;/h2&gt;

&lt;p&gt;TigerBeetle is a financial transactions database which appeared a couple of years ago. Their ambition is to provide a highly performant and reliable OLTP database for customers operating at massive scale. Reading about their &lt;a href="https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md" rel="noopener noreferrer"&gt;design decisions&lt;/a&gt; is rather captivating and in some way reminds me of the &lt;a href="https://martinfowler.com/articles/lmax.html" rel="noopener noreferrer"&gt;LMAX&lt;/a&gt; architecture. The schema is very simple, by design. The main concept is &lt;a href="https://docs.tigerbeetle.com/concepts/debit-credit/" rel="noopener noreferrer"&gt;debit / credit&lt;/a&gt;. It's a very flexible abstraction which can be applied to many use cases, even outside of the financial domain. After all, right, the idea of a "transaction" is pretty universal. On their website, you can find several recipes which can serve as a starting point of working with it. In the next sections, I will be applying the &lt;a href="https://docs.tigerbeetle.com/coding/recipes/rate-limiting/" rel="noopener noreferrer"&gt;rate limiting&lt;/a&gt; recipe. It's really clear what I have to do upon reading it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;When doing Spring Boot application development, I expect you will most frequently encounter Redis as a backing data store for rate limiting.  The existing integrations make it easy to start using it. You have the option to include &lt;a href="https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestratelimiter-factory.html" rel="noopener noreferrer"&gt;Spring Cloud Gateway&lt;/a&gt; as a dependency and you're off to the races after you configure some things. If you already have experience with Redis, that's a totally fine route to take as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;We start our work, as usual with Spring Boot development, by going to &lt;a href="https://start.spring.io/" rel="noopener noreferrer"&gt;start.spring.io&lt;/a&gt; and selecting &lt;code&gt;Spring Web&lt;/code&gt; as dependency. We'll develop this initial empty shell into a little web application with a single API endpoint. Let's add an initial class which will determine that we do when we get web requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&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;GreetingController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/greeting"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;greeting&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"hello"&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;h2&gt;
  
  
  Intercepting requests
&lt;/h2&gt;

&lt;p&gt;Now, we want to add rate limiting to this endpoint. This means that we have to hook into the Spring request handling mechanism and inject our rate limiting logic between the point where the request is received and when it's handed over to the &lt;code&gt;GreetingController&lt;/code&gt;. We do this by creating a class which implements the &lt;code&gt;HandlerInterceptor&lt;/code&gt; interface and then providing it to the &lt;code&gt;InterceptorRegistry&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rateLimitInterceptor&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When constructing the interceptor we have to provide the TigerBeetle &lt;a href="https://docs.tigerbeetle.com/coding/clients/java" rel="noopener noreferrer"&gt;client&lt;/a&gt; and the observation registry as collaborating services for the rate limiting. At this point, you might want to get an introduction to the observability registry and all the other related topics, I recommend &lt;a href="https://spring.io/blog/2022/10/12/observability-with-spring-boot-3" rel="noopener noreferrer"&gt;this post&lt;/a&gt; from the Spring blog for getting familiarised about how the integration between Spring Boot and the observability stack works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@RequestScope&lt;/span&gt;
&lt;span class="nc"&gt;HandlerInterceptor&lt;/span&gt; &lt;span class="nf"&gt;rateLimitInterceptor&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RateLimitInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observationRegistry&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;The logic for performing the rate limiting will be in the implementation of the &lt;code&gt;preHandle&lt;/code&gt; method which is part of the &lt;code&gt;HandlerInterceptor&lt;/code&gt; interface. &lt;/p&gt;

&lt;p&gt;Note that this means &lt;em&gt;all&lt;/em&gt; your endpoints will be subject to rate limiting. If you want to, you can define a list of exceptions, or create custom annotations which you will apply to specific endpoints for more fine-grain control. But for this post, we're keeping it simple. &lt;/p&gt;

&lt;h2&gt;
  
  
  Every request means a debit
&lt;/h2&gt;

&lt;p&gt;Let us now define two accounts: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the operator &lt;/li&gt;
&lt;li&gt;the user &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operator is responsible to initialise the user accounts with a finite amount from our application will deduct a finite amount when handling every request from that particular user. In addition, the user account has the following important restriction: the debits must not exceed the credits. For every request, we will make a transfer from the user to the operator, but if the limit is reached, we will short-circuit the request from proceeding as usual and return with &lt;code&gt;429&lt;/code&gt; ("Too Many Requests") response code right away. &lt;/p&gt;

&lt;p&gt;Worth mentioning, is that the general idea is we can represent any kind of resource we are interested in rate limiting, such as an IP, customer, etc. &lt;/p&gt;

&lt;p&gt;Here is how the creation of the user account looks. The &lt;code&gt;USER_ID&lt;/code&gt; is just a generated random integer, however you can imagine that in a real system it's retrieved from something like the an authentication system. In the reference &lt;a href="https://docs.tigerbeetle.com/coding/system-architecture/" rel="noopener noreferrer"&gt;system architecture&lt;/a&gt;, this would be what is depicted as the OLGP database (e.g. Postgres).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;   &lt;span class="nc"&gt;AccountBatch&lt;/span&gt; &lt;span class="n"&gt;accountBatch&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;AccountBatch&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="n"&gt;accountBatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
   &lt;span class="n"&gt;accountBatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="n"&gt;accountBatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLedger&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="n"&gt;accountBatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCode&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="n"&gt;accountBatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setFlags&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DEBITS_MUST_NOT_EXCEED_CREDITS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAccounts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountBatch&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the interface is modelled around batching. This comes back to the performance as a first class principle in TigerBeetle. With batching, we amortise the cost of overhead. Given the use-case we're tackling here, our batch is limited to one account, but normally you would have more.&lt;/p&gt;

&lt;p&gt;Onto the method we use to perform a transfer. It is invoked on every web request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;CreateTransferResultBatch&lt;/span&gt; &lt;span class="nf"&gt;makeTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;debitAcct&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;creditAcct&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;TransferBatch&lt;/span&gt; &lt;span class="n"&gt;transfer&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;TransferBatch&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="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setId&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;Random&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDebitAccountId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debitAcct&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCreditAccountId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creditAcct&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLedger&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="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCode&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="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAmount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setFlags&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTransfers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&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;The &lt;code&gt;flag&lt;/code&gt; and &lt;code&gt;timeout&lt;/code&gt; parameters are needed because for every user requests, we will create a "pending" transfer (this is a type of flag). This means it expires after &lt;code&gt;timeout&lt;/code&gt; seconds. This makes it so that the allowance will replenish after a configurable period, which we want to happen. &lt;/p&gt;

&lt;p&gt;On the first request by a user, we have to initialise the account, by doing a transfer from the operator to the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="n"&gt;makeTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
   &lt;span class="no"&gt;USER_CREDIT_INITIAL_AMOUNT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
   &lt;span class="no"&gt;OPERATOR_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
   &lt;span class="no"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
   &lt;span class="mi"&gt;0&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every intercepted request, we perform a deduction from the user's account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="nc"&gt;CreateTransferResultBatch&lt;/span&gt; &lt;span class="n"&gt;transferErrors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
   &lt;span class="n"&gt;makeTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
     &lt;span class="no"&gt;PER_REQUEST_DEDUCTION&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
     &lt;span class="no"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
     &lt;span class="no"&gt;OPERATOR_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
     &lt;span class="no"&gt;TIMEOUT_IN_SECONDS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
     &lt;span class="no"&gt;PENDING&lt;/span&gt;
   &lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the above operation returns an error of type &lt;code&gt;ExceedsCredits&lt;/code&gt; (one of the values of the &lt;code&gt;CreateTransferResult&lt;/code&gt; enum), this means that we will not let this request to proceed. We will send an observation towards our observability stack, set an attribute on the current tracing span, as well as set the response code to &lt;code&gt;429&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nc"&gt;Observation&lt;/span&gt; &lt;span class="n"&gt;observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ratelimit"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observationRegistry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"limited"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
  &lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;highCardinalityKeyValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;of&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;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
  &lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="nc"&gt;Span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setAttribute&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;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

  &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TOO_MANY_REQUESTS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;So far so good. Let's write a Spring Boot test in which we assert that what I've described above actually happens as we expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.tigerbeetle_ratelimiter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;micrometer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;observation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tck&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TestObservationRegistryAssert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;core&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webEnvironment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;WebEnvironment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RANDOM_PORT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Testcontainers&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RatelimiterApplicationTests&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/greeting"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Container&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;DockerComposeContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;environment&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;DockerComposeContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;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="s"&gt;"docker-compose.yml"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;TestRestTemplate&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;TestObservationRegistry&lt;/span&gt; &lt;span class="n"&gt;observationRegistry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;contextLoads&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="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;shouldRejectRequestsBeyondRateLimit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;0&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;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;USER_CREDIT_INITIAL_AMOUNT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="no"&gt;PER_REQUEST_DEDUCTION&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="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getForEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// The next request should be rate limited&lt;/span&gt;
        &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getForEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusCode&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;isEqualTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TOO_MANY_REQUESTS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;observationRegistry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasObservationWithNameEqualTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ratelimit"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasBeenStarted&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasBeenStopped&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@TestConfiguration&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ObservationTestConfiguration&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nd"&gt;@Bean&lt;/span&gt;
        &lt;span class="nc"&gt;TestObservationRegistry&lt;/span&gt; &lt;span class="nf"&gt;observationRegistry&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TestObservationRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&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;Time to show what happens when we run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Showtime
&lt;/h2&gt;

&lt;p&gt;In production environments, TigerBeetle is normally deployed as a cluster of multiple replicas. However, given that we're just experimenting with it locally, we'll start a single instance, fully accepting that it is not set up in a highly available fashion and we will not do this in production. &lt;/p&gt;

&lt;p&gt;Let's format the data file first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--security-opt&lt;/span&gt; &lt;span class="nv"&gt;seccomp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unconfined &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/data ghcr.io/tigerbeetle/tigerbeetle &lt;span class="se"&gt;\&lt;/span&gt;
    format &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;--replica&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;--replica-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 /data/0_0.tigerbeetle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You observed that as a result, a folder was created called &lt;code&gt;data&lt;/code&gt; having a file in it called &lt;code&gt;0_0.tigerbeetle&lt;/code&gt;. This single file is where the TigerBeetle replica will store its our rate limiting book-keeping data.&lt;/p&gt;

&lt;p&gt;We're now ready to start our docker-compose setup where everything is wired up and ready to go. &lt;/p&gt;

&lt;p&gt;We will first install the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./mvnw &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, we are ready to start our full environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all services started correctly, we're in business! &lt;/p&gt;

&lt;h2&gt;
  
  
  Load testing
&lt;/h2&gt;

&lt;p&gt;As a next step, let's set up some requests that will hit the endpoint. &lt;a href="https://k6.io/" rel="noopener noreferrer"&gt;k6s&lt;/a&gt; is a tool for doing load testing which is very handy for these situations. It's easy to work with it - you write javascript code to describe the load you want to generate and it will proceed to execute it against your target when you run it.&lt;/p&gt;

&lt;p&gt;This is the contents of the k6s script. We'll issue 3000 requests within a span of 30 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k6/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;vus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://host.docker.internal:8080/greeting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status is 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll now run the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; grafana/k6 run - &amp;lt;script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After 30 seconds, we get the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; █ TOTAL RESULTS 

    checks_total.......................: 3000   97.661337/s
    checks_succeeded...................: 16.80% 504 out of 3000
    checks_failed......................: 83.20% 2496 out of 3000

    ✗ status is 200
      ↳  16% — ✓ 504 / ✗ 2496

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, there are more requests which were rate limited than successful ones. We were not kidding. We applied quite a high deduction per request, but we might want to get our foot off the brakes in the context of a real app!&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualising rate limiting
&lt;/h2&gt;

&lt;p&gt;Moving over to Grafana. I've prepared a pre-configured dashboard for your convenience which we'll now open up and have a look. Let's go to &lt;code&gt;localhost:3000&lt;/code&gt; and fill in &lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;admin&lt;/code&gt; as credentials, and then click &lt;code&gt;Skip&lt;/code&gt; when asked about changing the password. Then, on the left side of the screen, click on &lt;code&gt;Dashboards&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%2Fdhr04jgfdq3o0lx8iw4i.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%2Fdhr04jgfdq3o0lx8iw4i.png" alt="dashboard" width="618" height="724"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll then see our preconfigured dashboard called &lt;code&gt;Rate limiting&lt;/code&gt;. Click on it and you will see the following:&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%2Fkl4tv5rs55nqqwq0cd4d.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%2Fkl4tv5rs55nqqwq0cd4d.png" alt="dash" width="800" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alright, time to have a look at the request traces. These show you the "path" taken by the request through our code. This is where you can find them in the menu. &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%2Fek5jt065ty0ukpq42bc4.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%2Fek5jt065ty0ukpq42bc4.png" alt="traces" width="600" height="1082"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the lower part of the next screen you will see some outstanding green dots. Those are so-called &lt;a href="https://grafana.com/docs/grafana/latest/fundamentals/exemplars/" rel="noopener noreferrer"&gt;exemplars&lt;/a&gt;. Metrics give you an aggregated perspective of what you're tracking, but with exemplars you can drill down to understand particular single instances. Here's how one looks like. I have highlighted the span attribute representing the user ID which we set in the Java code you've seen earlier.&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%2Fta6lomjv3zn1sj9or3ap.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%2Fta6lomjv3zn1sj9or3ap.png" alt="exemplars" width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The End
&lt;/h2&gt;

&lt;p&gt;Like I've mentioned before, having limits in place for everything is a good thing. Same goes for this post! 😀 &lt;br&gt;
So - that's all I have for you today, hope you enjoyed it and thanks for reading.&lt;/p&gt;

&lt;p&gt;Time to clean up by tearing down our setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose down -v
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks - until next time! &lt;/p&gt;

&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@spencer_demera?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Spencer DeMera&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-speed-limit-sign-sitting-on-the-side-of-a-road-uP_dMJpq2WA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>database</category>
      <category>java</category>
      <category>grafana</category>
    </item>
    <item>
      <title>High speed data loading into Postgres</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Tue, 04 Feb 2025 21:25:03 +0000</pubDate>
      <link>https://dev.to/mcadariu/high-speed-data-loading-in-postgres-3635</link>
      <guid>https://dev.to/mcadariu/high-speed-data-loading-in-postgres-3635</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In this post I'll show you how to speed up data loading in Postgres. Using a worked example, we'll start at half an hour runtime and end up with a version which is done in half a &lt;em&gt;minute&lt;/em&gt;. Step by step, we'll get it &lt;code&gt;~60x&lt;/code&gt; faster. This is so you wait less and can jump straight to querying your data right away.&lt;/p&gt;

&lt;p&gt;In the sections below we'll apply each of the above steps in order, as well as understand &lt;em&gt;why&lt;/em&gt; they speed up the data loading.&lt;/p&gt;

&lt;p&gt;I'm using Postgres on my laptop (Apple MacBook Pro M1 and 32GB RAM).  &lt;/p&gt;

&lt;h2&gt;
  
  
  Tables
&lt;/h2&gt;

&lt;p&gt;If you've checked out my other posts, you'll recognise familiar tables. It's the same ones I used for &lt;a href="https://dev.to/mcadariu/retrieving-the-latest-row-per-group-in-postgresql-247d"&gt;this&lt;/a&gt; post. We have &lt;code&gt;meters&lt;/code&gt; and their &lt;code&gt;readings&lt;/code&gt; stored in their respective tables, here's what they look like:&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;meters&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;  &lt;span class="n"&gt;uuid&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;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;

    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;uuid&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;meter_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rating&lt;/span&gt;       &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;         &lt;span class="nb"&gt;date&lt;/span&gt;

    &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;fk__readings_meters&lt;/span&gt; &lt;span class="k"&gt;foreign&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;meter_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;meters&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll be inserting 15000 meters with one reading every day for each one, for 5 years.&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;meters&lt;/span&gt; 
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;uuidv4&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;15000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&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;readings&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="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reading&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;uuidv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&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="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&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="s1"&gt;'2019-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm letting this run, and after a (rather long) while, it's done (in &lt;code&gt;2098.31s&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;It's just our starting point, we'll get it much faster. Buckle up!&lt;/p&gt;

&lt;h2&gt;
  
  
  UUID v7
&lt;/h2&gt;

&lt;p&gt;Let's start by focusing on the primary key. The UUID v4 is not the best when it comes to insert performance. I elaborated why that is in &lt;a href="https://dev.to/mcadariu/using-uuids-as-primary-keys-3e7a"&gt;another post&lt;/a&gt;, but the gist is that its randomness cause a lot of page modifications. The database has to do a lot of work in order to keep the B-tree balanced after every tuple inserted. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=78c5e141e9c139fc2ff36a220334e4aa25e1b0eb" rel="noopener noreferrer"&gt;Recently&lt;/a&gt;, Postgres got support for &lt;a href="https://uuid7.com/" rel="noopener noreferrer"&gt;UUID v7&lt;/a&gt;! It will be available in version 18, however we can already use it if we work with the source code directly. These are time-sortable identifiers, which means insertions will "affect" only a isolated and specific part of the B-tree instead of everything. This means much less work for the database to do. Let's give this a try.&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;meters&lt;/span&gt; 
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;uuidv7&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;15000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&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;readings&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="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reading&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;uuidv7&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&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="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&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="s1"&gt;'2019-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check this out - with this, we've reduced the time to more than half! It finished in &lt;code&gt;821.50s&lt;/code&gt;. So far so good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numeric IDs
&lt;/h2&gt;

&lt;p&gt;Let's try something else. The UUIDs themselves are generated before insertion (the call to &lt;code&gt;uuidv7()&lt;/code&gt;). Let's then replace the UUID primary key with a numeric one, which does not have to be generated, as it will be just read from a sequence. In addition, the corresponding data type (&lt;code&gt;bigint&lt;/code&gt;) will be half the size of a UUID. Sounds good, let's see where this brings us.&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;meters&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;bigint&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;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;readings&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;bigint&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;meter_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reading&lt;/span&gt;      &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;         &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;fk__readings_meters&lt;/span&gt; &lt;span class="k"&gt;foreign&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;meter_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;meters&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the updated script:&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;meters&lt;/span&gt; 
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;seq&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;15000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&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;readings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reading&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;m&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="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&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="s1"&gt;'2019-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets us a little further indeed! We're at &lt;code&gt;646.73s&lt;/code&gt;. So, a bit over 10 minutes. This is great, but we've still got work to do - remember, we're eventually going to get it about 20 times faster than this.&lt;/p&gt;

&lt;p&gt;Let's move on to doing some configuration tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared buffers
&lt;/h2&gt;

&lt;p&gt;Postgres uses its shared memory buffers to make reads and writes more efficient. It is a set of 8kb pages in memory which are used in order to avoid doing slower disk operations all the time.&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%2F0daet0wtmnw6mmmv8cm4.jpg" 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%2F0daet0wtmnw6mmmv8cm4.jpg" alt="shared-buffers" width="588" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we don't size the shared buffers correctly, we can expect a lot of &lt;a href="https://www.dbi-services.com/blog/postgresql-16-playing-with-pg_stat_io-2-evictions/" rel="noopener noreferrer"&gt;evictions&lt;/a&gt; during our data loading, slowing it down. The default setting of &lt;code&gt;128 MB&lt;/code&gt; is low compared to how much data we're inserting (~2GB), so I'll increase the shared buffers accordingly.&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;system&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;shared_buffers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2GB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Time to run it again. As expected, this brought us closer to our goal. We're now at &lt;code&gt;595.33s&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Full page writes
&lt;/h2&gt;

&lt;p&gt;As mentioned above, Postgres works with 8kb pages, however the OS and the disk do not (e.g. in Linux the page is 4kb, and a sector on disk is 512 bytes). This can lead to - in the event of a power failure - pages being only partially written. This would prevent Postgres from being able to do its data recovery, because it relies on the fact that the pages are not corrupted in any way when it starts its recovery protocol. The solution to this is that after every &lt;a href="https://www.postgresql.org/docs/current/sql-checkpoint.html" rel="noopener noreferrer"&gt;checkpoint&lt;/a&gt;, at the first update of a page, the full page is written instead of only the changes as is the common case.  &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%2Fhqxhn3vwkvek4ok6ax41.jpg" 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%2Fhqxhn3vwkvek4ok6ax41.jpg" alt="full-page-writes" width="330" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the sake of experimentation let's shut it off, however I do not recommend doing this in production, except only on a temporary basis strictly for the data loading.&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;system&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;full_page_writes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmm, well, it got us to &lt;code&gt;590.01s&lt;/code&gt;. It's not that much, but we'll take it! &lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints
&lt;/h2&gt;

&lt;p&gt;Next up, we'll remove the table constraints. From the script above, I'll remove the the &lt;code&gt;fk__readings_meters&lt;/code&gt; foreign key constraint. The database has to do less work because there's no more checking this at runtime.&lt;/p&gt;

&lt;p&gt;Quite a difference this made with this. We're now at &lt;code&gt;150.71s&lt;/code&gt;. This is the biggest gain so far. &lt;/p&gt;

&lt;h2&gt;
  
  
  Indexes
&lt;/h2&gt;

&lt;p&gt;We're onto something. I'll now remove the indexes as well. This means no more updating the index after every tuple inserted. By the way, we're dropping constraints and indexes but only temporarily. You can always recreate them after the data loading finished successfully.  &lt;/p&gt;

&lt;p&gt;These are my tables now.&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;meters&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;bigint&lt;/span&gt; 
&lt;span class="p"&gt;);&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;readings&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;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;meter_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reading&lt;/span&gt;      &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;         &lt;span class="nb"&gt;date&lt;/span&gt;

&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've ran the same import script as above, and now we're at &lt;code&gt;109.51s&lt;/code&gt;. Great stuff! Can we get it under 100s?&lt;/p&gt;

&lt;h2&gt;
  
  
  Unlogged tables
&lt;/h2&gt;

&lt;p&gt;Sure thing! But we'll have to make some more concessions. For example, for the rest of the experiment I'll be using &lt;a href="https://www.crunchydata.com/blog/postgresl-unlogged-tables" rel="noopener noreferrer"&gt;unlogged&lt;/a&gt; tables. Again, not a setting to keep on for production beyond strictly the data loading procedure. This is because this way the database does not ensure durability anymore because we've disabled the write-ahead logging facility.&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="n"&gt;unlogged&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;meters&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;bigint&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;unlogged&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;readings&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;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;meter_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reading&lt;/span&gt;      &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;         &lt;span class="nb"&gt;date&lt;/span&gt;

&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm now at &lt;code&gt;40.65s&lt;/code&gt;. Believe it or not, we're not done yet here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy
&lt;/h2&gt;

&lt;p&gt;The copy command is &lt;a href="https://wiki.postgresql.org/wiki/COPY" rel="noopener noreferrer"&gt;the Postgres method for data loading&lt;/a&gt;. Let's give it a go.&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="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;copy&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;path_to_file&amp;gt;/readings.csv'&lt;/span&gt; &lt;span class="k"&gt;delimiter&lt;/span&gt; &lt;span class="s1"&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 finishes in &lt;code&gt;35.41s&lt;/code&gt;. Amazing! &lt;/p&gt;

&lt;p&gt;Here are all our results in one view:&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%2F4o3b830k3fox3j8gkhr5.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%2F4o3b830k3fox3j8gkhr5.png" alt="chart" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's quite a difference from when we started out. As expected, using COPY lead to the shortest time. But what was interesting to see is the difference it made when we dropped the constraints, compared with the other changes. &lt;/p&gt;

&lt;p&gt;I want to add that I've experimented with &lt;a href="**https://www.enterprisedb.com/blog/basics-tuning-checkpoints"&gt;checkpoint tuning&lt;/a&gt; as well. It didn't yield any notable improvements for this experiment. Nonetheless, you might want to keep it in mind as it can affect performance if misconfigured.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@flo_stk?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Florian Steciuk&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/time-lapse-photography-of-highway-F7Rl02ir0Gg?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>performance</category>
      <category>sql</category>
    </item>
    <item>
      <title>Hierarchical data with Postgres and Spring Data JPA</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Thu, 31 Oct 2024 16:08:50 +0000</pubDate>
      <link>https://dev.to/mcadariu/hierarchical-data-with-postgresql-and-spring-data-jpa-3b42</link>
      <guid>https://dev.to/mcadariu/hierarchical-data-with-postgresql-and-spring-data-jpa-3b42</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;He who plants a tree,&lt;br&gt;
     Plants a hope.&lt;br&gt;
           &lt;em&gt;Plant a tree&lt;/em&gt; by Lucy Larcom 🌳 &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In this post I'm going to show you a couple of options for managing hierarchical data represented as a &lt;strong&gt;tree&lt;/strong&gt; structure. This applies when you need to implement things like: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file system paths &lt;/li&gt;
&lt;li&gt;org charts &lt;/li&gt;
&lt;li&gt;discussion forum comments&lt;/li&gt;
&lt;li&gt;a more contemporary topic: small2big retrieval for RAG applications &lt;a href="https://towardsdatascience.com/advanced-rag-01-small-to-big-retrieval-172181b396d4" rel="noopener noreferrer"&gt;[1]&lt;/a&gt;&lt;a href="https://blog.lancedb.com/modified-rag-parent-document-bigger-chunk-retriever-62b3d1e79bc6/" rel="noopener noreferrer"&gt;[2]&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, if you know what a graph is already, a tree is basically a graph &lt;strong&gt;without any cycles&lt;/strong&gt;. Visually, it looks like this:&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%2F1tk1sk9i743nrz8s04m9.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%2F1tk1sk9i743nrz8s04m9.png" alt="tree" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are multiple alternatives for storing trees in relational databases. In the sections below, I'll show you three ways of doing it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;adjacency list&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;materialized paths&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nested sets&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There will be two parts to this blog post. In this first one I show you how to load and store data using the above approaches - the basics. Having that out of the way, in the second part, the focus is more on their comparison and trade-offs, for example I want to look at what happens at increased data volumes and what are the appropriate indexing strategies. &lt;/p&gt;

&lt;p&gt;All the code you'll see in the sections below can be found &lt;a href="https://github.com/mcadariu/hierarchical-trees-demo" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The running use-case I picked will be &lt;strong&gt;employees&lt;/strong&gt; and their &lt;strong&gt;managers&lt;/strong&gt;, and the IDs for each will be exactly the ones you saw in the tree visualisation I showed above. &lt;/p&gt;

&lt;h2&gt;
  
  
  Local environment
&lt;/h2&gt;

&lt;p&gt;I'm using the recently released &lt;strong&gt;Postgres 17&lt;/strong&gt; with &lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;Testcontainers&lt;/a&gt;. This gives me a repeatable setup to work with on my laptop. For example, we can provide initialisation SQL scripts. I use this to automate the creation of a Postgres database with the necessary tables and populate with test data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@TestConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxyBeanMethods&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestcontainersConfiguration&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;POSTGRES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@ServiceConnection&lt;/span&gt;
    &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;postgresContainer&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;DockerImageName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres:latest"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;POSTGRES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;POSTGRES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDatabaseName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;POSTGRES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withInitScript&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"init-script.sql"&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;Let's jump in and have a look at the first approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The adjacency list model
&lt;/h2&gt;

&lt;p&gt;This was the first solution for managing hierarchical data, so we can expect that it's still widely present in codebases - chances are, you might encounter it sometime. The idea is that we store the manager's, or more generically said, the "parent ID" in the same row. &lt;/p&gt;

&lt;h3&gt;
  
  
  Schema
&lt;/h3&gt;

&lt;p&gt;Let's have a look at the table structure.&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;employees&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;bigserial&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;manager_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I omitted them here, but in order to ensure data integrity, we should also write constraint checks that ensure at least the following: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there is a single parent for every node&lt;/li&gt;
&lt;li&gt;no cycles&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Generating test data
&lt;/h3&gt;

&lt;p&gt;Especially for Part 2 of this post, we need a way to generate as much data as we want for populating the tables. Let's do it at first step by step for more clarity, then afterwards recursively.&lt;/p&gt;

&lt;h4&gt;
  
  
  Iteration 1 - step by step
&lt;/h4&gt;

&lt;p&gt;We start simple by explicitly inserting three levels of employees in the hierarchy. &lt;/p&gt;

&lt;p&gt;Now, you might know already about &lt;a href="https://www.postgresql.org/docs/current/queries-with.html" rel="noopener noreferrer"&gt;CTEs&lt;/a&gt; in Postgres - they are auxiliary queries executed within the context of a main query. Below, you can see how I construct each level on the basis of the level before.&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;with&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&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;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager_id&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="k"&gt;select&lt;/span&gt; 
        &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="s1"&gt;'root'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&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;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;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
      &lt;span class="n"&gt;returning&lt;/span&gt; 
        &lt;span class="n"&gt;employees&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="n"&gt;first_level&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&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;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager_id&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="k"&gt;select&lt;/span&gt; 
          &lt;span class="n"&gt;root&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="s1"&gt;'first_level'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&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;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;2&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="n"&gt;root&lt;/span&gt;
        &lt;span class="n"&gt;returning&lt;/span&gt; 
          &lt;span class="n"&gt;employees&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="n"&gt;second_level&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&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;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager_id&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="k"&gt;select&lt;/span&gt; 
          &lt;span class="n"&gt;first_level&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="s1"&gt;'second_level'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&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;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;2&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="n"&gt;first_level&lt;/span&gt;
        &lt;span class="n"&gt;returning&lt;/span&gt; 
          &lt;span class="n"&gt;employees&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;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; 
  &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager_id&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="k"&gt;select&lt;/span&gt; 
  &lt;span class="n"&gt;second_level&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="s1"&gt;'third_level'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&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;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;2&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="n"&gt;second_level&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cool. Let's now verify that it works as expected. We do a count to see how many elements have been inserted. You can compare it with the number of nodes in the tree visualisation I showed at the beginning of this post.&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;postgres&lt;/span&gt;&lt;span class="o"&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="o"&gt;*&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;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt; 
&lt;span class="c1"&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;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks alright! Three levels, and in total we get 15 nodes.&lt;/p&gt;

&lt;p&gt;Time to move on to the recursive approach. This is needed for Part 2 of this post, where we want to generate much larger volume of data.&lt;/p&gt;

&lt;h4&gt;
  
  
  Iteration 2 - recursive
&lt;/h4&gt;

&lt;p&gt;Writing recursive queries follows a standard procedure similar to how it works in regular software development. We define a &lt;strong&gt;base step&lt;/strong&gt; and a &lt;strong&gt;recursive step&lt;/strong&gt; then "connect" them to each other using &lt;code&gt;union all&lt;/code&gt;. At runtime Postgres will follow this recipe and generate all our results. Have a look.&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;temporary&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="n"&gt;employees_id_seq&lt;/span&gt;&lt;span class="p"&gt;;&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;employees&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="n"&gt;manager_id&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="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;recursive&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;level&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="k"&gt;AS&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;nextval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'employees_id_seq'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&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="s1"&gt;'root'&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;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;

    &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; 
      &lt;span class="n"&gt;nextval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'employees_id_seq'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;bigint&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="k"&gt;level&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="s1"&gt;'level'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&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;from&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;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;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; 
      &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&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="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;parent_id&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;from&lt;/span&gt; 
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="n"&gt;employees_id_seq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running it, let's do a count again to see if the same number of elements are inserted.&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;postgres&lt;/span&gt;&lt;span class="o"&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="o"&gt;*&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;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt; 
&lt;span class="c1"&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;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cool! We're in business. We can now populate the schema with however many levels and elements we want, and thus, completely control the inserted volume. No worries if for now recursive queries look a bit hard to grasp still, we'll actually revisit them a bit later with the occasion of writing the queries to retrieve the data.&lt;/p&gt;

&lt;p&gt;For now, let's proceed to have a look at the Hibernate entity we can use to map our table to a Java class. This is it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&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;"employees"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Getter&lt;/span&gt;
&lt;span class="nd"&gt;@Setter&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;Employee&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FetchType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LAZY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@JoinColumn&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;"manager_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;Employee&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"parent"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cascade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CascadeType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;orphanRemoval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;employees&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing special, you saw this coming, just a one-to-many relationship between managers and employees. Let's start querying! &lt;/p&gt;

&lt;h2&gt;
  
  
  Descendants (top-down)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;All subordinates of a manager&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For retrieving all &lt;code&gt;employees&lt;/code&gt; which are the subordinates of a specific manager referenced by her ID, we'll write a recursive query again. You'll see again a &lt;strong&gt;base step&lt;/strong&gt; and a &lt;strong&gt;recursive step&lt;/strong&gt; that is linked up with the base step. Postgres will then repeat this and retrieve all the need rows to satisfy the query. Let's take the employee with ID = 2 for example. This is a visual representation that I found helpful which helped me to understand how it works. I haven't included all the output results you'd get, just the first few to show the principle.&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%2Fuv77vqdrm9vrd38ivjyj.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%2Fuv77vqdrm9vrd38ivjyj.png" alt="tree" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the JPQL query for querying descendants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
 with employeeRoot as (
  select
    employee.employees employee
  from
    Employee employee
  where
    employee.id = :employeeId

  union all

  select
    employee.employees employee
  from
    Employee employee
  join
    employeeRoot root ON employee = root.employee
  order by
    employee.id
  )
  select 
    new Employee(
     root.employee.id
   )
  from 
  employeeRoot root
 """&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setParameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"employeeId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employeeId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResultList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to make the queries cleaner by not needing to write the fully qualified name of the record we write the results into, we can use the &lt;a href="https://github.com/vladmihalcea/hypersistence-utils" rel="noopener noreferrer"&gt;hypersistence-utils&lt;/a&gt; library to write a &lt;code&gt;ClassImportIntegratorProvider&lt;/code&gt;, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClassImportIntegratorProvider&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;IntegratorProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Integrator&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getIntegrators&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ClassImportIntegrator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;singletonList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                                &lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&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;h3&gt;
  
  
  Important: reviewing the generated queries
&lt;/h3&gt;

&lt;p&gt;It works, but let's have a deeper look at what Hibernate generated. It's always good to understand what's happening under the hood, otherwise we might incur inefficiencies that will happen with every user request - this will add up.&lt;/p&gt;

&lt;p&gt;For this, we'll start the Spring Boot app with the following setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@DynamicPropertySource&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;registerPgProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DynamicPropertyRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.jpa.show_sql"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&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;Alright, let's have a look. Here's the query for the descendants generated by Hibernate.&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;with&lt;/span&gt; &lt;span class="k"&gt;recursive&lt;/span&gt; &lt;span class="n"&gt;employeeRoot&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;e1_0&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;employees&lt;/span&gt; &lt;span class="n"&gt;eal1_0&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; 
  &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;eal1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;eal1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=?&lt;/span&gt;

&lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&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;e2_0&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;employees&lt;/span&gt; &lt;span class="n"&gt;eal2_0&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; 
  &lt;span class="n"&gt;employeeRoot&lt;/span&gt; &lt;span class="n"&gt;root1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;eal2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; 
  &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e2_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;eal2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&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;eal2_0&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; 
  &lt;span class="n"&gt;root2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
  &lt;span class="n"&gt;employeeRoot&lt;/span&gt; &lt;span class="n"&gt;root2_0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmm - looks like there's some extra steps in here! Let's see if we can simplify it a bit, keeping in mind the picture I showed you earlier about the base step and the recursive step linked with the base step. We shouldn't need to do more than that. See what you think of the following:&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;with&lt;/span&gt; &lt;span class="k"&gt;recursive&lt;/span&gt; &lt;span class="n"&gt;employeeRoot&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;e1_0&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;employees&lt;/span&gt; &lt;span class="n"&gt;e1_0&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; 
  &lt;span class="n"&gt;e1_0&lt;/span&gt;&lt;span class="p"&gt;.&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="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&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;e2_0&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;employees&lt;/span&gt; &lt;span class="n"&gt;e2_0&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; 
  &lt;span class="n"&gt;employeeRoot&lt;/span&gt; &lt;span class="n"&gt;root1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;e2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_id&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;e2_0&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; 
  &lt;span class="n"&gt;root2_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
  &lt;span class="n"&gt;employeeRoot&lt;/span&gt; &lt;span class="n"&gt;root2_0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better! We removed some unnecessary joins. This is expected to make the query go faster because it will have less work to do. &lt;/p&gt;

&lt;h4&gt;
  
  
  Final result
&lt;/h4&gt;

&lt;p&gt;As a final step let's clean up the query above and replace the table names that Hibernate adds with ones that are more human readable.&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;with&lt;/span&gt; &lt;span class="k"&gt;recursive&lt;/span&gt; &lt;span class="n"&gt;employee_root&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
  &lt;span class="n"&gt;employees&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="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&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;employees&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="n"&gt;employees&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;from&lt;/span&gt; 
  &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; 
  &lt;span class="n"&gt;employee_root&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;employee_root&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;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; 
  &lt;span class="n"&gt;employees&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="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="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
  &lt;span class="n"&gt;employee_root&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;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, time to see how we go "up" the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ancestors (bottom-up)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;All managers up the chain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's first try to write down the conceptual steps for getting the managers of employee with ID = 14.&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%2F0fzt7mk52i9c4u6ufnue.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%2F0fzt7mk52i9c4u6ufnue.png" alt="tree" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looks very much like the one for the descendants you saw above, just the connection between the base step and the recursive step is inverted.&lt;/p&gt;

&lt;p&gt;We can write the JPQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
   with employeeRoot as (
     select
       employee.id           as employeeId,
       employee.manager.id   as manager_id
     from
       Employee employee
     where
       employee.id = :employeeId

     union all

     select
       employee.id          as pid,
       employee.manager.id  as manager_id
     from
       Employee employee
     join
       employeeRoot root on employee.id = root.manager_id
     order by
       employee.id
      )
    select 
      new Employee(root.employeeId)
    from 
      employeeRoot root
   """&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
   &lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;
 &lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setParameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"employeeId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employeeId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResultList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! I have looked at the SQL query generated but I could not find any extra commands that I could shave off like before. Time to move on to approach 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Materialized paths
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ltree&lt;/code&gt; is a Postgres extension we can use to work with hierarchical tree structures as materialized paths (starting from the top of the tree). For example, this is how we will record the one path: &lt;code&gt;1.2.4.8&lt;/code&gt;. There are several useful functions it &lt;a href="https://www.postgresql.org/docs/current/ltree.html#LTREE-OPS-FUNCS" rel="noopener noreferrer"&gt;comes with&lt;/a&gt;. We can use it as a table column:&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;employees_ltree&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="n"&gt;bigserial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;      &lt;span class="n"&gt;ltree&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to populate the above table with test data, the approach I took is basically migrate the generated data from the table used for the adjacency list you saw before, using the following SQL command. It's again a recursive query which collects elements into an accumulator at every step.&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;with&lt;/span&gt; &lt;span class="k"&gt;recursive&lt;/span&gt; &lt;span class="n"&gt;leafnodes&lt;/span&gt; &lt;span class="k"&gt;AS&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="n"&gt;id&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;leaves&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt;
        &lt;span class="n"&gt;employees&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;not&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;manager_id&lt;/span&gt;
        &lt;span class="k"&gt;from&lt;/span&gt;
            &lt;span class="n"&gt;employees&lt;/span&gt;
        &lt;span class="k"&gt;where&lt;/span&gt;
            &lt;span class="n"&gt;manager_id&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="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;chain&lt;/span&gt; &lt;span class="k"&gt;as&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;employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;employees&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="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[]::&lt;/span&gt;&lt;span class="nb"&gt;bigint&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;descendants&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt;
        &lt;span class="n"&gt;employees&lt;/span&gt;

    &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt;
        &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;employees&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;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descendants&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt;
        &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;chain&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt;
        &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt;
&lt;span class="p"&gt;)&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;employees_ltree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;descendants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;ltree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt;
    &lt;span class="k"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;leafnodes&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
    &lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;
    &lt;span class="n"&gt;leaves&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descendants&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the entries that the above command generated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; postgres=# select * from employees_ltree;
 id |   path   
----+----------
  1 | 1.2.4.8
  2 | 1.3.5.9
  3 | 1.2.6.10
  4 | 1.3.7.11
  5 | 1.2.4.12
  6 | 1.3.5.13
  7 | 1.2.6.14
  8 | 1.3.7.15
(8 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have our table ready. We can proceed to write the Hibernate entity. In order to map columns of type &lt;code&gt;ltree&lt;/code&gt;, I implemented a &lt;a href="https://www.baeldung.com/hibernate-custom-types#2-implementingusertype" rel="noopener noreferrer"&gt;UserType&lt;/a&gt;. I can then map the &lt;code&gt;path&lt;/code&gt; field with &lt;code&gt;@Type(LTreeType.class)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&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;"employees_ltree"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Getter&lt;/span&gt;
&lt;span class="nd"&gt;@Setter&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;EmployeeLtree&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&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;"path"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&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="n"&gt;columnDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ltree"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Type&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LTreeType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&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;path&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;We're ready to write some queries. In native SQL, it would look like the following:&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt;
  &lt;span class="n"&gt;employees_ltree&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'*.2.*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can always write a native query in Spring Data JPA and call it a day. But let's push the envelope a bit and write our queries in JPQL. Because we're using a native Postgres feature it's not supported out of the box so we'll have to implement a couple of things to make it possible. We'll first write our custom &lt;code&gt;StandardSQLFunction&lt;/code&gt;. This will allow us to define a substitution for the Postgres native operator in our JPQL code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LtreePathContainsSQLFunction&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;StandardSQLFunction&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BasicTypeReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;RETURN_TYPE&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;BasicTypeReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"boolean"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SqlTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BOOLEAN&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LtreePathContainsSQLFunction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&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="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;RETURN_TYPE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SqlAppender&lt;/span&gt; &lt;span class="n"&gt;appender&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SqlAstNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ReturnableType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;returnType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SqlAstTranslator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;walker&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFirst&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walker&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;appender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"~"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;appender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"("&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&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="na"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walker&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;appender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;")::lquery"&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;We then have to register it as a &lt;code&gt;FunctionContributor&lt;/code&gt;, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomFunctionsContributor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;FunctionContributor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;contributeFunctions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FunctionContributions&lt;/span&gt; &lt;span class="n"&gt;functionContributions&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;functionName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ltree_contains"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;functionContributions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFunctionRegistry&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;functionName&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;LtreePathContainsSQLFunction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;functionName&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;The last step is to create a resource file in the &lt;code&gt;META-INF/services&lt;/code&gt; folder called &lt;code&gt;org.hibernate.boot.model.FunctionContributor&lt;/code&gt; where we will add a single line with the fully qualified name of the class above.&lt;/p&gt;

&lt;p&gt;Okay, cool! We can now write our JPQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Repository&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;EmployeeLtreeRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmployeeLtree&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
        select
          employee
        from
          EmployeeLtree employee
        where
          ltree_contains(path, :path)
        """&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmployeeLtree&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllByPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"path"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;path&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;This allows us now to pass an ltree path as argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;employeeLtreeRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAllByPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"*.2.*"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres offers a wide set of functions for working with ltrees. You can find them in the official &lt;a href="https://www.postgresql.org/docs/current/ltree.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt; page. As well, there's a useful &lt;a href="https://gist.github.com/oscarychen/2be59f931c05fb19ed1f414c4f485b79" rel="noopener noreferrer"&gt;cheatsheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Like with adjacency lists, it's important to add constraints to our schema in order to ensure data consistency - here's a good &lt;a href="https://medium.com/@msmer/ensuring-data-consistency-when-using-postgresql-ltree-extension-c05cfc4afada" rel="noopener noreferrer"&gt;resource&lt;/a&gt; I found on this topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Nested sets
&lt;/h2&gt;

&lt;p&gt;Easiest to understand is with an image showing the intuition behind it. At every node of the tree we have an extra "left" and a "right" column besides its ID. The rule is that all the children have their left and right in between their parent's left and right values. &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%2F03ql1yi5q40mbgjlx58s.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%2F03ql1yi5q40mbgjlx58s.png" alt="tree" width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the table structure to represent the tree above.&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;employees_nested_sets&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;bigint&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;span class="n"&gt;lft&lt;/span&gt;        &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rgt&lt;/span&gt;        &lt;span class="nb"&gt;integer&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to populate the table, I have converted the script from Joe Celko's "&lt;a href="https://www.amazon.co.uk/Joe-Celkos-SQL-Smarties-Programming/dp/0128007613" rel="noopener noreferrer"&gt;SQL for smarties&lt;/a&gt;" book into Postgres syntax. It migrates the data from the table used in the adjacency list section to this new table structure. Here it is in all its glory:&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;employees_copy&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;migrate_to_nested_sets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="k"&gt;as&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;counter&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;max_counter&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;current_top&lt;/span&gt; &lt;span class="nb"&gt;integer&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;counter&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;max_counter&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="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="o"&gt;*&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;employees_copy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;current_top&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="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;employees_nested_sets&lt;/span&gt;
    &lt;span class="k"&gt;select&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;id&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="n"&gt;max_counter&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;employees_copy&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&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;employees_copy&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;while&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_counter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;
            &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;employees_nested_sets&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employees_copy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack_top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;then&lt;/span&gt;
                &lt;span class="k"&gt;begin&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;employees_nested_sets&lt;/span&gt;
                    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_top&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t1&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="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;integer&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;employees_nested_sets&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employees_copy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;
                    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;
                      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack_top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_top&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;employees_copy&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="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;employees_nested_sets&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;stack_top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_top&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;counter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&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;current_top&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_top&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="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
                &lt;span class="k"&gt;begin&lt;/span&gt;
                    &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;employees_nested_sets&lt;/span&gt;
                    &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;rgt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;stack_top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;stack_top&lt;/span&gt;
                    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;stack_top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_top&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&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;current_top&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_top&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="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&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="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, I'm ready to do some queries. Here's how to retrieve the ancestors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
   select new Employee(
     manager.id
   )
   from
     EmployeeNestedSets employee,
     EmployeeNestedSets manager
    where
     employee.lft between manager.lft and manager.rgt and
     employee.id = :id
   """&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAncestorsOf&lt;/span&gt;&lt;span class="o"&gt;(&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the descendants, it looks a bit different, we have to first retrieve the left and right, after which we can use the below query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""" 
   select new Employee(
     employee.id
   )
   from
     EmployeeNestedSets employee
   where
     employee.lft &amp;gt; :lft and
     employee.rgt &amp;lt; :rgt
    """&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDescendantsUsing&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;lft&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;rgt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! You've seen how to go up or down the tree for all three approaches. I hope that you enjoyed the journey and you find it useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres vs. document/graph databases
&lt;/h2&gt;

&lt;p&gt;The database we've used for the examples above is &lt;strong&gt;Postgres&lt;/strong&gt;. It is not the only option, for example you might wonder why not choose a document database like MongoDB, or a graph databases like Neo4j, because they were actually built with this type of workload in mind. &lt;/p&gt;

&lt;p&gt;Chances are, you already have your source of truth data in Postgres in a relational model with transactional guarantees. In that case, you should first check how well Postgres itself handles your auxiliary use-cases as well, in order to keep everything in one place. This way, you will avoid the increased cost and operational complexity needed to spin up and maintain/upgrade a new separate specialised data store, as well as needing to get familiar with it. &lt;/p&gt;

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

&lt;p&gt;There are several interesting options for modelling hierarchical data in your database applications. In this post I've shown you three ways to do it. Stay tuned for Part 2 where we will compare them as well as see what happens with larger volume of data. &lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Before writing the this post I have looked at various existing ones on the topic and I am grateful for the authors for taking the time to write them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/yugabyte/learn-how-to-write-sql-recursive-cte-in-5-steps-3n88"&gt;https://dev.to/yugabyte/learn-how-to-write-sql-recursive-cte-in-5-steps-3n88&lt;/a&gt;&lt;br&gt;
&lt;a href="https://vladmihalcea.com/hibernate-with-recursive-query/" rel="noopener noreferrer"&gt;https://vladmihalcea.com/hibernate-with-recursive-query/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://vladmihalcea.com/dto-projection-jpa-query/" rel="noopener noreferrer"&gt;https://vladmihalcea.com/dto-projection-jpa-query/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://tudborg.com/posts/2022-02-04-postgres-hierarchical-data-with-ltree/" rel="noopener noreferrer"&gt;https://tudborg.com/posts/2022-02-04-postgres-hierarchical-data-with-ltree/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://aregall.tech/hibernate-6-custom-functions#heading-implementing-a-custom-function" rel="noopener noreferrer"&gt;https://aregall.tech/hibernate-6-custom-functions#heading-implementing-a-custom-function&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.amazon.co.uk/Joe-Celkos-SQL-Smarties-Programming/dp/0128007613" rel="noopener noreferrer"&gt;https://www.amazon.co.uk/Joe-Celkos-SQL-Smarties-Programming/dp/0128007613&lt;/a&gt; &lt;br&gt;
&lt;a href="https://madecurious.com/curiosities/trees-in-postgresql/" rel="noopener noreferrer"&gt;https://madecurious.com/curiosities/trees-in-postgresql/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://schinckel.net/2014/11/27/postgres-tree-shootout-part-2%3A-adjacency-list-using-ctes/" rel="noopener noreferrer"&gt;https://schinckel.net/2014/11/27/postgres-tree-shootout-part-2%3A-adjacency-list-using-ctes/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>java</category>
      <category>spring</category>
    </item>
    <item>
      <title>Faster table joins</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Mon, 02 Sep 2024 19:35:14 +0000</pubDate>
      <link>https://dev.to/mcadariu/fixing-a-slow-join-in-postgres-2b5</link>
      <guid>https://dev.to/mcadariu/fixing-a-slow-join-in-postgres-2b5</guid>
      <description>&lt;p&gt;We can keep our database-backed applications performing pretty well already by following a couple of simple rules, for example:   &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no &lt;a href="https://planetscale.com/blog/what-is-n-1-query-problem-and-how-to-solve-it" rel="noopener noreferrer"&gt;N+1 queries&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;adding adequate &lt;a href="https://use-the-index-luke.com" rel="noopener noreferrer"&gt;indexes&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;keeping the output "narrow" (retrieving only the required columns)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scenario I'm about to show you is a bit different. It was already down to just one query, it had the adequate indexes, but it was still taking &lt;code&gt;~30 seconds&lt;/code&gt; to run, so it had to be improved. Everything was in place such that it would be fast, but &lt;em&gt;somehow&lt;/em&gt;, it wasn't! I'll show you what I did such that it ran in &lt;code&gt;&amp;lt;1s&lt;/code&gt; and explain every step with the reasoning behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tables
&lt;/h2&gt;

&lt;p&gt;What I had is a many-to-many relationship, involving two entity classes, let's call them &lt;code&gt;Foo&lt;/code&gt; and &lt;code&gt;Bar&lt;/code&gt;. They were mapped with the &lt;code&gt;@ManyToMany&lt;/code&gt; JPA annotation in the Java code. At the database level, besides the corresponding tables for storing the entities, there was a link table called &lt;code&gt;foo_bar&lt;/code&gt; containing only two columns (foreign keys). &lt;/p&gt;

&lt;p&gt;The tables were all pretty large, especially the link table totalling more than a hundred million rows. I should note that the link table did have standard b-tree indexes on both columns.&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;Nr. of rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;foo&lt;/td&gt;
&lt;td&gt;2819724&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bar&lt;/td&gt;
&lt;td&gt;21109691&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;foo_bar&lt;/td&gt;
&lt;td&gt;126167975&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The query
&lt;/h2&gt;

&lt;p&gt;The query was the following, written in &lt;code&gt;JPQL&lt;/code&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;delete&lt;/span&gt; 
 &lt;span class="k"&gt;from&lt;/span&gt; 
 &lt;span class="n"&gt;Foo&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; 
 &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columnA&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;and&lt;/span&gt; 
 &lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columnB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on the above, Hibernate generated the following SQL 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;delete&lt;/span&gt;
 &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;foo_bar&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; 
 &lt;span class="n"&gt;foo_bar&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;f1_0&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;foo&lt;/span&gt; &lt;span class="n"&gt;f1_0&lt;/span&gt; 
             &lt;span class="k"&gt;where&lt;/span&gt; 
              &lt;span class="n"&gt;f1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columnA&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;and&lt;/span&gt; 
              &lt;span class="n"&gt;f1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columnB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
             &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, it's actually a &lt;code&gt;delete&lt;/code&gt;, however as you will see below, the bottleneck was a join operation in the execution plan.&lt;/p&gt;

&lt;p&gt;With hindsight, the query could have been written as a native query, and using the &lt;code&gt;CASCADE&lt;/code&gt; feature would allow solving it more declaratively. However, for the rest of this post I'll continue with the query that Hibernate generated, as I still think it's a good support for showing you some instruments you have when a query is slow. &lt;/p&gt;

&lt;p&gt;I've extracted the set of parameters for an exemplar to reproduce. I've then added a transaction block around the query, such that I can rollback and no actual deletes happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The explain plan
&lt;/h2&gt;

&lt;p&gt;When wanting to understand what exactly a query is doing to retrieve our data, we consult so-called &lt;a href="https://www.postgresql.org/docs/current/using-explain.html" rel="noopener noreferrer"&gt;explain plans&lt;/a&gt;. Let's have a look.&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%2Flsibimrm2885cjdfi3nl.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%2Flsibimrm2885cjdfi3nl.png" alt="s3" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whilst it's clear what's slowing it down (the sequential scan reading all the contents of the large link table - the thick line in the image above on the left side), it's unexpected (at least for me). Given the presence of the indexes and the fact that the sub-select by itself only returns about ~800 rows, I had expected that we could avoid reading the entire large table like that, because given the data volume, it will always be slow. &lt;/p&gt;

&lt;p&gt;No problem, let's see what our options are. &lt;/p&gt;

&lt;h2&gt;
  
  
  Looking for inspiration
&lt;/h2&gt;

&lt;p&gt;Let's first get some inspiration by disabling some operations the database can use in planning, for example let's prevent it from going for hash joins. It will then have to use one of its alternatives (the other two options are nested loop and merge joins).&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;set&lt;/span&gt; &lt;span class="n"&gt;enable_hashjoin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh - take a look at that! &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%2Fvv0tz3b43xi2yfbspr4b.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%2Fvv0tz3b43xi2yfbspr4b.png" alt="s3" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No more sequential scan of a hundred million rows. By disabling the hash join option, Postgres went for a much more efficient nested loop that fully utilises the indexes we've defined on the link table. &lt;/p&gt;

&lt;p&gt;So far so good. We now know what we're after, the next question is how to get there, because disabling the hash joins like this is only meant to be for experimentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost-based planning
&lt;/h2&gt;

&lt;p&gt;The question to ask is - what's &lt;em&gt;preventing&lt;/em&gt; Postgres from employing the more efficient nested loop alternative? Let's take a step back and reflect on how Postgres makes its decisions with regards to how it retrieves our data from disk. &lt;/p&gt;

&lt;p&gt;Postgres uses a cost-based optimiser that computes the cost for the various alternatives, and then selects the best one. The costs are calculated based on various factors, including table statistics of the data and configuration properties. &lt;/p&gt;

&lt;p&gt;From the Postgres codebase we learn that it collects a sample of 300 multiplied by the so-called &lt;em&gt;default statistics target&lt;/em&gt;, a configuration option we can control. If you're wondering like me why the 300, it's &lt;em&gt;not&lt;/em&gt; due the 300 Spartans confronting the Persians at Thermopylae. For the real reason, you can have a look &lt;a href="https://github.com/postgres/postgres/blob/master/src/backend/commands/analyze.c#L1912" rel="noopener noreferrer"&gt;here&lt;/a&gt;, where as usual, the code has a helpful comment indicating even the research paper on which the choice is based on.&lt;/p&gt;

&lt;p&gt;For our use-case, the intuition is that due to the size of the table (hundreds of millions), the sample might be too small to get an accurate representation of the data, which might lead to less efficient planning, but let's try to confirm that with some numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the statistics
&lt;/h2&gt;

&lt;p&gt;One of the statistics Postgres collects is an &lt;code&gt;n_distinct&lt;/code&gt;, an estimate of the number of distinct rows in the table. Let's see this for table &lt;code&gt;foo_bar&lt;/code&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;select&lt;/span&gt; 
 &lt;span class="n"&gt;n_distinct&lt;/span&gt; 
&lt;span class="k"&gt;from&lt;/span&gt; 
 &lt;span class="n"&gt;pg_stats&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; 
 &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'foo_bar'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; 
 &lt;span class="n"&gt;attname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'foo_id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

&lt;span class="mi"&gt;28001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's now compute the actual number of distinct values and compare with the estimate. For this, I'm using the following SQL 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;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo_bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foo_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;foo_bar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="mi"&gt;910322&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There we go! The entry in the &lt;code&gt;pg_stats&lt;/code&gt; is &lt;code&gt;~30&lt;/code&gt; times smaller than the actual number of distinct rows. &lt;/p&gt;

&lt;p&gt;This is a problem because it will impact the calculation of the &lt;code&gt;selectivity&lt;/code&gt; value used by the planner. To calculate this, Postgres uses the frequencies of the most common values (MCV) in a table. Note that how many we collect is bounded by the default statistics target value we talked about earlier. If the values we're looking for in a table are not in this list, Postgres fallbacks to using this following &lt;a href="https://www.postgresql.org/docs/current/row-estimation-examples.html" rel="noopener noreferrer"&gt;formula&lt;/a&gt; for selectivity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selectivity = (1 - sum(mcv_freqs))/(num_distinct - num_mcv)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if the values are not in the MCV (because we collected too few), and result of this is wrong because of the wrong estimate of distinct values, then indeed, the planner will not choose the best plan. We might get a nested loop when the selectivity is low, or a hash join when the selectivity is high, which we don't want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Increasing the amount of statistics
&lt;/h2&gt;

&lt;p&gt;Let's try to improve the situation by allowing Postgres to use a larger sample size in order to get better statistics. This means it will store more values in the MCV list as well as looking at more rows when determining the n_distinct. We have to keep in mind however that it will take longer for it to create plans ("Planning time" in the explain analyse output).&lt;/p&gt;

&lt;p&gt;Let's increase the sample size from the default of 100 to a value let's say 10 times bigger, but only for one column, like so:&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;foo_bar&lt;/span&gt; &lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="n"&gt;foo_id&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;statistics&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we query for the &lt;code&gt;n_distinct&lt;/code&gt; now we'll get the same value as before, it will only update after running:&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;analyse&lt;/span&gt; &lt;span class="n"&gt;foo_bar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will take a while. Remember, it has more work to do.&lt;/p&gt;

&lt;p&gt;After a couple of minutes, it finished. Let's have a look if the estimate is closer to reality now.&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_distinct&lt;/span&gt; 
&lt;span class="k"&gt;from&lt;/span&gt; 
 &lt;span class="n"&gt;pg_stats&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; 
 &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'foo_bar'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; 
 &lt;span class="n"&gt;attname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'foo_id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="mi"&gt;121894&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Progress! Still about 9 times smaller than the actual number, but let's run explain analyse now and see if we'll get the nested loop being chosen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bingo
&lt;/h2&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%2F4p7xt6alkbidq9lvgel5.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%2F4p7xt6alkbidq9lvgel5.png" alt="s3" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This executes in under a second, which is a big improvement over where we started. Nice!&lt;/p&gt;

&lt;p&gt;Altering the amount of statistics collected was enough to significantly improve this example, however, in case you need even more control, at least for the &lt;code&gt;n_distinct&lt;/code&gt; you can manually set it to whatever you want with the following command:&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;tool_analysis_finding&lt;/span&gt; &lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;tool_analysis_id&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;n_distinct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, I would advise against going directly for this approach, because it means we won't benefit anymore from the automatic statistic collection that the database does in the background in an unattended fashion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;random_page_cost&lt;/code&gt; setting
&lt;/h2&gt;

&lt;p&gt;Let's look at something else. From the Postgres code we can look up other variables that come into the picture when deciding to choose an index scan over a sequential scan. For example, there's this random_page_cost that can be found &lt;a href="https://github.com/postgres/postgres/blob/master/src/backend/optimizer/path/costsize.c#L741" rel="noopener noreferrer"&gt;here&lt;/a&gt; in the file &lt;code&gt;costsize.c&lt;/code&gt;.  &lt;/p&gt;

&lt;p&gt;For a description of this setting, have a look &lt;a href="https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-RANDOM-PAGE-COST" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Basically, it's the estimate for the cost of retrieving a page non-sequentially from disk. With modern hardware like SSDs, there isn't such a big difference between sequential retrieval and random. The default configuration of &lt;code&gt;4&lt;/code&gt; is not suitable therefore. For example, Crunchy Bridge has &lt;a href="https://docs.crunchybridge.com/changelog#postgres_random_page_cost_1_1" rel="noopener noreferrer"&gt;changed&lt;/a&gt; this value to &lt;code&gt;1.1&lt;/code&gt; for all new databases on their platform. &lt;/p&gt;

&lt;p&gt;Let's try adjusting this to &lt;code&gt;1.1&lt;/code&gt; and see what happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&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%2Fk2u7umehfdg5fcng6vki.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%2Fk2u7umehfdg5fcng6vki.png" alt="s3" width="800" height="511"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;It worked again! We got the nested loop and sub-second execution time again - great stuff. &lt;/p&gt;

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

&lt;p&gt;Postgres features a very clever query planner that does an excellent job finding an efficient way to retrieve our data in most of the cases. However, with some guidance from us (mainly in uncommon situations like very large tables), giving it more details about the context, or letting it use some more storage or time for its internal operations, it continues to deliver the results to our queries as fast as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bohanzhang.me/assets/blogs/run_analyze/run_analyze.html" rel="noopener noreferrer"&gt;Run ANALYZE. Run ANALYZE. Run ANALYZE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.shayon.dev/post/2024/55/100x-faster-query-in-aurora-postgres-with-a-lower-random_page_cost/" rel="noopener noreferrer"&gt;100x Faster Query in Aurora Postgres with a lower random_page_cost&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>performance</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>Spring AI, Llama 3 and pgvector: bRAGging rights!</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Sat, 15 Jun 2024 14:35:35 +0000</pubDate>
      <link>https://dev.to/mcadariu/springai-llama3-and-pgvector-bragging-rights-2n8o</link>
      <guid>https://dev.to/mcadariu/springai-llama3-and-pgvector-bragging-rights-2n8o</guid>
      <description>&lt;p&gt;In the very beginning at least, Python reigned supreme in terms of tooling for AI development. However, recently came the answer from the Spring community, and it's called &lt;a href="https://spring.io/projects/spring-ai" rel="noopener noreferrer"&gt;Spring AI&lt;/a&gt;! This means that if you're a Spring developer with working knowledge of concepts such as beans, auto-configurations and starters, you're covered, and you can write your AI apps following the standard patterns you're already familiar with. &lt;/p&gt;

&lt;p&gt;In this post, I want to share with you an exploration that started with the goal to take Spring AI for a little spin and try out the capabilities of open-source LLMs (large language models). I got curious along the way, and ended up also looking at some pretty low-level details about data storage for AI applications as well.&lt;/p&gt;

&lt;p&gt;To support the exploration, I've developed a simple &lt;a href="https://www.promptingguide.ai/techniques/rag" rel="noopener noreferrer"&gt;Retrieval Augmented Generation (RAG)&lt;/a&gt; workflow that works completely locally on the laptop for free. If you're interested, you can find the code itself &lt;a href="https://github.com/mcadariu/spring-ai-rag-demo/blob/main/src/test/java/com/example/braggingrights/demo/RAGDemoApplicationTests.java" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Basically, I've used &lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;&lt;code&gt;Testcontainers&lt;/code&gt;&lt;/a&gt; to create a Postgres database container with the &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; extension to store text &lt;a href="https://vickiboykis.com/what_are_embeddings/" rel="noopener noreferrer"&gt;embeddings&lt;/a&gt; and an open source LLM with which I send requests to: Meta's &lt;a href="https://ai.meta.com/blog/meta-llama-3/" rel="noopener noreferrer"&gt;&lt;code&gt;llama3&lt;/code&gt;&lt;/a&gt; (8B) through &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;&lt;code&gt;ollama&lt;/code&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Alright, let's have a look at what I'm building with this setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fun with sayings and essays
&lt;/h2&gt;

&lt;p&gt;The RAG workflow works like this. I first ask it to generate a couple of &lt;strong&gt;sayings&lt;/strong&gt;, for example this is one among the ones I got back: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A bad excuse is better than none.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, I ask it to "elaborate" and genarate an &lt;strong&gt;essay&lt;/strong&gt; (of under 200 words) about the saying it provided me earlier. Here is one such essay generated by &lt;code&gt;llama3&lt;/code&gt; for the saying above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The saying suggests that while an inadequate justification may not be satisfactory, it's still preferable to having no explanation at all. This phrase highlights the importance of accountability and acknowledging responsibility for one's actions or decisions.&lt;/p&gt;

&lt;p&gt;In essence, a bad excuse implies that some effort has been made to justify or rationalize one's behavior, even if the reasoning is flawed or unconvincing. On the other hand, not offering any excuse whatsoever can come across as evasive or dishonest, leading to mistrust and resentment.&lt;/p&gt;

&lt;p&gt;This saying encourages people to take ownership of their mistakes and apologize for any wrongdoing, rather than remaining silent and avoiding accountability. It also acknowledges that everyone makes errors and that a bad excuse is better than none because it shows a willingness to acknowledge and learn from those mistakes.&lt;/p&gt;

&lt;p&gt;Ultimately, the saying promotes honesty, responsibility, and personal growth by emphasizing the value of taking ownership of one's actions, no matter how imperfect the explanation may be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, I will take these essays and create &lt;a href="https://vickiboykis.com/what_are_embeddings/" rel="noopener noreferrer"&gt;embeddings&lt;/a&gt; from them, which I will store in Postgres, using the &lt;code&gt;pgvector&lt;/code&gt; extension in columns of &lt;code&gt;vector&lt;/code&gt; data type. All with the help of Spring AI abstractions and least amount of custom code. &lt;/p&gt;

&lt;p&gt;I will skip the part of this process called "chunking". When you are dealing with very large documents, or want to isolate sections in your data (like in e-mails where you have subject, sender, etc..) you might look into doing that.&lt;/p&gt;

&lt;p&gt;So far so good. At this point, we have stored the data we need in the next steps.&lt;/p&gt;

&lt;p&gt;I will then take each saying and do a &lt;strong&gt;similarity search&lt;/strong&gt; on the embeddings to retrieve the corresponding essay for each saying. Lastly, I will supply the retrieved essays back again to the LLM, and now ask it to &lt;strong&gt;guess the original saying&lt;/strong&gt; from which the essay was generated. Finally I will check how many it got right.&lt;/p&gt;

&lt;p&gt;What do you think, will it manage to correctly guess the saying from just the essay? After all, it has generated the essays from those sayings itself in the first place. A human would have no problem doing this.&lt;/p&gt;

&lt;p&gt;But let's first have a look at how the program is set up from a technical perspective. We will look at the results and find out how capable is the LLM a bit later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM and the vector store in Testcontainers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Testcontainers&lt;/code&gt; makes it very easy to integrate services that each play a specific role for use-cases like this. All that is required to set up a database and the LLM are the couple of lines below and you're good to go!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@TestConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxyBeanMethods&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RagDemoApplicationConfiguration&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;POSTGRES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@ServiceConnection&lt;/span&gt;
    &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;postgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"pgvector/pgvector:pg16"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;POSTGRES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;POSTGRES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDatabaseName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;POSTGRES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withInitScript&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"init-script.sql"&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="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@ServiceConnection&lt;/span&gt;
    &lt;span class="nc"&gt;OllamaContainer&lt;/span&gt; &lt;span class="nf"&gt;ollamaContainer&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OllamaContainer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama/ollama:latest"&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;I've used the &lt;code&gt;@ServiceConnections&lt;/code&gt; annotation that allows me to type &lt;a href="https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1" rel="noopener noreferrer"&gt;less configuration code&lt;/a&gt;. I can do this for the &lt;code&gt;ollama&lt;/code&gt; container too only since recently, thanks to this recent &lt;a href="https://github.com/spring-projects/spring-ai/pull/453" rel="noopener noreferrer"&gt;contribution&lt;/a&gt; from Eddú Meléndez. &lt;/p&gt;

&lt;p&gt;You might have noted there's an init script there. It's only a single line of code, and has the purpose to install a Postgres extension called &lt;a href="https://www.postgresql.org/docs/current/pgbuffercache.html" rel="noopener noreferrer"&gt;pg_buffercache&lt;/a&gt; which lets me inspect the contents of the Postgres &lt;a href="https://minervadb.xyz/understanding-shared-buffers-implementation-in-postgresql/" rel="noopener noreferrer"&gt;shared buffers&lt;/a&gt; in RAM. I'm interested in having a look at this in order to better understand the operational characteristics of working with vectors. With other words, what are the memory demands?&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="n"&gt;extension&lt;/span&gt; &lt;span class="n"&gt;pg_buffercache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, to fully initialise our LLM container such that it's ready to actually handle our requests for our sayings and essays, we need to pull the models we want to work with, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execInContainer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pull"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llama3"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execInContainer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pull"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nomic-embed-text"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you rerun the program again you will see that it will pull the models again. You can have a look at &lt;a href="https://github.com/ThomasVitale/llm-images" rel="noopener noreferrer"&gt;this&lt;/a&gt; repo and consider using the baked images that have the models within them already.&lt;/p&gt;

&lt;p&gt;You will notice that besides the &lt;code&gt;llama3&lt;/code&gt; that I mentioned before which will take care of generating text, I am also pulling a so-called embedding model: &lt;code&gt;nomic-embed-text&lt;/code&gt;. This is to be able to convert text into embeddings, to be able store them. &lt;/p&gt;

&lt;p&gt;The ones I'm using are not the only options. New LLM bindings and embedding models are added all the time in both Spring AI and ollama, so refer to the &lt;a href="https://spring.io/projects/spring-ai" rel="noopener noreferrer"&gt;docs&lt;/a&gt; for the up-to-date list, as well as the &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;ollama&lt;/a&gt; website. &lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration properties
&lt;/h2&gt;

&lt;p&gt;Let's have a look at the vector store configuration. Here's how that looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@DynamicPropertySource&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;pgVectorProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DynamicPropertyRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.ai.vectorstore.pgvector.index-type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"HNSW"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.ai.vectorstore.pgvector.distance-type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"COSINE_DISTANCE"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;     
  &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.ai.vectorstore.pgvector.dimensions"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;768&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;The first one is called &lt;code&gt;index-type&lt;/code&gt;. This means that we are creating an index in our vector store. We don't necessarily need to always use an index - it's a trade-off. With indexing, the idea is that we gain speed (and other things, like uniqueness, etc) at the expense of storage space. With indexing vectors however, the trade-off also includes the &lt;strong&gt;relevance&lt;/strong&gt; aspect. Without indexing, the similarity search is based on the kNN algorithm (k-nearest neigbours) where it checks all vectors in the table. However with indexing, it will perform an aNN (&lt;em&gt;approximate&lt;/em&gt; nearest neighbours) which is faster but might miss some results. Indeed, it's quite the balancing act.   &lt;/p&gt;

&lt;p&gt;Let's have a look at the other configuration options for indexing, which I extracted from the Spring AI code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;NONE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="no"&gt;IVFFLAT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="no"&gt;HNSW&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the beginning, there used to be only one option for indexing in pgvector, namely &lt;code&gt;ivfflat&lt;/code&gt;. More recently, the &lt;code&gt;HNSW&lt;/code&gt; (Hierarchical Navigable Small Worlds) one was added, which is based on different construction principles and is more performant, and keeps getting better. The general recommendation is to go for &lt;code&gt;HNSW&lt;/code&gt; as of now. &lt;/p&gt;

&lt;p&gt;The next configuration option is the &lt;code&gt;distance-type&lt;/code&gt; which is the procedure it uses to compare vectors in order to determine similarity. Here are our options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="no"&gt;EUCLIDEAN_DISTANCE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="no"&gt;NEGATIVE_INNER_PRODUCT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="no"&gt;COSINE_DISTANCE&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll go with the cosine distance, but it might be helpful to have a look at their &lt;a href="https://cmry.github.io/notes/euclidean-v-cosine" rel="noopener noreferrer"&gt;properties&lt;/a&gt; because it might make a difference for your use-case.&lt;/p&gt;

&lt;p&gt;The last configuration property is called &lt;code&gt;dimensions&lt;/code&gt;, which represent the number of components (tokenized float values) that the embeddings will be represented on. This number has to be correlated with the number of dimensions we set up in our vector store. In our example, the &lt;code&gt;nomic-embedding-text&lt;/code&gt; one has 768, but others have more, or less. If the model returns the embeddings in more dimensions than we have set up our table, it won't work. Now you might wonder, should you strive to have as high number of dimensions as possible? Actually the answer to this question is apparently no, this blog from Supabase shows that &lt;a href="https://supabase.com/blog/fewer-dimensions-are-better-pgvector" rel="noopener noreferrer"&gt;fewer dimensions are better&lt;/a&gt;.   &lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood - what's created in Postgres?
&lt;/h2&gt;

&lt;p&gt;Let's explore what Spring AI has created for us with this configuration in Postgres. In a production application however, you might want to take full control and drive the schema through SQL files managed by migration tools such as Flyway. We didn't do this here for simplicity. &lt;/p&gt;

&lt;p&gt;Firstly, we find it created a table called &lt;code&gt;vector_store&lt;/code&gt; with the following structure:&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;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                     &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;"public.vector_store"&lt;/span&gt;
  &lt;span class="k"&gt;Column&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="k"&gt;Type&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Collation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Nullable&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="k"&gt;Default&lt;/span&gt;       
&lt;span class="c1"&gt;-----------+-------------+-----------+----------+--------------------&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;        &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&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="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;uuid_generate_v4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;content&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;text&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="n"&gt;metadata&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;json&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="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&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="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"vector_store_pkey"&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;btree&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="nv"&gt;"spring_ai_vector_index"&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing surprising here. It's in-line with the configuration we saw above in the Java code I showed you earlier. For example, we notice the &lt;code&gt;embedding&lt;/code&gt; column of type &lt;code&gt;vector&lt;/code&gt;, of 768 dimensions. We notice also the index - &lt;code&gt;spring_ai_vector_index&lt;/code&gt; and the &lt;code&gt;vector_cosine_ops&lt;/code&gt; operator class, which we expected given what we set in the "distance-type" setting earlier. The other index, namely &lt;code&gt;vector_store_pkey&lt;/code&gt;, is created automatically by Postgres. It creates such an index for every primary key by itself.&lt;/p&gt;

&lt;p&gt;The command that Spring AI used to create our index is the following:&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;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="o"&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;embedding&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates an index with the default configuration. It might be good to know that you have a couple of &lt;a href="https://github.com/pgvector/pgvector?tab=readme-ov-file#index-options" rel="noopener noreferrer"&gt;options&lt;/a&gt; if you'd like to tweak the index configuration for potentially better results (depends on use-case):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;m&lt;/em&gt; - the max number of connections per layer &lt;/li&gt;
&lt;li&gt;
&lt;em&gt;ef_construction&lt;/em&gt; - the size of the dynamic candidate list for constructing the graph &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Theres are the boundaries you can pick from for these settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;default&lt;/th&gt;
&lt;th&gt;min&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;m&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;ef_construction&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In order to understand the internals of this index and what effect changing the above options might have, here is a &lt;a href="https://arxiv.org/pdf/1603.09320" rel="noopener noreferrer"&gt;link to the original paper&lt;/a&gt;. See also this &lt;a href="https://jkatz05.com/post/postgres/pgvector-hnsw-performance/" rel="noopener noreferrer"&gt;post&lt;/a&gt; by J. Katz in which he presents results of experimenting with various combinations of the above settings.&lt;/p&gt;

&lt;p&gt;When you know what values you want to set for these settings you can create the index like so:&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;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case you get an error when constructing an index, it's worth looking into if it has enough memory to perform this operation. You can adjust the memory for it through the &lt;code&gt;maintenance_work_mem&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;Let's now check how our &lt;code&gt;embedding&lt;/code&gt; column is actually stored on disk. We use the following &lt;a href="https://stackoverflow.com/a/49947950" rel="noopener noreferrer"&gt;query&lt;/a&gt; which will show us our next step.&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;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; 
             &lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="k"&gt;case&lt;/span&gt; 
             &lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attstorage&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'p'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'plain'&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'m'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'e'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'external'&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'x'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'extended'&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;attstorage&lt;/span&gt;
           &lt;span class="k"&gt;from&lt;/span&gt; 
            &lt;span class="n"&gt;pg_attribute&lt;/span&gt; &lt;span class="n"&gt;att&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="n"&gt;tbl&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrelid&lt;/span&gt;   
           &lt;span class="k"&gt;join&lt;/span&gt; 
            &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ns&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;where&lt;/span&gt; 
            &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;.&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;'vector_store'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; 
            &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;   
            &lt;span class="n"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'embedding'&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;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="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;RECORD&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="c1"&gt;---------&lt;/span&gt;
&lt;span class="n"&gt;attname&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;
&lt;span class="n"&gt;attstorage&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, so it uses the &lt;code&gt;external&lt;/code&gt; storage type. This means that it will store this column in a separate, so-called &lt;a href="https://www.postgresql.org/docs/current/storage-toast.html" rel="noopener noreferrer"&gt;TOAST&lt;/a&gt; table. Postgres does this when columns are so large it can't fit at least 4 rows in a &lt;a href="https://www.postgresql.org/docs/16/storage-page-layout.html" rel="noopener noreferrer"&gt;page&lt;/a&gt;. But interesting that it will &lt;em&gt;not&lt;/em&gt; attempt to also compress it to shrink it even more. For compressed columns it would have said &lt;code&gt;extended&lt;/code&gt; instead of &lt;code&gt;external&lt;/code&gt; in the result above. &lt;/p&gt;

&lt;p&gt;Normally, when you update one or multiple columns of a row, Postgres will, instead of overwriting, make a copy of the entire row (it's an &lt;a href="https://www.postgresql.org/docs/current/mvcc-intro.html" rel="noopener noreferrer"&gt;MVCC&lt;/a&gt; database). But if there are any large TOASTed columns, then during an update it will copy only the other columns. It will copy the TOASTed column only when that is updated. This makes it more efficient by minimising the amount of copying around of large values.  &lt;/p&gt;

&lt;p&gt;Where are these separate tables though? We haven't created them ourselves, they are managed by Postgres. Let's try to locate this separate TOAST table using this &lt;a href="https://medium.com/quadcode-life/toast-tables-in-postgresql-99e3403ed29b" rel="noopener noreferrer"&gt;query&lt;/a&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="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;select&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;oid&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="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;reltoastrelid&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;'vector_store'&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;vector_store&lt;/span&gt; 
            &lt;span class="k"&gt;where&lt;/span&gt; 
              &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reltoastrelid&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; 
              &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&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;indexrelid&lt;/span&gt; 
                     &lt;span class="k"&gt;from&lt;/span&gt; 
                      &lt;span class="n"&gt;pg_index&lt;/span&gt;
                     &lt;span class="k"&gt;where&lt;/span&gt; 
                      &lt;span class="n"&gt;indrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reltoastrelid&lt;/span&gt;
                      &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;       &lt;span class="n"&gt;relname&lt;/span&gt;        &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;oid&lt;/span&gt;  
&lt;span class="c1"&gt;----------------------+-------&lt;/span&gt;
 &lt;span class="n"&gt;pg_toast_16630&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;16634&lt;/span&gt;
 &lt;span class="n"&gt;pg_toast_16630_index&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;16635&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far so good. We now have the TOAST table ID. Let's use it to have a look at the structure of the TOAST table. For example, what columns does it have? Note that these tables are in the &lt;code&gt;pg_toast&lt;/code&gt; schema, by the way, so to get there, we have to set the &lt;code&gt;search_path&lt;/code&gt; to &lt;code&gt;pg_toast&lt;/code&gt;, like below:&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;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;pg_toast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;pg_toast_16630&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;TOAST&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="nv"&gt;"pg_toast.pg_toast_16630"&lt;/span&gt;
   &lt;span class="k"&gt;Column&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;Type&lt;/span&gt;   
&lt;span class="c1"&gt;------------+---------&lt;/span&gt;
 &lt;span class="n"&gt;chunk_id&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt;
 &lt;span class="n"&gt;chunk_seq&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;
 &lt;span class="n"&gt;chunk_data&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;bytea&lt;/span&gt;
&lt;span class="n"&gt;Owning&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;"public.vector_store"&lt;/span&gt;
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;"pg_toast_16630_index"&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;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can learn a couple of things from this. As expected, the large columns in the main table that have to be "TOASTed" are chunked (split up) and each chunk is identified by a sequence, and is always retrieved using an index.&lt;/p&gt;

&lt;p&gt;Postgres has a mechanism to avoid "blasting" the entire shared buffer cache when it needs to do large reads, like sequential scans of a large table. When it has to do this, it actually uses a 32 page ring buffer so that it doesn't evict other data from the cache. But this mechanism will not kick in for TOAST tables, so  vector-based workloads will be run without this form of protection.&lt;/p&gt;

&lt;p&gt;Okay! We had a very good look at the database part. Let's now "resurface" for a moment and have a look at other topics pertaining to the high level workflow of interacting with the LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Template-based prompts
&lt;/h2&gt;

&lt;p&gt;Initially, I had constructed the prompts for the request to the LLM in the same class where I was using them. However, I found the following different approach in the Spring AI repository itself and adopted it, because it's indeed cleaner to do it this way. It's based on externalised resource files, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:/generate-essay.st"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;generateEssay&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:/generate-saying.st"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;generateSaying&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:/guess-saying.st"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;guessSaying&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how one of them looks inside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write a short essay under 200 words explaining the 
meaning of the following saying: {saying}.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, I have not applied any sophisticated &lt;a href="https://www.promptingguide.ai/" rel="noopener noreferrer"&gt;prompt engineering&lt;/a&gt; whatsoever, and kept it simple and direct for now. &lt;/p&gt;

&lt;h2&gt;
  
  
  Calling the LLM
&lt;/h2&gt;

&lt;p&gt;Alright, the pieces are starting to fit together! The next thing I'd like to show you is how to call the LLM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createPromptFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promptTemplate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;promptTemplateValues&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResult&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOutput&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I am using the so-called &lt;a href="https://docs.spring.io/spring-ai/reference/api/chatmodel.html" rel="noopener noreferrer"&gt;&lt;code&gt;Chat Model API&lt;/code&gt;&lt;/a&gt;, a powerful abstraction over AI models. This design allows us to switch between models with minimal code changes. If you want to work with a different model, you just change the runtime configuration. This is a nice example of the Dependency Inversion Principle; where we have higher level modules that do not depend on low-level modules, both depend on abstractions. &lt;/p&gt;

&lt;h2&gt;
  
  
  Storing the embeddings
&lt;/h2&gt;

&lt;p&gt;To store the embeddings, I must say that I found it a pretty complicated procedure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just kidding, that's it! &lt;/p&gt;

&lt;p&gt;This single command will do several things. First convert the documents (our essays) to embeddings with the help of the embeddings model, then it will run the following batched insert statement to get the embeddings into our &lt;code&gt;vector_store&lt;/code&gt; 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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;vector_store&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&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="o"&gt;?&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;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&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;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&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;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&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;We can see it actually performs an update of the &lt;code&gt;content&lt;/code&gt; column in case there is already one row with that ID (taken care of by the &lt;code&gt;ON CONFLICT&lt;/code&gt; part in the query) present in the database. &lt;/p&gt;

&lt;h2&gt;
  
  
  Similarity searches
&lt;/h2&gt;

&lt;p&gt;To do a similarity search on the stored vectors with Spring AI, it's just a matter of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;similaritySearch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saying&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFirst&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again you get a couple of things done for you by Spring AI. It takes the parameter you supply ("saying" in our case), and first it creates its embedding using the embedding model we talked about before. Then it uses it to retrieve the most similar results, from which we pick only the first one.&lt;/p&gt;

&lt;p&gt;With this configuration (cosine similarity), the SQL query that it will run for you is the following:&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="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;nativeFilterExpression&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonpath&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;distance&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It selects all the columns in the table and adds a column with the calculated distance. The results are ordered by this distance column, and you can also specify a similarity threshold and a native filter expression using Postgres' jsonpath functionality. &lt;/p&gt;

&lt;p&gt;One thing to be noted, is that if you'd write the query yourself and run it with without letting Spring AI create it for you, you can &lt;a href="https://github.com/pgvector/pgvector?tab=readme-ov-file#query-options" rel="noopener noreferrer"&gt;customise&lt;/a&gt; the query by supplying different values for the &lt;code&gt;ef_search&lt;/code&gt; parameter (default: 40, min: 1, max: 1000), like so:&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;SET&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ef_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With it, you can influence the number of neighbours that it considers for the search. The more that are checked, the better the recall, but it will be at the expense of performance.&lt;/p&gt;

&lt;p&gt;Now that we know how to do perform similarity searches to retrieve semantically close data, we can also make a short incursion into how Postgres uses memory (shared buffers) when performing these retrievals.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much of the shared buffers got filled up?
&lt;/h2&gt;

&lt;p&gt;Let's now increase a bit the amount of essays we're working with to &lt;code&gt;100&lt;/code&gt;, and have a look what's in the Postgres shared buffers after we run the program. We'll use the &lt;code&gt;pg_buffercache&lt;/code&gt; extension that I mentioned before, which was installed in the init script. &lt;/p&gt;

&lt;p&gt;But first, let's start with looking at the size of the table and index, just to get some perspective.&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;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                                       &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;relations&lt;/span&gt;
 &lt;span class="k"&gt;Schema&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="k"&gt;Type&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;Owner&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Persistence&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Access&lt;/span&gt; &lt;span class="k"&gt;method&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;Size&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; 
&lt;span class="c1"&gt;--------+--------------+-------+----------+-------------+---------------+--------+-------------&lt;/span&gt;
 &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;permanent&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;584&lt;/span&gt; &lt;span class="n"&gt;kB&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;spring_ai_vector_index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                                                   &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;relations&lt;/span&gt;
 &lt;span class="k"&gt;Schema&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="k"&gt;Type&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;Owner&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="k"&gt;Table&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Persistence&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Access&lt;/span&gt; &lt;span class="k"&gt;method&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;Size&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; 
&lt;span class="c1"&gt;--------+------------------------+-------+----------+--------------+-------------+---------------+--------+-------------&lt;/span&gt;
 &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;spring_ai_vector_index&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;permanent&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;408&lt;/span&gt; &lt;span class="n"&gt;kB&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, so the table is &lt;code&gt;584 kB&lt;/code&gt; and the index is &lt;code&gt;408 kB&lt;/code&gt;. It seems the index gets pretty big, close to being about the same size of the table. We don't mind that much at such small scale, but if we assume this proportion will be maintained at large scale too, we will have to take it more seriously. &lt;/p&gt;

&lt;p&gt;To contrast with how other indexes behave, I checked a table we have at work that amounts to &lt;code&gt;40Gb&lt;/code&gt;. The corresponding B-tree primary key index is &lt;code&gt;10Gb&lt;/code&gt;, while other indexes of the same type for other columns are just &lt;code&gt;3Gb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'm using the following &lt;a href="https://tomasz-gintowt.medium.com/postgresql-extensions-pg-buffercache-b38b0dc08000" rel="noopener noreferrer"&gt;query&lt;/a&gt; to get an overview of what's in the shared buffers:&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;c&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="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;buffers&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
  &lt;span class="n"&gt;pg_buffercache&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; 
&lt;span class="k"&gt;inner&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relfilenode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg_relation_filenode&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;and&lt;/span&gt;
                &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reldatabase&lt;/span&gt; &lt;span class="k"&gt;in&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; 
                                        &lt;span class="n"&gt;oid&lt;/span&gt; 
                                      &lt;span class="k"&gt;from&lt;/span&gt; 
                                        &lt;span class="n"&gt;pg_database&lt;/span&gt;
                                      &lt;span class="k"&gt;where&lt;/span&gt; 
                                        &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_database&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                      &lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&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;relname&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="k"&gt;limit&lt;/span&gt; 
   &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;            &lt;span class="n"&gt;relname&lt;/span&gt;             &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt; 
&lt;span class="c1"&gt;--------------------------------+---------&lt;/span&gt;
 &lt;span class="n"&gt;pg_proc&lt;/span&gt;                        &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;61&lt;/span&gt;
 &lt;span class="n"&gt;pg_toast_16630&lt;/span&gt;                 &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;53&lt;/span&gt;
 &lt;span class="n"&gt;spring_ai_vector_index&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;51&lt;/span&gt;
 &lt;span class="n"&gt;pg_attribute&lt;/span&gt;                   &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;35&lt;/span&gt;
 &lt;span class="n"&gt;pg_proc_proname_args_nsp_index&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;30&lt;/span&gt;
 &lt;span class="n"&gt;pg_depend&lt;/span&gt;                      &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;23&lt;/span&gt;
 &lt;span class="n"&gt;pg_operator&lt;/span&gt;                    &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;19&lt;/span&gt;
 &lt;span class="n"&gt;pg_statistic&lt;/span&gt;                   &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;19&lt;/span&gt;
 &lt;span class="n"&gt;pg_class&lt;/span&gt;                       &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;18&lt;/span&gt;
 &lt;span class="n"&gt;vector_store&lt;/span&gt;                   &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;18&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We see that all the index in its entirety is in there. We deduced this because the size of the index is &lt;code&gt;408 Kb&lt;/code&gt;, as we saw before, and if we divide that by &lt;code&gt;8 Kb&lt;/code&gt;, which is the size of a Postgres page, we get exactly &lt;code&gt;51&lt;/code&gt; like we see in the above table (third row above). &lt;/p&gt;

&lt;p&gt;We can draw a conclusion from this - working with vectors in Postgres is going to be pretty demanding in terms of memory. As reference, vectors that have 1536 dimensions (probably the most common case) will occupy each about &lt;code&gt;6Kb&lt;/code&gt;. One million of them already gets us to &lt;code&gt;6Gb&lt;/code&gt;. In case we have other workloads next to the vectors, they might be affected in the sense that we start seeing cache evictions because there's no free buffer. This means we might even need to consider separating the vectors from the other data we have, in separate databases, in order to isolate the workloads in case we notice the performance going downhill. &lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;@ParameterizedTest&lt;/code&gt; JUnit annotation
&lt;/h2&gt;

&lt;p&gt;Alright, a last remark I want to make about this program is that it's set up to be able to experiment with other open-source LLMs. The entrypoint method I'm using to run the workflow, is a JUnit parameterized test where the arguments for each run can be the names of other LLM models distributed with ollama. This is how you set it up to run multiple times with a different LLM for every execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
&lt;span class="nd"&gt;@ValueSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"llama3"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llama2"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gemma"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mistral"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;rag_workflow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;model&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;h2&gt;
  
  
  Outputs
&lt;/h2&gt;

&lt;p&gt;Finally it's time to review how well did the LLM manage to guess the sayings. With no other help except for the initial essays provided in the prompt, it managed to guess the saying perfectly a grand total of... once! &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Saying&lt;/th&gt;
&lt;th&gt;LLM Guess&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your most powerful moments are born from the ashes of your greatest fears.&lt;/td&gt;
&lt;td&gt;What doesn't kill you...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every sunrise holds the promise of a new masterpiece.&lt;/td&gt;
&lt;td&gt;What lies within is far more important than what lies without.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every step forward is a declaration of your willingness to grow.&lt;/td&gt;
&lt;td&gt;Any Step Forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your most beautiful moments are waiting just beyond your comfort zone.&lt;/td&gt;
&lt;td&gt;What lies within...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light reveals itself in the darkness it creates.&lt;/td&gt;
&lt;td&gt;The darkness is not the absence of light but the presence of a different kind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Courage is not the absence of fear, but the willingness to take the next step anyway.&lt;/td&gt;
&lt;td&gt;Be brave.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Small sparks can ignite entire galaxies.&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Small sparks can ignite entire galaxies.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Believe in yourself, take the leap and watch the universe conspire to make your dreams come true.&lt;/td&gt;
&lt;td&gt;Take the leap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Life begins at the edge of what you're willing to let go.&lt;/td&gt;
&lt;td&gt;Take the leap.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some responses are quite amusing, like when it tries to be "mysterious" or more conversational by not completing the sentence fully and just ending it in three dots ("What doesn't kill you..."), and the ones where it reaches for extreme succintness ("Take the leap.", "Be brave."). &lt;/p&gt;

&lt;p&gt;Let's give it some help now. In the prompt, this time I'll provide all the sayings it initially generated as a list of options to pick from. Will it manage to pick the correct one from the bunch this way?&lt;/p&gt;

&lt;p&gt;Turns out, indeed, if I gave it options to pick from, it picked the right one, every time. Quite the difference between with or without RAG!&lt;/p&gt;

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

&lt;p&gt;Spring AI is a well designed application framework that helps you achieve a lot with little code. You can see it as the "linchpin" that helps you set up and easily evolve your AI use-cases in in stand-alone new applications or integrated with your existing Spring ones. It already has many integrations with specialised AI services and the list keeps growing constantly.&lt;/p&gt;

&lt;p&gt;The open-source LLMs I tried have not raised to the occasion, and haven't passed my "challenge" to guess the initial sayings they themselves generated from their (also own) essays. They seem not ready to perform well for use-cases that require this kind of precise and correct "synthesised" answers, but I will keep trying new models as they are made available. &lt;/p&gt;

&lt;p&gt;However, they are still useful if you know what you can expect from them - they are very good for storing and retrieving many loosely connected facts, a clear value-add when needing to brainstorm for example. &lt;/p&gt;

&lt;p&gt;When I gave it the options, the difference is like night and day compared to when I didn't. If given the options, it picked the right answer every time, flawlessly. &lt;/p&gt;

&lt;p&gt;We also looked at how embeddings are stored internally with the &lt;code&gt;pgvector&lt;/code&gt; extension, and how "memory-hungry" this is - we should account for this and make some arrangements at the beginning of the project in order to have smooth operation when the scale grows.&lt;/p&gt;

&lt;p&gt;Thanks for reading! &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Query optimisation guided by explain plans</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Wed, 03 Apr 2024 14:33:30 +0000</pubDate>
      <link>https://dev.to/mcadariu/retrieving-the-latest-row-per-group-in-postgresql-247d</link>
      <guid>https://dev.to/mcadariu/retrieving-the-latest-row-per-group-in-postgresql-247d</guid>
      <description>&lt;p&gt;&lt;u&gt;&lt;strong&gt;Use-case&lt;/strong&gt;&lt;/u&gt;: we have a set of &lt;code&gt;meters&lt;/code&gt; (e.g. gas, electricity, etc) that regularly record &lt;code&gt;readings&lt;/code&gt;. Our task is to retrieve the latest reading of every meter (or more generically phrased: &lt;code&gt;retrieving the latest row per group&lt;/code&gt;) using SQL.&lt;/p&gt;

&lt;p&gt;SQL is a declarative language. Therefore, we achieve goal using it by expressing only &lt;em&gt;what&lt;/em&gt; we want in the form of queries, instead of providing precise instructions about &lt;em&gt;how&lt;/em&gt; to retrieve data. Then the database component called the planner will determine what's the best way to do that based on several factors such as table statistics. However, as we shall see, going for the first approach that comes to mind when writing queries might not yield the best performance. So ideally, we know as much as possible what the database does internally to retrieve our data, and what are our options to influence this towards the optimal access path for our use-cases. &lt;/p&gt;

&lt;p&gt;In the sections below, to explain exactly what makes the alternatives I show you faster, I use as support visualisations of Postgres &lt;a href="https://www.postgresql.org/docs/current/using-explain.html" rel="noopener noreferrer"&gt;explain plans&lt;/a&gt;, showing how the database processes our queries internally. As you will see, some adjustments will make quite a difference - we will go from &lt;code&gt;hundreds&lt;/code&gt; of ms to about &lt;code&gt;5&lt;/code&gt;. I sometimes thought of side notes to the main story to share with you that I've marked appropriately. &lt;/p&gt;

&lt;p&gt;I'm using &lt;code&gt;Postgres&lt;/code&gt; version &lt;code&gt;16.2&lt;/code&gt; (with its default configuration) running on my laptop .&lt;/p&gt;

&lt;h2&gt;
  
  
  Tables
&lt;/h2&gt;

&lt;p&gt;First, we create the table of &lt;code&gt;meters&lt;/code&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;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;meters&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;bigint&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&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 create the table of &lt;code&gt;readings&lt;/code&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;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;readings&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;bigint&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;meter_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;         &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="n"&gt;reading&lt;/span&gt;      &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;fk__readings_meters&lt;/span&gt; &lt;span class="k"&gt;foreign&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;meter_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;meters&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For enforcing integrity, the tables are linked together with a foreign key constraint. I am also &lt;a href="https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_serial" rel="noopener noreferrer"&gt;not using serial&lt;/a&gt; when setting up the primary keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating test data
&lt;/h2&gt;

&lt;p&gt;Let's now populate our tables with some rows: we add &lt;code&gt;500&lt;/code&gt; meters, all having one reading every day for a year. For generating test data for situations like this, the &lt;a href="https://www.postgresql.org/docs/current/functions-srf.html#FUNCTIONS-SRF" rel="noopener noreferrer"&gt;generate_series&lt;/a&gt; function is invaluable.&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;meters&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&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;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&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;readings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reading&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;m&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="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&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="s1"&gt;'2024-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Querying
&lt;/h2&gt;

&lt;p&gt;Our tables are now populated and ready to be queried. Let's get to it!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Window functions: &lt;code&gt;250ms&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We first express that we want to partition our readings per meter IDs, and then use the &lt;a href="https://www.postgresql.org/docs/current/functions-window.html" rel="noopener noreferrer"&gt;&lt;code&gt;row_number&lt;/code&gt;&lt;/a&gt; function to assign an "order number" for every reading based on its age among its peers within an individual partition. You then use this to filter the result set and return only ones that are at the top (have row number equal to 1) per every partition.&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;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;readings_with_rownums&lt;/span&gt; &lt;span class="k"&gt;as&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;meters&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;as&lt;/span&gt; &lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                                                                                                                                                                
        &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;partition&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;meters&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;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;desc&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;rownum&lt;/span&gt;                                                                                                                                                                                                              
    &lt;span class="k"&gt;from&lt;/span&gt;                                                                                                                                                                                                                                                                                                  
        &lt;span class="n"&gt;readings&lt;/span&gt;                                                                                                                                                                                                                                                                                      
    &lt;span class="k"&gt;join&lt;/span&gt;                                                                                                                                                                                                                                                                                                  
         &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&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;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reading&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
    &lt;span class="n"&gt;readings_with_rownums&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
    &lt;span class="n"&gt;readings_with_rownums&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rownum&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs in about &lt;code&gt;250ms&lt;/code&gt;. Actually, not that bad of a starting point. However, this timing does mean that the users will not perceive the response as being &lt;a href="https://www.nngroup.com/articles/response-times-3-important-limits/" rel="noopener noreferrer"&gt;instantaneous&lt;/a&gt; (~100ms). Let's try to do better!&lt;/p&gt;

&lt;p&gt;But how? Time to have a first look at the &lt;a href="https://www.postgresql.org/docs/current/using-explain.html" rel="noopener noreferrer"&gt;explain plan&lt;/a&gt; and try to get some clues. &lt;/p&gt;

&lt;p&gt;It's more pleasant to look at explain plans using visualisation tools instead of in text form (the way we get them from Postgres itself by default). You have several great alternatives, here's some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.pgmustard.com/" rel="noopener noreferrer"&gt;pgMustard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pganalyze.com/" rel="noopener noreferrer"&gt;pganalyze&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://explain.dalibo.com/" rel="noopener noreferrer"&gt;Dalibo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://explain.depesz.com/" rel="noopener noreferrer"&gt;Depesz explain&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the rest of this blog I'm going to use the one from Dalibo. Here's our first one:&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%2Frky2tam6nnbvhk0cvajl.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%2Frky2tam6nnbvhk0cvajl.png" alt="explain-analyze" width="800" height="1094"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A word on how to interpret them. The "flow" of data is from the bottom side upwards, as the arrow in the lower left part indicates. With other words, it should be read from bottom to top. I've also opened the relevant explain plan nodes which I want to explore further, and highlighted the row counts, which are the first thing that jumped out to me.  &lt;/p&gt;

&lt;p&gt;Looking at the row counts we can conclude the query efficiency is low. It reads, and then discards &lt;code&gt;99%&lt;/code&gt; of the rows before returning the final results. You can see that in the &lt;code&gt;Sort&lt;/code&gt; node, where &lt;code&gt;183500&lt;/code&gt; rows arrive as input from the nodes below, but then, only &lt;code&gt;500&lt;/code&gt; are actually returned in the final result set. This way to get our results has low &lt;a href="https://use-the-index-luke.com/sql/testing-scalability/data-volume" rel="noopener noreferrer"&gt;scalability&lt;/a&gt;, being very sensitive to increases in the dataset. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Side note&lt;/u&gt;&lt;/strong&gt;: Let's also consider the overall resource utilisation for a moment too. When the database is performing sorts like this, especially over and over again (like in an application used by several users concurrently) on potentially very large tables, you will most probably see a spike in the CPU utilisation, like I did one time. If possible, we should consider limiting CPU usage for situations where there are actually no alternatives. One of the reasons to do this could be that some cloud provider databases don't let you get more CPU without a proportional increase in RAM or other resources. &lt;/p&gt;

&lt;p&gt;Alright, time to move on. Let's proceed and look at other options.  &lt;/p&gt;

&lt;h3&gt;
  
  
  2. DISTINCT ON: &lt;code&gt;250ms&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I found this approach rather elegant actually, and I was rooting for it to perform better. To understand this works, you can have a look at the &lt;a href="https://www.postgresql.org/docs/current/sql-select.html" rel="noopener noreferrer"&gt;DISTINCT clause&lt;/a&gt; section in the Postgres docs. Here's how it looks like when we apply it to retrieve our readings:&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;explain&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffers&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;distinct&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meters&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="n"&gt;meters&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;as&lt;/span&gt; &lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; 
    &lt;span class="n"&gt;meters&lt;/span&gt; 
&lt;span class="k"&gt;join&lt;/span&gt; 
    &lt;span class="n"&gt;readings&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&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;meters&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="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&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;I didn't get a noticeable difference with regards to how fast it runs compared with the previous approach with the window functions. The explain plan looks quite similar to the one we have seen before:&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%2Fz6l6hwe5yge1dm3se49z.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%2Fz6l6hwe5yge1dm3se49z.png" alt="explain-analyze" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can observe a difference though. It doesn't contain the &lt;code&gt;WindowAgg&lt;/code&gt; node anymore you've seen before, in general it is indeed good to have less operations, but this didn't get us very far in our pursuit of reducing the query time. It's is still inefficient, reading a lot of rows and discarding the majority before returning the final results. &lt;/p&gt;

&lt;p&gt;One thing I noticed in the explain plan is the following detail which is definitely worth taking a closer look at when you see it in your explain plans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sort Method: external merge  Disk: 3968kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that the sort operation is being slowed down because it is forced to use the disk. This happens when the &lt;a href="https://www.citusdata.com/blog/2018/06/12/configuring-work-mem-on-postgres" rel="noopener noreferrer"&gt;&lt;code&gt;work_mem&lt;/code&gt;&lt;/a&gt; setting is too low given the size of the dataset. The sort can't be done fully in memory, so it has no choice but to "spill" the operation to disk. The default setting for &lt;code&gt;work_mem&lt;/code&gt; in Postgres is &lt;code&gt;4MB&lt;/code&gt;, which in this case proves to be too little.&lt;/p&gt;

&lt;p&gt;Let's increase this setting to make sure it's sufficient to do the sort in memory. But note that if you give it too much, it can be problematic because if the load increases a lot surprisingly, you can run out of memory - it's a balancing act.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set work_mem='16MB';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don't have to restart Postgres for this to take effect. For other settings we have to. &lt;/p&gt;

&lt;p&gt;We retry the query above, and I we can confirm that the sort does happen in memory now; the proof is that we can then the following  in the explain plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sort Method: quicksort  Memory: 14951kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did this make a difference though? Not really - we're still at ~&lt;code&gt;250ms&lt;/code&gt; response time. Ah well, no problem, we still got other options. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Side note&lt;/u&gt;&lt;/strong&gt;: This experiment is on my laptop, and the CPU / memory / storage are all on one machine. But for example when using Amazon RDS, the storage relies on another service - EBS, which is separated by a network to the database. So in those scenarios, avoiding disk "spills" like the one I showed you above will make &lt;em&gt;more&lt;/em&gt; of a difference because the data has to "travel" more between memory and disk. You might also like to know that recently AWS introduced the &lt;a href="https://aws.amazon.com/blogs/database/introducing-optimized-reads-for-amazon-rds-for-postgresql/" rel="noopener noreferrer"&gt;Optimised Reads&lt;/a&gt; option, where for these operations, the database instance can use a fast local NVMe SSD disk instead of the network attached EBS volume, so the disk spills are less impactful. &lt;/p&gt;

&lt;h3&gt;
  
  
  3. MAX(DATE): &lt;code&gt;115ms&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Time to switch gears again. What do you think of this approach, this time using SQL aggregate functions:&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;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;latest_reads_per_meter&lt;/span&gt; &lt;span class="k"&gt;as&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;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;   &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;date&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;reading_date&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt;
        &lt;span class="n"&gt;readings&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;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&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;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt;
    &lt;span class="n"&gt;meters&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt;
    &lt;span class="n"&gt;readings&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt;
    &lt;span class="n"&gt;latest_reads_per_meter&lt;/span&gt; &lt;span class="n"&gt;lrpm&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;lrpm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meters&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;and&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lrpm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reading_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As usual, the explain plan:&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%2F9y7goyeo0hb5526xy4sy.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%2F9y7goyeo0hb5526xy4sy.png" alt="explain-analyze" width="800" height="917"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This looks a bit different than what we have seen before. In a good way! Let's understand why. The key is that we're now doing the sort much &lt;code&gt;earlier&lt;/code&gt;, and so consequently we're discarding the irrelevant rows earlier. It doesn't "carry them over" all the way throughout the execution process. This means less memory consumed because the intermediate results are smaller. However, does it speed up our query? &lt;/p&gt;

&lt;p&gt;Indeed it does! Have a look at this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 114.942 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, some solid progress! We cut the runtime we started with in half. But can we do better? You bet, even reduce it by one order of magnitude. &lt;/p&gt;

&lt;h3&gt;
  
  
  4. Loose index scans: &lt;code&gt;13ms&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Let's have a look at how the loose index scan works, since by now you're probably already wondering, when are you going to "bring in" the indexes?!&lt;/p&gt;

&lt;p&gt;When creating indexes, the order of columns matters. The columns have to be defined exactly in this order I'm about to show you, with the "grouping" element first and then the other column which will be used for determining "latest" within a group.&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;index&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then write the query 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;explain&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffers&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;meters&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;as&lt;/span&gt; &lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt;
    &lt;span class="n"&gt;meters&lt;/span&gt;
&lt;span class="k"&gt;cross&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="k"&gt;lateral&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;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;reading&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt;
        &lt;span class="n"&gt;readings&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt;
        &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meters&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;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt;
        &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
    &lt;span class="k"&gt;limit&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;readings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 13.814 ms
&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%2Fx17j3wnef91e6ca87v8b.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%2Fx17j3wnef91e6ca87v8b.png" alt="explain-analyze" width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alright, now we're talking! This is much faster, but let's find out why. For starters - you don't see the &lt;code&gt;183500&lt;/code&gt; rows anywhere at all in the explain plan! We are also not sorting anything anymore, because the index keeps our data sorted already. &lt;/p&gt;

&lt;p&gt;But, let's push the envelope to see how far we can take this. Let's open the &lt;code&gt;IO &amp;amp; Buffers&lt;/code&gt; tab of the index scan node in the above diagram and have a look in there. You don’t get buffers information by default, so make sure to use the &lt;a href="https://postgres.ai/blog/20220106-explain-analyze-needs-buffers-to-improve-the-postgres-query-optimization-process" rel="noopener noreferrer"&gt;buffers&lt;/a&gt; option. Here it is: &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%2Fugq601b5q93fiyjfufet.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%2Fugq601b5q93fiyjfufet.png" alt="explain-plans" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's take a step back and consider how the index-based retrieval works at a high level. There are essentially two steps. What happens is that as a first step, the B-tree index is traversed to determine the rows that match the query predicate, however, after this step, Postgres has to now actually go ahead and retrieve the rows from the table (or heap). &lt;/p&gt;

&lt;p&gt;As we can see from the explain plan, to perform the two steps described above it amounts to &lt;code&gt;2000&lt;/code&gt; blocks read, so about &lt;code&gt;16MiB&lt;/code&gt; (2000 * 8 kibibytes). A block (or page) is the fundamental unit by which Postgres stores data, have a look &lt;a href="https://www.postgresql.org/docs/current/storage-page-layout.html" rel="noopener noreferrer"&gt;here&lt;/a&gt; for details. I should also add that when you see &lt;code&gt;Nested loop&lt;/code&gt; nodes in the explain plans, you have to be careful to not mistakenly conclude that the buffer count displayed is of distinct buffers - if Postgres reads the same block several times it will simply add these up towards the total amount and not differentiate.&lt;/p&gt;

&lt;p&gt;Let's try to put the &lt;code&gt;2000&lt;/code&gt; blocks in context and try to interpret it a bit. For example we can ask ourselves: how many blocks does the table &lt;code&gt;readings&lt;/code&gt; have in total?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT relpages FROM pg_class WHERE relname = 'readings';
 relpages 
----------
     1350
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Answer: 1350. So it seems to be reading &lt;em&gt;more&lt;/em&gt; blocks with this index-based approach than are in the actual table! If we'd just do a sequential scan and simply read the entire table, sort, and discard, like you’ve seen in the approaches before this one, we'd read only 1350 blocks. What you're witnessing is a trade-off to watch out for and factor in your design. Using an index does lead to more speed (no sorts), but actually adds up to more I/O (more blocks touched) operations. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Side note&lt;/u&gt;&lt;/strong&gt;: it is good to know that for example, if you're on Amazon Aurora or other cloud databases, you pay for every block that it has to retrieve from storage because it couldn't find it in memory. A nice cautionary tale about keeping the I/O under control on Aurora would be &lt;a href="https://gridium.com/migrating-to-aurora-easy-except-the-bill/" rel="noopener noreferrer"&gt;this one&lt;/a&gt;. However, this situation in my generated dataset is a byproduct of how "narrow" my tables are (small number of columns) - it's an artificial setup after all. If there would be more columns, then the number of pages in the table would be much larger, so the B-tree traversing would add up to comparatively much less blocks than there are in the actual table. &lt;/p&gt;

&lt;p&gt;Let's see if we can reduce the number of blocks. You might wonder, can we avoid the extra step (reading from the table after reading from the index)? Exactly! &lt;/p&gt;

&lt;h3&gt;
  
  
  5. Loose index-only scans: &lt;code&gt;5ms&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We can implement a so-called &lt;code&gt;index-only scan&lt;/code&gt;. To do this, we use the &lt;code&gt;include&lt;/code&gt; option when creating the index, like so:&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;index&lt;/span&gt; &lt;span class="n"&gt;idx_with_including&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;readings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's now retry the query and look at the relevant tabs again. &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%2Fzi9lp4arzimbjhk66qqd.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%2Fzi9lp4arzimbjhk66qqd.png" alt="explain-plan" width="800" height="550"&gt;&lt;/a&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%2Fmipzxnczaodepezopmku.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%2Fmipzxnczaodepezopmku.png" alt="explain-plans" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two important things to observe here. First, notice the &lt;code&gt;Heap Fetches: 0&lt;/code&gt;, which indicates that it does not go to the heap to get the rows because they are in the index already. Secondly, the total number of blocks is now &lt;code&gt;1501&lt;/code&gt;, so 500 less than before. Another confirmation that it indeed doesn't read anything from the heap.  &lt;/p&gt;

&lt;p&gt;It can happen that you try this out, and don't see the &lt;code&gt;Heap Fetches: 0&lt;/code&gt; in your setup, and you might wonder why. This can happen when the &lt;a href="https://www.postgresql.org/docs/current/storage-vm.html" rel="noopener noreferrer"&gt;visibility map&lt;/a&gt; is not up to date, and Postgres has no other option but to go to the heap to get the visibility information about a row. As the visibility map is kept up to date by the &lt;a href="https://www.postgresql.org/docs/current/routine-vacuuming.html" rel="noopener noreferrer"&gt;&lt;code&gt;autovacuum&lt;/code&gt;&lt;/a&gt; process, it is important to regularly visit the configuration settings to make sure it is able to keep up.  &lt;/p&gt;

&lt;p&gt;Let's look at the timing. How fast did we get it?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 5.448 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great stuff! Let's now understand it a bit better how exactly the B-tree index is traversed for retrieving the data. In Postgres, we have three types of nodes in a B-tree: the root node, intermediate nodes (used only for traversal) and leaf nodes (these contain what we’re interested in: pointers to tuples on the heap or included data). Here's a diagram showing what happens, where each level contains the type of nodes I just mentioned.&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%2Fj9jb0nvhnklv22m6m742.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%2Fj9jb0nvhnklv22m6m742.png" alt="b-tree-vis" width="800" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each row returned in the final result, it will do a number of 3 index page read operations - first for the root page, then for an intermediate B-tree page, and lastly it will read the leaf page, from where it will collect the included reading. In the diagram above, I have marked these steps with &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;, which are repeated 500 times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;u&gt;Side note&lt;/u&gt;&lt;/strong&gt;: including columns in the index does increase its overall size, which consequently increases the time needed to traverse it. In some cases, this might make the difference, and it will influence the overall retrieval timing in such a way that it is not beneficial anymore.  &lt;/p&gt;

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

&lt;p&gt;Quite the journey we've been on! After seeing all these querying techniques, we've finally arrived at our destination - you've seen a loose index-only scan in action, which gave us our results in the shortest time. Note though, that as the saying goes, there is no free lunch: every index gives the database more work to do at every insert, so you will have to decide on a case by case basis if it's worth it, using measurements. Also, the speedup does depend on the data distribution. In the scenario described above, we have many readings for every meter. Your mileage may vary. But it’s always worth a try! &lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com/a/7630564" rel="noopener noreferrer"&gt;Stack Overflow&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/yugabyte/loose-index-scan-aka-skip-scan-in-postgresql-1jfo"&gt;Loose Index Scan aka Skip Scan in PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.timescale.com/blog/select-the-most-recent-record-of-many-items-with-postgresql/" rel="noopener noreferrer"&gt;Select the Most Recent Record (of Many Items) With PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cybertec-postgresql.com/en/performance-tuning/" rel="noopener noreferrer"&gt;PERFORMANCE TUNING: MAX AND GROUP BY&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.postgresql.%20org/wiki/Loose_indexscan" rel="noopener noreferrer"&gt;Loose index scan Postgres wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/message-id/c5c5c974714a47f1b226c857699e8680@opammb0561.comp.optiver.com" rel="noopener noreferrer"&gt;RE: Index Skip Scan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading! &lt;/p&gt;

</description>
      <category>sql</category>
      <category>performance</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>To UUID, or not to UUID, that is the primary key question</title>
      <dc:creator>Mircea Cadariu</dc:creator>
      <pubDate>Fri, 02 Feb 2024 20:28:01 +0000</pubDate>
      <link>https://dev.to/mcadariu/using-uuids-as-primary-keys-3e7a</link>
      <guid>https://dev.to/mcadariu/using-uuids-as-primary-keys-3e7a</guid>
      <description>&lt;p&gt;For most scenarios, it's beneficial to always set up a primary key for your tables. However, you might be in doubt about what you should go for. There are several options - in this post you'll learn why should you consider UUIDs. Like with many other such technical decisions it's a trade-off, however my goal is that you will make an informed one and know what to expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are UUIDs
&lt;/h2&gt;

&lt;p&gt;The abbreviation stands for &lt;strong&gt;U&lt;/strong&gt;niversal &lt;strong&gt;U&lt;/strong&gt;nique &lt;strong&gt;ID&lt;/strong&gt;entifier. UUIDs are sequences of 128 bits and have been invented to uniquely represent data in computer systems. In Postgres, you have the UUID &lt;a href="https://www.postgresql.org/docs/current/datatype-uuid.html" rel="noopener noreferrer"&gt;data type&lt;/a&gt; you can use for column types. This is what they look like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;Note that what you see above is the hex-encoded string representation, it is not how they are stored internally. &lt;/p&gt;

&lt;h2&gt;
  
  
  Primary key options
&lt;/h2&gt;

&lt;p&gt;When creating a database table, one of the first decisions you make is whether or not you'll use a &lt;a href="https://www.cockroachlabs.com/blog/how-to-choose-a-primary-key/" rel="noopener noreferrer"&gt;natural or synthetic&lt;/a&gt; primary key. If you'll go for a synthetic one, UUIDs are an option. They are not the &lt;em&gt;only&lt;/em&gt; option. Like with many other software-related decisions, this is not clear-cut and the trade-offs involved can be a source of long debates. For contrast, let's have a look at probably the most popular alternative solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-incrementing integers
&lt;/h3&gt;

&lt;p&gt;You can use auto-incrementing integers (1, 2, 3... etc) that are created by the database. Postgres creates these IDs using an object called &lt;a href="https://www.postgresql.org/docs/current/sql-createsequence.html" rel="noopener noreferrer"&gt;&lt;code&gt;sequence&lt;/code&gt;&lt;/a&gt;. It's essentially a single row table that keeps track of the current number and can give the next one. &lt;/p&gt;

&lt;p&gt;Advantages: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;easier to work with (you can remember them)&lt;/li&gt;
&lt;li&gt;predictable &lt;/li&gt;
&lt;li&gt;occupy a smaller size on disk &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disadvantages: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can expose information, like user count&lt;/li&gt;
&lt;li&gt;potential bottleneck in distributed systems (centralisation)&lt;/li&gt;
&lt;li&gt;need to sync them when upgrading using replication&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  UUIDs
&lt;/h3&gt;

&lt;p&gt;Let's have a look at UUIDs now. &lt;/p&gt;

&lt;p&gt;Advantages: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more secure - you can't guess them&lt;/li&gt;
&lt;li&gt;great fit for &lt;a href="https://questdb.io/blog/uuid-coordination-free-unique-keys" rel="noopener noreferrer"&gt;use-cases&lt;/a&gt; where centralised generation of IDs is not feasible &lt;/li&gt;
&lt;li&gt;no need to reset sequences in upgrades / migrations &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disadvantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;occupy larger size on disk&lt;/li&gt;
&lt;li&gt;harder to work with, you can't remember them&lt;/li&gt;
&lt;li&gt;randomness impacts internal operations (B-Tree operations)&lt;/li&gt;
&lt;li&gt;WAL amplification &lt;a href="https://www.2ndquadrant.com/en/blog/sequential-uuid-generators/" rel="noopener noreferrer"&gt;1&lt;/a&gt; &lt;a href="https://www.2ndquadrant.com/en/blog/sequential-uuid-generators-ssd/" rel="noopener noreferrer"&gt;2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;can't use directly for build pagination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's say your use-case is better served by UUIDs as a primary key. What are you getting into? To understand the trade-off, let's start by looking at the data structure that Postgres creates for every primary key you create. &lt;/p&gt;

&lt;h2&gt;
  
  
  The structure of B-tree indexes
&lt;/h2&gt;

&lt;p&gt;For all primary keys you define, Postgres creates an index  for them automatically. This index is backed by a B-tree data structure. In order for B-trees to be able to do operations like primary key lookups and range queries very fast, the index pages are kept balanced and sorted at all times. Here is how one looks like, from the paper by &lt;a href="https://dl.acm.org/doi/pdf/10.1145/319628.319663" rel="noopener noreferrer"&gt;P. Lehman and S. Yao&lt;/a&gt; (the Postgres implementation is based).&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%2F5sgdo515m5p0innoitkl.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%2F5sgdo515m5p0innoitkl.png" alt="tree" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The levels-based structure make reads faster because the number of steps to find and return a specific entry (in the picture - the "associated information" node) are significantly reduced. &lt;/p&gt;

&lt;h2&gt;
  
  
  Random vs. Time-ordered UUIDs
&lt;/h2&gt;

&lt;p&gt;Given how B-trees work, random UUIDs are not ideal, because this means a lot of "work" (page modifications) have to happen in order to keep the tree pages balanced and sorted with every new element we store. &lt;/p&gt;

&lt;p&gt;Can this be alleviated? Let's have a look at &lt;a href="https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7" rel="noopener noreferrer"&gt;UUID version 7&lt;/a&gt;. An important difference with other versions is that it has a timestamp-based component at the beginning, which means they manifest a natural ordering. This is good news! The database will do less work to keep the B-tree balanced, as all new elements will always go on the "right side", and a lot of the rest of the tree will remain untouched.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://fillumina.wordpress.com/2023/02/06/the-primary-key-dilemma-id-vs-uuid-and-some-practical-solutions/" rel="noopener noreferrer"&gt;TSIDs&lt;/a&gt; are another variant of UUIDs. Vlad Mihalcea considers this &lt;a href="https://vladmihalcea.com/uuid-database-primary-key" rel="noopener noreferrer"&gt;the best option&lt;/a&gt; of UUIDs for primary key. While also being time-ordered like the UUIDv7, they can be stored in a 8 bytes &lt;code&gt;bigint&lt;/code&gt; data type, instead of 16 like the v7. &lt;/p&gt;

&lt;p&gt;Let's conduct a small experiment with these variants mentioned so far (random UUIDs, UUIDv7 and TSIDs). For this, I'm inserting all at once about &lt;code&gt;40k&lt;/code&gt; rows in a simple table which has a UUID as primary key. In the case of the TSID, I use &lt;code&gt;bigint&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can't yet generate all these UUID types in Postgres directly. In Java, there are several options of libraries you can use to generate such UUIDs. This would be one option for UUID v7: &lt;a href="https://github.com/cowtowncoder/java-uuid-generator" rel="noopener noreferrer"&gt;java-uuid-generator&lt;/a&gt;. And this one for TSIDs: &lt;a href="https://github.com/vladmihalcea/hypersistence-tsid" rel="noopener noreferrer"&gt;hypersistence-tsid&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment: inserting 40k rows
&lt;/h2&gt;

&lt;p&gt;I ran the same inserts of the 40k rows, for 5 test runs per each UUID. When moving on to the next UUID type I reset the statistics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results: timing
&lt;/h3&gt;

&lt;p&gt;It took roughly the same time for each. Despite having a lot more work to do to keep the B-tree balanced for the random ones, Postgres processed all the insertions very fast anyway. &lt;/p&gt;

&lt;p&gt;Only looking at timing is inconclusive. It's better to also look at amount of pages read/written (or with other words, amount of I/O) that were done for each, as timing is more &lt;a href="https://postgres.ai/blog/20220106-explain-analyze-needs-buffers-to-improve-the-postgres-query-optimization-process" rel="noopener noreferrer"&gt;volatile&lt;/a&gt;. For example, in production we might or might not have all the dataset present in the shared buffers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results: I/O
&lt;/h3&gt;

&lt;p&gt;I queried &lt;a href="https://www.postgresql.org/docs/current/monitoring-stats.html" rel="noopener noreferrer"&gt;pg_statio_all_indexes&lt;/a&gt; after every one of the 5 runs per each UUID in my experiment, and looked at the difference in the number of blocks for the primary key index. &lt;/p&gt;

&lt;p&gt;I noted down the following columns after every run: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;idx_blks_read&lt;/code&gt; (number of disk blocks read)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;idx_blks_hit&lt;/code&gt; (number of shared buffer hits)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's have a look at the results!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TSID&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;idx_blks_read&lt;/th&gt;
&lt;th&gt;idx_blks_hit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;td&gt;88573&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;245&lt;/td&gt;
&lt;td&gt;177552&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;367&lt;/td&gt;
&lt;td&gt;266532&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;490&lt;/td&gt;
&lt;td&gt;327560&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;611&lt;/td&gt;
&lt;td&gt;372717&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Index size: &lt;code&gt;4888 kB&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UUID V7&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;idx_blks_read&lt;/th&gt;
&lt;th&gt;idx_blks_hit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;88943&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;513&lt;/td&gt;
&lt;td&gt;163091&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;769&lt;/td&gt;
&lt;td&gt;233844&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1012&lt;/td&gt;
&lt;td&gt;310091&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1245&lt;/td&gt;
&lt;td&gt;387387&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Index size: &lt;code&gt;9960 kB&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Random UUID&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;idx_blks_read&lt;/th&gt;
&lt;th&gt;idx_blks_hit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;234&lt;/td&gt;
&lt;td&gt;89027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;459&lt;/td&gt;
&lt;td&gt;207315&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;628&lt;/td&gt;
&lt;td&gt;340988&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;920&lt;/td&gt;
&lt;td&gt;474911&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1149&lt;/td&gt;
&lt;td&gt;608634&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Index size: &lt;code&gt;8968 kB&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We can see that for the UUIDv7 and TSID runs, the numbers grow at a different rate than the random ones. Zooming-in on the &lt;code&gt;idx_blks_read&lt;/code&gt; column only, the blocks read from disk, we see that TSIDs accumulated only half compared with the others. Expected, due to the data type storage difference (8 vs 16 bytes). We notice this proportion reflected also in the total size of the index. &lt;/p&gt;

&lt;p&gt;I made a simple graph with the number of blocks added up (both from memory and from disk) after each run where it is visible that the growth is indeed faster for random ones.&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%2F4w5wvjc6i55q1naxdnsc.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%2F4w5wvjc6i55q1naxdnsc.png" alt="Image" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing that I found initially surprising looking at the results, is the number of index blocks from shared buffers appearing in the statistics (&lt;code&gt;idx_blks_hit&lt;/code&gt;). When inserting ~44k rows with TSIDs as primary keys, we see ~88k in the &lt;code&gt;idx_blks_hit&lt;/code&gt;. This looks like roughly 2 page hits for every index entry that we have inserted. What exactly are these 2 buffer hits per element? I was kind of expecting just 1 - the rightmost page of the index. Let's explore the Postgres source code in order to understand why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Postgres B-tree index fastpath optimisation
&lt;/h3&gt;

&lt;p&gt;The answer can be found in &lt;a href="https://github.com/postgres/postgres/blob/master/src/backend/access/nbtree/nbtinsert.c#L282" rel="noopener noreferrer"&gt;this comment&lt;/a&gt; in &lt;code&gt;nbtinsert.c&lt;/code&gt;. The one extra hit is for the root page. The other hit is for the page where the new entry will be inserted, to which we get to, from the root page, when we insert a new element. &lt;/p&gt;

&lt;p&gt;But note a &lt;code&gt;fastpath optimization&lt;/code&gt; is mentioned (caching the rightmost leaf page). When the conditions are met to apply this, it doesn't read the root page anymore for every insert. Why is it not happening for my experiment though?&lt;/p&gt;

&lt;p&gt;The answer to this question is &lt;a href="https://github.com/postgres/postgres/blob/master/src/backend/access/nbtree/nbtinsert.c#L1422" rel="noopener noreferrer"&gt;here&lt;/a&gt;, again in &lt;code&gt;nbtinsert.c&lt;/code&gt;. This optimisation &lt;a href="https://github.com/postgres/postgres/blob/master/src/backend/access/nbtree/nbtinsert.c#L1422" rel="noopener noreferrer"&gt;does not get applied&lt;/a&gt; for small indexes like the one I created with 44k entries. Fair enough. Let me try out a larger one, say 30 million entries, to see this optimisation in action.&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;experiment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;bigserial&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;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;experiment&lt;/span&gt; &lt;span class="k"&gt;select&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;30000000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I can now query the &lt;code&gt;pg_statio_all_indexes&lt;/code&gt; and expect to &lt;em&gt;not&lt;/em&gt; see 30 million x2 in the &lt;code&gt;idx_blks_hit&lt;/code&gt; column. And indeed! Getting just &lt;code&gt;30639018&lt;/code&gt; and not ~&lt;code&gt;6000000&lt;/code&gt;. This is the effect of the fastpath optimisation where the rightmost leaf is cached, so it's accessed directly and not through the root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partitioning and UUIDs
&lt;/h2&gt;

&lt;p&gt;A bonus for the UUIDv7 or TSIDs, you can set up &lt;a href="https://elixirforum.com/t/partitioning-postgres-tables-by-timestamp-based-uuids/60916" rel="noopener noreferrer"&gt;partitioning based on the timestamp component&lt;/a&gt;. Why does this matter?&lt;/p&gt;

&lt;p&gt;Partitioning is very useful because it helps with predictability of operations. It makes reads faster because, while the table might grow indefinitely, the database will not scan it in its entirety, but only a smaller part of it (the relevant partition). Also, if you need to insert a lot of data at once, partitioning enables constant &lt;a href="https://aws.amazon.com/blogs/database/designing-high-performance-time-series-data-tables-on-amazon-rds-for-postgresql" rel="noopener noreferrer"&gt;ingestion rates&lt;/a&gt;. &lt;/p&gt;

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

&lt;p&gt;Timestamp-based UUIDs make the negative trade-offs involved in choosing between UUIDs and auto-incrementing numeric primary keys. It still depends on your specific use-case though. I suggest looking at TSIDs because they generated the least I/O in my experiments and can also be stored in 8 bytes instead of 16, which is a welcome bonus. &lt;/p&gt;

&lt;p&gt;Especially with cloud databases like Aurora from AWS being adopted more and more, we're working with a different pricing model -- pay per I/O consumed (the requests to read from the storage when it can't be found in the shared buffers). Applying strategies like going for modern UUIDs which can be processed more efficiently by the database helps to keep the costs low. Even if you're not looking to reduce the bill, being aware of, and reducing the amount of work that has to be done in the background by the database, gets you more overall throughput and better resource utilisation. If Postgres spends less time keeping the B-tree balanced it can instead can handle more of your business use-cases. &lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;Photo by James Wheeler: &lt;a href="https://www.pexels.com/photo/photo-of-pathway-surrounded-by-fir-trees-1578750/" rel="noopener noreferrer"&gt;https://www.pexels.com/photo/photo-of-pathway-surrounded-by-fir-trees-1578750/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
