<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[pganalyze Blog - Articles by Leigh Halliday]]></title><description><![CDATA[Monitoring Postgres and tuning query performance]]></description><link>https://pganalyze.com</link><generator>GatsbyJS</generator><lastBuildDate>Thu, 07 May 2026 18:37:12 GMT</lastBuildDate><atom:link href="https://pganalyze.com/feed/leigh-halliday.xml" rel="self" type="application/rss+xml"/><item><title><![CDATA[PostGIS vs. Geocoder in Rails]]></title><description><![CDATA[This article sets out to compare PostGIS in Rails with Geocoder and to highlight a couple of the areas where you'll want to (or need to) reach for one over the other. I will also present some of the terminology and libraries that I found along the way of working on this project and article as I set out to understand PostGIS better and how it is integrated with Rails. If you are interested in learning how to work with geospatial data with PostGIS in Django I recommend having a look at our blog…]]></description><link>https://pganalyze.com/blog/postgis-rails-geocoder</link><guid isPermaLink="false">https://pganalyze.com/blog/postgis-rails-geocoder</guid><dc:creator><![CDATA[Leigh Halliday]]></dc:creator><pubDate>Thu, 01 Oct 2020 12:00:00 GMT</pubDate><content:encoded>&lt;![CDATA[ &lt;p&gt;This article sets out to compare PostGIS in Rails with Geocoder and to highlight a couple of the areas where you&apos;ll want to (or need to) reach for one over the other. I will also present some of the terminology and libraries that I found along the way of working on this project and article as I set out to understand PostGIS better and how it is integrated with Rails.&lt;/p&gt;
&lt;p&gt;If you are interested in learning how to work with geospatial data with PostGIS in Django I recommend having a look at our blog post &lt;a href=&quot;https://pganalyze.com/blog/geodjango-postgis&quot;&gt;Using GeoDjango and PostGIS in Django&lt;/a&gt; here.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#installing-postgis&quot;&gt;Installing PostGIS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#activerecord-postgis-adapter&quot;&gt;ActiveRecord PostGIS Adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#our-example-data&quot;&gt;Our Example Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#building-a-geo-helper-class-with-postgis&quot;&gt;Building a Geo Helper Class with PostGIS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#finding-nearby-records-with-postgis-and-geocoder&quot;&gt;Finding Nearby Records with PostGIS and Geocoder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#finding-records-within-a-bounding-box-with-postgis-and-geocoder&quot;&gt;Finding Records Within a Bounding Box with PostGIS and Geocoder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#finding-records-within-a-polygon-with-postgis-and-geocoder&quot;&gt;Finding Records Within a Polygon with PostGIS and Geocoder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#finding-nearby-related-records-with-postgis-and-geocoder&quot;&gt;Finding Nearby Related Records with PostGIS and Geocoder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#about-the-author&quot;&gt;About the Author&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;PostGIS vs. Geocoder in Rails&quot;
        title=&quot;PostGIS vs. Geocoder in Rails&quot;
        src=&quot;https://pganalyze.com/static/383f2659b144f300d98f78a94aefe750/acb04/postgis_rails_geocoder_pganalyze.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;
Picture via &lt;a href=&quot;https://unsplash.com/@anniespratt&quot;&gt;Annie Spratt&lt;/a&gt; on Unsplash&lt;/p&gt;
&lt;p&gt;I have built a number of Rails applications over the years that show locations on a map, have nearby search functionality, and I had never used &lt;a href=&quot;https://postgis.net/&quot;&gt;PostGIS&lt;/a&gt; before! How was this possible? The reason is that there is a Ruby gem named &lt;a href=&quot;https://github.com/alexreisner/geocoder&quot;&gt;Geocoder&lt;/a&gt; which enables you to do these sorts of queries, and it&apos;s quite efficient! That said, there is a reason that PostGIS exists. For more complex geo queries I’d recommend reaching beyond Geocoder to PostGIS.&lt;/p&gt;
&lt;p&gt;As an example, if you wanted to find homes which have a school within 1km of them, or if you wanted to draw an oddly shaped polygon on a map and search within it, this is the world where PostGIS shines and makes these complex geo queries possible.&lt;/p&gt;
&lt;p&gt;In this article we will be covering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostGIS in Rails setup&lt;/li&gt;
&lt;li&gt;Finding nearby records (Geocoder + PostGIS)&lt;/li&gt;
&lt;li&gt;Finding records within a bounding box (Geocoder + PostGIS)&lt;/li&gt;
&lt;li&gt;Finding records within a polygon (PostGIS)&lt;/li&gt;
&lt;li&gt;Finding nearby related records (PostGIS)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The source code referenced in this article can be &lt;a href=&quot;https://github.com/pganalyze-resources/rails-postgis-demo&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;installing-postgis&quot; &gt;&lt;a href=&quot;#installing-postgis&quot; aria-label=&quot;installing postgis permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Installing PostGIS&lt;/h2&gt;
&lt;p&gt;Postgres comes with a number of built-in extensions that you can enable, but unfortunately PostGIS (Spatial and Geographic objects for Postgres) isn&apos;t one of them. In order to enable this extension, you will have to use a Postgres install with PostGIS support. I recommend using the &lt;a href=&quot;https://registry.hub.docker.com/r/postgis/postgis&quot;&gt;official postgis docker image&lt;/a&gt;, but luckily many hosted Postgres solutions come with PostGIS already available. If you are not sure, you can query the available extensions with the following query:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;select&lt;/span&gt; &lt;span &gt;*&lt;/span&gt;
&lt;span &gt;from&lt;/span&gt; pg_available_extensions
&lt;span &gt;where&lt;/span&gt; name &lt;span &gt;like&lt;/span&gt; &lt;span &gt;&apos;%postgis%&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you&apos;d like to see if the extension is &lt;em&gt;already&lt;/em&gt; enabled, you can run this query:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;select&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;from&lt;/span&gt; pg_extension&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And finally, to enable this extension, you can use the command &lt;code &gt;create extension postgis&lt;/code&gt;, but since we&apos;re working within Rails, there is a Gem that will take care of this step for us as we&apos;ll see below.&lt;/p&gt;
&lt;h2 id=&quot;activerecord-postgis-adapter&quot; &gt;&lt;a href=&quot;#activerecord-postgis-adapter&quot; aria-label=&quot;activerecord postgis adapter permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;ActiveRecord PostGIS Adapter&lt;/h2&gt;
&lt;p&gt;If you have confirmed that your version of Postgres supports the &lt;code &gt;postgis&lt;/code&gt; extension, you&apos;re ready to integrate it with your Rails application. This can be done by using the &lt;a href=&quot;https://github.com/rgeo/activerecord-postgis-adapter&quot;&gt;activerecord-postgis-adapter&lt;/a&gt; gem. Two things need to be done to get up and running. The first is to update the &lt;code &gt;adapter&lt;/code&gt; within &lt;code &gt;config/database.yml&lt;/code&gt; to be set to &lt;code &gt;postgis&lt;/code&gt;. Next, if this is a new application, you can run &lt;code &gt;rails db:create&lt;/code&gt; as normal, but if it is an existing one, you&apos;ll have to run the command &lt;code &gt;rake db:gis:setup&lt;/code&gt;. This command is enabling the postgis extension in your database.&lt;/p&gt;
&lt;h2 id=&quot;our-example-data&quot; &gt;&lt;a href=&quot;#our-example-data&quot; aria-label=&quot;our example data permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Our Example Data&lt;/h2&gt;
&lt;p&gt;We&apos;ll be working with sample data for a realtor website that allows us to find homes in a variety of ways, including homes that are nearby a local school. There are two models: &lt;code &gt;homes&lt;/code&gt; and &lt;code &gt;schools&lt;/code&gt;. The Rails migration to create these tables is below:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;CreateHomes&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Migration&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;6.0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;change&lt;/span&gt;&lt;/span&gt;
    create_table &lt;span &gt;:homes&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;t&lt;span &gt;|&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;string &lt;span &gt;:name&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;string &lt;span &gt;:status&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;bigint &lt;span &gt;:price&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;integer &lt;span &gt;:beds&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; default&lt;span &gt;:&lt;/span&gt; &lt;span &gt;0&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;integer &lt;span &gt;:baths&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; default&lt;span &gt;:&lt;/span&gt; &lt;span &gt;0&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;st_point &lt;span &gt;:coords&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; geographic&lt;span &gt;:&lt;/span&gt; &lt;span &gt;true&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;float &lt;span &gt;:longitude&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;float &lt;span &gt;:latitude&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;timestamps

      t&lt;span &gt;.&lt;/span&gt;index &lt;span &gt;:coords&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:gist&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;index &lt;span &gt;%i[latitude longitude]&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;index &lt;span &gt;:status&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;index &lt;span &gt;:price&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;

    create_table &lt;span &gt;:schools&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;t&lt;span &gt;|&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;st_point &lt;span &gt;:coords&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; geographic&lt;span &gt;:&lt;/span&gt; &lt;span &gt;true&lt;/span&gt;

      t&lt;span &gt;.&lt;/span&gt;index &lt;span &gt;:coords&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:gist&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;timestamps
    &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By using &lt;code &gt;activerecord-postgis-adapter&lt;/code&gt; we are able to define PostGIS columns within our migration file. When working with PostGIS you can store a point (latitude + longitude) as a single column of type &lt;code &gt;ts_point&lt;/code&gt;, whereas when working with &lt;a href=&quot;https://github.com/alexreisner/geocoder&quot;&gt;Geocoder&lt;/a&gt; the latitude and longitude are stored as floats in separate columns. Because we are comparing the two approaches, we will store the data both ways, but typically you would choose one approach or the other.&lt;/p&gt;
