kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Envoy の generate_request_id パラメータがデフォルト true であることを検証した

前に Try Envoy で Envoy と Jaeger を組み合わせた「トレーシング」を試したときに,Envoy の設定ファイル envoy.yamlgenerate_request_id: true を設定した.そのときは generate_request_id: true を設定すると Envoy が x-request-id ヘッダを自動的に付けてくれるという理解だった.

kakakakakku.hatenablog.com

generate_request_id はデフォルト true だった

最近 Envoy のドキュメントを読み直す機会があり,HTTP connection manager のパラメータを確認したところ,generate_request_id はデフォルト true だった.よって,明示的に generate_request_id を設定しなくても x-request-id ヘッダは付くことになる.むしろ「UUID4 の生成コスト」を考慮すると,ハイパフォーマンスを求める場面では false にするという内容も書いてあった.

(BoolValue) Whether the connection manager will generate the x-request-id header if it does not exist. This defaults to true. Generating a random UUID4 is expensive so in high throughput scenarios where this feature is not desired it can be disabled.

www.envoyproxy.io

検証環境

実際に検証環境を構築して,動作確認をした.今回は Docker Compose で「Envoy コンテナ(フロントエンド)」「Sinatra コンテナ(バックエンド)」を起動し,Sinatra で x-request-id ヘッダを表示するプロトタイプ実装にした.構成図は以下のようになる.

f:id:kakku22:20200227130721p:plain

envoy.yaml の一部を以下に載せておく.generate_request_id の部分を修正しながら動作確認をした.

  • generate_request_id なし
  • generate_request_id: true
  • generate_request_id: false
(中略)

filter_chains:
- filters:
  - name: envoy.http_connection_manager
    config:
      codec_type: auto
      stat_prefix: ingress_http
      generate_request_id: true

(中略)

バックエンド実装の一部を以下に載せておく.Sinatra でヘッダを取得するために request.env を使った.

get '/' do
  { HTTP_X_REQUEST_ID: request.env['HTTP_X_REQUEST_ID'] }.to_json
end

なお,検証環境の設定などは全て GitHub に置いてある.自由に使ってもらえればと!

github.com

検証結果

計3パターンを検証した.挙動は(当然ながら)ドキュメントの通りだったけど,実際に確認できて良かった.

  • generate_request_id なし : x-request-id ヘッダ "あり"
  • generate_request_id: true : x-request-id ヘッダ "あり"
  • generate_request_id: false : x-request-id ヘッダ "なし"
# `generate_request_id` なし
$ curl http://localhost:8080
{"HTTP_X_REQUEST_ID":"6075e8ae-9b4a-4ffd-b820-8113307bfd61"}

$ curl http://localhost:8080
{"HTTP_X_REQUEST_ID":"ed90417b-711c-4d7a-93b7-9f710864706e"}

# `generate_request_id: true`
$ curl http://localhost:8080
{"HTTP_X_REQUEST_ID":"cae38cdb-ac80-45c2-8de1-ca1d3463f762"}

$ curl http://localhost:8080
{"HTTP_X_REQUEST_ID":"e313e08f-4542-4214-b10b-8dce9fad7b5f"}

# `generate_request_id: false`
$ curl http://localhost:8080
{"HTTP_X_REQUEST_ID":null}

$ curl http://localhost:8080
{"HTTP_X_REQUEST_ID":null}

まとめ

個人的な検証記事だけど,Envoy の generate_request_id パラメータの動作確認をした.Docker Compose を使って簡単に検証環境を作れるのは便利だし,今後もドキュメントを読んで終わりにするのではなく,積極的に検証する気持ちを大切にしていく!

Mackerel : グラフアノテーションを登録するパターン集

新機能ではないけど,Mackerel の「グラフアノテーション機能」は便利でよく使っている.メトリクスの推移を分析するときに,同じタイミングで発生したイベント情報(デプロイ/キャンペーン開始/テレビ放送など)と紐付けることができる.実際にアノテーションを登録するときに「個人的によく使うパターン」がいくつかある.今回はパターンごとに「アノテーションを登録するスニペット」を整理しておく.

mackerel.io

コンソールを使う

アドホックにアノテーションを登録する場合は,1番簡単な「コンソール」を使う.とは言え,以下の画像のように「グラフの時刻部分をドラッグする」のは気付くにくく,Mackerel 初学者には使ってもらえなさそう.

f:id:kakku22:20200225115147p:plain

mkr コマンドを使う

シェルからアノテーションを登録する場合は,mkr コマンドを使う.mkr annotations を使うと簡単に登録できる.--from--to に指定するタイムスタンプは「UNIX 時間(エポック秒)」にする.

