フューチャー技術ブログ

エンジニア歴6ヶ月による「SQLアンチパターン」書評(クエリのアンチパターン編)

読書感想文連載 の9冊目の記事です。

はじめに

今回はエンジニア界隈で名著と名高いBill Karwin著「SQLアンチパターン」を読んだので書評をかいていきたいと思います。

私が基本的な研修を終えて、現場に配属後に「SQLを触ろう」となった際に、先輩に「アンチパターンさえやらなきゃいいよ」とアドバイスされたことがこの本を読むきっかけだったので、同じような境遇の初心者の方が現場でSQLを触ることになった時、最低限避けるべき項目の雰囲気を掴む、容易に確認できる記事になれば幸いです。

本書のスタイルと本記事で注目すること

本書は「データベース論理設計のアンチパターン」「データベース物理設計のアンチパターン」「クエリのアンチパターン」「アプリケーション開発のアンチパターン」の4部からなりそれぞれアンチパターンが数種類紹介されています。アンチパターンごとに「目的」「アンチパターン」「アンチパターンの見つけ方」「アンチパターンを用いても良い場合」「解決策」に分けて解説されています。

本記事では上記のうち、初心者が一番触れることが多い「クエリのアンチパターン」の5つにフォーカスして、書評を書いていきたいと思います。

クエリのアンチパターン

1. フィア・オブ・ジ・アンノウン

〜NULLは数字でも文字でもない!!〜

プログラミング初心者がわかるようで分からないものの1つがNULLだと思います。

SQLのアンチパターンでもNULLに関して言及がありました。やりがちなミスとして、NULLを数値や文字列などと同列に扱ってしまう例などが上げられていました(NULLをスカラー式論理式で使うと想定外の結果を返す等)

実はこの本を読んでいたおかげでアンチパターンを回避できた場面がありました。やりたかったこととしては、金額がはいっている列の合計が欲しかったので以下のようなクエリを書いていました。

SELECT SUM(kingaku) FROM Table

ただ、kingaku列はNULLを許容する列だったのでもし1つでもNULLが設定されていた場合上記のクエリが返す結果はNULLになってしまいます。それをこの書籍で知識として得ていたおかげで、NULLを0として扱うように以下のクエリに事前に修正できました。

SELECT SUM(COALESCE(kingaku,0)) FROM Table

基本的なことかもしれませんが、先に知識としてインプットしておくことで「気づく」ことができたため、余裕があるときの勉強って大事だなと身に染みて感じました。

また、NULLがデータベースにあることで上記のような余計な気配りが必要になることから、NOT NULL制約を使ったり、他の値(0、-1)に置き換えてその手間を省く方法があるようですがそれに関しても推奨されていないようです。

2. アンビギュアスグループ

〜GROUP BYを使用した時、グループ内で一意でない値は取得できない!!〜

SQLを始めて勉強してクエリを書いている際に出てきたエラーに対して「なんでエラーが出るの?」と思った時に大抵の原因だったのがこのアンチパターンを踏んでいることでした。

例えば、動物の種類ごとに一番重いペットの名前を取得しようと(謎設定)以下のクエリを書いてもエラーになります。

SELECT name,MAX(weight),animal
FROM Pets
GROUP BY animal

これはanimalでグループ化しているのにも関わらず、nameが一意に特定できないからです(MAXは集約関数で一意に特定できる)

この意図としてはMAX(weight)と同じ行にあるnameを取得したいのですが、SQL側にはその意図を認識するすべがないため、どのnameを返すべきか分からずエラーになってしまいます。

本書の内容を踏まえて改善したSQLが以下になります。

この場合は、GROUP BYを使わずに以下のようなクエリを実装することで想定の値を取得できます。

SELECT p1.name,m.weight,m.animal
FROM pets p1
INNER JOIN
(SELECT MAX(p2.weight) AS weight,p2.animal
FROM pets p2
GROUP BY p2.animal) AS m
ON m.animal=p1.animal AND m.weight=p1.weight

基本的にはこのアンチパターンを踏んでいる際はエラーが起きるおかげで成果物として提出してしまうことはないと思いますが、私のようにエラーが出てきた際に「???」とならないように押さえておきたいですね。

3. ランダムセレクション

