はじめに
「同じように作成したデータのはずなのに、開発環境とテストコード上(または本番環境)で並び順が異なってる!いったいなんで!?」と困っているRails開発者さんをときどき見かけます。
これはたとえば、開発環境でUserの一覧を見ると、
- Alice
- Bob
- Carol
と並んでいるのに、テストコード上で実行すると、
- Carol
- Bob
- Alice
の順で並んでいる、というような現象です。
この記事ではこういった問題が起きる原因(確認ポイント)と対処方法を説明します。
確認ポイント: ちゃんとorder(SQLのORDER BY句)を指定しているか?
たとえばRailsでUser.all
を実行すると、その裏では次のようなSQLが発行されています。
SELECT "users".* FROM "users"
実際に発行されるSQLは、Railsのログを見たり、User.all.to_sql
のようにto_sql
を付けたりすることで確認できます。
puts User.all.to_sql
#=> SELECT "users".* FROM "users"
上のコードで注目してほしい点は、orderを全く指定していないところです。
発行されるSQLにもORDER BY句が付いていません。
コンピュータを日常的に触っているプログラマの感覚だと、経験的に「データの並び順ってデータを作成した順番とかで勝手に並んでくれるんじゃないの?」と考えてしまうかもしれません。
が!!
SQLにおいてはその常識(経験則?)は当てはまりません。
ORDER BY句を指定しない場合の並び順は「不定」となります。
開発環境でUserの一覧を見たときに、いつも
- Alice
- Bob
- Carol
の順で並んでいたとしても、ORDER BY句を指定していなければそれは ただの偶然 です。
なので、明日突然、
- Carol
- Bob
- Alice
の順で並び始めたとしても、「データベースのバグだ!!」と叫ぶことはできません。
ORDER BY句を付けなければ、データベースはデータの並び順を 一切保証しない のです。
ゆえに、開発環境とテストコード上でデータの並び順が異なることがあっても、ある意味それが「SQLの仕様」ということになります。
対処方法: 必ずorderを指定しよう
というわけで、開発環境とテストコード上でデータの並び順が異なる場合の解決方法はもう明らかですね。
そうです。orderを明示的に指定すればいいのです。
さっそくコントローラのコードを次のように修正しましょう。
class UsersController < ApplicationController
def index
- @users = User.all
+ @users = User.order(:id)
end
end
orderを指定すれば、SQLにもORDER BY句が付与されます。
puts User.order(:id).to_sql
#=> SELECT "users".* FROM "users" ORDER BY "users"."id" ASC
こうすることで、データベースは毎回「usersテーブルのid順」にデータを並び替えることを保証してくれます。
開発環境もテスト環境でも、idがAlice, Bob, Carolの順に割り振られていれば、必ずAlice, Bob, Carolの順で並ぶようになります。
注意点: 並び順は必ずユニークになるようにしよう
orderを指定する際に注意してほしいことが一つあります。
それは「必ずユニークになるカラムの組み合わせを指定する」ということです。
たとえば、「Userの作成日時順に並び替えたい」という要件があった場合、次のようなコードは不完全なorderの指定方法になります。
User.order(:created_at)
なぜなら、(現実的にはほぼあり得ないものの)作成日時が全く同じUserが複数存在する可能性があるからです。
もし、データベース上のデータが次のようになっていたら、User.order(:created_at)
のままではBobとCarolの並び順が保証されません。
id | name | created_at | |
---|---|---|---|
1 | Alice | 2018/09/20 7:00:00 | |
2 | Bob | 2018/09/20 7:30:00 | ← 作成日時が同じ |
3 | Carol | 2018/09/20 7:30:00 | ← 作成日時が同じ |
4 | Dave | 2018/09/20 8:00:00 |
こういった場合は、「絶対ユニークであることが保証されるカラムの組み合わせ」をorderに指定します。
たとえば、さきほどの例であれば、「作成日時が全く同じだった場合はid順で並べる」という仕様が考えられます。
なぜなら、(Railsの場合は通常)idは必ずユニークなので、「作成日時もidも全く同じUser」は絶対に存在しないからです。
よって、次のようにorderを指定すれば、毎回必ず並び順が保証されます。
# 作成日時順、id順でUserを並び替える
User.order(:created_at, :id)
念のため、発行されるSQLも確認しておきましょう。
puts User.order(:created_at, :id).to_sql
#=> SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC, "users"."id" ASC
こうすれば作成日時が同じでも、必ずBob, Carolの順でデータが並ぶことが保証されます。
このように、orderを指定する場合は「これで絶対にユニークな並び順が保証されるか?」ということをしっかり確認しながら指定するようにしてください。
そうでなければ、「ある日突然テストコードや本番環境でデータの並び順が変わってしまう」といった問題が発生しかねません。
追記: 必ずユニークになるカラムの組み合わせを指定する例をもう一つ
この記事を公開したあとに、このようなリプライをもらいました。
idまで指定したことはないですねえ。
— daisuke furukawa (@mogya) September 20, 2018
仕様で作成順となっていたらそれ以上細かいことは気にしないし、テストのときにわざわざ同値のcreated_atを作ったりしないからかなあ。
うーん、なるほど。
上で説明した「作成日時が全く同じだった場合はid順で並べる」の例は、あくまで一つの例であり、本当に伝えたかったのは「ユニークな並び順が保証されるカラムの組み合わせを考えよう」ということでした。
また、作成日時が重複する可能性は限りなく小さいものの、「絶対ない」と言いきれないのであれば、idのように一意性を保証できるカラムを追加すべき、というのが僕のスタンスです。
が、おっしゃるように「わざわざそこまでする?」と思う気持ちは理解できます。
その点については持ち出した例が少し悪かったなあ、と反省しています😓
というわけで、もう一つ別の例を考えてみます。
たとえばこんなUserのデータがあったとします。
id | name | ||
---|---|---|---|
1 | Alice | alice.foo@example.com | |
2 | Bob | bob.foo@example.com | ← ユーザー名が同じ |
3 | Bob | bob.bar@example.com | ← ユーザー名が同じ |
4 | Carol | carol.foo@example.com |
このデータを「名前順に並べたい」という要件があった場合、次のようなコードではダメなのはもう明らかですよね。
User.order(:name)
そうです。同姓同名のユーザーがいるかもしれないからです。
実際、上のデータでは同名のBobさんが2人います。
order(:name)
だけだと、id=2のBobさんとid=3のBobさんの並び順は不定となります。
こういう場合は、nameに加えて他に一意性を保証してくれるカラムを追加する必要があります。
たとえば「usersテーブルのemail列はシステムログイン時に使用するので絶対にユニークであること(つまり、email列に一意制約が付けられていること)」が保証されているのであれば、次のようにしてnameとemailをorderに指定します。
# 名前順、email順でUserを並び替える
User.order(:name, :email)
こうすれば、毎回Alice, Bob(id=3)、Bob(id=2)、Carolの順で並ぶようになります。
もちろん、nameとidの順に並べるのもOKです(どの順番にするかはシステムの要件によります)。
# 名前順、id順でUserを並び替える
User.order(:name, :id)
name, id順であれば、毎回Alice, Bob(id=2)、Bob(id=3)、Carolの順で並びます。
いずれにしても、order(ORDER BY句)には一意性が保証されるようなカラムを指定することが重要です。
2019.9.30追記: 並び順がユニークでないとページングの結果もおかしくなるので要注意
当たり前と言えば当たり前なのですが、並び順がユニークでないとkaminariのようなgemを使ってページングするときもおかしな結果になります。
この「おかしな結果」というのは具体的にいうと、
- 1ページ目に出てきたAさんが2ページ目にも再度表示される
- 本来表示されるべきBさんが、どのページを開いても表示されない
といった結果になることです。
「並び順は不定でも、AさんとBさんはいずれかのページに1回ずつ表示されるはず」と高をくくっていると痛い目を見る(Aさんが2回出現するかも!Bさんは一度も出現しないかも!!)ので注意してください。
参考: 標準SQLの規格を確認する
標準SQLはISOやJISで規格が決まっています。
ORDER BY句を指定しない場合に並び順が不定になることもこの規格に記載されています。
JIS X 3005-2:2015 データベース言語SQL 第2部:基本機能(SQL/Foundation)
4.15.3 導出表
(中略)
<問合せ式>によって指定される表の行の順序付けは,<ORDER BY 句>を直に含む<問合せ式>に対してだけ保証される。
MySQLやPostgreSQLといったメジャーなRDBMSはこの標準SQLにほぼ準拠するように作られています。
そのため、データの並び順に関するこの考え方はRDBMSを問わず適用可能です。
たとえば、PostgreSQLの公式ドキュメントにもこの内容は明記されています。
並べ替えが選ばれなかった場合、行は無規則な順序で返されます。 そのような場合、実際の順序は、スキャンや結合計画の種類や、ディスク上に格納されている順序に依存します。 しかし、当てにしてはいけません。 明示的に並べ替え手続きを選択した場合にのみ、特定の出力順序は保証されます。
まとめ
というわけで、この記事では開発環境とテストコード上(または本番環境)でデータの並び順が異なる場合の原因と対処方法を紹介しました。
開発環境で動かしたときは毎回同じ並び順で並ぶのだから、当然テスト環境でも同じように並ぶはず!!・・・とは考えずに、ちゃんとユニークになるorderを指定して、実行環境を問わず毎回並び順が保証される実装を心がけるようにしましょう😉
あわせて読みたい
orderの付け忘れを防止するgemがあるので、これを使うのもうっかりミスを防ぐ一つの手ですね。
Railsでorder忘れを防止するためのgemを作りました - PIYO - Tech & Life -
謝辞
標準SQLの規格やPostgreSQLのドキュメントは同僚の @mah_lab と @tkawa が見つけてくれました。どうもありがとう〜😄