$ mkr annotations create --title 'アノテーション (Title) from mkr' \
  --description 'アノテーション (Description) from mkr' \
  --from $(date +%s) \
  --to $(date +%s) \
  --service 'xxxxx'

github.com

f:id:kakku22:20200225113343p:plain

Mackerel Client を使う

Ruby と Go で実装したアプリケーションからアノテーションを登録する場合は「Mackerel Client」を使う.例えば,mackerel-client-rubymackerel-client-go はグラフアノテーションをサポートしている(他の Client も使えると思う).以下は mackerel-client-rubypost_graph_annotation() を使っている.

require 'mackerel-client'
require 'time'

client = Mackerel::Client.new(mackerel_api_key: ENV['MACKEREL_API_KEY'])

client.post_graph_annotation(
  {
    title: 'アノテーション (Title) from Ruby Client',
    description: 'アノテーション (Description) from Ruby Client',
    from: Time.now.to_i,
    to: Time.now.to_i,
    service: 'xxxxx'
  }
)

github.com

f:id:kakku22:20200225113404p:plain

Mackerel API を使う

Python で実装したアプリケーションからアノテーションを登録する場合は「Mackerel API」を直接 requests で実行して使う.

import json
import os
import requests
import time

params = {
    'title': 'アノテーション (Title) from API',
    'description': 'アノテーション (Description) from API',
    'from': int(time.time()),
    'to': int(time.time()),
    'service': 'xxxxx'
}

requests.post(
    'https://api.mackerelio.com/api/v0/graph-annotations',
    json.dumps(params),
    headers={'Content-Type': 'application/json', 'X-Api-Key': os.environ['MACKEREL_API_KEY']}
)

mackerel.io

f:id:kakku22:20200225113425p:plain

まとめ

完全網羅を目指すのではなく,個人的によく使う以下のパターンに限定して「グラフアノテーションを登録するスニペット」を整理した.今まで何度も同じ検索を繰り返していたため,今後アノテーションを使うときは kakakakakku blog を見るぞ!便利!

  • アドホックなら「コンソール」を使う
  • シェルならmkr コマンド」を使う
  • Ruby と Go なら「Mackerel Client」を使う
  • Python なら「Mackerel API」を使う

Jupyter Notebook でパフォーマンスを計測するなら %%timeit と書こう

Python で実装した処理のパフォーマンスを計測するときに,たまに timeit を使っている.timeit は処理を繰り返し実行することにより,精緻な計測結果を把握できる.今までは timeitimport して直接実行していたけど,よく調べてみると,Jupyter Notebook (IPython) で「マジックコマンド」として %%timeit と書けることを最近知った.import も必要なく使えて便利だった!

ipython.readthedocs.io

%%time を使う

実行回数は「ループ数」「繰り返し数」から決まる.%%timeit にオプションを指定せず実行すると「ループ数」は適切な精度が得られるように自動的に決まる仕組みになっている.なお「繰り返し数」はデフォルト 7 となる.Jupyter Notebook で簡単なサンプルコードを実装し,セルの頭に %%timeit と書いて実行すると,以下のようになる.mean ± std と書いてあるため「平均実行時間」「標準偏差」だとわかる.なお「ループ数」1000000 になっていた.

%%timeit
sum(range(100))
1.42 µs ± 48.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%time -n を使う

%%timeit にオプション -n を指定すると「ループ数」を固定できる.今回は 10 に固定して実行した.

%%timeit -n 10
sum(range(100))
1.25 µs ± 30 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%time -n -r を使う

%%timeit にオプション -r を指定すると「繰り返し数」を固定できる.今回は 2 に固定して実行した.

%%timeit -n 10 -r 2
sum(range(100))
1.28 µs ± 66.9 ns per loop (mean ± std. dev. of 2 runs, 10 loops each)

Jupyter Notebook と VS Code

以下の記事に書いた通り,最近は Jupyter Notebook を実装するときに Visual Studio Code (VS Code) を使う機会が増えている.

kakakakakku.hatenablog.com

今回の %%timeit の検証も VS Code を使っている.

f:id:kakku22:20200224232058p:plain

まとめ

Jupyter Notebook でパフォーマンスを計測するときは「マジックコマンド」として %%timeit を使うと便利だった.オプションにより「ループ数」「繰り返し数」を固定できるけど,デフォルトで適切な精度が得られるように実行されるため,あまり気にしなくて良さそうに思う.

MySQL のサンプルデータセット "world" データベース と "world_x" データベースの差とは?

