Quantcast
Channel: Planet MySQL
Viewing all 1081 articles
Browse latest View live

MySQLのオプティマイザヒント

$
0
0
免責事項

この記事はSergey Glukhov氏によるMySQL Server Blogの投稿「New Optimizer Hints in MySQL」(2015/7/29)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


MySQLのオプティマイザが特殊なシステム変数、optimizer_switchを持つことは良く知られており、これは様々なインデックスコンディションプッシュダウン(ICP)、バッチキーアクセスなどの様々なオプティマイザのモードを制御できるようにするものだ。optimizer_switchを利用する1つの大きな欠点はこれがステートメント全体に影響することで、しばしば特定のテーブルあるいはクエリブロックの挙動のみを変えた方が望ましい、変える必要があることがある。これらの問題に対処し、使い勝手を向上させる為に、新しくヒント句が追加された。これにより、より強力な、またきめの細かいオプティマイザの挙動の制御がSQL文中で直接実施できる。

新しいヒント句に関していくつかの重要なポイントをあげる。

  • コメント文法 /*+ */ が新しいヒント句として利用できる
  • 同一コメント中で、複数のヒント句が指定できる。
  • クエリブロックはヒント句を含め1つしかコメントを持つことが出来ず、コメントはSELECT、UPDATE、INSERT、REPLACE、またはDELETEの直後に付与しなければならない。
  • ヒント句中の正しくない、または無効な文法は警告を出力させる。
  • ヒントがコンフリクトしている場合、最初のヒント句のみ有効となりそれに続くコンフリクトしているあるいは重複するヒントは警告を出力し無効化される。
  • 複数レベルのヒント句がサポートされている。例えば、ヒント句は特定のテーブルまたはクエリブロックのみに影響を与えるようにすることができる。

次のヒント句はMySQL 5.7.7から追加されたものだ。

  • BKANO_BKA : 特定のテーブルまたはクエリブロックへのバッチキーアクセスアルゴリズムの利用を制御する
  • BNLNO_BNL : 特定のテーブルまたはクエリブロックへのブロックネスティッドループアルゴリズムの利用を制御する
  • MRRNO_MRR : 特定のインデックスまたはテーブルへのマルチレンジリードを制御する
  • NO_ICP : 特定のインデックスまたはテーブルへのインデックスコンディションプッシュダウンの利用を無効化する
  • NO_RANGE_OPTIMIZATION : 特定のインデックスまたはテーブルへのレンジアクセスの利用を無効化する
  • MAX_EXECUTION_TIME : ステートメント実行タイムアウトをNミリ秒に設定する
  • QB_NAME : 特定のクエリブロックに名前を付ける為の補助的なヒント句。この名前は後で複雑な複合ステートメントの中でのヒント句の指定をシンプル化する為に利用できる

MySQL 5.7.8では、Oystein Grovlenがサブクエリのオプティマイザを制御するヒント句戦略を追加した。

  • SEMIJOINNO_SEMIJOIN : セミジョイン戦略を有効化または無効化する
  • SUBQUERY : サブクエリ実体化またはEXISTS戦略(IN-to-EXISTS transformations)を使うかどうかに影響する

新しいヒント句に関する追加の営みの詳細については、マニュアルの新しい節を参照してほしい。

私は新しいヒント句が役立つと分かってもらえるとうれしい! 新しいヒント句について疑問点をお持ちの場合や、問題に遭遇した場合は、ここのコメントで知らせていただき、bugs.mysql.comのバグレポートをオープンするかサポートチケットをオープンして欲しい。(訳注: コメントは元サイトに記載お願いします)

MySQLを使ってくれてありがとう!


PlanetMySQL Voting: Vote UP / Vote DOWN

An idea for using MySQL 5.7's generated column like CHECK constraint

$
0
0
This is translation for my early post in Japanese.

As of MySQL 5.7.6, generated column was introduced as 5.7's new feature.
I found that generated column could behave like CHECK constraint.
Let's start :)


mysql57> CREATE TABLE t1 (num int primary key, val varchar(32)) Engine= InnoDB;
Query OK, 0 rows affected (0.03 sec)

mysql57> INSERT INTO t1 (num, val) VALUES (1, '2015-08-06');
Query OK, 1 row affected (0.01 sec)

mysql57> SELECT * FROM t1;
+-----+------------+
| num | val |
+-----+------------+
| 1 | 2015-08-06 |
+-----+------------+
1 row in set (0.00 sec)

First, there's the bad designed table which has DATE-string in its VARCHAR column.


mysql57> ALTER TABLE t1 ADD check_val datetime AS (CAST(val AS datetime));
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

Second, add VIRTUAL generated column `check_val` which calculates DATETIME from `val`'s DATE-string.


mysql57> INSERT INTO t1 (num, val) VALUES (2, '2015-09-31');
ERROR 1292 (22007): Incorrect datetime value: '2015-09-31'

mysql57> SELECT * FROM t1;
+-----+------------+---------------------+
| num | val | check_val |
+-----+------------+---------------------+
| 1 | 2015-08-06 | 2015-08-06 00:00:00 |
+-----+------------+---------------------+
1 row in set (0.00 sec)

mysql57> INSERT INTO t1 (num, val) VALUES (2, '2015-09-30');
Query OK, 1 row affected (0.00 sec)

mysql57> SELECT * FROM t1;
+-----+------------+---------------------+
| num | val | check_val |
+-----+------------+---------------------+
| 1 | 2015-08-06 | 2015-08-06 00:00:00 |
| 2 | 2015-09-30 | 2015-09-30 00:00:00 |
+-----+------------+---------------------+
2 rows in set (0.00 sec)

After that, you can see INSERT statement with incorrect DATE-string fails because of `check_val`'s CAST function raises warning and strict sql_mode can't allow that warning.


mysql57> SET sql_mode= '';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql57> SELECT @@sql_mode;
+------------+
| @@sql_mode |
+------------+
| |
+------------+
1 row in set (0.00 sec)

mysql57> INSERT INTO t1 (num, val) VALUES (3, '2015-11-31');
Query OK, 1 row affected, 1 warning (0.02 sec)

mysql57> SHOW WARNINGS;
+---------+------+----------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------+
| Warning | 1292 | Incorrect datetime value: '2015-11-31' |
+---------+------+----------------------------------------+
1 row in set (0.00 sec)

mysql57> SELECT * FROM t1;
+-----+------------+---------------------+
| num | val | check_val |
+-----+------------+---------------------+
| 1 | 2015-08-06 | 2015-08-06 00:00:00 |
| 2 | 2015-09-30 | 2015-09-30 00:00:00 |
| 3 | 2015-11-31 | NULL |
+-----+------------+---------------------+
3 rows in set, 1 warning (0.00 sec)

This way depends on sql_mode is strict or not.
Then I tried another way using IF function, which returns NULL when check condition is false, with NOT NULL constraint.


mysql57> CREATE TABLE t2 (num int primary key, val varchar(32), check_val tinyint AS (IF(val IN ('Tokyo', 'Osaka'), 1, NULL)) NOT NULL) Engine= InnoDB;
Query OK, 0 rows affected (0.02 sec)

mysql57> INSERT INTO t2 (num, val) VALUES (1, 'Tokyo');
Query OK, 1 row affected (0.00 sec)

mysql57> INSERT INTO t2 (num, val) VALUES (2, 'Osaka');
Query OK, 1 row affected (0.01 sec)

mysql57> INSERT INTO t2 (num, val) VALUES (3, 'Nara');
ERROR 1048 (23000): Column 'check_val' cannot be null

mysql57> SELECT * FROM t2;
+-----+-------+-----------+
| num | val | check_val |
+-----+-------+-----------+
| 1 | Tokyo | 1 |
| 2 | Osaka | 1 |
+-----+-------+-----------+
2 rows in set (0.00 sec)

I seem this approach is better.
NOT NULL constraint doesn't depend on sql_mode.

This generated column feature can be introduced into tables which have already been filled data.


mysql57> SELECT * FROM t3; -- Invalid data has already been inserted.
+-----+-------+
| num | val |
+-----+-------+
| 1 | Tokyo |
| 2 | Osaka |
| 3 | Nara |
+-----+-------+
3 rows in set (0.00 sec)

mysql57> ALTER TABLE t3 ADD check_val tinyint AS (IF(val IN ('Tokyo', 'Osaka'), 1, NULL)) NOT NULL; -- Add VIRTUAL generated column.
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql57> SELECT * FROM t3;
+-----+-------+-----------+
| num | val | check_val |
+-----+-------+-----------+
| 1 | Tokyo | 1 |
| 2 | Osaka | 1 |
| 3 | Nara | 0 |
+-----+-------+-----------+
3 rows in set (0.00 sec)

mysql57> INSERT INTO t3 (num, val) VALUES (4, 'Kyoto');
ERROR 1048 (23000): Column 'check_val' cannot be null

With VIRTUAL generated column, the data which is already stored don't be affected by fake CHECK constraint.

VIRTUAL: Column values are not stored, but are evaluated when rows are read, immediately after any BEFORE triggers.
http://dev.mysql.com/doc/refman/5.7/en/create-table.html#create-table-generated-columns

VIRTUAL generated `check_val` doesn't calculate at the time of ALTER TABLE, thus this case doesn't affect stored data but new data are under constraint.


mysql57> ALTER TABLE t3 DROP check_val;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql57> SELECT * FROM t3;
+-----+-------+
| num | val |
+-----+-------+
| 1 | Tokyo |
| 2 | Osaka |
| 3 | Nara |
+-----+-------+
3 rows in set (0.00 sec)

mysql57> ALTER TABLE t3 ADD check_val tinyint AS (IF(val IN ('Tokyo', 'Osaka'), 1, NULL)) STORED NOT NULL;
ERROR 1048 (23000): Column 'check_val' cannot be null

The other hand, with STORED generated column, all exist data are calculated and checked during ALTER TABLE.
If the table already has invalid (for fake CHECK constraint) data, ALTER TABLE fails.

This is a stage of idea, is not practice in production environment.
But this idea maybe satisfy some case of "OMG, doesn't MySQL have CHECK constraint!?" situation.
PlanetMySQL Voting: Vote UP / Vote DOWN

MySQLによるJSONデータ型処理

$
0
0

MySQL5.7において、JSON(JavaScript Object Notation)がサポートされていて、
様々な言語やアプリ間で、より簡単にデータの受け渡しが行えるようになりました。
ドキュメントをJSONフォーマットでデータベースに保存して、
様々なアプリケーションから呼び出して利用出来るなど、
汎用性と運用面での活用が出来るようになっています。

1) ネイティブのJSONデータ型
効率的なデータ処理と保管にネイティブ内部バイナリ形式をサポート。
2) 組み込みJSONファンクション
効率よくドキュメントを保存,検索,更新,操作する事を可能にします。
3) JSON コンパレーター
文書データを容易にSQLクエリと統合することが可能
4) Generated Columnsを利用し、ドキュメントにインデックスを利用する事が可能。
新しいアナライザーは,自動的に利用可能で最適な機能インデックスを利用。

ファンクションの詳細は、こちらを参照してください。
12.16 JSON Functions

検証バージョン


root@localhost [USER01]> select @@version;
+--------------+
| @@version    |
+--------------+
| 5.7.8-rc-log |
+--------------+
1 row in set (0.00 sec)

検証用テーブル


root@localhost [USER01]> show create table T_JSON\G
*************************** 1. row ***************************
       Table: T_JSON
