ちょっと硬派なコンピュータフリークのBlogです。

カスタム検索

2010-03-09

InnoDBでCOUNT()を扱う際の注意事項あれこれ。

InnoDB使MyISAMCOUNT()InnoDBMyISAMCOUNT()InnoDBCOUNT()COUNT()COUNT()

COUNT(*)COUNT(col)COUNT(1)

COUNT(*)COUNT(col)COUNT(*)COUNT(col)colNULLcolCOUNT()
mysql> CREATE TABLE num_tbl (a INT) ENGINE InnoDB;
Query OK, 0 rows affected (0.44 sec)

mysql> INSERT INTO num_tbl VALUES(0),(1),(2),(3),(NULL);
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> SELECT COUNT(*) FROM num_tbl;
+----------+
| COUNT(*) |
+----------+
|        5 |
+----------+
1 row in set (0.00 sec)

mysql> SELECT COUNT(a) FROM num_tbl;
+----------+
| COUNT(a) |
+----------+
|        4 | <--- 値がNULLのカラムがカウントされない。
+----------+
1 row in set (0.00 sec)

mysql> SELECT COUNT(100/a) FROM num_tbl;
+--------------+
| COUNT(100/a) |
+--------------+
|            3 | <--- ゼロ除算の結果はNULLなのでカウントされない。
+--------------+
1 row in set (0.00 sec)
このことはチューニングする上で意味があることなので覚えていて貰いたい。 ちなみに、COUNT(*)という表記は慣習的なものであり、実はアスタリスクを指定する意味はあまりない。COUNT(1)を指定しても同じ結果が得られるのである。

よく用いられるCOUNT()高速化対策法

COUNT()を高速化する方法として最もよく利用されるのが、トリガを使う方法であろう。行数を保持するテーブルを作成して、COUNT()したいテーブルにトリガを設定し、INSERTされるごとに行数を+1、DELETEされるごとに-1することで、別テーブルで行数をメンテしようというものである。これにより、COUNT(*)をする代わりに行数を保持しているテーブルから1行のレコードをフェッチするだけで済むため、行数を数える処理が格段に高速化するというわけだ。ただし、この手法では、既にKazuhooku氏がブログで紹介しているように、INSERT時にオーバーヘッドが生じるという問題がある。また、わざわざ別テーブルとトリガをいちいち作成するのは面倒であり、運用の手間が増えてしまう。このようなデメリットがあるとはいえ、それでもCOUNT()を高速化したいんだよ!という場合には、非常に有効な対策である。 また、テーブルがキューとして使われている場合のように、主キーが整数型で途中に欠番がないような場合には、MIN()/MAX()を活用することでCOUNT()と同様の結果を得られるという方法がある。こちらについてはKamipo氏がブログで紹介しているのでそちらを参照されたい。

MyISAMにおけるCOUNT()の限界

MyISAMを利用しているといつでもCOUNT()が高速であるか?というと、そういうわけではない。実はMyISAMを利用している場合であっても、高速なのはSELECT COUNT(*) FROM tblというように、テーブル全体の行数を取得するようなクエリでないと速くないのである。例えば、COUNT(col)でNULL値を含む可能性のあるカラムを指定すると、テーブルスキャンが必要になるため、COUNT()は別に速くもなんともない。 GROUP BYを利用した場合も同様で、MyISAMテーブルが保持している「テーブル全体の行数」は役に立たないため、スキャンが必要になる。以下は、MySQL公式サンプルであるWorldデータベースを用いた例である。テーブルスキャンが発生していることが分かる。
mysql> EXPLAIN SELECT COUNT(*) FROM Country GROUP BY Continent;
+----+-------------+---------+------+---------------+------+---------+------+------+---------------------------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra                           |
+----+-------------+---------+------+---------------+------+---------+------+------+---------------------------------+
|  1 | SIMPLE      | country | ALL  | NULL          | NULL | NULL    | NULL |  239 | Using temporary; Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+---------------------------------+
1 row in set (0.01 sec)
WHERE
MyISAM
NULLCOUNT(col)

GROUP BY

WHERE
 

使

