<?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: Eugene Leontev </title>
    <description>The latest articles on DEV Community by Eugene Leontev  (@madmatvey).</description>
    <link>https://dev.to/madmatvey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F255805%2Fb499dcf5-e059-42e0-91eb-32868bef93df.jpeg</url>
      <title>DEV Community: Eugene Leontev </title>
      <link>https://dev.to/madmatvey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/madmatvey"/>
    <language>en</language>
    <item>
      <title>Optimizing SQL Queries or Tracking Dangerous Criminals</title>
      <dc:creator>Eugene Leontev </dc:creator>
      <pubDate>Fri, 26 Jun 2026 09:05:00 +0000</pubDate>
      <link>https://dev.to/madmatvey/optimizing-sql-queries-or-tracking-dangerous-criminals-58k</link>
      <guid>https://dev.to/madmatvey/optimizing-sql-queries-or-tracking-dangerous-criminals-58k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://eugenetheengineer.com/posts/sql-optimization-or-criminal-tracking/" rel="noopener noreferrer"&gt;EugeneTheEngineer.com&lt;/a&gt;&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2026 update:&lt;/strong&gt; This case study started the PostgreSQL series on this blog. Continue with the &lt;a href="https://eugenetheengineer.com/postgresql-performance-playbook/" rel="noopener noreferrer"&gt;PostgreSQL Performance Playbook&lt;/a&gt; – especially &lt;a href="https://eugenetheengineer.com/assets/posts/how-we-reduced-postgresql-query-time-from-250ms-to-20ms/" rel="noopener noreferrer"&gt;250ms → 20ms&lt;/a&gt;, &lt;a href="https://eugenetheengineer.com/posts/the-index-lie-why-just-add-an-index-can-make-your-query-slower/" rel="noopener noreferrer"&gt;The Index Lie&lt;/a&gt;, and &lt;a href="https://eugenetheengineer.com/posts/stop-reading-explain-analyze-start-cross-examining-it/" rel="noopener noreferrer"&gt;Cross-Examining EXPLAIN&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I believe that virtually every project using Ruby on Rails and Postgres as its main backend tools is in a constant struggle between development speed, code readability/maintainability, and project performance in production. I will share my experience balancing these three pillars in a case where readability and performance suffered initially, but in the end, I managed to achieve what several talented engineers had unsuccessfully attempted before me.&lt;/p&gt;

&lt;p&gt;The whole story will take several parts. This is the first one, where I'll talk about what PMDSC is for optimizing SQL queries, share useful tools for measuring query performance in Postgres, and remind you of one useful old cheat sheet that is still relevant.&lt;/p&gt;

&lt;p&gt;Now, after some time has passed, in hindsight, I realize that I didn't expect everything to work out in this case. Therefore, this post will be more useful for bold and less experienced developers than for super-seniors who have seen Rails with raw SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;At Appbooster, we focus on promoting mobile applications. To easily propose and test hypotheses, we develop several of our applications. The backend for most of them is a Rails API and Postgresql.&lt;/p&gt;

&lt;p&gt;The hero of this publication has been developed since the end of 2013—just after &lt;code&gt;rails 4.1.0.beta1&lt;/code&gt; was released. Since then, the project has grown into a fully loaded web application that runs on several servers in Amazon EC2 with a separate database instance in Amazon RDS &lt;code&gt;(db.t3.xlarge with 4 vCPU and 16 GB RAM)&lt;/code&gt;. Peak loads reach 25k RPM, with an average load of 8-10k RPM during the day.&lt;/p&gt;

&lt;p&gt;This story began with the database instance, or more precisely, with its credit balance.&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%2Feugenetheengineer.com%2Fassets%2Fimg%2Fsql-optimization%2Frds-credit-balance-low.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%2Feugenetheengineer.com%2Fassets%2Fimg%2Fsql-optimization%2Frds-credit-balance-low.png" alt="RDS Credit balance low" width="800" height="364"&gt;&lt;/a&gt;{: .normal }&lt;/p&gt;

