6/7/2017
このエントリーをはてなブックマークに追加

clojure.java.jdbcのマルチインサートは引数に注意

ClojureのJDBCラッパーであるclojure.java.jdbcを用いて、MySQLなどのRDBにデータを高速インサートしたい場合、insert-multi!を用いて一括インサートすると良い。しかし、この関数は引数の与え方によってパフォーマンスが大きく変わってくるので注意しなければならない。

insert-multi!はインサートしたいデータに対応する引数として、

  1. マップのシーケンスを渡す: ({:foo 1 :bar -1} {:foo 2 :bar -2} ...)
  2. ベクタ+ベクタのシーケンスを渡す: [:foo :bar] ([1 -1] [2 -2] ...)

の2種類の方法がある。実は、方法2のほうが高速である。

clojure.java.jdbc 0.7.0-alpha3を使用して検証する。たとえば次のテーブル、

CREATE TABLE example (
  id INT NOT NULL AUTO_INCREMENT,
  val1 INT,
  val2 INT,
  PRIMARY KEY (id))
ENGINE = InnoDB;

に対して、1,000レコードのマルチインサートを100回行ってみる。

方法1を用いてマルチインサートを行うと、

(time
 (dotimes [_ 100]
   (jdbc/insert-multi! db-spec :example
     (map (fn [n]
            {:val1 n, :val2 (inc n)})
          (take 1000 (repeatedly #(rand-int 100)))))))
;; "Elapsed time: 11948.622137 msecs"

だが、方法2を用いてマルチインサートを行うと、

(time
 (dotimes [_ 100]
   (jdbc/insert-multi! db-spec :example
     [:val1 :val2]
     (map (fn [n]
            [n (inc n)])
          (take 1000 (repeatedly #(rand-int 100)))))))
;; "Elapsed time: 6845.109233 msecs"

となり、この例では方法2のほうが1.74倍速い。

ソースコードを読むとわかるが、方法1ではマップごとに別々のSQLが生成され、まとめて実行される。これでもinsert!を繰り返すよりは十分速いが、方法2ではひとつのSQLにデータを全て入れて実行されるため、本来の意味でのマルチインサートとなっており、さらに高速である。

一方で、方法1ではAUTO INCREMENTなidの値が:generated_keyとして返ってくるが、方法2では何も返らない、という違いもある。使用シーンに応じて適切に使い分けるのが良い。

Tags: Clojure