<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Thomas Dournet</title>
    <description>The latest articles on Forem by Thomas Dournet (@tdournet).</description>
    <link>https://forem.com/tdournet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3426562%2Ffaffc80d-36e6-4efe-86eb-f530a088be6c.png</url>
      <title>Forem: Thomas Dournet</title>
      <link>https://forem.com/tdournet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tdournet"/>
    <language>en</language>
    <item>
      <title>How to Optimise PostgreSQL LIKE and ILIKE Queries</title>
      <dc:creator>Thomas Dournet</dc:creator>
      <pubDate>Mon, 11 Aug 2025 08:46:51 +0000</pubDate>
      <link>https://forem.com/tdournet/how-to-optimise-postgresql-like-and-ilike-queries-494i</link>
      <guid>https://forem.com/tdournet/how-to-optimise-postgresql-like-and-ilike-queries-494i</guid>
      <description>&lt;p&gt;If you've ever watched your PostgreSQL queries crawl through millions of rows while your users impatiently tap their fingers, you know the pain of unoptimized LIKE and ILIKE searches. These seemingly simple pattern-matching queries can bring even well-designed databases to their knees, turning what should be instant searches into multi-second ordeals.&lt;/p&gt;

&lt;p&gt;The good news? With the right optimization techniques, you can transform those sluggish searches into lightning-fast operations. &lt;/p&gt;

&lt;p&gt;Let's dive into how you can achieve these dramatic performance improvements in your own PostgreSQL database.&lt;/p&gt;

&lt;h2&gt;
  
  
  1 – LIKE vs ILIKE: syntax and case-sensitivity
&lt;/h2&gt;

&lt;p&gt;Understanding the fundamental difference between LIKE and ILIKE is crucial for optimization. LIKE performs case-sensitive pattern matching, while ILIKE ignores case differences entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- LIKE: Case-sensitive matching&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'John%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Matches: 'John', 'Johnny', 'Johnson'&lt;/span&gt;
&lt;span class="c1"&gt;-- Doesn't match: 'john', 'JOHN'&lt;/span&gt;

&lt;span class="c1"&gt;-- ILIKE: Case-insensitive matching&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'John%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Matches: 'John', 'john', 'JOHN', 'Johnny', 'JOHNNY'&lt;/span&gt;

&lt;span class="c1"&gt;-- Using wildcards&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%phone%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- Contains 'phone'&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'smart%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- Starts with 'smart' (any case)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%_pro'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- Ends with single char + 'pro'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While ILIKE seems more user-friendly, it comes with a significant performance cost that we'll explore next.&lt;/p&gt;

&lt;h2&gt;
  
  
  2 – Why LIKE/ILIKE hurt performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Full table scan explained
&lt;/h3&gt;

&lt;p&gt;When PostgreSQL encounters a LIKE or ILIKE query, especially with a leading wildcard, if left unoptimized, it often resorts to a sequential scan—reading every single row in your table. Here's what this looks like :&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquerysharp.com%2Fblog-diagrams%2Fsequential-scan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquerysharp.com%2Fblog-diagrams%2Fsequential-scan.png" title="Sequential Scan" alt="Sequential Scan" width="800" height="673"&gt;&lt;/a&gt;&lt;br&gt;
This is literally the worst case scenario for a query. PostgreSQL will read every single row in the table, which takes a lot of time.&lt;/p&gt;
&lt;h3&gt;
  
  
  2.2 Why a "basic" B-tree index often can't be used
&lt;/h3&gt;