&lt;p&gt;How a "t" type Postgres instance works in Amazon RDS: if your database operates with an average CPU utilization below a certain threshold, credits accumulate in your account, which the instance can spend on CPU consumption during high-load hours—this allows you to avoid overpaying for server power and cope with high loads. More details on what and how much you pay using AWS can be found in &lt;a href="https://medium.com/@kelion/%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B8%D0%B7%D0%B8%D1%80%D1%83%D0%B5%D0%BC-%D1%80%D0%B0%D1%81%D1%85%D0%BE%D0%B4%D1%8B-%D0%BD%D0%B0-amazon-aws-81354d27dfaa" rel="noopener noreferrer"&gt;our CTO's article (RU)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At some point, the credit balance was depleted. For some time, this was not given much importance, as the credit balance could be replenished with money—this cost us about $20 per month, which is not very significant compared to the overall expenses for renting computational resources. In product development, the focus is usually on tasks formulated from business requirements. Increased consumption of CPU resources by the database server falls under technical debt and is covered by small expenses for purchasing credit balance.&lt;/p&gt;

&lt;p&gt;One fine day, I wrote in the daily summary that I was very tired of extinguishing the periodic "fires" that occurred in different parts of the project. If this continues, the burned-out developer will spend time on business tasks. On the same day, I approached the project manager, explained the situation, and asked for time to investigate the causes of the periodic fires and make repairs. After receiving approval, I began to collect data from various monitoring systems.&lt;/p&gt;

&lt;p&gt;We use New Relic to track the overall response time over the course of a day. The picture looked 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnynf9y95xiarrndkqc8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnynf9y95xiarrndkqc8r.png" alt="Newrelic dashboard screenshoot" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The yellow highlighted part on the graph represents the response time taken by Postgres. As can be seen, sometimes the response time reached 1000 ms, and most of the time, it was the database that was pondering the response. This means we need to look at what's happening with the SQL queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  PMDSC is a simple and understandable practice for &lt;del&gt;any tedious&lt;/del&gt; task of optimizing SQL queries.
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Play it!&lt;/li&gt;
&lt;li&gt;Measure it!&lt;/li&gt;
&lt;li&gt;Draw it!&lt;/li&gt;
&lt;li&gt;Suppose it!&lt;/li&gt;
&lt;li&gt;Check it!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Play it!
&lt;/h3&gt;

&lt;p&gt;Perhaps the most important part of the whole practice. When someone mentions "Optimizing SQL queries," it tends to provoke yawning and boredom in the vast majority of people. When you say "Detective investigation and tracking down dangerous villains," it engages and sets you in the right mood. So, it's important to get into the game. I enjoyed playing detective. I imagined that database problems were either dangerous criminals or rare diseases. And I imagined myself as Sherlock Holmes, Lieutenant Columbo, or Dr. House. Choose the hero to your liking and go ahead!&lt;/p&gt;

&lt;h3&gt;
  
  
  Measure It!
&lt;/h3&gt;

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

&lt;p&gt;For analyzing query statistics, I installed &lt;a href="https://github.com/ankane/pghero" rel="noopener noreferrer"&gt;PgHero&lt;/a&gt;. This is a very convenient way to read data from the &lt;code&gt;pg_stat_statements&lt;/code&gt; extension for Postgres. We go to &lt;code&gt;/queries&lt;/code&gt; and look at the statistics of all queries for the last day. The default sorting of queries is by the &lt;code&gt;Total Time column&lt;/code&gt; - the proportion of total time the database processes the query - a valuable source in the search for suspects. &lt;code&gt;Average Time&lt;/code&gt; - how long, on average, a query takes to execute. &lt;code&gt;Calls&lt;/code&gt; - how many queries were there in the selected time frame. PgHero considers slow queries those that were executed more than 100 times per day and took on average more than 20 milliseconds. The list of slow queries is on the first page, right after the list of duplicate indexes.&lt;/p&gt;

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

&lt;p&gt;We take the first one in the list and look at the details of the query, where you can immediately see its explain analyze. If the planning time is significantly less than the execution time, it means there is something wrong with this query, and we focus our attention on this suspect.&lt;/p&gt;

&lt;p&gt;PgHero has its way of visualization, but I preferred using &lt;a href="https://explain.depesz.com/" rel="noopener noreferrer"&gt;explain.depesz.com&lt;/a&gt; by copying the data from explain analyze there.&lt;/p&gt;

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