Create Table: CREATE TABLE `T_JSON` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `body` json DEFAULT NULL,
  `WithTax` decimal(10,2) GENERATED ALWAYS AS (json_extract(body,'$.price')*1.08) VIRTUAL,
  PRIMARY KEY (`id`),
  KEY `idx_total_cost_v` (`WithTax`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

root@localhost [USER01]> 

※WithTaxは、MySQL5.7から利用可能なGenerated Columnsで作成して、
 文書内のデータからインデックスを作成出来るようにしています。
 ここでは、文書からpriceを抜き出して、TAXを含むコストをベースにインデックスを作成しています。

テストデータ


root@localhost [USER01]> INSERT INTO T_JSON(body) VALUES ('{"id":1,"name":"自転車","price":10000,"Conditions":["NEW",2015]}');
Query OK, 1 row affected (0.15 sec)

root@localhost [USER01]> INSERT INTO T_JSON(body) VALUES ('{"id":2,"name":"TV","price":30000,"Conditions":["USED",2013]}');
Query OK, 1 row affected (0.01 sec)

root@localhost [USER01]> INSERT INTO T_JSON(body) VALUES ('{"id":3,"name":"冷蔵庫","price":50000,"Conditions":["NEW",2015]}');
Query OK, 1 row affected (0.00 sec)

root@localhost [USER01]> INSERT INTO T_JSON(body) VALUES ('{"id":4,"name":"冷蔵庫","price":50000,"Conditions":["NEW",2015]}');
Query OK, 1 row affected (0.00 sec)

root@localhost [USER01]> INSERT INTO T_JSON(body) VALUES ('{"id":5,"name":"自転車","price":25000,"Conditions":["NEW",2015]}');
Query OK, 1 row affected (0.01 sec)

<SNIP>

JSONデータ型で作成した列に、データをINSERTする時にフォーマットが正しいかバリデーションしてくれます。
間違えていると以下のようにエラーになります。


root@localhost [USER01]> INSERT INTO T_JSON(body) VALUES ('{"id":6,"name":"冷蔵庫","price":50000,"Conditions":["NEW",2015]');
ERROR 3140 (22032): Invalid JSON text: "Missing a comma or '}' after an object member." at position 66 in value (or column) '{"id":6,"name":"冷蔵庫","price":50000,"Conditions":["NEW",2015]'.
root@localhost [USER01]> 

データの参照


root@localhost [USER01]> select * from T_JSON;
+----+-----------------------------------------------------------------------------+----------+
| id | body                                                                        | WithTax  |
+----+-----------------------------------------------------------------------------+----------+
|  1 | {"id": 1, "name": "自転車", "price": 10000, "Conditions": ["NEW", 2015]}    | 10800.00 |
|  2 | {"id": 2, "name": "TV", "price": 30000, "Conditions": ["USED", 2013]}       | 32400.00 |
|  3 | {"id": 3, "name": "冷蔵庫", "price": 50000, "Conditions": ["NEW", 2015]}    | 54000.00 |
|  4 | {"id": 4, "name": "冷蔵庫", "price": 50000, "Conditions": ["NEW", 2015]}    | 54000.00 |
|  5 | {"id": 5, "name": "自転車", "price": 25000, "Conditions": ["NEW", 2015]}    | 27000.00 |
+----+-----------------------------------------------------------------------------+----------+
5 rows in set (0.01 sec)

root@localhost [USER01]> SELECT json_extract(body,'$.name') FROM T_JSON;
+-----------------------------+
| json_extract(body,'$.name') |
+-----------------------------+
| "自転車"                    |
| "TV"                        |
| "冷蔵庫"                    |
| "冷蔵庫"                    |
| "自転車"                    |
+-----------------------------+
5 rows in set (0.00 sec)

root@localhost [USER01]> 

select

該当のデータを含むデータの抽出例 (json_search())等。


root@localhost [USER01]> SELECT json_extract(body,'$.name')="自転車" FROM T_JSON;
+-----------------------------------------+
| json_extract(body,'$.name')="自転車"    |
+-----------------------------------------+
|                                       1 |
|                                       0 |
|                                       0 |
|                                       0 |
|                                       1 |
+-----------------------------------------+
5 rows in set (0.00 sec)

root@localhost [USER01]> SELECT id,json_search(body,'one','%自転車%') FROM T_JSON;
+----+---------------------------------------+
| id | json_search(body,'one','%自転車%')    |
+----+---------------------------------------+
|  1 | "$.name"                              |
|  2 | NULL                                  |
|  3 | NULL                                  |
|  4 | NULL                                  |
|  5 | "$.name"                              |
+----+---------------------------------------+
5 rows in set (0.00 sec)

root@localhost [USER01]> SELECT json_extract(body,'$.name') 
    -> FROM T_JSON where json_extract(body,'$.name') = '自転車';
+-----------------------------+
| json_extract(body,'$.name') |
+-----------------------------+
| "自転車"                    |
| "自転車"                    |
+-----------------------------+
2 rows in set (0.00 sec)

root@localhost [USER01]> 

JONS_WHERE

Generated Columnにインデックスを張ってあるので、インデックスを使用したドキュメントの検索の確認。


root@localhost [USER01]> SELECT json_extract(body,'$.name') FROM T_JSON where json_extract(body,'$.price') > 40000;
+-----------------------------+
| json_extract(body,'$.name') |
+-----------------------------+
| "冷蔵庫"                    |
| "冷蔵庫"                    |
+-----------------------------+
2 rows in set (0.00 sec)

root@localhost [USER01]> SELECT json_extract(body,'$.name') FROM T_JSON where WithTax > 40000;
+-----------------------------+
| json_extract(body,'$.name') |
+-----------------------------+
| "冷蔵庫"                    |
| "冷蔵庫"                    |
+-----------------------------+
2 rows in set (0.00 sec)

root@localhost [USER01]> explain SELECT json_extract(body,'$.name') FROM T_JSON where json_extract(body,'$.price') > 40000;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | T_JSON | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

root@localhost [USER01]> explain SELECT json_extract(body,'$.name') FROM T_JSON where WithTax > 40000;
+----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+
| id | select_type | table  | partitions | type  | possible_keys    | key              | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | T_JSON | NULL       | range | idx_total_cost_v | idx_total_cost_v | 6       | NULL |    2 |   100.00 | Using where |
+----+-------------+--------+------------+-------+------------------+------------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

root@localhost [USER01]> 

index

参考:
12.16 JSON Functions
https://dev.mysql.com/doc/refman/5.7/en/json-functions.html

JSON Labs Release: Effective Functional Indexes in InnoDB
http://mysqlserverteam.com/json-labs-release-effective-functional-indexes-in-innodb/

MySQL and PostgreSQL JSON functions: do they differ much?
https://blogs.oracle.com/svetasmirnova/entry/mysql_and_postgresql_json_functions

JSON Labs Release: Effective Functional Indexes in InnoDB
http://mysqlserverteam.com/json-labs-release-effective-functional-indexes-in-innodb/


PlanetMySQL Voting: Vote UP / Vote DOWN

InnoDBにおける効果的な関数インデックス(MySQL Server Blogより)

$
0
0
免責事項

この記事はJimmy Yang氏によるMySQL Server Blogの投稿「JSON Labs Release: Effective Functional Indexes in InnoDB」(2015/4/9)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


MySQL 5.7.6では、我々は生成列(Generated Columns)と呼ばれる新しい機能を追加した。最初の段階では全ての生成列は、仮想的なものであってもマテリアライズされていた。これでは不要なディスクスペースが使用され、ディスクI/Oが発生するだけでなく、いかなるテーブルの変更に対してもテーブルを完全にリビルドする必要がある。新しいMySQL 5.7.7のJSON Labリリースでは、全ての問題を新しい機能、すなわちユーザーがマテリアライズされない仮想列を生成できるだけでなく、インデックスを作成できる機能、これをInnoDBを実装することで解決した。仮想列のデータは関数を利用して生成することもできるので、ある程度までは、"仮想インデックス"を関数インデックスまたは関数ベースのインデックスとしてみることができる。

このブログ投稿では、仮想列と仮想インデックスの設計について少し詳細にみて、InnoDBでどのように実装されているかの理解をお手伝いしようと思う。

InnoDBにおける仮想列

まずはInnoDB内部で仮想列がどのように表現されるかをみてみよう。簡単にいうと、仮想列は今やInnoDBのユーザーテーブルにも、クラスタインデックスにも全く保存されず、InnoDBのシステムテーブル上で表現される。詳細をみてみよう。


1. 仮想列はInnoDBのテーブル内には保存されない

仮想列はInnoDBにおいて今や真の"仮想"であり、これはInnoDBはクラスタインデックス(テーブルのデータを基本的に格納するのに使われる)内の列にいかなるデータも保存しないことを意味する。例を見てみよう。

# 仮想列の例
mysql> CREATE TABLE t (a INT, b INT, c INT GENERATED ALWAYS AS(a+b), PRIMARY KEY(a));
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO t VALUES (11, 3, default);
Query OK, 1 row affected (0.00 sec)

ここで列'c'は仮想列である。InnoDBのこのテーブルの物理的なデータ配置を見てみると、2つのユーザー列'a'と'b'および、2つの標準であるInnoDBの隠し/内部列(DATA_TRX_IDとDATA_ROLL_PTR)しか持っていない

# 物理的なレコード配置
not-deleted PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 8000000b; asc     ;;           /* column 'a' */
 1: len 6; hex 00000000670b; asc     g ;;     /* InnoDB hidden column */
 2: len 7; hex a90000011d0110; asc        ;;  /* InnoDB hidden column */
 3: len 4; hex 80000003; asc     ;;           /* column 'b' */

したがって'c'列はInnoDBのテーブルや列として保存されておらず、代わりにテーブルにクエリを発行した時にその場で計算される。

# クエリの例
mysql> SELECT * FROM t;
+----+------+------+
| a  | b    | c    |
+----+------+------+
| 11 |    3 |   14 |
+----+------+------+
1 row in set (0.00 sec)

2. 仮想列のメタデータの表現

仮想列自体はInnoDB内に保存されないとしても、メタデータはある。そのような列にセカンダリインデックスを作成するのをサポートする為にメタデータを保存する必要があるのだ。

仮想列のメタデータ情報は、InnoDBのSYS_COLUMNSシステムテーブルに他の列と共に保存されており、'PRTYPE'の値に追加のDATA_VIRTUAL(8192)ビットが設定されている点のみが異なる。

# SYS_COLUMNSの例
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_COLUMNS 
WHERE TABLE_ID IN (SELECT TABLE_ID FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES WHERE NAME LIKE "t%");

+----------+------+-------+-------+--------+-----+
| TABLE_ID | NAME | POS   | MTYPE | PRTYPE | LEN |
+----------+------+-------+-------+--------+-----+
|       74 | a    |     0 |     6 |   1283 |   4 |
|       74 | b    |     1 |     6 |   1027 |   4 |
|       74 | c    | 65538 |     6 |   9219 |   4 |
+----------+------+-------+-------+--------+-----+
3 rows in set (0.01 sec)

仮想列'c'が、SYS_COLUMNSシステムテーブルに登録されており、'PRTYPE'にDATA_VIRTUAL(8192)ビットがたっている点に注意して欲しい。'POS'フィールドもまた特別で、元のテーブルの位置(第3カラム)と、仮想列としてのシークエンス(最初の仮想列)の両方をエンコードしている。

SYS_COLUMNSシステムテーブルに加え、SYS_VIRTUALと呼ばれるシステムテーブルも新しく追加した。仮想列が他の列(基底列, the base columns)にもとづいて生成されているかどうかを記録するためである。上記の例では、'c'列は'a'列および'b'列から計算されている。

# SYS_VIRTUALの例
myql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_VIRTUAL;
+----------+-------+----------+
| TABLE_ID | POS   | BASE_POS |
+----------+-------+----------+
|       74 | 65538 |        0 |
|       74 | 65538 |        1 |
+----------+-------+----------+
2 rows in set (0.00 sec)

上記の例では、'POS'列はSYS_COLUMNS内の仮想列の'POS'の値を表し(この例では'c'列)、'BASE_POS'はSYS_COLUMNS内の基底列の'POS'の値(0は'a'列、1は'b'列)を表す。現在のところ、"基底列"は通常のマテリアライズされた列のみで構成可能であり、他の生成列からは構成できない。

仮想列が他の標準の列と同様にシステムテーブルに追加されたが、これらは別ドメインに表現されておりメモリ内のメタデータを格納する為の通常の列ではない。このようにして、InnoDBにはほとんど変更を加える必要はない。なぜならば、大多数のケースで仮想列が存在しないものとして動作し続けるからである。しかし、それと同時に実際に必要になればいつでも仮想列の情報を取得することができる。例えば、dict_table_t::cols構造体は通常のマテリアライズされた列の全ての情報を持っており、一方で新しいdict_table_t::v_cols構造体は仮想またはマテリアライズされない列の全ての情報を持っている。


この設計において、仮想列は簡単に追加して削除することができ、かつテーブル全体をリビルドする必要もない。これにより関連するテーブルのスキーマ変更がとても簡単かつ高速になる。

# 高速な仮想列の管理
mysql> ALTER TABLE t ADD new_col INT GENERATED ALWAYS AS (a - b) VIRTUAL;
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM t;
+----+------+------+---------+
| a  | b    | c    | new_col |
+----+------+------+---------+
| 11 |    3 |   14 |       8 |
+----+------+------+---------+
1 row in set (0.01 sec)

マテリアライズしない仮想カラムへのインデックスの作成

前の節でみたように、仮想列はとても柔軟であり、簡単に追加や削除ができる。しかしながら、仮想列はInnoDBのクラスタインデックス内に保存されていない為、クエリで値を取得する為に条件を満たす可能性のある各列に対し、基底列のデータを取得し、必要な計算をする必要がある。これによりクエリが遅く、非効率なものになる。しかし通常の列と同じ位クエリを効率的にする方法がある!今や仮想列にセカンダリインデックスを簡単に作ることができるのだ!

ひとたびセカンダリインデックスが仮想列に作成されたら、仮想列のデータはセカンダリインデックスのレコードに必然的にマテリアライズされ格納される。これは仮想列にクエリが発行された際に、仮想列の値を計算する必要がないことを意味している。再度になるが、これは関数インデックス関数ベースのインデックスを効果的にするものだ。

1. 仮想インデックスの作成

インデックス作成の記法は他のセカンダリインデックスを作成する時と同様である。

# 仮想列上のインデックスの作成
mysql> CREATE INDEX idx ON t(c);
Query OK, 1 row affected (0.08 sec)
Records: 1  Duplicates: 0  Warnings: 0

新しい'idx'インデックスがSYS_INDEXESシステムテーブルに追加され、仮想列'c'はSYS_FIELDSシステムテーブルに追加される。

# SYS_INDEXSおよびSYS_FIELDSレコードの例
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_INDEXES WHERE TABLE_ID IN (SELECT TABLE_ID FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES WHERE NAME LIKE "t%");
+----------+---------+----------+------+----------+---------+-------+-----------------+
| INDEX_ID | NAME    | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE | MERGE_THRESHOLD |
+----------+---------+----------+------+----------+---------+-------+-----------------+
|      123 | PRIMARY |       75 |    3 |        1 |       3 |    63 |              50 |
|      124 | idx     |       75 |  128 |        1 |       4 |    63 |              50 |
+----------+---------+----------+------+----------+---------+-------+-----------------+
2 rows in set (0.01 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_FIELDS WHERE INDEX_ID = 124;
+----------+------+-----+
| INDEX_ID | NAME | POS |
+----------+------+-----+
|      124 | c    |   0 |
+----------+------+-----+
1 row in set (0.03 sec)

仮想列はInnoDBのメタデータシステムテーブル内にある他の列と同じように保存されるため、仮想インデックスのメタデータは通常のインデックスのメタデータと同じように表現される。

通常の列に対するインデックス作成との違いは、インデックス作成時に、インデックスがはられた列が仮想列だと分かったら、最終的に特定の生成関数を呼ぶ前にその"基底列"が取得されコールバック関数が基底列にアクセスするのに使われるところだ。一度このコールバック関数から値が計算されれば、値はソーターに渡され、後でインデックスレコードをインスタンス化するのに使われる。

2. DMLの実行

仮想列のデータは今やセカンダリインデックスを通じて"マテリアライズされ"ているため、どのようなDML(INSERT、UPDATE、DELETE)でもインデックスに影響をおよぼしうる。仮想列の値は他のインデクスがはられた列の値と同様に更新される。しかし、仮想列に対して値を直接INSERTまたはUPDATEすることはできない。代わりにINSERTとUPDATE操作は基底列の変更を通じて間接的に実行される。前に示した例のテーブルを使ってこれを実演してみよう。

# インデックスレコードの更新の実例
mysql> select * from t;
+----+------+------+---------+
| a  | b    | c    | new_col |
+----+------+------+---------+
| 11 | 3    | 14   | 8       |
+----+------+------+---------+
1 row in set (0.00 sec)

mysql> UPDATE t SET a = 20;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from t;
+----+------+------+---------+
| a  | b    | c    | new_col |
+----+------+------+---------+
| 20 | 3    | 23   | 17      |
+----+------+------+---------+
1 row in set (0.01 sec)

mysql> EXPLAIN SELECT c FROM t;
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| 1  | SIMPLE      | t     | NULL       | index | NULL          | idx  | 5       | NULL | 1    | 100.00   | Using index |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT c FROM t;
+------+
| c    |
+------+
| 23   |
+------+
1 row in set (0.00 sec)

ご覧いただいたように、列'c'のインデックスの値は基底列'a'が更新されるのに伴い更新されている。

ここで注目いただきたいのは、データの変更がMVCC、クラッシュリカバリ、UNDO操作に対して再計算が不要であるように仮想列のDMLをログに記録していることである。もちろんこれらのインデックスがはられた仮想列への操作だけは記録される。

"関数インデックス"を使ったクエリ

仮想列の"関数インデックス"があれば、ユーザーは条件を満たした列をカバードスキャンおよびノンカバードスキャンの両方を利用して検索できる。"関数インデックス"はクエリ可能で、シナリオ(分離レベルなど)に応じて、クラスタインデックスが続いて参照される。

UNDOログ内に仮想カラムの更新も記録する為、仮想列へのクエリもMVCCをサポートする。しかしながら、最大インデックスサイズの制限を考慮する必要があり、COMPACT/REDUNDANT ROWフォーマットなら767バイト、COMPRESSED/DYNAMIC ROWフォーマットなら3072バイトである。クエリが発行されたオブジェクトがこれより長い場合は、値は基底列からその場で生成されることになる。

クエリは全ての分離レベルをサポートしており、これはしかるべき場合にはインデックスがはられた仮想列にギャップロックを生成しうることを意味する。

その上、"関数インデックス"はユニークインデックスだけでなく、プレフィックスインデックスもサポートしている。ここに新しいJSONサポートを含む全ての例を示す。

# JSONベースの例
mysql> create table employees(id bigint not null primary key auto_increment, info JSON);
Query OK, 0 rows affected (0.20 sec)

mysql> insert into employees (info) values ('{ "name": "Matt Lord", "age": 38, "Duties": { "Product Manager": ["stuff", "more stuff"]} }');
Query OK, 1 row affected (0.04 sec)

mysql> select jsn_valid(info) from employees;
+-----------------+
| jsn_valid(info) |
+-----------------+
|               1 |
+-----------------+
1 row in set (0.00 sec)

mysql> select id, jsn_extract(info, '$.name') from employees;
+----+-----------------------------+
| id | jsn_extract(info, '$.name') |
+----+-----------------------------+
|  1 | "Matt Lord"                 |
+----+-----------------------------+
1 row in set (0.00 sec)

mysql> alter table employees add name varchar(100) generated always as (jsn_extract(info, '$.name')) virtual;
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table employees add index (name);
Query OK, 1 row affected (0.51 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> show create table employees\G
*************************** 1. row ***************************
   Table: employees
Create Table: CREATE TABLE `employees` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `info` json DEFAULT NULL,
  `name` varchar(100) GENERATED ALWAYS AS (jsn_extract(info, '$.name')) VIRTUAL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> explain format=JSON select id, name from employees where name = "Matt Lord"\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
"select_id": 1,
"cost_info": {
  "query_cost": "1.20"
},
"table": {
  "table_name": "employees",
  "access_type": "ref",
  "possible_keys": [
    "name"
  ],
  "key": "name",
  "used_key_parts": [
    "name"
  ],
  "key_length": "103",
  "ref": [
    "const"
  ],
  "rows_examined_per_scan": 1,
  "rows_produced_per_join": 1,
  "filtered": "100.00",
  "using_index": true,
  "cost_info": {
    "read_cost": "1.00",
    "eval_cost": "0.20",
    "prefix_cost": "1.20",
    "data_read_per_join": "128"
  },
  "used_columns": [
    "id",
    "info",
    "name"
  ]
}
  }
}
1 row in set, 1 warning (0.00 sec)

制限事項

仮想インデックス周辺には現在いくつかの制限事項があり、そのうちのいくつかをここにあげる。

  1. 主キーはいかなる仮想列も包含できない
  2. 仮想列と非仮想列の混在に対してはインデックスが生成できない
  3. 仮想列に空間あるいはフルテキストインデックスは生成できない(この制限事項は今後解消される)
  4. 仮想インデックスは外部キーとして利用できない

まとめ

要約すれば、新しく実装された仮想列、仮想インデックス、効果的な"関数インデックス"により、ユーザーは新しい仮想列をすばやく追加/削除できるようになり、そのような列にセカンダリインデックスを生成することで効果的なクエリが発行できる可能性がある。大きなTEXT/JSONフィールドやその他のリレーショナルでないデータのインデクスの理想的な解決策となり、効率的なデータ収容、操作、そのようなデータへのクエリ発行の助けとなる。

これらの新機能についての考えを教えて欲しい!みなからフィードバックを必要としており、生成列やJSONサポートの拡張関連でその他に知りたいことを教えて欲しい。新しい機能を使う上で問題に直面したら、ここのコメントで知らせていただき、bugs.mysql.comのバグレポートをオープンするかサポートチケットをオープンして欲しい。

MySQLを使ってくれてありがとう!


PlanetMySQL Voting: Vote UP / Vote DOWN

【挑戦者求む!】The MySQL 5.7のオプティマイザ チャレンジ

$
0
0
出典について

この記事はMaster MySQL内のMorgan Tocker氏の記事「The MySQL 5.7 Optimizer Challenge」(2015/7/15)を翻訳したものです。


MySQLチームでは、オプティマイザのリファクタリングおよびコストモデルの改善に力を入れて取組んできた。 ストレージエンジンのオプティマイザへのハックはロールバックされ、以前にリリースされたMySQLより最適な実行計画が得られる確率ははるかに高くなったはずだ。

オプティマイザチームは、サーバーおよびストレージエンジンの両面の観点に基づいて、コストに関する定数をコンフィグ可能とし、その結果として我々は標準のInnoDBエンジンが常に「MyISAM同様によく」機能するであろうと自信を持っている(オプティマイザは元々大多数がMyISAMを中心として作られたため、MyISAMはアドバンテージがある)

今日、ある課題を発表したいと思う

オプティマイザがMyISAMに対しては正しいが、InnoDBのテーブルに対しては誤った実行計画を選択する例を見つけて欲しい。 テストケースの再現を実証できれば、あなたにはMySQL 5.7のコミュニティー コントリビューターのポロシャツをプレゼントしよう。

この特別なTシャツには数に限りがあるが、世界中のどこにでもお届けしよう。

The MySQL 5.7 Community Contributor Polo, as modeled by Daniël van Eeden. I'm the guy on the left.

MySQL 5.7のコミュニティー コントリビューターのポロシャツはDaniël van Eedenがデザインしてくれた。左側が私である。

※訳注: 元の記事のコメント欄などに書込んでみてください。


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQL Enterprise Auditの監査ログのデータをパースしてテーブルに挿入する(MySQL Server Blogより)

$
0
0
免責事項

この記事はTony Darnell氏によるMySQL Server Blogの投稿「MySQL Enterprise Audit : Parsing Audit Information From Log Files, Inserting Into MySQL Table」(2015/7/8)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


MySQL Enterprise Auditプラグインは、MySQL Enterprise Edition(有償ライセンス)の一部分である。基本的には、Enterprise AuditはMySQLサーバ上で起こっているすべてのことを追跡し、情報が誤って使われることを防ぎ、検知するのに利用できる。 これはまたHIPAASarbanes-OxlayそしてPCI-DSSを含む有名なコンプライス基準を満たすことができる。

MySQL Enterprise Auditでは、公開されているMySQL Audit APIを利用しており、標準で利用できる特定MySQLサーバ上でのポリシーベースのモニタリングおよび、接続や実行されたクエリの記録を実現している。Oracleの監査仕様を満たす為に、MySQL Enterprise Auditは特有の使いやすいアプリケションの監査およびコンプライアンスのソリューションを提供し、これは内外両方の規約で管理される。

インストールされた場合は、監査プラグインはMySQLサーバがサーバの活動に関する監査レコードを含むログファイルを生成する機能を有効化する。 ログにはいつクライアントが接続し切断したか、接続中に何をしたのか、どのデータベースおよびテーブルにアクセスしたかなどが記録される。

(訳注: 原文はマニュアルの英語の引用だが翻訳した文章を記載した。マニュアル原文は、下記アドレスを参照のこと。)

(from https://dev.mysql.com/doc/refman/5.6/en/audit-log-plugin.html)

MySQL Enterprise Auditを有効化すれば、ログファイルがMySQLのデータディレクトリに生成される。 MySQL Workbench(Enterprise Edition)、mysqlauditgrep、またはOracle Audit Vaultの様なツールを使うと、ログのデータをインポートし、内容を確認したり、検索したり、レポートを作成したりできる。

監査のデータがテーブルに格納できるかどうか知りたがっているある顧客と話をする機会があった。 現在(MySQL 5.6.25)では、監査の情報は監査ログファイルにXML形式で保存される。 そこで私はPerlのスクリプトを即興で作成し、XMLのログファイルをパースしてMySQLデータベースに情報を挿入するようにすることを決めた。 my.cnfまたはmy.iniの設定ファイルにaudit_log_rotate_on_size変数を監査ログファイルのサイズとして指定する必要があり、またこのサイズはデータベースの利用状況やスクリプトがログファイルをどの程度うまくパースできるかを元に調整する必要がある。ログファイルが巨大な場合は、Perlのスクリプトが処理中に問題を起こす可能性があり、この場合ログファイルのサイズを減らしスクリプトをもっと高頻度に動作させたくなるだろう。


警告

Enterprise Auditを利用するにはMySQLのライセンスが必要となる。もしEnterpriseのサブスクリプションに興味を持たれたら下のコメント欄から私に連絡して欲しい。(訳注: 元記事のコメント欄に記載のこと)。Enterpriseのライセンスをお持ちの場合は、Enterprise Auditの設定を最初に行う必要がある。詳細については、Enterprise Auditのオンラインマニュアルを見るか、MySQL Supportに連絡して欲しい。

データフィールドについては、MySQL.comAudit Log Fileのページの監査ログファイルフォーマットの情報を利用した。

私のMySQLサーバーはそんなに高負荷ではないので、データフィールドのサイズを各フィールドのサイズを各フィールドがとりうるサイズが確保されるよう設定してみた。 これらのフィールドを増やしたりデータ型を変えなければいけないこともありうるだろう。最大のフィールドはSQL_TEXTフィールドでSQL文を格納するものだ。 各テーブルは65,535バイトの最大列サイズを有している。従って、この例に対するSQL_TEXTフィールドのとりうる最大の値は、約63,200バイト(65,535バイト引く他のフィールドの合計サイズ、引く各varchar列に対する1バイトまたは2バイト長のプレフィックスサイズ)である。 この例ではSQL_TEXTフィールドは8,096バイトに設定されており、ご利用になる際はこの値を増やすか減らすかする必要があるだろう。

私はIDという名称の主キーフィールドを除いて、各フィールドにvarcharのデータ型を利用した。 データベースのスキーマにたくさん時間を使わなかったので、少し修正したいと思うかもしれない。 いくつかのフィールドはintegerであることは知っているが、各フィールドに対してとりうる値を確実に決めるだけの十分なデータがログファイルになかった。オンラインマニュアルを読み、CONNECTION_ID,SERVER_ID,STATUS,STATUS_CODEそしてVERSIONunsigned integerであると記載されていたが、varcharのままとした。


注意

スクリプトは、対象がMySQL 5.6.20またはそれより新しいバージョンで利用できる新しいフォーマットの監査ログファイルである必要がある。

1つのデータベースを作成し、そこには2つのテーブルを作った。1つはログファイルの情報を保存し、もう1つはどのファイルがすでにパースされMySQLに挿入されたかを追跡する履歴テーブルで、ログファイルのエントリ数と同数のエントリがある。CREATE DATABASECREATE TABLEは次の通りである。

CREATE DATABASE `audit_information` /*!40100 DEFAULT CHARACTER SET latin1 */

CREATE TABLE `audit_parsed` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `COMMAND_CLASS` varchar(64) DEFAULT NULL,
  `CONNECTIONID` varchar(32) DEFAULT NULL,
  `DB_NAME` varchar(64) DEFAULT NULL,
  `HOST_NAME` varchar(256) DEFAULT NULL,
  `IP_ADDRESS` varchar(16) DEFAULT NULL,
  `MYSQL_VERSION` varchar(64) DEFAULT NULL,
  `COMMAND_NAME` varchar(64) DEFAULT NULL,
  `OS_LOGIN` varchar(64) DEFAULT NULL,
  `OS_VERSION` varchar(256) DEFAULT NULL,
  `PRIV_USER` varchar(16) DEFAULT NULL,
  `PROXY_USER` varchar(16) DEFAULT NULL,
  `RECORD_ID` varchar(64) DEFAULT NULL,
  `SERVER_ID` varchar(32) DEFAULT NULL,
  `SQL_TEXT` varchar(8096) DEFAULT NULL,
  `STARTUP_OPTIONS` varchar(1024) DEFAULT NULL,
  `COMMAND_STATUS` varchar(64) DEFAULT NULL,
  `STATUS_CODE` varchar(11) DEFAULT NULL,
  `DATE_TIMESTAMP` varchar(24) DEFAULT NULL,
  `USER_NAME` varchar(128) DEFAULT NULL,
  `LOG_VERSION` varchar(11) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

CREATE TABLE `audit_history` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `AUDIT_LOG_NAME` varchar(64) DEFAULT NULL,
  `PARSED_DATE_TIME` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `LOG_ENTRIES` int(11) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

Perlのスクリプトは非活性のログファイルを見つけ(.xmlで終わり、例えば audit.log.14357895017796690.xml といったもの)、データをパースし、INSERT文を書いたSQLファイルを生成し、MySQLのcli経由でデータをインポートし、それからログファイルとSQLファイルをあるディレクトリに移動する。履歴テーブルは、どのファイルを処理したかを記録するため、同じファイルを誤って2度処理してしまうことは発生しない。

Perlスクリプトの最初の部分は、システムに応じて変更すべき値がいくつかある。その値は"values needed"の段落にある。Perlスクリプト(audit.pl)の例をここに示す。

#!/usr/bin/perl
# audit.pl

use DBI;
use CGI;
use XML::Simple;

#----------------------------------------------------------
# values needed
$Database = "audit_information";
$MYSQL_DATA_DIR = "/usr/local/mysql/data";
$MySQL_Host_IP_Name = "192.168.1.2";
$mysql_user = "root";
$mysql_password = "password_needed";

# directory to store old audit files after parsing
$audit_directory = "$MYSQL_DATA_DIR/audit_files";

# make an audit_files directory if one does not exist
mkdir($audit_directory) unless(-d $audit_directory);
#----------------------------------------------------------


#----------------------------------------------------------
#for each file do this
@files = @files = ;;
foreach $file_name_to_parse (@files) {

    #----------------------------------------------------------
    # check to see if file has already been parsed
    $dbh1 = ConnectToMySql($Database);
    $query1 = "select AUDIT_LOG_NAME from audit_history where AUDIT_LOG_NAME = '$file_name_to_parse'";
    $sth1 = $dbh1->prepare($query1);
    $sth1->execute();

      while (@data = $sth1->fetchrow_array()) {

           $audit_log_name = $data[0];

            }

    # if length of audit_log_name is less than 1, process file
    if (length($audit_log_name) $PARSED_FILE") or die print "Couldn't open log_file: $!";

        $count = 0;

        # XML::Simple variable - SuppressEmpty => 1   ignore empty values
        $xml = XML::Simple->new(SuppressEmpty => 1);
        $data = $xml->XMLin("$file_name_to_parse");

        foreach $info (@{$data->{AUDIT_RECORD}})
        {
            # replace tick marks ' with \' in the SQL TEXT
            $info->{"SQLTEXT"} =~ s/'/\\'/g;

            print LOGOUT "INSERT INTO audit_information.AUDIT_PARSED (COMMAND_CLASS, CONNECTIONID, DB_NAME, HOST_NAME, IP_ADDRESS, MYSQL_VERSION, COMMAND_NAME, OS_LOGIN, OS_VERSION, PRIV_USER, PROXY_USER, RECORD_ID, SERVER_ID, SQL_TEXT, STARTUP_OPTIONS, COMMAND_STATUS, STATUS_CODE, DATE_TIMESTAMP, USER_NAME, LOG_VERSION) values ('" . $info->{"COMMAND_CLASS"} . "', '" . $info->{"CONNECTION_ID"} . "', '" . $info->{"DB"} . "', '" . $info->{"HOST"} . "', '" . $info->{"IP"} . "', '" . $info->{"MYSQL_VERSION"} . "', '" . $info->{"NAME"} . "', '" . $info->{"OS_LOGIN"} . "', '" . $info->{"OS_VERSION"} . "', '" . $info->{"PRIV_USER"} . "', '" . $info->{"PROXY_USER"} . "', '" . $info->{"RECORD_ID"} . "', '" . $info->{"SERVER_ID"} . "', '" . $info->{"SQLTEXT"} . "', '" . $info->{"STARTUP_OPTIONS"} . "', '" . $info->{"STATUS"} . "', '" . $info->{"STATUS_CODE"} . "', '" . $info->{"TIMESTAMP"} . "', '" . $info->{"USER"} . "', '" . $info->{"VERSION"} . "');\n";
            $count++;

        # end foreach $info (@{$data->{AUDIT_RECORD}})
        }

        # load parsed file into MySQL - hide warnings
        system("mysql -u$mysql_user -p$mysql_password  /dev/null 2>&1");

        $dbh2 = ConnectToMySql($Database);
        $query2 = "insert into audit_information.audit_history (AUDIT_LOG_NAME, LOG_ENTRIES) values ('$file_name_to_parse', '$count')";

        # optional print output - uncomment if desired
        # print "$query2\n";

        $sth2 = $dbh2->prepare($query2);
        $sth2->execute();

        # close audit log file
        close(INFILE);

        # optional print output - uncomment if desired
        # print "Moving audit log ($file_name_to_parse) and log file ($PARSED_FILE) to $audit_directory.\n";

        # strip directories off $file_name_to_parse
        @file_name_to_move_array = split("\/",$file_name_to_parse);
        $directory_count = $#file_name_to_move_array;
        $file_name_to_move = $file_name_to_move_array[$directory_count];


        # optional print output - uncomment if desired
        # print "mv $file_name_to_move $file_name_to_parse\n";
        # print "mv $PARSED_FILE $audit_directory\n";

        # move audit log files and parsed log files to $audit_directory
        system("mv $file_name_to_parse $audit_directory");
        system("mv $PARSED_FILE $audit_directory");


    # end - if (length($audit_log_name) connect($connectionInfo,$mysql_user,$mysql_password);
   return $l_dbh;

}

必要な値をスクリプトに正しく設定してさえいれば、audit.plはどこで実行しても良い。 このスクリプトを、MySQLのデータディレクトリにあり現在のログであるため、まだローテートされていないログに対して実行しようとした場合は、エラーとなる。 現在のログファイルはaudit.logという名前である。

# pwd
/usr/local/mysql/data
# ls -l audit.log
-rw-rw----  1 mysql  _mysql  9955118 Jul  2 15:25 audit.log
It should not matter where you execute audit.pl, as long as you have correctly entered the required values in the script. You might get errors if you try to run this script on a log file that has not been rotated, which is the current log file in your MySQL data directory. The current log file is named audit.log.

スクリプトは.xmlで終わるファイルにのみ使える。テストには小さく(そして同一の)監査ログファイルを使った。

# pwd
/usr/local/mysql/data
# ls -l *xml
-rw-rw----  1 mysql  wheel   15508 Jul  2 12:20 audit.log.14357895017796690.xml
-rw-r-----  1 mysql  _mysql  15508 Jul  2 13:46 audit.log.14357895017796691.xml
-rw-r-----  1 mysql  _mysql  15508 Jul  2 13:46 audit.log.14357895017796692.xml
-rw-r-----  1 mysql  _mysql  15508 Jul  2 13:46 audit.log.14357895017796693.xml

Perlスクリプトのプリント文をコメントアウトしたが、コメントアウトを外せば、それぞれのログファイルに対してこの出力が得られる。

# perl audit.pl
Parsing - /usr/local/mysql/data/audit.log.14357895017796690.xml
insert into audit_information.audit_history (AUDIT_LOG_NAME, LOG_ENTRIES) values ('/usr/local/mysql/data/audit.log.14357895017796690.xml', '34')
Moving audit log (/usr/local/mysql/data/audit.log.14357895017796690.xml) and log file (/usr/local/mysql/data/audit.log.14357895017796690_parsed.sql) to /usr/local/mysql/data/audit_files.
mv audit.log.14357895017796690.xml /usr/local/mysql/data/audit.log.14357895017796690.xml
mv /usr/local/mysql/data/audit.log.14357895017796690_parsed.sql /usr/local/mysql/data/audit_files
....

テストスクリプトの実行後には、次のデータがaudit_historyテーブルに格納される。

mysql> use audit_information
Database changed
mysql> select * from audit_history;
+----+-------------------------------------------------------+---------------------+-------------+
| ID | AUDIT_LOG_NAME                                        | PARSED_DATE_TIME    | LOG_ENTRIES |
+----+-------------------------------------------------------+---------------------+-------------+
|  1 | /usr/local/mysql/data/audit.log.14357895017796690.xml | 2015-07-02 15:25:07 | 34          |
|  2 | /usr/local/mysql/data/audit.log.14357895017796691.xml | 2015-07-02 15:25:08 | 34          |
|  3 | /usr/local/mysql/data/audit.log.14357895017796692.xml | 2015-07-02 15:25:08 | 34          |
|  4 | /usr/local/mysql/data/audit.log.14357895017796693.xml | 2015-07-02 15:25:09 | 34          |
+----+-------------------------------------------------------+---------------------+-------------+
4 rows in set (0.00 sec)

audit_parsedテーブルの例(1行)は次のとおりだ。

mysql> select * from audit_parsed limit 1 \G
*************************** 1. row ***************************
     ID: 1
  COMMAND_CLASS: select
   CONNECTIONID: 10093
    DB_NAME: 
  HOST_NAME: localhost
 IP_ADDRESS: 127.0.0.1
  MYSQL_VERSION: 
   COMMAND_NAME: Query
   OS_LOGIN: 
 OS_VERSION: 
  PRIV_USER: 
 PROXY_USER: 
  RECORD_ID: 1614933_2015-07-01T22:08:58
  SERVER_ID: 
   SQL_TEXT: SELECT (UNIX_TIMESTAMP(now()) - CAST(variable_value AS SIGNED))*1000 as serverStartMillis 
  FROM information_schema.global_status 
 WHERE variable_name='uptime'
STARTUP_OPTIONS: 
 COMMAND_STATUS: 0
STATUS_CODE: 0
 DATE_TIMESTAMP: 2015-07-01T22:08:58 UTC
  USER_NAME: root[root] @ localhost [127.0.0.1]
LOG_VERSION: 
1 row in set (0.00 sec)

ログファイルをパースしたら、監査データに対して自身で検索クエリを作成できる。 自動的にスクリプトが実行されファイルをパースするように、cronにこのスクリプトを入れても良いだろう。

しかし通常通り、プロダクション環境に適用する前にこのスクリプトをテストし気をつけて使って欲しい。保存する必要がないデータがフィルタリングされるようにPerlスクリプトを修正してもよいだろう。

このスクリプトを利用し、意見や、疑問がわいたらどのようなものでも下記のコメント欄にコメントを残していただきたい。(※訳注:元記事はこちら)

これが興味を持っていただき、MySQL Enterprise Auditのログを処理すると何ができるかという例として役立ってくれることを願う。 以上である、いつもの通り、MySQLをつかってくれてありがとう!


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQLのinformation_schemaの便利なクエリ(MySQL Step-by-Step Blogより)

$
0
0
出典について

この記事はMySQL Step-by-Step BlogUseful queries on MySQL information_schema(2015/07/15)を翻訳したものです。


MySQLのinformation_schemaはデータベースのインスタンス、ステータス...などなどの日々のDBAの仕事に必要とされる有用な情報を備えている。 私が日々使う基本的なinformation_schemaへのいくつかのシンプルなクエリがあり、私自身が参照するためにこの投稿を書いており、または誰か他の人にも良いリファレンスになるだろう...

主キーまたはユニークキーがないテーブルをみつける

主キーは非常に重要であり、MySQLのInnoDBのテーブルでは主キーをクラスタインデックスとして利用するため、主キーがないと深刻な性能問題につながりかねない。

主キーがないと、スレーブのロギングの問題の主な要因の1つとなり、これは主にRBR(行ベースのレプリケーション)に発生する。例えば、マスタで発行されたdelete文が100万レコードを主キーなしに削除したとすると、フルテーブルスキャンが発生するだろう。これはマスタ上では問題にならない"かもしれない"が、スレーブでは100万行のフルテーブルスキャンが発生する。なぜならば、各行への変更はクエリそのものではなく、ROW形式でバイナリログに記録されるからだ。これはもちろんスレーブに遅延を引き起こす。レプリケーションの形式についての詳細は、マニュアルのドキュメントを参照して欲しい。

Gelera Clusterの場合は、主キーがないテーブルはスレーブへのレプリケーション遅延を引き起こすだけでなく、他の問題にもつながる。

もしテーブルに主キーがない場合、クラスタの異なるノードで行が異なる順番となりうる。そうなると、SELECT...LIMIT...の様なクエリが異なる結果を返すことになる。その上、そのようなテーブルではDELETE文はサポートされていない。

Galera Clusterの制限に関する詳細は、ここで確認できる。

従って主キーがないテーブルがないか確認するのは問題をできるだけ早く修正する為に非常に重要である。

SELECT t.TABLE_SCHEMA,t.TABLE_NAME,ENGINE
FROM information_schema.TABLES t
INNER JOIN information_schema.COLUMNS c
ON t.TABLE_SCHEMA=c.TABLE_SCHEMA
AND t.TABLE_NAME=c.TABLE_NAME
AND t.TABLE_SCHEMA NOT IN ('performance_schema','information_schema','mysql')
GROUP BY t.TABLE_SCHEMA,t.TABLE_NAME
HAVING sum(if(column_key in ('PRI','UNI'), 1,0))=0;

外部キー制約を確認する

Percona社のpt-online-schema-changeをスキーマを変更するのに利用している場合は、テーブル内に外部キーがあるとツールの操作がより複雑となり、追加の操作が必要となる。

外部キーはツールの操作を複雑なものとし、追加のリスクを発生させる。外部キーがテーブルを参照している場合は、アトミックに元のテーブルと新しいテーブルの面賞を変更するテクニックが使えない。ツールはスキーマの変更が完了してから、外部キーが新しいテーブルを参照するように更新する必要がある。ツールはこれを達成する為に2つの方法を提供している。これに関しての詳細な情報については -alter-foreign-keys-methodのドキュメントを参照してほしい。

外部キーはいくつかの副作用をも引き起こす。最後のテーブルは元のテーブルと同じ外部キーとインデックスをもつ(ALTER文の中で違うものを指定しない限り)が、MySQLおよびInnoDBでのオブジェクト名が衝突しないようにオブジェクトの名称が少し変化する。

pt-online-schema-changeの使い方に関する詳細は私のブログを確認して欲しい!

また、外部キーはInnoDBでのみサポートされているため、テーブルをMyISAMに変換する必要が発生した場合は、外部キー制約を始めに確認し、MyISAMに変換する前に削除する必要がある。さもなくばALTER文は失敗するだろう。

SELECT referenced_table_name parent, table_name child, constraint_name
FROM information_schema.KEY_COLUMN_USAGE
WHERE referenced_table_name IS NOT NULL
ORDER BY referenced_table_name;

断片化を確認する

テーブルは幾度もの書込み(insert, update, delete)により断片化するため、テーブルとインデックスを再構成し、性能向上および、OS上で利用されるディスクスペース回収をおこなう。 (InnoDBのテーブルを作成する前に、innodb_file_per_tableオプションが有効化されていたものと仮定する)。これは"OPTIMIZE TABLE"文を実行することにより実現できるが、一方でこのクエリは負荷がかかる。しかしながら、我々はテーブルの断片化をまず始めに確認し、それから"OPTIMIZE TABLE"を実行することで断片化が進んでいるテーブルだけに対して適用可能となる(次に示すクエリは100MB以上の未使用領域があるテーブルのdb_nameのみを返す)。

SELECT TABLE_NAME, (DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 AS sizeMb,DATA_FREE / 1024 / 1024 AS data_free_MB
FROM information_schema.tables
WHERE engine LIKE 'InnoDB'
and TABLE_SCHEMA = 'db_name'
AND DATA_FREE > 100 * 1024 * 1024;

MyISAMのテーブルがあるかどうか確認する

MyISAMはトランザクションをサポートしていないため、MyISAMのテーブルがあるときに一貫性のあるバックアップをとる為には、全てのテーブルをロックする必要がある。 従って、システムのバックアップ計画を考える前に、MyISAMのテーブルがあるかどうかを確認することが推奨される。

SELECT TABLE_SCHEMA, TABLE_NAME
FROM `information_schema`.`TABLES`
WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql')
AND ENGINE='MyISAM';

information_schemaに関するご利用の便利なクエリはあるだろうか?気軽に下のコメントに書込んでいただきたい :)

(訳注: 元記事はこちら)


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQLおよびMariaDBに大きなサイズの行を挿入する

$
0
0
出典について

この記事は「Inserting large rows in MySQL and MariaDB」(2015/7/20)を翻訳したものである。


MySQLにおけるLONGBLOBの最大容量は4GBであり、max_allowed_packetの最大値のサイズは1GBである為、私はLONGBLOBを完全に使いきることができるのだろうかと思い立った。

そこでこれをテストし始めた。MySQL Connector/Pythonを使ってPythonスクリプトを書き、MySQL Sandboxを使ってMySQL 5.6.25のインスタンスを作成した。

最初に変えるべき設定はmax_allowed_packetであり、これは予想通りであった。この値は1GBに設定した。

次の設定は、少し予想通りではなくなり、innodb_log_file_sizeであった。InnoDBログファイルの10%以下のサイズにトランザクションが収まるようにサーバが制限するのだ。従って、(だいたい)1GBの1レコードが挿入できるように、5GBのファイルが2つとなるように設定した。

パケット内部にはいくらかのオーバーヘッドがあるため、1GBより少し小さい行を扱い、1GBちょうどになるようにした。

(1GBへの)次のステップは、複数のチャンクを使ったデータアップロードを可能とするmysql_stmt_send_log_data()関数を使えるようにするため、PythonをやめてC言語に切り替えた。

これはうまく動作するだろうと期待していたが、実際は動作しなかった。これはMySQL 5.6またはそれ以上では、max_long_data_sizeがmax_allowed_packetに置き換えられていた為だ。 しかし、max_long_data_sizeは4GBに設定できるのに対し、max_allowed_packetは最大1GBまでしか設定できないのだ。

そこで、MySQL 5.6からMariaDB 10.1に切り替えた。MariaDBではmax_long_data_sizeがまだ利用できたためだ。これで動作させることができ、(おおよそ)4GBの行をアップロードすることができた。

私はInnoDBが大きなタプルに対してエラーログを出力することにも気付いた。

InnoDB: Warning: tuple size very big: 1100000025

従って、CDのISOイメージはデータベースに挿入することができる。小さなDVDイメージに関しては、ご利用のコネクタがCOM_STMT_SEND_LONG_DATAコマンドを使っている場合にうまく動作する。

しかしながらこれは避けて、行のサイズが小さくなるようにした方が良いだろう。

テストで使ったスクリプト(と見つけたバグへのリファレンス)は下記の通りである。 https://github.com/dveeden/mysql_supersize


PlanetMySQL Voting: Vote UP / Vote DOWN

類い稀なMySQLのJavaScript Connector (MySQL High Availabilityより)

$
0
0
出典について

この記事はcraigrussell氏によるMySQL High Availabilityの投稿「The JavaScript Connector for MySQL you never heard of」(2015/7/14)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


Webサーバをたてて、node.jsExpressを使ってデータベースに接続するのはかつてないほど簡単になっている。node.jsを使ってRDBからデータを取り出すには、かつてはユーザーがSQLの技術に熟練している必要があった。

そこでMySQLの開発チームは、MySQLからのドキュメントを保存、検索し、node.jsのアプリケーションに渡すのを何でもなくする(うまく、簡単にする)プロジェクトを立ち上げた。SQLを使わずにだ。この成果はmysql-jsと呼ばれ、ここでご紹介したいと思う。今日、githubでダウンロードできる。

プロジェクトは比較的シンプルなJSONのドキュメントを保存するところから検討を始めた。ドキュメントの各属性はデータベースのカラムに格納される。このモデルはシンプルなドキュメントをリレーショナルデータモデルの利点を最大限活用できるように保存できるようになっている。しかし、複雑な属性を保存するのは、簡単なことではなかった。

そこで、我々はある技術を開発した。この技術は、複雑な属性(例えば、配列、ネストされたJavaScripのオブジェクトなど)を保存される前にJSONにシリアライズする。データベースから属性値を検索する時は、シリアライズの処理が逆方向に行われ、元の属性が復元される。

この変更によって、構造がうまく定義されている均一なドキュメントを保存することが出来るようになった。しかし、追加の属性を持つドキュメントを保存するテーブルは依然として問題がたくさんあった。そこで全ての属性に対し、データベース内にカラムがない場合に保存するカラムを導入した。

このデータモデルが導入とともに、我々はデータアクセスAPIに専念した。基本的なこと、つまりinsertとdeleteからまず始めた。テーブルは主キーのカラムを持っており、ドキュメント内に対応する属性を持っていたため、ドキュメントを保存するのはどのテーブルにそのデータが属するかを判別し書込むのと同様、簡単であった。

ドキュメントの削除は簡単である。主キーもしくはユニークキーで削除したいドキュメントを識別すれば良い。

一度保存したドキュメントを検索するには、ドキュメントを主キーかユニークキーで判別できる必要がある。複数のドキュメントを返す任意の属性でのクエリは、明らかにより複雑であるためこれは別の問題として扱った。

ドキュメントの更新には2つのことが必要である。主キーまたはユニークキーを使ってどのドキュメントを更新すべきかを特定し、どの属性を変えるべきかを特定する。

コネクタへの主要なユーザーインターフェースはSessionである。MySQLのアーキテクチャにおいては、セッションはサーバ側のものでユーザーの代わりにデータにアクセスし、リクエストを受付け、結果を届ける役割がある。JavaScriptコネクターでは、ローカルセッションの中でリクエストを生成し、コネクタはサーバーセッションに転送する。

コードの例

セッションを取得するのは分かりやすい。MySQLサーバの接続情報を知っていれば、JavaScriptのオブジェクトが作成でき、セッションサービスにセッションを使うよう要請すればよい。サーバーが標準のホストおよびポートを利用していれば、コネクタが既知のデフォルトを利用できる。クレデンシャルを追加したければ、接続情報に追加して欲しい。

var mySQLClient = require('mysql-js');
var properties = new mySQLClient.ConnectionProperties('mysql');
properties.user = 'user1';
properties.password = 'sekrit';
mySQLClient.openSession(properties, onOpenSession);

セッションはコールバック内で受け渡される。コールバックはnode.jsの慣例に従い、2パラメータを持つ。errとdataである。このケースでは、dataはデータベースとやりとりをするのに使うセッションオブジェクトである。コネクタはPromisesやA+もサポートする(後ほど詳細を記述する)

CRUD操作

SessionはCRUD操作、クエリおよぼいいくつかのユーティリティー関数をサポートしている。CRUD操作では、データベース内の1行と、JavaScriptの1つのオブジェクトを扱う。オブジェクトはJSONのリテラル記法('{name: value}'の記法で直接作成されたもの)かコンストラクタで生成可能で、オブジェクト指向プログラミングの恩恵にあずかることができるだろう。

次に示すいくつかの例では、標準のデータベースにあるテーブルがあると仮定して欲しい。

CREATE TABLE author (
  user_name varchar(20) NOT NULL PRIMARY KEY,
  full_name varchar(250),
  posts int unsigned not null default 0
) ENGINE=ndbcluster;

Insert

authorテーブルに行を挿入するには、insert関数を使う。

var craig = {'user_name': 'clr', 'full_name': 'Craig Russell'};
session.insert('author', craig, onInsert);

posts列はデフォルト値を持っているので、挿入するデータに含まれていなくても良い。

Insert or Update

行をauthorテーブルに挿入、もしくはすでに存在すれば更新するには、save関数を使う。これはSQLのON DUPLICATE KEY UPDATEに相当する。(いくつかのAPIでは"write"という単語を使っているが、これにはそんなに意味内容がない。他のAPIでは"upsert"とよんでいるが、この言葉は混乱を呼ぶものと考えている。われわれは"indate"と考えたがこれでは役にたたないようである。)

var craig = {'user_name': 'clr', 'full_name': 'Craig Russell', posts:100};
session.save('author', craig, onSave);

Find

データベース内の単一行を検索するには、find関数を使う。キーは完全な主キーであるプリミティブ、または主キーを属性として持つオブジェクト、またはユニークキーを属性として持つオブジェクトである。

function onFound(err, row) {
 // error handling omitted for clarity
 // prints {'user_name': 'clr', 'full_name': 'Craig Russell' 'posts': 100}
  console.log(row); 
}
// find(tableName, key, callback)
session.find('author', 'clr', onFound);

Update

データベース内の単一行を更新するには、update関数を使う。キーは行をユニークに特定する為だけに利用され、値は属性を指定された値に更新する為だけに使われる。

// update(tableName, key, value, callback)
session.update('author', 'clr', {'full_name': 'Craig L Russell'}, onUpdate);

Delete

データベース内の単一行を削除する為には、delete関数を使う。(t関数に別名をつけremove関数にエイリアスした。IDEがdeleteの語句を予測しないコンテキストに推測するのが気にくわない場合に対応する為だ。)(訳注: エイリアスはdelete関数 > remove関数の誤りか。原文を合わせて参照ください)

// delete(tableName, key, callback)
session.delete('author', 'clr', onDelete);

コンストラクタの利用

アプリケーションをよりよく作り込む為には、コンストラクタを使いたくなるかもしれない。我々はMartin Fowlerのすばらしいリファレンスにあるドメインモデルのパターン、パターンオブエンタープライズアプリケーションアーキテクチャをサポートしている。このケースでは、セッションの操作の中でコンストラクタを定義してそれ(もしくはインスタンスを)使えばよい。

function Author(name, full_name) {
  if (name) this.user_name = name;
  if (full_name) this.full_name = full_name;
}
Author.prototype.getNumberOfPosts = function() {
  return this.posts;
}
Author.prototype.toString = function() {
  return ((this.posts > 100)?'Esteemed ':'') + 
  'Author: ' + this.name + 
  ' Full Name: ' + this.full_name + 
  ' posts: ' + this.posts;
}

コンストラクタを利用する際に1つだけ追加でしなければならないことがある。現在、コンストラクタはデータを保存するのに使うテーブル名の注記が付与されている。テーブル名が標準のコンストラクタの関数名となるよう改良中である。

new mySQLClient.TableMapping('author').applyToClass(Author);

Insert

authorテーブルに行を挿入するには、insert関数を使う。コンストラクタを使っているので、テーブル名を指定する必要はない。コネクタはTableMappingのテーブルを利用するだろう。

var craig = new Author('clr', 'Craig Russell';
session.insert(craig, onInsert);

posts列は標準の値を持っているので、挿入データに含まれる必要はない。

Insert or Update

authorテーブルに行を挿入するか、すでに存在する場合は更新するには、save関数を使う。

var craig = new Author('clr', 'Craig Russell');
craig.posts = 100;
session.save('author', craig, onSave);

Find

データベース内の単一行を検索するには、find関数を使う。

function onFound(err, row) {
  // prints Author: clr Full Name: Craig Russell posts: 0
  console.log(row);
}
session.find(Author, 'clr', onFound);

Update

データベース内の単一行を更新するには、update関数を使う。

var key = new Author('clr');
var changes = new Author('', 'Craig L. Russell');
session.update(Author, key, changes, onUpdate);

Delete

データベース内の単一行を削除するには、delete(もしくはremove)関数を使う。

session.delete(Author, 'clr', onDelete);

Promise

コールバックとJavaScriptを使っている場合(言い換えれば、node.jsを通常通り使っている場合)、エラーハンドリングのコードは、アプケーションのコードを分かりづらくしうる。Promiseはエラーハンドリングが抽象化されており、よりきれいなコードを書く1つの手段である。

例えば、エラーハンドリングのコードは下記のようになるだろう。

// find an object
function onSession(err, s) {
  session = s;
  if (err) {
console.log('Error onSession:', err);
process.exit(0);
  } else {
session.find('Author', 'clr', onFindByTableName);
  }
};

実際のコードは1行しかないが、エラーハンドリングの中にうずもれている。

Promiseを利用すれば、その代わりに次のように書くことができる。 それぞれの関数はthen関数から呼び出され連続して実行されるだろう。

var session;

function exit(code) {
  process.exit(code);
}
function setSession(s) {
  session = s;
}
function insertByConstructor() {
  return session.insert(Author, new Author('sam', 'Sammy Snead'));
}
function insertByTableName() {
  return session.insert('author', 
{user_name: 'char', full_name: 'Charlene LeMain'});
}
function saveByConstructor() {
  return session.save(Author, new Author('sam', 'Sammy Snead'));
}
function saveByTableName() {
  return session.save('author', 
{user_name: 'char', full_name: 'Charlene LeMain'});
}
function findByConstructor() {
  return session.find(Author, 'sam');
}
function findByTableName() {
  return session.find('author', 'char');
}
function updateByConstructor() {
  return session.update(Author, 'sam', 
new Author(null, 'Samuel K Snead'));
}
function updateByTableName() {
  return session.update('author', 'char', 
{full_name: 'Charlene K LeMain'});
}
function deleteByConstructor(sam) {
  return session.delete(Author, 'sam');
}
function deleteByTableName(char) {
  return session.delete('author', 'char');
}
function closeSession() {
  return session.close();
}
function reportSuccess() {
  session.sessionFactory.close();
  console.log('All done.');
  exit(0);
}
function reportError(e) {
  console.log('error:', e);
  exit(1);
}

mySQLClient.openSession(dbProperties, null)
.then(setSession)
.then(insertByTableName)
.then(insertByConstructor)
.then(saveByTableName)
.then(saveByConstructor)
.then(updateByTableName)
.then(updateByConstructor)
.then(findByTableName)
.then(deleteByTableName)
.then(findByConstructor)
.then(deleteByConstructor)
.then(closeSession)
.then(reportSuccess, reportError);

Promiseは2つの引数を持つthenメソッドの実装(mysql-js connector)定義を必要とする。すなわち非同期の操作が成功した後、単一の値を返す関数と、非同期の操作が失敗した時だけに呼び出され、例外を発生させる関数である。アプリケーションはthen関数を利用でき、エラーハンドリングを含め、様々な非同期の関数を作れる。

ディスカッションすべきトピックは、まだまだたくさんある。ACID属性が保証されたトランザクション、複数のバックエンドストレージ、複雑なクエリそして複雑なドキュメントストレージの利用および扱い、ドキュメント指向のテーブルの正規化されたリレーショナルテーブルとの結合など。しばらくお待ちいただきたい。

Githubからフォークする

https://github.com/mysql/mysql-js


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQL 5.7.7におけるレプリケーションのデフォルト値(MySQL High Availabilityより)

$
0
0
免責事項

この記事はSujatha Sivakumar氏によるMySQL High Availabilityの投稿「Replication Defaults in MySQL-5.7.7」(2015/7/8)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


コミュニティーのフィードバックにもとづいて、MySQL 5.7.7のリリースの一部として、レプリケーション機能のデフォルト値という観点でいくつかの改善が行われた。これはMySQLのレプリケーションをより安全に、かつより使いやすくするだろう。

このブログはこれらのデフォルト値の変更についての情報を提供することを目的としており、簡単にその長所を説明する。

1) 単純化されたGTIDリカバリがデフォルトで有効化されるようになった

binlog_gtid_simple_recovery=TRUEがデフォルトとなった。

https://dev.mysql.com/doc/refman/5.7/en/replication-options-gtids.html#sysvar_binlog_gtid_simple_recovery

この変数はMySQLが起動されたり再起動された時に、GTIDを検索する最中にどのようにバイナリログファイルが繰返し利用されるかを制御する。 このオプションをTRUEにすることでリカバリの性能は向上する。このオプションによりサーバーの起動とバイナリログのパージが高速になる。 このパラメータが有効化されたときは、一番古い、そして一番新しいログファイルだけを開き、gtid_purgedとgtid_executedが、これらのファイルのprevious_gtid_log_eventまたはgtid_log_eventから計算される。

MySQL 5.6でこのオプションをTRUEにすると性能が良くなるが、いくつかの特異なケースで、バイナリログ中のgtidのセットの計算が不正確になる可能性がある。このオプションをFALSEにすると常に正確な結果となる。

MySQL 5.7.7では、速度と安全性のトレードオフがほとんどなくなった。このオプションをTRUEにするといくつかの特異なケースを除き、いつも正確な値を返すようになった。

特異なケースとしては下記があげられる。

  • 最新のバイナリログがMySQL 5.7.5以降で生成されており、gtid_modeがいくつかのバイナリログでONとなっているが、最新のバイナリログではOFFとなっている
  • SET GTID_PURGED文はMySQL 5.7.7以前で問題があり、SET GTID_PURGEDがまだパージされていないときにアクティブになったバイナリログ

従って、より高速なオプションは大抵の場合よりよい選択肢であり、デフォルトのモードとなった。

この機能がOFFになると、リカバリの最中でgtid_executedを初期化するために、最新のファイル以降の全てのバイナリログが検査され、またgtid_purgedを初期化する為に一番古いバイナリログから最新のログファイルまでの全てのバイナリログが検査される。これは潜在的に長い時間がかかりうる。

2) 行ベースのバイナリログ形式がデフォルトとなった

binlog-format=ROWがデフォルトとなった。

http://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html#sysvar_binlog_format

バイナリログがONでバイナリログ形式がROWモードで運用している際には、各テーブルの行に対する変更がバイナリログに記録される。それからこれらがスレーブ側に適用される。これはSTATEMENT形式を利用している場合とは異なり、STATEMENT形式ではバイナリログにSTATEMENTs(SQL文)を記録し、これがスレーブで再実行される。

全ての種類の変更を複製できるので、これが最も安全なレプリケーション形式である。あるSQL文では、ステートメントベースレプリケーションに比べ、行のロックが少なくてすむようになる。以前のSTATEMENTオプションでは非決定的なSQL文がスレーブでマスターと異なる結果となりえる。

3) binlog_error_action=ABORT_SERVERがデフォルトとなった

binlog_error_actionは、サーバーがバイナリログを書込めないときにどうすべきかを制御するものだ。

http://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html#sysvar_binlog_error_action

binlog_error_action=ABORT_SERVERに設定すると、サーバーはディスクフルまたは、ファイルシステムがリードオンリーとなった場合などの場合と同様にfatalエラーを出して異常終了する。 ABORT_SERVERオプションをつければ、バイナリログおよびスレーブは安全となる為、デフォルトの値を変更した。

以前のデフォルトであったbinlog_error_action=IGNORE_ERRORでは、MySQLサーバーがバイナリログを書込めないエラーが発生した場合、エラーログにある適切なエラーメッセージを書込み、バイナリログを無効化する。サーバーはバイナリログを無効化した状態で動作を継続する点に注意してほしく、このようにしてスレーブがエラーが起きた後の変更が適用できなくなる。

4) クラッシュセーフなバイナリログがデフォルトとなった

