articles
Go で RDBMS のいろんな Utility を作ってみた
はじめに
この記事はGo2アドベントカレンダー2019の11日目の記事です。
今年の春ぐらいから、sqlparserを Go で書いていて、作った parser を使っていくつか Utility 系のツールを書きました。
xsqlparser
簡単に書いたものの紹介と、Parser を書いた上での感想・反省点なんかを書いていこうと思います。
SQL Parser
sqlparserをフルスクラッチでわざわざ書いている理由は、調べたなかだと、Go の SQL Parser は MySQL に対応した Parser ばかりで、 当時業務で使っていた PostgreSQL のクエリ「も」サポートした parser がぱっと見なかったということがまずあります。 (あとはシンプルに一度くらいは parser をフルスクラッチで書いてみたかったという気持ちもありますが)
そんな中、Rust の SQL Parser の実装を見つけて、これならなんとか Go で rewrite できつつカスタマイズできそうだなと思ったので、余暇の時間を使ってちょっとづつ実装することに決めました。
一番最初にクエリが parse できるようになったのが GW 明けくらい、あとは、元実装だと足りないところや、 それ以降は、 Rust 実装の AST 的にちょっと厳しかったところを標準の syntaxをみながら、そっちに寄せていくという作業をやっていました。
あとは、後々のアプリ実装が楽になるように、Go のast パッケージや、astutilなんかを参考にしならがら、utility を実装していました。 例えば、AST を書き換えるApplyの実装や、 AST からコメントを取得するCommentMapなんかを追加していく、といった作業を行ってました。
Example
せっかくなので、いくつか Parser の example を載せておきます。
基本の使い方
NewParser
に SQL の src(io.Reader
)とDialect
(今の所 GenericDialect
のみ)を渡して初期化し、 ParseFile()
を呼び出します。
(go/parser
の Parser と API が食い違っていて、ちょっと気持ち悪いですが、無視してください...)
これで SQL の AST を取得できます。
str := "SELECT * from test_table;"
parser, err := xsqlparser.NewParser(bytes.NewBufferString(str), &dialect.GenericSQLDialect{})
if err != nil {
log.Fatal(err)
}
file, err := parser.ParseFile()
if err != nil {
log.Fatal(err)
}
Inspect
Go の標準と同じような使い勝手のInspect
といったメソッドも実装してます。他にも、astutil
の Apply などを提供しているので、AST を編集することができます。(この機能を後述のquerydigest
で使っています)
// つづき
var list []sqlast.Node
sqlast.Inspect(file, func(node sqlast.Node) bool {
switch node.(type) {
case nil:
return false
default:
list = append(list, node)
return true
}
})
作ったツールたち
Parser だけではなくて、それを使っていくつかアプリケーションを書いたので簡単に紹介します。
pgmigrate
xmigrate
skeemaやsqldefの PostgreSQL 特化版のつもりで作りました。
SERIAL
なんかの型も対応しています。
詳しくは紹介記事を参照してください。
querydigest
querydigest
ISUCON に出た人はもしかしたら使ったことがあるかもしれないですが、pt-query-digestの Go のリライトです。
本家の pt-query-digest
ほどの機能(例えば tcpdump から統計をとる機能など)はないですが、一応それっぽくは動くと思います。
機能を削っているのと、pt-query-digest
がシングルコアで動くのに対し、並列に query を parse しているので、少なくとも体感的には高速に動作します。
ISUCON の過去問をやっているときに、500MB くらいの Slow Query Log を pt-query-digest
で解析しようとした時、なんとなく遅いなあと感じたので、
parser を使えばそこまで難しくなくリライトできそうだったこともあり、Go でリライトしてみました。
pt-query-digest
は、MySQL の Slow Query Log からどのクエリがどれくらいのリソースを消費したのかを集計し、統計をとって表示してくれるツールですが、その際に、SQL のパラメータを normalize した上で集計してくれます。
例えば、以下の 2 つのクエリは where
句のパラメータがそれぞれ異なっていますが、そこを無視し、同一のクエリと見なした上で集計をしてくれます。
SELECT * from users where user_id = 1;
SELECT * from users where user_id = 2;
僕の作ったquerydigest
でも同様のことを行っていて、その際に parser を使っています。
ただし、前述の開発モチベーションでも触れた通り、この parser はもともと PostgreSQL を第1サポートにしようとして開発したものなので、MySQL では parse できないクエリもまだまだあります。
parse できなかったクエリは集計から除外されるので、 pt-query-digest
の方が正確な結果が求められると思います。
あと、ISUCON あるあるで、DB に画像のバイナリを突っ込むといったような query の場合、parser が死んでしまうため、正常に動かないかと思います(これはあとで治そうかと思います)。
Parser を作ってみての感想
辛い。
もちろん parser の実装自体をちゃんとやるのは大変という話もあるのですが、いろんな DB の実装がある中、いい感じに対応するような parser を書くのはちょっと今の状態だと厳しいかなという気持ちがあります。
一応、標準の仕様というものは存在しているものの、
実際に動いている DB で使われているような SQL をパースしようと思ったら標準をサポートするだけでは足りないパターンが多いです。
例えば、CREATE INDEX
は標準には存在しておらず、 各 DB ベンダが実装しているものですが、今やサポートしていない DBMS はあまりみかけないため、実用的な SQL parser を作ろうと思ったらもちろん対応する必要があります。
標準以外のクエリをサポートしきるのは厳しく、今は中途半端に混ざった状態になってしまっているので、最初からターゲットを決めて、そこに寄り添った形で実装すればよかったなあという反省があります。
他にも、AST に埋め込む position 情報をもうちょい考えるべきだったとか、Parse
の 関数のシグネチャがおかしいとか、そういった細かい反省点は多々あるのですが、
とはいえ、parser を一から実装することにより得られた学びもあります。軽く例をあげると、
- SQL の文法に対する理解(なんだかんだいってこれが一番大きい気がする)と各 DB ベンダごとの実装差異
- パーサやコードジェネレータを実装する上でのテストの効果的な方法
- AST の設計についての知見
- Go にもある CommentMap や astutil を再実装することによる、Go のそれらのパッケージに対する理解
- などなど...
こういった学びを生かして次に生かしていければとは思います。(ただ、次にもし SQL の parser を書く機会があったら、多分 yacc とか使う気がします...)
まとめ
sql parser をフルスクラッチで実装し、それを利用したツールをいくつか作ってみました。 parser を使うことは多いのですが、作ることは滅多に無いので、 辛い点も多かったのですが、なんだかんだで作っているときは楽しかったし、動くものもいくつかできたので、供養のつもりで記事を書きました。
もし、バグやクエリのサポートの要望などがありましたら、Github にて報告してくださるとありがたいです。
以上です。読んでいただきありがとうございました。
About
技術ネタ中心にその他雑多なことを。