MySQL 関連の検証をしたり,データベース未経験者に SQL を教えたりするときに,よく MySQL 公式の「world データベース」を使っている.「国と都市と言語」を対象にしたデータセットとなり,とても使いやすいと思う.例えばRedash ハンズオンでも使っている.

Example Databases

MySQL 公式のデータセットは他にもある.個人的に簡単に試すなら worldを使って,ある程度の規模を必要とするなら employeesakila を使っている.

  • employee data(従業員データ)
  • world database(国データ)
  • world_x database(国データ)
  • sakila database(DVD レンタルショップデータ)
  • menagerie database(ペットデータ)

dev.mysql.com

world と world_x の差は?

今まで「world_x データベース」を使ったことがなく,そもそも worldworld_x の差を把握できていなかった.今回は world_x を調べた結果をまとめる.なお,MySQL 8.0.19 を検証環境にした.

結論から整理すると,データサイズに差はなく,スキーマに差がある.具体的には world_x データベースでは一部のデータが「JSON 型」で,Document Store として使えるようになっている.よって,MySQL の「X DevAPI」を試せる環境となり,データベース名も world_x になっていると推測できる.やはり,ハンズオンなど簡単に使うなら今後も world で良さそう.なお,テーブル情報の概要は以下に載っている.

テーブル

まず,worldworld_x のテーブルを確認する.world「計3テーブル」となり,world_x テーブルは「計4テーブル」となる.とは言え,どちらも「国」「都市」「言語」のデータを持っている.

mysql> SHOW TABLES FROM world;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+
3 rows in set (0.01 sec)

mysql> SHOW TABLES FROM world_x;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city              |
| country           |
| countryinfo       |
| countrylanguage   |
+-------------------+
4 rows in set (0.01 sec)

レコード

次に,worldworld_x のレコード数を確認する.既に書いた通り,データサイズに差はなかった.

  • world データベース
    • city : 4079 レコード
    • country : 239 レコード
    • countrylanguage : 984 レコード
  • world_x データベース
    • city : 4079 レコード
    • country : 239 レコード
    • countryinfo : 239 レコード
    • countrylanguage : 984 レコード

SQL も残しておく.

-- world
SELECT COUNT(*) FROM world.city;
SELECT COUNT(*) FROM world.country;
SELECT COUNT(*) FROM world.countrylanguage;

-- world_x
SELECT COUNT(*) FROM world_x.city;
SELECT COUNT(*) FROM world_x.country;
SELECT COUNT(*) FROM world_x.countryinfo;
SELECT COUNT(*) FROM world_x.countrylanguage;

スキーマ

SHOW COLUMNS を使って worldworld_x のスキーマを確認する.

world は一般的なスキーマとなり,city.CountryCodecountrylanguage.CountryCodecountry.Code の外部キーになっている.個人的にも使い慣れたスキーマと言える.

world スキーマ

mysql> SHOW COLUMNS FROM world.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   | MUL |         |                |
| District    | char(20) | NO   |     |         |                |
| Population  | int      | NO   |     | 0       |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world.country;
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Field          | Type                                                                                  | Null | Key | Default | Extra |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Code           | char(3)                                                                               | NO   | PRI |         |       |
| Name           | char(52)                                                                              | NO   |     |         |       |
| Continent      | enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') | NO   |     | Asia    |       |
| Region         | char(26)                                                                              | NO   |     |         |       |
| SurfaceArea    | decimal(10,2)                                                                         | NO   |     | 0.00    |       |
| IndepYear      | smallint                                                                              | YES  |     | NULL    |       |
| Population     | int                                                                                   | NO   |     | 0       |       |
| LifeExpectancy | decimal(3,1)                                                                          | YES  |     | NULL    |       |
| GNP            | decimal(10,2)                                                                         | YES  |     | NULL    |       |
| GNPOld         | decimal(10,2)                                                                         | YES  |     | NULL    |       |
| LocalName      | char(45)                                                                              | NO   |     |         |       |
| GovernmentForm | char(45)                                                                              | NO   |     |         |       |
| HeadOfState    | char(60)                                                                              | YES  |     | NULL    |       |
| Capital        | int                                                                                   | YES  |     | NULL    |       |
| Code2          | char(2)                                                                               | NO   |     |         |       |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
15 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world.countrylanguage;
+-------------+---------------+------+-----+---------+-------+
| Field       | Type          | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+-------+
| CountryCode | char(3)       | NO   | PRI |         |       |
| Language    | char(30)      | NO   | PRI |         |       |
| IsOfficial  | enum('T','F') | NO   |     | F       |       |
| Percentage  | decimal(4,1)  | NO   |     | 0.0     |       |
+-------------+---------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

