<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Collation on Tarragon</title><link>https://tarrragon.github.io/blog/tags/collation/</link><description>Recent content in Collation on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 02 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/collation/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.15 字串比較與 collation：相等、排序、LIKE 與索引可用性</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/string-comparison-and-collation/</link><pubDate>Wed, 02 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/string-comparison-and-collation/</guid><description>&lt;p>兩個字串相不相等，答案由一條叫 &lt;strong>collation&lt;/strong> 的規則給出——前提是兩邊都是字串型別。型別先發言：PostgreSQL 18 上 &lt;code>'a '::char(2) = 'a'&lt;/code> 為真而 &lt;code>'a '::text = 'a'::text&lt;/code> 為假，同一條 collation，差別在 &lt;code>char(n)&lt;/code> 的等號自己會剝掉尾隨空白。下文談的都是兩邊都是變長字串的情形。collation 是掛在資料庫、表或欄位上的一份比較規則，它規定兩個字串怎麼判相等、以及誰排在前面——大小寫算不算差異、重音符號算不算差異、字元的先後怎麼定。這條規則不在查詢的文字裡，所以同一段 &lt;code>WHERE 姓名 = 'anna'&lt;/code> 在不同的地方會回不同的列。&lt;/p>
&lt;p>大小寫這件事在 SQL 裡落在兩個彼此獨立的層。&lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/identifier-rules/" data-link-title="SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字" data-link-desc="表名與欄名送進引擎之後被怎麼改寫、PostgreSQL 與 SQLite、DuckDB 在引號上的差別、MySQL 的反引號，以及全部不加引號、全部加引號與混用各自在換引擎時的後果">SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字&lt;/a> 處理的是&lt;strong>名字&lt;/strong>送進引擎會被怎麼摺疊，這一篇處理的是&lt;strong>值&lt;/strong>被拿去比較時套的是哪一把尺。兩層的規則互不影響：PostgreSQL 把沒加引號的表名摺成小寫，卻不會把 &lt;code>'Anna'&lt;/code> 這個值摺成小寫。&lt;/p>
&lt;p>本篇把同一條規則作用的四個表面走完：什麼算相等、&lt;code>LIKE&lt;/code> 為什麼會與等號分岔、誰排在前面、以及索引走不走得了。四者收在同一個處置上——規則要寫出來，而寫在資料庫、表或欄位哪一層決定它涵蓋到哪裡。&lt;/p>
&lt;p>本篇的顧客表有五列：&lt;code>Anna&lt;/code>、&lt;code>anna&lt;/code>、&lt;code>ANNA&lt;/code>、&lt;code>Ánna&lt;/code>、&lt;code>佳穎&lt;/code>。&lt;/p>
&lt;h2 id="等值比較在各家引擎的預設-collation-下命中的列">等值比較在各家引擎的預設 collation 下命中的列&lt;/h2>
&lt;p>每一家的預設規則對大小寫與重音的規定不同，所以同一個等值條件在三家上命中的列不是同一批：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">WHERE 姓名 = &amp;#39;anna&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">SQLite 3.51 anna
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">PostgreSQL 18 anna
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">MySQL 8.4 Anna, anna, ANNA, Ánna&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>MySQL 的預設 collation 是 &lt;code>utf8mb4_0900_ai_ci&lt;/code>，名字裡的 &lt;code>ai&lt;/code> 與 &lt;code>ci&lt;/code> 就是它的規定：accent-insensitive 與 case-insensitive——重音與大小寫都不算差異，所以 &lt;code>Ánna&lt;/code> 與 &lt;code>anna&lt;/code> 在這條規則底下是同一個值。SQLite 與 PostgreSQL 的預設把這兩者都算差異，於是只有逐字相同的那一列命中。&lt;/p>
&lt;p>&lt;strong>這三個答案都正確&lt;/strong>，因為「相等」的定義本來就由那條規則給，而查詢的文字沒有選過它。一段在 MySQL 上寫好的登入查詢搬到 PostgreSQL，比對帳號的那一段會突然變成大小寫敏感，而它照樣執行、照樣回一批形狀正確的列。&lt;/p>
&lt;h2 id="sqlite-的-like-與等號各自套用的比較規則">SQLite 的 LIKE 與等號各自套用的比較規則&lt;/h2>
&lt;p>&lt;code>LIKE&lt;/code> 是另一個做字串比較的運算子，而它套的規則未必與 &lt;code>=&lt;/code> 相同：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl"> SQLite 3.51 PostgreSQL 18 MySQL 8.4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">= &amp;#39;anna&amp;#39; anna anna Anna, anna, ANNA, Ánna
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">LIKE &amp;#39;anna&amp;#39; Anna, anna, ANNA anna Anna, anna, ANNA, Ánna&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>PostgreSQL 與 MySQL 的兩列一致，SQLite 的兩列分岔：它的 &lt;code>=&lt;/code> 逐位元組比較，而它的 &lt;code>LIKE&lt;/code> 預設不分大小寫。所以在 SQLite 上把 &lt;code>=&lt;/code> 換成 &lt;code>LIKE&lt;/code>（例如為了加一個 &lt;code>%&lt;/code>）會順帶換掉比較規則，而這件事沒有任何提示。&lt;/p>
&lt;p>SQLite 這個不分大小寫的範圍限定在 ASCII 字母：&lt;code>LIKE 'ánna'&lt;/code> 在同一批資料上回零列，&lt;code>Ánna&lt;/code> 沒有被當成同一個字串。所以那條規則的完整敘述是「A 到 Z 這二十六個字母摺疊，其餘逐位元組」，而不是「不分大小寫」。&lt;/p>
&lt;p>要把它調成一致，&lt;code>PRAGMA case_sensitive_like = ON&lt;/code> 讓 &lt;code>LIKE&lt;/code> 跟著 &lt;code>=&lt;/code> 走；反向的做法是在條件上寫 &lt;code>COLLATE NOCASE&lt;/code>，讓 &lt;code>=&lt;/code> 跟著 &lt;code>LIKE&lt;/code> 走。&lt;strong>選哪一邊都行，而選了要寫出來&lt;/strong>——沒寫出來的時候讀查詢的人得先知道它跑在哪一家引擎上，才知道這一句在比什麼。&lt;/p>
&lt;h2 id="collation-規定的排序先後">collation 規定的排序先後&lt;/h2>
&lt;p>collation 除了規定兩個字串相不相等，也規定誰排在前面。同一批五個名字，三家排出三種順序：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ORDER BY 姓名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">SQLite 3.51 ANNA, Anna, anna, Ánna, 佳穎
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">PostgreSQL 18 anna, Anna, ANNA, Ánna, 佳穎
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">MySQL 8.4 Anna, anna, ANNA, Ánna, 佳穎&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SQLite 那一列是位元組的先後——大寫字母的碼位小於小寫，所以 &lt;code>ANNA&lt;/code> 整批排在前面。PostgreSQL 這一列取決於資料庫建立時選的 collation，量測用的這個是 &lt;code>en_US.utf8&lt;/code>（查法是 &lt;code>SELECT datcollate FROM pg_database&lt;/code>），它照人類語言的習慣把大小寫視為同一個字母的變體、再用大小寫決定同分時的先後。同一段排序在 PostgreSQL 上改用 &lt;code>C&lt;/code> 這條逐位元組比較的規則，順序就與 SQLite 一致：&lt;/p></description><content:encoded><![CDATA[<p>兩個字串相不相等，答案由一條叫 <strong>collation</strong> 的規則給出——前提是兩邊都是字串型別。型別先發言：PostgreSQL 18 上 <code>'a '::char(2) = 'a'</code> 為真而 <code>'a '::text = 'a'::text</code> 為假，同一條 collation，差別在 <code>char(n)</code> 的等號自己會剝掉尾隨空白。下文談的都是兩邊都是變長字串的情形。collation 是掛在資料庫、表或欄位上的一份比較規則，它規定兩個字串怎麼判相等、以及誰排在前面——大小寫算不算差異、重音符號算不算差異、字元的先後怎麼定。這條規則不在查詢的文字裡，所以同一段 <code>WHERE 姓名 = 'anna'</code> 在不同的地方會回不同的列。</p>
<p>大小寫這件事在 SQL 裡落在兩個彼此獨立的層。<a href="/blog/backend/01-database/sql/identifier-rules/" data-link-title="SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字" data-link-desc="表名與欄名送進引擎之後被怎麼改寫、PostgreSQL 與 SQLite、DuckDB 在引號上的差別、MySQL 的反引號，以及全部不加引號、全部加引號與混用各自在換引擎時的後果">SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字</a> 處理的是<strong>名字</strong>送進引擎會被怎麼摺疊，這一篇處理的是<strong>值</strong>被拿去比較時套的是哪一把尺。兩層的規則互不影響：PostgreSQL 把沒加引號的表名摺成小寫，卻不會把 <code>'Anna'</code> 這個值摺成小寫。</p>
<p>本篇把同一條規則作用的四個表面走完：什麼算相等、<code>LIKE</code> 為什麼會與等號分岔、誰排在前面、以及索引走不走得了。四者收在同一個處置上——規則要寫出來，而寫在資料庫、表或欄位哪一層決定它涵蓋到哪裡。</p>
<p>本篇的顧客表有五列：<code>Anna</code>、<code>anna</code>、<code>ANNA</code>、<code>Ánna</code>、<code>佳穎</code>。</p>
<h2 id="等值比較在各家引擎的預設-collation-下命中的列">等值比較在各家引擎的預設 collation 下命中的列</h2>
<p>每一家的預設規則對大小寫與重音的規定不同，所以同一個等值條件在三家上命中的列不是同一批：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">WHERE 姓名 = &#39;anna&#39;
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">SQLite 3.51        anna
</span></span><span class="line"><span class="ln">4</span><span class="cl">PostgreSQL 18      anna
</span></span><span class="line"><span class="ln">5</span><span class="cl">MySQL 8.4          Anna, anna, ANNA, Ánna</span></span></code></pre></div><p>MySQL 的預設 collation 是 <code>utf8mb4_0900_ai_ci</code>，名字裡的 <code>ai</code> 與 <code>ci</code> 就是它的規定：accent-insensitive 與 case-insensitive——重音與大小寫都不算差異，所以 <code>Ánna</code> 與 <code>anna</code> 在這條規則底下是同一個值。SQLite 與 PostgreSQL 的預設把這兩者都算差異，於是只有逐字相同的那一列命中。</p>
<p><strong>這三個答案都正確</strong>，因為「相等」的定義本來就由那條規則給，而查詢的文字沒有選過它。一段在 MySQL 上寫好的登入查詢搬到 PostgreSQL，比對帳號的那一段會突然變成大小寫敏感，而它照樣執行、照樣回一批形狀正確的列。</p>
<h2 id="sqlite-的-like-與等號各自套用的比較規則">SQLite 的 LIKE 與等號各自套用的比較規則</h2>
<p><code>LIKE</code> 是另一個做字串比較的運算子，而它套的規則未必與 <code>=</code> 相同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">                    SQLite 3.51        PostgreSQL 18      MySQL 8.4
</span></span><span class="line"><span class="ln">2</span><span class="cl">= &#39;anna&#39;            anna               anna               Anna, anna, ANNA, Ánna
</span></span><span class="line"><span class="ln">3</span><span class="cl">LIKE &#39;anna&#39;         Anna, anna, ANNA   anna               Anna, anna, ANNA, Ánna</span></span></code></pre></div><p>PostgreSQL 與 MySQL 的兩列一致，SQLite 的兩列分岔：它的 <code>=</code> 逐位元組比較，而它的 <code>LIKE</code> 預設不分大小寫。所以在 SQLite 上把 <code>=</code> 換成 <code>LIKE</code>（例如為了加一個 <code>%</code>）會順帶換掉比較規則，而這件事沒有任何提示。</p>
<p>SQLite 這個不分大小寫的範圍限定在 ASCII 字母：<code>LIKE 'ánna'</code> 在同一批資料上回零列，<code>Ánna</code> 沒有被當成同一個字串。所以那條規則的完整敘述是「A 到 Z 這二十六個字母摺疊，其餘逐位元組」，而不是「不分大小寫」。</p>
<p>要把它調成一致，<code>PRAGMA case_sensitive_like = ON</code> 讓 <code>LIKE</code> 跟著 <code>=</code> 走；反向的做法是在條件上寫 <code>COLLATE NOCASE</code>，讓 <code>=</code> 跟著 <code>LIKE</code> 走。<strong>選哪一邊都行，而選了要寫出來</strong>——沒寫出來的時候讀查詢的人得先知道它跑在哪一家引擎上，才知道這一句在比什麼。</p>
<h2 id="collation-規定的排序先後">collation 規定的排序先後</h2>
<p>collation 除了規定兩個字串相不相等，也規定誰排在前面。同一批五個名字，三家排出三種順序：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">ORDER BY 姓名
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">SQLite 3.51        ANNA, Anna, anna, Ánna, 佳穎
</span></span><span class="line"><span class="ln">4</span><span class="cl">PostgreSQL 18      anna, Anna, ANNA, Ánna, 佳穎
</span></span><span class="line"><span class="ln">5</span><span class="cl">MySQL 8.4          Anna, anna, ANNA, Ánna, 佳穎</span></span></code></pre></div><p>SQLite 那一列是位元組的先後——大寫字母的碼位小於小寫，所以 <code>ANNA</code> 整批排在前面。PostgreSQL 這一列取決於資料庫建立時選的 collation，量測用的這個是 <code>en_US.utf8</code>（查法是 <code>SELECT datcollate FROM pg_database</code>），它照人類語言的習慣把大小寫視為同一個字母的變體、再用大小寫決定同分時的先後。同一段排序在 PostgreSQL 上改用 <code>C</code> 這條逐位元組比較的規則，順序就與 SQLite 一致：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- PostgreSQL 18，資料庫的 collation 是 en_US.utf8
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">姓名</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- anna, Anna, ANNA, Ánna, 佳穎
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">COLLATE</span><span class="w"> </span><span class="s2">&#34;C&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- ANNA, Anna, anna, Ánna, 佳穎（與 SQLite 的預設順序相同）</span></span></span></code></pre></div><p><strong>同一家引擎、同一批資料，換一條規則就換一種順序</strong>，而 SQLite 的預設順序就是位元組序這件事也因此驗得出來。</p>
<p>MySQL 那一列要換個方式讀。在 <code>ai_ci</code> 底下四個 A 開頭的名字<strong>彼此相等</strong>，所以它們之間沒有先後可言——那一列印的是它們進表的順序。<strong>同一個排序鍵改由 <code>GROUP_CONCAT</code> 取回，四個名字的順序整個倒過來</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- MySQL 8.4，同一張表、同一批資料
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">姓名</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- Anna, anna, ANNA, Ánna, 佳穎（進表的順序）
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">GROUP_CONCAT</span><span class="p">(</span><span class="err">姓名</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="err">姓名</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- Ánna,ANNA,anna,Anna,佳穎（四個 A 開頭的名字倒過來，佳穎仍在最後）</span></span></span></code></pre></div><p>兩次都沒有違反 collation，因為 collation 對這四個之間什麼都沒有規定。這是 collation 與分頁交會的地方：一條把大小寫與重音都忽略的規則，會讓原本以為唯一的排序鍵變成不唯一，而沒有規定的那一段在分頁時會變成同一列出現兩次、另一列一次都不出現——<a href="/blog/backend/01-database/sql/pagination-needs-a-total-order/" data-link-title="SQL.12 分頁的排序鍵與接續標記：同分的列、全序與 OFFSET 的代價" data-link-desc="每一頁各查一次時同分的列與翻頁途中的寫入怎麼造成重複與遺漏、排序鍵補到兩兩可分的條件、展開式與列值式的接續標記條件在排序方向上的差別，以及 OFFSET 隨頁數成長的代價">SQL.12 分頁的排序鍵與接續標記</a> 寫那個機制與把排序鍵補到兩兩可分的判斷標準。</p>
<h2 id="索引的比較規則與條件的比較規則">索引的比較規則與條件的比較規則</h2>
<p>索引把值按某一條規則排好，所以它只服務照同一條規則發問的條件。索引與條件的規則對不上，在小表上感覺不出來——資料長到某個量之後前綴搜尋開始變慢，而查詢與索引都沒有動過。這一節的計畫也要在夠大的表上才看得出來——五列的表無論條件寫成什麼，引擎都直接掃完，所以下面兩組輸出量在二十萬列的同結構表上。SQLite 上的三種問法，索引是同一個：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">CREATE INDEX ixn ON 顧客(姓名);            -- 預設規則，逐位元組
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">WHERE 姓名 = &#39;anna&#39;          SEARCH ... (姓名=?)      查找
</span></span><span class="line"><span class="ln">4</span><span class="cl">WHERE lower(姓名) = &#39;anna&#39;   SCAN                     整段掃過
</span></span><span class="line"><span class="ln">5</span><span class="cl">WHERE 姓名 LIKE &#39;ann%&#39;       SCAN                     整段掃過</span></span></code></pre></div><p>第二行是欄位被函式包住，索引上排好的是原值而條件問的是摺過的值，兩者對不起來（<a href="/blog/backend/01-database/sql/knowledge-cards/sargable/" data-link-title="Sargable（可走索引的條件形狀）" data-link-desc="索引存在而計畫仍走全表掃描時，查條件的寫法有沒有擋住那個索引">Sargable</a>）。<strong>第三行的成因不同</strong>：前綴比對本來可以翻成索引上的一段範圍，而 <code>LIKE</code> 預設不分大小寫，索引卻是逐位元組排的——規則對不上，範圍就算不出來。條件的規則與索引的規則，換掉任何一個都讓計畫回到範圍查找，而兩種換法回的列不同——改條件的規則也改掉了 <code>LIKE</code> 的答案：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 改條件的規則：LIKE 改成分大小寫，回到位元組規則
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">case_sensitive_like</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">PLAN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;ann%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- SEARCH ... (姓名&gt;? AND 姓名&lt;?)
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">-- 本篇五列的顧客表上回 anna
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- 改索引的規則：LIKE 維持預設的不分大小寫，另建一個按摺疊規則排的索引；SQLite 的索引一定要取名字
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">case_sensitive_like</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OFF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">ixn_nocase</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="p">(</span><span class="err">姓名</span><span class="w"> </span><span class="k">COLLATE</span><span class="w"> </span><span class="n">NOCASE</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">PLAN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;ann%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- SEARCH ... (姓名&gt;? AND 姓名&lt;?)
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">-- 本篇五列的顧客表上回 Anna, anna, ANNA</span></span></span></code></pre></div><p>PostgreSQL 上，索引與條件套的規則對不上，同樣會讓前綴搜尋用不到索引，只是外觀不同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">WHERE 姓名 = &#39;anna&#39;          Index Cond: (姓名 = &#39;anna&#39;)          查找
</span></span><span class="line"><span class="ln">2</span><span class="cl">WHERE lower(姓名) = &#39;anna&#39;   Filter: (lower(姓名) = &#39;anna&#39;)       逐列過濾
</span></span><span class="line"><span class="ln">3</span><span class="cl">WHERE 姓名 LIKE &#39;ann%&#39;       Filter: (姓名 ~~ &#39;ann%&#39;)             逐列過濾</span></span></code></pre></div><p><code>en_US.utf8</code> 的排序規則與位元組序不同，所以 <code>ann</code> 這個前綴在索引上不對應一段連續的區間。PostgreSQL 的解法是另建一個按位元組排的索引；<code>lower(姓名)</code> 那一行則由建在運算式上的索引接住：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 前綴搜尋：text_pattern_ops 讓這個索引逐字元比較，不照資料庫的 en_US.utf8
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="p">(</span><span class="err">姓名</span><span class="w"> </span><span class="n">text_pattern_ops</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;ann%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- Index Cond: (姓名 ~&gt;=~ &#39;ann&#39; AND 姓名 ~&lt;~ &#39;ano&#39;)：前綴翻成一段範圍
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- 欄位被函式包住：索引直接建在 lower(姓名) 上
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="p">(</span><span class="k">lower</span><span class="p">(</span><span class="err">姓名</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">lower</span><span class="p">(</span><span class="err">姓名</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;anna&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="c1">-- Index Cond: (lower(姓名) = &#39;anna&#39;)</span></span></span></code></pre></div><p>三種修法的共同形狀是<strong>讓索引與條件套同一條規則</strong>，而它們把改動放在不同的一邊：改條件、改索引的 collation、或另建一個按別的規則排的索引。比較規則對得上只是索引派得上用場的條件之一：條件的形狀是另一個（<code>lower(姓名)</code> 那一行就是形狀出的問題），而形狀與規則都對了之後，引擎還要看這個條件留下多少列才決定值不值得走索引，留下多少列怎麼估計在 <a href="/blog/backend/01-database/sql/knowledge-cards/cardinality-and-selectivity/" data-link-title="Cardinality 與 Selectivity（基數與選擇率）" data-link-desc="看到「這一欄基數太低、索引沒用」，或想知道最佳化器憑什麼決定要不要走索引時，查這兩個數各自量什麼">Cardinality 與 Selectivity</a>。</p>
<h2 id="collation-宣告在資料庫表欄位或條件上各自的涵蓋範圍">collation 宣告在資料庫、表、欄位或條件上各自的涵蓋範圍</h2>
<p>預設值在單一環境底下是隱形的——查詢跑得對，沒有人會去問「相等」現在是怎麼定義的。它在三個時刻現形：換一家引擎、換一個資料庫（同一家引擎的不同資料庫可以有不同的 collation）、以及有人在一欄上單獨指定了規則而其餘欄位沒有。要同時支援好幾家引擎的時候，collation 只是要逐項盤點的預設差異之一，識別字的摺疊是另一項，換引擎才浮現的形態在 <a href="/blog/backend/01-database/sql/identifier-rules/" data-link-title="SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字" data-link-desc="表名與欄名送進引擎之後被怎麼改寫、PostgreSQL 與 SQLite、DuckDB 在引號上的差別、MySQL 的反引號，以及全部不加引號、全部加引號與混用各自在換引擎時的後果">SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字</a> 的「SQLite 與 DuckDB 對識別字大小寫的處理」一節；各家預設差異的完整並置與分級在 <a href="/blog/backend/01-database/sql/engine-leniency-and-portability/" data-link-title="SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機" data-link-desc="SQLite、DuckDB、PostgreSQL 與 MySQL 在未分組欄位、WHERE 用別名、同表未取別名、字串比對與空值排序上的實測對照，差異從當場報錯到完全靜默的分級，以及依目標組態數決定哪幾級差異在寫的當下處理">SQL.19 引擎寬鬆度與可攜性：各家對同一組寫法的差異、分級與處理時機</a>。</p>
<p>所以規則的住址要選定，而可選的住址比多數人用到的多。開頭那一句說 collation 掛在資料庫、表或欄位上——那是三層，<strong>越外層涵蓋越廣而越容易被裡層蓋掉</strong>：建庫時選的那一條是所有沒有另外指定的欄位的預設，建表時選的蓋過它，欄位上寫的又蓋過表。來歷不明的建表模板最常把規則留在表層或庫層，所以查一欄的實際規則要從欄位往外找，找到第一個明寫的為止。</p>
<p><strong>寫在欄位上</strong>是最貼近資料的一種：<code>姓名 VARCHAR(50) COLLATE utf8mb4_0900_as_cs</code>（<code>as_cs</code> 是 accent-sensitive、case-sensitive）把這一欄固定住，往後每一段查詢、每一個索引都套它。<strong>寫在條件上</strong>（<code>WHERE 姓名 = 'anna' COLLATE NOCASE</code>）只涵蓋那一句。<strong>還有一個住址不在 collation 這條路上</strong>：寫入的時候就把值正規化，另存一欄摺過的版本，讀的時候比那一欄。開頭提過的登入查詢——帳號比對在 MySQL 上不分大小寫、搬到 PostgreSQL 突然變成分大小寫——實際的解法多半是這一種：比較規則變成一次寫入時的決定，而讀的那一句與它的索引都回到最單純的等值比較。同一個登入功能裡兩種住址各有位置：帳號欄整欄要不分大小寫，那是欄位的事；而某支後台工具要逐字比對，那是那一句的事。代價是寫在條件上的那一句走不了按預設規則建的索引。</p>
<p>判斷標準與<a href="/blog/backend/01-database/sql/knowledge-cards/constraint/" data-link-title="Constraint（約束）" data-link-desc="看到「這個保證要來自約束而不是來自習慣」，或想確認某一欄會不會出現 NULL 時，查這一層由誰擋">約束</a>那一條同源：一個保證要涵蓋往後的每一次使用，它就得寫在結構上，而不是靠每一句查詢各自記得。差別在於約束擋的是寫入，collation 決定的是讀出來的時候什麼算相等。</p>
]]></content:encoded></item></channel></rss>