sync_binlog変数によってバイナリログがクラッシュセーフとなるかどうかが制御される。この変数は、バイナリログをディスクに同期する前の、バイナリログのコミットグループ数を指定する。

http://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html#sysvar_sync_binlog

sync_binlog=1では、全てのトランザクションがコミットされる前にバイナリログに同期される。従って、予期せぬ再起動が起こったとしても、バイナリログにないトランザクションは、どれをとっても準備完了状態となっているに過ぎない。サーバーの自動リカバリの時にこれらのトランザクションをロールバックする。これはバイナリログからロストするトランザクションはないことを保証し、従ってこれが最も安全なオプションである。実際のところ、これはfsyncが呼び出される総数を増やすが、MySQL 5.6からは、サーバーはトランザクションおよびそれらのfysncをグループ化し、パフォーマンスへの影響を最小化する。

sync_binlog=0では、MySQLサーバーによってバイナリログがディスクに同期されることはなく、この場合はサーバーはバイナリログの内容のフラッシュをOS任せにし、他のファイルをその時々でフラッシュするときにフラッシュされる。このようにして、電源故障やOSのクラッシュが起きた場合、いくつかのトランザクションはサーバーがコミットしているが、バイナリログが同期されていないことが起こりうる。そうするとリカバリ処理でこれらのトランザクションをリカバリすることは出来ず、バイナリログからも失われているだろう。

