<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>MySQL | きいちログ</title>
	<atom:link href="https://wptech.kiichiro.work/tag/mysql/feed/" rel="self" type="application/rss+xml" />
	<link>https://wptech.kiichiro.work</link>
	<description>WordPressとかAWSとかPHPとか</description>
	<lastBuildDate>Thu, 15 Jun 2023 15:19:48 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>
	<item>
		<title>【Laravel】JSON型のカラムにindexを生成してもindexが効かなくなった</title>
		<link>https://wptech.kiichiro.work/95lrw1tolj/</link>
		
		<dc:creator><![CDATA[むらおか]]></dc:creator>
		<pubDate>Thu, 15 Jun 2023 15:19:47 +0000</pubDate>
				<category><![CDATA[Tech]]></category>
		<category><![CDATA[Laravel]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://wptech.kiichiro.work/?p=1708</guid>

					<description><![CDATA[本稿では、JSON型のカラムにindexを貼ってみたものの、Laravelで想定通りindexを使ってくれなかったケースを紹介します。 目次 JSON型カラムJSON型のカラムにindex貼ってみたLaravelでやって [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="">本稿では、JSON型のカラムにindexを貼ってみたものの、Laravelで想定通りindexを使ってくれなかったケースを紹介します。</p>




  <div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-2" checked><label class="toc-title" for="toc-checkbox-2">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">JSON型カラム</a></li><li><a href="#toc2" tabindex="0">JSON型のカラムにindex貼ってみた</a></li><li><a href="#toc3" tabindex="0">Laravelでやってみた</a></li><li><a href="#toc4" tabindex="0">indexが使われない原因</a></li><li><a href="#toc5" tabindex="0">indexが使われるようにする</a></li><li><a href="#toc6" tabindex="0">おまけ</a></li><li><a href="#toc7" tabindex="0">参考リンク</a></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">JSON型カラム</span></h2>



<p class="">MySQLのJSON型って便利ですよね。あらかじめデータ構造を定義する必要が無いので、データ構造がまだ決まっていなかったり、頻繁に変更があったりする場合は、とりあえずJSON型にして突っ込んでおくという使い方ができますし。</p>



<p class="">ただ、なんでもかんでもJSON型のカラムに突っ込んで痛い目を見てきた身からすると、使うケースは熟考した方が良いだろうなとも思います。特にJSON型のカラムのindex周りは注意が必要です。</p>



<h2 class="wp-block-heading"><span id="toc2">JSON型のカラムにindex貼ってみた</span></h2>



<p class="">MySQLでJSON型のカラムにindexを付けるには、Generated Columnを使うことが多いようです。Laravelのmigrationでは、<code>virtualAs</code> を使ってGenerated Columnを利用することができます。</p>



<p class="">以下は <code>post_meta</code> カラムはJSON型で、<code>slug</code> という属性に対してindexを貼りたいときのmigrationの例です。<code>post_meta_slug</code> をGenerated Columnで追加し、そこにindexを追加しています。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-php" data-file="migration.php" data-lang="PHP"><code>public function up(): void
{
    Schema::create(&#39;posts&#39;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;json(&#39;post_meta&#39;);
        $table-&gt;string(&#39;post_meta_slug&#39;)-&gt;nullable()-&gt;virtualAs(&#39;JSON_UNQUOTE(post_meta-&gt;&quot;$.slug&quot;)&#39;);
        $table-&gt;index(&#39;post_meta_slug&#39;);
    });
}</code></pre></div>



<p class="">CREATE TABLEを見てみると、Generated Columnが生成され、indexが追加されていることが確認できます。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>mysql&gt; show create table posts;
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                                                                                                                            |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| posts | CREATE TABLE `posts` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `post_meta` json NOT NULL,
  `post_meta_slug` varchar(255) COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (json_unquote(json_extract(`post_meta`,_utf8mb4&#39;$.slug&#39;))) VIRTUAL,
  PRIMARY KEY (`id`),
  KEY `posts_post_meta_slug_index` (`post_meta_slug`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)</code></pre></div>



<p class="">実際にテストデータを入れてindexが効いているかどうかの確認のため、実行計画を見てみます。<br>まずはデータを用意します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-sql" data-lang="SQL"><code>INSERT INTO posts (post_meta) values
(&#39;{&quot;slug&quot;:&quot;6nwolmsp8y&quot;, &quot;title&quot;: &quot;AWS-CDKWordPress&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;4yeiux7wqq&quot;, &quot;title&quot;: &quot;WPScanxmlrpc&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;3yjela8tgv&quot;, &quot;title&quot;: &quot;WordPressHTTPHTTPS (version5.7)&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;4771lv37ot&quot;, &quot;title&quot;: &quot;Laravel SailMySQLPostgres&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;2xch5zq3e9&quot;, &quot;title&quot;: &quot;BeautifulSoupSelenium&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;1xbq0pridw&quot;, &quot;title&quot;: &quot;ACF10Delicious Brains Inc.&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;10bwr53umq&quot;, &quot;title&quot;: &quot;Amazon LightsailAWS CDK&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;43nyielz64&quot;, &quot;title&quot;: &quot;Amazon LightsailSSL&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;40a2u2w7rv&quot;, &quot;title&quot;: &quot;AWS Solutions Architect Associate&quot;}&#39;),
(&#39;{&quot;slug&quot;:&quot;26ru99uao0&quot;, &quot;title&quot;: &quot;AWS CDKTypeScriptLambda + SAM Local&quot;}&#39;)
;</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>mysql&gt; select * from posts;
+----+------------------------------------------------------------------------+----------------+
| id | post_meta                                                              | post_meta_slug |
+----+------------------------------------------------------------------------+----------------+
|  1 | {&quot;slug&quot;: &quot;6nwolmsp8y&quot;, &quot;title&quot;: &quot;AWS-CDKWordPress&quot;}                    | 6nwolmsp8y     |
|  2 | {&quot;slug&quot;: &quot;4yeiux7wqq&quot;, &quot;title&quot;: &quot;WPScanxmlrpc&quot;}                        | 4yeiux7wqq     |
|  3 | {&quot;slug&quot;: &quot;3yjela8tgv&quot;, &quot;title&quot;: &quot;WordPressHTTPHTTPS (version5.7)&quot;}     | 3yjela8tgv     |
|  4 | {&quot;slug&quot;: &quot;4771lv37ot&quot;, &quot;title&quot;: &quot;Laravel SailMySQLPostgres&quot;}           | 4771lv37ot     |
|  5 | {&quot;slug&quot;: &quot;2xch5zq3e9&quot;, &quot;title&quot;: &quot;BeautifulSoupSelenium&quot;}               | 2xch5zq3e9     |
|  6 | {&quot;slug&quot;: &quot;1xbq0pridw&quot;, &quot;title&quot;: &quot;ACF10Delicious Brains Inc.&quot;}          | 1xbq0pridw     |
|  7 | {&quot;slug&quot;: &quot;10bwr53umq&quot;, &quot;title&quot;: &quot;Amazon LightsailAWS CDK&quot;}             | 10bwr53umq     |
|  8 | {&quot;slug&quot;: &quot;43nyielz64&quot;, &quot;title&quot;: &quot;Amazon LightsailSSL&quot;}                 | 43nyielz64     |
|  9 | {&quot;slug&quot;: &quot;40a2u2w7rv&quot;, &quot;title&quot;: &quot;AWS Solutions Architect Associate&quot;}   | 40a2u2w7rv     |
| 10 | {&quot;slug&quot;: &quot;26ru99uao0&quot;, &quot;title&quot;: &quot;AWS CDKTypeScriptLambda + SAM Local&quot;} | 26ru99uao0     |
+----+------------------------------------------------------------------------+----------------+
10 rows in set (0.00 sec)</code></pre></div>



<p class=""><code>post_meta_slug</code> にダブルクオーテーションが除去された文字列が入っていることが確認できます。</p>



<p class="">JSON型のカラムの属性でWHERE句で絞り込みたい場合は、JSON_EXTRACTまたは短縮演算子-&gt;を使用します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>mysql&gt; SELECT * FROM posts WHERE `post_meta`-&gt;&quot;$.slug&quot; = &quot;6nwolmsp8y&quot;;
+----+-----------------------------------------------------+----------------+
| id | post_meta                                           | post_meta_slug |
+----+-----------------------------------------------------+----------------+
|  1 | {&quot;slug&quot;: &quot;6nwolmsp8y&quot;, &quot;title&quot;: &quot;AWS-CDKWordPress&quot;} | 6nwolmsp8y     |
+----+-----------------------------------------------------+----------------+
1 row in set (0.02 sec)

mysql&gt; SELECT * FROM posts WHERE JSON_UNQUOTE(JSON_EXTRACT(`post_meta`, &quot;$.slug&quot;))  = &quot;6nwolmsp8y&quot;;
+----+-----------------------------------------------------+----------------+
| id | post_meta                                           | post_meta_slug |
+----+-----------------------------------------------------+----------------+
|  1 | {&quot;slug&quot;: &quot;6nwolmsp8y&quot;, &quot;title&quot;: &quot;AWS-CDKWordPress&quot;} | 6nwolmsp8y     |
+----+-----------------------------------------------------+----------------+
1 row in set (0.00 sec)
</code></pre></div>



<p class="">実行計画を出してみます。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>mysql&gt; EXPLAIN SELECT * FROM posts WHERE `post_meta`-&gt;&quot;$.slug&quot; = &quot;6nwolmsp8y&quot;;
+----+-------------+-------+------------+------+----------------------------+----------------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys              | key                        | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+----------------------------+----------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | posts | NULL       | ref  | posts_post_meta_slug_index | posts_post_meta_slug_index | 1023    | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+----------------------------+----------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql&gt; EXPLAIN SELECT * FROM posts WHERE JSON_UNQUOTE(JSON_EXTRACT(`post_meta`, &quot;$.slug&quot;))  = &quot;6nwolmsp8y&quot;;
+----+-------------+-------+------------+------+----------------------------+----------------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys              | key                        | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+----------------------------+----------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | posts | NULL       | ref  | posts_post_meta_slug_index | posts_post_meta_slug_index | 1023    | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+----------------------------+----------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)</code></pre></div>



<p class="">keyを見ると、Generated Columnで追加したカラムを含むindexが使用されていることが確認できます。</p>



<h2 class="wp-block-heading"><span id="toc3">Laravelでやってみた</span></h2>



<p class="">Laravel側の準備をします。postsテーブルに対応するPostモデルを作っておきます。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-php" data-file="Post.php" data-lang="PHP"><code>&lt;?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model { }</code></pre></div>



<p class="">tinkerを起動してEloquentを使ってクエリを組み立てます。今回は実行計画を見たいので、explain()を使います。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>&gt; \App\Models\Post::where(&#39;post_meta-&gt;slug&#39;, &#39;6nwolmsp8y&#39;)-&gt;explain();
= Illuminate\Support\Collection {#6207
    all: [
      {#7161
        +&quot;id&quot;: 1,
        +&quot;select_type&quot;: &quot;SIMPLE&quot;,
        +&quot;table&quot;: &quot;posts&quot;,
        +&quot;partitions&quot;: null,
        +&quot;type&quot;: &quot;ALL&quot;,
        +&quot;possible_keys&quot;: null,
        +&quot;key&quot;: null,
        +&quot;key_len&quot;: null,
        +&quot;ref&quot;: null,
        +&quot;rows&quot;: 10,
        +&quot;filtered&quot;: 100.0,
        +&quot;Extra&quot;: &quot;Using where&quot;,
      },
    ],
  }</code></pre></div>



<p class="">indexは使われていないようです。。</p>



<h2 class="wp-block-heading"><span id="toc4">indexが使われない原因</span></h2>



<p class="">toSqlで実際にどのようなクエリが組み立てられているのかを確認してみます。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>&gt; \App\Models\Post::where(&#39;post_meta-&gt;slug&#39;, &#39;6nwolmsp8y&#39;)-&gt;toSql();
= &quot;select * from `posts` where json_unquote(json_extract(`post_meta`, &#39;$.&quot;slug&quot;&#39;)) = ?&quot;</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-sql" data-lang="SQL"><code>SELECT * FROM `posts` WHERE JSON_UNQUOTE(JSON_EXTRACT(`post_meta`, &#39;$.&quot;slug&quot;&#39;)) = &quot;6nwolmsp8y&quot;;</code></pre></div>



<p class="">一見、何も問題ないように見えますが、slugがダブルクオーテーションで括られていることがわかります。</p>



<p class="">Laravelではwhereで「->」を含む文字列が渡された場合、JSON型へのクエリーであると判断してJSONの組み込み関数へと展開されます。同時にJSONのパスをダブルクオーテーションで括るように変換するようです。</p>



<p class="">公式ドキュメントには以下の記述があります。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="">クエリー式が生成されたカラム定義と一致するには、式が同一であり、同じ結果タイプである必要があります。 たとえば、生成されたカラム式が&nbsp;<code>f1 + 1</code>&nbsp;の場合、クエリーで&nbsp;<code>1 + f1</code>&nbsp;が使用されているか、<code>f1 + 1</code>&nbsp;(整数式) が文字列と比較されても、オプティマイザは一致を認識しません。</p>
<cite><a href="https://dev.mysql.com/doc/refman/8.0/ja/generated-column-index-optimizations.html">https://dev.mysql.com/doc/refman/8.0/ja/generated-column-index-optimizations.html</a></cite></blockquote>



<p class="">おそらく、オプティマイザがGenerated Columnの式とクエリー式が一致していないと判断しているため、indexが使われないと考えられます。</p>



<h2 class="wp-block-heading"><span id="toc5">indexが使われるようにする</span></h2>



<p class="">1つは、<code>whereRaw</code> を使う方法が挙げられます。<code>whereRaw</code>は素のwhere句を記述することができるので、勝手に変換されることを回避できます。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>&gt; \App\Models\Post::query()-&gt;whereRaw(&#39;JSON_UNQUOTE(JSON_EXTRACT(post_meta, &quot;$.slug&quot;)) = ?&#39;, [&#39;4yeiux7wqq&#39;])-&gt;explain();
= Illuminate\Support\Collection {#7174
    all: [
      {#6210
        +&quot;id&quot;: 1,
        +&quot;select_type&quot;: &quot;SIMPLE&quot;,
        +&quot;table&quot;: &quot;posts&quot;,
        +&quot;partitions&quot;: null,
        +&quot;type&quot;: &quot;ref&quot;,
        +&quot;possible_keys&quot;: &quot;posts_post_meta_slug_index&quot;,
        +&quot;key&quot;: &quot;posts_post_meta_slug_index&quot;,
        +&quot;key_len&quot;: &quot;1023&quot;,
        +&quot;ref&quot;: &quot;const&quot;,
        +&quot;rows&quot;: 1,
        +&quot;filtered&quot;: 100.0,
        +&quot;Extra&quot;: null,
      },
    ],
  }</code></pre></div>



<p class="">もう一つは、Generated Columnをwhere句に指定する方法です。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>&gt; \App\Models\Post::query()-&gt;where(&#39;post_meta_slug&#39;, &#39;4yeiux7wqq&#39;)-&gt;explain();
= Illuminate\Support\Collection {#7175
    all: [
      {#7165
        +&quot;id&quot;: 1,
        +&quot;select_type&quot;: &quot;SIMPLE&quot;,
        +&quot;table&quot;: &quot;posts&quot;,
        +&quot;partitions&quot;: null,
        +&quot;type&quot;: &quot;ref&quot;,
        +&quot;possible_keys&quot;: &quot;posts_post_meta_slug_index&quot;,
        +&quot;key&quot;: &quot;posts_post_meta_slug_index&quot;,
        +&quot;key_len&quot;: &quot;1023&quot;,
        +&quot;ref&quot;: &quot;const&quot;,
        +&quot;rows&quot;: 1,
        +&quot;filtered&quot;: 100.0,
        +&quot;Extra&quot;: null,
      },
    ],
  }</code></pre></div>



<p class="">どちらもindexを使っていることを確認できますが、個人的には後者の方がシンプルで良いと思います。</p>



<h2 class="wp-block-heading"><span id="toc6">おまけ</span></h2>



<p class="">MySQL 8.0.13以降では、関数インデックスが使用できます。やっていることはGenerated Columnとあまり変わりないですが、CHARでCASTすることとcollationを明示する必要があるところが異なります。</p>



<p class="">LaravelではrawIndexでもcollationの指定ができなかった(多分)ので、DB::statementで直接ALTER TABLEを書いています。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-php" data-file="migration.php" data-lang="PHP"><code>public function up(): void
{
    Schema::create(&#39;posts&#39;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;json(&#39;post_meta&#39;);
    });
    DB::statement(&#39;ALTER TABLE posts ADD INDEX post_meta_slug_index((CAST(post_meta-&gt;&gt;&quot;$.slug&quot; as CHAR(255)) COLLATE utf8mb4_bin))&#39;);
}</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>mysql&gt; show create table posts;
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                                                    |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| posts | CREATE TABLE `posts` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `post_meta` json NOT NULL,
  PRIMARY KEY (`id`),
  KEY `post_meta_slug_index` (((cast(json_unquote(json_extract(`post_meta`,_utf8mb4&#39;$.slug&#39;)) as char(255) charset utf8mb4) collate utf8mb4_bin)))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)</code></pre></div>



<p class="">関数インデックスでも式が一致していないとindexが使われないというのは同じようです。</p>



<h2 class="wp-block-heading"><span id="toc7">参考リンク</span></h2>



<ul class="wp-block-list">
<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/json.html">https://dev.mysql.com/doc/refman/8.0/ja/json.html</a></li>



<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/create-table-generated-columns.html">https://dev.mysql.com/doc/refman/8.0/ja/create-table-generated-columns.html</a></li>



<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/generated-column-index-optimizations.html">https://dev.mysql.com/doc/refman/8.0/ja/generated-column-index-optimizations.html</a></li>



<li><a href="https://dev.mysql.com/doc/refman/8.0/ja/create-index.html#create-index-functional-key-parts">https://dev.mysql.com/doc/refman/8.0/ja/create-index.html#create-index-functional-key-parts</a></li>



<li><a href="https://dev.mysql.com/blog-archive/indexing-json-documents-via-virtual-columns/">https://dev.mysql.com/blog-archive/indexing-json-documents-via-virtual-columns/</a></li>



<li><a href="https://blogs.oracle.com/mysql-jp/post/indexing-json-data-in-mysql-jp">https://blogs.oracle.com/mysql-jp/post/indexing-json-data-in-mysql-jp</a></li>



<li><a href="https://planetscale.com/blog/indexing-json-in-mysql">https://planetscale.com/blog/indexing-json-in-mysql</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
