last update: 2014/10/30

2014/09/03

golangのpqドライバでRedShiftにつないでちょっとハマった話

YAPCが終わり、夏も終わり、みなさんいかがお過ごしでしょうか。れもんです。季節感を先取りしてデザインを冬っぽい感じにしました。

さて、最近仕事でRedshiftをつかっておりまして、ご存じのない方に簡単に説明するとAWSで使えるマネージドで列指向でシェアードナッシングな分散データベースっていう説明でいいんでしょうか。あんまり定義の分野に明るくないのでとりあえずぼくはそんな認識で使っております。MPP! MPP! みたいな感じです。

で、「Goで行こう」というダジャレなのかどうかよくわからないことをぶち上げたというのと、BIツール的なところは比較的言語の作りに依存してどうこうみたいなハマリかたをしなさそうで新しい言語を投入するには無難だなというところで、Redshiftを叩くのをgolangでやっております。

RedshiftはPostgreSQLのドライバで接続可能なので、golangに限らずPostgreSQL用のデータベースドライバを使ってプログラムを書けば良いです。ホントかよと思ってPerlで最初に実験したときはDBD::Pgでいけましたし、chefレシピで冪等にテーブルが存在するようにCREATE TABLEするレシピを書いたときはRubyのpgを使ってクエリを送って大丈夫だねーという確認をしていました。

ということで! 実際にRedshiftを叩いてWebUIでグラフとか出すところをgolangとpqドライバで書き始めて、最初は順調だったんですが、db.Query("SELECT 〜", date) みたいなところでconnection refusedのエラーがでて必ず死ぬ問題に直面しました。クエリは仮ですが、ざっくりこんなコードです。db.Queryでいきなり死んで仕事にならん感じでした。

today := time.Now().Format("2006-01-02")
rows, err := db.Query(
    `SELECT user_id, access_at FROM access_log WHERE access_at = $1`,
     today,
)
if err != nil {
    fmt.Println("error: ", err)
    return nil, err
}
defer rows.Close()

result := []interface{}
for rows.Next() {
    var days_count, count int
    if err = rows.Scan(&days_count, &count); err != nil {
        fmt.Println(err)
    } else {
        // write something cool business logic here...

    }
}
return result, nil

とりあえず社内IRCの #go で「ふぇーRedshiftがエラーになるーたすけてー」みたいな感じで呟いてみたところ、fujiwaraさんが出てきてどれどれみたいな感じになり、2人でソース読んだりRedshiftじゃなくてPostgreSQLの挙動を確認した結果、Redshiftは無名PREPAREステートメントに対応してないっぽいというところまでわかりました。ちなみにfujiwaraさんは前職でPostgreSQLをずっと使ってたそうで、とりあえずPostgreSQLでわかんないことあったらfujiwaraさんに聞くとなんとかなるのでいつもお世話になっています。

で、pqドライバのソースを読んで確認した結果はこんな感じでした。

まず、pqドライバはPrepared Statementをサーバ側で処理させるようになっていて、db.Queryでコネクションから直接args付きでPrepared Statementを投げると無名Prepared Statement(stmt.nameが空文字のPrepared Statement)でクエリを投げます。

このときPostgreSQLなら普通に無名Prepared Statementが使えるらしいのですが、どうやらRedshiftは無名Prepared Statementに対応していないようで、コネクションが切られてしまいます。

じゃあどうすればよいかというと、普通にdb.Prepareでstmtを取得すればOKです。pqドライバはdb.PrepareでStmtオブジェクトを生成してクエリを飛ばす場合はコネクションIDから一意のStatement名を生成します。名前付きでPrepared Statementを実行するのでエラーになりません。

today := time.Now().Format("2006-01-02")
stmt, err := db.Prepare(`SELECT user_id, access_at FROM access_log WHERE access_at = $1`)
if err != nil {
    fmt.Println("prepare error: ", err)
    return nil, err
}
defer stmt.Close()

var rows *sql.Rows
rows, err = stmt.Query(today)
if err != nil {
    fmt.Println("query error: ", err)
    return nil, err
}
defer rows.Close()

result := []interface{}
for rows.Next() {
    var days_count, count int
    if err = rows.Scan(&days_count, &count); err != nil {
        fmt.Println(err)
    } else {
        // write something cool business logic here...

    }
}
return result, nil

とまぁそんな感じで、golangでdb.Queryでargs付きのクエリをRedshiftに投げようとして撃沈してしまった方は、普通にdb.Prepareでstmtを作って回避する方法をお試しください。

comments powered by Disqus