&lt;p&gt;One of the suspect queries uses an Index Scan. The visualization shows that this index is not efficient and is a weak point—highlighted in red. Great! We have examined the suspect's tracks and found an important clue! Justice is inevitable!&lt;/p&gt;

&lt;h3&gt;
  
  
  Draw it!
&lt;/h3&gt;

&lt;p&gt;Let's draw a set of data used in the problematic part of the query. It will be useful to compare it with the data covered by the index.&lt;/p&gt;

&lt;p&gt;A bit of context. We were testing one of the ways to retain users in the application—something like a lottery where you can win some internal currency. You place a bet, guess a number from 0 to 100, and take the whole bank if your number is closest to what the random number generator got. We called it the "Arena," and the draws were called "Battles."&lt;/p&gt;

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

&lt;p&gt;In the database at the time of the investigation, there were about five hundred thousand records of battles. In the problematic part of the query, we are looking for battles where the bet does not exceed the user's balance and the status of the battle is "waiting for players." We see that the intersection of sets (highlighted in orange) is a very small number of records.&lt;/p&gt;

&lt;p&gt;The index used in the suspected part of the query covers all created battles by the created_at field. The query goes through 505,330 records, selects 40 of them, and filters out 505,290. It looks very wasteful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suppose it!
&lt;/h3&gt;

&lt;p&gt;We make a hypothesis. What will help the database find forty records out of five hundred thousand? Let's try to create an index that covers the bet field, but only for battles with the status "waiting for players" — a partial index.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:arena_battles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;where: &lt;/span&gt;&lt;span class="s2"&gt;"status = 'waiting_for_players'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"index_arena_battles_on_bet_partial_status"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/10/indexes-partial.html" rel="noopener noreferrer"&gt;Partial index&lt;/a&gt; - exists only for those records that meet the condition: the status field is equal to "waiting_for_players" and indexes the bet field - exactly what is in the query condition. It is very beneficial to use this index: it takes up only 40 kilobytes and does not cover battles that have already been played and are not needed for our selection. For comparison - the index index_arena_battles_on_created_at, which was used by the suspect, takes up about 40 MB, and the battles table is about 70 MB. This index can be safely deleted if it is not used by other queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check it!
&lt;/h3&gt;

&lt;p&gt;We deploy a migration with a new index to production and observe how the response of the endpoint with battles changes.&lt;/p&gt;

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

&lt;p&gt;The graph shows when we deployed the migration. In the evening of December 6, the response time decreased by about 10 times, from ~500 ms to ~50 ms. The suspect in the trial received the status of a prisoner and is now in jail. Excellent!&lt;/p&gt;

&lt;h3&gt;
  
  
  Prison Break
&lt;/h3&gt;

&lt;p&gt;A few days later, we realized that we were celebrating too early. It seems the prisoner found accomplices, developed and executed an escape 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyizr7erpc4rqt2fff321.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyizr7erpc4rqt2fff321.png" alt="Battles SQL visualisation" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the morning of December 11, the Postgres query planner decided that using the fresh partial index was no longer beneficial and started using the old one again.&lt;/p&gt;

&lt;p&gt;We are back to the "Suppose it!" stage. We are gathering a differential diagnosis, in the spirit of Dr. House:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maybe we need to optimize Postgres settings;&lt;/li&gt;
&lt;li&gt;Perhaps, perform a minor update of Postgres to a new version (9.6.11 -&amp;gt; 9.6.15);&lt;/li&gt;
&lt;li&gt;Or maybe, carefully study which SQL query Rails is forming?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We checked all three hypotheses. The last one led us to the next accomplice.&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="nv"&gt;"arena_battles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"arena_battles"&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"arena_battles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"status"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'waiting_for_players'&lt;/span&gt; 
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arena_battles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bet&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;98&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="k"&gt;AND&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;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&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;FROM&lt;/span&gt; &lt;span class="n"&gt;arena_participations&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;arena_battle_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arena_battles&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arena_profile_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;46809&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;))&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"arena_battles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"created_at"&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; 
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's walk through this SQL together. We select all fields of battles from the battles table where the status is "waiting for players" and the bet is less than or equal to a certain number. So far, so good. The next part of the condition looks awful.&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;NOT&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="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;arena_participations&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;arena_battle_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arena_battles&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arena_profile_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;46809&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 are looking for a non-existent result of a subquery. Fetch the first field from the battle participation table, where the battle identifier matches and the participant's profile belongs to our player. I'll try to draw the set described in the subquery.&lt;/p&gt;

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