MySQL Workbench で生成した ER も載せておく.

f:id:kakku22:20200217102133p:plain

world_x スキーマ

world_xcity.Infocountryinfo.doc など,「JSON 型」のカラムを持ったテーブルがある.MySQL を Document Store として使う検証環境として最適だと思う.

mysql> SHOW COLUMNS FROM world_x.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   |     |         |                |
| District    | char(20) | NO   |     |         |                |
| Info        | json     | YES  |     | NULL    |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.country;
+---------+----------+------+-----+---------+-------+
| Field   | Type     | Null | Key | Default | Extra |
+---------+----------+------+-----+---------+-------+
| Code    | char(3)  | NO   | PRI |         |       |
| Name    | char(52) | NO   |     |         |       |
| Capital | int      | YES  |     | NULL    |       |
| Code2   | char(2)  | NO   |     |         |       |
+---------+----------+------+-----+---------+-------+
4 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.countryinfo;
+--------------+---------------+------+-----+---------+-------------------+
| Field        | Type          | Null | Key | Default | Extra             |
+--------------+---------------+------+-----+---------+-------------------+
| doc          | json          | YES  |     | NULL    |                   |
| _id          | varbinary(32) | NO   | PRI | NULL    | STORED GENERATED  |
| _json_schema | json          | YES  |     | NULL    | VIRTUAL GENERATED |
+--------------+---------------+------+-----+---------+-------------------+
3 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.countrylanguage;
+-------------+---------------+------+-----+---------+-------+
| Field       | Type          | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+-------+
| CountryCode | char(3)       | NO   | PRI |         |       |
| Language    | char(30)      | NO   | PRI |         |       |
| IsOfficial  | enum('T','F') | NO   |     | F       |       |
| Percentage  | decimal(4,1)  | NO   |     | 0.0     |       |
+-------------+---------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

MySQL Workbench で生成した ER も載せておく.テーブル定義を確認すると world_x.city には外部キーの指定がないこともわかる.

f:id:kakku22:20200217102237p:plain

なお,「JSON 型」 だとデータ構造を確認しにくいため,実際のデータを JSON_PRETTY() で載せておく.city.Info はシンプルに Population(人口) のみとなる.countryinfo.docName(国名)government(政府情報)geography(地理情報) など,様々な情報が JSON に含まれている.

mysql> SELECT JSON_PRETTY(Info) FROM world_x.city WHERE CountryCode = 'JPN' AND NAME = 'Tokyo';
+-----------------------------+
| JSON_PRETTY(Info)           |
+-----------------------------+
| {
  "Population": 7980230
} |
+-----------------------------+
1 row in set (0.01 sec)

mysql> SELECT JSON_PRETTY(doc) FROM world_x.countryinfo WHERE JSON_EXTRACT(doc, '$.Code') = 'JPN' LIMIT 1;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| JSON_PRETTY(doc)                                                                                                                                                                                                                                                                                                                                                                                                                 |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {
  "GNP": 3787042,
  "_id": "00005de917d8000000000000006d",
  "Code": "JPN",
  "Name": "Japan",
  "IndepYear": -660,
  "geography": {
    "Region": "Eastern Asia",
    "Continent": "Asia",
    "SurfaceArea": 377829
  },
  "government": {
    "HeadOfState": "Akihito",
    "GovernmentForm": "Constitutional Monarchy"
  },
  "demographics": {
    "Population": 126714000,
    "LifeExpectancy": 80.69999694824219
  }
} |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

world_x クエリ : 東京都の Population(人口) を取得する

最後にクエリサンプルを載せておく.

まず,city テーブルから東京都の Population(人口) を取得する.今回は -> オペレータを使う.

mysql> SELECT Info->'$.Population' FROM world_x.city WHERE CountryCode = 'JPN' AND Name = 'Tokyo';
+----------------------+
| Info->'$.Population' |
+----------------------+
| 7980230              |
+----------------------+
1 row in set (0.01 sec)

world_x クエリ : 日本の Region(地域)Population(人口) を取得する

今度は日本の Region(地域)Population(人口) を取得する.world_x の場合はテーブルが分割されているため,country テーブルと countryinfo テーブルを JOIN する必要がある.今回は -> オペレータと ->> オペレータを使う.通常の char と JSON の中にある文字列を ON で紐付けた.