このようにして、新しいsync_binlog=1はより安全である。

5) slave_net_timeoutのデフォルトの値が小さくなった

slave_net_timeoutは、スレーブがコネクションが切断されたり、読込みが中断されたりして再接続する前に、マスタからのデータを待ち受ける秒数を表す。この変数はマスターとスレーブの間のハートビートの頻度にも影響を与える。ハートビートの間隔はデフォルトで、slave_net_timeoutを2で割った値であるためだ。

http://dev.mysql.com/doc/refman/5.7/en/replication-options-slave.html#option_mysqld_slave-net-timeout

新しいデフォルト値は、以前のデフォルト値が3600秒であるのに対し、slave_net_timeout=60である。古い設定では、一時的なネットワーク遅延によって大きなレプリケーション遅延が発生しうる。

6) @@session.gtid_executedは廃止予定となった

@@global.gtid_executed変数はバイナリログの記録されている全てのトランザクションセットの情報を保持している。

http://dev.mysql.com/doc/refman/5.7/en/replication-options-gtids.html#sysvar_gtid_executed

セッションのスコープで使われた場合は、この変数はトランザクションキャッシュに書込まれたトランザクションセットの情報を保持する。具体的には、SESSION.GTID_EXECUTEDは、GTID_NEXTがUUID:NUMBERの形式をしており、少なくとも1つのDMLが実行されたがコミットされていない場合、UUID:NUMBERに等しい。GTID_NEXTが他の値に設定されたとき、SESSION.GTID_EXECUTEDは空白文字となる。MySQL 5.7.7ではこのセッションスコープの変数が使われた時に警告を出力するようになった。グローバルスコープの変数である@@global.gtid_executedには何の変更もない。@@global.gtid_executedは頻繁に使われている一方で、ユーザーがglobalキーワードをつけ忘れた時に正しくない値を示すセッション変数を参照できてしまう。混乱を防ぐ為にセッション変数は廃止予定となった。また、このセッション変数がいくつかの用途で非常に有用であることが知られていないため、廃止予定となった。


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQL 5.7.8のmysqlpumpについての注意事項