〜データをRAND関数を使ってソートすると無駄なコストがかかる!!〜

データをランダムに取得したいときはRAND関数を使うことが普通だと思っている方は知らないうちに非常にコストの高いクエリを作成してしまっているかもしれません。

というのも本書によるとRAND関数というものは手作業でソートしており、

これはテーブルスキャンと呼ばれる手法で多くの場合、全ての結果を一時的なテーブルとして保存し、物理的に行を入れ替える処理を行います。

データが大きければ大きいほどパフォーマンスが悪化するようなので今一度自身の実装を見直してみる必要があるかもしれません…。

ランダムに値を取得したい場面は今のプロジェクトではないため、今のところは私は頭の片隅に入れておく程度になると思いますが、パフォーマンスの観点を持って指摘を入れられるエンジニアは一段レベルが違い、チームの中で重宝される存在だと思っているので、そのような存在になれるよう温めておきたい知識でした。

4. スパゲッティクエリ

〜複雑なソースはどんなプログラミングでも悪!!〜

私は研修で初めに触れた言語がJavaだったのですが、その際にも「複雑なスパゲッティコードは良くない」というようなことを言われました。

「一度にやりたいことをできた方が良さそう!」と思って善意でひとまとめにしようとし、あくせくしながら実装し、いざ完成しても、深すぎるネストや、飛び飛びの処理のせいで、自分が何を考えて実装したのかすら分からなくなっている、といった経験はみなさんあると信じています。

やはり、SQLでも複雑な実装は他言語と同様ご法度とされているようですね。

SQLで複雑なクエリを実装すると、全ての行が結合してしまう「デカルト積」なるものが発生してしまい、実際の結果より多くの結果が返されることもあるようです。

SQLではそこまで複雑な実装をする必要が現場でないのですが、Javaで一度スパゲッティコードをしてしまい、指摘をされた際にも、そもそもコードの意図を説明することが大変だった経験もあるため、そういった副次的なコストも回避するためにも常にシンプルでスマートな実装をできるように心がけていきます!

5. インプリシットカラム

〜「とりあえず何でも頂戴!」はトラブルの元!!〜

列を指定せずにワイルドカード機能を使うSELECT、INSERTはテーブル定義が変わった時に元の想定と違う動きをすることがあるので避けるべき手法のようです。

もしワイルドカードを使用したクエリがテーブル定義変更により返す結果が変わって、クエリ実装時に省略した何倍ものタイプ数を費やして修正作業しなければならない場面を想像すると、丁寧に列の指定くらいは行わなければという強迫観念めいたものが生まれました(修正作業は実装の何倍もコストがかかるのはエンジニアの歴が浅くても痛いほど身に染みているので…)

また、以下のようにワイルドカードを使って取得すると余計なデータまで取ってきてパフォーマンス低下に繋がるようなので、「パフォーマンスを考慮できるかっこいいエンジニア」を目指している身としてはより気をつけたいポイントだと思いました。

SELECT * FROM Table

これは書籍には載っていなかったのですが、「往復するデータが増えてパフォーマンスが落ちる」という話題に関して現場での経験から得た、紹介したいアンチパターンがあり、それは「SQLを何度も呼び出すな」というものです。

現プロジェクトではJavaのServiceクラスからクエリを実行しDBアクセスする仕組みなのですが、参照するテーブルが同じだったり、検索条件が多少違うだけの場合、何度もSQLを呼び出すのではなく、一度のDBアクセスで必要な値を取り出してJava側で必要な値を場面に応じて参照することでデータベースとアプリケーションを往復するデータ量を減らし、パフォーマンス向上につなげることができます。

頭の片隅にでも置いておいて、少しでも質の良い成果物作成に貢献できたら幸いです!

まとめ

今まで自分が思いっきり踏んでいたアンチパターンから本書で知ることで未然に防げたアンチパターンまで載っていたため、非常に有用な書籍だったなと感じました。原著ではより細かいデータベース設定の中でより多くのアンチパターン回避方法について触れているので、今後SQLに触れていく同志にはぜひ読んでいただき、より質の高い成果物を作成していただければと思います。

本記事で少しでも情報を得ることができ、皆様の日々の開発の一助になれていれば幸いです。ご拝読ありがとうございました。