mysql> SELECT ci.doc->>'$.Code', ci.doc->'$.demographics.Population', c.Capital FROM world_x.country c INNER JOIN world_x.countryinfo ci ON c.Code = ci.doc->>'$.Code' WHERE c.Name = 'Japan';
+----------------------------------------------+---------------------------------------------------+---------+
| JSON_UNQUOTE(JSON_EXTRACT(c2.doc, '$.Code')) | JSON_EXTRACT(c2.doc, '$.demographics.Population') | Capital |
+----------------------------------------------+---------------------------------------------------+---------+
| JPN                                          | 126714000                                         |    1532 |
+----------------------------------------------+---------------------------------------------------+---------+
1 row in set (0.01 sec)

なお,JSON_EXTRACT()->JSON_UNQUOTE(JSON_EXTRACT())->> の話は昨日の記事にまとめてある.

kakakakakku.hatenablog.com

まとめ

MySQL 公式のデータセット worldworld_x の差を把握するために検証環境を構築して調べた.今後も SQL を教えたりする簡単な場面では world を使おうと思う.もし「JSON 型」などを検証する必要があったら world_x を使おうと思う.差を把握できて良かった!

MySQL で JSON 型からクオートを除去した文字列を取得するなら ->> を使う

MySQL(5.7 以降)で「JSON 型」のカラムから指定したキーを取得する場合 JSON_EXTRACT() を使う.もしくは JSON_EXTRACT() と同じ動作をする -> オペレータを使うこともできる.以下に members テーブルの info カラムから name キーを取得する SQL を載せる.なお,今回は MySQL 8.0.19 を検証環境にした.

-- サンプルテーブルを作成する
mysql> CREATE TABLE members ( info json DEFAULT NULL );
Query OK, 0 rows affected (0.10 sec)

-- サンプルデータを追加する
mysql> INSERT INTO members VALUES ('{"id": 1, "name": "kakakakakku"}');
Query OK, 1 row affected (0.10 sec)

-- サンプルデータを確認する
mysql> SELECT * FROM members;
+----------------------------------+
| info                             |
+----------------------------------+
| {"id": 1, "name": "kakakakakku"} |
+----------------------------------+
1 row in set (0.01 sec)

-- JSON_EXTRACT() を使って取得する
mysql> SELECT JSON_EXTRACT(info, '$.name') FROM members;
+------------------------------+
| JSON_EXTRACT(info, '$.name') |
+------------------------------+
| "kakakakakku"                |
+------------------------------+
1 row in set (0.01 sec)

-- -> を使って取得する
mysql> SELECT info->'$.name' FROM members;
+----------------+
| info->'$.name' |
+----------------+
| "kakakakakku"  |
+----------------+
1 row in set (0.01 sec)

JSON_UNQUOTE() と組み合わせる

実際に JSON_EXTRACT() を使って文字列を取得すると "kakakakakku" のように「ダブルクオート付き」になる.例えば CHARVARCHAR と比較するときに困るため,JSON_UNQUOTE() と組み合わせるとクオートを除去できる.

-- JSON_UNQUOTE(JSON_EXTRACT()) を使って取得する
mysql> SELECT JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) FROM members;
+--------------------------------------------+
| JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) |
+--------------------------------------------+
| kakakakakku                                |
+--------------------------------------------+
1 row in set (0.01 sec)

->> オペレータを使う

JSON_UNQUOTE(JSON_EXTRACT()) を使うと JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) のように長くなってしまう.箇所が多いと SQL の可読性に影響する可能性もある.そこで JSON_UNQUOTE(JSON_EXTRACT()) と同じ動作をする ->> オペレータを使うこともできる.MySQL 5.7.13 で導入された ->> オペレータの存在は今まで気付いていなかった!

-- ->> を使って取得する
mysql> SELECT info->>'$.name' FROM members;
+-----------------+
| info->>'$.name' |
+-----------------+
| kakakakakku     |
+-----------------+
1 row in set (0.01 sec)

ドキュメントを読むと ->>equivalent to JSON_UNQUOTE(JSON_EXTRACT()) と書いてある.

名前 説明
-> Return value from JSON column after evaluating path; equivalent to JSON_EXTRACT().
->> Return value from JSON column after evaluating path and unquoting the result; equivalent to JSON_UNQUOTE(JSON_EXTRACT()).

dev.mysql.com

まとめ

  • MySQL で「JSON 型」のカラムから指定したキーを取得する場合は JSON_EXTRACT()JSON_UNQUOTE() を組み合わせる
  • JSON_UNQUOTE(JSON_EXTRACT()) と同じ動作をする ->> オペレータを使うと短く書ける

関連記事

4年前に JSON_EXTRACT() など JSON を操作する関数を調査した記事も参考になった.とは言え,MySQL 5.7 も MySQL 8.0 も JSON を操作する関数が増えているため,もう1度調査をしておくと良さそう.

kakakakakku.hatenablog.com