&lt;p&gt;It's hard to make sense of, but in the end, this subquery was an attempt to exclude battles in which the player is already participating. We look at the overall explain of the query and see &lt;code&gt;Planning time: 0.180 ms&lt;/code&gt;, &lt;code&gt;Execution time: 12.119 ms&lt;/code&gt;. We found an accomplice!&lt;/p&gt;

&lt;p&gt;Now it's time for my favorite cheat sheet, which has been circulating on the internet since 2008. 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4sd77c1p85h9wtzb660a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4sd77c1p85h9wtzb660a.png" alt="SQL Optimisation cheet sheet" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes! Whenever a query includes something that should exclude a certain number of records based on data from another table, this meme with a beard and curls should come to mind.&lt;/p&gt;

&lt;p&gt;Actually, here's what we need:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4z84pe55n8ilgxn22si.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4z84pe55n8ilgxn22si.jpeg" alt="Actual SQL Optimisation cheet sheet" width="799" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save this picture for yourself, and even better, print it out and hang it in several places in the office.&lt;/p&gt;

&lt;p&gt;We rewrite the subquery to LEFT JOIN WHERE B.key IS NULL, and we get:&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="nv"&gt;"arena_battles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"arena_battles"&lt;/span&gt; 
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;arena_participations&lt;/span&gt; 
   &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;arena_participations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arena_battle_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arena_battles&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arena_participations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arena_profile_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;46809&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"arena_battles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"status"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'waiting_for_players'&lt;/span&gt; 
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arena_battles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bet&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;98&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="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arena_participations&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;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;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"arena_battles"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"created_at"&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corrected query runs immediately on both tables. We joined the table with records of user participation in battles "on the left" and added a condition that the participation identifier does not exist. We look at the explain analyze of the resulting query: &lt;code&gt;Planning time: 0.185 ms&lt;/code&gt;, &lt;code&gt;Execution time: 0.337 ms&lt;/code&gt;. Excellent! Now the query planner will not hesitate to use the partial index, but will use the fastest option. The escaped prisoner and his accomplice are sentenced to life imprisonment in a maximum-security facility. It will be harder for them to escape.&lt;/p&gt;

&lt;h3&gt;
  
  
  In conclusion:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://newrelic.com" rel="noopener noreferrer"&gt;New Relic&lt;/a&gt; or a similar service to find clues. We realized that the problem lies specifically in the database queries.&lt;/li&gt;
&lt;li&gt;Use the PMDSC practice - it works and is engaging in any case.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://github.com/ankane/pghero" rel="noopener noreferrer"&gt;PgHero&lt;/a&gt; to find suspects and examine clues in SQL query statistics.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://explain.depesz.com" rel="noopener noreferrer"&gt;explain.depesz.com&lt;/a&gt; - it's convenient to read explain analyze of queries there.&lt;/li&gt;
&lt;li&gt;Try drawing data sets when you don't understand what a query is doing.&lt;/li&gt;
&lt;li&gt;Remember the tough guy with curls in different places on his head when you see a subquery that looks for something that doesn't exist in another table.&lt;/li&gt;
&lt;li&gt;Play detective, you might even get a badge.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The original article was published in the Russian-speaking community Habr in 2020. The translation into English was done with the help of GPT-3. The original article can be found &lt;a href="https://habr.com/ru/articles/509406/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>postgres</category>
      <category>rails</category>
      <category>sql</category>
      <category>performance</category>
    </item>
    <item>
      <title>AI Search Optimization for Jekyll: JSON-LD, llms.txt, and Entity Graphs</title>
      <dc:creator>Eugene Leontev </dc:creator>
      <pubDate>Thu, 09 Apr 2026 12:23:57 +0000</pubDate>
      <link>https://dev.to/madmatvey/ai-search-optimization-for-jekyll-json-ld-llmstxt-and-entity-graphs-13g3</link>
      <guid>https://dev.to/madmatvey/ai-search-optimization-for-jekyll-json-ld-llmstxt-and-entity-graphs-13g3</guid>
      <description>&lt;h2&gt;
  
  
  Most developer blogs are invisible to AI.
