免責事項
この記事はErik Frøseth氏によるHistogram statistics in MySQL(2017/10/2)をユーザが翻訳したものであり、Oracle公式の文書ではありません。
MySQL 8.0.3では、ヒストグラム統計を作成しオプティマイザがより多くの統計情報を利用できるようにすることができます。このブログ投稿では、どのようにヒストグラム統計を作成し、どのような時に役立つかについて説明しようと思います。
ヒストグラムとは何か
クエリーオプティマイザはデータベースの中で、SQLクエリーを可能な限り最も効率の良い実行計画に変換する役割を担っています。 時にはクエリーオプティマイザは最も効率的な実行計画を見つけるのに失敗し、クエリーの実行に必要以上に時間を使うことになることがあります。この主な理由は、オプティマイザがこれから実行しようとしているクエリーのデータに関する次の十分な情報がないためです。
- 各テーブルには何行あるのか?
- 各列は何種類の値をとるのか?
- 各列のデータはどのように分布しているか?
クエリーオプティマイザに重要な情報が欠けているシンプルな例として、就寝時間に関するテーブルについて考えてみましょう:
CREATE TABLE bedtime (
person_id INT,
time_of_day TIME);
time_of_day
列に関しては、大体はほとんどの人々が就寝する夜11時周辺となるでしょう。したがって下記の1番目のクエリーに対しては、2番目のクエリーより遥かに多い行が返ることでしょう。
1) SELECT * FROM bedtime WHERE time_of_day BETWEEN "22:00:00" AND "23:59:00"
2) SELECT * FROM bedtime WHERE time_of_day BETWEEN "12:00:00" AND "14:00:00"
統計が利用できなければ、オプティマイザはtime_of_day
は均等分布しているものと仮定します(つまり、午後3時頃就寝している人は夜11時頃就寝している人と同じくらいいる、ということです)。クエリーオプティマイザはどのようにしてデータの偏りを検知できるでしょうか?1つの方法としてその列に対してヒストグラム統計を作成することが挙げられます。
ヒストグラムは列のデータ分布の概算です。データ分布に偏りがあってもなくてもまずまずの精度を返してくれ、データベースサーバーがデータの性質を理解するのに役に立ちます。
ヒストグラムには多くの種類があり、MySQLでは2つの異なる形式を選択しました。「シングルトン(singleton)」ヒストグラムと「等高(equi-height)」ヒストグラムです。全てのヒストグラム形式に共通であることは、データセットが「バケット」に分割され設定されることです。MySQLは自動的に値をバケットに分割し、同時にどの形式のヒストグラムを生成するかを自動的に決定します。
ヒストグラム統計を作成削除するには
ヒストグラム統計を管理するために、私たちはANALYZE TABLEを2項目拡張しました。
ANALYZE TABLE tbl_name UPDATE HISTOGRAM ON col_name [, col_name] WITH N BUCKETS;
ANALYZE TABLE tbl_name DROP HISTOGRAM ON col_name [, col_name];
最初の構文を利用すると、1列または複数列のヒストグラム統計を同時に作成できます。
mysql> ANALYZE TABLE payment UPDATE HISTOGRAM ON amount WITH 32 BUCKETS;
+----------------+-----------+----------+---------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+---------------------------------------------------+
| sakila.payment | histogram | status | Histogram statistics created for column 'amount'. |
+----------------+-----------+----------+---------------------------------------------------+
1 row in set (0.27 sec)
mysql> ANALYZE TABLE payment UPDATE HISTOGRAM ON amount, payment_date WITH 32 BUCKETS;
+----------------+-----------+----------+---------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+---------------------------------------------------------+
| sakila.payment | histogram | status | Histogram statistics created for column 'amount'. |
| sakila.payment | histogram | status | Histogram statistics created for column 'payment_date'. |
+----------------+-----------+----------+---------------------------------------------------------+
バケットの数を指定しなければならないことに注意する必要があり、これは1〜1024の範囲の値をとります。データセットに対してバケット数をいくつにするべきかは、どの程度固有の値があるのか、どの程度データセットの偏りがあるか、どの程度の精度が必要か、といった多くの要素に関係します。しかしながら、一定以上のバケット数に達すると、それ以上は精度はあまり向上しなくなります。したがって、例えば32といった小さな値から始めて、必要な条件を満たさなければこれを増やすのが良いでしょう。
上記の例では、「amount」列に対して2回ヒストグラムを作成していることがわかります。最初のクエリーでは新しいヒストグラムが作成され、2番目のクエリーでは、「amount」列に対するヒストグラムは自動的に上書きされます。
作成済みのヒストグラム統計を削除したくなった場合は、DROP HISTOGRAM
構文を使って簡単に削除できます。
mysql> ANALYZE TABLE payment DROP HISTOGRAM ON payment_date;
+----------------+-----------+----------+---------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+---------------------------------------------------------+
| sakila.payment | histogram | status | Histogram statistics removed for column 'payment_date'. |
+----------------+-----------+----------+---------------------------------------------------------+
UPDATE HISTOGRAMに関しては、同一コマンドで複数列を指定できます。1つ特筆すべき動作があり、ANALYZE TABLEコマンドは、たとえコマンドの途中で何かがうまくいかなかったとしても、可能なことは全て実施しようとします。例えば3つの列を指定したとして、2番目の列が存在しなかったとします。サーバーは1番目および3番目の列に対してはヒストグラムを作成し保存することでしょう。
mysql> ANALYZE TABLE customer UPDATE HISTOGRAM ON c_birth_day, c_foobar, c_birth_month WITH 32 BUCKETS;
+----------------+-----------+----------+----------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+----------------------------------------------------------+
| tpcds.customer | histogram | status | Histogram statistics created for column 'c_birth_day'. |
| tpcds.customer | histogram | status | Histogram statistics created for column 'c_birth_month'. |
| tpcds.customer | histogram | Error | The column 'c_foobar' does not exist. |
+----------------+-----------+----------+----------------------------------------------------------+
3 rows in set (0.15 sec)
裏側はどうなっているのか
MySQLのマニュアルを既にお読みであれば、histogram_generation_max_mem_sizeという新しいシステム変数をご覧になっているかもしれません。この変数はヒストグラム統計を作成する際にサーバーがどのくらいのメモリーを利用してよいかをおおよそ制御するための変数です。それでは、なぜこれを制御しなければいけないのでしょうか?
ヒストグラムを作成しこれを指定する際は、サーバーは全てのデータをメモリーに読込み、(ソートを含めた)すべての操作をメモリー上で実施します。巨大なテーブルに対してヒストグラムを生成する場合、数百メガバイトのデータをメモリーに読込むリスクがあり、これは望ましくありません。この問題を扱うために、MySQLはhistogram_generation_max_mem_sizeシステム変数で指定されたメモリーサイズに対して、何行のデータが収まるかを計算します。与えられたメモリー制限内だと、一部の行しか収まらないことがわかった場合は、サンプリングを行います。これは「sampling-rate」の値をみることで確認できます。
mysql> SET histogram_generation_max_mem_size = 1000000;
Query OK, 0 rows affected (0.00 sec)
mysql> ANALYZE TABLE customer UPDATE HISTOGRAM ON c_birth_country WITH 16 BUCKETS;
+----------------+-----------+----------+------------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+------------------------------------------------------------+
| tpcds.customer | histogram | status | Histogram statistics created for column 'c_birth_country'. |
+----------------+-----------+----------+------------------------------------------------------------+
1 row in set (0.22 sec)
mysql> SELECT histogram->>'$."sampling-rate"'
-> FROM information_schema.column_statistics
-> WHERE table_name = "customer"
-> AND column_name = "c_birth_country";
+---------------------------------+
| histogram->>'$."sampling-rate"' |
+---------------------------------+
| 0.048743243211626014 |
+---------------------------------+
1 row in set (0.00 sec)
オプティマイザが「c_birth_country」列の約4.8%のデータを読込みヒストグラムを作成したことがわかります。サンプリングは非決定的であるため、「ANALYZE TABLE tbl UPDATE HISTOGRAM …」を同じデータに2度連続して実行した際に、サンプリングが適用されれば、2つのヒストグラムは異なるものとなる可能性があることは、認識しておくべきことでしょう。
クエリーの例
ヒストグラム統計を使って何ができるのでしょうか?1つヒストグラムを加えるだけでクエリーの実行時間が大きく変わるTPC-DSベンチマークスイートの2, 3のクエリーを見てみましょう。ここでは、TPC-DSをデータベースがおよそ1GBとなるスケールファクター 1 で利用します。検証機はIntel Core i7-4770で、Debian Stretch上で MySQL 8.0 RC1を動作させています。設定はデータベース全体を全てバッファープールにのせるためにinnodb_buffer_pool_sizeが2Gに増やしてあること以外は標準の値です。
オプティマイザが実際にヒストグラムの統計を利用するようにするためには、オプティマイザ スイッチの「condition_fanout_filter」が有効化されていることを確認すればよいだけです。この設定は標準で有効化されています。
クエリー 90
※訳注: 次のサイトのドキュメントでクエリー番号に対応するクエリーが定義されています。
このクエリーはベンチマークスイート上では次のような意味づけです「ある特定の付帯条件のもとで、インターネット上で午前に売れる商品数と、夕方売れる商品数の割合。コンテンツの多いウェブサイトのみを考えます」
mysql> SELECT CAST(amc AS DECIMAL(15, 4)) / CAST(pmc AS DECIMAL(15, 4)) am_pm_ratio
-> FROM (SELECT COUNT(*) amc
-> FROM web_sales,
-> household_demographics,
-> time_dim,
-> web_page
-> WHERE ws_sold_time_sk = time_dim.t_time_sk
-> AND ws_ship_hdemo_sk = household_demographics.hd_demo_sk
-> AND ws_web_page_sk = web_page.wp_web_page_sk
-> AND time_dim.t_hour BETWEEN 9 AND 9 + 1
-> AND household_demographics.hd_dep_count = 2
-> AND web_page.wp_char_count BETWEEN 5000 AND 5200) at,
-> (SELECT COUNT(*) pmc
-> FROM web_sales,
-> household_demographics,
-> time_dim,
-> web_page
-> WHERE ws_sold_time_sk = time_dim.t_time_sk
-> AND ws_ship_hdemo_sk = household_demographics.hd_demo_sk
-> AND ws_web_page_sk = web_page.wp_web_page_sk
-> AND time_dim.t_hour BETWEEN 15 AND 15 + 1
-> AND household_demographics.hd_dep_count = 2
-> AND web_page.wp_char_count BETWEEN 5000 AND 5200) pt
-> ORDER BY am_pm_ratio
-> LIMIT 100;
+-------------+
| am_pm_ratio |
+-------------+
| 1.27619048 |
+-------------+
1 row in set (1.48 sec)
クエリーを実行するのにおよそ1.5秒かかっていることがわかります。これは大して長くないように見えますが、ある単一列にヒストグラムを追加することによりこのクエリーは3倍高速化できます(クエリーは可読性を優先するためくずしてます)。
mysql> ANALYZE TABLE web_page UPDATE HISTOGRAM ON wp_char_count WITH 8 BUCKETS;
+----------------+-----------+----------+----------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+----------------------------------------------------------+
| tpcds.web_page | histogram | status | Histogram statistics created for column 'wp_char_count'. |
+----------------+-----------+----------+----------------------------------------------------------+
1 row in set (0.06 sec)
mysql> SELECT ...
+-------------+
| am_pm_ratio |
+-------------+
| 1.27619048 |
+-------------+
1 row in set (0.50 sec)
この1つのヒストグラムにより、クエリーの実行時間は約0.5秒となります。なぜこのようになるのでしょうか?この主な理由は、「web_page.wp_char_count BETWEEN 5000 AND 5200」という述部から見いだせます。統計が一切利用できなければ、オプティマイザは「web_page」テーブルの11.11%の行が与えられた述部に一致すると予想するでしょう。しかし、これは誤っています。テーブルを実際に確認すると、1.6%しか該当データは存在しません(60行のうち1行)。
mysql> SELECT
-> (SELECT COUNT(*) FROM web_page WHERE web_page.wp_char_count BETWEEN 5000 AND 5200)
-> /
-> (SELECT COUNT(*) FROM web_page) AS ratio;
+--------+
| ratio |
+--------+
| 0.0167 |
+--------+
1 row in set (0.00 sec)
ヒストグラム統計が利用できれば、オプティマイザはこれを知っているため結合順序においてそのテーブルを前に移動し、結果として3倍高速化する実行計画を作成します。
クエリー 61
このクエリーは次のように記載されています。「ある年のある月で宣伝をした場合としなかった場合の売上比率を確認。ある特定のタイムゾーンに住んでいる顧客に売られたある特定カテゴリーの商品のみが考慮されています」。これは大きな、複数の結合を含む複雑なクエリーです。
mysql> SELECT promotions,
-> total,
-> CAST(promotions AS DECIMAL(15, 4)) / CAST(total AS DECIMAL(15, 4)) * 100
-> FROM (SELECT SUM(ss_ext_sales_price) promotions
-> FROM store_sales,
-> store,
-> promotion,
-> date_dim,
-> customer,
-> customer_address,
-> item
-> WHERE ss_sold_date_sk = d_date_sk
-> AND ss_store_sk = s_store_sk
-> AND ss_promo_sk = p_promo_sk
-> AND ss_customer_sk = c_customer_sk
-> AND ca_address_sk = c_current_addr_sk
-> AND ss_item_sk = i_item_sk
-> AND ca_gmt_offset = -5
-> AND i_category = 'Home'
-> AND ( p_channel_dmail = 'Y'
-> OR p_channel_email = 'Y'
-> OR p_channel_tv = 'Y' )
-> AND s_gmt_offset = -5
-> AND d_year = 2000
-> AND d_moy = 12) promotional_sales,
-> (SELECT SUM(ss_ext_sales_price) total
-> FROM store_sales,
-> store,
-> date_dim,
-> customer,
-> customer_address,
-> item
-> WHERE ss_sold_date_sk = d_date_sk
-> AND ss_store_sk = s_store_sk
-> AND ss_customer_sk = c_customer_sk
-> AND ca_address_sk = c_current_addr_sk
-> AND ss_item_sk = i_item_sk
-> AND ca_gmt_offset = -5
-> AND i_category = 'Home'
-> AND s_gmt_offset = -5
-> AND d_year = 2000
-> AND d_moy = 12) all_sales
-> ORDER BY promotions,
-> total
-> LIMIT 100;
+------------+------------+--------------------------------------------------------------------------+
| promotions | total | CAST(promotions AS DECIMAL(15, 4)) / CAST(total AS DECIMAL(15, 4)) * 100 |
+------------+------------+--------------------------------------------------------------------------+
| 3213210.07 | 5966836.78 | 53.85114741 |
+------------+------------+--------------------------------------------------------------------------+
1 row in set (2.78 sec)
ご覧の通り、クエリーには約2.8秒かかっています。しかしながら、クエリーオプティマイザは「s_gmt_offset」列は1つの値しか持たないことを知りません。統計を全く利用できない場合、オプティマイザはハードコードされた予測をし、10%の行が「ca_gmt_offset = -5」の述部に一致すると予想します。この列にヒストグラムを加えれば、オプティマイザはテーブル内の全行がこの条件をみたすことを理解しより良い実行計画を導くでしょう。(クエリーは可読性を優先するためくずしてます)。
mysql> ANALYZE TABLE store UPDATE HISTOGRAM ON s_gmt_offset WITH 8 BUCKETS;
+-------------+-----------+----------+---------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+-------------+-----------+----------+---------------------------------------------------------+
| tpcds.store | histogram | status | Histogram statistics created for column 's_gmt_offset'. |
+-------------+-----------+----------+---------------------------------------------------------+
1 row in set (0.06 sec)
mysql> SELECT ...
+------------+------------+--------------------------------------------------------------------------+
| promotions | total | CAST(promotions AS DECIMAL(15, 4)) / CAST(total AS DECIMAL(15, 4)) * 100 |
+------------+------------+--------------------------------------------------------------------------+
| 3213210.07 | 5966836.78 | 53.85114741 |
+------------+------------+--------------------------------------------------------------------------+
1 row in set (1.37 sec)
ヒストグラムを利用することによって、クエリーの実行は1.4秒以下に短縮され、2倍以上改善されています。これは最初の実行計画では、オプティマイザは最初の派生テーブルがstoreテーブルに対してフルテーブルスキャンを行い、それからitem、store_sales、date_dim、customerそしてcustomer_addressテーブルそれぞれに主キーのルックアップを行うことを選択するためです。しかしstoreテーブルがヒストグラム統計を利用しなかった場ときと比べて多くの行を返すと判明すれば、オプティマイザはフルテーブルスキャンをitemテーブルに対して実施し、store_sales、store、date_dim、customerそして最後にcustomer_addressテーブルそれぞれに主キールックアップを行います。
なぜインデックスではないのか?
みなさんのうち何名かは、インデックスでも同じようにできるのではないかとお考えでしょうし、それはその通りです。
mysql> CREATE INDEX s_gmt_offset_idx ON store (s_gmt_offset);
Query OK, 0 rows affected (0.53 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SELECT ...
+------------+------------+--------------------------------------------------------------------------+
| promotions | total | CAST(promotions AS DECIMAL(15, 4)) / CAST(total AS DECIMAL(15, 4)) * 100 |
+------------+------------+--------------------------------------------------------------------------+
| 3213210.07 | 5966836.78 | 53.85114741 |
+------------+------------+--------------------------------------------------------------------------+
1 row in set (1.41 sec)
しかし、インデックスの代わりにヒストグラムを考慮する2つの理由があります。
- インデックスの維持は高コストです。インデックスを生成すると、INSERT/UPDATE/DELETEの度に、インデックスの更新が発生します。これは"ただ"ではなく、性能に影響を与えます。一方でヒストグラムは一度生成されれば、明示的に要求をしない限り決して更新されません。このような理由から、INSERT/UPDATE/DELETEの性能に影響を与えません。
- インデックスがある場合、オプティマイザは与えられた範囲のレコード数を見積もるために、いわゆる「インデックス ダイブ」を実行します。これにも一定のコストがかかり、例えばクエリーに長いIN句がついている場合は非常に大きなコストとなります。この場合はヒストグラム統計はより低コストであり、したがってより適しているといえます。
ヒストグラム統計を詳しくみる
ヒストグラム統計はJSONオブジェクトとしてデータディクショナリに保存されており、柔軟性をもったままデータを参照できるようになっています。ヒストグラムから情報を抽出するには「組み込みのJSON関数」をご利用になれます。「payment」テーブルの「amount」列のヒストグラムがいつ作成/更新されたか知りたいとしましょう。この情報を取得するにはJSONアンクォート抽出演算子(JSON unquoting extraction operator)(※訳注: ->>)を利用すれば簡単に取得できます。
mysql> SELECT
-> HISTOGRAM->>'$."last-updated"' AS last_updated
-> FROM INFORMATION_SCHEMA.COLUMN_STATISTICS
-> WHERE
-> SCHEMA_NAME = "sakila"
-> AND TABLE_NAME = "payment"
-> AND COLUMN_NAME = "amount";
+----------------------------+
| last_updated |
+----------------------------+
| 2017-09-15 11:54:25.000000 |
+----------------------------+
ANALYZE TABLE文で指定されたバケット数に対して、何バケットあるのか確認するには次のクエリーが利用できます。
mysql> SELECT
-> TABLE_NAME,
-> COLUMN_NAME,
-> HISTOGRAM->>'$."number-of-buckets-specified"' AS num_buckets_specified,
-> JSON_LENGTH(HISTOGRAM, '$.buckets') AS num_buckets_created
-> FROM INFORMATION_SCHEMA.COLUMN_STATISTICS
-> WHERE
-> SCHEMA_NAME = "sakila";
+------------+--------------+-----------------------+---------------------+
| TABLE_NAME | COLUMN_NAME | num_buckets_specified | num_buckets_created |
+------------+--------------+-----------------------+---------------------+
| payment | amount | 32 | 19 |
| payment | payment_date | 32 | 32 |
+------------+--------------+-----------------------+---------------------+
ヒストグラムからどのような情報が取得できるかについての詳細についてはドキュメントをご参照ください。
オプティマイザトレース
ヒストグラムによって実施された見積もりを知りたい場合は、EXPLAINの出力を見るのが一番簡単です。
mysql> EXPLAIN SELECT * FROM customer WHERE c_birth_day BETWEEN 1 AND 10;
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | customer | NULL | ALL | NULL | NULL | NULL | NULL | 98633 | 11.11 | Using where |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
mysql> ANALYZE TABLE customer UPDATE HISTOGRAM ON c_birth_day WITH 32 BUCKETS;
+----------------+-----------+----------+--------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+-----------+----------+--------------------------------------------------------+
| tpcds.customer | histogram | status | Histogram statistics created for column 'c_birth_day'. |
+----------------+-----------+----------+--------------------------------------------------------+
1 row in set (0.10 sec)
mysql> EXPLAIN SELECT * FROM customer WHERE c_birth_day BETWEEN 1 AND 10;
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | customer | NULL | ALL | NULL | NULL | NULL | NULL | 98633 | 32.12 | Using where |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
「filtered」列を見ると、デフォルトの11.11%からより正確な32.12%と変化していることが分かるでしょう。しかしながら、複数の条件があり、いくつかの列にはヒストグラム統計がありその他の列にはヒストグラムがない場合はオプティマイザ統計が何を見積もったかを知るのは困難でしょう。
mysql> EXPLAIN SELECT * FROM customer WHERE c_birth_day <= 20 AND c_birth_year = 1967;
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | customer | NULL | ALL | NULL | NULL | NULL | NULL | 98633 | 6.38 | Using where |
+----+-------------+----------+------------+------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
ヒストグラムにより実施された見積もりについてより詳細に知りたい場合は、クエリーのトレースを見てみると良いでしょう。
mysql> SET OPTIMIZER_TRACE = "enabled=on";
Query OK, 0 rows affected (0.00 sec)
mysql> SET OPTIMIZER_TRACE_MAX_MEM_SIZE = 1000000;
Query OK, 0 rows affected (0.00 sec)
mysql> EXPLAIN SELECT * FROM customer WHERE c_birth_day <= 20 AND c_birth_year = 1967;
mysql> SELECT JSON_EXTRACT(TRACE, "$**.filtering_effect") FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
+----------------------------------------------------------------------------------------+
| JSON_EXTRACT(TRACE, "$**.filtering_effect") |
+----------------------------------------------------------------------------------------+
| [[{"condition": "(`customer`.`c_birth_day` <= 20)", "histogram_selectivity": 0.6376}]] |
+----------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
ここではトレースの出力から関係ある部分を抽出するのにJSON_EXTRACT関数を使っています。各条件でヒストグラムが利用されていれば、選択性の見積もりが分かるでしょう。このケースでは選択性はある条件(c_birth_day <= 20)にとってのみ見積もられており、ヒストグラムは対象行の63.76%の行が条件が一致すると見積もりました。これは実データと一致します。
mysql> SELECT
-> (SELECT count(*) FROM customer WHERE c_birth_day <= 20)
-> /
-> (SELECT COUNT(*) FROM customer) AS ratio;
+--------+
| ratio |
+--------+
| 0.6376 |
+--------+
1 row in set (0.03 sec)