読者です 読者をやめる 読者になる 読者になる

アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

COUNT関数の使い方と速度検証

COUNT関数の基本的な使い方いろいろ

-- `table`テーブルの件数(全件)
SELECT COUNT(*) AS `count` FROM `table`;

-- `table`テーブルの件数(カラム指定)
-- この場合、指定されたカラムがNULLでないものをカウント。
-- プライマリキーなど、NULL不可のカラムを指定すれば全件となる
SELECT COUNT(`id`) AS `count` FROM `table`;

-- `hoge`フィールドの種類数
-- レコード数ではなく、重複を除いたカウント
SELECT COUNT(DISTINCT `hoge`) AS `count` FROM `table`;

-- `id`の値が100未満のレコードの件数
SELECT COUNT(*) AS `count` FROM `table` WHERE `id` < 100;
SELECT COUNT(`id` < '100' OR NULL) AS `count` FROM `table`;
SELECT COUNT(IF(`id` < '100', 1, NULL)) AS `count` FROM `table`;

要するに「COUNTの中身がNULLでないレコード全件」ということになります。

上記の通り、COUNTの中身に条件式を指定することもできますが、単純な条件ではWHERE句での指定に比べて圧倒的に遅いです。
複数の条件のカウントを行う場合は一考の価値ありで、問い合わせ回数と相談して速い方を採用する方がよいでしょう。

参考:【SQL複数の条件のcountを1回の問い合わせでおこなう - Softelメモ
https://www.softel.co.jp/blogs/tech/archives/3267

ついでの簡易速度検証

  • 登場人物
    • 約660万件のレコードを持ち、そこそこのINSERTと月1回のDELETEが発生するInnoDBテーブル `table`
    • auto_incrementのプライマリキーなカラム `id`
    • インデックスの貼られていないNULL不可なカラム `value_1`
    • インデックスの貼られていないNULL可なカラム `value_2`
    • インデックスの貼られていないNULL不可なカラム `hoge`
    • インデックスの貼られているNULL不可なカラム `fuga`
  • WHERE句で条件を指定しない編
SELECT COUNT(*) FROM `table`;
-- 1.19sec

SELECT COUNT(`id`) FROM `table`;
-- 1.34sec

SELECT COUNT(`value_1`) FROM `table`;
-- 1.91sec

SELECT COUNT(`value_2`) FROM `table`;
-- 1.94sec (※結果531万件)

普通に全件取得するなら「*」指定が最速でした。
(プライマリキー指定が最速だと思ってた…恥ずかしい)

  • WHERE句で条件を指定する編(インデックスあり)
SELECT COUNT(*) FROM `table` WHERE `id` > '4000000';
-- 0.87sec (※結果26万件)

SELECT COUNT(`id`) FROM `table` WHERE `id` > '4000000';
-- 0.87sec (※結果26万件)

SELECT COUNT(`value_1`) FROM `table` WHERE `id` > '4000000';
-- 0.99sec (※結果26万件)

SELECT COUNT(`value_2`) FROM `table` WHERE `id` > '4000000';
-- 1.00sec (※結果21万件)

あまり変わらず。とはいえこれも「*」で良さそう。

  • WHERE句で条件を指定する編(インデックスなし)
SELECT COUNT(*) FROM `table` WHERE `hoge` = '1';
-- 1.93sec (※結果21万件)

SELECT COUNT(`id`) FROM `table` WHERE `hoge` = '1';
-- 2.07sec (※結果21万件)

SELECT COUNT(`value_1`) FROM `table` WHERE `hoge` = '1';
-- 2.12sec (※結果21万件)

SELECT COUNT(`value_2`) FROM `table` WHERE `hoge` = '1';
-- 2.18sec (※結果17万件)

おっそ!!!
WHERE句のせいですね。
しかしまあ、WHERE句の内容でCOUNTの内容を変えるようなことは必要なさそう?

  • 単純な条件をつける場合のいろいろ比較
SELECT COUNT(`id`) FROM `table` WHERE `id` > '4000000';
-- 0.87 sec (※結果26万件)

SELECT COUNT(`id` > '4000000' OR NULL) FROM `table`;
-- 1.76 sec (※結果26万件)

SELECT COUNT(IF(`id` > '4000000', 1, NULL)) FROM `table`;
-- 1.75 sec (※結果26万件)

もちろん結果はすべて同じ。
このレベルの条件カウントであればおとなしくWHERE句で指定しましょう。

  • 種類数をカウントする場合の比較(インデックスあり・なし)
SELECT COUNT(DISTINCT `hoge`) FROM `table`;
-- 3.02 sec (※結果43件)

SELECT COUNT(*) FROM (SELECT `hoge` FROM `table` GROUP BY `hoge`) AS `tmp`;
-- 4.82 sec (※結果43件)

SELECT COUNT(DISTINCT `fuga`) FROM `table`;
-- 0.08 sec (※結果7,000件)

SELECT COUNT(*) FROM (SELECT `fuga` FROM `table` GROUP BY `fuga`) AS `tmp`;
-- 0.08 sec (※結果7,000件)

もうカウントの方法がどうのとかじゃなくてインデックス大事だねって話になりかねない差なのですが…
インデックスの貼られていないカラムの場合は差がわかりやすいですね。
このように単純な種類数を取得する場合、COUNT(DISTINCT ~)は有効なようです。

  • ちなみに

このへんの検証にするにあたり、EXPLAINはあまり役に立ちません。
「SELECT count(*) FROM `table`;」「SELECT count(`id`) FROM `table`;」が同じ結果、
「SELECT count(`value_1`) FROM `table`;」「SELECT count(`value_2`) FROM `table`」も同じ結果。
WHERE句で条件指定した場合に至ってはすべて同じ結果になります。

パフォーマンス改善の際はEXPLAINだけを頼らず、インデックスを意識したり実際にクエリを流してみたりすることもそれなりに必要だなーと改めて思いました。

参考:COUNT 関数を使ってMySQL のインデックスの基本を理解する - Slow Dance
http://d.hatena.ne.jp/LukeSilvia/20091102/p1