DjangoのQuerySetで行数を数える時のcount()とlen()の違い

最近受けたコードレビューで、len()で行数を取得していた部分をcount()に変更してほしいというコメントをもらった。ORMとDjangoの初心者なのでStack Overflowをいくつか漁ってみたところ、行数を確認する時はなるべくcountを使って取得するのが推奨されているようだ。では、その理由が何なのか確認してみよう。
現在自分のサービスにいるUserの数を調べるとしよう。
(ユーザーが100万人、1000万人いるような環境でこういうことを遊び半分でやると、会社のDevOpsの先輩方に呼び出されるので注意しよう。)
qs = User.objects.all()\n\n# querysetのcount()を使用する方法がある。\ndef user_count_by_count():\n return qs.count()\n\n# lenで確認する方法\ndef user_count_by_len():\n return len(qs)この時、count()の場合はRDBMS上でSELECT COUNT(*) FROM Userが実行され、そのカウント値がPythonに渡される。
len()の場合は、RDBMS上でSELECT * FROM Userを一度実行し、Pythonに渡された結果をlen()で数えることになる。
よく「行数を数える必要があるならcount()を使え」と言われる。その理由は、len()でカウントを代用しようとすると、フェッチする際に時間がO(N)だけかかるからだ。また、この結果をWebサーバーのメモリに格納する際にストレージにO(N)が発生し、コピーにかかる時間のせいで時間にもO(N)が追加で発生する。さらに加えて、len()が実行される時間も発生する。この2つのプロセスはインフラ環境によって異なるが、countの方が2倍ほど速いそうだ。
しかし、len()を使う方がcount()より有利な場合もある。
qs = User.objects.all()\n\ndef prefer_len_to_count():\n # User情報の中にtelephoneフィールドがあると仮定し、その情報をリストにまとめる。\n telephones = [p.telephone for p in qs]\n # その情報で何か作業をして...\n ...\n # qsの個数を取得する(用途は各自で考えてみよう)\n len(qs)上記のようにQuerySetをフェッチして使用する状況であれば、lenの方がほんの少し有利だ。理由は、count()の場合はRDBMSに2回(フェッチで1回、count()で1回)アクセスしなければならないからだ。
DevOpsの先輩方に怒られそうな言葉だが、ものすごく多くの(この基準はインフラ状況によって大きく異なる)行を数えるのでなければ、そのままcountを使っても差し支えない。
当然、テンプレートでもcountの使用が推奨される。
以下のcountの方が、
{{ some_queryset.count }}以下のlenよりも推奨される。
{{ len(some_queryset) }}参考: stackoverflow.com/questions/14327036/count-vs-len-on-a-django-queryset