JDBCを利用したORマッパーのMyBatisとSQLDatabaseの組み合わせで、パフォーマンス
問題(スロークエリ)が発生し、解決まで導いたので、メモ。
問題は、char、varcharのフィールドに対する暗黙の型変換と文字コードが悪さをしていた。
MyBatisからSQLServerやSQLDatabaseを利用する際、設定を変更するか、DBの型をncharやnvarcharを使用しましょうという教訓です。
バージョン
・Java:1.8.0_242, vendor: AdoptOpenJDK,
・SpringBoot:2.2.4
・SQLDatabase:15.0.2000.2107
・mybatis-spring-boot-starter:2.1.1
ちょっと古いバージョンだったりするのだが、バージョンが上がっても、この問題にはぶつかる可能性は大いにある。
事象
1.対象テーブルの件数が20万件くらいから、スロークエリが発生
2.Where句に特定の条件を指定すると、30秒以上かかってしまい、クエリタイムアウトしてしまう。
3.Where句で使用するフィールドにはインデックスが貼ってあり、利いている状態。
4.DTUを調整したり、プランを上げたり、対応をしたがそれでも発生する。
5.アプリから実行するとタイムアウト(結果の返却に30秒以上)するが、sqlcmdを利用
して直接想定するクエリを実行すると約2秒くらいしかかからない。
6.Where句で指定するテーブルのフィールドの型はcharで、「A001」のような英数混合のコード値を保持している
データ件数が増えてきて、問題が発生したのだが、実行計画を見ると遅くなりそうな問題点はなかった。
そして、一番の問題点は、5のアプリからしか問題が発生せず、想定するクエリ(これ自体は正しい)を叩くと早いという、アプリやORマッパーの問題に見える点。
実際の問題は、JDBCとMyBatisの仕様に対して、charやvarcharの文字コードの問題だった。
解決の糸口
a.実行しているSQLを確認すると、プレースホルダーに対する変数の文字列(@が付いた変数)に対して、nvarcharと書いてあった。
→本来であれば、charやvarcharくらいなら分かるのだが、nがついているのは違和感。
b.charのフィールドに対して、charにするようにMyBatisの設定を確認したが、設定できるようなものはない。
→ORマッパーを使わず、PreparedStatamentを利用していたりすれば、文字列に対して、setStringやSetNStringという型の指定ができるのだが、MyBatisにはそのような設定がなかった
c.SQLServerのJDBC(SQLDatabaseでもSQLServerのJDBCを使う)の文字列の初期設定が、sendStringParametersAsUnicode=trueであるということが判明
→つまり、指定された文字列は基本Unicodeで扱うということ。Unicodeを使うなら、DBのテーブルのフィールドの型はncharやnvarcharとなる
aで使用したSQL 最近の10件を確認できるSQL
SELECT TOP(10)
,st.text
,last_execution_time
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY last_execution_time DESC
上記のことから、charとnvarcharの型不一致の問題ではないかという仮説を立てた。
対処方法は3つ考えられた。
①クエリにCASTを付ける
②sendStringParametersAsUnicode=falseにする
③該当テーブルのフィールドの型をcharからncharに変更する
結果的には、③を実行した。
①はクエリの修正となり、MyBatisのxmlファイル内の記述の変更を行うことになる。そして、リリース作業が必要となるので、作業コストがかかる。
②は全体に影響を与えてしまうため、問題のクエリ以外の全部のクエリをテストしないといけない。そして、リリース作業も必要となり、この中では影響範囲もコストも高くて、却下。
③は型の変更のみ。さらに、charからncharで、英数しか入らないフィールドなのは確定だったため、文字化けがしないかくらいの確認くらいで作業が済む。
これで、タイムアウトしていた、クエリが3秒くらいで返ってくるようになったので、まあまあ想定の範囲内となったかなと。
MyBatisを使うのならば、設計の時点で、charやvarcharは使わず、ncharやnvarcharのみでも良いかもしれない。