この記事は備忘録です
DB(例)
(id, first_name, last_name, email)
1 しのぶ 胡蝶 aaa@aa.com
2 杏寿郎 煉獄 bbb@bb.com
3 義勇 冨岡 ccc@cc.com
4 小芭内 伊黒 ddd@dd.com
5 天元 宇髄 eee@ee.com
6 蜜璃 甘露寺 fff@ff.com
モデル(例)
# User.rb
# id :int
# first_name :string
# last_name :string
# email :string
first_name_attr = "しのぶ"
last_name_attr = nil
email_attr = "fff@ff.com"
User.where(first_name: first_name_attr)
.or( User.where(last_name: last_name_attr) )
.or( User.where(email: email_attr) )
この場合、last_nameのところ、いらないっすよね〜でもこれって、sql的に
SELECT COUNT(*) FROM `users` WHERE (`users`.`first_name` = 'しのぶ' OR `users`.`last_name` IS NULL ... )
となるんですけど、orなのに IS NULL って処理いらないなって。(whereでandとかになるならもちろんいる)
そして、この処理が結構重いので、railsの仕組みを使って軽くしよう、というのが今回の目的です。
(※ まあこれは実践的ではないかもしれないのでメンバーと要相談ということでお願いいたします。。)
結論
nilだと IS NULL の処理が走ってしまうので[]にする。
last_name = (last_name == nil ? [] : last_name)
nilの場合は[]に変換することで
SELECT COUNT(*) FROM `users` WHERE (`users`.`first_name` = 'しのぶ' OR 1=0 ... )
なって処理を飛ばすことができ、大幅に早くなります。
もし開発する上で、表示するとき遅いし、一旦仮で早くしたいとかいう事情がある場合には結構有効かなと思います。
ただし、上記にもあるように、
「実践的ではないかも。。」「ちょっと気持ち悪いな〜」
みたいな意見があり、この実装は見送られておりますのでご注意を。
おそらく、シンプルにifで分岐する方がいいのかなという意見です。
ただし、sqlは一つに纏まるような記述が必要そうです。
実践的にはこうなった (2021/05/06 更新)
users = first_name.present? ? User.where(first_name: first_name_attr ) : User.none
users = users.or(User.where(last_name: last_name_attr)) if last_name.present?
users = users.or(User.where(email: email_attr)) if email.present?
モデル.noneで空のActiveRecord_Relationを取得
こうすることで、attrがない場合、sqlを無視することができるのでより無駄を減らせる。
これを応用し、現在携わるプロジェクトでは
1822.0ms => 3.9ms
これだけ改善した。
IS NULL がなくなるだけでこれだけ軽くなるんだなと改めて血肉になった思い。
sql改善例
改善前
User Load ( ms) SELECT `users`.* FROM `users` WHERE ((`users`.`first_name` IS NULL OR `users`.`last_name` = '冨岡' ) OR `users`.`email` IS NULL )
改善後
User Load ( ms) SELECT `users`.* FROM `users` WHERE `users`.`last_name` = '冨岡'