GA

2017/04/12

MySQL 8.0.1の新顔、GROUPING集約関数

TL;DR
WITH ROLLUPの結果行をHAVING条件に書けるようすることができる。 それ以外の時には使わない。
使い方。 そもそも WITH ROLLUP の使い方を知らないと楽しくもなんともないので WITH ROLLUP の説明から。
まずは WITH ROLLUP なしバージョン(SUM関数を噛ませてるのはあとで WITH ROLLUP した時のため)
mysql80> SELECT Continent, Name, SUM(Population) AS Population FROM country GROUP BY Continent, Name;
+---------------+----------------------------------------------+------------+
| Continent     | Name                                         | Population |
+---------------+----------------------------------------------+------------+
| North America | Aruba                                        |     103000 |
| Asia          | Afghanistan                                  |   22720000 |
| Africa        | Angola                                       |   12878000 |
..
| Africa        | South Africa                                 |   40377000 |
| Africa        | Zambia                                       |    9169000 |
| Africa        | Zimbabwe                                     |   11669000 |
+---------------+----------------------------------------------+------------+
239 rows in set (0.00 sec)
おっと… GROUP BYが暗黙のソートをしなくなった件 を垣間見ることもできた。 5.7とそれ以前と出力結果を一緒にするためには、 ORDER BY Continent, Name も追加する必要がある。
ともあれ、こんなフツーの GROUP BY なクエリーに WITH ROLLUP を足してやると
mysql80> SELECT Continent, Name, SUM(Population) AS Population FROM country GROUP BY Continent, Name WITH ROLLUP;
+---------------+----------------------------------------------+------------+
| Continent     | Name                                         | Population |
+---------------+----------------------------------------------+------------+
| Asia          | Afghanistan                                  |   22720000 |
| Asia          | Armenia                                      |    3520000 |
| Asia          | Azerbaijan                                   |    7734000 |
..
| Asia          | Yemen                                        |   18112000 |
| Asia          | NULL                                         | 3705025700 |
| Europe        | Albania                                      |    3401200 |
..
| Europe        | Yugoslavia                                   |   10640000 |
| Europe        | NULL                                         |  730074600 |
| North America | Anguilla                                     |       8000 |
..
| South America | Venezuela                                    |   24170000 |
| South America | NULL                                         |  345780000 |
| NULL          | NULL                                         | 6078749450 |
+---------------+----------------------------------------------+------------+
247 rows in set (0.00 sec)
こうなる。 Continent単位で合計したものが Name IS NULL として集約行が作られて、全てを合計した値で Continent IS NULL, Name IS NULL として集約行が作られる。
これ、NULLになったカラムはSQLの中から条件指定が不可能(WHEREはGROUP BYが処理される前のフィルターだし、HAVINGフィルターよりも更に後に集約行が作成されるのでダメらしい( MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.19.2 GROUP BY 修飾子
なので、この集約行にだけアクセスしたい場合(最初からそこでGROUP BYしろよとは思うけれどなんでなのか俺もやりたがった記憶がある)、アプリケーションの中で結果セットを受け取ってからカラムがNULLかどうかチェックして集約行判定をしなければならなかった。 たとえばこんな風に( !(defined($_->{Continent})) && !(defined(_->{Name}))
my $conn= DBI->connect("dbi:mysql:world;mysql_socket=/usr/mysql/8.0.1/data/mysql.sock", "root", "");

my $sql= "SELECT Continent, Name, SUM(Population) AS Population FROM country GROUP BY Continent, Name WITH ROLLUP";
foreach (@{$conn->selectall_arrayref($sql, {Slice => {}})})
{
  print Dumper $_ if !(defined($_->{Continent})) && !(defined($_->{Name}));
}

=pod
$VAR1 = {
          'Continent' => undef,
          'Name' => undef,
          'Population' => '6078749450'
        };
=cut
で、これをHAVINGの中で言及できるようにする関数が GROUPING らしい。
mysql80> SELECT Continent, Name, SUM(Population) AS Population FROM country GROUP BY Continent, Name WITH ROLLUP HAVING GROUPING(Continent) AND GROUPING(Name);
+-----------+------+------------+
| Continent | Name | Population |
+-----------+------+------------+
| NULL      | NULL | 6078749450 |
+-----------+------+------------+
1 row in set (0.00 sec)

mysql80> SELECT Continent, Name, SUM(Population) AS Population FROM country GROUP BY Continent, Name WITH ROLLUP HAVING GROUPING(Name);
+---------------+------+------------+
| Continent     | Name | Population |
+---------------+------+------------+
| Asia          | NULL | 3705025700 |
| Europe        | NULL |  730074600 |
| North America | NULL |  482993000 |
| Africa        | NULL |  784475000 |
| Oceania       | NULL |   30401150 |
| Antarctica    | NULL |          0 |
| South America | NULL |  345780000 |
| NULL          | NULL | 6078749450 |
+---------------+------+------------+
8 rows in set (0.00 sec)
最初っからそこで GROUP BY しなよ感があるけれど、GROUPING が1か0を返すからORDER BYで集約行を先に持ってこられたらいいかな? と思った。
mysql80> SELECT Continent, Name, SUM(Population) AS Population FROM country GROUP BY Continent, Name WITH ROLLUP HAVING GROUPING(Name) ORDER BY GROUPING(Name);
ERROR 1221 (HY000): Incorrect usage of CUBE/ROLLUP and ORDER BY
WITH ROLLUPとORDER BYが同時に使えないという制約があるので並べ替えには使えなかった。 残念。。

2017/04/11

MySQL 8.0.1でutf8mb4_ja_0900_as_csが導入された


Sushi = Beer ?! An introduction of UTF8 support in MySQL 8.0 | MySQL Server Blog (ユーザーによる日本語訳: 寿司=ビール問題 : MySQL 8.0でのUTF8サポート入門 (MySQL Server Blogより) | Yakst)で言及されていた日本語用の照合順序 utf8mb4_ja_0900_as_cs
MySQL 8.0.1 で実装されていたので試してみた。
mysql80> SHOW COLLATION LIKE 'utf8%ja%';
+-----------------------+---------+-----+---------+----------+---------+
| Collation             | Charset | Id  | Default | Compiled | Sortlen |
+-----------------------+---------+-----+---------+----------+---------+
| utf8mb4_ja_0900_as_cs | utf8mb4 | 303 |         | Yes      |      24 |
+-----------------------+---------+-----+---------+----------+---------+
1 row in set (0.00 sec)
まずは「ハハ=パパ」問題。 (MySQLは真偽値を0(=FALSE)と1(=TRUE)で返すのでそのつもりで)
mysql80> SELECT 'ハハ' = 'パパ' COLLATE utf8mb4_ja_0900_as_cs;
+---------------------------------------------------+
| 'ハハ' = 'パパ' COLLATE utf8mb4_ja_0900_as_cs     |
+---------------------------------------------------+
|                                                 0 |
+---------------------------------------------------+
1 row in set (0.04 sec)
ハハパパケースセンシティブ。 ひらがな=カタカナ問題。
mysql80> SELECT 'ハハ' = 'はは' COLLATE utf8mb4_ja_0900_as_cs;
+---------------------------------------------------+
| 'ハハ' = 'はは' COLLATE utf8mb4_ja_0900_as_cs     |
+---------------------------------------------------+
|                                                 1 |
+---------------------------------------------------+
1 row in set (0.00 sec)
ひらがなカタカナケースインセンシティブ。 次は半角全角。
mysql80> SELECT 'ハハ' = 'ハハ' COLLATE utf8mb4_ja_0900_as_cs;
+---------------------------------------------------+
| 'ハハ' = 'ハハ' COLLATE utf8mb4_ja_0900_as_cs       |
+---------------------------------------------------+
|                                                 1 |
+---------------------------------------------------+
1 row in set (0.00 sec)

mysql80> SELECT 'はは' = 'ハハ' COLLATE utf8mb4_ja_0900_as_cs;
+---------------------------------------------------+
| 'はは' = 'ハハ' COLLATE utf8mb4_ja_0900_as_cs       |
+---------------------------------------------------+
|                                                 1 |
+---------------------------------------------------+
1 row in set (0.00 sec)
半角全角ケースインセンシティブ。 拗音。
mysql80> SELECT 'びょういん' = 'びよういん' COLLATE utf8mb4_ja_0900_as_cs;
+---------------------------------------------------------------------+
| 'びょういん' = 'びよういん' COLLATE utf8mb4_ja_0900_as_cs           |
+---------------------------------------------------------------------+
|                                                                   0 |
+---------------------------------------------------------------------+
1 row in set (0.00 sec)
病院≠美容院。拗音ケースセンシティブ。 最後🍣=🍺だけ俺はウインドーズでターミナルから直接打ち込めないので画像で。
ちょっと見にくいけど0。
utf8mb4_bin utf8mb4_general_ci utf8mb4_unicode_ci utf8mb4_unicode_520_ci utf8mb4_ja_0900_as_cs
Hiragana-Katakana cs (unkind) cs (unkind) ci (good) ci(good) ci(good)
Youon cs (good) cs (good) ci (critical) ci(critical) cs(good)
Dakuten-Handakuten cs (good) cs (good) ci (critical) ci(critical) cs(good)
Wide-Narrow cs (unkind) cs (unkind) ci (good) ci(good) ci(good)
Sushi-Beer cs ci ci cs cs
おおー、結構いいセン行ってるんじゃないだろうか。
なお、斎藤さんは斉藤さんかとかそういうことを考え出すと、どうすればいいのか俺にもよくわからないけど一応センシティブ(中国語圏の人とかどうあるべきだと思うんだろう)
mysql80> SELECT '斎藤' = '斉藤' COLLATE utf8mb4_ja_0900_as_cs;
+---------------------------------------------------+
| '斎藤' = '斉藤' COLLATE utf8mb4_ja_0900_as_cs     |
+---------------------------------------------------+
|                                                 0 |
+---------------------------------------------------+
1 row in set (0.00 sec)
あとはこの設定を秘伝のmy.cnfのmysqldセクションに書き込んでおけばOK。 character_set_server はデフォルトがutf8mb4になったけれど一応ついでに。
$ vim my.cnf
..
[mysqld]
character_set_server = utf8mb4
collation_server = utf8mb4_ja_0900_as_cs
..

2017/04/03

mysqlディレクトリーに知らない.ibdファイルがある in MySQL 8.0.0

InnoDBログをcatしたら見知らぬibdファイルの名前が書いてあることに気が付いた。 mysql/character_sets.ibd なるファイルに書き込みをしているようだが、
mysql80> SHOW TABLES FROM mysql LIKE '%char%';
Empty set (0.00 sec)
そんなテーブルは存在しない。
$ ll data/mysql/character_sets.ibd
-rw-r----- 1 yoku0825 yoku0825 163840 Apr  3 14:44 data/mysql/character_sets.ibd
ファイルは確かにある。 なんだこれ…? と思ってたら、なんか他にもいっぱいあった。
$ diff -y <(./use -sse "SHOW TABLES FROM mysql" | sort) <(ls data/mysql/*.ibd | perl -nle 's/.+\///; s/\.ibd//; print' | sort)
                                                              > catalogs
                                                              > character_sets
                                                              > collations
                                                              > columns
columns_priv                                                    columns_priv
column_stats                                                    column_stats
                                                              > column_type_elements
component                                                       component
db                                                              db
default_roles                                                   default_roles
engine_cost                                                     engine_cost
                                                              > events
                                                              > foreign_key_column_usage
                                                              > foreign_keys
func                                                            func
general_log                                                   <
gtid_executed                                                   gtid_executed
help_category                                                   help_category
help_keyword                                                    help_keyword
help_relation                                                   help_relation
help_topic                                                      help_topic
                                                              > index_column_usage
                                                              > indexes
                                                              > index_partitions
                                                              > index_stats
innodb_index_stats                                              innodb_index_stats
innodb_table_stats                                              innodb_table_stats
                                                              > parameters
                                                              > parameter_type_elements
plugin                                                          plugin
procs_priv                                                      procs_priv
proxies_priv                                                    proxies_priv
role_edges                                                      role_edges
                                                              > routines
                                                              > schemata
server_cost                                                     server_cost
servers                                                         servers
slave_master_info                                               slave_master_info
slave_relay_log_info                                            slave_relay_log_info
slave_worker_info                                               slave_worker_info
slow_log                                                      | st_spatial_reference_systems
                                                              > table_partitions
                                                              > table_partition_values
                                                              > tables
                                                              > tablespace_files
                                                              > tablespaces
tables_priv                                                     tables_priv
                                                              > table_stats
time_zone                                                       time_zone
time_zone_leap_second                                           time_zone_leap_second
time_zone_name                                                  time_zone_name
time_zone_transition                                            time_zone_transition
time_zone_transition_type                                       time_zone_transition_type
                                                              > triggers
user                                                            user
                                                              > version
                                                              > view_routine_usage
                                                              > view_table_usage
向かって右がibdファイルしかないやつ。 general_logとslow_log(なんか変にst_spatial_reference_systemsと混じってやんの。。)はCSVストレージエンジンだからテーブルしかないのは良いとして、tables.ibdとかがテーブルのメタデータを集めたibdファイルになっているっぽい( od -c してみたらどうやらそれっぽい) これが、「 information_schema.tables は実際のInnoDBテーブルへの参照になって、メタデータを .frm から集めなくなるから速くなる」の正体か。
.ibdファイルからレコードを引き出すユーティリティーが欲しくなるなあ。。 とか思ったけど innodb_ruby がまだフツーに使えた。やった。
mysql80> CREATE TABLE t1 (num serial, val varchar(32)) comment = 'yoku0825';
Query OK, 0 rows affected (0.02 sec)

$ innodb_space -s data/ibdata1 -T mysql/tables space-indexes
id          name                            root        fseg        used        allocated   fill_factor
35          PRIMARY                         5           internal    1           1           100.00%
35          PRIMARY                         5           leaf        28          28          100.00%
36          schema_id                       6           internal    1           1           100.00%
36          schema_id                       6           leaf        0           0           0.00%
37          engine                          7           internal    1           1           100.00%
37          engine                          7           leaf        0           0           0.00%
38          engine_2                        8           internal    1           1           100.00%
38          engine_2                        8           leaf        0           0           0.00%
39          collation_id                    9           internal    1           1           100.00%
39          collation_id                    9           leaf        0           0           0.00%
40          tablespace_id                   10          internal    1           1           100.00%
40          tablespace_id                   10          leaf        0           0           0.00%

$ innodb_space -s data/ibdata1 -T mysql/tables -p 5 page-dump | less
{:format=>:compact,
 :offset=>668,
 :header=>
  {:next=>112,
   :type=>:node_pointer,
   :heap_number=>29,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :nulls=>[],
   :lengths=>{},
   :externs=>[],
   :length=>5},
 :next=>112,
 :type=>:clustered,
 :key=>[{:name=>"id", :type=>"BIGINT UNSIGNED", :value=>318}],
 :row=>[],
 :sys=>[],
 :child_page_number=>38,
 :length=>12}

$ innodb_space -s data/ibdata1 -T mysql/tables -p 38 page-dump | less
{:format=>:compact,
 :offset=>12645,
 :header=>
  {:next=>112,
   :type=>:conventional,
   :heap_number=>6,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :nulls=>
    ["se_private_data",
     "se_private_id",
     "tablespace_id",
     "partition_type",
     "partition_expression",
     "default_partitioning",
     "subpartition_type",
     "subpartition_expression",
     "default_subpartitioning",
     "view_definition",
     "view_definition_utf8",
     "view_check_option",
     "view_is_updatable",
     "view_algorithm",
     "view_security_type",
     "view_definer",
     "view_client_collation_id",
     "view_connection_collation_id"],
   :lengths=>{"name"=>2, "engine"=>6, "comment"=>8, "options"=>105},
   :externs=>[],
   :length=>12},

 :next=>112,
 :type=>:clustered,
 :key=>[{:name=>"id", :type=>"BIGINT UNSIGNED", :value=>322}],
 :row=>
  [{:name=>"schema_id", :type=>"BIGINT UNSIGNED", :value=>6},
   {:name=>"name", :type=>"VARCHAR(192)", :value=>"t1"},
   {:name=>"type", :type=>"CHAR(1) UNSIGNED", :value=>"\x01"},
   {:name=>"engine", :type=>"VARCHAR(192)", :value=>"InnoDB"},
   {:name=>"mysql_version_id", :type=>"INT UNSIGNED", :value=>80000},
   {:name=>"row_format", :type=>"CHAR(1) UNSIGNED", :value=>"\x02"},
   {:name=>"collation_id", :type=>"BIGINT UNSIGNED", :value=>8},
   {:name=>"comment", :type=>"VARCHAR(6144)", :value=>"yoku0825"},
   {:name=>"hidden", :type=>"TINYINT", :value=>0},
   {:name=>"options",
    :type=>"BLOB",
    :value=>
     "avg_row_length=0;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;"},
   {:name=>"se_private_data", :type=>"BLOB", :value=>:NULL},
   {:name=>"se_private_id", :type=>"BIGINT UNSIGNED", :value=>:NULL},
   {:name=>"tablespace_id", :type=>"BIGINT UNSIGNED", :value=>:NULL},
   {:name=>"partition_type", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"partition_expression", :type=>"VARCHAR(6144)", :value=>:NULL},
   {:name=>"default_partitioning", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"subpartition_type", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"subpartition_expression", :type=>"VARCHAR(6144)", :value=>:NULL},
   {:name=>"default_subpartitioning",
    :type=>"CHAR(1) UNSIGNED",
    :value=>:NULL},

   {:name=>"created", :type=>"TIMESTAMP", :value=>"2017-04-03 06:47:50"},
   {:name=>"last_altered", :type=>"TIMESTAMP", :value=>"2017-04-03 06:47:50"},
   {:name=>"view_definition", :type=>"BLOB", :value=>:NULL},
   {:name=>"view_definition_utf8", :type=>"BLOB", :value=>:NULL},
   {:name=>"view_check_option", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"view_is_updatable", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"view_algorithm", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"view_security_type", :type=>"CHAR(1) UNSIGNED", :value=>:NULL},
   {:name=>"view_definer", :type=>"VARCHAR(279)", :value=>:NULL},
   {:name=>"view_client_collation_id",
    :type=>"BIGINT UNSIGNED",
    :value=>:NULL},
   {:name=>"view_connection_collation_id",
    :type=>"BIGINT UNSIGNED",
    :value=>:NULL}],
 :sys=>
  [{:name=>"DB_TRX_ID", :type=>"TRX_ID", :value=>1571},
   {:name=>"DB_ROLL_PTR",
    :type=>"ROLL_PTR",
    :value=>
     {:is_insert=>true, :rseg_id=>58, :undo_log=>{:page=>303, :offset=>272}}}],
 :length=>173,
 :transaction_id=>1571,
 :roll_pointer=>
  {:is_insert=>true, :rseg_id=>58, :undo_log=>{:page=>303, :offset=>272}}}
ふむ。。この手の隠しibdはもうちょっと探ってみても面白い鴨。