&lt;/h2&gt;

&lt;p&gt;Not because of content quality.&lt;br&gt;&lt;br&gt;
Because of missing structure.&lt;/p&gt;




&lt;p&gt;I ran into this while working with static sites (Jekyll).&lt;/p&gt;

&lt;p&gt;Great content.&lt;br&gt;&lt;br&gt;
Good SEO.&lt;br&gt;&lt;br&gt;
Zero presence in AI answers (ChatGPT, Perplexity, Google AI Overviews).&lt;/p&gt;

&lt;p&gt;The problem wasn’t traffic.&lt;/p&gt;

&lt;p&gt;It was &lt;strong&gt;entity understanding&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;Static site owners wanted visibility in AI search.&lt;/p&gt;

&lt;p&gt;But Jekyll has… nothing for that.&lt;/p&gt;

&lt;p&gt;No tooling for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structured entity graphs
&lt;/li&gt;
&lt;li&gt;LLM ingestion
&lt;/li&gt;
&lt;li&gt;AI crawler policies
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So even well-written posts never showed up in AI-generated answers.&lt;/p&gt;




&lt;h3&gt;
  
  
  What I built
&lt;/h3&gt;

&lt;p&gt;I created a Jekyll plugin:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/madmatvey/jekyll-ai-visible-content" rel="noopener noreferrer"&gt;https://github.com/madmatvey/jekyll-ai-visible-content&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Core idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat your site as a &lt;strong&gt;knowledge graph&lt;/strong&gt;, not a collection of pages.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  What it actually does
&lt;/h3&gt;

&lt;p&gt;— Generates JSON-LD (Person, BlogPosting, FAQ, HowTo)&lt;br&gt;&lt;br&gt;
— Creates &lt;code&gt;llms.txt&lt;/code&gt; for LLM ingestion&lt;br&gt;&lt;br&gt;
— Configures &lt;code&gt;robots.txt&lt;/code&gt; for AI crawlers (GPTBot, PerplexityBot, ClaudeBot)&lt;br&gt;&lt;br&gt;
— Maintains consistent entity identity across pages (&lt;code&gt;@id&lt;/code&gt;, &lt;code&gt;sameAs&lt;/code&gt;)&lt;br&gt;&lt;br&gt;
— Adds semantic linking between entities&lt;br&gt;&lt;br&gt;
— Validates content at build time (missing metadata, inconsistencies)&lt;/p&gt;




&lt;h3&gt;
  
  
  The real problem I didn’t expect
&lt;/h3&gt;

&lt;p&gt;Most sites fail not on content…&lt;/p&gt;

&lt;p&gt;…but on &lt;strong&gt;identity consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Different author names.&lt;br&gt;&lt;br&gt;
Missing &lt;code&gt;sameAs&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
No stable entity reference.&lt;/p&gt;

&lt;p&gt;To an LLM, that’s not “a person”.&lt;/p&gt;

&lt;p&gt;That’s noise.&lt;/p&gt;




&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;p&gt;Published as a Ruby gem.&lt;br&gt;&lt;br&gt;
Already being tested by developer blogs.&lt;/p&gt;

&lt;p&gt;And the interesting part:&lt;/p&gt;

&lt;p&gt;Once structured correctly →&lt;br&gt;&lt;br&gt;
content starts becoming &lt;strong&gt;machine-answerable&lt;/strong&gt;, not just searchable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;SEO optimized for keywords.&lt;/p&gt;

&lt;p&gt;AI search optimizes for &lt;strong&gt;entities + relationships&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your site doesn’t expose that:&lt;/p&gt;

&lt;p&gt;You don’t exist in AI.&lt;/p&gt;




&lt;p&gt;Save this — you’ll need it when AI becomes your main traffic source.&lt;/p&gt;