$
0
0
  • MySQL 5.7.8-rc2が本日 (訳注:この 元記事 は2015/08/03に公開された) リリースされた。新しいmysqlpumpという新しいサーバーユーティリティも一緒にリリースされている。このユーティリティーはmysqldumpにいくつかの新しい機能を取り込んだものだ、たとえば

  • データベース, テーブルのパラレルダンプ。これはダンプの速度を向上させる。

  • ダンプファイルのリストア時にInnoDBのfast index creationを有効利用する。InnoDBのテーブルに対しては、行をINSERTした後にセカンダリーインデックスを後から追加するようになっている。

  • テーブル, ビュー, ストアドプロシージャ, ユーザーアカウントなど、データベース内のオブジェクトをより上手くコントロールする

  • ユーザーアカウントの情報を、mysql.userテーブルへのINSERTではなく、CREATE USERやGRANTステートメントなどのユーザーアカウント制御用のステートメントとしてダンプする

  • 圧縮されたダンプの出力をサポートしている

  • ダンプの進捗状況がレポートされる

  • だが、ここで注意しておいてほしいのは、mysqlpumpは 今はまだ一貫したバックアップが取れない ことだ。今の実装では、それぞれのダンプスレッドはバックアップを開始する時点を同期していない(訳注: mysqldump --single-transactionでいうSTART TRANSACTIONの直前のFLUSH TABLES WITH READ LOCKにあたる部分がないため、個々のスレッドがトランザクションを開始する間に更新トランザクションが入るとバックアップの一貫性が崩れる)。 現時点では 、通常のバックアップをmysqldumpからmysqlpumpに切り替えるのは安全ではないだろう。

  • mysqlpumpの開発者はこの制約を知っていて、この制約を克服する機能を追加するために努力しているところだ。


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQL 5.7におけるサーバーのデフォルト値の改善 (MySQL Server Blogより)

$
0
0
免責事項

この記事はMatt Lord氏によるMySQL Server Blogの投稿「Improved Server Defaults in 5.7」(2015/7/22)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


Morganと私はしばらく前にMySQLの"今やもう良くない(out of the box)"設定および挙動のデフォルト値を改善する計画を開始した。コミュニティーと密接に実施することにより、すばらしい改善の一覧が作成できた。これはMySQL 5.7.7のリリースから成果を出し始めた。私は何が変わったか、通常のユーザーおよびインストールの場合、なぜMySQLが改善されるかをざっと紹介したい。

レプリケーションの改善

  • 詳細はSujathaのすばらしいブログ投稿をみていただきたい。

InnoDBの改善

  • innodb_checksum_algorithm : デフォルト値を"innodb"から"crc32"に変更した。CRC32を使えば今日の大多数のサーバーではハードウェアアクセラレーションが利用でき、これにより適切な全般的性能強化が適用されるはずである。
  • innodb_page_cleaners : デフォルト値を1から4に変更した。これによりバッファプールからダーティーページをフラッシュする処理が標準でマルチスレッド化される(innodb_buffer_pool_instancesの値の方が小さい場合は自動的に小さくなる)
  • innodb_purge_threads : デフォルト値を1から4に変更した。これにより利用されなくなった値をインデックスからパージして削除し、前にDELETE文によって削除とマークされた行を物理的に削除する処理が標準でマルチスレッド化される。これは高負荷なシステムの全体の性能を向上させる。
  • innodb_strict_mode : デフォルト値をOFFからONに変更した。これによりInnoDBが一致協力してMySQLをデフォルトでより厳格に動作させるという全体のゴールに沿うようになり、データの完全性、信頼性、堅牢性を確かなものにするのに役立つ。これはMySQLをよりSQL標準準拠させるというゴールにも該当することになる。
  • InnoDBバッファプールのウォームアップ : 次の変更によりMySQLを再起動した時に、バッファプールのページのうち最もホットである25%のページを維持しキャッシュが暖まった状態を維持できるようになる。すなわち、innodb_buffer_pool_dump_at_shutdown, innodb_buffer_pool_load_at_startupそして、innodb_buffer_pool_dump_pctである。追加の情報については、Tonyのすばらしいブログ投稿を見て欲しい。これは、MySQLを再起動した時のアプリケーション性能への影響を小さくするのに役立つ。
  • innodb_file_format : 我々は最新のファイルフォーマットである、Barracudaをデフォルトとした。これにより行フォーマットのあらゆる制約が取り除かれる。これは圧縮のような全ての利用可能な機能を標準で利用可能とし(MySQLの不要な再起動をせずに)、全体のユーザーエクスペリアンスを向上させる。
  • innodb_large_prefix : インデックスのキーのプレフィックスの制限を767バイトから、3072バイトに増やす。これは重要で、とりわけより多くのユーザーがUnicodeをデフォルトの文字コードとして利用するようになるに従って重要となる。

パフォーマンススキーマの改善

  • 追加のコンシューマの有効化 : 我々はevents_statements_historyevents_transactions_historyコンシューマーをデフォルトで有効化するようにした。これにより最近システム上でどのようなSQL文およびトランザクションが実行されたかというDBAが知る必要のあるとても役に立つ情報がわかる。この情報は無数の問題(その時間に実行された関連あるSQLどれでも)を追跡してとらえる時に非常に価値がある。そしてパフォーマンススキーマのオーバヘッドを小さくする絶え間ない努力により、ごくわずかな性能影響でこれが有効化できるようになった(オーバーヘッドに関する詳細な情報はTariqueのすばらしいブログ投稿をご覧いただきたい)。

セキュリティーの改善

  • sql_mode : NO_AUTO_CREATE_USERを作りこれをデフォルトとした。これはGRANT文が予期せずそして自動的に認証情報が特定されずに新規アカウントを作成してしまうのを防ぐ為のものだ。これはMySQLをデフォルトでより安全にする為の取組みの1つで(セキュリティーおよびデータの安全性の改善)、デフォルトでより標準に準拠するようにもしている。
  • それに加え、テストデータベースと匿名ユーザーのアカウントはMySQLのインストールの過程でもはや作成されなくなる。

