MySQLとの戯れ:カラムの別名とグルーピングの動作について

 この日記は、少し時間が取れるようになった私が自由気ままにMySQLと戯れた結果を、つらつらと書き殴ったものです。明確な目的もなく、ただただMySQLと会話をして(そう、MySQLには「対話モード」があるのですよ)、楽しかったぁ、という日記です:-)

 今回は、MySQLのカラムの別名について遊んでみました。特に結論はなく、こういう動きをするのかぁという読み物として見ていただければと思います。原理や内部構造の追加解説は歓迎します。

準備と基本事項の確認

 とりあえず、遊ぶためのテーブルとデータを作成します。

CREATE TABLE t1 (c1 VARCHAR(10), c2 VARCHAR(10));
INSERT INTO t1 VALUES ("TEST11", "TEST12");
INSERT INTO t1 VALUES ("TEST21", "TEST22");
INSERT INTO t1 VALUES ("TEST31", "TEST32");

 2つのカラム、c1 と c2 をSELECTしてみます。普通の動作ですね。

mysql> SELECT c1, c2 FROM t1;
+--------+--------+
| c1     | c2     |
+--------+--------+
| TEST11 | TEST12 |
| TEST21 | TEST22 |
| TEST31 | TEST32 |
+--------+--------+
3 rows in set (0.00 sec)

 同じカラムを何度も取得してみても、まったく問題ありません(ただし、プログラムから呼ばれる時に、この結果をどのように扱えるかについて、問題は発生しそう)。

mysql> SELECT c1, c1 FROM t1;
+--------+--------+
| c1     | c1     |
+--------+--------+
| TEST11 | TEST11 |
| TEST21 | TEST21 |
| TEST31 | TEST31 |
+--------+--------+
3 rows in set (0.00 sec)

別名についての戯れ

 じゃぁ、本当は別のカラムなのに同じ名前にして取得することってできるのかな? ねぇ、MySQLさん、できる?

mysql> SELECT c1, c2 c1 FROM t1;
+--------+--------+
| c1     | c1     |
+--------+--------+
| TEST11 | TEST12 |
| TEST21 | TEST22 |
| TEST31 | TEST32 |
+--------+--------+
3 rows in set (0.00 sec)

 できるみたい。 どっちが本当の c1 か分からないけど、どっちも c1 として別々の値が取得できています。
もちろん、意味もなく使うひとを混乱させるだけの、こんなことだってできます。c1はc2でc2はc1なのでc2という結果は元はc1でc1だと言っているものは元はc2ですからc1が(以下略)

mysql> SELECT c1 c2, c2 c1 FROM t1;
+--------+--------+
| c2     | c1     |
+--------+--------+
| TEST11 | TEST12 |
| TEST21 | TEST22 |
| TEST31 | TEST32 |
+--------+--------+
3 rows in set (0.00 sec)

グルーピングしてみる

 ここまでは、ただ別名を付けただけで、そのまま出力に供される程度のものでした。付けた別名をGROUP BY で使用するケースではどうなるでしょうか。別名を GROUP BY で使えるという MySQL ならではのお遊びです。

 まず基本事項の確認。 c1 でグルーピングするのは、こんなふうに書けます。

mysql> SELECT c1, MAX(c2) FROM t1 GROUP BY c1;
+--------+---------+
| c1     | MAX(c2) |
+--------+---------+
| TEST11 | TEST12  |
| TEST21 | TEST22  |
| TEST31 | TEST32  |
+--------+---------+
3 rows in set (0.00 sec)

 c1に対して別名を付けても、GROUP BY にもとのカラム名(c1)を指定して問題なく動作します。

mysql> SELECT c1 c2, MAX(c2) FROM t1 GROUP BY c1;
+--------+---------+
| c2     | MAX(c2) |
+--------+---------+
| TEST11 | TEST12  |
| TEST21 | TEST22  |
| TEST31 | TEST32  |
+--------+---------+
3 rows in set (0.00 sec)

 c1という名前が、「元々 c1 だったカラム」を明確に示しているので、エイリアスではなく元々のカラム名と解釈してくれているように見えます。
 では、c1という別名を持つカラムがある場合はどう? 混乱する?

mysql> SELECT c1 c2, MAX(c2) c1 FROM t1 GROUP BY c1;
+--------+--------+
| c2     | c1     |
+--------+--------+
| TEST11 | TEST12 |
| TEST21 | TEST22 |
| TEST31 | TEST32 |
+--------+--------+
3 rows in set, 1 warning (0.00 sec)

 「もともとのc1」でグルーピングしてくれますね。すごいや MySQL
でもよく見ると、ワーニングが出ています。なんだ、言いたいことがあるなら、そう言ってくれればいいのに。(言ってる)