&lt;p&gt;PostGIS &lt;strong&gt;geographic&lt;/strong&gt; columns can be indexed using &lt;a href=&quot;https://www.postgresql.org/docs/current/gist-intro.html&quot;&gt;GiST&lt;/a&gt; style indexes. GiST indexes are required over B-Tree indexes when working with geographic data because coordinates cannot be easily sorted along a single axis (such as numbers, letters, dates, etc...) in a way that would allow the database to speed up common geographic operations.&lt;/p&gt;
&lt;p&gt;The example project for this article contains a seeds file (run with &lt;code &gt;rake db:seed&lt;/code&gt;) which will generate 100k homes and 100 schools in and around the Atlanta, Georgia area.&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/efficient-search-in-rails-with-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        title=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        src=&quot;https://pganalyze.com/static/3e8bb134d6b5689ee9d20a10e6699b6c/acb04/ebook_promo_rails_search.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;building-a-geo-helper-class-with-postgis&quot; &gt;&lt;a href=&quot;#building-a-geo-helper-class-with-postgis&quot; aria-label=&quot;building a geo helper class with postgis permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Building a Geo Helper Class with PostGIS&lt;/h2&gt;
&lt;p&gt;The Rails PostGIS adapter is based on a library named &lt;a href=&quot;https://github.com/rgeo/rgeo&quot;&gt;RGeo&lt;/a&gt;, which while incredibly powerful, I found a little bit confusing due to a lack of documentation. I ended up building a small helper class to generate different geo objects for me. The first thing to point out is what &lt;a href=&quot;https://en.wikipedia.org/wiki/Spatial_reference_system&quot;&gt;SRID&lt;/a&gt; is. Just like the imperial and metric systems are used to measure and weigh amounts using an agreed upon reference point, coordinates also need a coordinate reference system to ensure that the latitude and longitude that one uses means the same thing to different people when referring to a single place on earth. &lt;a href=&quot;https://spatialreference.org/ref/epsg/wgs-84/&quot;&gt;4326&lt;/a&gt; is the spatial system used for GPS satellite navigation systems and the one we will be using within this article.&lt;/p&gt;
&lt;p&gt;One last thing to define is what &lt;a href=&quot;https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry&quot;&gt;WKT&lt;/a&gt; is. Well-known Text representation of geometry is a string representation of a point, line string, and polygon (among other things) that we will be using in our examples in this article. This is the format Postgres (PostGIS) receives and displays geographic data types in.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Geo&lt;/span&gt;
  &lt;span &gt;SRID&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; &lt;span &gt;4326&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;factory&lt;/span&gt;&lt;/span&gt;
    &lt;span &gt;@@factory&lt;/span&gt; &lt;span &gt;||&lt;/span&gt;&lt;span &gt;=&lt;/span&gt; &lt;span &gt;RGeo&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Geographic&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;spherical_factory&lt;span &gt;(&lt;/span&gt;srid&lt;span &gt;:&lt;/span&gt; &lt;span &gt;SRID&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;pairs_to_points&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;pairs&lt;span &gt;)&lt;/span&gt;
    pairs&lt;span &gt;.&lt;/span&gt;map &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;pair&lt;span &gt;|&lt;/span&gt; point&lt;span &gt;(&lt;/span&gt;pair&lt;span &gt;[&lt;/span&gt;&lt;span &gt;0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; pair&lt;span &gt;[&lt;/span&gt;&lt;span &gt;1&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;point&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;longitude&lt;span &gt;,&lt;/span&gt; latitude&lt;span &gt;)&lt;/span&gt;
    factory&lt;span &gt;.&lt;/span&gt;point&lt;span &gt;(&lt;/span&gt;longitude&lt;span &gt;,&lt;/span&gt; latitude&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;line_string&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;points&lt;span &gt;)&lt;/span&gt;
    factory&lt;span &gt;.&lt;/span&gt;line_string&lt;span &gt;(&lt;/span&gt;points&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;polygon&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;points&lt;span &gt;)&lt;/span&gt;
    line &lt;span &gt;=&lt;/span&gt; line_string&lt;span &gt;(&lt;/span&gt;points&lt;span &gt;)&lt;/span&gt;
    factory&lt;span &gt;.&lt;/span&gt;polygon&lt;span &gt;(&lt;/span&gt;line&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;to_wkt&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;feature&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;&quot;srid=&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;&lt;span &gt;SRID&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;;&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;feature&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;finding-nearby-records-with-postgis-and-geocoder&quot; &gt;&lt;a href=&quot;#finding-nearby-records-with-postgis-and-geocoder&quot; aria-label=&quot;finding nearby records with postgis and geocoder permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Finding Nearby Records with PostGIS and Geocoder&lt;/h2&gt;
&lt;p&gt;One of the most common geo queries used in applications is to find all records within X distance from a known point (the user&apos;s location, an event, a search, etc...). Because we installed &lt;code &gt;Geocoder&lt;/code&gt; and added &lt;code &gt;reverse_geocoded_by :latitude, :longitude&lt;/code&gt; to our &lt;code &gt;Home&lt;/code&gt; class, we can use the &lt;code &gt;nearby&lt;/code&gt; method to find all homes within 5km of this latitude and longitude (which happens to be Atlanta, Georgia). Geocoder likes to have arrays with latitude and then longitude, as opposed to PostGIS which &lt;strong&gt;prefers the exact opposite&lt;/strong&gt; order!&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Home&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;near&lt;span &gt;(&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;33.753746&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.386330&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:all&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;# ~5ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This query ran in about 5ms on my computer (searching through 100k records)... pretty fast! The reason it is fast is because we added an index on the latitude and longitude fields, but also because Geocoder applies a bounding box filter which utilises the index. Remember the Spatial Reference System (SRID) that we mentioned above? Because our coordinates do not take place on a &lt;a href=&quot;https://en.wikipedia.org/wiki/Cartesian_coordinate_system&quot;&gt;Cartesian plane&lt;/a&gt;, we can’t use a standard distance formula to calculate the &lt;a href=&quot;https://www.mathsisfun.com/algebra/distance-2-points.html&quot;&gt;distance between two points&lt;/a&gt;. Although we won’t venture further into the math of this query below, it takes into consideration the Earth’s spherical nature when calculating the distance between two coordinates as specified by latitude and longitude. &lt;a href=&quot;https://www.movable-type.co.uk/scripts/latlong.html&quot;&gt;This article&lt;/a&gt; dives into more detail on these calculations if you are interested.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;homes&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;homes&lt;span &gt;.&lt;/span&gt;latitude &lt;span &gt;BETWEEN&lt;/span&gt; &lt;span &gt;33.708779919704064&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; &lt;span &gt;33.798712080295935&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; homes&lt;span &gt;.&lt;/span&gt;longitude &lt;span &gt;BETWEEN&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.44041260768655&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.33224739231345&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;&lt;span &gt;6371.0&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; &lt;span &gt;2&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; ASIN&lt;span &gt;(&lt;/span&gt;SQRT&lt;span &gt;(&lt;/span&gt;POWER&lt;span &gt;(&lt;/span&gt;SIN&lt;span &gt;(&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;33.753746&lt;/span&gt; &lt;span &gt;-&lt;/span&gt; homes&lt;span &gt;.&lt;/span&gt;latitude&lt;span &gt;)&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; PI&lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;/&lt;/span&gt; &lt;span &gt;180&lt;/span&gt; &lt;span &gt;/&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;+&lt;/span&gt; COS&lt;span &gt;(&lt;/span&gt;&lt;span &gt;33.753746&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; PI&lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;/&lt;/span&gt; &lt;span &gt;180&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; COS&lt;span &gt;(&lt;/span&gt;homes&lt;span &gt;.&lt;/span&gt;latitude &lt;span &gt;*&lt;/span&gt; PI&lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;/&lt;/span&gt; &lt;span &gt;180&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; POWER&lt;span &gt;(&lt;/span&gt;SIN&lt;span &gt;(&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.38633&lt;/span&gt; &lt;span &gt;-&lt;/span&gt; homes&lt;span &gt;.&lt;/span&gt;longitude&lt;span &gt;)&lt;/span&gt; &lt;span &gt;*&lt;/span&gt; PI&lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;/&lt;/span&gt; &lt;span &gt;180&lt;/span&gt; &lt;span &gt;/&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;BETWEEN&lt;/span&gt; &lt;span &gt;0.0&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We&apos;ll have to build our own &lt;code &gt;near&lt;/code&gt; query when working with PostGIS, but don&apos;t worry, it&apos;s pretty straight forward! The &lt;code &gt;g_near&lt;/code&gt; method lives within the &lt;code &gt;Home&lt;/code&gt; model, and takes advantage of the &lt;a href=&quot;https://postgis.net/docs/ST_DWithin.html&quot;&gt;ST_DWithin&lt;/a&gt; function provided by PostGIS. Remember that we have to convert our point into the correct WKT format so that PostGIS understands the data we are passing it.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;g_near&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;point&lt;span &gt;,&lt;/span&gt; distance&lt;span &gt;)&lt;/span&gt;
  where&lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;&apos;ST_DWithin(coords, :point, :distance)&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;{&lt;/span&gt; point&lt;span &gt;:&lt;/span&gt; &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to_wkt&lt;span &gt;(&lt;/span&gt;point&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; distance&lt;span &gt;:&lt;/span&gt; distance &lt;span &gt;*&lt;/span&gt; &lt;span &gt;1000&lt;/span&gt; &lt;span &gt;}&lt;/span&gt; &lt;span &gt;# wants meters not kms&lt;/span&gt;
  &lt;span &gt;)&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;

&lt;span &gt;Home&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;g_near&lt;span &gt;(&lt;/span&gt;&lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;point&lt;span &gt;(&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.386330&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.753746&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count &lt;span &gt;# ~5ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This query performs just about as fast as the Geocoder version (because of our GiST index on the &lt;code &gt;coords&lt;/code&gt; column), but is definitely a little easier on the eyes to read.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;homes&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;ST_DWithin&lt;span &gt;(&lt;/span&gt;coords&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;srid=4326;POINT (-84.38633 33.753746)&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;5000&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;finding-records-within-a-bounding-box-with-postgis-and-geocoder&quot; &gt;&lt;a href=&quot;#finding-records-within-a-bounding-box-with-postgis-and-geocoder&quot; aria-label=&quot;finding records within a bounding box with postgis and geocoder permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Finding Records Within a Bounding Box with PostGIS and Geocoder&lt;/h2&gt;
&lt;p&gt;Geocoder provides us a way to find all records within a bounding box (roughly a rectangle, ignoring projection onto a sphere), and we just have to pass it the bottom left (south west) and top right (north east) coordinates.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Home&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;within_bounding_box&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;[&lt;/span&gt;&lt;span &gt;33.7250057553&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.4224209302&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;[&lt;/span&gt;&lt;span &gt;33.774350796&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.3570139222&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count &lt;span &gt;# ~5ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because it can use the index on latitude and longitude, it is quite efficient.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;homes&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;homes&lt;span &gt;.&lt;/span&gt;latitude &lt;span &gt;BETWEEN&lt;/span&gt; &lt;span &gt;33.7250057553&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; &lt;span &gt;33.774350796&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; homes&lt;span &gt;.&lt;/span&gt;longitude &lt;span &gt;BETWEEN&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.4224209302&lt;/span&gt; &lt;span &gt;AND&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.3570139222&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To perform a bounding box query using PostGis, we&apos;ll create a method named &lt;code &gt;g_within_box&lt;/code&gt; inside of the &lt;code &gt;Home&lt;/code&gt; model, and utilize a PostGIS function named &lt;a href=&quot;https://postgis.net/docs/ST_MakeEnvelope.html&quot;&gt;ST_MakeEnvelope&lt;/a&gt; along with the &lt;code &gt;&amp;amp;&amp;amp;&lt;/code&gt; operator.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;g_within_box&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;sw_point&lt;span &gt;,&lt;/span&gt; ne_point&lt;span &gt;)&lt;/span&gt;
  where&lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;&quot;coords &amp;amp;&amp;amp; ST_MakeEnvelope(:sw_lon, :sw_lat, :ne_lon, :ne_lat, &lt;span &gt;&lt;span &gt;#{&lt;/span&gt;
      &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;SRID&lt;/span&gt;
    &lt;span &gt;}&lt;/span&gt;&lt;/span&gt;)&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;{&lt;/span&gt;
      sw_lon&lt;span &gt;:&lt;/span&gt; sw_point&lt;span &gt;.&lt;/span&gt;longitude&lt;span &gt;,&lt;/span&gt;
      sw_lat&lt;span &gt;:&lt;/span&gt; sw_point&lt;span &gt;.&lt;/span&gt;latitude&lt;span &gt;,&lt;/span&gt;
      ne_lon&lt;span &gt;:&lt;/span&gt; ne_point&lt;span &gt;.&lt;/span&gt;longitude&lt;span &gt;,&lt;/span&gt;
      ne_lat&lt;span &gt;:&lt;/span&gt; ne_point&lt;span &gt;.&lt;/span&gt;latitude
    &lt;span &gt;}&lt;/span&gt;
  &lt;span &gt;)&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;

&lt;span &gt;Home&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;g_within_box&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;point&lt;span &gt;(&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.4224209302&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.7250057553&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;point&lt;span &gt;(&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.3570139222&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.774350796&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count &lt;span &gt;# ~5ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Again, this version performs at about the same efficiency as the Geocoder version.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;homes&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;coords &lt;span &gt;&amp;amp;&amp;amp;&lt;/span&gt; ST_MakeEnvelope&lt;span &gt;(&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.4224209302&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.7250057553&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.3570139222&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.774350796&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;4326&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;span
      
      
    &gt;
      &lt;a
    
    src=&quot;https://pganalyze.com/static/0f8374c9f7ad161492697445d965c753/9c7c2/postgis_rails_geocoder_bounding-box_pganalyze.jpg&quot;
    
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;PostGIS vs. Geocoder in Rails - Bounding Box&quot;
        title=&quot;PostGIS vs. Geocoder in Rails - Bounding Box&quot;
        src=&quot;https://pganalyze.com/static/0f8374c9f7ad161492697445d965c753/acb04/postgis_rails_geocoder_bounding-box_pganalyze.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;finding-records-within-a-polygon-with-postgis-and-geocoder&quot; &gt;&lt;a href=&quot;#finding-records-within-a-polygon-with-postgis-and-geocoder&quot; aria-label=&quot;finding records within a polygon with postgis and geocoder permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Finding Records Within a Polygon with PostGIS and Geocoder&lt;/h2&gt;
&lt;p&gt;We&apos;re now into territory that &lt;strong&gt;requires&lt;/strong&gt; PostGIS. To find records inside of a &lt;a href=&quot;https://en.wikipedia.org/wiki/Polygon&quot;&gt;polygon&lt;/a&gt;, along with the help of our &lt;code &gt;Geo&lt;/code&gt; class helper and the &lt;a href=&quot;https://postgis.net/docs/ST_Covers.html&quot;&gt;ST_Covers&lt;/a&gt; function from PostGIS, we can create a method named &lt;code &gt;g_within_polygon&lt;/code&gt; in our &lt;code &gt;Home&lt;/code&gt; model. This polygon is a triangle, where the last point is the same as the first one, thereby &quot;closing&quot; the shape of the polygon.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;g_within_polygon&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;points&lt;span &gt;)&lt;/span&gt;
  polygon &lt;span &gt;=&lt;/span&gt; &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;polygon&lt;span &gt;(&lt;/span&gt;points&lt;span &gt;)&lt;/span&gt;
  where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;ST_Covers(:polygon, coords)&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; polygon&lt;span &gt;:&lt;/span&gt; &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to_wkt&lt;span &gt;(&lt;/span&gt;polygon&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;

&lt;span &gt;Home&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;g_within_polygon&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;Geo&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;pairs_to_points&lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;[&lt;/span&gt;
      &lt;span &gt;[&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.39731626974567&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.75570358345219&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
      &lt;span &gt;[&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.33139830099567&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.86524376001825&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
      &lt;span &gt;[&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.25243406759724&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.770545357734925&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
      &lt;span &gt;[&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;84.39731626974567&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;33.75570358345219&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
    &lt;span &gt;]&lt;/span&gt;
  &lt;span &gt;)&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count &lt;span &gt;# ~5ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This query remains efficient due to the use of our GiST index, searching through 100k records in about 5ms.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;homes&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;ST_Covers&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;srid=4326;POLYGON ((-84.39731626974567 33.75570358345219, -84.33139830099567 33.86524376001825, -84.25243406759724 33.770545357734925, -84.39731626974567 33.75570358345219))&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; coords&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;span
      
      
    &gt;
      &lt;a
    
    src=&quot;https://pganalyze.com/static/ce2f065e32b345867f58e3bc79c5159c/c222a/postgis_rails_geocoder_polygon_pganalyze.jpg&quot;
    
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;PostGIS vs. Geocoder in Rails - Bounding Box&quot;
        title=&quot;PostGIS vs. Geocoder in Rails - Bounding Box&quot;
        src=&quot;https://pganalyze.com/static/ce2f065e32b345867f58e3bc79c5159c/acb04/postgis_rails_geocoder_polygon_pganalyze.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;finding-nearby-related-records-with-postgis-and-geocoder&quot; &gt;&lt;a href=&quot;#finding-nearby-related-records-with-postgis-and-geocoder&quot; aria-label=&quot;finding nearby related records with postgis and geocoder permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Finding Nearby Related Records with PostGIS and Geocoder&lt;/h2&gt;
&lt;p&gt;Using PostGIS it is also possible to find &lt;strong&gt;related&lt;/strong&gt; nearby records. What do I mean by that? Let&apos;s try to find &lt;code &gt;available&lt;/code&gt; homes that are within 1km of a school. This can be done by joining to the &lt;code &gt;schools&lt;/code&gt; table and utilizing &lt;a href=&quot;https://postgis.net/docs/ST_DWithin.html&quot;&gt;ST_DWithin&lt;/a&gt; for the &lt;code &gt;on&lt;/code&gt; clause. Starting with the SQL we&apos;d like to produce:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  &lt;span &gt;count&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;DISTINCT&lt;/span&gt; homes&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt;
  homes
  &lt;span &gt;INNER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; schools &lt;span &gt;ON&lt;/span&gt; ST_DWithin &lt;span &gt;(&lt;/span&gt;homes&lt;span &gt;.&lt;/span&gt;coords&lt;span &gt;,&lt;/span&gt; schools&lt;span &gt;.&lt;/span&gt;coords&lt;span &gt;,&lt;/span&gt; &lt;span &gt;1000&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;WHERE&lt;/span&gt;
  homes&lt;span &gt;.&lt;/span&gt;&lt;span &gt;status&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; &lt;span &gt;&apos;available&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Within the &lt;code &gt;Home&lt;/code&gt; model of our Rails application, we can create two scopes that allow us to find these homes. We&apos;re able to join 100k homes to the schools table (100 schools) based on their proximity in approximately 16ms.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Home&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  scope &lt;span &gt;:available&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; where&lt;span &gt;(&lt;/span&gt;status&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;available&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
  scope &lt;span &gt;:near_school&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
        lambda &lt;span &gt;{&lt;/span&gt;
          select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;DISTINCT ON (homes.id) homes.*&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;joins&lt;span &gt;(&lt;/span&gt;
            &lt;span &gt;&apos;INNER JOIN schools ON ST_DWithin (homes.coords, schools.coords, 1000)&apos;&lt;/span&gt;
          &lt;span &gt;)&lt;/span&gt;
        &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;
&lt;span &gt;# Example using the scopes declared above&lt;/span&gt;
&lt;span &gt;Home&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;available&lt;span &gt;.&lt;/span&gt;near_school&lt;span &gt;.&lt;/span&gt;count&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;distinct homes.id&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;# 16ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;conclusion&quot; &gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We&apos;ve only scratched the surface of what you can do with PostGIS, yet we were able to cover a ton of functionality that is common among websites that allow you to filter results based on their location. That said, if PostGIS isn&apos;t available as an extension on your version of Postgres, or you aren&apos;t requiring the power that PostGIS provides, Geocoder offers you a great alternative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share this article:&lt;/strong&gt; If you liked this article you might want to &lt;a href=&quot;https://twitter.com/intent/tweet?text=%22PostGIS%20vs.%20Geocoder%20in%20Rails%22%20-%20This%20article%20by%20%40pganalyze%20compares%20PostGIS%20in%20%23Rails%20with%20Geocoder%20and%20highlights%20areas%20where%20you%27ll%20want%20to%20reach%20for%20one%20over%20the%20other%3A%20https%3A%2F%2Fpganalyze.com%2Fblog%2Fpostgis-rails-geocoder&quot;&gt;tweet it to your peers&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;about-the-author&quot; &gt;&lt;a href=&quot;#about-the-author&quot; aria-label=&quot;about the author permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About the Author&lt;/h2&gt;
&lt;p&gt;Leigh Halliday is a guest author for the &lt;a src=&quot;https://pganalyze.com/&quot;&gt;pganalyze&lt;/a&gt; blog. He is a developer based out of Canada who works at &lt;a href=&quot;https://www.flipgive.com&quot;&gt;FlipGive&lt;/a&gt; as a full-stack developer. He writes about Ruby and React on &lt;a href=&quot;https://www.leighhalliday.com&quot;&gt;his blog&lt;/a&gt; and publishes React tutorials on &lt;a href=&quot;https://youtube.com/leighhalliday&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt; ]]&gt;</content:encoded></item><item><title><![CDATA[Advanced Active Record: Using Subqueries in Rails]]></title><description><![CDATA[Active Record provides a great balance between the ability to perform simple queries simply, and also the ability to access the raw SQL sometimes required to get our jobs done. In this article, we will see a number of real-life examples of business needs that may arise at our jobs. They will come in the form of a request for data from someone else at the company, where we will first translate the request into SQL, and then into the Rails code necessary to find those records. We will be covering…]]></description><link>https://pganalyze.com/blog/active-record-subqueries-rails</link><guid isPermaLink="false">https://pganalyze.com/blog/active-record-subqueries-rails</guid><dc:creator><![CDATA[Leigh Halliday]]></dc:creator><pubDate>Wed, 24 Jun 2020 12:00:00 GMT</pubDate><content:encoded>&lt;![CDATA[ &lt;p&gt;Active Record provides a great balance between the ability to perform simple queries simply, and also the ability to access the raw SQL sometimes required to get our jobs done. In this article, we will see a number of real-life examples of business needs that may arise at our jobs.&lt;/p&gt;
&lt;p&gt;They will come in the form of a request for data from someone else at the company, where we will first translate the request into SQL, and then into the Rails code necessary to find those records. We will be covering five different types of subqueries to help us find the requested data.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#working-with-active-record-in-rails&quot;&gt;Working with Active Record in Rails&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#what-are-subqueries-in-rails&quot;&gt;What are Subqueries in Rails&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#an-overview-of-our-data&quot;&gt;An Overview of our Data&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-where-subquery&quot;&gt;The Where Subquery&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#where-not-exists&quot;&gt;Where Not Exists&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-select-subquery&quot;&gt;The Select Subquery&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-from-subquery&quot;&gt;The From Subquery&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-having-subquery&quot;&gt;The Having Subquery&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#you-might-also-be-interested-in&quot;&gt;You might also be interested in&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#about-the-author&quot;&gt;About the author&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;Let&apos;s take a look at why subqueries matter:&lt;/p&gt;
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;
&lt;!DOCTYPE svg PUBLIC &quot;-//W3C//DTD SVG 1.1//EN&quot; &quot;http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd&quot;&gt;
&lt;svg version=&quot;1.1&quot; xmlns:xl=&quot;http://www.w3.org/1999/xlink&quot; xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;29.5 167 879.5 452&quot; width=&quot;879.5&quot; height=&quot;452&quot;&gt;
  &lt;defs&gt;
    &lt;font-face font-family=&quot;Helvetica Neue&quot; font-size=&quot;16&quot; panose-1=&quot;2 0 8 3 0 0 0 9 0 4&quot; units-per-em=&quot;1000&quot; underline-position=&quot;-100&quot; underline-thickness=&quot;50&quot; slope=&quot;0&quot; x-height=&quot;524&quot; cap-height=&quot;722&quot; ascent=&quot;975.0061&quot; descent=&quot;-216.99524&quot; font-weight=&quot;700&quot;&gt;
      &lt;font-face-src&gt;
        &lt;font-face-name name=&quot;HelveticaNeue-Bold&quot;/&gt;
      &lt;/font-face-src&gt;
    &lt;/font-face&gt;
    &lt;marker orient=&quot;auto&quot; overflow=&quot;visible&quot; markerUnits=&quot;strokeWidth&quot; id=&quot;FilledArrow_Marker&quot; stroke-linejoin=&quot;miter&quot; stroke-miterlimit=&quot;10&quot; viewBox=&quot;-1 -4 10 8&quot; markerWidth=&quot;10&quot; markerHeight=&quot;8&quot; color=&quot;#cc0102&quot;&gt;
      &lt;g&gt;
        &lt;path d=&quot;M 8 0 L 0 -3 L 0 3 Z&quot; fill=&quot;currentColor&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
    &lt;/marker&gt;
    &lt;marker orient=&quot;auto&quot; overflow=&quot;visible&quot; markerUnits=&quot;strokeWidth&quot; id=&quot;Arrow_Marker&quot; stroke-linejoin=&quot;miter&quot; stroke-miterlimit=&quot;10&quot; viewBox=&quot;-1 -4 10 8&quot; markerWidth=&quot;10&quot; markerHeight=&quot;8&quot; color=&quot;#346591&quot;&gt;
      &lt;g&gt;
        &lt;path d=&quot;M 8 0 L 0 -3 L 0 3 Z&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
    &lt;/marker&gt;
    &lt;marker orient=&quot;auto&quot; overflow=&quot;visible&quot; markerUnits=&quot;strokeWidth&quot; id=&quot;FilledArrow_Marker_2&quot; stroke-linejoin=&quot;miter&quot; stroke-miterlimit=&quot;10&quot; viewBox=&quot;-1 -4 10 8&quot; markerWidth=&quot;10&quot; markerHeight=&quot;8&quot; color=&quot;#cb0200&quot;&gt;
      &lt;g&gt;
        &lt;path d=&quot;M 8 0 L 0 -3 L 0 3 Z&quot; fill=&quot;currentColor&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
    &lt;/marker&gt;
    &lt;font-face font-family=&quot;Monaco&quot; font-size=&quot;14&quot; units-per-em=&quot;1000&quot; underline-position=&quot;-37.597656&quot; underline-thickness=&quot;75.68359&quot; slope=&quot;0&quot; x-height=&quot;545.41016&quot; cap-height=&quot;757.8125&quot; ascent=&quot;1e3&quot; descent=&quot;-250&quot; font-weight=&quot;400&quot;&gt;
      &lt;font-face-src&gt;
        &lt;font-face-name name=&quot;Monaco&quot;/&gt;
      &lt;/font-face-src&gt;
    &lt;/font-face&gt;
    &lt;font-face font-family=&quot;Monaco&quot; font-size=&quot;13&quot; units-per-em=&quot;1000&quot; underline-position=&quot;-37.597656&quot; underline-thickness=&quot;75.68359&quot; slope=&quot;0&quot; x-height=&quot;545.41016&quot; cap-height=&quot;757.8125&quot; ascent=&quot;1e3&quot; descent=&quot;-250&quot; font-weight=&quot;400&quot;&gt;
      &lt;font-face-src&gt;
        &lt;font-face-name name=&quot;Monaco&quot;/&gt;
      &lt;/font-face-src&gt;
    &lt;/font-face&gt;
  &lt;/defs&gt;
  &lt;g id=&quot;Canvas_1&quot;  fill-opacity=&quot;1&quot; fill=&quot;none&quot; stroke-opacity=&quot;1&quot; stroke=&quot;none&quot;&gt;
    &lt;title&gt;Canvas 1&lt;/title&gt;
    &lt;g id=&quot;Canvas_1: Layer 1&quot;&gt;
      &lt;title&gt;Layer 1&lt;/title&gt;
      &lt;g id=&quot;Graphic_2&quot;&gt;
        &lt;rect x=&quot;765.5&quot; y=&quot;177&quot; width=&quot;133.5&quot; height=&quot;43.75&quot; fill=&quot;#326691&quot;/&gt;
        &lt;text transform=&quot;translate(770.5 189.14294)&quot; fill=&quot;white&quot;&gt;
          &lt;tspan font-family=&quot;Helvetica Neue&quot; font-size=&quot;16&quot; font-weight=&quot;700&quot; fill=&quot;white&quot; x=&quot;27.67&quot; y=&quot;16&quot;&gt;Postgres&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_3&quot;&gt;
        &lt;rect x=&quot;39.5&quot; y=&quot;177&quot; width=&quot;133.5&quot; height=&quot;43.75&quot; fill=&quot;#cc0100&quot;/&gt;
        &lt;text transform=&quot;translate(44.5 189.14294)&quot; fill=&quot;white&quot;&gt;
          &lt;tspan font-family=&quot;Helvetica Neue&quot; font-size=&quot;16&quot; font-weight=&quot;700&quot; fill=&quot;white&quot; x=&quot;42.958&quot; y=&quot;16&quot;&gt;Rails&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_9&quot;&gt;
        &lt;text transform=&quot;translate(309.538 466.27576)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Helvetica Neue&quot; font-size=&quot;16&quot; font-weight=&quot;700&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;16&quot;&gt;Advanced Active Record with Subqueries:&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_12&quot;&gt;
        &lt;line x1=&quot;106.25&quot; y1=&quot;220.75&quot; x2=&quot;106.25&quot; y2=&quot;608.5&quot; stroke=&quot;#c00&quot;  stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_13&quot;&gt;
        &lt;line x1=&quot;831.75&quot; y1=&quot;220.75&quot; x2=&quot;831.75&quot; y2=&quot;608.5&quot; stroke=&quot;#326690&quot;  stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_15&quot;&gt;
        &lt;line x1=&quot;117.5&quot; y1=&quot;296.5&quot; x2=&quot;811.1&quot; y2=&quot;296.5&quot; marker-end=&quot;url(#FilledArrow_Marker)&quot; stroke=&quot;#cc0102&quot; stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_17&quot;&gt;
        &lt;line x1=&quot;821&quot; y1=&quot;328&quot; x2=&quot;127.4&quot; y2=&quot;328&quot; marker-end=&quot;url(#Arrow_Marker)&quot; stroke=&quot;#346591&quot;  stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_18&quot;&gt;
        &lt;line x1=&quot;117.5&quot; y1=&quot;375&quot; x2=&quot;811.1&quot; y2=&quot;376.97186&quot; marker-end=&quot;url(#FilledArrow_Marker_2)&quot; stroke=&quot;#cb0200&quot; stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_20&quot;&gt;
        &lt;text transform=&quot;translate(350.01 224.35327)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Helvetica Neue&quot; font-size=&quot;16&quot; font-weight=&quot;700&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;16&quot;&gt;Simple usage of Active Record:&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_21&quot;&gt;
        &lt;rect x=&quot;94.5&quot; y=&quot;293&quot; width=&quot;22.5&quot; height=&quot;119&quot; fill=&quot;#cc0100&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_25&quot;&gt;
        &lt;rect x=&quot;821&quot; y=&quot;293&quot; width=&quot;22.5&quot; height=&quot;38.25049&quot; fill=&quot;#326691&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_40&quot;&gt;
        &lt;text transform=&quot;translate(320.5 273.54273)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Monaco&quot; font-size=&quot;14&quot; font-weight=&quot;400&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;14&quot;&gt;SELECT AVG(salary) FROM employees&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_43&quot;&gt;
        &lt;text transform=&quot;translate(431.8181 309.16504)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Monaco&quot; font-size=&quot;13&quot; font-weight=&quot;400&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;13&quot;&gt;99306.4&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_44&quot;&gt;
        &lt;text transform=&quot;translate(265.8911 352.83105)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Monaco&quot; font-size=&quot;14&quot; font-weight=&quot;400&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;14&quot;&gt;SELECT * FROM employees WHERE salary &amp;gt; 99306.4&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_48&quot;&gt;
        &lt;line x1=&quot;821&quot; y1=&quot;408.7495&quot; x2=&quot;127.4&quot; y2=&quot;408.7495&quot; marker-end=&quot;url(#Arrow_Marker)&quot; stroke=&quot;#346591&quot;  stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_47&quot;&gt;
        &lt;rect x=&quot;821&quot; y=&quot;372.64905&quot; width=&quot;22.5&quot; height=&quot;38.25049&quot; fill=&quot;#326691&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_46&quot;&gt;
        &lt;text transform=&quot;translate(435.71875 389.91455)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Monaco&quot; font-size=&quot;13&quot; font-weight=&quot;400&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;13&quot;&gt;Result&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_53&quot;&gt;
        &lt;line x1=&quot;117.5&quot; y1=&quot;534&quot; x2=&quot;811.1&quot; y2=&quot;534&quot; marker-end=&quot;url(#FilledArrow_Marker_2)&quot; stroke=&quot;#cb0200&quot; stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_52&quot;&gt;
        &lt;text transform=&quot;translate(151.27197 511.0972)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Monaco&quot; font-size=&quot;14&quot; font-weight=&quot;400&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;14&quot;&gt;SELECT * FROM employees WHERE salary &amp;gt; (SELECT AVG(salary) FROM employees)&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Line_51&quot;&gt;
        &lt;line x1=&quot;821&quot; y1=&quot;565.0156&quot; x2=&quot;127.4&quot; y2=&quot;565.0156&quot; marker-end=&quot;url(#Arrow_Marker)&quot; stroke=&quot;#346591&quot;  stroke-width=&quot;1&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_50&quot;&gt;
        &lt;rect x=&quot;821&quot; y=&quot;529.76514&quot; width=&quot;22.5&quot; height=&quot;38.25049&quot; fill=&quot;#326691&quot;/&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_49&quot;&gt;
        &lt;text transform=&quot;translate(438.71875 546.1807)&quot; fill=&quot;black&quot;&gt;
          &lt;tspan font-family=&quot;Monaco&quot; font-size=&quot;13&quot; font-weight=&quot;400&quot; fill=&quot;black&quot; x=&quot;0&quot; y=&quot;13&quot;&gt;Result&lt;/tspan&gt;
        &lt;/text&gt;
      &lt;/g&gt;
      &lt;g id=&quot;Graphic_54&quot;&gt;
        &lt;rect x=&quot;94.5&quot; y=&quot;530.0156&quot; width=&quot;22.5&quot; height=&quot;38.25049&quot; fill=&quot;#cc0100&quot;/&gt;
      &lt;/g&gt;
    &lt;/g&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;p&gt;In the first case, without subqueries, we are going to the database twice: First to get the average salary, and then again to get the result set. With a subquery, we can avoid the extra roundtrip, getting the result directly with a single query.&lt;/p&gt;
&lt;h2 id=&quot;working-with-active-record-in-rails&quot; &gt;&lt;a href=&quot;#working-with-active-record-in-rails&quot; aria-label=&quot;working with active record in rails permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Working with Active Record in Rails&lt;/h2&gt;
&lt;p&gt;Active Record is a little like a walled garden. It protects us as developers (and our users) from the harsh realities of what lies beyond those walls: Differences in SQL between databases (MySQL, Postgres, SQLite), knowing how to properly escape strings to avoid &lt;a href=&quot;https://en.wikipedia.org/wiki/SQL_injection&quot;&gt;SQL injection attacks&lt;/a&gt;, and generally providing an elegant abstraction to interact with our database using the language of our choice, Ruby.&lt;/p&gt;
&lt;p&gt;But, SQL is extremely powerful! By understanding the SQL that Active Record is executing, we can open the gate in our walled garden to &lt;strong&gt;reach beyond what you may think is possible to accomplish in Rails&lt;/strong&gt;, taking advantage of optimizations and flexibility that may be difficult to achieve otherwise.&lt;/p&gt;
&lt;h2 id=&quot;what-are-subqueries-in-rails&quot; &gt;&lt;a href=&quot;#what-are-subqueries-in-rails&quot; aria-label=&quot;what are subqueries in rails permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What are Subqueries in Rails&lt;/h2&gt;
&lt;p&gt;In this article, we will be learning how to use subqueries in Active Record. Subqueries are what their name implies: A query within a query. We will look at how to embed subqueries into the &lt;code &gt;SELECT&lt;/code&gt;, &lt;code &gt;FROM&lt;/code&gt;, &lt;code &gt;WHERE&lt;/code&gt;, and &lt;code &gt;HAVING&lt;/code&gt; clauses of SQL, to meet the demands of our business counterparts who are asking to view data in different and interesting ways.&lt;/p&gt;
&lt;p&gt;We&apos;ll be playing the role of a developer fielding questions from HR. They are asking for reports about our employees at BCE (Best Company Ever), and we&apos;ll do our best to find the data they need using Active Record.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/pganalyze/subqueries-rails-example&quot;&gt;source code for this article&lt;/a&gt; is available on GitHub.&lt;/p&gt;
&lt;h2 id=&quot;an-overview-of-our-data&quot; &gt;&lt;a href=&quot;#an-overview-of-our-data&quot; aria-label=&quot;an overview of our data permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;An Overview of our Data&lt;/h2&gt;
&lt;p&gt;Our database has 4 tables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;roles&lt;/strong&gt;: The job roles of our employees (Finance, Engineering, Sales, HR, etc...)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;employees&lt;/strong&gt;: The people that work for BCE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;performance_reviews&lt;/strong&gt;: Performance reviews carried out by an employee&apos;s manager, giving them a score between 0 and 100&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vacations&lt;/strong&gt;: Keeping track of when employees have taken vacation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using &lt;a href=&quot;https://dbdiagram.io/&quot;&gt;https://dbdiagram.io/&lt;/a&gt; we&apos;re able to see how these tables relate to each other:&lt;/p&gt;
&lt;p &gt;
&lt;span  &gt;
      &lt;a  src=&quot;https://pganalyze.com/static/d18c3ddf9bf418f5b36c85d3e8f623a5/c2d9c/subqueries-rails-diagram-dark.png&quot;  target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span  &gt;&lt;/span&gt;
  &lt;img  alt=&quot;An Overview of how 4 tables in our database relate to each other&quot; title=&quot;An Overview of how 4 tables in our database relate to each other&quot; src=&quot;https://pganalyze.com/static/d18c3ddf9bf418f5b36c85d3e8f623a5/1d69c/subqueries-rails-diagram-dark.png&quot;    loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;p&gt;If you are following along, the &lt;code &gt;rails db:seed&lt;/code&gt; command will generate 1,000 employees, 1,000 vacations, and 10,000 performance reviews.&lt;/p&gt;
&lt;h2 id=&quot;the-where-subquery&quot; &gt;&lt;a href=&quot;#the-where-subquery&quot; aria-label=&quot;the where subquery permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Where Subquery&lt;/h2&gt;
&lt;p&gt;Now that we have our data set and we’re ready to go let’s help our HR team with their first request:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Leigh, could you find us all the employees that make &lt;em&gt;more than the average salary&lt;/em&gt; at BCE?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here we will use a subquery within the &lt;code &gt;WHERE&lt;/code&gt; clause to find the employees that match HR&apos;s request:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt; employees
&lt;span &gt;WHERE&lt;/span&gt;
  employees&lt;span &gt;.&lt;/span&gt;salary &lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;salary&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;FROM&lt;/span&gt; employees&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My first attempt at replicating the query above looked like this:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;salary &gt; :avg&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; avg&lt;span &gt;:&lt;/span&gt; &lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;average&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:salary&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;But what it produced was two queries&lt;/em&gt;: One to find the average, and a second to query employees with a salary greater than that number. Not technically wrong, but &lt;strong&gt;it doesn&apos;t line up with the SQL we were going for.&lt;/strong&gt; There is also a potential performance impact of two round-trip requests to the database server, along with potential inconsistencies if a new employee making $1B/year is hired between queries one and two. Although this is unlikely in this particular scenario, it’s something to consider as a potential risk.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;-- find the average&lt;/span&gt;
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;AVG&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;employees&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;employees&quot;&lt;/span&gt;
&lt;span &gt;-- find the employees&lt;/span&gt;
&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;&quot;employees&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;employees&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;salary &lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;99306.4&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What we shouldn’t forget about &lt;a href=&quot;https://guides.rubyonrails.org/active_record_querying.html&quot;&gt;Active Record&lt;/a&gt; is that certain methods, such as &lt;code &gt;average(:salary)&lt;/code&gt;, actually execute the query and return a result, while other methods implement &lt;a href=&quot;https://en.wikipedia.org/wiki/Method_chaining&quot;&gt;Method Chaining&lt;/a&gt;, allowing you to chain multiple Active Record methods together, building up more complex SQL statements prior to their execution.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;salary &gt; (:avg)&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; avg&lt;span &gt;:&lt;/span&gt; &lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;avg(salary)&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This produces the SQL we want, but note that we had to wrap the placeholder condition &lt;code &gt;:avg&lt;/code&gt; in brackets, because the database wants subqueries wrapped in brackets as well.&lt;/p&gt;
&lt;p&gt;Because the seed data is generated randomly, your results will vary from mine, but I am seeing &lt;em&gt;487&lt;/em&gt; matching employees, getting a result that looks like this:&lt;/p&gt;
&lt;div  data-language=&quot;text&quot;&gt;&lt;pre &gt;&lt;code &gt;#&amp;lt;ActiveRecord::Relation [#&amp;lt;Employee id: 4, role_id: 5, name: &quot;Bob Williams&quot;, salary: 127053.0, created_at: &quot;2020-04-26 18:42:53&quot;, updated_at: &quot;2020-04-26 18:42:53&quot;&gt;, #&amp;lt;Employee id: 5, role_id: 4, name: &quot;Bob Florez&quot;, salary: 149218.0, created_at: &quot;2020-04-26 18:42:53&quot;, updated_at: &quot;2020-04-26 18:42:53&quot;&gt;, ...]&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/advanced-database-programming-rails-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        title=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        src=&quot;https://pganalyze.com/static/24260e03f3c098e161f84b87ce28122b/acb04/ebook_promo_advanced_database_programming_rails_postgres.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;where-not-exists&quot; &gt;&lt;a href=&quot;#where-not-exists&quot; aria-label=&quot;where not exists permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Where Not Exists&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Leigh, we would like to encourage employees to have a healthy work-life balance, and were hoping you could provide us with a list of all the &lt;em&gt;employees who have yet to take any vacation time&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For this case, &lt;code &gt;NOT EXISTS&lt;/code&gt; is a perfect fit, since it only matches records that &lt;strong&gt;do not&lt;/strong&gt; have a match in the subquery. An alternative is to perform a left outer join, only choosing the records with no matches on the right side. This is referred to as an &lt;a href=&quot;https://gerardnico.com/data/type/relation/sql/anti_join&quot;&gt;anti-join&lt;/a&gt;, where the purpose of the join is to find records that &lt;strong&gt;do not&lt;/strong&gt; have a matching record.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt; employees
&lt;span &gt;WHERE&lt;/span&gt;
  &lt;span &gt;NOT&lt;/span&gt; &lt;span &gt;EXISTS&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;1&lt;/span&gt;
    &lt;span &gt;FROM&lt;/span&gt; vacations
    &lt;span &gt;WHERE&lt;/span&gt; vacations&lt;span &gt;.&lt;/span&gt;employee_id &lt;span &gt;=&lt;/span&gt; employees&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you&apos;re interested in the &lt;strong&gt;LEFT OUTER JOIN&lt;/strong&gt; equivalent, it might look like this:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; employees&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt;
  employees
  &lt;span &gt;LEFT&lt;/span&gt; &lt;span &gt;OUTER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; vacations &lt;span &gt;ON&lt;/span&gt; vacations&lt;span &gt;.&lt;/span&gt;employee_id &lt;span &gt;=&lt;/span&gt; employees&lt;span &gt;.&lt;/span&gt;id
&lt;span &gt;WHERE&lt;/span&gt; vacations&lt;span &gt;.&lt;/span&gt;id &lt;span &gt;IS&lt;/span&gt; &lt;span &gt;NULL&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The subquery depends on a match between the &lt;code &gt;employees.id&lt;/code&gt; column and the &lt;code &gt;vacations.employee_id&lt;/code&gt; column, making it a &lt;a href=&quot;https://learnsql.com/blog/correlated-sql-subqueries-newbies/&quot;&gt;correlated subquery&lt;/a&gt;. Because Rails follows standard naming conventions when querying (the downcased plural form of our model), we can add the above condition into our subquery without too much difficulty.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;&apos;NOT EXISTS (:vacations)&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  vacations&lt;span &gt;:&lt;/span&gt; &lt;span &gt;Vacation&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;1&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;employees.id = vacations.employee_id&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Using my seed data, I am seeing &lt;em&gt;369&lt;/em&gt; employees that have yet to take any vacations.&lt;/p&gt;
&lt;div  data-language=&quot;text&quot;&gt;&lt;pre &gt;&lt;code &gt;#&amp;lt;ActiveRecord::Relation [#&amp;lt;Employee id: 2, role_id: 2, name: &quot;Alice Florez&quot;, salary: 86920.0, created_at: &quot;2020-04-26 18:42:53&quot;, updated_at: &quot;2020-04-26 18:42:53&quot;&gt;, #&amp;lt;Employee id: 5, role_id: 4, name: &quot;Bob Florez&quot;, salary: 149218.0, created_at: &quot;2020-04-26 18:42:53&quot;, updated_at: &quot;2020-04-26 18:42:53&quot;&gt;, ...]&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-select-subquery&quot; &gt;&lt;a href=&quot;#the-select-subquery&quot; aria-label=&quot;the select subquery permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Select Subquery&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Leigh, could you provide us with a list of employees, &lt;em&gt;including the average salary&lt;/em&gt; of a BCE employee, and how much this &lt;em&gt;employee&apos;s salary differs from the average&lt;/em&gt;?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  &lt;span &gt;*&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;(&lt;/span&gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;salary&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;FROM&lt;/span&gt; employees&lt;span &gt;)&lt;/span&gt; avg_salary&lt;span &gt;,&lt;/span&gt;
  salary &lt;span &gt;-&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;salary&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;FROM&lt;/span&gt; employees&lt;span &gt;)&lt;/span&gt; above_avg
&lt;span &gt;FROM&lt;/span&gt; employees&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because the subquery is repeated, we can save ourselves a little bit of hassle by placing the subquery SQL into a variable that we&apos;ll embed into the outer query. The &lt;code &gt;to_sql&lt;/code&gt; method is perfect for this, but it&apos;s also fantastic to peak into the SQL that Rails is producing without actually executing the query.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;avg_sql &lt;span &gt;=&lt;/span&gt; &lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;avg(salary)&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to_sql

&lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;&apos;*&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;(&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;avg_sql&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;) avg_salary&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;salary - (&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;avg_sql&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;) avg_difference&quot;&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This query does not limit the results in any way, but instead selects two additional columns (&lt;code &gt;avg_salary&lt;/code&gt; and &lt;code &gt;avg_difference&lt;/code&gt;). Looking at the first three results, I am seeing:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;[&lt;/span&gt;
  &lt;span &gt;{&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;role_id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;name&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;&quot;Joe Serna&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;86340.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;99306.4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_difference&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;12966.399999999994&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; 
  &lt;span &gt;{&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;2&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;role_id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;2&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;name&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;&quot;Alice Florez&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;86920.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;99306.4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_difference&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;12386.399999999994&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; 
  &lt;span &gt;{&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;3&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;role_id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;3&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;name&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;&quot;Amanda Florez&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;93600.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;99306.4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_difference&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;-&lt;/span&gt;&lt;span &gt;5706.399999999994&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;
&lt;span &gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As with any SQL query, there are often many ways to arrive at the same result. In this example we used subqueries to find the average employee salary, but it may have been better to use &lt;a href=&quot;https://www.postgresql.org/docs/current/tutorial-window.html&quot;&gt;window functions&lt;/a&gt; instead. They give us the same result, but provide a simpler query which is actually more performant as well. Even on a small dataset of 1000 employees, this query takes approximately 12ms vs 18ms for the subquery equivalent.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  &lt;span &gt;*&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;salary&lt;span &gt;)&lt;/span&gt; &lt;span &gt;OVER&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; avg_salary&lt;span &gt;,&lt;/span&gt;
  salary &lt;span &gt;-&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;salary&lt;span &gt;)&lt;/span&gt; &lt;span &gt;OVER&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; avg_salary
&lt;span &gt;FROM&lt;/span&gt;
  employees&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The window function approach is actually easier to write in Rails as well!&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;&apos;*&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;avg(salary) OVER () avg_salary&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;salary - avg(salary) OVER () avg_difference&quot;&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-from-subquery&quot; &gt;&lt;a href=&quot;#the-from-subquery&quot; aria-label=&quot;the from subquery permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The From Subquery&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Leigh, we&apos;d like to know the &lt;em&gt;average performance review score&lt;/em&gt; given across all our managers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After clarifying with HR, they are looking to take the average score each manager has given, and then take the average of those averages. In other words, the average average. When you are dealing with an &lt;strong&gt;aggregate of aggregates&lt;/strong&gt;, it needs to be accomplished in two steps. This can be done using a subquery as the &lt;code &gt;FROM&lt;/code&gt; clause, essentially giving us a temporary table to then select from, allowing us to find the average of those averages.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;avg_score&lt;span &gt;)&lt;/span&gt; reviewer_avg
&lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;SELECT&lt;/span&gt; reviewer_id&lt;span &gt;,&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;score&lt;span &gt;)&lt;/span&gt; avg_score
  &lt;span &gt;FROM&lt;/span&gt; performance_reviews
  &lt;span &gt;GROUP&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; reviewer_id&lt;span &gt;)&lt;/span&gt; reviewer_avgs&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To keep our Ruby code clean, we&apos;ll place the subquery into a variable which can then be embedded into the main query.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;from_sql &lt;span &gt;=&lt;/span&gt;
  &lt;span &gt;PerformanceReview&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:reviewer_id&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;avg(score) avg_score&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;group&lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;:reviewer_id&lt;/span&gt;
  &lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to_sql

&lt;span &gt;PerformanceReview&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;avg(avg_score) reviewer_avg&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;from&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;&quot;(&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;from_sql&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;) as reviewer_avgs&quot;&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;take&lt;span &gt;.&lt;/span&gt;reviewer_avg&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The result of this query is &lt;code &gt;50.652&lt;/code&gt;. This makes sense given that the seed data used a random value between 1 and 100 (&lt;code &gt;rand(1..100)&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/efficient-search-in-rails-with-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        title=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        src=&quot;https://pganalyze.com/static/3e8bb134d6b5689ee9d20a10e6699b6c/acb04/ebook_promo_rails_search.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-having-subquery&quot; &gt;&lt;a href=&quot;#the-having-subquery&quot; aria-label=&quot;the having subquery permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Having Subquery&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Leigh, certain reviewers are consistently giving low performance review scores. Could you find us a list of all the managers whose average score is 25% below our company average? We need to find out what is happening.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We will start by joining the &lt;code &gt;employees&lt;/code&gt; table to the &lt;code &gt;performance_reviews&lt;/code&gt; table where the employee is the reviewer (a manager), and then take their average score. Then we will filter out these managers using a &lt;code &gt;HAVING&lt;/code&gt; clause to only include those whose score increased by 25% is still lower than the company average.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  employees&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;score&lt;span &gt;)&lt;/span&gt; avg_score&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;(&lt;/span&gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;score&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;FROM&lt;/span&gt; performance_reviews&lt;span &gt;)&lt;/span&gt; company_avg
&lt;span &gt;FROM&lt;/span&gt;
  employees
  &lt;span &gt;INNER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; performance_reviews
    &lt;span &gt;ON&lt;/span&gt; performance_reviews&lt;span &gt;.&lt;/span&gt;reviewer_id &lt;span &gt;=&lt;/span&gt; employees&lt;span &gt;.&lt;/span&gt;id
&lt;span &gt;GROUP&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; employees&lt;span &gt;.&lt;/span&gt;id
&lt;span &gt;HAVING&lt;/span&gt;
  &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;score&lt;span &gt;)&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;0.75&lt;/span&gt; &lt;span &gt;*&lt;/span&gt;
    &lt;span &gt;(&lt;/span&gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;avg&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;score&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;FROM&lt;/span&gt; performance_reviews&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You&apos;ll notice that I actually included &lt;strong&gt;two&lt;/strong&gt; subqueries in the above SQL. Because the SQL was saved to a variable (&lt;code &gt;avg_sql&lt;/code&gt;), we were able to reuse this both within the &lt;code &gt;SELECT&lt;/code&gt; portion of the query, and also within the &lt;code &gt;HAVING&lt;/code&gt; clause.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;avg_sql &lt;span &gt;=&lt;/span&gt; &lt;span &gt;PerformanceReview&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;avg(score)&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to_sql

&lt;span &gt;Employee&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;joins&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:employee_reviews&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;select&lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;&apos;employees.*&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&apos;avg(score) avg_score&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;(&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;avg_sql&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;) company_avg&quot;&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;group&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;employees.id&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;having&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;avg(score) &amp;lt; 0.75 * (&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;avg_sql&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;)&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The result of this query gives me 103 employees, and the first three of them look like:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;[&lt;/span&gt;
  &lt;span &gt;{&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;173&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;role_id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;name&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;&quot;Bob Williams&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;109206.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_score&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;23.75&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;company_avg&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;50.04&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; 
  &lt;span &gt;{&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;390&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;role_id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;5&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;name&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;&quot;Bob Serna&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;127559.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_score&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;26.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;company_avg&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;50.04&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; 
  &lt;span &gt;{&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;802&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;role_id&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;name&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;&quot;Alice Halliday&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;salary&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;94956.0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;avg_score&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;35.88&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;company_avg&quot;&lt;/span&gt;&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;50.04&lt;/span&gt;&lt;span &gt;}&lt;/span&gt;
&lt;span &gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;conclusion&quot; &gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this article we were able to see a number of (somewhat) real-life examples of real business needs translating first into SQL, and then into the Rails code necessary to find those records. A backend developer&apos;s career will consist in most likely hundreds of similar requests!&lt;/p&gt;
&lt;p&gt;Active Record gives us the ability to perform simple queries simply, but also lets us access the raw SQL which is sometimes required to get our jobs done. Subqueries are a perfect example of that, and we saw how to create subqueries in Rails and Active Record in the &lt;code &gt;SELECT&lt;/code&gt;, &lt;code &gt;FROM&lt;/code&gt;, &lt;code &gt;WHERE&lt;/code&gt;, and &lt;code &gt;HAVING&lt;/code&gt; clauses of an SQL statement. As we have seen in the examples above, with the expressiveness of Active Record, one doesn’t have to resort to writing completely in SQL to use a subquery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share this article:&lt;/strong&gt; If you liked this article we’d appreciate it if you’d &lt;a href=&quot;https://ctt.ac/_6t20&quot;&gt;tweet it to your peers&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;you-might-also-be-interested-in&quot; &gt;&lt;a href=&quot;#you-might-also-be-interested-in&quot; aria-label=&quot;you might also be interested in permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;You might also be interested in&lt;/h2&gt;
&lt;p&gt;Learn more about how to make the most of Postgres and Ruby on Rails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/optimizing-postgres-query-performance&quot;&gt;eBook: Best Practices for Optimizing Postgres Query Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a src=&quot;https://pganalyze.com/blog/materialized-views-ruby-rails&quot;&gt;Effectively Using Materialized Views in Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a src=&quot;https://pganalyze.com/blog/efficient-graphql-queries-in-ruby-on-rails-and-postgres&quot;&gt;Efficient GraphQL queries in Ruby on Rails &amp;#x26; Postgres&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;about-the-author&quot; &gt;&lt;a href=&quot;#about-the-author&quot; aria-label=&quot;about the author permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About the author&lt;/h2&gt;
&lt;p&gt;Leigh Halliday is a guest author for the &lt;a src=&quot;https://pganalyze.com/&quot;&gt;pganalyze&lt;/a&gt; blog. He is a developer based out of Canada who works at &lt;a href=&quot;https://www.flipgive.com&quot;&gt;FlipGive&lt;/a&gt; as a full-stack developer. He writes about Ruby and React on &lt;a href=&quot;https://www.leighhalliday.com&quot;&gt;his blog&lt;/a&gt; and publishes React tutorials on &lt;a href=&quot;https://youtube.com/leighhalliday&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt; ]]&gt;</content:encoded></item><item><title><![CDATA[Full Text Search in Milliseconds with Rails and PostgreSQL]]></title><description><![CDATA[Imagine the following scenario: You have a database full of job titles and descriptions, and you’re trying to find the best match. Typically you’d start by using an ILIKE expression, but this requires the search phrase to be an exact match. Then you might use trigrams, allowing spelling mistakes and inexact matches based on word similarity, but this makes it difficult to search using multiple words. What you really want to use is Full Text Search, providing the benefits of ILIKE and trigrams…]]></description><link>https://pganalyze.com/blog/full-text-search-ruby-rails-postgres</link><guid isPermaLink="false">https://pganalyze.com/blog/full-text-search-ruby-rails-postgres</guid><dc:creator><![CDATA[Leigh Halliday]]></dc:creator><pubDate>Thu, 16 Apr 2020 12:00:00 GMT</pubDate><content:encoded>&lt;![CDATA[ &lt;p &gt;
&lt;span  &gt;
      &lt;a  src=&quot;https://pganalyze.com/static/9d4c1a81657f389003bb2790dc2af0ec/2cefc/header.png&quot;  target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span  &gt;&lt;/span&gt;
  &lt;img  alt=&quot;Postgres Full Text Search Example&quot; title=&quot;Postgres Full Text Search Example&quot; src=&quot;https://pganalyze.com/static/9d4c1a81657f389003bb2790dc2af0ec/1d69c/header.png&quot;    loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Imagine the following scenario:&lt;/strong&gt; You have a database full of job titles and descriptions, and you’re trying to find the best match. Typically you’d start by using an &lt;a href=&quot;https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE&quot;&gt;ILIKE expression&lt;/a&gt;, but this requires the search phrase to be an exact match. Then you might use &lt;a href=&quot;https://pganalyze.com/blog/similarity-in-postgres-and-ruby-on-rails-using-trigrams&quot;&gt;trigrams&lt;/a&gt;, allowing spelling mistakes and inexact matches based on word similarity, but this makes it difficult to search using multiple words. What you really want to use is &lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch.html&quot;&gt;Full Text Search&lt;/a&gt;, providing the benefits of ILIKE and trigrams, with the added ability to easily search through large documents using natural language.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#the-foundations-of-full-text-search&quot;&gt;The Foundations of Full Text Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#implementing-postgres-full-text-search-in-rails&quot;&gt;Implementing Postgres Full Text Search in Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#configuring-pg_search&quot;&gt;Configuring pg_search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#optimizing-full-text-search-queries-in-rails&quot;&gt;Optimizing Full Text Search Queries in Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#you-might-also-be-interested-in&quot;&gt;You might also be interested in&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#about-the-author&quot;&gt;About the author&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;To summarize, here is a quick overview of popular built-in Postgres search options:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Postgres Feature&lt;/th&gt;
&lt;th&gt;Typical Use Case&lt;/th&gt;
&lt;th&gt;Can be indexed?&lt;/th&gt;
&lt;th&gt;Performance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LIKE/ILIKE&lt;/td&gt;
&lt;td&gt;Wildcard-style search for small data&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;td&gt;Unpredictable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pg_trgm&lt;/td&gt;
&lt;td&gt;Similarity search for names, etc&lt;/td&gt;
&lt;td&gt;Yes (GIN/GIST)&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Text Search&lt;/td&gt;
&lt;td&gt;Natural language search&lt;/td&gt;
&lt;td&gt;Yes (GIN/GIST)&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In this article, we are going to learn about the inner workings of Full Text Search in Postgres and how to easily integrate Full Text Search into your Rails application using a fantastic gem named &lt;a href=&quot;https://rubygems.org/gems/pg_search&quot;&gt;pg_search&lt;/a&gt;. We will learn how to search multiple columns at once, to give one column precedence over another, and how to optimize our Full Text Search implementation, taking a single query from &lt;strong&gt;130ms&lt;/strong&gt; to &lt;strong&gt;7ms&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The full source code used in this article can &lt;a href=&quot;https://github.com/pganalyze/full-text-search-rails&quot;&gt;be found here&lt;/a&gt;. Instructions on how to run this application locally and how to load the sample data referenced within this article can be found in the README.&lt;/p&gt;
&lt;p&gt;If you are interested in efficient &lt;a href=&quot;https://pganalyze.com/blog/full-text-search-django-postgres&quot;&gt;Full Text Search in Postgres with Django&lt;/a&gt;, you can read our article about it.&lt;/p&gt;
&lt;h2 id=&quot;the-foundations-of-full-text-search&quot; &gt;&lt;a href=&quot;#the-foundations-of-full-text-search&quot; aria-label=&quot;the foundations of full text search permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Foundations of Full Text Search&lt;/h2&gt;
&lt;p&gt;Let&apos;s break down the basics of Full Text Search, defining and explaining some of the most common terms you&apos;ll run into. Taking the text “looking for the right words”, we can see how Postgres stores this data internally, using the &lt;code &gt;to_tsvector&lt;/code&gt; function:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;looking for the right words&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;;&lt;/span&gt;
&lt;span &gt;-- &apos;look&apos;:1 &apos;right&apos;:4 &apos;word&apos;:5&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the above SQL we have some text; often referred to as a &lt;code &gt;document&lt;/code&gt; when talking about Full Text Search. A document &lt;em&gt;must&lt;/em&gt; be parsed and converted into a special data type called a &lt;code &gt;tsvector&lt;/code&gt;, which we did using the function &lt;code &gt;to_tsvector&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code &gt;tsvector&lt;/code&gt; data type is comprised of &lt;a href=&quot;https://en.wikipedia.org/wiki/Lexeme&quot;&gt;lexemes&lt;/a&gt;. Lexemes are &lt;a href=&quot;https://github.com/Casecommons/pg_search#normalization&quot;&gt;normalized key words&lt;/a&gt; which were contained in the document that will be used when searching through it. In this case we used the &lt;code &gt;english&lt;/code&gt; language dictionary to normalize the words, breaking them down to their root. This means that &lt;code &gt;words&lt;/code&gt; became &lt;code &gt;word&lt;/code&gt;, and &lt;code &gt;looking&lt;/code&gt; became &lt;code &gt;look&lt;/code&gt;, with &lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch-dictionaries.html#TEXTSEARCH-STOPWORDS&quot;&gt;very common words&lt;/a&gt; such as &lt;code &gt;for&lt;/code&gt; and &lt;code &gt;the&lt;/code&gt; being removed completely, to avoid false positives.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;looking for the right words&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; @@ to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;words&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;;&lt;/span&gt;
&lt;span &gt;-- TRUE&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code &gt;@@&lt;/code&gt; operator allows us to check if a query (data type &lt;code &gt;tsquery&lt;/code&gt;) exists within a document (data type &lt;code &gt;tsvector&lt;/code&gt;). Much like &lt;code &gt;tsvector&lt;/code&gt;, &lt;code &gt;tsquery&lt;/code&gt; is also normalized prior to searching the document for matches.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  ts_rank&lt;span &gt;(&lt;/span&gt;
    to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;looking for the right words&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;words&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
   &lt;span &gt;)&lt;/span&gt;&lt;span &gt;;&lt;/span&gt;
&lt;span &gt;-- 0.06079271&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code &gt;ts_rank&lt;/code&gt; function takes a &lt;code &gt;tsvector&lt;/code&gt; and a &lt;code &gt;tsquery&lt;/code&gt;, returning a number that can be used when sorting the matching records, allowing us to sort the results from highest to lowest ranking.&lt;/p&gt;
&lt;p&gt;Now that you have seen a few examples, let’s have a look at one last one before getting to Rails. Following, you can see an example of a query which searches through the &lt;code &gt;jobs&lt;/code&gt; table where we are storing the &lt;code &gt;title&lt;/code&gt; and &lt;code &gt;description&lt;/code&gt; of each job. Here we are searching for the words &lt;code &gt;ruby&lt;/code&gt; and &lt;code &gt;rails&lt;/code&gt;, grabbing the 3 highest ranking results.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  id&lt;span &gt;,&lt;/span&gt;
  title&lt;span &gt;,&lt;/span&gt;
  ts_rank&lt;span &gt;(&lt;/span&gt;
    to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; title&lt;span &gt;)&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; description&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;ruby &amp;amp; rails&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;)&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; rank
&lt;span &gt;FROM&lt;/span&gt; jobs
&lt;span &gt;WHERE&lt;/span&gt;
  to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; title&lt;span &gt;)&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; description&lt;span &gt;)&lt;/span&gt; @@
  to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;ruby &amp;amp; rails&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;ORDER&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; rank &lt;span &gt;DESC&lt;/span&gt;
&lt;span &gt;LIMIT&lt;/span&gt; &lt;span &gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The highest ranking result is a job with the title &quot;Ruby on Rails Developer&quot;... perfect! The full results of this query are:&lt;/p&gt;
&lt;div  data-language=&quot;json&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;[&lt;/span&gt;
  &lt;span &gt;{&lt;/span&gt;
    &lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;&quot;title&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&quot;Ruby on Rails Developer&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;&quot;rank&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;0.40266925&lt;/span&gt;
  &lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;{&lt;/span&gt;
    &lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;109&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;&quot;title&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&quot;Senior Ruby Developer - Remote&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;&quot;rank&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;0.26552397&lt;/span&gt;
  &lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;{&lt;/span&gt;
    &lt;span &gt;&quot;id&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;151&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;&quot;title&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&quot;Team-Lead Developer&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    &lt;span &gt;&quot;rank&quot;&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;0.14533159&lt;/span&gt;
  &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This query is actually concatenating (using &lt;code &gt;||&lt;/code&gt;) two &lt;code &gt;tsvector&lt;/code&gt; fields together. This allows us to search both the &lt;code &gt;title&lt;/code&gt; and the &lt;code &gt;description&lt;/code&gt; at the same time. Later, we&apos;ll see how to give additional weight (precedence) to the &lt;code &gt;title&lt;/code&gt; column.&lt;/p&gt;
&lt;h2 id=&quot;implementing-postgres-full-text-search-in-rails&quot; &gt;&lt;a href=&quot;#implementing-postgres-full-text-search-in-rails&quot; aria-label=&quot;implementing postgres full text search in rails permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Implementing Postgres Full Text Search in Rails&lt;/h2&gt;
&lt;p&gt;With a basic understanding of Full Text Search under our belts, it&apos;s time to take our knowledge over to Rails. We will be using the &lt;a href=&quot;https://rubygems.org/gems/pg_search&quot;&gt;pg_search&lt;/a&gt; Gem, which can be used in two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Casecommons/pg_search#multi-search&quot;&gt;Multi Search&lt;/a&gt;: Search across multiple models and return a single array of results. Imagine having three models: Product, Brand, and Review. Using &lt;strong&gt;Multi Search&lt;/strong&gt; we could search across all of them at the same time, seeing a single set of search results. This would be perfect for adding &lt;a href=&quot;https://en.wikipedia.org/wiki/Federated_search&quot;&gt;federated search&lt;/a&gt; functionality to your app.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Casecommons/pg_search#pg_search_scope&quot;&gt;Search Scope&lt;/a&gt;: Search within a single model, but with greater flexibility.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We will be focusing on the &lt;strong&gt;Search Scope&lt;/strong&gt; approach in this article, as it lets us dive into the configuration options available when working with Full Text Search in Rails. Let&apos;s add the Gem to our &lt;code &gt;Gemfile&lt;/code&gt; and get started:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;# Gemfile&lt;/span&gt;
gem &lt;span &gt;&apos;pg_search&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;~&gt; 2.3&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&gt;= 2.3.2&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With that done, we can include a module in our &lt;code &gt;Job&lt;/code&gt; model, and define our first searchable field:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Job&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  &lt;span &gt;include&lt;/span&gt; &lt;span &gt;PgSearch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Model&lt;/span&gt;
  pg_search_scope &lt;span &gt;:search_title&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; against&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:title&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This adds a class level method to &lt;code &gt;Job&lt;/code&gt;, allowing us to find jobs with the following line, which automatically returns them ranked from best match to worst.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Job&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;search_title&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;Ruby on Rails&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we were to append &lt;code &gt;to_sql&lt;/code&gt; to the above Ruby statement, we can see the SQL that is being generated. I have to warn you, it’s a bit messy, but that is because it handles not only searching, but also putting the results in the correct order using the &lt;code &gt;ts_rank&lt;/code&gt; function.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  &lt;span &gt;&quot;jobs&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt;
  &lt;span &gt;&quot;jobs&quot;&lt;/span&gt;
  &lt;span &gt;INNER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
    &lt;span &gt;SELECT&lt;/span&gt;
      &lt;span &gt;&quot;jobs&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; pg_search_id&lt;span &gt;,&lt;/span&gt;
      &lt;span &gt;(&lt;/span&gt;ts_rank&lt;span &gt;(&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;coalesce&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;jobs&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;title&quot;&lt;/span&gt;::&lt;span &gt;text&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&apos; &apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos;Ruby&apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos; &apos;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;&amp;amp;&amp;amp;&lt;/span&gt; to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&apos; &apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos;on&apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos; &apos;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;&amp;amp;&amp;amp;&lt;/span&gt; to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&apos; &apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos;Rails&apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos; &apos;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;0&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; rank
    &lt;span &gt;FROM&lt;/span&gt;
      &lt;span &gt;&quot;jobs&quot;&lt;/span&gt;
    &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;coalesce&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;jobs&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;title&quot;&lt;/span&gt;::&lt;span &gt;text&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; @@ &lt;span &gt;(&lt;/span&gt;to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&apos; &apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos;Ruby&apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos; &apos;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;&amp;amp;&amp;amp;&lt;/span&gt; to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&apos; &apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos;on&apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos; &apos;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;&amp;amp;&amp;amp;&lt;/span&gt; to_tsquery&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;simple&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;&apos;&apos; &apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos;Rails&apos;&lt;/span&gt; &lt;span &gt;||&lt;/span&gt; &lt;span &gt;&apos; &apos;&apos;&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; pg_search_5d9a17cb70b9733aadc073 &lt;span &gt;ON&lt;/span&gt; &lt;span &gt;&quot;jobs&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; pg_search_5d9a17cb70b9733aadc073&lt;span &gt;.&lt;/span&gt;pg_search_id
&lt;span &gt;ORDER&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt;
  pg_search_5d9a17cb70b9733aadc073&lt;span &gt;.&lt;/span&gt;rank &lt;span &gt;DESC&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;jobs&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt; &lt;span &gt;ASC&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;configuring-pg_search&quot; &gt;&lt;a href=&quot;#configuring-pg_search&quot; aria-label=&quot;configuring pg_search permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Configuring pg_search&lt;/h2&gt;
&lt;p&gt;There are a number of ways you can configure pg_search: From support for &lt;a href=&quot;https://github.com/Casecommons/pg_search#prefix-postgresql-84-and-newer-only&quot;&gt;prefixes&lt;/a&gt; and &lt;a href=&quot;https://github.com/Casecommons/pg_search#negation&quot;&gt;negation&lt;/a&gt;, to specifying which language dictionary to use when normalizing the document, as well as adding multiple, weighted columns.&lt;/p&gt;
&lt;p&gt;By default &lt;code &gt;pg_search&lt;/code&gt; uses the &lt;code &gt;simple&lt;/code&gt; dictionary, which does zero normalization, but if we wanted to normalize our document using the &lt;code &gt;english&lt;/code&gt; dictionary, searching across both the &lt;code &gt;title&lt;/code&gt; and &lt;code &gt;description&lt;/code&gt;, it would look like:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Job&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  &lt;span &gt;include&lt;/span&gt; &lt;span &gt;PgSearch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Model&lt;/span&gt;
  pg_search_scope &lt;span &gt;:search_job&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
                  against&lt;span &gt;:&lt;/span&gt; &lt;span &gt;%i[title description]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
                  using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; tsearch&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; dictionary&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;english&apos;&lt;/span&gt; &lt;span &gt;}&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can perform a search in the same way we did before: &lt;code &gt;Job.search_job(&quot;Ruby on Rails&quot;)&lt;/code&gt;. If we wanted to give higher precedence to the &lt;code &gt;title&lt;/code&gt; column, we can add &lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch-controls.html&quot;&gt;weighting scores&lt;/a&gt; to each of the columns, with possible values of: A, B, C, D.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Job&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  &lt;span &gt;include&lt;/span&gt; &lt;span &gt;PgSearch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Model&lt;/span&gt;
  pg_search_scope &lt;span &gt;:search_job&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
                  against&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; title&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;A&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; description&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;B&apos;&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
                  using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; tsearch&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; dictionary&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;english&apos;&lt;/span&gt; &lt;span &gt;}&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When you start combining columns, weighting them, and choosing which dictionary provides the best results, it really comes down to trial and error. Play around with it, try some queries and see if the results you get back match with what you are expecting!&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/efficient-search-in-rails-with-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        title=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        src=&quot;https://pganalyze.com/static/3e8bb134d6b5689ee9d20a10e6699b6c/acb04/ebook_promo_rails_search.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;optimizing-full-text-search-queries-in-rails&quot; &gt;&lt;a href=&quot;#optimizing-full-text-search-queries-in-rails&quot; aria-label=&quot;optimizing full text search queries in rails permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Optimizing Full Text Search Queries in Rails&lt;/h2&gt;
&lt;p&gt;We have a problem! The query that is produced by &lt;code &gt;Job.search_job(&quot;Ruby on Rails&quot;)&lt;/code&gt; takes an astounding &lt;strong&gt;130ms&lt;/strong&gt;. That may not &lt;em&gt;seem&lt;/em&gt; like such a large number, but it is astounding because there are only 145 records in my database. Imagine if there were thousands! The majority of time is spent in the &lt;code &gt;to_tsvector&lt;/code&gt; function. We can verify this by running this streamlined query below, which takes almost as much time to execute as the full query which actually finds the matching jobs:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; to_tsvector&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; description&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; jobs&lt;span &gt;;&lt;/span&gt;
&lt;span &gt;-- ~130ms&lt;/span&gt;

&lt;span &gt;SELECT&lt;/span&gt; description &lt;span &gt;FROM&lt;/span&gt; jobs&lt;span &gt;;&lt;/span&gt;
&lt;span &gt;-- ~15ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This tells me that the slowness is in re-parsing and normalizing the document into a &lt;code &gt;tsvector&lt;/code&gt; data type every single time the query is executed. The folks at thoughtbot have a &lt;a href=&quot;https://thoughtbot.com/blog/optimizing-full-text-search-with-postgres-tsvector-columns-and-triggers&quot;&gt;great article about Full Text Search optimizations&lt;/a&gt;, where they add a pre-calculated &lt;code &gt;tsvector&lt;/code&gt; column, keeping it up-to-date with triggers. This is great because it &lt;strong&gt;allows us to avoid re-parsing our document for every query&lt;/strong&gt; and also lets us index this column!&lt;/p&gt;
&lt;p&gt;There is a similar but slightly different approach I want to cover today which I learned by reading through the Postgres documentation. It also involves adding a pre-calculated &lt;code &gt;tsvector&lt;/code&gt; column, but is done using a &lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch-tables.html&quot;&gt;stored generated column&lt;/a&gt;. &lt;strong&gt;This means we don&apos;t need any triggers!&lt;/strong&gt; It should be noted that this approach is &lt;strong&gt;only available in Postgres 12 and above.&lt;/strong&gt; If you are using version 11 or earlier, the approach in the thoughtbot article is probably still the best one.&lt;/p&gt;
&lt;p&gt;As we are venturing into the territory of more custom Postgres functionality, not easily supported by the Rails schema file in Ruby, we&apos;ll want to &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_record_migrations.html#types-of-schema-dumps&quot;&gt;switch the schema format&lt;/a&gt; from &lt;code &gt;:ruby&lt;/code&gt; to &lt;code &gt;:sql&lt;/code&gt;. This line can be added to the &lt;code &gt;application.rb&lt;/code&gt; file:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;config&lt;span &gt;.&lt;/span&gt;active_record&lt;span &gt;.&lt;/span&gt;schema_format &lt;span &gt;=&lt;/span&gt; &lt;span &gt;:sql&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, let&apos;s generate a migration to add a new column to the &lt;code &gt;jobs&lt;/code&gt; table which will be automatically generated based on the &lt;code &gt;setweight&lt;/code&gt; and &lt;code &gt;to_tsvector&lt;/code&gt; functions:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;AddSearchableColumnToJobs&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Migration&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;6.0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;up&lt;/span&gt;&lt;/span&gt;
    execute &lt;span &gt;&lt;span &gt;&lt;span &gt;&amp;lt;&amp;lt;-&lt;/span&gt;SQL&lt;/span&gt;
      ALTER TABLE jobs
      ADD COLUMN searchable tsvector GENERATED ALWAYS AS (
        setweight(to_tsvector(&apos;english&apos;, coalesce(title, &apos;&apos;)), &apos;A&apos;) ||
        setweight(to_tsvector(&apos;english&apos;, coalesce(description,&apos;&apos;)), &apos;B&apos;)
      ) STORED;
    &lt;span &gt;SQL&lt;/span&gt;&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;down&lt;/span&gt;&lt;/span&gt;
    remove_column &lt;span &gt;:jobs&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:searchable&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that, as of the writing of this article, Postgres always &lt;strong&gt;requires a generated column to be a “Stored” column&lt;/strong&gt;. That means it actually occupies space in your table and gets written on each INSERT/UPDATE. This also means that when you add a generated column to a table, it will require a rewrite of the table to actually set the values for all existing rows. This &lt;strong&gt;may block other operations&lt;/strong&gt; on your database.&lt;/p&gt;
&lt;p&gt;With our &lt;code &gt;tsvector&lt;/code&gt; column added (which is giving precedence to the &lt;code &gt;title&lt;/code&gt; over the &lt;code &gt;description&lt;/code&gt;, is using the &lt;code &gt;english&lt;/code&gt; dictionary, and is coalescing &lt;code &gt;null&lt;/code&gt; values into empty strings), we&apos;re ready to add an index to it. Either &lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch-indexes.html&quot;&gt;GIN or GiST indexes&lt;/a&gt; can be used to speed up full text searches, but Postgres recommends &lt;code &gt;GIN&lt;/code&gt; as the preferred index due to &lt;code &gt;GiST&lt;/code&gt; searches being lossy, which &lt;strong&gt;may produce false matches&lt;/strong&gt;. We&apos;ll &lt;a href=&quot;https://thoughtbot.com/blog/how-to-create-postgres-indexes-concurrently-in&quot;&gt;add it concurrently&lt;/a&gt; to avoid locking issues when adding an index to large tables.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;AddIndexToSearchableJobs&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Migration&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;6.0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  disable_ddl_transaction&lt;span &gt;!&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;change&lt;/span&gt;&lt;/span&gt;
    add_index &lt;span &gt;:jobs&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:searchable&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:gin&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; algorithm&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:concurrently&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The last thing we need to do is to tell &lt;code &gt;pg_search&lt;/code&gt; to &lt;a href=&quot;https://github.com/Casecommons/pg_search#using-tsvector-columns&quot;&gt;use our tsvector&lt;/a&gt; &lt;code &gt;searchable&lt;/code&gt; column, rather than re-parsing the &lt;code &gt;title&lt;/code&gt; and &lt;code &gt;description&lt;/code&gt; fields each time. This is done by adding the &lt;code &gt;tsvector_column&lt;/code&gt; option to &lt;code &gt;tsearch&lt;/code&gt;:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Job&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  &lt;span &gt;include&lt;/span&gt; &lt;span &gt;PgSearch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Model&lt;/span&gt;
  pg_search_scope &lt;span &gt;:search_job&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
                  against&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; title&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;A&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; description&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;B&apos;&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
                  using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
                    tsearch&lt;span &gt;:&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
                      dictionary&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;english&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; tsvector_column&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;searchable&apos;&lt;/span&gt;
                    &lt;span &gt;}&lt;/span&gt;
                  &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With this optimization done, we have gone from around &lt;strong&gt;130ms&lt;/strong&gt; to &lt;strong&gt;7ms&lt;/strong&gt; per query... not bad at all!&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/advanced-database-programming-rails-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        title=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        src=&quot;https://pganalyze.com/static/24260e03f3c098e161f84b87ce28122b/acb04/ebook_promo_advanced_database_programming_rails_postgres.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; &gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Let’s have a look at a real-life data set. We can prove the precision of our approach by looking at my database: Out of 145 jobs pulled from the &lt;a href=&quot;https://jobs.github.com/&quot;&gt;GitHub&lt;/a&gt; and &lt;a href=&quot;https://news.ycombinator.com/jobs&quot;&gt;Hacker News&lt;/a&gt; job APIs, searching for &quot;Ruby on Rails&quot; returns the following results:&lt;/p&gt;
&lt;div  data-language=&quot;json&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;[&lt;/span&gt;
  &lt;span &gt;&quot;Ruby on Rails Developer&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;Senior Ruby Developer - Remote&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;Gobble (YC W14) – Senior Full Stack Software Engineers – Toronto, On&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;DevOps (Remote - Europe)&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;CareRev (YC S16) Is Hiring a Senior Back End Engineer in Los Angeles&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;Software Engineer, Full Stack (Rails, React)&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;Software Engineer&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;&quot;Technology Solutions Developer&quot;&lt;/span&gt;
&lt;span &gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;To summarize:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We have shown how to use Postgres&apos; Full Text Search within Rails and also how to customize it both in terms of functionality, but also in terms of performance. We ended up with a performant and flexible solution right inside the database we were already using.&lt;/p&gt;
&lt;p&gt;Many use cases for Full Text Search can be implemented directly inside Postgres, avoiding the need to install and maintain additional services such as Elasticsearch.&lt;/p&gt;
&lt;p&gt;If you find this article useful and want to share it with your peers you can &lt;a href=&quot;https://ctt.ac/dc_7d&quot;&gt;tweet about it here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;you-might-also-be-interested-in&quot; &gt;&lt;a href=&quot;#you-might-also-be-interested-in&quot; aria-label=&quot;you might also be interested in permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;You might also be interested in&lt;/h2&gt;
&lt;p&gt;Learn more about how to make the most of Postgres and Ruby on Rails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/optimizing-postgres-query-performance&quot;&gt;eBook: Best Practices for Optimizing Postgres Query Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a src=&quot;https://pganalyze.com/blog/materialized-views-ruby-rails&quot;&gt;Effectively Using Materialized Views in Ruby on Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a src=&quot;https://pganalyze.com/blog/efficient-graphql-queries-in-ruby-on-rails-and-postgres&quot;&gt;Efficient GraphQL queries in Ruby on Rails &amp;#x26; Postgres&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;about-the-author&quot; &gt;&lt;a href=&quot;#about-the-author&quot; aria-label=&quot;about the author permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About the author&lt;/h2&gt;
&lt;p&gt;Leigh Halliday is a guest author for the &lt;a src=&quot;https://pganalyze.com/&quot;&gt;pganalyze&lt;/a&gt; blog. He is a developer based out of Canada who works at &lt;a href=&quot;https://www.flipgive.com&quot;&gt;FlipGive&lt;/a&gt; as a full-stack developer. He writes about Ruby and React on &lt;a href=&quot;https://www.leighhalliday.com&quot;&gt;his blog&lt;/a&gt; and publishes React tutorials on &lt;a href=&quot;https://youtube.com/leighhalliday&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt; ]]&gt;</content:encoded></item><item><title><![CDATA[Effectively Using Materialized Views in Ruby on Rails]]></title><description><![CDATA[It's every developer's nightmare: SQL queries that get large and unwieldy. This can happen fairly quickly with the addition of multiple joins, a subquery and some complicated filtering logic. I have personally seen queries grow to nearly one hundred lines long in both the financial services and health industries. Luckily Postgres provides two ways to encapsulate large queries: Views and Materialized Views. In this article, we will cover in detail how to utilize both views and materialized views…]]></description><link>https://pganalyze.com/blog/materialized-views-ruby-rails</link><guid isPermaLink="false">https://pganalyze.com/blog/materialized-views-ruby-rails</guid><dc:creator><![CDATA[Leigh Halliday]]></dc:creator><pubDate>Thu, 16 Jan 2020 12:00:00 GMT</pubDate><content:encoded>&lt;![CDATA[ &lt;p&gt;It&apos;s every developer&apos;s nightmare: SQL queries that get large and unwieldy. This can happen fairly quickly with the addition of multiple joins, a subquery and some complicated filtering logic. I have personally seen queries grow to nearly one hundred lines long in both the financial services and health industries.&lt;/p&gt;
&lt;p&gt;Luckily Postgres provides two ways to encapsulate large queries: &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-createview.html&quot;&gt;Views&lt;/a&gt; and &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-creatematerializedview.html&quot;&gt;Materialized Views&lt;/a&gt;. In this article, we will cover in detail how to utilize both views and materialized views within &lt;strong&gt;Ruby on Rails&lt;/strong&gt;, and we can even take a look at creating and modifying them with database migrations.&lt;/p&gt;
&lt;p&gt;Our example will be a real-world sized dataset of hockey teams and their top scorers. If you&apos;d like to follow along, the source code covered in this article &lt;a href=&quot;https://github.com/pganalyze/materialized-views-demo&quot;&gt;can be found here&lt;/a&gt;.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-is-a-view&quot;&gt;What is a view?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-makes-a-view-materialized&quot;&gt;What makes a view materialized?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#creating-a-materialized-view&quot;&gt;Creating a materialized view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#utilizing-a-materialized-view&quot;&gt;Utilizing a materialized view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#refreshing-a-materialized-view&quot;&gt;Refreshing a materialized view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#when-to-use-views-vs-materialized-views&quot;&gt;When to use views vs. materialized views?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#migrating-views&quot;&gt;Migrating views&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#testing-with-materialized-views&quot;&gt;Testing with materialized views&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#about-the-author&quot;&gt;About the Author&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;We&apos;ll also talk a bit about the performance benefits that a &lt;strong&gt;Materialized View&lt;/strong&gt; can bring to your application:&lt;/p&gt;
&lt;p &gt;
&lt;span  &gt;
      &lt;a  src=&quot;https://pganalyze.com/static/e8d6f0687816761fa4f44cf291976afc/d698c/header.png&quot;  target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span  &gt;&lt;/span&gt;
  &lt;img  alt=&quot;Visualization of Materialized View Plan Difference&quot; title=&quot;Visualization of Materialized View Plan Difference&quot; src=&quot;https://pganalyze.com/static/e8d6f0687816761fa4f44cf291976afc/1d69c/header.png&quot;    loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;h2 id=&quot;what-is-a-view&quot; &gt;&lt;a href=&quot;#what-is-a-view&quot; aria-label=&quot;what is a view permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What is a view?&lt;/h2&gt;
&lt;p&gt;A view allows us to query against the result of another query, providing a powerful way of abstracting away a complex query full of joins, conditions, groupings, and any other clause that can be added to an SQL query. Looking at the query below, it isn&apos;t overly complex, but it &lt;em&gt;does&lt;/em&gt; include 3 joins, grouping by a number of fields to aggregate the numbers of goals scored for a player each season.&lt;/p&gt;
&lt;p&gt;It takes approximately 450ms to execute on my computer. I am using seed data that generates &lt;a href=&quot;https://www.nhl.com/info/teams&quot;&gt;31 teams&lt;/a&gt;, each playing 200 games in a season, scoring 20 goals per game... a little unrealistic, but I wanted the dataset used to be substantial!&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt;
  players&lt;span &gt;.&lt;/span&gt;name &lt;span &gt;AS&lt;/span&gt; player_name&lt;span &gt;,&lt;/span&gt;
  players&lt;span &gt;.&lt;/span&gt;id &lt;span &gt;AS&lt;/span&gt; player_id&lt;span &gt;,&lt;/span&gt;
  players&lt;span &gt;.&lt;/span&gt;position &lt;span &gt;AS&lt;/span&gt; player_position&lt;span &gt;,&lt;/span&gt;
  matches&lt;span &gt;.&lt;/span&gt;season &lt;span &gt;AS&lt;/span&gt; season&lt;span &gt;,&lt;/span&gt;
  teams&lt;span &gt;.&lt;/span&gt;name &lt;span &gt;AS&lt;/span&gt; team_name&lt;span &gt;,&lt;/span&gt;
  teams&lt;span &gt;.&lt;/span&gt;id &lt;span &gt;AS&lt;/span&gt; team_id&lt;span &gt;,&lt;/span&gt;
  &lt;span &gt;count&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;goals&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt; &lt;span &gt;AS&lt;/span&gt; goal_count
&lt;span &gt;FROM&lt;/span&gt; goals
  &lt;span &gt;INNER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; players &lt;span &gt;ON&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;goals&lt;span &gt;.&lt;/span&gt;player_id &lt;span &gt;=&lt;/span&gt; players&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;INNER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; matches &lt;span &gt;ON&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;goals&lt;span &gt;.&lt;/span&gt;match_id &lt;span &gt;=&lt;/span&gt; matches&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;INNER&lt;/span&gt; &lt;span &gt;JOIN&lt;/span&gt; teams &lt;span &gt;ON&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;goals&lt;span &gt;.&lt;/span&gt;team_id &lt;span &gt;=&lt;/span&gt; teams&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;GROUP&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; players&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;,&lt;/span&gt; teams&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;,&lt;/span&gt; matches&lt;span &gt;.&lt;/span&gt;season&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A view allows us to take the final result of this query, and query &lt;em&gt;against&lt;/em&gt; that as if it were any other table. You can see why views can come in handy in many different scenarios. They allow for the succinct abstraction of a complicated query, and allow us to re-use this logic in a simple to understand way.&lt;/p&gt;
&lt;p&gt;Now, we could make a new view by running &lt;code &gt;CREATE VIEW&lt;/code&gt; in Postgres. But, as we all know, one-off schema changes are hard to keep track of. Instead, let&apos;s try something thats closer to how Rails does things. How does that look like? First things first, we&apos;ll create a view using &lt;a href=&quot;https://github.com/scenic-views/scenic&quot;&gt;Scenic&lt;/a&gt;. Scenic gives us the ability to define migrations that create, update, or drop views, just as you&apos;re used to doing with regular tables in Rails.&lt;/p&gt;
&lt;div  data-language=&quot;shell&quot;&gt;&lt;pre &gt;&lt;code &gt;rails g scenic:view top_scorers&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will generate two files. The first is named &lt;code &gt;db/views/top_scorers_v01.sql&lt;/code&gt;, and in it we will paste the SQL for the underlying query (from above). The second is &lt;code &gt;db/migrate/[date]_create_top_scorers.rb&lt;/code&gt;, and this is where the migration will live to migrate/rollback the creation of our view:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;CreateTopScorers&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Migration&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;6.0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;change&lt;/span&gt;&lt;/span&gt;
    create_view &lt;span &gt;:top_scorers&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With the view in place, we can now query against it. This query takes approximately 50ms to execute.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt; top_scorers
&lt;span &gt;WHERE&lt;/span&gt;
  team_name &lt;span &gt;=&lt;/span&gt; &lt;span &gt;&apos;Toronto Maple Leafs&apos;&lt;/span&gt;
&lt;span &gt;ORDER&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; goal_count &lt;span &gt;DESC&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By creating a model in Rails, we can interact with it much like we would be able to with a typical model which is backed by a table. First things first, let&apos;s define the model, letting Rails know it is &lt;strong&gt;read-only&lt;/strong&gt;. Whilst some views &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS&quot;&gt;can be updated&lt;/a&gt;, this view contains a top-level &lt;code &gt;GROUP BY&lt;/code&gt; clause and thus can&apos;t be updated.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;# app/models/top_scorer.rb&lt;/span&gt;
&lt;span &gt;class&lt;/span&gt; &lt;span &gt;TopScorer&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;readonly&lt;/span&gt;&lt;/span&gt;&lt;span &gt;?&lt;/span&gt;
    &lt;span &gt;true&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now we can perform the same SQL query using the &lt;code &gt;TopScorer&lt;/code&gt; model:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;TopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;team_name&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;Toronto Maple Leafs&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;order&lt;span &gt;(&lt;/span&gt;goal_count&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:desc&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;what-makes-a-view-materialized&quot; &gt;&lt;a href=&quot;#what-makes-a-view-materialized&quot; aria-label=&quot;what makes a view materialized permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What makes a view materialized?&lt;/h2&gt;
&lt;p&gt;A regular view still performs the underlying query which defined it. &lt;strong&gt;It will only be as efficient as its underlying query is&lt;/strong&gt;. This means, if the larger query discussed above takes 450ms to execute, executing &lt;code &gt;SELECT * FROM top_scorers&lt;/code&gt; will also take 450ms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Materialized views take regular views to the next level&lt;/strong&gt;, though they aren&apos;t without their drawbacks. The difference is that they save the result of the original query to a cached/temporary table. When you query a materialized view, you aren&apos;t querying the source data, rather the cached result.&lt;/p&gt;
&lt;p&gt;This can provide serious performance benefits, especially considering you can index materialized views. But, when the underlying data from the source tables is updated, the materialized view becomes out of date, serving up an older cached version of the data. We can resolve this by refreshing the materialized view, which we&apos;ll get to in a bit.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-materialized-view&quot; &gt;&lt;a href=&quot;#creating-a-materialized-view&quot; aria-label=&quot;creating a materialized view permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Creating a materialized view&lt;/h2&gt;
&lt;p&gt;Just like we saw with our regular view, materialized views begin the same way, by executing a command to generate a new view migration: &lt;code &gt;rails g scenic:view mat_top_scorers&lt;/code&gt;. This produces two files, the first of which contains the SQL to produce the underlying view of the data. The difference is in the migration, passing in &lt;code &gt;materialized: true&lt;/code&gt; to the &lt;code &gt;create_view&lt;/code&gt; method. Also &lt;strong&gt;notice that we are able to add indexes to the materialized view&lt;/strong&gt;.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;CreateMatTopScorers&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Migration&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;6.0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;change&lt;/span&gt;&lt;/span&gt;
    create_view &lt;span &gt;:mat_top_scorers&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; materialized&lt;span &gt;:&lt;/span&gt; &lt;span &gt;true&lt;/span&gt;

    add_index &lt;span &gt;:mat_top_scorers&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:player_name&lt;/span&gt;
    add_index &lt;span &gt;:mat_top_scorers&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:player_id&lt;/span&gt;
    add_index &lt;span &gt;:mat_top_scorers&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:team_name&lt;/span&gt;
    add_index &lt;span &gt;:mat_top_scorers&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:team_id&lt;/span&gt;
    add_index &lt;span &gt;:mat_top_scorers&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:season&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;utilizing-a-materialized-view&quot; &gt;&lt;a href=&quot;#utilizing-a-materialized-view&quot; aria-label=&quot;utilizing a materialized view permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Utilizing a materialized view&lt;/h2&gt;
&lt;p&gt;Like a regular view, we are able to define an ActiveRecord model that can query it. Also notice that we can define relationships which point to other ActiveRecord models. If you didn&apos;t know, you might not even realize it is pointing to a materialized view, except for the &lt;code &gt;readonly?&lt;/code&gt; method which was defined.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;MatTopScorer&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  belongs_to &lt;span &gt;:player&lt;/span&gt;
  belongs_to &lt;span &gt;:team&lt;/span&gt;
  belongs_to &lt;span &gt;:match&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;top_scorer_for_season&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;season&lt;span &gt;)&lt;/span&gt;
    where&lt;span &gt;(&lt;/span&gt;season&lt;span &gt;:&lt;/span&gt; season&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;order&lt;span &gt;(&lt;/span&gt;goal_count&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:desc&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;first
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;readonly&lt;/span&gt;&lt;/span&gt;&lt;span &gt;?&lt;/span&gt;
    &lt;span &gt;true&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let&apos;s take our new materialized view for a spin! Running the query &lt;code &gt;select * from mat_top_scorers&lt;/code&gt;, which took 450ms as a view, takes 5ms as a materialized view, &lt;strong&gt;90x faster&lt;/strong&gt;! The ruby code below, which took 50ms as a view, takes under 1ms to execute!&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;team_name&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;Toronto Maple Leafs&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;order&lt;span &gt;(&lt;/span&gt;goal_count&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:desc&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For a side-by-side comparison, this performs the same query on both views:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;irb&lt;span &gt;(&lt;/span&gt;main&lt;span &gt;)&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;001&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;0&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;TopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;team_name&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;Toronto Maple Leafs&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count
   &lt;span &gt;(&lt;/span&gt;&lt;span &gt;60.2&lt;/span&gt;ms&lt;span &gt;)&lt;/span&gt;  &lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;top_scorers&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;&quot;top_scorers&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;team_name&quot;&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; $&lt;span &gt;1&lt;/span&gt;  &lt;span &gt;[&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;&quot;team_name&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;Toronto Maple Leafs&quot;&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;30&lt;/span&gt;

irb&lt;span &gt;(&lt;/span&gt;main&lt;span &gt;)&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;002&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;0&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;team_name&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&apos;Toronto Maple Leafs&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count
   &lt;span &gt;(&lt;/span&gt;&lt;span &gt;1.3&lt;/span&gt;ms&lt;span &gt;)&lt;/span&gt;  &lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;COUNT&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;mat_top_scorers&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;&quot;mat_top_scorers&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;team_name&quot;&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; $&lt;span &gt;1&lt;/span&gt;  &lt;span &gt;[&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;&quot;team_name&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;Toronto Maple Leafs&quot;&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
&lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;30&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;refreshing-a-materialized-view&quot; &gt;&lt;a href=&quot;#refreshing-a-materialized-view&quot; aria-label=&quot;refreshing a materialized view permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Refreshing a materialized view&lt;/h2&gt;
&lt;p&gt;As mentioned previously, materialized views cache the underlying query&apos;s result to a temporary table. This is what gives us the speed improvements and the ability to add indexes. The downside is that we have to control when the cache is refreshed. Modifying the &lt;code &gt;MatTopScorer&lt;/code&gt; model, let&apos;s add a &lt;code &gt;refresh&lt;/code&gt; method that can be called any time the data is to be refreshed. You will need to figure out how often it makes sense to update the data for your specific use-case, depending on how often the data is changing and how quickly those changes need to be reflected to the end user.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;MatTopScorer&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  belongs_to &lt;span &gt;:player&lt;/span&gt;
  belongs_to &lt;span &gt;:team&lt;/span&gt;
  belongs_to &lt;span &gt;:match&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;refresh&lt;/span&gt;&lt;/span&gt;
    &lt;span &gt;Scenic&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;database&lt;span &gt;.&lt;/span&gt;refresh_materialized_view&lt;span &gt;(&lt;/span&gt;table_name&lt;span &gt;,&lt;/span&gt; concurrently&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; cascade&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;top_scorer_for_season&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;season&lt;span &gt;)&lt;/span&gt;
    where&lt;span &gt;(&lt;/span&gt;season&lt;span &gt;:&lt;/span&gt; season&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;order&lt;span &gt;(&lt;/span&gt;goal_count&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:desc&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;first
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;readonly&lt;/span&gt;&lt;/span&gt;&lt;span &gt;?&lt;/span&gt;
    &lt;span &gt;true&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To schedule the refresh, I like to use the &lt;a href=&quot;https://github.com/javan/whenever&quot;&gt;whenever gem&lt;/a&gt;. Let&apos;s call a rake task to refresh the materialized view every hour:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;# config/schedule.rb&lt;/span&gt;
every &lt;span &gt;1.&lt;/span&gt;hour &lt;span &gt;do&lt;/span&gt;
  rake &lt;span &gt;&quot;refreshers:mat_top_scorers&quot;&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The rake task is simple, only calling the &lt;code &gt;refresh&lt;/code&gt; method defined on the &lt;code &gt;MatTopScorer&lt;/code&gt; model.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;# lib/tasks/refreshers.rake&lt;/span&gt;
namespace &lt;span &gt;:refreshers&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
  desc &lt;span &gt;&quot;Refresh materialized view for top scorers&quot;&lt;/span&gt;
  task mat_top_scorers&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:environment&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
    &lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;refresh
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;when-to-use-views-vs-materialized-views&quot; &gt;&lt;a href=&quot;#when-to-use-views-vs-materialized-views&quot; aria-label=&quot;when to use views vs materialized views permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;When to use views vs. materialized views?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Views&lt;/strong&gt; focus on abstracting away complexity and encouraging reuse. Views allow you to interact with the &lt;em&gt;result&lt;/em&gt; of a query as if it were a table itself, but they do not provide a performance benefit, as the underlying query is still executed, perfect for sharing logic but still having real-time access to the source data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Materialized Views&lt;/strong&gt; are related to views, but go a step further. You get all the abstraction and reuse of a view, but the underlying data is cached, providing serious performance benefits. Materialized views are especially useful for - for example - reporting dashboards because they can be indexed to allow for performant filtering.&lt;/p&gt;
&lt;p&gt;If the purpose of the view is to provide a cleaner interface to complicated joins and query logic, and performance isn&apos;t too much of an issue, by all means stick with a regular view. Views have the advantage of always being &lt;em&gt;real-time&lt;/em&gt;, since they simply reference the real underlying data rather than a cached copy of it.&lt;/p&gt;
&lt;p&gt;If your purpose is to provide a cleaner interface in addition to performance improvements, and you can live with the data being not quite real-time, then creating it as a materialized view can provide some great benefits.&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/efficient-search-in-rails-with-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        title=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        src=&quot;https://pganalyze.com/static/3e8bb134d6b5689ee9d20a10e6699b6c/acb04/ebook_promo_rails_search.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;migrating-views&quot; &gt;&lt;a href=&quot;#migrating-views&quot; aria-label=&quot;migrating views permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Migrating views&lt;/h2&gt;
&lt;p&gt;It&apos;s easy to &lt;a href=&quot;https://github.com/scenic-views/scenic#cool-but-what-if-i-need-to-change-that-view&quot;&gt;migrate views&lt;/a&gt; in Scenic. Views are versioned by default in Scenic and generating a view with the same name will create a v2, providing two files, just like it did the first time we generated a view (earlier in this article). Likewise, Scenic also provides a way to &lt;a href=&quot;https://github.com/scenic-views/scenic#i-dont-need-this-view-anymore-make-it-go-away&quot;&gt;drop a view&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;testing-with-materialized-views&quot; &gt;&lt;a href=&quot;#testing-with-materialized-views&quot; aria-label=&quot;testing with materialized views permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Testing with materialized views&lt;/h2&gt;
&lt;p&gt;Views and materialized views aren&apos;t particularly challenging to test, but it does require remembering that both types of views don&apos;t contain any original data in and of themselves, they are either a live view of an underlying query, or a cached view of an underlying query, as in the case of materialized views.&lt;/p&gt;
&lt;p&gt;Let&apos;s see how we would populate and then test our &lt;code &gt;MatTopScorer&lt;/code&gt; model in &lt;a href=&quot;https://rspec.info/&quot;&gt;RSpec&lt;/a&gt; and &lt;a href=&quot;https://github.com/thoughtbot/factory_bot&quot;&gt;factory_bot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After creating some test data using factory_bot, we&apos;ll call a method which is supposed to return the top scorer for a given season. It returns &lt;code &gt;nil&lt;/code&gt;, and that is expected. The underlying data exists, but because materialized views must be refreshed, something we haven&apos;t done yet, there is no data to be found.&lt;/p&gt;
&lt;p&gt;After calling &lt;code &gt;MatTopScorer.refresh&lt;/code&gt;, we&apos;re now able to retrieve the expected result.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;RSpec&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;describe &lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; type&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:model&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
  describe &lt;span &gt;&quot;#top_scorer_for_season&quot;&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
    it &lt;span &gt;&quot;finds top scorer&quot;&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
      &lt;span &gt;# create some data using factory_bot helper methods&lt;/span&gt;
      match &lt;span &gt;=&lt;/span&gt; create&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:match&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
      player &lt;span &gt;=&lt;/span&gt; create&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:player&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
      goal &lt;span &gt;=&lt;/span&gt; create&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:goal&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; match&lt;span &gt;:&lt;/span&gt; match&lt;span &gt;,&lt;/span&gt; player&lt;span &gt;:&lt;/span&gt; player&lt;span &gt;)&lt;/span&gt;

      &lt;span &gt;# without any data in materialized view&lt;/span&gt;
      expect&lt;span &gt;(&lt;/span&gt;&lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;top_scorer_for_season&lt;span &gt;(&lt;/span&gt;match&lt;span &gt;.&lt;/span&gt;season&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to eq&lt;span &gt;(&lt;/span&gt;&lt;span &gt;nil&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;

      &lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;refresh

      &lt;span &gt;# with data in materialized view&lt;/span&gt;
      top_scorer &lt;span &gt;=&lt;/span&gt; &lt;span &gt;MatTopScorer&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;top_scorer_for_season&lt;span &gt;(&lt;/span&gt;match&lt;span &gt;.&lt;/span&gt;season&lt;span &gt;)&lt;/span&gt;
      expect&lt;span &gt;(&lt;/span&gt;top_scorer&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to be_present
      expect&lt;span &gt;(&lt;/span&gt;top_scorer&lt;span &gt;.&lt;/span&gt;player&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to eq&lt;span &gt;(&lt;/span&gt;player&lt;span &gt;)&lt;/span&gt;
      expect&lt;span &gt;(&lt;/span&gt;top_scorer&lt;span &gt;.&lt;/span&gt;goal_count&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to eq&lt;span &gt;(&lt;/span&gt;&lt;span &gt;1&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;conclusion&quot; &gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With the help of Scenic, using views and materialized views feels right at home in Rails. Truthfully, I haven&apos;t used views as much as I have used materialized views. In particular, I&apos;ve found materialized views incredibly useful when building searchable reporting dashboards.&lt;/p&gt;
&lt;p&gt;The ability to group and summarize data by geographic region, category, grouped by date, in combination with adding the correct indexes has provided an efficient way to report on large amounts of data without relying on external reporting systems or causing excessive load on the production database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share this article:&lt;/strong&gt; If you liked this article we’d appreciate it if you’d &lt;a href=&quot;https://ctt.ac/3w1DT&quot;&gt;tweet it to your peers&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;about-the-author&quot; &gt;&lt;a href=&quot;#about-the-author&quot; aria-label=&quot;about the author permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About the Author&lt;/h2&gt;
&lt;p&gt;Leigh Halliday is a guest author for the pganalyze blog. He is a developer based out of Canada who works at &lt;a href=&quot;https://www.flipgive.com&quot;&gt;FlipGive&lt;/a&gt; as a full-stack developer. He writes about Ruby and React on &lt;a href=&quot;https://www.leighhalliday.com&quot;&gt;his blog&lt;/a&gt; and publishes React tutorials on &lt;a href=&quot;https://youtube.com/leighhalliday&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt; ]]&gt;</content:encoded></item><item><title><![CDATA[Similarity in Postgres and Rails using Trigrams]]></title><description><![CDATA[You typed "postgras", did you mean "postgres"? Use the best tool for the job. It seems like solid advice, but there's something to say about keeping things simple. There is a training and maintenance cost that comes with supporting an ever growing number of tools. It may be better advice to use an existing tool that works well, although not perfect, until it hurts. It all depends on your specific case. Postgres is an amazing relational database, and it supports more features than you might…]]></description><link>https://pganalyze.com/blog/similarity-in-postgres-and-ruby-on-rails-using-trigrams</link><guid isPermaLink="false">https://pganalyze.com/blog/similarity-in-postgres-and-ruby-on-rails-using-trigrams</guid><dc:creator><![CDATA[Leigh Halliday]]></dc:creator><pubDate>Tue, 19 Nov 2019 12:00:00 GMT</pubDate><content:encoded>&lt;![CDATA[ &lt;p&gt;&lt;span
      
      
    &gt;
      &lt;a
    
    src=&quot;https://pganalyze.com/static/90cd2f76d32d33a522ec1069621e6651/2cefc/postgres_trigrams.png&quot;
    
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Trigram Example&quot;
        title=&quot;Trigram Example&quot;
        src=&quot;https://pganalyze.com/static/90cd2f76d32d33a522ec1069621e6651/1d69c/postgres_trigrams.png&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;You typed &lt;strong&gt;&quot;postgras&quot;&lt;/strong&gt;, did you mean &lt;strong&gt;&quot;postgres&quot;&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Use the best tool for the job.&lt;/em&gt; It seems like solid advice, but there&apos;s something to say about keeping things simple. There is a training and maintenance cost that comes with supporting an ever growing number of tools. It may be better advice to use an existing tool that works well, although not perfect, until it hurts. It all depends on your specific case.&lt;/p&gt;
&lt;p&gt;Postgres is an amazing relational database, and it supports more features than you might initially think! It has &lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch.html&quot;&gt;full text search&lt;/a&gt;, &lt;a href=&quot;https://www.postgresql.org/docs/current/datatype-json.html&quot;&gt;JSON documents&lt;/a&gt;, and support for similarity matching through its &lt;a href=&quot;https://www.postgresql.org/docs/current/pgtrgm.html&quot;&gt;pg_trgm&lt;/a&gt; module.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#what-are-trigrams&quot;&gt;What are Trigrams?&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#postgres-trigram-example&quot;&gt;Postgres Trigram example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ruby-trigram-example&quot;&gt;Ruby Trigram example&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#using-trigrams-in-rails&quot;&gt;Using Trigrams in Rails&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#showing-the-closest-matches-for-a-term-based-on-its-similarity&quot;&gt;Showing the closest matches for a term based on its similarity&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#about-the-author&quot;&gt;About the Author&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;Today, we will break down how to use &lt;strong&gt;pg_trgm&lt;/strong&gt; for a light-weight, built-in similarity matcher. Why are we doing this? Well, before reaching for a tool purpose-built for search such as &lt;a href=&quot;https://www.elastic.co&quot;&gt;Elasticsearch&lt;/a&gt;, potentially complicating development by adding another tool to your development stack, it&apos;s worth seeing if Postgres suits your application&apos;s needs! You may be surprised!&lt;/p&gt;
&lt;p&gt;In this article, we will look at how it works under the covers, and how to use it efficiently in your Rails app.&lt;/p&gt;
&lt;h2 id=&quot;what-are-trigrams&quot; &gt;&lt;a href=&quot;#what-are-trigrams&quot; aria-label=&quot;what are trigrams permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What are Trigrams?&lt;/h2&gt;
&lt;p&gt;Trigrams, a subset of &lt;a href=&quot;https://en.wikipedia.org/wiki/N-gram&quot;&gt;n-grams&lt;/a&gt;, break text down into groups of three consecutive letters. Let&apos;s see an example: &lt;code &gt;postgres&lt;/code&gt;. It is made up of &lt;strong&gt;six&lt;/strong&gt; groups: pos, ost, stg, tgr, gre, res.&lt;/p&gt;
&lt;p&gt;This process of breaking a piece of text into smaller groups allows you to compare the groups of one word to the groups of another word. Knowing how many groups are shared between the two words allows you to make a comparison between them based on how similar their groups are.&lt;/p&gt;
&lt;h3 id=&quot;postgres-trigram-example&quot; &gt;&lt;a href=&quot;#postgres-trigram-example&quot; aria-label=&quot;postgres trigram example permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Postgres Trigram example&lt;/h3&gt;
&lt;p&gt;Postgres&apos; &lt;code &gt;pg_trgm&lt;/code&gt; module comes with a number of functions and operators to compare strings. &lt;strong&gt;We&apos;ll look at the &lt;code &gt;show_trgm&lt;/code&gt; and &lt;code &gt;similarity&lt;/code&gt; functions, along with the &lt;code &gt;%&lt;/code&gt; operator below:&lt;/strong&gt;&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;select&lt;/span&gt;
  show_trgm&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;postgras&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;as&lt;/span&gt; tri1&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-- {&quot;  p&quot;,&quot; po&quot;,&quot;as &quot;,gra,ost,pos,ras,stg,tgr}&lt;/span&gt;
  show_trgm&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;postgres&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;as&lt;/span&gt; tri2&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-- {&quot;  p&quot;,&quot; po&quot;,&quot;es &quot;,gre,ost,pos,res,stg,tgr}&lt;/span&gt;
  similarity&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&apos;postgras&apos;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;&lt;span &gt;&apos;postgres&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-- 0.5&lt;/span&gt;
  &lt;span &gt;&apos;postgras&apos;&lt;/span&gt; &lt;span &gt;%&lt;/span&gt; &lt;span &gt;&apos;postgres&apos;&lt;/span&gt; &lt;span &gt;-- TRUE&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code &gt;show_trgm&lt;/code&gt; function isn&apos;t one you&apos;d necessarily use day-to-day, but it&apos;s good to see how Postgres breaks a string down into trigrams. You&apos;ll notice something interesting here, that two spaces are added to the beginning of the string, and a single space is added to the end.&lt;/p&gt;
&lt;p&gt;This is done for a couple of reasons:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The first reason&lt;/strong&gt; is that it allows trigram calculations on words with less than three characters, such as &lt;code &gt;Hi&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Secondly&lt;/strong&gt;, it ensures the first and last characters are not overly de-emphasized for comparisons. If we used only strict triplets, the first and last letters in longer words would each occur in only a single group: with padding they occur in three (for the first letter) and two (for the last). The last letter is less important for matching, which means that &lt;code &gt;postgres&lt;/code&gt; and &lt;code &gt;postgrez&lt;/code&gt; are more similar than &lt;code &gt;postgres&lt;/code&gt; and &lt;code &gt;postgras&lt;/code&gt;, even though they are both off by a single character.&lt;/p&gt;
&lt;p&gt;The &lt;code &gt;similarity&lt;/code&gt; function compares the trigrams from two strings and outputs a similarity number between 1 and 0. 1 means a perfect match, and 0 means no shared trigrams.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lastly&lt;/strong&gt;, we have the &lt;code &gt;%&lt;/code&gt; operator, which gives you a boolean of whether two strings are similar. By default, Postgres uses the number 0.3 when making this decision, but you can always update this setting.&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/efficient-search-in-rails-with-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        title=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        src=&quot;https://pganalyze.com/static/3e8bb134d6b5689ee9d20a10e6699b6c/acb04/ebook_promo_rails_search.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;ruby-trigram-example&quot; &gt;&lt;a href=&quot;#ruby-trigram-example&quot; aria-label=&quot;ruby trigram example permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Ruby Trigram example&lt;/h3&gt;
&lt;p&gt;You don&apos;t need to know how to build a trigram in order to use them in Postgres, but it doesn&apos;t hurt to dive deeper and expand your knowledge. Let&apos;s take a look at how to implement something similar ourselves in Ruby.&lt;/p&gt;
&lt;p&gt;The first method will take a string, and output an array of trigrams, adding two spaces to the front, and one to the back of the original string, just like Postgres does.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;trigram&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;word&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;return&lt;/span&gt; &lt;span &gt;[&lt;/span&gt;&lt;span &gt;]&lt;/span&gt; &lt;span &gt;if&lt;/span&gt; word&lt;span &gt;.&lt;/span&gt;strip &lt;span &gt;==&lt;/span&gt; &lt;span &gt;&quot;&quot;&lt;/span&gt;

  parts &lt;span &gt;=&lt;/span&gt; &lt;span &gt;[&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  padded &lt;span &gt;=&lt;/span&gt; &lt;span &gt;&quot;  &lt;span &gt;&lt;span &gt;#{&lt;/span&gt;word&lt;span &gt;}&lt;/span&gt;&lt;/span&gt; &quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;downcase
  padded&lt;span &gt;.&lt;/span&gt;chars&lt;span &gt;.&lt;/span&gt;each_cons&lt;span &gt;(&lt;/span&gt;&lt;span &gt;3&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;w&lt;span &gt;|&lt;/span&gt; parts &lt;span &gt;&amp;lt;&lt;/span&gt;&lt;span &gt;&amp;lt;&lt;/span&gt; w&lt;span &gt;.&lt;/span&gt;join &lt;span &gt;}&lt;/span&gt;
  parts
&lt;span &gt;end&lt;/span&gt;

p trigram&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;postgras&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;# [&quot;  p&quot;, &quot; po&quot;, &quot;pos&quot;, &quot;ost&quot;, &quot;stg&quot;, &quot;tgr&quot;, &quot;gra&quot;, &quot;ras&quot;, &quot;as &quot;]&lt;/span&gt;
p trigram&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;postgres&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;# [&quot;  p&quot;, &quot; po&quot;, &quot;pos&quot;, &quot;ost&quot;, &quot;stg&quot;, &quot;tgr&quot;, &quot;gre&quot;, &quot;res&quot;, &quot;es &quot;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Next up, we&apos;ll compare the trigrams from our two words together, giving a ratio of how similar they are:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;similarity&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;word1&lt;span &gt;,&lt;/span&gt; word2&lt;span &gt;)&lt;/span&gt;
  tri1 &lt;span &gt;=&lt;/span&gt; trigram&lt;span &gt;(&lt;/span&gt;word1&lt;span &gt;)&lt;/span&gt;
  tri2 &lt;span &gt;=&lt;/span&gt; trigram&lt;span &gt;(&lt;/span&gt;word2&lt;span &gt;)&lt;/span&gt;

  &lt;span &gt;return&lt;/span&gt; &lt;span &gt;0.0&lt;/span&gt; &lt;span &gt;if&lt;/span&gt; &lt;span &gt;[&lt;/span&gt;tri1&lt;span &gt;,&lt;/span&gt; tri2&lt;span &gt;]&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;any&lt;span &gt;?&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;arr&lt;span &gt;|&lt;/span&gt; arr&lt;span &gt;.&lt;/span&gt;size &lt;span &gt;==&lt;/span&gt; &lt;span &gt;0&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;

  &lt;span &gt;# Find number of trigrams shared between them&lt;/span&gt;
  same_size &lt;span &gt;=&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;tri1 &lt;span &gt;&amp;amp;&lt;/span&gt; tri2&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;size
  &lt;span &gt;# Find unique total trigrams in both arrays&lt;/span&gt;
  all_size &lt;span &gt;=&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;tri1 &lt;span &gt;|&lt;/span&gt; tri2&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;size

  same_size&lt;span &gt;.&lt;/span&gt;to_f &lt;span &gt;/&lt;/span&gt; all_size
&lt;span &gt;end&lt;/span&gt;

p similarity&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;postgras&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;postgres&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;# 0.5&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now that we have our similarity calculator, we can implement a simple &lt;code &gt;similar?&lt;/code&gt; method, which checks if the similarity is above the threshold of 0.3:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;similar&lt;/span&gt;&lt;/span&gt;&lt;span &gt;?&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;word1&lt;span &gt;,&lt;/span&gt; word2&lt;span &gt;)&lt;/span&gt;
  similarity&lt;span &gt;(&lt;/span&gt;word1&lt;span &gt;,&lt;/span&gt; word2&lt;span &gt;)&lt;/span&gt; &lt;span &gt;&gt;=&lt;/span&gt; &lt;span &gt;0.3&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;

p similar&lt;span &gt;?&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;postgras&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&quot;postgres&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;# true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;using-trigrams-in-rails&quot; &gt;&lt;a href=&quot;#using-trigrams-in-rails&quot; aria-label=&quot;using trigrams in rails permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Using Trigrams in Rails&lt;/h2&gt;
&lt;p&gt;There aren&apos;t too many gotchas in order to use these similarity functions and operators within your Rails app, but there are a couple!&lt;/p&gt;
&lt;p&gt;Below we have a migration to create a &lt;code &gt;cities&lt;/code&gt; table. When indexing the &lt;code &gt;name&lt;/code&gt; column, to ensure that querying with the similarity operator stays fast, we&apos;ll need to ensure that we use either a &lt;code &gt;gin&lt;/code&gt; or &lt;code &gt;gist&lt;/code&gt; index. We do this by indicating &lt;code &gt;using: :gin&lt;/code&gt;. &lt;strong&gt;In addition to that, we have to pass the opclass option&lt;/strong&gt; &lt;code &gt;opclass: :gin_trgm_ops&lt;/code&gt;, so it knows which type of &lt;code &gt;gin&lt;/code&gt; index to create.&lt;/p&gt;
&lt;p&gt;Unless you have already enabled the &lt;code &gt;pg_trgm&lt;/code&gt; extension, you will most likely receive an error, but this is easily fixed by adding &lt;code &gt;enable_extension :pg_trgm&lt;/code&gt; to your migration.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;CreateCities&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Migration&lt;/span&gt;&lt;span &gt;[&lt;/span&gt;&lt;span &gt;6.0&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;change&lt;/span&gt;&lt;/span&gt;
    enable_extension &lt;span &gt;:pg_trgm&lt;/span&gt;

    create_table &lt;span &gt;:cities&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;t&lt;span &gt;|&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;string &lt;span &gt;:name&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;
      t&lt;span &gt;.&lt;/span&gt;timestamps
      t&lt;span &gt;.&lt;/span&gt;index &lt;span &gt;:name&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; opclass&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:gin_trgm_ops&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; using&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:gin&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now that we have the &lt;code &gt;pg_trgm&lt;/code&gt; extension enabled, and have correctly indexed the table, we can use the similarity operator &lt;code &gt;%&lt;/code&gt; inside of our where clauses, such as in the scope below:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;City&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  scope &lt;span &gt;:name_similar&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;name&lt;span &gt;)&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;name % :name&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; name&lt;span &gt;:&lt;/span&gt; name&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;

&lt;span &gt;City&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;name_similar&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;Torono&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;count
&lt;span &gt;# SELECT COUNT(*) FROM &quot;cities&quot; WHERE (name % &apos;Torono&apos;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;showing-the-closest-matches-for-a-term-based-on-its-similarity&quot; &gt;&lt;a href=&quot;#showing-the-closest-matches-for-a-term-based-on-its-similarity&quot; aria-label=&quot;showing the closest matches for a term based on its similarity permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Showing the closest matches for a term based on its similarity&lt;/h2&gt;
&lt;p&gt;We may not want to only limit by similarity using the &lt;code &gt;%&lt;/code&gt; operator, but also &lt;strong&gt;order the results from most similar to least similar&lt;/strong&gt;. Take the example query and its result below:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;select&lt;/span&gt; name&lt;span &gt;,&lt;/span&gt; similarity&lt;span &gt;(&lt;/span&gt;name&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;Dease Lake&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;from&lt;/span&gt; cities
&lt;span &gt;where&lt;/span&gt; name &lt;span &gt;%&lt;/span&gt; &lt;span &gt;&apos;Dease Lake&apos;&lt;/span&gt;
&lt;span &gt;order&lt;/span&gt; &lt;span &gt;by&lt;/span&gt; &lt;span &gt;2&lt;/span&gt; &lt;span &gt;desc&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This query finds cities which have a name similar to &lt;code &gt;Dease Lake&lt;/code&gt;, but you can see that we actually get seven results back, though we can clearly see that there was an exact match. Ideally then, we wouldn&apos;t just limit our query by similarity, but put it in the correct order as well.&lt;/p&gt;
&lt;div  data-language=&quot;text&quot;&gt;&lt;pre &gt;&lt;code &gt;Dease Lake  1
Deer Lake   0.5
Lake Louise 0.375
Lynn Lake   0.33333334
Red Lake    0.33333334
Cat Lake    0.33333334
Baker Lake  0.3125&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can do this by updating our scope to order by similarity. We have to be careful about this, because in order to use the similarity function, we need to pass in the user input of &lt;code &gt;&apos;Dease Lake&apos;&lt;/code&gt;. To avoid SQL injection attacks and to ensure safe string quoting, we&apos;ll use the &lt;code &gt;quote_string&lt;/code&gt; method from ActiveRecord::Base.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;City&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;ApplicationRecord&lt;/span&gt;
  scope &lt;span &gt;:name_similar&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;name&lt;span &gt;)&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
    quoted_name &lt;span &gt;=&lt;/span&gt; &lt;span &gt;ActiveRecord&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Base&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;connection&lt;span &gt;.&lt;/span&gt;quote_string&lt;span &gt;(&lt;/span&gt;name&lt;span &gt;)&lt;/span&gt;
    where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;name % :name&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; name&lt;span &gt;:&lt;/span&gt; name&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      order&lt;span &gt;(&lt;/span&gt;&lt;span &gt;Arel&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;sql&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;similarity(name, &apos;&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;quoted_name&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;&apos;) DESC&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now when we use the &lt;code &gt;name_similar&lt;/code&gt; scope, the result will be ordered with the most similar city first, allowing us to find &lt;code &gt;Dease Lake&lt;/code&gt;:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;City&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;name_similar&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;Dease Lake&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;first&lt;span &gt;.&lt;/span&gt;name
&lt;span &gt;# =&gt; Dease Lake&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And the SQL produced looks like:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;&quot;cities&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;cities&quot;&lt;/span&gt;
&lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;name &lt;span &gt;%&lt;/span&gt; &lt;span &gt;&apos;Dease Lake&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;ORDER&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; similarity&lt;span &gt;(&lt;/span&gt;name&lt;span &gt;,&lt;/span&gt; &lt;span &gt;&apos;Dease Lake&apos;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;DESC&lt;/span&gt;
&lt;span &gt;LIMIT&lt;/span&gt; $&lt;span &gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;conclusion&quot; &gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this article, we took a dive into the &lt;code &gt;pg_trgm&lt;/code&gt; extension, seeing first what trigrams actually are, and then how we can practically use similarity functions and operators in our Rails apps. &lt;strong&gt;This allows us to improve keyword searching&lt;/strong&gt;, by finding similar, rather than exact matches. We also managed to accomplish all of this &lt;strong&gt;without adding an additional backend service&lt;/strong&gt;, or too much additional complexity to our application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share this article:&lt;/strong&gt; If you liked this article we&apos;d appreciate it if you&apos;d &lt;a href=&quot;https://ctt.ac/LZ466&quot;&gt;tweet it to your peers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/advanced-database-programming-rails-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        title=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        src=&quot;https://pganalyze.com/static/24260e03f3c098e161f84b87ce28122b/acb04/ebook_promo_advanced_database_programming_rails_postgres.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;about-the-author&quot; &gt;&lt;a href=&quot;#about-the-author&quot; aria-label=&quot;about the author permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About the Author&lt;/h2&gt;
&lt;p&gt;Leigh Halliday is a guest author for the pganalyze blog. He is a developer based out of Canada who works at &lt;a href=&quot;https://www.flipgive.com&quot;&gt;FlipGive&lt;/a&gt; as a full-stack developer. He writes about Ruby and React on &lt;a href=&quot;https://www.leighhalliday.com&quot;&gt;his blog&lt;/a&gt; and publishes React tutorials on &lt;a href=&quot;https://youtube.com/leighhalliday&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt; ]]&gt;</content:encoded></item><item><title><![CDATA[Efficient GraphQL queries in Ruby on Rails & Postgres]]></title><description><![CDATA[GraphQL puts the user in control of their own destiny. Yes, they are confined to your schema, but beyond that they can access the data in any which way. Will they ask only for the "events", or also for the "category" of each event? We don't really know! In REST based APIs we know ahead of time what will be rendered, and can plan ahead by generating the required data efficiently, often by eager-loading the data we know we'll need. In this article, we will discuss what N+1 queries are, how they…]]></description><link>https://pganalyze.com/blog/efficient-graphql-queries-in-ruby-on-rails-and-postgres</link><guid isPermaLink="false">https://pganalyze.com/blog/efficient-graphql-queries-in-ruby-on-rails-and-postgres</guid><dc:creator><![CDATA[Leigh Halliday]]></dc:creator><pubDate>Tue, 24 Sep 2019 12:00:00 GMT</pubDate><content:encoded>&lt;![CDATA[ &lt;p&gt;GraphQL puts the user in control of their own destiny. Yes, they are confined to your schema, but beyond that they can access the data in any which way. Will they ask only for the &quot;events&quot;, or also for the &quot;category&quot; of each event? We don&apos;t really know! In REST based APIs we know ahead of time what will be rendered, and can plan ahead by generating the required data efficiently, often by &lt;a href=&quot;https://apidock.com/rails/ActiveRecord/QueryMethods/includes&quot;&gt;eager-loading&lt;/a&gt; the data we know we&apos;ll need.&lt;/p&gt;
&lt;p&gt;In this article, we will discuss what &lt;strong&gt;N+1 queries&lt;/strong&gt; are, how they are easily produced in &lt;a href=&quot;https://graphql.org/learn/&quot;&gt;GraphQL&lt;/a&gt;, and how to solve them using the &lt;a href=&quot;https://github.com/Shopify/graphql-batch&quot;&gt;graphql-batch&lt;/a&gt; gem along with a few custom batch loaders.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/pganalyze/graphql-batch-example&quot;&gt;source code for this article&lt;/a&gt; is available on GitHub.&lt;/p&gt;
&lt;div &gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-are-n1-queries&quot;&gt;What are N+1 queries?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#n1-queries-in-graphql&quot;&gt;N+1 queries in GraphQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#optimizing-graphql-queries&quot;&gt;Optimizing GraphQL queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#batch-loading-single-records&quot;&gt;Batch loading single records&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#batch-loading-many-records&quot;&gt;Batch loading many records&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#batch-loading-many-records-more-efficiently&quot;&gt;Batch loading many records more efficiently&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#batch-loading-active-storage-attachments&quot;&gt;Batch loading active storage attachments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#about-the-author&quot;&gt;About the Author&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-are-n1-queries&quot; &gt;&lt;a href=&quot;#what-are-n1-queries&quot; aria-label=&quot;what are n1 queries permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What are N+1 queries?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sitepoint.com/silver-bullet-n1-problem/&quot;&gt;N+1 queries&lt;/a&gt; can occur when you have one-to-many relationships in your models. Each &lt;code &gt;Event&lt;/code&gt; belongs to a &lt;code &gt;Category&lt;/code&gt;. Let&apos;s say that you find the last five events and you want to get the &lt;strong&gt;category name&lt;/strong&gt; for each of them.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Event&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;last&lt;span &gt;(&lt;/span&gt;&lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;event&lt;span &gt;|&lt;/span&gt; puts event&lt;span &gt;.&lt;/span&gt;category&lt;span &gt;.&lt;/span&gt;name &lt;span &gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Seems simple enough! We unfortunately just produced six queries. The first query to find the events, and another query to find each category&apos;s name. This is an easy problem to solve in Rails by using &lt;strong&gt;eager-loading&lt;/strong&gt;:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;Event&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;includes&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:category&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;last&lt;span &gt;(&lt;/span&gt;&lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;event&lt;span &gt;|&lt;/span&gt; puts event&lt;span &gt;.&lt;/span&gt;category&lt;span &gt;.&lt;/span&gt;name &lt;span &gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By using the &lt;code &gt;includes&lt;/code&gt; method we&apos;ve been able to knock our queries down from six to two: The first to find the events, and the second to find the categories for those events.&lt;/p&gt;
&lt;h2 id=&quot;n1-queries-in-graphql&quot; &gt;&lt;a href=&quot;#n1-queries-in-graphql&quot; aria-label=&quot;n1 queries in graphql permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;N+1 queries in GraphQL&lt;/h2&gt;
&lt;p&gt;As we mentioned earlier, in GraphQL, the user is in charge of their own destiny. They may or may not ask for the category name for each event. The query below will produce N+1 SQL queries as it finds the category for each event:&lt;/p&gt;
&lt;div  data-language=&quot;graphql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;{&lt;/span&gt;
  &lt;span &gt;events&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
    &lt;span &gt;id&lt;/span&gt;
    &lt;span &gt;name&lt;/span&gt;
    &lt;span &gt;category&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
      &lt;span &gt;id&lt;/span&gt;
      &lt;span &gt;name&lt;/span&gt;
    &lt;span &gt;}&lt;/span&gt;
  &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;optimizing-graphql-queries&quot; &gt;&lt;a href=&quot;#optimizing-graphql-queries&quot; aria-label=&quot;optimizing graphql queries permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Optimizing GraphQL queries&lt;/h2&gt;
&lt;p&gt;Yes, we could solve the N+1 query in the previous example by eager-loading the &lt;code &gt;category&lt;/code&gt; relationship, but if the user didn&apos;t actually want the category, why load it? We don&apos;t know what the user will ask for. There just so happens to be a better way, by &lt;strong&gt;lazy-loading&lt;/strong&gt; data only as its needed using the graphql-batch gem.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://twitter.com/__xuorig__&quot;&gt;Marc-André Giroux&lt;/a&gt; has written a very thorough article about the &lt;a href=&quot;https://medium.com/@__xuorig__/the-graphql-dataloader-pattern-visualized-3064a00f319f&quot;&gt;GraphQL Dataloader Pattern&lt;/a&gt; which I highly recommend reading before continuing.&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/advanced-database-programming-rails-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        title=&quot;Download Free eBook: Advanced Database Programming with Rails and Postgres&quot;
        src=&quot;https://pganalyze.com/static/24260e03f3c098e161f84b87ce28122b/acb04/ebook_promo_advanced_database_programming_rails_postgres.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;batch-loading-single-records&quot; &gt;&lt;a href=&quot;#batch-loading-single-records&quot; aria-label=&quot;batch loading single records permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Batch loading single records&lt;/h2&gt;
&lt;p&gt;The simplest case for batch loading data is the example of each event belonging to a category. Inside of our &lt;code &gt;EventType&lt;/code&gt; class, there is a field called &lt;code &gt;category&lt;/code&gt; which allows the user to access the category of an event.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;EventType&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;BaseObject&lt;/span&gt;
  field &lt;span &gt;:category&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;CategoryType&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;category&lt;/span&gt;&lt;/span&gt;
    &lt;span &gt;# avoid `object.category`&lt;/span&gt;
    &lt;span &gt;RecordLoader&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;for&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;Category&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;load&lt;span &gt;(&lt;/span&gt;object&lt;span &gt;.&lt;/span&gt;category_id&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By using the &lt;code &gt;RecordLoader&lt;/code&gt; class to load the category, we actually avoid loading the category right away, and instead load all of the required categories with a single query. The query it ends up producing may end up looking like:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;&quot;categories&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt; &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;categories&quot;&lt;/span&gt; &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;&quot;categories&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt; &lt;span &gt;IN&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;$&lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;2&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;3&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Looking at the &lt;code &gt;RecordLoader&lt;/code&gt; class we can see how it works. The &lt;code &gt;perform&lt;/code&gt; method will receive all of the ids for a single model (Category in this case), load the records in a single SQL query, and then call the fulfill method for each of them. The &lt;code &gt;fulfill&lt;/code&gt; method resolves the promise, which is basically like putting a face to a name... you gave me an ID, and I&apos;ve fulfilled my promise to provide you with the corresponding record.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;RecordLoader&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;GraphQL&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Batch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Loader&lt;/span&gt;
  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;initialize&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;model&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;@model&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; model
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;perform&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;ids&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;# Find all ids for this model and fulfill their promises&lt;/span&gt;
    &lt;span &gt;@model&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;id&lt;span &gt;:&lt;/span&gt; ids&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;record&lt;span &gt;|&lt;/span&gt; fulfill&lt;span &gt;(&lt;/span&gt;record&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;,&lt;/span&gt; record&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
    &lt;span &gt;# Handle cases where a record was not found and fulfill the value as nil&lt;/span&gt;
    ids&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;id&lt;span &gt;|&lt;/span&gt; fulfill&lt;span &gt;(&lt;/span&gt;id&lt;span &gt;,&lt;/span&gt; &lt;span &gt;nil&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;unless&lt;/span&gt; fulfilled&lt;span &gt;?&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can write a test for this class to ensure that it finds the records correctly, keeping in mind that in order for the lazy/promise based code to function correctly it needs to be wrapped inside something called an &lt;code &gt;executor&lt;/code&gt;.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;describe &lt;span &gt;RecordLoader&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
  it &lt;span &gt;&apos;loads&apos;&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
    event &lt;span &gt;=&lt;/span&gt; create&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:event&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    result &lt;span &gt;=&lt;/span&gt; &lt;span &gt;GraphQL&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Batch&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;batch &lt;span &gt;do&lt;/span&gt;
      &lt;span &gt;RecordLoader&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;for&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;Event&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;load&lt;span &gt;(&lt;/span&gt;event&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;
    expect&lt;span &gt;(&lt;/span&gt;result&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to eq&lt;span &gt;(&lt;/span&gt;event&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;batch-loading-many-records&quot; &gt;&lt;a href=&quot;#batch-loading-many-records&quot; aria-label=&quot;batch loading many records permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Batch loading many records&lt;/h2&gt;
&lt;p&gt;We&apos;ve covered the case where we are batch loading &lt;strong&gt;a single record at a time&lt;/strong&gt;, but how do we handle the reverse scenario? We are displaying categories along with the first five events for each category, which would also produce an N+1 query, so let&apos;s see how we can solve it using a batch loader. The query we&apos;re discussing would look something like this:&lt;/p&gt;
&lt;div  data-language=&quot;graphql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;{&lt;/span&gt;
  &lt;span &gt;categories&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
    &lt;span &gt;id&lt;/span&gt;
    &lt;span &gt;name&lt;/span&gt;
    &lt;span &gt;events&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;first&lt;/span&gt;&lt;span &gt;:&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;{&lt;/span&gt;
      &lt;span &gt;id&lt;/span&gt;
      &lt;span &gt;name&lt;/span&gt;
    &lt;span &gt;}&lt;/span&gt;
  &lt;span &gt;}&lt;/span&gt;
&lt;span &gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I have created a custom loader called &lt;code &gt;ForeignKeyLoader&lt;/code&gt; for this purpose. It will load the &lt;code &gt;events&lt;/code&gt; using the foreign key &lt;code &gt;category_id&lt;/code&gt;. I also added the ability to pass a lambda to merge in additional scopes into the query that will be run.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;CategoryType&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;BaseObject&lt;/span&gt;
  field &lt;span &gt;:events&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;[&lt;/span&gt;&lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;EventType&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
    argument &lt;span &gt;:first&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;Int&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; required&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; default_value&lt;span &gt;:&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;events&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;first&lt;span &gt;:&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;ForeignKeyLoader&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;for&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;Event&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:category_id&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; merge&lt;span &gt;:&lt;/span&gt; &lt;span &gt;-&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; order&lt;span &gt;(&lt;/span&gt;id&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:asc&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      load&lt;span &gt;(&lt;/span&gt;object&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;then&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;records&lt;span &gt;|&lt;/span&gt;
        records&lt;span &gt;.&lt;/span&gt;first&lt;span &gt;(&lt;/span&gt;first&lt;span &gt;)&lt;/span&gt;
      &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The query that gets produced looks something like:&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;
&lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;category_id&quot;&lt;/span&gt; &lt;span &gt;IN&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;$&lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;2&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;3&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; $&lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;ORDER&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;id&quot;&lt;/span&gt; &lt;span &gt;ASC&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice in this case that we call the &lt;code &gt;then&lt;/code&gt; method to execute some code after the promise has been resolved. Here we see the first issue with this method... we only wanted five events for each category, but our query will load &lt;strong&gt;ALL&lt;/strong&gt; events for each category, and then, using the &lt;code &gt;first&lt;/code&gt; method on the resulting Array, narrow it down to only the first five events. If there are thousands of events, we could run into some serious issues.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;ForeignKeyLoader&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;GraphQL&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Batch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Loader&lt;/span&gt;
  attr_reader &lt;span &gt;:model&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:foreign_key&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:merge&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;loader_key_for&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;group_args&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;# avoiding including the `merge` lambda in loader key&lt;/span&gt;
    &lt;span &gt;# each lambda is unique which defeats the purpose of&lt;/span&gt;
    &lt;span &gt;# grouping queries together&lt;/span&gt;
    &lt;span &gt;[&lt;/span&gt;&lt;span &gt;self&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;concat&lt;span &gt;(&lt;/span&gt;group_args&lt;span &gt;.&lt;/span&gt;slice&lt;span &gt;(&lt;/span&gt;&lt;span &gt;0&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;&lt;span &gt;2&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;initialize&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;model&lt;span &gt;,&lt;/span&gt; foreign_key&lt;span &gt;,&lt;/span&gt; merge&lt;span &gt;:&lt;/span&gt; &lt;span &gt;nil&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;@model&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; model
    &lt;span &gt;@foreign_key&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; foreign_key
    &lt;span &gt;@merge&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; merge
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;perform&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;foreign_ids&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;# find all the records&lt;/span&gt;
    scope &lt;span &gt;=&lt;/span&gt; model&lt;span &gt;.&lt;/span&gt;where&lt;span &gt;(&lt;/span&gt;foreign_key &lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; foreign_ids&lt;span &gt;)&lt;/span&gt;
    scope &lt;span &gt;=&lt;/span&gt; scope&lt;span &gt;.&lt;/span&gt;merge&lt;span &gt;(&lt;/span&gt;merge&lt;span &gt;)&lt;/span&gt; &lt;span &gt;if&lt;/span&gt; merge&lt;span &gt;.&lt;/span&gt;present&lt;span &gt;?&lt;/span&gt;
    records &lt;span &gt;=&lt;/span&gt; scope&lt;span &gt;.&lt;/span&gt;to_a

    foreign_ids&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;foreign_id&lt;span &gt;|&lt;/span&gt;
      &lt;span &gt;# find the records required to fulfill each promise&lt;/span&gt;
      matching_records &lt;span &gt;=&lt;/span&gt; records&lt;span &gt;.&lt;/span&gt;select &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;r&lt;span &gt;|&lt;/span&gt;
        foreign_id &lt;span &gt;==&lt;/span&gt; r&lt;span &gt;.&lt;/span&gt;send&lt;span &gt;(&lt;/span&gt;foreign_key&lt;span &gt;)&lt;/span&gt;
      &lt;span &gt;end&lt;/span&gt;
      fulfill&lt;span &gt;(&lt;/span&gt;foreign_id&lt;span &gt;,&lt;/span&gt; matching_records&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;batch-loading-many-records-more-efficiently&quot; &gt;&lt;a href=&quot;#batch-loading-many-records-more-efficiently&quot; aria-label=&quot;batch loading many records more efficiently permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Batch loading many records more efficiently&lt;/h2&gt;
&lt;p&gt;It turns out that there &lt;em&gt;is&lt;/em&gt; a way to perform a query that says &quot;find me the first N records for each X&quot; (find me the first 5 records for each category), and that involves using &lt;a href=&quot;https://www.postgresql.org/docs/9.3/functions-window.html&quot;&gt;Postgres Window Functions&lt;/a&gt;. While researching this concept, this article about &lt;a href=&quot;https://spin.atomicobject.com/2016/03/12/select-top-n-per-group-postgresql/&quot;&gt;window functions&lt;/a&gt; was useful along with this article about bringing &lt;a href=&quot;https://blog.codeship.com/folding-postgres-window-functions-into-rails/&quot;&gt;window functions into Rails&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The following query produces the data that we want... we just need to figure out how to write a batch loader that generates the same result.&lt;/p&gt;
&lt;div  data-language=&quot;sql&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;SELECT&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;*&lt;/span&gt;
&lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
  &lt;span &gt;SELECT&lt;/span&gt;
    &lt;span &gt;*&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
    row_number&lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;OVER&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
      &lt;span &gt;PARTITION&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; category_id &lt;span &gt;ORDER&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; start_time &lt;span &gt;desc&lt;/span&gt;
    &lt;span &gt;)&lt;/span&gt; &lt;span &gt;as&lt;/span&gt; rank
  &lt;span &gt;FROM&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;
  &lt;span &gt;WHERE&lt;/span&gt; &lt;span &gt;&quot;events&quot;&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;&quot;category_id&quot;&lt;/span&gt; &lt;span &gt;IN&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;&lt;span &gt;1&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;3&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;4&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
&lt;span &gt;)&lt;/span&gt; &lt;span &gt;as&lt;/span&gt; events
&lt;span &gt;WHERE&lt;/span&gt; rank &lt;span &gt;&amp;lt;=&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For this we&apos;ll create a batch loader called &lt;code &gt;WindowKeyLoader&lt;/code&gt; which is used like:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;CategoryType&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;BaseObject&lt;/span&gt;
  field &lt;span &gt;:events&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;[&lt;/span&gt;&lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;EventType&lt;/span&gt;&lt;span &gt;]&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
    argument &lt;span &gt;:first&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;Int&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; required&lt;span &gt;:&lt;/span&gt; &lt;span &gt;false&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; default_value&lt;span &gt;:&lt;/span&gt; &lt;span &gt;5&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;events&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;first&lt;span &gt;:&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;WindowKeyLoader&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;for&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;Event&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:category_id&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
      limit&lt;span &gt;:&lt;/span&gt; first&lt;span &gt;,&lt;/span&gt;
      order_col&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:start_time&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
      order_dir&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:desc&lt;/span&gt;
    &lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;load&lt;span &gt;(&lt;/span&gt;object&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can see the difference already. I am no longer required to slice the first N array elements in the &lt;code &gt;then&lt;/code&gt; block of the resolved promise. The actual batch loader class looks like:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;WindowKeyLoader&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;GraphQL&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Batch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Loader&lt;/span&gt;
  attr_reader &lt;span &gt;:model&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:foreign_key&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:limit&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:order_col&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:order_dir&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;initialize&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;model&lt;span &gt;,&lt;/span&gt; foreign_key&lt;span &gt;,&lt;/span&gt; limit&lt;span &gt;:&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; order_col&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:id&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; order_dir&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:asc&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;@model&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; model
    &lt;span &gt;@foreign_key&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; foreign_key
    &lt;span &gt;@limit&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; limit
    &lt;span &gt;@order_col&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; order_col
    &lt;span &gt;@order_dir&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; order_dir
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;perform&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;foreign_ids&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;# build the sub-query, limiting results by foreign key at this point&lt;/span&gt;
    &lt;span &gt;# we don&apos;t want to execute this query but get its SQL to be used later&lt;/span&gt;
    ranked_from &lt;span &gt;=&lt;/span&gt; model&lt;span &gt;.&lt;/span&gt;
      select&lt;span &gt;(&lt;/span&gt;&quot;&lt;span &gt;*&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
        row_number&lt;span &gt;(&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;OVER&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;
          &lt;span &gt;PARTITION&lt;/span&gt; &lt;span &gt;BY&lt;/span&gt; &lt;span &gt;#{foreign_key} ORDER BY #{order_col} #{order_dir}&lt;/span&gt;
        &lt;span &gt;)&lt;/span&gt; as rank&quot;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      where&lt;span &gt;(&lt;/span&gt;foreign_key &lt;span &gt;=&lt;/span&gt;&lt;span &gt;&gt;&lt;/span&gt; foreign_ids&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      to_sql

    &lt;span &gt;# use the sub-query from above to query records which have a rank&lt;/span&gt;
    &lt;span &gt;# value less than or equal to our limit&lt;/span&gt;
    records &lt;span &gt;=&lt;/span&gt; model&lt;span &gt;.&lt;/span&gt;
      from&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;(&lt;span &gt;&lt;span &gt;#{&lt;/span&gt;ranked_from&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;) as &lt;span &gt;&lt;span &gt;#{&lt;/span&gt;model&lt;span &gt;.&lt;/span&gt;table_name&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      where&lt;span &gt;(&lt;/span&gt;&lt;span &gt;&quot;rank &amp;lt;= &lt;span &gt;&lt;span &gt;#{&lt;/span&gt;limit&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      to_a

    &lt;span &gt;# match records and fulfill promises&lt;/span&gt;
    foreign_ids&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;foreign_id&lt;span &gt;|&lt;/span&gt;
      matching_records &lt;span &gt;=&lt;/span&gt; records&lt;span &gt;.&lt;/span&gt;select &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;r&lt;span &gt;|&lt;/span&gt;
        foreign_id &lt;span &gt;==&lt;/span&gt; r&lt;span &gt;.&lt;/span&gt;send&lt;span &gt;(&lt;/span&gt;foreign_key&lt;span &gt;)&lt;/span&gt;
      &lt;span &gt;end&lt;/span&gt;
      fulfill&lt;span &gt;(&lt;/span&gt;foreign_id&lt;span &gt;,&lt;/span&gt; matching_records&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We&apos;re able to test the &lt;code &gt;WindowKeyLoader&lt;/code&gt; by creating three events for a category but only asking for the first two of them:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;describe &lt;span &gt;WindowKeyLoader&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
  it &lt;span &gt;&apos;loads&apos;&lt;/span&gt; &lt;span &gt;do&lt;/span&gt;
    category &lt;span &gt;=&lt;/span&gt; create&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:category&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    events &lt;span &gt;=&lt;/span&gt; &lt;span &gt;(&lt;/span&gt;&lt;span &gt;1.&lt;/span&gt;&lt;span &gt;.3&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to_a&lt;span &gt;.&lt;/span&gt;map &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;n&lt;span &gt;|&lt;/span&gt;
      create&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:event&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; name&lt;span &gt;:&lt;/span&gt; &lt;span &gt;&quot;Event &lt;span &gt;&lt;span &gt;#{&lt;/span&gt;n&lt;span &gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; category&lt;span &gt;:&lt;/span&gt; category&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;

    result &lt;span &gt;=&lt;/span&gt; &lt;span &gt;GraphQL&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Batch&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;batch &lt;span &gt;do&lt;/span&gt;
      &lt;span &gt;WindowKeyLoader&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;for&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;
        &lt;span &gt;Event&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
        &lt;span &gt;:category_id&lt;/span&gt;&lt;span &gt;,&lt;/span&gt;
        limit&lt;span &gt;:&lt;/span&gt; &lt;span &gt;2&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; order_col&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:id&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; order_dir&lt;span &gt;:&lt;/span&gt; &lt;span &gt;:asc&lt;/span&gt;
      &lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;load&lt;span &gt;(&lt;/span&gt;category&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;

    expect&lt;span &gt;(&lt;/span&gt;result&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;to eq&lt;span &gt;(&lt;/span&gt;events&lt;span &gt;.&lt;/span&gt;first&lt;span &gt;(&lt;/span&gt;&lt;span &gt;2&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;batch-loading-active-storage-attachments&quot; &gt;&lt;a href=&quot;#batch-loading-active-storage-attachments&quot; aria-label=&quot;batch loading active storage attachments permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Batch loading active storage attachments&lt;/h2&gt;
&lt;p&gt;You may run into situations where you&apos;re loading polymorphic data, or other types of relationships which don&apos;t exactly fit into the mold of your standard has-many or belongs-to relationships. One case is with &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html&quot;&gt;ActiveStorage&lt;/a&gt;. In the code below we&apos;ll load an image URL for an event:&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;EventType&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;Types&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;BaseObject&lt;/span&gt;
  field &lt;span &gt;:image&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;String&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; null&lt;span &gt;:&lt;/span&gt; &lt;span &gt;true&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;image&lt;/span&gt;&lt;/span&gt;
    &lt;span &gt;# produces 2N + 1 queries... yikes!&lt;/span&gt;
    &lt;span &gt;# url_for(object.image.variant({ quality: 75 }))&lt;/span&gt;

    &lt;span &gt;AttachmentLoader&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;for&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:Event&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:image&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;load&lt;span &gt;(&lt;/span&gt;object&lt;span &gt;.&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;&lt;span &gt;then&lt;/span&gt; &lt;span &gt;do&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;image&lt;span &gt;|&lt;/span&gt;
      url_for&lt;span &gt;(&lt;/span&gt;image&lt;span &gt;.&lt;/span&gt;variant&lt;span &gt;(&lt;/span&gt;&lt;span &gt;{&lt;/span&gt; quality&lt;span &gt;:&lt;/span&gt; &lt;span &gt;75&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;end&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This data is stored using a polymorphic relationship that loads an &lt;code &gt;ActiveStorage::Attachment&lt;/code&gt; record, which then needs to load an &lt;code &gt;ActiveStorage::Blob&lt;/code&gt; record in order to produce the image url. It ends up producing a 2N + 1 query... yikes! Our &lt;code &gt;AttachmentLoader&lt;/code&gt; is able to completely optimize this field by cutting it down to just two queries to load as many images as you&apos;d like.&lt;/p&gt;
&lt;div  data-language=&quot;ruby&quot;&gt;&lt;pre &gt;&lt;code &gt;&lt;span &gt;class&lt;/span&gt; &lt;span &gt;AttachmentLoader&lt;/span&gt; &lt;span &gt;&amp;lt;&lt;/span&gt; &lt;span &gt;GraphQL&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Batch&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Loader&lt;/span&gt;
  attr_reader &lt;span &gt;:record_type&lt;/span&gt;&lt;span &gt;,&lt;/span&gt; &lt;span &gt;:attachment_name&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;initialize&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;record_type&lt;span &gt;,&lt;/span&gt; attachment_name&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;@record_type&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; record_type
    &lt;span &gt;@attachment_name&lt;/span&gt; &lt;span &gt;=&lt;/span&gt; attachment_name
  &lt;span &gt;end&lt;/span&gt;

  &lt;span &gt;def&lt;/span&gt; &lt;span &gt;&lt;span &gt;perform&lt;/span&gt;&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;record_ids&lt;span &gt;)&lt;/span&gt;
    &lt;span &gt;# find records and fulfill promises&lt;/span&gt;
    &lt;span &gt;ActiveStorage&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;:&lt;/span&gt;&lt;span &gt;Attachment&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      includes&lt;span &gt;(&lt;/span&gt;&lt;span &gt;:blob&lt;/span&gt;&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      where&lt;span &gt;(&lt;/span&gt;record_type&lt;span &gt;:&lt;/span&gt; record_type&lt;span &gt;,&lt;/span&gt; record_id&lt;span &gt;:&lt;/span&gt; record_ids&lt;span &gt;,&lt;/span&gt; name&lt;span &gt;:&lt;/span&gt; attachment_name&lt;span &gt;)&lt;/span&gt;&lt;span &gt;.&lt;/span&gt;
      &lt;span &gt;each&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;record&lt;span &gt;|&lt;/span&gt; fulfill&lt;span &gt;(&lt;/span&gt;record&lt;span &gt;.&lt;/span&gt;record_id&lt;span &gt;,&lt;/span&gt; record&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;

    &lt;span &gt;# fulfill unfound records&lt;/span&gt;
    record_ids&lt;span &gt;.&lt;/span&gt;&lt;span &gt;each&lt;/span&gt; &lt;span &gt;{&lt;/span&gt; &lt;span &gt;|&lt;/span&gt;id&lt;span &gt;|&lt;/span&gt; fulfill&lt;span &gt;(&lt;/span&gt;id&lt;span &gt;,&lt;/span&gt; &lt;span &gt;nil&lt;/span&gt;&lt;span &gt;)&lt;/span&gt; &lt;span &gt;unless&lt;/span&gt; fulfilled&lt;span &gt;?&lt;/span&gt;&lt;span &gt;(&lt;/span&gt;id&lt;span &gt;)&lt;/span&gt; &lt;span &gt;}&lt;/span&gt;
  &lt;span &gt;end&lt;/span&gt;
&lt;span &gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this case we &lt;em&gt;are&lt;/em&gt; taking advantage of eager-loading, because for each attachment we will need its corresponding blob record.&lt;/p&gt;
&lt;p&gt;&lt;a src=&quot;https://pganalyze.com/ebooks/efficient-search-in-rails-with-postgres&quot;&gt;&lt;span
      
      
    &gt;
      &lt;span
    
    
  &gt;&lt;/span&gt;
  &lt;img
        
        alt=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        title=&quot;Download Free eBook: Efficient Search in Rails with Postgres&quot;
        src=&quot;https://pganalyze.com/static/3e8bb134d6b5689ee9d20a10e6699b6c/acb04/ebook_promo_rails_search.jpg&quot;
        
        
        
        loading=&quot;lazy&quot;
        decoding=&quot;async&quot;
      /&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; &gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;GraphQL &lt;em&gt;can&lt;/em&gt; be as efficient as REST, but requires approaching optimizations from a different angle. Instead of upfront optimizations, we lazy-load data only when required, loading it in batches to avoid excess trips to the database. In this article, we covered techniques to load single records, multiple records, and records with different types of relationships, as is the case with Active Storage which has a polymorphic relationship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share this article:&lt;/strong&gt; If you liked this article we&apos;d appreciate it if you&apos;d &lt;a href=&quot;https://ctt.ac/Esi54&quot;&gt;tweet it to your peers&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;about-the-author&quot; &gt;&lt;a href=&quot;#about-the-author&quot; aria-label=&quot;about the author permalink&quot; &gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About the Author&lt;/h2&gt;
&lt;p&gt;Leigh Halliday is a guest author for the pganalyze blog. He is a developer based out of Canada who works at &lt;a href=&quot;https://www.flipgive.com&quot;&gt;FlipGive&lt;/a&gt; as a full-stack developer. He writes about Ruby and React on &lt;a href=&quot;https://www.leighhalliday.com&quot;&gt;his blog&lt;/a&gt; and publishes React tutorials on &lt;a href=&quot;https://youtube.com/leighhalliday&quot;&gt;YouTube&lt;/a&gt;.&lt;/p&gt; ]]&gt;</content:encoded></item></channel></rss>