実行時および全般的な改善

  • table_open_caches_instances : このオプションはCPUハードウェアスレッドが多数あるシステムにおけるテーブルキャッシュ上の競合を減らすことを意図したものだ(vCPU: ソケット、コア、スレッドの組合せ)。今や汎用のデスクトップマシンでさえ、しばしば16ハードウェアスレッドを備えているので(2CPUソケット、各ソケット4コア、各コアSMT/ハイパースレッディングで2スレッド)、デフォルトを16と設定し、最近のハードウェアに対して性能が向上するようにした。
  • sql_mode : STRICT_TRANS_TABLES、ERROR_FOR_DIVISION_BY_ZERO、NO_ZERO_DATE、そしてNO_ZERO_IN_DATE SQLモードがデフォルトでコンパイルされ有効となる。これにより不完全な、範囲外の、または不正な日付に対しエラーを生成するようになる。これは我々の2つの構想のキーとなる部分である。1. デフォルトでデータの妥当性および完全性を保証する、2. デフォルトでよりSQL標準に準拠する。(注記: 5.7.8以前は、これら全ての設定が1つのSTRICT_TRANS_TABLEモードにまとめられたが、コミュニティーのフィードバックにより、これらは再度分離され、MySQL 5.6またはそれ以前のリリースの状態となった。)
  • show_compatibility_56 : MySQL 5.7では、SHOWコマンドの挙動の改善を行い、追加の情報を加えるだけでなく、GLOBALとSESSIONの内容にすっきりした概要説明を提供した(これはいくつかの奇妙な動作とバグレポートとなってしまった)。後方互換が必要となるケースによりよく良く対応する為に、show_compatibility_56という新しいオプションを導入し、5.7.8ではデフォルトでOFFとなっている。以前のリリースとの互換を保ちたい場合は、ユーザーはこの設定をONにするだろう。
  • log_warnings : 効果的にエラーログの出力レベルをあげる為に、デフォルト値を1から2に変更した。MySQL 5.7では新しいlog_error_verbosityオプションとなり、このコンフィグオプションは廃止予定となることにも注意して欲しい。

オプティマイザの改善

  • sql_mode : ONLY_FULL_GROUP_BYの挙動が大幅に改善され、現在ではデフォルトで有効化されるようになった。これらの変更についての詳細はGuilhemのすばらしいブログ投稿で確認できる。
  • eq_range_index_dive_limit : デフォルトを10から200に変更した。この変更は大多数の場合で全体の挙動を改善するはずだ。この変更の詳細はJorgenのすばらしいブログ投稿で確認できる。
  • 新しいオプティマイザスイッチを2つ導入した。condition_fanout_filterおよびderived_mergeであり、現在ではデフォルトで有効化される。大多数のケースでよりよい性能となるため、ユーザーは設定をこのままにしたいと考えるだろう。しかしながら、ある特定のサブクエリがありこれは派生マージ(derived merge)をサポートしておらず、この特異ケースは"SQLの変更"としてMySQL 5.7のアップグレードノートに記載されている。

これらの変更についての疑問や、他の変更の推奨があれば、コメント欄で教えて欲しい。 (訳注: 元サイトのコメント欄に記載ください)

以上である。MySQLをご利用いただきありがとう。これによって、改善に寄与するだろう!


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQLのオプティマイザヒント

$
0
0
免責事項

この記事はSergey Glukhov氏によるMySQL Server Blogの投稿「New Optimizer Hints in MySQL」(2015/7/29)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


MySQLのオプティマイザが特殊なシステム変数、optimizer_switchを持つことは良く知られており、これは様々なインデックスコンディションプッシュダウン(ICP)、バッチキーアクセスなどの様々なオプティマイザのモードを制御できるようにするものだ。optimizer_switchを利用する1つの大きな欠点はこれがステートメント全体に影響することで、しばしば特定のテーブルあるいはクエリブロックの挙動のみを変えた方が望ましい、変える必要があることがある。これらの問題に対処し、使い勝手を向上させる為に、新しくヒント句が追加された。これにより、より強力な、またきめの細かいオプティマイザの挙動の制御がSQL文中で直接実施できる。

新しいヒント句に関していくつかの重要なポイントをあげる。

  • コメント文法 /*+ */ が新しいヒント句として利用できる
  • 同一コメント中で、複数のヒント句が指定できる。
  • クエリブロックはヒント句を含め1つしかコメントを持つことが出来ず、コメントはSELECT、UPDATE、INSERT、REPLACE、またはDELETEの直後に付与しなければならない。
  • ヒント句中の正しくない、または無効な文法は警告を出力させる。
  • ヒントがコンフリクトしている場合、最初のヒント句のみ有効となりそれに続くコンフリクトしているあるいは重複するヒントは警告を出力し無効化される。
  • 複数レベルのヒント句がサポートされている。例えば、ヒント句は特定のテーブルまたはクエリブロックのみに影響を与えるようにすることができる。

次のヒント句はMySQL 5.7.7から追加されたものだ。

  • BKANO_BKA : 特定のテーブルまたはクエリブロックへのバッチキーアクセスアルゴリズムの利用を制御する
  • BNLNO_BNL : 特定のテーブルまたはクエリブロックへのブロックネスティッドループアルゴリズムの利用を制御する
  • MRRNO_MRR : 特定のインデックスまたはテーブルへのマルチレンジリードを制御する
  • NO_ICP : 特定のインデックスまたはテーブルへのインデックスコンディションプッシュダウンの利用を無効化する
  • NO_RANGE_OPTIMIZATION : 特定のインデックスまたはテーブルへのレンジアクセスの利用を無効化する
  • MAX_EXECUTION_TIME : ステートメント実行タイムアウトをNミリ秒に設定する
  • QB_NAME : 特定のクエリブロックに名前を付ける為の補助的なヒント句。この名前は後で複雑な複合ステートメントの中でのヒント句の指定をシンプル化する為に利用できる

MySQL 5.7.8では、Oystein Grovlenがサブクエリのオプティマイザを制御するヒント句戦略を追加した。

  • SEMIJOINNO_SEMIJOIN : セミジョイン戦略を有効化または無効化する
  • SUBQUERY : サブクエリ実体化またはEXISTS戦略(IN-to-EXISTS transformations)を使うかどうかに影響する

新しいヒント句に関する追加の営みの詳細については、マニュアルの新しい節を参照してほしい。

私は新しいヒント句が役立つと分かってもらえるとうれしい! 新しいヒント句について疑問点をお持ちの場合や、問題に遭遇した場合は、ここのコメントで知らせていただき、bugs.mysql.comのバグレポートをオープンするかサポートチケットをオープンして欲しい。(訳注: コメントは元サイトに記載お願いします)

MySQLを使ってくれてありがとう!


PlanetMySQL Voting: Vote UP / Vote DOWN

InnoDBにおける効果的な関数インデックス(MySQL Server Blogより)

$
0
0
免責事項

この記事はJimmy Yang氏によるMySQL Server Blogの投稿「JSON Labs Release: Effective Functional Indexes in InnoDB」(2015/4/9)をユーザが翻訳したものであり、Oracle公式の文書ではありません。


MySQL 5.7.6では、我々は生成列(Generated Columns)と呼ばれる新しい機能を追加した。最初の段階では全ての生成列は、仮想的なものであってもマテリアライズされていた。これでは不要なディスクスペースが使用され、ディスクI/Oが発生するだけでなく、いかなるテーブルの変更に対してもテーブルを完全にリビルドする必要がある。新しいMySQL 5.7.7のJSON Labリリースでは、全ての問題を新しい機能、すなわちユーザーがマテリアライズされない仮想列を生成できるだけでなく、インデックスを作成できる機能、これをInnoDBを実装することで解決した。仮想列のデータは関数を利用して生成することもできるので、ある程度までは、"仮想インデックス"を関数インデックスまたは関数ベースのインデックスとしてみることができる。

このブログ投稿では、仮想列と仮想インデックスの設計について少し詳細にみて、InnoDBでどのように実装されているかの理解をお手伝いしようと思う。

InnoDBにおける仮想列

まずはInnoDB内部で仮想列がどのように表現されるかをみてみよう。簡単にいうと、仮想列は今やInnoDBのユーザーテーブルにも、クラスタインデックスにも全く保存されず、InnoDBのシステムテーブル上で表現される。詳細をみてみよう。


1. 仮想列はInnoDBのテーブル内には保存されない

仮想列はInnoDBにおいて今や真の"仮想"であり、これはInnoDBはクラスタインデックス(テーブルのデータを基本的に格納するのに使われる)内の列にいかなるデータも保存しないことを意味する。例を見てみよう。

# 仮想列の例
mysql> CREATE TABLE t (a INT, b INT, c INT GENERATED ALWAYS AS(a+b), PRIMARY KEY(a));
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO t VALUES (11, 3, default);
Query OK, 1 row affected (0.00 sec)

ここで列'c'は仮想列である。InnoDBのこのテーブルの物理的なデータ配置を見てみると、2つのユーザー列'a'と'b'および、2つの標準であるInnoDBの隠し/内部列(DATA_TRX_IDとDATA_ROLL_PTR)しか持っていない

# 物理的なレコード配置
not-deleted PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 8000000b; asc     ;;           /* column 'a' */
 1: len 6; hex 00000000670b; asc     g ;;     /* InnoDB hidden column */
 2: len 7; hex a90000011d0110; asc        ;;  /* InnoDB hidden column */
 3: len 4; hex 80000003; asc     ;;           /* column 'b' */

したがって'c'列はInnoDBのテーブルや列として保存されておらず、代わりにテーブルにクエリを発行した時にその場で計算される。

# クエリの例
mysql> SELECT * FROM t;
+----+------+------+
| a  | b    | c    |
+----+------+------+
| 11 |    3 |   14 |
+----+------+------+
1 row in set (0.00 sec)

2. 仮想列のメタデータの表現

仮想列自体はInnoDB内に保存されないとしても、メタデータはある。そのような列にセカンダリインデックスを作成するのをサポートする為にメタデータを保存する必要があるのだ。

仮想列のメタデータ情報は、InnoDBのSYS_COLUMNSシステムテーブルに他の列と共に保存されており、'PRTYPE'の値に追加のDATA_VIRTUAL(8192)ビットが設定されている点のみが異なる。

# SYS_COLUMNSの例
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_COLUMNS 
WHERE TABLE_ID IN (SELECT TABLE_ID FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES WHERE NAME LIKE "t%");

+----------+------+-------+-------+--------+-----+
| TABLE_ID | NAME | POS   | MTYPE | PRTYPE | LEN |
+----------+------+-------+-------+--------+-----+
|       74 | a    |     0 |     6 |   1283 |   4 |
|       74 | b    |     1 |     6 |   1027 |   4 |
|       74 | c    | 65538 |     6 |   9219 |   4 |
+----------+------+-------+-------+--------+-----+
3 rows in set (0.01 sec)

仮想列'c'が、SYS_COLUMNSシステムテーブルに登録されており、'PRTYPE'にDATA_VIRTUAL(8192)ビットがたっている点に注意して欲しい。'POS'フィールドもまた特別で、元のテーブルの位置(第3カラム)と、仮想列としてのシークエンス(最初の仮想列)の両方をエンコードしている。

SYS_COLUMNSシステムテーブルに加え、SYS_VIRTUALと呼ばれるシステムテーブルも新しく追加した。仮想列が他の列(基底列, the base columns)にもとづいて生成されているかどうかを記録するためである。上記の例では、'c'列は'a'列および'b'列から計算されている。

# SYS_VIRTUALの例
myql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_VIRTUAL;
+----------+-------+----------+
| TABLE_ID | POS   | BASE_POS |
+----------+-------+----------+
|       74 | 65538 |        0 |
|       74 | 65538 |        1 |
+----------+-------+----------+
2 rows in set (0.00 sec)

上記の例では、'POS'列はSYS_COLUMNS内の仮想列の'POS'の値を表し(この例では'c'列)、'BASE_POS'はSYS_COLUMNS内の基底列の'POS'の値(0は'a'列、1は'b'列)を表す。現在のところ、"基底列"は通常のマテリアライズされた列のみで構成可能であり、他の生成列からは構成できない。

仮想列が他の標準の列と同様にシステムテーブルに追加されたが、これらは別ドメインに表現されておりメモリ内のメタデータを格納する為の通常の列ではない。このようにして、InnoDBにはほとんど変更を加える必要はない。なぜならば、大多数のケースで仮想列が存在しないものとして動作し続けるからである。しかし、それと同時に実際に必要になればいつでも仮想列の情報を取得することができる。例えば、dict_table_t::cols構造体は通常のマテリアライズされた列の全ての情報を持っており、一方で新しいdict_table_t::v_cols構造体は仮想またはマテリアライズされない列の全ての情報を持っている。


この設計において、仮想列は簡単に追加して削除することができ、かつテーブル全体をリビルドする必要もない。これにより関連するテーブルのスキーマ変更がとても簡単かつ高速になる。

# 高速な仮想列の管理
mysql> ALTER TABLE t ADD new_col INT GENERATED ALWAYS AS (a - b) VIRTUAL;
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM t;
+----+------+------+---------+
| a  | b    | c    | new_col |
+----+------+------+---------+
| 11 |    3 |   14 |       8 |
+----+------+------+---------+
1 row in set (0.01 sec)

マテリアライズしない仮想カラムへのインデックスの作成

前の節でみたように、仮想列はとても柔軟であり、簡単に追加や削除ができる。しかしながら、仮想列はInnoDBのクラスタインデックス内に保存されていない為、クエリで値を取得する為に条件を満たす可能性のある各列に対し、基底列のデータを取得し、必要な計算をする必要がある。これによりクエリが遅く、非効率なものになる。しかし通常の列と同じ位クエリを効率的にする方法がある!今や仮想列にセカンダリインデックスを簡単に作ることができるのだ!

ひとたびセカンダリインデックスが仮想列に作成されたら、仮想列のデータはセカンダリインデックスのレコードに必然的にマテリアライズされ格納される。これは仮想列にクエリが発行された際に、仮想列の値を計算する必要がないことを意味している。再度になるが、これは関数インデックス関数ベースのインデックスを効果的にするものだ。

1. 仮想インデックスの作成

インデックス作成の記法は他のセカンダリインデックスを作成する時と同様である。

# 仮想列上のインデックスの作成
mysql> CREATE INDEX idx ON t(c);
Query OK, 1 row affected (0.08 sec)
Records: 1  Duplicates: 0  Warnings: 0

新しい'idx'インデックスがSYS_INDEXESシステムテーブルに追加され、仮想列'c'はSYS_FIELDSシステムテーブルに追加される。

# SYS_INDEXSおよびSYS_FIELDSレコードの例
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_INDEXES WHERE TABLE_ID IN (SELECT TABLE_ID FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES WHERE NAME LIKE "t%");
+----------+---------+----------+------+----------+---------+-------+-----------------+
| INDEX_ID | NAME    | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE | MERGE_THRESHOLD |
+----------+---------+----------+------+----------+---------+-------+-----------------+
|      123 | PRIMARY |       75 |    3 |        1 |       3 |    63 |              50 |
|      124 | idx     |       75 |  128 |        1 |       4 |    63 |              50 |
+----------+---------+----------+------+----------+---------+-------+-----------------+
2 rows in set (0.01 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_FIELDS WHERE INDEX_ID = 124;
+----------+------+-----+
| INDEX_ID | NAME | POS |
+----------+------+-----+
|      124 | c    |   0 |
+----------+------+-----+
1 row in set (0.03 sec)

仮想列はInnoDBのメタデータシステムテーブル内にある他の列と同じように保存されるため、仮想インデックスのメタデータは通常のインデックスのメタデータと同じように表現される。

通常の列に対するインデックス作成との違いは、インデックス作成時に、インデックスがはられた列が仮想列だと分かったら、最終的に特定の生成関数を呼ぶ前にその"基底列"が取得されコールバック関数が基底列にアクセスするのに使われるところだ。一度このコールバック関数から値が計算されれば、値はソーターに渡され、後でインデックスレコードをインスタンス化するのに使われる。

2. DMLの実行

仮想列のデータは今やセカンダリインデックスを通じて"マテリアライズされ"ているため、どのようなDML(INSERT、UPDATE、DELETE)でもインデックスに影響をおよぼしうる。仮想列の値は他のインデクスがはられた列の値と同様に更新される。しかし、仮想列に対して値を直接INSERTまたはUPDATEすることはできない。代わりにINSERTとUPDATE操作は基底列の変更を通じて間接的に実行される。前に示した例のテーブルを使ってこれを実演してみよう。

# インデックスレコードの更新の実例
mysql> select * from t;
+----+------+------+---------+
| a  | b    | c    | new_col |
+----+------+------+---------+
| 11 | 3    | 14   | 8       |
+----+------+------+---------+
1 row in set (0.00 sec)

mysql> UPDATE t SET a = 20;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from t;
+----+------+------+---------+
| a  | b    | c    | new_col |
+----+------+------+---------+
| 20 | 3    | 23   | 17      |
+----+------+------+---------+
1 row in set (0.01 sec)

mysql> EXPLAIN SELECT c FROM t;
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| 1  | SIMPLE      | t     | NULL       | index | NULL          | idx  | 5       | NULL | 1    | 100.00   | Using index |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT c FROM t;
+------+
| c    |
+------+
| 23   |
+------+
1 row in set (0.00 sec)

ご覧いただいたように、列'c'のインデックスの値は基底列'a'が更新されるのに伴い更新されている。

ここで注目いただきたいのは、データの変更がMVCC、クラッシュリカバリ、UNDO操作に対して再計算が不要であるように仮想列のDMLをログに記録していることである。もちろんこれらのインデックスがはられた仮想列への操作だけは記録される。

"関数インデックス"を使ったクエリ

仮想列の"関数インデックス"があれば、ユーザーは条件を満たした列をカバードスキャンおよびノンカバードスキャンの両方を利用して検索できる。"関数インデックス"はクエリ可能で、シナリオ(分離レベルなど)に応じて、クラスタインデックスが続いて参照される。

UNDOログ内に仮想カラムの更新も記録する為、仮想列へのクエリもMVCCをサポートする。しかしながら、最大インデックスサイズの制限を考慮する必要があり、COMPACT/REDUNDANT ROWフォーマットなら767バイト、COMPRESSED/DYNAMIC ROWフォーマットなら3072バイトである。クエリが発行されたオブジェクトがこれより長い場合は、値は基底列からその場で生成されることになる。

クエリは全ての分離レベルをサポートしており、これはしかるべき場合にはインデックスがはられた仮想列にギャップロックを生成しうることを意味する。

その上、"関数インデックス"はユニークインデックスだけでなく、プレフィックスインデックスもサポートしている。ここに新しいJSONサポートを含む全ての例を示す。

# JSONベースの例
mysql> create table employees(id bigint not null primary key auto_increment, info JSON);
Query OK, 0 rows affected (0.20 sec)

mysql> insert into employees (info) values ('{ "name": "Matt Lord", "age": 38, "Duties": { "Product Manager": ["stuff", "more stuff"]} }');
Query OK, 1 row affected (0.04 sec)

mysql> select jsn_valid(info) from employees;
+-----------------+
| jsn_valid(info) |
+-----------------+
|               1 |
+-----------------+
1 row in set (0.00 sec)

mysql> select id, jsn_extract(info, '$.name') from employees;
+----+-----------------------------+
| id | jsn_extract(info, '$.name') |
+----+-----------------------------+
|  1 | "Matt Lord"                 |
+----+-----------------------------+
1 row in set (0.00 sec)

mysql> alter table employees add name varchar(100) generated always as (jsn_extract(info, '$.name')) virtual;
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table employees add index (name);
Query OK, 1 row affected (0.51 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> show create table employees\G
*************************** 1. row ***************************
   Table: employees
Create Table: CREATE TABLE `employees` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `info` json DEFAULT NULL,
  `name` varchar(100) GENERATED ALWAYS AS (jsn_extract(info, '$.name')) VIRTUAL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> explain format=JSON select id, name from employees where name = "Matt Lord"\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
"select_id": 1,
"cost_info": {
  "query_cost": "1.20"
},
"table": {
  "table_name": "employees",
  "access_type": "ref",
  "possible_keys": [
    "name"
  ],
  "key": "name",
  "used_key_parts": [
    "name"
  ],
  "key_length": "103",
  "ref": [
    "const"
  ],
  "rows_examined_per_scan": 1,
  "rows_produced_per_join": 1,
  "filtered": "100.00",
  "using_index": true,
  "cost_info": {
    "read_cost": "1.00",
    "eval_cost": "0.20",
    "prefix_cost": "1.20",
    "data_read_per_join": "128"
  },
  "used_columns": [
    "id",
    "info",
    "name"
  ]
}
  }
}
1 row in set, 1 warning (0.00 sec)