MyISAMInnoDBCOUNT()使WHEREGROUP BYInnoDBInnoDBInnoDB COUNT(*)
CREATE TABLE t1 (
  a bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  b int(11) DEFAULT NULL,
  c tinyint(4) DEFAULT NULL,
  d date DEFAULT NULL,
  e varchar(200) DEFAULT NULL,
  f varchar(200) DEFAULT NULL,
  g varchar(200) DEFAULT NULL,
  h varchar(200) DEFAULT NULL,
  i varchar(200) DEFAULT NULL,
  PRIMARY KEY (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
COUNT(*)1000COUNT(*)
mysql> explain select count(*) from t1;
+----+-------------+-------+-------+---------------+---------+---------+------+----------+-------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows     | Extra       |
+----+-------------+-------+-------+---------------+---------+---------+------+----------+-------------+
|  1 | SIMPLE      | t1    | index | NULL          | PRIMARY | 8       | NULL | 10044347 | Using index | 
+----+-------------+-------+-------+---------------+---------+---------+------+----------+-------------+
1 row in set (0.07 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
| 10000000 | 
+----------+
1 row in set (1 min 3.30 sec)
1
innodb_buffer_pool_size=1GB I/O bINTcTINYINTdDATEeVARCHAR(200) CHARACTER SET utf8
mysql> select count(*) from t1;
mysql> explain select count(*) from t1;
+----+-------------+-------+-------+---------------+------+---------+------+----------+-------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows     | Extra       |
+----+-------------+-------+-------+---------------+------+---------+------+----------+-------------+
|  1 | SIMPLE      | t1    | index | NULL          | c    | 2       | NULL | 10042706 | Using index | 
+----+-------------+-------+-------+---------------+------+---------+------+----------+-------------+
1 row in set (0.01 sec)

+----------+
| count(*) |
+----------+
| 10000000 | 
+----------+
1 row in set (2.82 sec)
MySQL
13 COUNT(*)COUNT()NULLNULLCOUNT(c)COUNT(*)COUNT(a)caNOT NULLCOUNT(a)COUNT(*) 使NOT NULLNULLCOUNT(b)bINTdDATEeVARCHAR(200) CHARACTER SET utf8
mysql> select count(b) from t1;
+----------+
| count(b) |
+----------+
| 10000000 | 
+----------+
1 row in set (3.03 sec)

mysql> select count(d) from t1;
+----------+
| count(d) |
+----------+
| 10000000 | 
+----------+
1 row in set (3.02 sec)

mysql> select count(e) from t1;
+----------+
| count(e) |
+----------+
| 10000000 | 
+----------+
1 row in set (14.08 sec)
bdc
InnoDB
TABLE: name test/t1, id 0 263, columns 12, indexes 5, appr.rows 10179472
  COLUMNS: a: DATA_INT DATA_UNSIGNED DATA_BINARY_TYPE DATA_NOT_NULL len 8; b: DATA_INT DATA_BINARY_TYPE len 4; c: DATA_INT DATA_BINARY_TYPE len 1; d: DATA_INT DATA_BINARY_TYPE len 3; e: type 12 len 600; f: type 12 len 600; g: type 12 len 600; h: type 12 len 600; i: type 12 len 600; DB_ROW_ID: DATA_SYS prtype 256 len 6; DB_TRX_ID: DATA_SYS prtype 257 len 6; DB_ROLL_PTR: DATA_SYS prtype 258 len 7; 
  INDEX: name PRIMARY, id 0 456, fields 1/11, uniq 1, type 3
   root page 131076, appr.key vals 10179472, leaf pages 360335, size pages 360768
   FIELDS:  a DB_TRX_ID DB_ROLL_PTR b c d e f g h i
  INDEX: name b, id 0 457, fields 1/2, uniq 2, type 0
   root page 131077, appr.key vals 8533926, leaf pages 16443, size pages 18944
   FIELDS:  b a
  INDEX: name c, id 0 458, fields 1/2, uniq 2, type 0
   root page 131078, appr.key vals 9, leaf pages 9847, size pages 11313
   FIELDS:  c a
  INDEX: name d, id 0 459, fields 1/2, uniq 2, type 0
   root page 131079, appr.key vals 2541345, leaf pages 16343, size pages 18816
   FIELDS:  d a
  INDEX: name e, id 0 460, fields 1/2, uniq 2, type 0
   root page 131080, appr.key vals 8703960, leaf pages 103311, size pages 119360
   FIELDS:  e a
c9800bd16000e100000COUNT()InnoDB NULLEXPLAINtype=ALLInnoDB
mysql> select count(i) from t1;
+----------+
| count(i) |
+----------+
| 10000000 | 
+----------+
1 row in set (1 min 34.41 sec)

Covering Index

GROUP BYWHERECovering Index dCovering Index
mysql> SELECT FLOOR(YEAR(d)/100)*100 AS drange, COUNT(*) FROM t1 GROUP BY drange HAVING drange IS NOT NULL;;
+--------+----------+
| drange | COUNT(*) |
+--------+----------+
|   1000 |     1283 | 
・・・出力省略・・・
|   9900 |     1272 | 
+--------+----------+
90 rows in set (10.95 sec)

mysql> SELECT COUNT(*) FROM t1 WHERE d BETWEEN '1990-01-01' AND '2000-01-01';
+----------+
| COUNT(*) |
+----------+
|     1034 | 
+----------+
1 row in set (0.00 sec)
(c,d)
Covering Index
mysql> SELECT FLOOR(YEAR(d)/100)*100 AS drange, COUNT(*) FROM t1 WHERE c = 100 GROUP BY drange HAVING drange IS NOT NULL;
・・・出力省略・・・
(c,d)(c)使c=100GROUP BYCovering Index ALTER稿Covering Index

COUNT()COUNT()COUNT()COUNT()

GROUP BY使使
CREATE TABLE t1_count_by_d (
  drange date NOT NULL,
  nrows int(10) unsigned NOT NULL,
  PRIMARY KEY (drange, nrows)
) ENGINE=InnoDB;
drangeCovering Index

2 コメント:

Unknown さんのコメント...

B-TreeIndex


2010/07/05 15:04:00
Mikiya Okuno ...






> B-TreeIndex



InnoDBMVCC

コメントを投稿