React.js & Next.jsの勉強メモ
概要
React.js & Next.js超入門 第2版 を読んでちまちま写経していたが、 firebaseのところが現行のversion9とは結構乖離が大きくなっていたので、差分をメモしておく。 (買ってから1年以上放置してたのがそもそも悪い)
差分
初期化
書籍内
import firebase from 'firebase' const firebaseConfig = {..} if ( firebase.apps.length == 0) { firebase.initializeApp(firebaseConfig) }
現行
import { initializeApp } from 'firebase/app'; const firebaseConfig = {...} const app = initializeApp(firebaseConfig); export default app;
firestoreオブジェクトの取得
書籍内
import firebase from 'firebase' const db = firebase.firestore()
現行
import { getFirestore } from 'firebase/firestore'; const db = getFirestore(app)
ドキュメントの取得(複数)
書籍内
db.collection('mydata').get().then((snapshot) => { snapshot.forEach((document) => { const doc = document.data() ... }) })
現行
import { getFirestore, collection, getDocs } from 'firebase/firestore'; const cols = collection(db, 'mydata') getDocs(cols).then((snapshot) => { snapshot.forEach((document) => { const doc = document.data() ... }) })
ドキュメントの取得(単一)
書籍内
import firebase from 'firebase' import { useRouter } from 'next/router' const router = useRouter() if ( router.query.id != undefined ) { db.collection('mydata').doc(router.query.id).get().then(ob => { const data = ob.data() ... }) }
現行
import { getFirestore, collection, doc, getDoc } from 'firebase/firestore' import { useRouter } from 'next/router' const router = useRouter() if ( router.isReady && router.query.id !== undefined ) { const docRef = doc(db, 'mydata', router.query.id) getDoc(docRef).then(ob => { const data = ob.data() ... }) }
追加
書籍内
import firebase from 'firebase' const db = { name: name, mail: mail, age: age } db.collection('mydata').add(ob).then(ref => { ... })
現行
import { getFirestore, collection, addDoc } from "firebase/firestore" const ob = { name: name, mail: mail, age: age } addDoc(collection(db, 'mydata'), ob).then(ref => { router.push('/fire') })
削除
書籍内
import firebase from 'firebase' import { useRouter } from 'next/router' const router = useRouter() db.collection('mydata').doc(router.query.id) .delete().then(ref => { ... })
現行
import { getFirestore, doc, getDoc } from 'firebase/firestore' import { useRouter } from 'next/router' const router = useRouter() const docRef = doc(db, 'mydata', router.query.id) deleteDoc(docRef).then(ref => { ... })
検索
書籍内
import firebase from 'firebase' // 検索対象の文字列 const name = '...' // 完全一致 db.collection('mydata').where('name', '==', name).get() .then(snapshot => { snapshot.forEach((document) => { ... }) }) // 前方一致 db.collection('mydata').orderBy('name').startAt(name).endAt(name + "\uf8ff") .then(snapshot => { snapshot.forEach((document) => { ... }) })
現行
import { getFirestore, collection, query, where, orderBy, startAt, endAt, getDocs } from 'firebase/firestore/lite'; // 検索対象の文字列 const name = '...' // 完全一致 const cols = collection(db, 'mydata') const q = query(cols, where('name', '==', name)) getDocs(q).then(snapshot => { snapshot.forEach(document => { ... }) }) // 前方一致 const cols = collection(db, 'mydata') const q = query(cols, orderBy('name'), startAt(name), endAt(name + "\uf8ff")) getDocs(q).then(snapshot => { snapshot.forEach(document => { ... }) })
ちなみに startAt(name) endAt(name + "\uf8ff")
は範囲指定を意味している。
\uf8ff
はコードポイントの最大値であり、上記はnameに続く文字が 0x0000〜0xf8ff
の範囲にあることを示している。
例えば name: hoge
のとき、
- 先頭4文字が
hoge
に一致し、 - 5文字目が
0x0000〜0xf8ff
の範囲にある、つまり終端や全ての文字に一致する
という条件になるらしい。
Docker + SolrCloud
概要
以下の書籍の内容をベースにDocker + Solr8でSolrCloud環境を構築した。
構成
- ZooKeeper
- Solr
- Nginx
- 2つのSolrへリクエストを振り分けるLB
- 片方が死んでいる場合は生きている方に流す
コード
https://github.com/sota2502/japan_address
使用するデータ
郵便番号データダウンロード - 日本郵便 を使用した。 全都道府県で約12万件。 Shift-JIS → UTF-8 への変更とスキーマに合わせたヘッダーを追加している。
ZooKeeper
いきなりSolrCloudの構成を作る前にZooKeeperに慣れておくとよい。
docker-compose-zk.yml
version: '3.7' x-zoo-service: &zoo-service image: zookeeper:3.5 restart: always networks: - solr services: zoo1: <<: *zoo-service container_name: zoo1 hostname: zoo1 ports: - 2181:2181 env_file: ./zoo.env environment: ZOO_MY_ID: 1 volumes: - ./data/zoo1/data:/data - ./data/zoo1/datalog:/datalog zoo2: <<: *zoo-service container_name: zoo2 hostname: zoo2 ports: - 2182:2181 env_file: ./zoo.env environment: ZOO_MY_ID: 2 volumes: - ./data/zoo2/data:/data - ./data/zoo2/datalog:/datalog zoo3: <<: *zoo-service container_name: zoo3 hostname: zoo3 ports: - 2183:2181 env_file: ./zoo.env environment: ZOO_MY_ID: 3 volumes: - ./data/zoo3/data:/data - ./data/zoo3/datalog:/datalog networks: solr:
各々のZooKeeperのデータを永続化するため、/data
,/datalog
をVolumeにマウントする。これによりrestartなどをしてもデータは失われない。
ZooKeeperのクラスタを起動。
$ docker-compose -f docker-compose-zk.yml up -d
ZooKeeperのクライアントを用意する。Macであればhomebrewでインストールするのが簡単。
$ brew install zookeeper
zoo1
に接続してznodeを作成。
$ zkCli -server localhost:2181 create /hoge fuga Connecting to localhost:2181 WATCHER:: WatchedEvent state:SyncConnected type:None path:null Created /hoge
先ほど作ったznodeのデータを zoo2
から読み出す。
$ zkCli -server localhost:2182 get /hoge Connecting to localhost:2182 WATCHER:: WatchedEvent state:SyncConnected type:None path:null fuga
SolrのCollection用Configsetの作成
_default
のConfigsetをベースに郵便番号を管理するためのConfigsetを作成する。これはCollectionを作るときに使う。
以下のコマンドを実行して、テンポラリなコンテナを作って、その中から _default
Configsetsを取り出す。
$ docker create --name mysolr solr:8.5.2 $ docker cp mysolr:/opt/solr/server/solr/configsets/_default ./configsets $ docker rm mysolr
この中で solrconfig.xml
、 schema.xml
を編集していく。
特別なポイントはUUIDの設定をしているところ。 Solrのドキュメントを更新するためにはUniqueKeyが必要になる。しかし、郵便番号データにはUniqueKeyに適したものが存在しない。(同一の郵便番号の市町村が複数存在するケースがある) この制約はCSVを編集してIDを採番することでも満たすことは可能。 しかし、UUIDを採番するようにしておくほうが手間は省ける。
solrconfig.xml
差分は以下の通り。
38a39 > <schemaFactory class="ClassicIndexSchemaFactory" /> 1111c1112 < <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:true}" --- > <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:false}" 1117a1119,1124 > <updateRequestProcessorChain name="uuid" default="true"> > <processor class="solr.UUIDUpdateProcessorFactory"> > <str name="fieldName">uuid</str> > </processor> > <processor class="solr.RunUpdateProcessorFactory" /> > </updateRequestProcessorChain>
<schemaFactory class="ClassicIndexSchemaFactory" />
は managed-schema
の代わりに schema.xml
を使用する場合に記述する。
add-unknown-fields-to-the-schema
の UpdateRequestProcessorChain
のdefault値をfalseにしておく。これはschemalessモードを使わないための設定。
uuid
の UpdateRequestProcessorChain
はUUIDを採番するための設定。
schema.xml
_default
Configsetsには schema.xml
は存在しない。
これは managed-schema
を schema.xml
にリネームして手動編集する。
差分は以下のようになる。これは郵便番号データに対応したfieldの定義の追加と、不要なfieldの定義の削除、UniqueKeyの変更をしている。
111a112,127 > <field name="uuid" type="uuid" multiValued="false" indexed="true" required="true" stored="true"/> > <field name="jis" type="string" indexed="true" required="true" stored="true"/> > <field name="postcode" type="string" indexed="true" required="true" stored="true"/> > <field name="postcode5" type="string" indexed="true" required="true" stored="true"/> > <field name="prefecture_kana" type="text_ja" required="true"/> > <field name="city_kana" type="text_ja" required="true"/> > <field name="town_area_kana" type="text_ja" required="true"/> > <field name="prefecture" type="text_ja" indexed="true" required="true" stored="true"/> > <field name="city" type="text_ja" indexed="true" required="true" stored="true"/> > <field name="town_area" type="text_ja" indexed="true" required="true" stored="true"/> > <field name="is_one_town_by_multi_postcode" type="boolean" required="true"/> > <field name="is_need_small_area_address" type="boolean" required="true"/> > <field name="is_chome" type="boolean" required="true"/> > <field name="is_multi_town_by_one_postcode" type="boolean" required="true"/> > <field name="updated" type="boolean" required="true"/> > <field name="update_reason" type="boolean" required="true"/> 113d128 < <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" /> 117,122d131 < <!-- If you don't use child/nested documents, then you should remove the next two fields: --> < <!-- for nested documents (minimal; points to root document) --> < <field name="_root_" type="string" indexed="true" stored="false" docValues="false" /> < <!-- for nested documents (relationship tracking) --> < <field name="_nest_path_" type="_nest_path_" /><fieldType name="_nest_path_" class="solr.NestPathField" /> < 169c178 < <uniqueKey>id</uniqueKey> --- > <uniqueKey>uuid</uniqueKey> 1012a1022,1023 > <fieldType name="uuid" class="solr.UUIDField" indexed="true" /> > 1023d1033 <
SolrCloud環境の構築
docker-compose
で ZooKeeper
、 Solr
、 Nginx
のSolrCloud環境を構築する。
docker-compose
docker-compose.yml
やその他に必要なファイルは以下のようになる。
docker-compose.yml
version: '3.7' x-zoo-service: &zoo-service image: zookeeper:3.5 restart: always networks: - solr x-solr-service: &solr-service image: solr:8.5.2 environment: ZK_HOST: zoo1:2181,zoo2:2181,zoo3:2181 SOLR_JAVA_MEM: -Xms1g -Xmx1g -Xmn512m networks: - solr depends_on: - zoo1 - zoo2 - zoo3 services: zoo1: <<: *zoo-service container_name: zoo1 hostname: zoo1 env_file: ./zoo.env environment: ZOO_MY_ID: 1 volumes: - ./data/zoo1/data:/data - ./data/zoo1/datalog:/datalog zoo2: <<: *zoo-service container_name: zoo2 hostname: zoo2 env_file: ./zoo.env environment: ZOO_MY_ID: 2 volumes: - ./data/zoo2/data:/data - ./data/zoo2/datalog:/datalog zoo3: <<: *zoo-service container_name: zoo3 hostname: zoo3 env_file: ./zoo.env environment: ZOO_MY_ID: 3 volumes: - ./data/zoo3/data:/data - ./data/zoo3/datalog:/datalog solr1: <<: *solr-service container_name: solr1 volumes: - ./data/solr1:/var/solr solr2: <<: *solr-service container_name: solr2 volumes: - ./data/solr2:/var/solr nginx: image: nginx:1.19.2 container_name: nginx volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - 8080:80 networks: - solr depends_on: - solr1 - solr2 networks: solr:
Solrのデータを永続化するために /var/solr
をVolumeにマウントする。
zoo.env
ZOO_SERVERS=server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 ZOO_4LW_COMMANDS_WHITELIST=mntr,conf,ruok
nginx.conf
user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { upstream solr { server solr1:8983; server solr2:8983; } server { listen 80; client_max_body_size 20M; location / { proxy_pass http://solr; } } }
コンテナを起動。
$ docker-compose up -d
起動が完了したら http://localhost:8080/solr/ にブラウザでアクセスする。SolrのWeb管理画面が表示される。 Cloud → Graph でSolrノードの繋がりが見える。
ConfigsetsのUpload
[SolrのCollection用Configsetの作成] で作ったConfigsetsをアップロードする。 これは一度zipで一つにしたものをAPIで送る。
$ cd configsets/conf $ zip -r japan_address.zip * $ curl -X POST --header "Content-Type:application/octet-stream" \ --data-binary @japan_address.zip \ "http://localhost:8080/solr/admin/configs?action=UPLOAD&name=japan_address"
ZooKeeperコンテナに対して2181ポートをマッピングしておいて、 zkCli
で ls
コマンドを実行するとConfigsetsに対するznodeができているのが確認できる。
(上記のdocker-compose.ymlではポートをマッピングしていないので、これを行う場合には ports
の記述を追加する必要がある)
$ zkCli -server localhost:2181 ls /configsets
Collectionの作成
http://localhost:8080/solr/#/~collections:Solr管理コンソール からCollectionを作成する。 今回はシャード1、レプリカ2なので以下のように入力する。
項目 | 値 |
---|---|
name | japan_address |
configset | japan_address |
numShards | 1 |
replicationFactor | 2 |
Add Collection
をクリックして画面をリロードするとCollectionが作成されたのが確認できる。
CSVデータのインポート
以下のコマンドを実行してCSVデータをインポートする。これは結構時間がかかる。
$ curl -H 'Content-type: text/csv; charset=utf-8' \ --data-binary @ken_all.csv \ 'http://localhost:8080/solr/japan_address/update?commit=true'
検索の実行
Curlコマンドか Solr管理画面 でクエリを実行すると結果が返ってくるのが見える。
$ curl 'http://localhost:8080/solr/japan_address/select?q=*:*'
フェールオーバーの確認
現在は2つのSolrにNginxがリクエストを振り分けている状態。 ここから片方のSolrが落ちた状態でもリクエストを処理していることを確認する。
以下のコマンドで solr2
のコンテナを停止させる。
$ docker-compose stop solr2
Solr管理画面のCloud → Tree で片方が停止しているのが把握できる。
Curlで何回かクエリを実行して、いずれもレスポンスが返ってきているのが確認できる。
WWW::Curl::Easyでファイルをアップロード
目的
WWW::Curl::EasyでファイルをPOSTでアップロードしたい。 ただし、ファイルは巨大になるケースを想定して、一旦メモリに読み込むのを避けたい。
コード
use strict; use warnings; use WWW::Curl::Easy; my $file = 'hoge.csv'; my $size = -s $file; open my $fh, '<', $file or die 'Cannot open file'; my $url = 'http://example.com/sample'; my @headers = ( "Content-type: text/csv; charset=utf8" ); my $curl = WWW::Curl::Easy->new(); $curl->setopt(CURLOPT_POST, 1); $curl->setopt(CURLOPT_URL, $url); $curl->setopt(CURLOPT_POSTFIELDSIZE_LARGE, $size); $curl->setopt(CURLOPT_READDATA, \$fh); $curl->setopt(CURLOPT_HTTPHEADER, \@headers); if ( $curl->perform != 0) { die 'fail to upload'; } my $code = $curl->getinfo(CURLINFO_HTTP_CODE); print "Code: $code"; close $fh;
参考
https://stackoverflow.com/questions/9485157/wwwcurl-how-to-upload-post-large-files
YAPC::Tokyo 2019 に参加した感想
とりあえず雑でも感想を書いておく。後にすると絶対書かない。
メッセージングのプロトコル
https://speakerdeck.com/lyokato/yapc-japan-tokyo-2019-metusezingufalsepurotokoru
- 発表後の本人のMediumのエントリ を読むと発表の意図がわかりやすい。確かに、特定の技術に対して安易にマイナスなイメージをつけないようにされていて、その点はすごくよかった。
- どうすれば標準化がうまくいって広まったんだろう?慎重になりすぎてたんだろうか?
- スライドの文字が小さくて読みづらく、マイクが離れていたので聞きづらかったので、情報を得るのがしんどかった。。
ランチセッション Garuda:汎用コネクティビティ プラットフォーム Perlをサイエンスの世界につなぐ
- 創薬はすごい手間も時間もかかるし、そこに非エンジニアのリソースをバンバン投入したいので、そういうところで重宝されるんだろうな。そういった用途で非エンジニア向けにはすごく需要高そう。
ランチセッション 20歳の僕が経験したPerlエンジニアへの道
- 「やりたい」「やりたくない」だけで選ばず、やることによる理由をポジティブに捉えてるのがよかった。
- 知らない言語の現場に放り込まれるというのはきっとあるだろうし、自分の学習プロセスを構築するのにはいい経験。
綱渡りバッチ脱出大作戦
https://speakerdeck.com/shinpeim/gang-du-ribatutituo-chu-da-zuo-zhan
- 直近で全cronサーバーの入れ替えのため、全バッチが何をやってどういう挙動をしているかを調査しないといけなかったので、苦労にものすごく共感した。。
- 成否やスケジューリングの管理をしたくてRundeckとか試してたけど、前者だけならhorensoだけで良さそう。後者に関しては冗長化の観点をすっぽり抜けていて「確かに」と思った。
- 前職もそうだったけど、過度にcronに依存するシステムはつらい。。
- プレゼンが聞きやすかった。
Wazuhを利用した大統一サーバ監査基盤
https://speakerdeck.com/pyama86/wazuhwoli-yong-sita-da-tong-sabajian-cha-ji-pan
- セキュリティインシデントの調査は本当にすごく手間だし時間もかかるよね。。
- またToB要件で顧客情報を扱うケースに遭遇したら入れておきたい
エンジニアリング組織論への招待 -2つのDXと技術的負債-
https://speakerdeck.com/hirokidaichi/2tufalsedxtoji-shu-de-fu-zhai-yapc-tokyo-2019
Perlでも分散トレーシングしたい!AWS::XRay による解析とその実装
https://speakerdeck.com/fujiwara3/yapc-tokyo-2019
- 新規でPerlでサービス作って、マイクロサービスを進めるときには使いたい。
- 今担当しているサービスはモノリスで、ここからマイクロサービスに切り分けてくこともないので、Devel::KYTProfで十分そう。
Dive into MySQL Error
https://speakerdeck.com/yoku0825/dive-into-mysql-error-yapc-tokyobaziyon
Erlangことはじめ
Erlangことはじめ
erlangを書くことになったので、初歩的なスクリプトの実行からテストの実行までを勉強。
ディレクトリ構成
myproject ├── ebin │ └── mymath.beam # コンパイル時に生成される ├── rebar ├── src │ └── mymath.erl └── test └── mymath_test.erl
rebarを持ってくる
Erlangのビルドツールであるrebarを持ってくる。コンパイルやテスト実行が簡単になる。
git clone https://github.com/rebar/rebar.git cd rebar make
コードを書く
フィボナッチ数、階乗を返す関数を定義したモジュールを書く。
src/mymath.erl
-module(mymath). -export([fib/1, fact/1]). fib(X) when X < 0 -> throw({error, 'Negative Number'}); fib(0) -> 1; fib(1) -> 1; fib(X) -> fib(X - 1) + fib(X - 2). fact(X) when X < 0 -> throw({error, 'Negative Number'}); fact(0) -> 0; fact(1) -> 1; fact(X) -> X * fact(X - 1).
試しに実行してみる。
$ rebar compile $ erl -pa ebin Erlang/OTP 17 [erts-6.4] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false] Eshell V6.4 (abort with ^G) 1> mymath:fib(3). 3 2> mymath:fact(3). 6
テストを書く
mymath.erlに対する単体テストを書く。 テストのファイルはmymath_test.erlになる。 テストでは*_testのメソッドが呼ばれる。
test/mymath_test.erl
-module(mymath_test). -include_lib("eunit/include/eunit.hrl"). fib_test() -> ?assertThrow({error, 'Negative Number'}, mymath:fib(-1)), ?assertEqual(1, mymath:fib(0)), ?assertEqual(1, mymath:fib(1)), ?assertEqual(2, mymath:fib(2)), ?assertEqual(3, mymath:fib(3)), ?assertEqual(5, mymath:fib(4)). fact_test() -> ?assertThrow({error, 'Negative Number'}, mymath:fact(-1)), ?assertEqual(0, mymath:fact(0)), ?assertEqual(1, mymath:fact(1)), ?assertEqual(2, mymath:fact(2)), ?assertEqual(6, mymath:fact(3)), ?assertEqual(24, mymath:fact(4)).
テストを実行する
テストを実行してテストが通るか確かめる。
$ ./rebar eunit ==> myproject (eunit) All 3 tests passed.
参考
リーダブルコードを読んだ
今更ながら「リーダブルコード」を読んだ。
きっかけ
職場では開発したコードをチーム内のメンバーでお互いに見せ合い、内容に問題ないかをレビューしている。 あるとき別のメンバー同士でレビューをしてる際に、「リーダブルコードに書かれているから…」という指摘でもめた。 「リーダブルコードに書かれているから」だけではそもそもどういう意味を持っているのかがわからない。 リーダブルの定義は各々によって違うはず。 そのため指摘理由にはならないと思った。 しかし、そもそも何が書かれてるのか知らないのはよくないと思い読んでみた。
自分の立場
そもそも自分の立場はチーム内に限らず社内で書かれているコード全般のレビューを担当している。
所感
コードに美しさを求め、読みやすさやメンテナンス性を向上させようという意図は概ね同意。 ただ、内容というよりは、これを読んだチームメンバーの解釈には疑問を感じた。
コメント
コードだけでは意図が伝わりにくい箇所の要約や実装理由の説明目的に使うのは良いが、とにかく何でも説明すれば良いという勢いでダラダラ書くのはよくない。 コメントがあれば多少複雑でも構わないだろう、という免罪符にはなり得ない。 13章「短いコードを書く」という意識がごっそり抜け落ちているケースが多い。 「どうコメントを書くか」よりも「どうすればコメントを書かなくて済むか」をより意識すべきだと思う。
レビューにどう適用していくか
書く側がコードの美しさに意識を置くのは良いと思う。 しかし、レビューする側がこれを強制して、これを満たしていないコードを修正させるのはそれほど良いこととは思わない。 チーム内で開発するコードに美しさを求めるのであれば、レビューされる側・する側でこの本を読むかコーディングガイドラインを作って明文化するなどして共通認識を持つ必要がある。 そうでなければ、「リーダブルじゃない」という指摘ではどう修正すれば分からないし、個人に依存する部分が強いので、感情的な対立を生んで不毛な争いになるおそれがある。
自分としては、インデントがバラバラなどよほどひどい場合であれば別だが、カンマや演算子の位置が揃ってない程度であれば、レビューで指摘はしない。 これを指摘し始めると結構キリがなくなるケースがあり、そちらに気をとられて、本来もっと大切な無駄なロジックやバグを見落として指摘が漏れてしまうから。 また、コードの整形に関して指摘する場合は、例えば「カンマの位置を揃えてください」と単に指摘するよりも、「perltidyのような整形ツールにかけてください」という指摘をするようにしている。 これは、手動でカンマの位置を揃える手間に面倒さを感じて対応してもらえないケースをできるだけ回避するのと、ツールを使ってくださいということで指摘にある程度の客観性が生まれて受け入れてもらいやすいと考えているからである。
Slow Query 対策 - コンテンツの前後リンク
Slow Query 対策 - コンテンツの前後リンク
背景
例えば日記などのコンテンツのように、ユーザーとコンテンツが1対Nで存在するケースで、任意のコンテンツで前後のコンテンツのリンクを出したい。 そのためには、あるコンテンツのIDの近傍データにアクセスしてタイトルなどを取ってくる必要がある。
想定するテーブルは下記の通り。
CREATE TABLE `diary` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned NOT NULL, `title` varchar(100) NOT NULL, `body` text NOT NULL, `post_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_1` (`user_id`,`post_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
idx_1は任意のユーザーの日記を更新順で取得するためのものである。ユーザーの日記一覧で必要になるクエリで利用する。
user_idのばらつきにより既存クエリでは使われるインデックスが変わってくるので、今回のケースではユーザー数が1000、1ユーザーあたり約1000件の日記を保持するケースで考える。
既存クエリ
次の日記を取得する場合では、閲覧中の日記IDよりも多いものをWHERE句で限定して、IDでソートして最小のものを1件取得していた。
SELECT id, user_id, title FROM diary WHERE user_id = 1 AND id > 498740 ORDER BY id ASC LIMIT 1;
EXPLAIN結果は次の通り。
mysql> EXPLAIN SELECT id, user_id, title FROM diary WHERE user_id = 1 AND id > 498740 ORDER BY id ASC LIMIT 1; +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+ | 1 | SIMPLE | diary | ref | idx_1 | idx_1 | 4 | const | 1005 | Using where; Using filesort | +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+
filesortが発生しており、これがソート対象の件数によってはSlow Queryになることがある。
改善案
サブクエリを利用して、指定したIDより大きくて最小のものを求め、それをWHERE句で指定するようにする。
SELECT id, user_id, title FROM diary WHERE id = (SELECT MIN(id) FROM diary WHERE user_id = 1 AND id > 498740);
EXPLAIN結果は次の通り。
mysql> EXPLAIN SELECT id, user_id, title FROM diary WHERE id = -> (SELECT MIN(id) FROM diary WHERE user_id = 1 AND id > 498740); +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------------------------+ | 1 | PRIMARY | diary | const | PRIMARY | PRIMARY | 4 | const | 1 | | | 2 | SUBQUERY | diary | ref | PRIMARY,idx_1 | idx_1 | 4 | | 1005 | Using where; Using index | +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------------------------+
filesortが消えた
ベンチマーク
下記コードで上記2パターンについてベンチマークを行い比較した。 一番古い日記から順番に次の日記へ辿っていくケースである。
use strict; use warnings; use DBI; use Benchmark qw(:all); my $dbh = DBI->connect('dbi:mysql:sota', 'root', ''); my $sth1 = $dbh->prepare(q{ SELECT id, user_id, title FROM diary FORCE INDEX(idx_1) WHERE user_id = ? AND id > ? ORDER BY id ASC LIMIT 1 }); my $sth2 = $dbh->prepare(q{ SELECT id, user_id, title FROM diary WHERE id = (SELECT MIN(id) FROM diary WHERE user_id = ? AND id > ?) }); my $user_id = 1; my $start_id = do { my ($row) = $dbh->selectrow_array('SELECT MIN(id) FROM diary WHERE user_id = ?', undef, $user_id); +$row; }; cmpthese(10, { before => sub { my $id = $start_id; while ( $id ) { $sth1->execute($user_id, $id); my $row = $sth1->fetch; $id = $row ? $row->[0] : undef; } }, after => sub { my $id = $start_id; while ( $id ) { $sth2->execute($user_id, $id); my $row = $sth2->fetch; $id = $row ? $row->[0] : undef; } }, });
結果を以下に示す。
Rate before after before 17.2/s -- -17% after 20.8/s 21% --
改善案の方が高速になった。
余談1
実は、(user_id, id)のインデックスを張ればどちらも高速化される。 特に既存クエリはfilesortが発生しなくなるので、修正の必要がなくなる。
Rate after before after 35.7/s -- -4% before 37.2/s 4% --
ただ、今回遭遇した現場では、既に100近くのDBにshardingされており、総レコード数も数千万のオーダーのため、ALTER文が実行しづらい状況にあった。
余談2
前後リンクはSEO対策の一環で、検索クローラーの回遊性向上のために必要になった。 しかし、普段ユーザーがアクセスしないデータにまでアクセスが発生するようになったため、I/Oがかなり増えたのが痛い。 リンクに日記のタイトルを出さなくてもよいのであれば、色々ごまかし方はあるのだが…。