&lt;p&gt;If you’re running a Jekyll blog, I’d love feedback:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/madmatvey/jekyll-ai-visible-content" rel="noopener noreferrer"&gt;https://github.com/madmatvey/jekyll-ai-visible-content&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ruby</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Introducing jekyll-crypto-donations: Easily Add Crypto Donation Blocks to Your Jekyll Site</title>
      <dc:creator>Eugene Leontev </dc:creator>
      <pubDate>Thu, 13 Jun 2024 22:18:11 +0000</pubDate>
      <link>https://dev.to/madmatvey/introducing-jekyll-crypto-donations-easily-add-crypto-donation-blocks-to-your-jekyll-site-3557</link>
      <guid>https://dev.to/madmatvey/introducing-jekyll-crypto-donations-easily-add-crypto-donation-blocks-to-your-jekyll-site-3557</guid>
      <description>&lt;p&gt;Hello Dev Community!&lt;/p&gt;

&lt;p&gt;I'm excited to share the release of my new Jekyll plugin, &lt;code&gt;jekyll-crypto-donations&lt;/code&gt;. This gem allows you to seamlessly integrate cryptocurrency donation blocks into your Jekyll-generated websites. Whether you're a blogger, content creator, or developer, this plugin can help you receive support from your audience through crypto donations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why jekyll-crypto-donations?
&lt;/h2&gt;

&lt;p&gt;Cryptocurrency donations offer a decentralized and borderless way to receive support from your audience. With the rise of digital currencies, it's essential to have a simple solution for integrating donation options into your site. That's where &lt;code&gt;jekyll-crypto-donations&lt;/code&gt; comes in. This plugin provides a straightforward way to display donation addresses and track received funds in Bitcoin (BTC), Ethereum (ETH), and USDT (TRC-20).&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy Integration:&lt;/strong&gt; Add crypto donation blocks to your Jekyll site with minimal configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support for Multiple Cryptocurrencies:&lt;/strong&gt; Display donation addresses and total received funds for Bitcoin, Ethereum, and USDT (TRC-20).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive Design:&lt;/strong&gt; The plugin includes CSS that ensures your donation block looks great on both light and dark themes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy and QR Code Buttons:&lt;/strong&gt; Users can easily copy your donation address or scan a QR code for convenient transfers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable:&lt;/strong&gt; Configure wallet addresses in your Jekyll site's &lt;code&gt;_config.yml&lt;/code&gt; file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;To get started, add the gem to your Jekyll site's &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ss"&gt;:jekyll_plugins&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'jekyll-crypto-donations'&lt;/span&gt; 
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, configure your wallet addresses in &lt;code&gt;_config.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;crypto_donations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;btc_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-bitcoin-address"&lt;/span&gt;
  &lt;span class="na"&gt;eth_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-ethereum-address"&lt;/span&gt;
  &lt;span class="na"&gt;usdt_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-usdt-address"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;To include the donation block in your pages or posts, use the &lt;code&gt;{% crypto_donations %}&lt;/code&gt; Liquid tag. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Support Us&lt;/span&gt;

{% crypto_donations Your support helps us keep creating awesome content! %}

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;You can see a &lt;a href="https://eugenetheengineer.com/about/#donate-me" rel="noopener noreferrer"&gt;live demo of the plugin in action on my website&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;p&gt;I'm continuously working on improving this plugin, and I welcome contributions from the community. If you have suggestions, feature requests, or find any bugs, please open an issue or submit a pull request on &lt;a href="https://github.com/madmatvey/jekyll-crypto-donations" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;I'm thrilled to bring &lt;code&gt;jekyll-crypto-donations&lt;/code&gt; to the Jekyll community. This plugin aims to make it easier for creators to receive support from their audience through cryptocurrency donations. I hope you find it useful and look forward to seeing it in action on your sites.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eugenetheengineer.com/" rel="noopener noreferrer"&gt;Eugene Leontev&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Feel free to reach out if you have any questions or need assistance with the setup. Let's continue building and supporting each other in the open-source community!&lt;/p&gt;

</description>
      <category>staticwebapps</category>
      <category>jekyll</category>
      <category>ruby</category>
      <category>cryptocurrency</category>
    </item>
  </channel>
</rss>