制限事項

仮想インデックス周辺には現在いくつかの制限事項があり、そのうちのいくつかをここにあげる。

  1. 主キーはいかなる仮想列も包含できない
  2. 仮想列と非仮想列の混在に対してはインデックスが生成できない
  3. 仮想列に空間あるいはフルテキストインデックスは生成できない(この制限事項は今後解消される)
  4. 仮想インデックスは外部キーとして利用できない

まとめ

要約すれば、新しく実装された仮想列、仮想インデックス、効果的な"関数インデックス"により、ユーザーは新しい仮想列をすばやく追加/削除できるようになり、そのような列にセカンダリインデックスを生成することで効果的なクエリが発行できる可能性がある。大きなTEXT/JSONフィールドやその他のリレーショナルでないデータのインデクスの理想的な解決策となり、効率的なデータ収容、操作、そのようなデータへのクエリ発行の助けとなる。

これらの新機能についての考えを教えて欲しい!みなからフィードバックを必要としており、生成列やJSONサポートの拡張関連でその他に知りたいことを教えて欲しい。新しい機能を使う上で問題に直面したら、ここのコメントで知らせていただき、bugs.mysql.comのバグレポートをオープンするかサポートチケットをオープンして欲しい。

MySQLを使ってくれてありがとう!


PlanetMySQL Voting: Vote UP / Vote DOWN

分散DB本読書会第46回メモ「STOP AFTER 10」

$
0
0
実は一年以上前から隔週で「Principles of Distributed Database System」という本の読書会にでています。

多くの気づきとタイポと、やるべき課題が見付かるのですが、毎回飲んでは忘れてしまうというていたらくで、どうにもまとまる気がしません。なので、今回からメモを残していくことにします。木村自身はデータベースの専門教育をうけたことがないので、いろいろ間違いもあるかと思いますが、その点はコメントやtwitter/facebookにてつっこんでいただければ修正します。

分散DB本読書会第46回: 16.2 Schema Mapping in P2P System(p.624)-16.3.1.1 Basic TechniquesのThreshould Algorithm(TA) (p.632)

top-k Queriesのサンプルクエリ(p.629)に以下のようなものがありました。

SELECT *
FROM Patient P
WHERE P.disease = ''diabetes''
AND P.height < 170
AND P.weight > 160
ORDER BY scoring-function(height, weight)
STOP AFTER 10

"STOP AFTER 10"は見たことのない記法ですが、みるからにLIMIT 10ですよね。。。。
ググってみるとIBM Almaden Research Center の Michael J. Careyらが提案していた
LIMIT相当の機能のようです。

Reducing the Braking Distance of an SQL Query Engine

On Saying “Enough Already!” in SQL

結局実装としてはMySQLが先行し、SQL標準になったのはSQL:2008なのでRDBMSの最近のバージョンでしかまだサポートされていません。

IBMの人が提案していたのだから、そのままDB2でSTOP AFTER句が実装されていれば、
いろいろスムーズだったと思うのですがねぇ。

Typo:
p.627 Fig. 16.9 右側のCSD1がCSD2では?

これは「Handbook of Peer-to-Peer Networking」のp.554に「Fig 15 Common Agreement Schema Mapping in APPA」に同じ図画CSD2で掲載されている。

JUGEMテーマ:コンピュータ



PlanetMySQL Voting: Vote UP / Vote DOWN

MySQL 5.7.7におけるオプティマイザヒント: マニュアルがない (The Percona Performance Blogより)

$
0
0
出典について

この記事はSveta Smirnova氏によるThe Percona Performance Blogの記事「Optimizer hints in MySQL 5.7.7 – The missed manual」(2015/4/30)を翻訳したものである。


MySQL 5.7.7ではOracle社は新しく頼もしい機能を明らかにした。オプティマイザヒントである。しかしこのヒントに関してはドキュメントが公開されていない。ヒントに関してユーザーマニュアルで見つけた記載は下記のみである。


  • 今やオプティマイザにヒントを付けることが可能となり、/*+ ... */のコメントをSQL文のSELECTINSERTREPLACEUPDATE、またはDELETEキーワードに続けて付けることによってこれが実現できる。このようなSQL文はEXPLAINと共に利用することもできる。例は次の通り。
SELECT /*+ NO_RANGE_OPTIMIZATION(t3 PRIMARY, f2_idx) */ f1
FROM t3 WHERE f1 > 30 AND f1 < 33;
SELECT /*+ BKA(t1, t2) */ * FROM t1 INNER JOIN t2 WHERE ...;
SELECT /*+ NO_ICP(t1) */ * FROM t1 WHERE ...;

3つのワークログもある。WL #3996WL #8016、そしてWL #8017である。しかしこれらは一般的な概念について述べているだけで、どの最適化が使えてどのように使えるかに関する情報が多くはなかった。これに関する詳細はPercona LiveのØystein Grøvlenのセッションのスライド 59ページ目にある。これが全てである。"公式の"利用可能な最適化のリストがなく、使い方がなく... 何もない。

私は自分で整理を試みた。

最初は最初にスライド 59は可能なインデックスヒントの7つのうち6つがリストされていることを見つけた。これは新しい機能の為に作られた、MySQLのソースツリーのsqlディレクトリ下にある2つの新しいファイルののうちの1つで確認した。

$cat sql/opt_hints.h
...
/**
  Hint types, MAX_HINT_ENUM should be always last.
  This enum should be synchronized with opt_hint_info
  array(see opt_hints.cc).
*/
enum opt_hints_enum
{
  BKA_HINT_ENUM= 0,
  BNL_HINT_ENUM,
  ICP_HINT_ENUM,
  MRR_HINT_ENUM,
  NO_RANGE_HINT_ENUM,
  MAX_EXEC_TIME_HINT_ENUM,
  QB_NAME_HINT_ENUM,
  MAX_HINT_ENUM
};

sql/opt_hints.ccファイルを見てみると、これらの最適化にはそんなに選択肢がないことが分かった。有効か、無効かのいずれかである。

$cat sql/opt_hints.cc
...
struct st_opt_hint_info opt_hint_info[]=
{
  {"BKA", true, true},
  {"BNL", true, true},
  {"ICP", true, true},
  {"MRR", true, true},
  {"NO_RANGE_OPTIMIZATION", true, true},
  {"MAX_EXECUTION_TIME", false, false},
  {"QB_NAME", false, false},
  {0, 0, 0}
};

SQL文にヒントを入れる入れ方: コメント内に"+"記号で /*+ NO_RANGE_OPTIMIZATION(t3 PRIMARY, f2_idx) */ とし、これはOracleのオプティマイザヒントの文法と互換がある。

実のところ、これらのヒントは以前もoptimizer_switch変数を通じて利用することができた。少なくともBKABNLICPMRRのような最適化に関しては。しかし新たな文法でこれらをグローバルまたはセッション毎に変更できるだけでなく、クエリのあるテーブルと列に特定の最適化を有効化したり無効化したりすることができる。これを下記の極めて人工的ではあるが常に実施できる例で実演する。

# NO_RANGE_OPTIMIZATION
mysql> use mysql
Database changed
mysql> explain select * from user where host in ('%', '127.0.0.1');
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | PRIMARY       | PRIMARY | 180     | NULL |    2 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)
mysql> explain select /*+ NO_RANGE_OPTIMIZATION(user PRIMARY) */ * from user where host in ('%', '127.0.0.1');
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ALL  | PRIMARY       | NULL | NULL    | NULL |    5 |    40.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

私は以前は直接は有効化あるいは無効化ができなかったヒントを利用した。レンジの最適化である。

もう1つ"直感的に"ドキュメント化される機能は特定の最適化を有効化したり無効化したりするものだ。これは、BKABNLICP、そしてMRRにのみ有効である。JOINにおけるインデックスの利用または特定のテーブルに対するこれらのアルゴリズムの利用を避けたい場合、NO_BKA(table[[, table]...])、NO_BNL(table[[, table]...])、NO_ICP(table indexes[[, table indexes]...])、そしてNO_MRR(table indexes[[, table indexes]...])を指定する。

MAX_EXECUTION_TIMEはテーブル名やキー名を引数に必要としない。その代わり、クエリを実行する最大の時間をミリ秒で指定する必要がある。

# MAX_EXECUTION_TIMEの例
mysql> select /*+ MAX_EXECUTION_TIME(1000) */  sleep(1) from user;
ERROR 3024 (HY000): Query execution was interrupted, max_statement_time exceeded
mysql> select /*+ MAX_EXECUTION_TIME(10000) */  sleep(1) from user;
+----------+
| sleep(1) |
+----------+
|        0 |
|        0 |
|        0 |
|        0 |
|        0 |
+----------+
5 rows in set (5.00 sec)

QB_NAMEはより複雑である。WL #8017はこれがカスタムコンテキストであることを示している。しかしこれはいったい何であろうか?答えはMySQLのテストスイートにある。オプティマイザヒントへのテストは、t/opt_hints.test内にあり、QB_NAMEの最初のエントリは次のクエリである。

# QB_NAMEの例
EXPLAIN SELECT /*+ NO_ICP(t3@qb1 f3_idx) */ f2 FROM
  (SELECT /*+ QB_NAME(QB1) */ f2, f3, f1 FROM t3 WHERE f1 > 2 AND f3 = 'poiu') AS TD
WHERE TD.f1 > 2 AND TD.f3 = 'poiu';

従って、どんなサブクエリに対してもカスタムのQB_NAMEを指定し、そのコンテクストのみに対するオプティマイザヒントを指定することができる。

この短い要旨を結論づける為に、クエリのヒントが本当に必要となる実用的な例を示したいと思う。先週、ある顧客がMySQLのバージョンを5.5から5.6にアップグレードしたところ、いくつかのクエリが以前より遅くなったという問題に取組んだ。面白い回答を導いたが、なお正しいものである。"このような挙動の1つの原因はオプティマイザの改善である。全体としては良いパフォーマンスになっているが、いくつかのクエリは、古いバージョンに対し最適化され、以前より遅くなりうるのだ。"

このようなクエリの公知の例を実演する為に、私はお気に入りの情報源を使う。MySQL Community Bugs Databaseである。オプティマイザのリグレッションバグでまだ改修されていないものを探す上で、我々はbug #68919を見つけ、これはLIMIT句のあるクエリでMRRアルゴリズムが使われるケースでのリグレッションである。クエリを実行する際に、バグレポートで示されている通り、大きな違いが発生することが分かるだろう。

# Bug #68919とNO_MRRのヒント
mysql> SELECT * FROM t1 WHERE i1>=42 AND i2<=42 LIMIT 1;
+----+----+----+----+
| pk | i1 | i2 | i3 |
+----+----+----+----+
| 42 | 42 | 42 | 42 |
+----+----+----+----+
1 row in set (6.88 sec)
mysql> explain SELECT * FROM t1 WHERE i1>=42 AND i2<=42 LIMIT 1;
+----+-------------+-------+------------+-------+---------------+------+---------+------+---------+----------+----------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows    | filtered | Extra                            |
+----+-------------+-------+------------+-------+---------------+------+---------+------+---------+----------+----------------------------------+
|  1 | SIMPLE      | t1    | NULL       | range | idx           | idx  | 4       | NULL | 9999958 |    33.33 | Using index condition; Using MRR |
+----+-------------+-------+------------+-------+---------------+------+---------+------+---------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT /*+ NO_MRR(t1) */ *  FROM t1  WHERE i1>=42 AND i2<=42 LIMIT 1;
+----+----+----+----+
| pk | i1 | i2 | i3 |
+----+----+----+----+
| 42 | 42 | 42 | 42 |
+----+----+----+----+
1 row in set (0.00 sec)

MRRが使われている場合のクエリ実行は6.88秒かかっており、MRRが使われていない場合は0秒である!しかしバグレポート自体はoptimizer_switch="mrr=off";を利用することを一時的な対処として提案している。これはSET optimizer_switch="mrr=off";で実行してよければ完全にうまくいく。クエリを実行するたび毎回OFFにする恩恵に授かる。オプティマイザヒントを使えば、あるクエリの特定のテーブルに対する特定の最適化アルゴリズムをONにし、他のクエリに対してはOFFにできる。ここで再度、とても人為的な例で、これを実演する。

# 同一クエリ内のMRRのオンオフ
mysql> explain select /*+ MRR(dept_emp) */ * from dept_emp where to_date in  (select /*+ NO_MRR(salaries)*/ to_date from salaries where salary >40000 and salary <45000) and emp_no >10100 and emp_no < 30200 and dept_no in ('d005', 'd006','d007');
+----+--------------+-------------+------------+--------+------------------------+------------+---------+----------------------------+---------+----------+-----------------------------------------------+
| id | select_type  | table       | partitions | type   | possible_keys          | key        | key_len | ref                        | rows    | filtered | Extra                                         |
+----+--------------+-------------+------------+--------+------------------------+------------+---------+----------------------------+---------+----------+-----------------------------------------------+
|  1 | SIMPLE       | dept_emp    | NULL       | range  | PRIMARY,emp_no,dept_no | dept_no    | 8       | NULL                       |   10578 |   100.00 | Using index condition; Using where; Using MRR |
|  1 | SIMPLE       | <subquery2> | NULL       | eq_ref | <auto_key>             | <auto_key> | 3       | employees.dept_emp.to_date |       1 |   100.00 | NULL                                          |
|  2 | MATERIALIZED | salaries    | NULL       | ALL    | salary                 | NULL       | NULL    | NULL                       | 2838533 |    17.88 | Using where                                   |
+----+--------------+-------------+------------+--------+------------------------+------------+---------+----------------------------+---------+----------+-----------------------------------------------+
3 rows in set, 1 warning (0.00 sec)

PlanetMySQL Voting: Vote UP / Vote DOWN

#yapcasia でMySQL 5.7の罠についてLTしてきました

$
0
0
YAPC::Asia Tokyo 2015 お疲れ様でした!

2日目のライトニングトークでしゃべらせていただきました。




ネタ的には 発掘するたび書き溜めてきたブログ記事 から 笑いが取れそうなものを 大事そうなもののみをピックアップして紹介した感じです。

知らないと致命傷、でも知ってれば予防できる(はず)
MySQL 5.7で不幸になる人が1人でも少なくなってくれることを願っています。


さて、今年のYAPC::Asiaはメイントラックもトークを応募していたのですが見事に落選したので、1日目は完全にリラックスして過ごしました。LTの採否、当日になるまでわかんないのか大変だなーとか、他人事だったんですが、