&lt;p&gt;B-tree indexes, PostgreSQL's default index type, excel at finding exact matches and range queries. However, they're built on sorted order, which makes them ineffective for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patterns with leading wildcards (&lt;code&gt;%term&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Case-insensitive searches without proper function indexes&lt;/li&gt;
&lt;li&gt;Complex patterns in the middle of strings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A B-tree index on &lt;code&gt;name&lt;/code&gt; can only accelerate queries where the pattern starts with known characters, like &lt;code&gt;'laptop%'&lt;/code&gt;, because it can navigate directly to the sorted position where 'laptop' entries begin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquerysharp.com%2Fblog-diagrams%2Fbtree-and-like-operator2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquerysharp.com%2Fblog-diagrams%2Fbtree-and-like-operator2.png" title="B-tree Index" alt="B-tree Index" width="800" height="934"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3 – Fix #1: B-tree + suffix-only searches
&lt;/h2&gt;

&lt;p&gt;For patterns that start with known characters (suffix wildcards only like &lt;code&gt;'lap%'&lt;/code&gt; and not &lt;code&gt;'%lap%'&lt;/code&gt;), a standard B-tree index works brilliantly.&lt;br&gt;
For ILIKE queries, to achieve the same effect, you need to create a functional index on the lowercased version of the column, and then use the &lt;code&gt;LOWER()&lt;/code&gt; function in the query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create index for case-sensitive prefix searches&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- This query can use the index efficiently&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'laptop%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- For case-insensitive prefix searches, use a functional index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_name_lower&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Rewrite your query to use LOWER()&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Laptop%'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Or simply:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'laptop%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works perfectly when users search from the beginning of a field, such as autocomplete functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  4 – Fix #2: Trigram magic with pg_trgm + GIN
&lt;/h2&gt;

&lt;p&gt;For the holy grail of pattern matching—fast searches anywhere in the string—PostgreSQL's pg_trgm extension with GIN indexes is your best friend.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Installing pg_trgm and creating the index
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable the extension (once per database)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_trgm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a GIN index with trigram support&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_name_trgm&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;gin_trgm_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For case-insensitive searches, index the lowercased version&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_name_trgm_lower&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;gin_trgm_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your wildcard queries fly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- These queries can now use the GIN index&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%laptop%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'%LAPTOP%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%laptop%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 The "3-character" edge-case and how to mitigate
&lt;/h3&gt;

&lt;p&gt;Trigrams work by breaking text into 3-character chunks. Searches for patterns shorter than 3 characters can't use the index effectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- These won't use the trigram index efficiently&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%a%'&lt;/span&gt;    &lt;span class="c1"&gt;-- 1 character&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%ab%'&lt;/span&gt;   &lt;span class="c1"&gt;-- 2 characters&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mitigation strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set a minimum search length in your application (3+ characters)&lt;/li&gt;
&lt;li&gt;For short patterns, consider using a separate strategy like full-text search&lt;/li&gt;
&lt;li&gt;My favorite, when applicable : if term is less than 3 characters, remove the leading wildcard, and use a B-tree index. If more than 3 characters, you can safely use '%abc%'. Sometimes, the data consistency trade-off is worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5 – When LIKE is not enough: full-text search vs external engines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 In-database full-text search
&lt;/h3&gt;

&lt;p&gt;PostgreSQL's built-in full-text search offers powerful features beyond simple pattern matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create a text search column&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Populate it with weighted content&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a GIN index on the tsvector&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Search with ranking&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plainto_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'gaming laptop'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Language-aware stemming (laptop/laptops match)&lt;/li&gt;
&lt;li&gt;Ranking and relevance scoring&lt;/li&gt;
&lt;li&gt;Phrase searching and boolean operators&lt;/li&gt;
&lt;li&gt;Stop word elimination&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.2 Elasticsearch / OpenSearch / Meilisearch
&lt;/h3&gt;

&lt;p&gt;For ultimate search capabilities, consider dedicated search engines:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Elasticsearch/OpenSearch&lt;/strong&gt;: Industry standard for complex search requirements&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fuzzy matching and typo tolerance&lt;/li&gt;
&lt;li&gt;Faceted search and aggregations&lt;/li&gt;
&lt;li&gt;Near real-time indexing&lt;/li&gt;
&lt;li&gt;Distributed architecture for scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Meilisearch&lt;/strong&gt;: Modern, developer-friendly alternative&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instant search as you type&lt;/li&gt;
&lt;li&gt;Built-in typo tolerance&lt;/li&gt;
&lt;li&gt;Simple setup and maintenance&lt;/li&gt;
&lt;li&gt;Great for small to medium datasets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose external engines when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Typo tolerance and fuzzy matching&lt;/li&gt;
&lt;li&gt;Complex relevance tuning&lt;/li&gt;
&lt;li&gt;Multi-language support with advanced analyzers&lt;/li&gt;
&lt;li&gt;Search across multiple data sources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6 – Checklist &amp;amp; FAQ
&lt;/h2&gt;

&lt;p&gt;Before optimizing, answer these questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does your pattern start with &lt;code&gt;%&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If yes, you need GIN indexes with pg_trgm or consider full-text search. B-tree indexes won't help.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need case-insensitive search?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use functional indexes with LOWER() instead of ILIKE for better performance. Create indexes on LOWER(column).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is your average keyword &amp;lt; 3 chars?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Trigram indexes won't be effective. Consider full-text search or enforce minimum search length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How large is the table (≥ 1,000,000 rows)?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Large tables amplify the importance of proper indexing.&lt;/p&gt;

&lt;h2&gt;
  
  
  7 – Key takeaways
&lt;/h2&gt;

&lt;p&gt;• &lt;strong&gt;Start with the right pattern&lt;/strong&gt;: Prefix-only patterns ('term%') can use simple B-tree indexes for massive speedups&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Embrace pg_trgm for flexibility&lt;/strong&gt;: GIN indexes with trigrams handle any wildcard pattern at the cost of index size&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Case-insensitive doesn't mean ILIKE&lt;/strong&gt;: Functional indexes on LOWER() often perform better&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Measure everything&lt;/strong&gt;: Use EXPLAIN (ANALYZE, BUFFERS) before and after each optimization. For ongoing monitoring, consider tools like &lt;a href="https://querysharp.com" rel="noopener noreferrer"&gt;QuerySharp&lt;/a&gt; to track query performance over time and catch regressions early.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Know when to go beyond LIKE&lt;/strong&gt;: Full-text search or dedicated search engines solve problems LIKE queries weren't designed for&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Index maintenance matters&lt;/strong&gt;: Remember to VACUUM and ANALYZE regularly, especially after bulk updates&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Consider your growth trajectory&lt;/strong&gt;: What works at 100K rows might not at 10M rows—plan accordingly&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Full disclosure&lt;/em&gt;: I'm the founder of &lt;a href="https://querysharp.com" rel="noopener noreferrer"&gt;QuerySharp&lt;/a&gt;, a PostgreSQL query optimization app that helps developers identify and fix slow queries like the ones we've discussed in this article.&lt;br&gt;
While writing this guide, I realized how many teams struggle with the same LIKE/ILIKE performance issues we see in our user data every day. The optimization techniques above can dramatically improve your query performance, but the real challenge is often knowing which queries need optimization in the first place.&lt;br&gt;
That's exactly why we built QuerySharp—to automatically surface slow queries, track performance over time, and help you prioritize which optimizations will have the biggest impact on your users. If you're dealing with performance issues beyond just LIKE queries, or want to proactively monitor your database health, give it a try.&lt;br&gt;
Whether you use our tool or not, the techniques in this article will serve you well. The key is to measure, optimize, and monitor—your users (and your on-call schedule) will thank you.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>webdev</category>
      <category>devops</category>
      <category>database</category>
    </item>
  </channel>
</rss>