mysql> SHOW WARNINGS;
+---------+------+---------------------------------------------+
| Level   | Code | Message                                     |
+---------+------+---------------------------------------------+
| Warning | 1052 | Column 'c1' in group statement is ambiguous |
+---------+------+---------------------------------------------+
1 row in set (0.00 sec)

 c1が ambiguous だって言ってますね、「わかんないよー」って。 でも、分からないのに結果は一応返してくれるMySQLさん、素敵。

エラーにしてみる(いじめ)

 
 ここからは、MySQLさんにもっと無茶振りしてみます。もはや、いじめの様相。
でも、無理なものは無理って言ってくれるので、安心です(なにが)。

 まず、グルーピングカラムに別名をつけて、その別名でグルーピングしてみます。付けた別名は他のカラムに実在の "c2"。

mysql> SELECT c1 c2, MAX(c2) FROM t1 GROUP BY c2;
ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.t1.c1' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

 別名をつけているのに、c2 を別名ではなく実在カラムのほうとして解釈して、エラーとなっているようですね。
念のため(実在カラムとは異なる名前の)別名を付けてみて、別名でグルーピングできること自体は認識誤りでないことを確認してみます。

SELECT c1 a2, MAX(c2) FROM t1 GROUP BY a2;
mysql> SELECT c1 a2, MAX(c2) FROM t1 GROUP BY a2;
+--------+---------+
| a2     | MAX(c2) |
+--------+---------+
| TEST11 | TEST12  |
| TEST21 | TEST22  |
| TEST31 | TEST32  |
+--------+---------+
3 rows in set (0.00 sec)

 a2 の a は alias の a のつもりですが、そんなのどうでもいいです。ちなみに、c1 の c は column ですし、t1 の t は table です。どうでもいいです。
 ここで大事なのは、 GROUP BY a2 という、GROUP BY に別名を与える書き方自体には問題がないことが確認できたことです。

 じゃぁ a2 っていう別名なら動作するので、集約カラムに対しても a2 って名前を付けてみたら、どう? どっちの a2 だと理解してくれるのかな。
 

mysql> SELECT c1 a2, MAX(c2) a2 FROM t1 GROUP BY a2;
ERROR 1056 (42000): Can't group on 'a2'

 わかんないんだって。そりゃそうですよね。
そもそも、 1056 のエラーってほとんど見たことがない気がする。新たな出会いがあった気がして、少し嬉しい。

その他のエラー

 あまり見たことがない(気がする)エラーを見て、面白くなってきたので、グルーピングの他のエラーを試みたくなってきました。
MySQL のエラーリストを見ながら、GROUP BY に関係しそうなものを探して、トライしてみることにします。
https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html

 グループ関数の使い方がおかしい(WHEREで使おうとしている)場合に出るエラー 1111。

mysql> SELECT c1, AVG(c2) a2 FROM t1 WHERE AVG(c2)=0 GROUP BY c1;
ERROR 1111 (HY000): Invalid use of group function

 念のため紹介しておくと、別名を使えばいいとかそういう話ではありません。処理順序の話(SELECT句の別名が決まるのは結構アトのほう)をイメージすると良いと思います。たぶん。ちなみに、この時にはまた別のエラーコードになっていることにも注目。

mysql> SELECT c1, AVG(c2) a2 FROM t1 WHERE a2=0 GROUP BY c1;
ERROR 1054 (42S22): Unknown column 'a2' in 'where clause'


 もうひとつ。1140 エラーを出してみます。書かなければならないGROUP BY を省略した時のエラーです。

mysql> SELECT c1,MAX(c2) FROM t1;
ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'test.t1.c1'; this is incompatible with sql_mode=only_full_group_by

フィナーレ

 あぁ、楽しかった。楽しかったね MySQLさん。
そろそろ飽きてきたから今日は帰るね。あ、そうそう。最後になっちゃったけど、MySQLさん、自己紹介しておく? 
じゃぁね。

mysql> STATUS
--------------
mysql  Ver 8.0.19 for Linux on x86_64 (MySQL Community Server - GPL)

Connection id:          7
Current database:       test
Current user:           root@
SSL:                    Not in use
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server version:         8.0.19 MySQL Community Server - GPL
Protocol version:       10
Connection:             Localhost via UNIX socket
Server characterset:    utf8mb4
Db     characterset:    utf8mb4
Client characterset:    utf8mb4
Conn.  characterset:    utf8mb4
UNIX socket:            /var/run/mysqld/mysqld.sock
Binary data as:         Hexadecimal
Uptime:                 2 days 15 hours 15 min 1 sec

Threads: 1  Questions: 72  Slow queries: 0  Opens: 117  Flush tables: 3  Open tables: 53  Queries per second avg: 0.000
--------------
mysql> select @@session.sql_mode;
+-----------------------------------------------------------------------------------------------------------------------+
| @@session.sql_mode                                                                                                    |
+-----------------------------------------------------------------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)