Tips16. count(*)は遅いのか -mysql編-

Tips.6 本当にCOUNT(*)は遅いのか?COUNT(列名)は速いのか?~処理時間を計測~ では、Oracleでの count(*) 呼び出しが遅くなるという噂について検証しました。
他のDBでも同じように「count(*)は使ってはいけない」という噂があると聞きましたので、試してみることにしました。

前回の Tips.15 様々なDB環境を Docker で構築 で、各種DB環境を作成しましたので試してみます。
このページでは、MySQLでの検証を行います。

docker コンテナの作成

次のコマンドでdockerコンテナを作成・実行し、docker execでコンテナ内に入ります。

docker run -d --name mysql_container -v ./mysql:/home/mysql -e TZ=UTC -p 33006:3306 -e MYSQL_ROOT_PASSWORD=rootpassword -e MYSQL_DATABASE=count_test ubuntu/mysql
docker exec -it mysql_container /bin/bash

このコマンドでは、

  • 「mysql_container」という名前のコンテナを作成
  • ローカルのカレントディレクトリ下「./mysql」と、コンテナ側の「/home/mysql」をマッピング
  • タイムゾーンはUTC(協定世界時)
  • ポート番号は、ローカル側が33006、コンテナ側が3306
  • 環境変数「MYSQL_DATABASE」でデータベース名「count_test」、「MYSQL_ROOT_PASSWORD」でROOTパスワード「rootpassword」を指定
  • dockerイメージは「ubuntu/mysql」を使用。事前に docker pull ubuntu/mysql:latest でダウンロードしておいてください。

を設定しています。

コンテナ内で、mysqlの起動・実行~テーブル作成

コンテナ作成時にデータベースは作成済みですので、すぐに作業が始められます。
次のコマンドでmysql内に入り、テーブルの作成まで行います。

-- mysqlにログイン
mysql -u root -p --enable-local-infile
-- 文字コードの設定
set names utf8mb4;
-- データベースの切替え
use count_test
-- テーブルを作成
create table count_test
(
  id int primary key,
  last_name varchar(30),
  first_name varchar(30),
  sex varchar(4),
  date_of_birth date,
  age int,
  address varchar(1000)
)
character set utf8mb4 collate utf8mb4_unicode_ci;
-- age列にだけインデックスを作成
create index count_test_idx on count_test(age);

終わったら、desc count_test; でテーブルが正しく作成されていることを確認します。

mysql> desc count_test;
+---------------+---------------+------+-----+---------+-------+
| Field         | Type          | Null | Key | Default | Extra |
+---------------+---------------+------+-----+---------+-------+
| id            | int           | NO   | PRI | NULL    |       |
| last_name     | varchar(30)   | YES  |     | NULL    |       |
| first_name    | varchar(30)   | YES  |     | NULL    |       |
| sex           | varchar(4)    | YES  |     | NULL    |       |
| date_of_birth | date          | YES  |     | NULL    |       |
| age           | int           | YES  | MUL | NULL    |       |
| address       | varchar(1000) | YES  |     | NULL    |       |
+---------------+---------------+------+-----+---------+-------+
7 rows in set (0.01 sec)

テストデータの取り込み

mysqlでは、load dataコマンドでCSV/TSVファイルの取り込みを行う事が出来ますが、事前に設定が必要です。
/etc/mysql/my.cnf ファイルを開き、

local-infile = 1

となっていることを確認、もしくは修正してください。

また、

select @@local_infile;

とクエリを実行し、「1」が返ってくることを確認してください。
1以外であれば、

set persist local_infile=1;

と打つことで設定を変更できます。

テストデータには、下記のようなフォーマットのTSVファイルを準備します。
前述のテーブル定義に合わせたデータになっています。人名はランダム。住所はとにかく長い住所を選んで入れています(重複しています)。
データは100万件あります。

1    滝口  沙織  女   1988-03-02  36  京都府京都市東山区三条通南裏二筋目白川筋西入二丁目北側北木之元町
2    横尾  和也  男   1988-04-15  36  静岡県静岡市大字上土新田下足洗新田川合新田請新田
3    松山  隆   男   1955-07-06  69  京都府京都市上京区寺町通今出川上る一丁目西入三筋目上る上塔之段町
4    清水  節子  女   1947-06-18  77  静岡県静岡市大字上土新田下足洗新田川合新田請新田
5    竹田  恵子  女   1962-02-23  62  愛知県海部郡飛島村大字飛島新田字竹之郷ヨタレ南ノ割
:
999997    高倉  ゆかり 女   1964-01-03  60  長野県岡谷市川岸字内山芦ノ沢笹見平木伐通芓久保馬ふ入岩良久保大久保細久保
999998    小坂  武   男   1944-03-15  80  静岡県静岡市大字上土新田下足洗新田川合新田請新田
999999    宇佐美 莉子  女   2010-12-10  13  京都府京都市上京区寺町通今出川上る一丁目西入三筋目上る上塔之段町
1000000    梅沢  修   男   1946-07-23  78  愛知県海部郡飛島村大字飛島新田字竹之郷ヨタレ南ノ割

下記のコマンドでTSVファイルをロードします。
パスは、docker run コマンドで指定したコンテナ側のパスを使用してデータファイルを参照します。

load data local infile '/home/mysql/person_testdata.tsv'
 into table count_test.count_test
 fields terminated by '\t';

取り込みが完了したら、データが正しく取り込まれているかデータ内容を確認します。
特にマルチバイト文字化けしていないか、列ズレしていないかといったところがポイントになるかと思います。
表示文字数が多いとマルチバイト文字列の表示がずれることがありますので、列名は明記した方が良いかもしれません。

select * from count_test limit 10;
select id, first_name, last_name from count_test limit 10;

select count() の実行

下記いくつかのバリエーションのselect count()を実行します。
キャッシュが効く・効かない、バックグラウンド処理の動作の影響などもありますので、複数回実行した方が良いでしょう。

-- COUNT --
select count(*) from count_test;
select count(1) from count_test;
-- プライマリキー項目
select count(id) from count_test;
-- インデックス項目
select count(age) from count_test;
-- 長い文字列項目
select count(address) from count_test;
-- 関数のある項目
select count(concat(first_name, last_name, address)) from count_test;

3回実行してみた結果を整理すると下記のようになりました。

mysql1回目2回目3回目
count(*)0.0700.0800.070
count(1)0.0800.0700.070
count(id)0.0900.0800.080
count(age)0.0900.0900.090
count(address)0.1800.1800.170
count(concat(first_name, last_name, address))0.2800.2700.280

count(*),count(1),count(id) の差は無いと言って良いでしょう。count(age)もほとんど同じです。
count(address)の長い文字列になると違いが出て、concat関数が入ると明らかに遅くなります。
いずれにしても、count(*)を使用すると遅くなるという事は無さそうです。

 :
 :
 


ちなみに、MySQL8.0では本当にcount(*)が遅くなるという不具合があったそうです。

MySQL 8.0 で SELECT COUNT(*) が失速する
https://zenn.dev/hmatsu47/articles/mysql80-count-slowdown

既に解消されているそうです。

MySQL8.0でSELECT COUNT(*)が低速になる動作は8.0.37で解消されていた!
https://ca-srg.dev/dba9a0f2aa9049259a9610274b258bf2

なお、今回の検証は、8.0.39で実施しております。

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.39-0ubuntu0.22.04.1 (Ubuntu)

Copyright (c) 2000, 2024, Oracle and/or its affiliates.

Oracle,SQLcount,COUNT(1),SQL,パフォーマンス

Posted by tfurukaw