1日目のLT見るじゃないですか。

面白いじゃないですか。

俺もしゃべりたくなるじゃないですか。

なったんですよ!!1


が、翌朝になってLTの応募ページをたどってみると

"Currently talk submissions are disabled" Σ(゚д゚lll)
— yoku0825 (@yoku0825) 2015, 8月 22



昨日のうちにやっておくべきだった。。
— yoku0825 (@yoku0825) 2015, 8月 22


しかしその後復活していたので


駆け込みで応募!! / “MySQL 5.7の罠があなたを狙っている - YAPC::Asia Tokyo 2015” http://t.co/HEpibPCozW
— yoku0825 (@yoku0825) 2015, 8月 22


申し込み!


本日のLTです!ご確認ください!倍率3倍以上っておかしい! http://t.co/hMllgT89Ew
— yapcasia (@yapcasia) 2015, 8月 22


その1時間後にはもう締め切られていた! (あるいは、情けで応募させてくれたのかも。。)
とてもありがたいことに採択してもらえましたが、こういう悪ノリが出来るのもYAPCだからなのかなって思います。
(わたしはChiba.pm出身なので、たぶん結構ノリが近くて)

YAPCで「1年ぶりー」って挨拶した人もいっぱいいて、なんかこう同窓会っぽくて楽しいね、なんて話をしてたりもしました。


ありがとうYAPC::Asia 2015!!
PlanetMySQL Voting: Vote UP / Vote DOWN

MySQLのタイムゾーン

$
0
0

YAPC::Asia 2015 のセッションで、MySQL のタイムゾーンの話が出ていましたが、以前タイムゾーン周りで少しはまったことがあったのを思い出したので書いてみます。

MySQLのデフォルトのタイムゾーンは mysqld 起動時のシステム設定です。TZ 環境変数の値か、変数が設定されていなければ /etc/localtime(Ubuntu の場合) です。

# TZ=Japan /usr/sbin/mysqld
mysql> SHOW VARIABLES LIKE '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | JST    |
| time_zone        | SYSTEM |
+------------------+--------+
mysql> system date; SELECT NOW();
2015年  8月 22日 土曜日 21:39:12 JST
+---------------------+
| now()               |
+---------------------+
| 2015-08-22 21:39:12 |
+---------------------+
# TZ=UTC /usr/sbin/mysqld
mysql> SHOW VARIABLES LIKE '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | UTC    |
| time_zone        | SYSTEM |
+------------------+--------+
mysql> system date; SELECT NOW();
2015年  8月 22日 土曜日 21:39:52 JST
+---------------------+
| now()               |
+---------------------+
| 2015-08-22 12:39:52 |
+---------------------+

MySQL のサーバーとクライアントでタイムゾーンが合っていないと、何かまずいことが起きるのか見てみます。

mysql> CREATE TABLE test (ts TIMESTAMP, dt DATETIME);
mysql> INSERT INTO test (ts, dt) VALUES (NOW(), NOW());
mysql> INSERT INTO test (ts, dt) VALUES ('2015-08-22 21:56:39', '2015-08-22 21:56:39');
mysql> SELECT * FROM test;
+---------------------+---------------------+
| ts                  | dt                  |
+---------------------+---------------------+
| 2015-08-22 12:56:09 | 2015-08-22 12:56:09 |
| 2015-08-22 21:56:39 | 2015-08-22 21:56:39 |
+---------------------+---------------------+

NOW() はサーバー側で実行されるので、サーバー側の時刻になります。クライアントが自分のタイムゾーンの時刻を取得して値を設定するとクライアントの時刻になります。

ここで、タイムゾーンの設定が誤っていることに気づいて、mysqld を起動しなおしました。

# TZ=Japan /usr/sbin/mysqld
mysql> select * from test;
+---------------------+---------------------+
| ts                  | dt                  |
+---------------------+---------------------+
| 2015-08-22 21:56:09 | 2015-08-22 12:56:09 |
| 2015-08-23 06:56:39 | 2015-08-22 21:56:39 |
+---------------------+---------------------+

TIMESTAMP カラムに設定した値が変わってしまいました。

TIMESTAMP カラムは内部的には 1970-01-01 00:00:00 UTC からの経過秒数で値を保持していて、その値を現在のタイムゾーンに合わせて日時形式に変換しているためです。 UTC の 12:56:09 と JST の 21:56:09 は TIMESTAMP の内部表現的には同じ値になっています。

DATETIME カラムは指定した値がそのまま保持されているため、タイムゾーンの影響を受けません。

TIMESTAMP カラムに値を登録した後にタイムゾーンを変更するようなことは避けたほうがいいでしょう。 個人的には、時刻を保持するには DATETIME カラムにして、TIMESTAMP は使わないのをおすすめしたいです。

DATETIME カラムの場合でも、NOW() 等の時刻取得関数はタイムゾーンの影響を受けるので、サーバーとクライアントのタイムゾーンは合わせておいた方が無難です。

AWS の RDS のように、システム設定を変更したり、mysqld 起動時の設定をいじることができない場合は、接続後に time_zone 変数を設定することで動きを変更できます。

mysql> SELECT @@system_time_zone, @@global.time_zone, @@time_zone;
+--------------------+--------------------+-------------+
| @@system_time_zone | @@global.time_zone | @@time_zone |
+--------------------+--------------------+-------------+
| UTC                | SYSTEM             | SYSTEM      |
+--------------------+--------------------+-------------+
mysql> SET time_zone = '+09:00';
mysql> SELECT @@system_time_zone, @@global.time_zone, @@time_zone;
+--------------------+--------------------+-------------+
| @@system_time_zone | @@global.time_zone | @@time_zone |
+--------------------+--------------------+-------------+
| UTC                | SYSTEM             | +09:00      |
+--------------------+--------------------+-------------+
mysql> SET global time_zone = '+09:00';
mysql> SELECT @@system_time_zone, @@global.time_zone, @@time_zone;
+--------------------+--------------------+-------------+
| @@system_time_zone | @@global.time_zone | @@time_zone |
+--------------------+--------------------+-------------+
| UTC                | +09:00             | +09:00      |
+--------------------+--------------------+-------------+

system_time_zone は変更できません。time_zone は現在の接続のみで有効です。global.time_zone はすべての接続で有効になります。既に接続中のクライアントには影響しません。global.time_zone の設定は mysqld が終了するまで有効です。

日本の場合は夏時間が無く、UTC との時差は常に +09:00 なので上記のような設定でいいのですが、夏時間があり、UTC との時差が季節によって異なるような地域では固定の値の設定では問題になります。

デフォルト状態では Japan のようなシンボルを time_zone 変数に指定することはできません。

mysql> SET time_zone = 'Japan';
ERROR 1298 (HY000): Unknown or incorrect time zone: 'Japan'

実は MySQL はタイムゾーン用のシステムテーブルを持っています。ただし初期状態では中身は空っぽです。

mysql> USE mysql
mysql> SHOW TABLES LIKE '%time_zone%';
+-------------------------------+
| Tables_in_mysql (%time_zone%) |
+-------------------------------+
| time_zone                     |
| time_zone_leap_second         |
| time_zone_name                |
| time_zone_transition          |
| time_zone_transition_type     |
+-------------------------------+
mysql> SELECT * FROM time_zone;
Empty set (0.00 sec)

mysql_tzinfo_to_sql というコマンドで OS のタイムゾーン情報を、これらのテーブルに格納するような SQL 文に変換することができます。

# mysql_tzinfo_to_sql /usr/share/zoneinfo
TRUNCATE TABLE time_zone;
TRUNCATE TABLE time_zone_name;
TRUNCATE TABLE time_zone_transition;
TRUNCATE TABLE time_zone_transition_type;
INSERT INTO time_zone (Use_leap_seconds) VALUES ('N');
SET @time_zone_id= LAST_INSERT_ID();
INSERT INTO time_zone_name (Name, Time_zone_id) VALUES ('Africa/Abidjan', @time_
zone_id);
INSERT INTO time_zone_transition (Time_zone_id, Transition_time, Transition_type
_id) VALUES
 (@time_zone_id, -2147483648, 0)
,(@time_zone_id, -1830383032, 1)
;
〜後略〜

このクエリを次のようにして mysql データベースで実行するとタイムゾーンが登録されます。

# mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot mysql

これで Japan のようなシンボルで指定することができるようになります。

mysql> set time_zone = 'Japan';
mysql> select @@system_time_zone, @@global.time_zone, @@time_zone;
+--------------------+--------------------+-------------+
| @@system_time_zone | @@global.time_zone | @@time_zone |
+--------------------+--------------------+-------------+
| UTC                | SYSTEM             | Japan       |
+--------------------+--------------------+-------------+

なお、time_zone が SYSTEM の場合はシステムのタイムゾーンが使われるので、夏時間も正しく処理されます。

自分は夏時間が絡むようなシステムは作ったことないのであまり知見がないのですが、時刻は UTC で登録しておいて、アプリ側で必要に応じて変換する方がいいと思っています。なんとなく…。


PlanetMySQL Voting: Vote UP / Vote DOWN

MySQL 5.6のパラレルレプリケーションの効用はいかほど?

$
0
0

この質問を今までとても良く受けてきた。"負荷が高い時には、リードレプリカがしばしば遅延し始める。N個のスキーマを利用しているが、MySQL 5.6のパラレルレプリケーションを使うとどのくらい性能が改善するのだろうか?"ここでは潜在的な効果を、素早く大雑把に見積もる方法をご紹介する。

基本的な考え方

MySQL 5.6では、スキーマレベルで並列処理が行われる。従って理論上は、N個のスキーマがあり、N個の並列スレッドを利用すれば、レプリケーションは最大N倍高速化する。これは少なくとも2つのことを仮定している。

  • レプリケーションのスループットは並列スレッドの数に対し線形にスケールする
  • 書込みはスキーマ間で均等に分布している

もちろんいずれの仮定も現実的ではない。しかし、書込みの分布を知るのは容易であり、これによりパラレルレプリケーションによりどの程度の恩恵がえられるかを見積もる助けとなる。

書込みはバイナリログに保存されるが、スロークエリの方がはるかに扱いやすいので、一定の間、全てのクエリに対してスロークエリログの記録を有効化する。long_query_time=0として、pt-query-digestを出力されるログファイルの解析に利用するのだ。

テストサーバーに3つのスキーマがあるとし、それなりのスロークエリログファイルをえる為にsysbenchの負荷をいくらかかける。 負荷がかけ終わったら次のコマンドを実施する。

pt-query-digest --filter '$event->{arg} !~ m/^select|^set|^commit|^show|^admin|^rollback|^begin/i' --group-by db --report-format profile slow_query.log > digest.out

得られた結果は次の通りである。

# Profile
# Rank Query ID Response time  Calls  R/Call V/M   Item
# ==== ======== ============== ====== ====== ===== ====
#    1 0x       791.6195 52.1% 100028 0.0079  0.70 db3
#    2 0x       525.1231 34.5% 100022 0.0053  0.68 db1
#    3 0x       203.4649 13.4% 100000 0.0020  0.64 db2

スレッドが3並列で、各スキーマが書込み負荷の33%を処理している理想的な環境においては、3倍の性能改善が見込めた。

しかし、結果レポートにおいては3つのレプリケーションスレッドは、最善のケースで25%(13.4/52.1 = 0.25)の時間しか同時に処理していないことが分かる。 負荷の一部では2レプリケーションスレッドのみが同時に動作することもありうるが、明確にする為にこれは無視する。

これは、理論上の200%の性能改善(100% 3並列スレッド稼働)に対し、せいぜい50%の性能改善(25%の時間 3並列稼働)しかえられないことを見積もれることを示している。 そして現実には、受けられる恩恵はそれよりはるかに低くなるだろう。

結論

MySQL 5.6におけるパラレルレプリケーションは大きな前進であるが、書込み負荷が全てのスキーマで均等に分布していない場合はあまり期待しないことだ。 ご紹介したpt-query-digestにより、ご利用の環境の負荷が、MySQL 5.6のスレーブのマルチスレッド処理に適しているかどうかを大雑把に把握できる。

MySQL 5.7では、並列処理が異なる方法で実現されていることだけでなく、バイナリログのグループコミットの設定を調整することによって並列レプリケーションの効率を調整できるため、はるかに改善されていることを期待している。


PlanetMySQL Voting: Vote UP / Vote DOWN

日本MySQLユーザ会会2015年8月、に参加してきた

$
0
0

8/27夜に東京渋谷で開催された、日本MySQLユーザ会会2015年8月、に参加してきました。
今回は Yoku0825さんが幹事をしてくださったので、私は気楽に一参加者で。

https://atnd.org/events/68323



f:id:sakaik:20150827182652j:plain


 kamipo さんが Oracle ACE(おらくるえーす)になったので、お祝いに講演いただくのが主たる趣旨(というか名目というか)。
Oracle ACE というのは、Oracle製品について著しい何らかの成果をあげた人を Oracle 社が認定するもので、Oracle ACE になると、なんか透明な置物がもらえるという特典があるそうです。
 MySQLをExpertとする Oracle ACE は、世界でも30人しかおらず*1、日本人では、松信さん、SH2さん、Yoku0825さんに次いで4人目。


 今回のメニューは、ざっとこんな感じ。

@kamipo さん、ぼくがやってきたこと、的なお話
@i_rethi さん、dimSTATというベンチマークツールの紹介話
@kazeburo さん、メルカリでのDB話と、PHPMySQLの怖い話の二本立て
@RKajiyama さん、MySQL 5.7 の RC2 (GAじゃなかった) の最新情報など


 細かい内容については、yamamo-i さんがざっとまとめてアップしてくださっているので、以下を参照。
http://qiita.com/yamamo-i/items/62b027e89d00a4542751

 私はメモも取らずに気楽にきいていたので、この日記では主に印象面を中心に、たよりない記憶で書きます。

kamipoさんのお話

 「困っている人を助けたい」という言葉に、kamipoさんのバイタリティの源を理解しました。
知らずによろしくない使い方をしている人を見ると放っておけず、Twitterで探し出して教えてあげるなど、熱量半端ない。
MySQLだけが良くなってもダメなんですよ。ユーザ(開発者)は開発言語を通してMySQLを見るわけだから、ORマッパやミドルウェアの使い勝手が悪いと "MySQLつかえね" になっちゃう」的なお話に、いちいち肯きました。

i_rethi さんのお話

 むつかしかったです。dimSTAT というツールを使うとperformance_schema(P_S)から色々とってきてグラフを描いてくれて便利、ということは分かりました。終了後のYokuさんの言葉。「結論だけ教えてくれw」
 自分の生活エリアでは、こうやって大規模なパフォーマンスを気にする事案が最近ないので、細かいところは分からないなりに楽しく聞かせて貰いました。とりあえず dimSTAT を、必要となったときにさくっと使える状態にはなっておきたいですね。

kazeburo さんのお話

 使用している環境のこととか工夫のこととか色々聞かせてもらったのですが、もう、印象のすべては「MySQL 5.7 Multi source replication」に持って行かれました(笑)。使ってるんだ!使えるんだ! ハマったところや、遅延規模等、思っていたのと違った点などがあれば、いずれかの機会にお話伺いたいものです。
 その他、PHPでのDBアクセスまわりのエラーハンドリングの話も、気をつけていないとスルーしちゃうコードを自分でも書きそうで、怖かった。先日YAPC後の知人との呑みの際に教えてもらった「postgreSQLの、トランザクション失敗時の挙動」に匹敵する(?)怖さです。(トランザクション中にエラーが発生すると、その後のクエリはすべてエラーとなる。ただし commit を投げるとエラーにならずに rollback される、というお話)

RKajiyama さんのお話

 要約すると「海外の開発者達は容赦なく夏休みを取るので、今日お披露目したかったものがまだ公開に至っていない」というお話w。
 JSON_*関数→(別物)→JSN_*関数→(同じ)→JSON_*関数 の変遷は、調べ物をするときにそれがいつ書かれたかに要注意な事案となりそう。
 「疑似同期レプリケーション」という、謎な概念が出始めている模様。準同期ではなくて。杉山さんのスライドが参考になるかも。あとで読むhttp://www.slideshare.net/ShinyaSugiyama/mysql-57

私から最後にアナウンス

 MySQLユーザ会は 2000年3月の設立から、今年で15年を迎えました。記念に、MySQLユーザ会会特別版みたいな感じで、記念パーティをやります(飲食付きの会会)。日程が先ほど決定して「10月30日(金)」です。15年を振り返る的なテーマでやりたいと考えていますので、ご興味の方はスケジュール空けておいてください!
(アナウンスしなかったけど、だいたい 19:00-21:30 かなと考えています。多少前後するかも)


 そんなわけで、その後知人らと飲みに行って終電ぎりぎり(ちゃんと帰った私は勝ち組(?))。木曜日って電車空いているんですね。


 会場&飲み物を提供してくださった GMO さま、どうもありがとうございました。



★適宜追記:関連リンク集
http://blog.nomadscafe.jp/2015/08/mynamysql-20158-php.html
http://qiita.com/yamamo-i/items/62b027e89d00a4542751
http://togetter.com/li/866422
http://smallpalace.hatenablog.com/entry/2015/08/27/225023

*1:2015年8月現在


PlanetMySQL Voting: Vote UP / Vote DOWN
Viewing all 1081 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>