MySQL – データベースの複製

mysqldumpによる方法

元のデータベースのダンプファイルを作成。

MySQLで新データベースを作成。またはOSのコマンドラインからmysqladminで新データベースを作成。

ダンプファイルの内容を新データベースに再現

 

さくらサーバー – MySQLのアップグレード

概要

2021年11月、さくらインターネットのカスタマーセンターよりメールがあり、内容はMySQL5.1, 5.5から5.7へのアップグレードを推奨するものだった。5.5のサポート終了やWordPressなどCMSの推奨環境のバージョンアップなどの背景によるもの。

私の環境はさくらレンタルサーバーのスタンダードプラン。後述の経緯もあって少しとまどったが、アップグレードとWordPressの設定を終えた。

状況としては、

  • MySQL5.5と並んで、既にMySQL5.7が稼働していた
  • WordPressで使っていたのはMySQL5.5の方

手順の概要は

  1. 5.7のWordPress用データベースを削除
  2. 5.5のWordPress用データベース以外を削除
  3. アップグレード予約・実行
  4. wp-config.php書き換え

経緯

今回の経緯は以下のとおり。

  1. 2016年、さくらレンタルサーバー(スタンダードプラン)の利用開始
    • このときにMySQL、WordPressも運用開始
  2. その後2021年6月までの間に5.5→5.7へアップグレード
    • アップグレード後も5.5は存続
    • このときにWordPressの設定は変更せず
    • このため、これ以降も投稿等は5.5の方に記録
  3. 2021年11月に5.7へのアップグレード推奨メール
  4. 2021年12月に再度アップグレードとWordPress設定変更完了

確認作業

データベース構成

メールを受けてさくらサーバーのコントロールパネルで確認したところ、以下の2つのデータベース(以下、DB)が存在。

  • mysql57.アカウント名.sakura.ne.jp→MySQL5.7
    (mysql2019.db.sakura.ne.jp)
    →システムの管理で使われるホスト名
  • mysql304.db.sakura.ne.jp→MySQL5.5

5.5のデータベースは以下のとおり。

  • information_schemaはMySQLの管理用
  • _wp1はWordPress用のDB
  • _main_testの2つはテスト用に自分で作成したもので、特に重要ではない

5.7のデータベースは以下のとおり。こちらにもWordPress用の_wp1があり、他のDBも5.5と同じ。

WordPress用データベース

MySQL5.5、5.7ともアカウント名_wp1のテーブル構成は同じで、以下のとおり。

ここで、5.5と5.7でwp_ts01_postsに以下のような違いがあった。

5.5の投稿数。投稿日付は2016年6月16日~2021年12月13日で確認時点の日付まで。

5.7の投稿数。2016年6月16日~2021年2月8日まで。

5.5に対して5.7の投稿数が少なく、途中までの投稿記録で途切れている。

wp-config.phpのMySQLホスト名を確認すると、以下のようにMySQL5.5の方のホスト名になっていて、MySQL5.7運用開始後もWordPressは5.5を使っている。

したがってMySQL5.7運用開始後もWordPressでは利用されず、投稿等は5.5の方に記録されていることになる。このことから5.7のWordPress用データベースを破棄して5.5のデータベースに入れ替え、設定変更することにする。

アップグレード

要点

アップグレードの要点は、さくらインターネットのサイトによると以下のとおり。

  • MySQLの5.5→5.7へのアップグレードは、既に5.7が存在していても可能
  • アップグレード先の5.7に5.5と同じ名前のDBが存在する場合はアップグレード不可
  • アップグレードはDBを指定して実行時間を予約
  • アップグレード後にWordPress等の設定変更が必要

データベースの整理方針

5.5のデータベースをアカウント名_wp1のみ残して削除し、アップグレード先の5.7の同じ名前のDBを削除。

  • 5.5の他のデータベースは自分で作成したテスト用のものだけなので、削除しても問題ない
  • 5.7のアカウント名_wp1は途中までしか投稿記録がなく、不要と判断
  • 双方のinformation_schemaは残置

アップグレードの予約

アップグレードの予約はコントロールパネルで行った。

  • さくらのコントロールパネルで
    • 5.7のアカウント名_wp1を削除
    • 5.5のアカウント名_wp1以外を削除
  • information_schemaはコントロールパネルでは表示されないが5.5、5.7とも残置
  • 12/21の02時にアップグレード予約

アップグレードの確認

翌朝確認したところ、MySQL5.7の方にアカウント名_wp1が作成されており、コンソールで確認したところ昨日までの最新の投稿が記録されていた。

コメントが2件、2時以降についていたが、いずれもspam判定のもので、内容も確認して無視。

WordPressの設定変更

コンソールでも可能だが、さくらインターネットサイトにあるとおりファイルマネージャーで実行。

  1. コントロールパネル左の”Webサイト/データ”→”インストール済み一覧”を選択して、でWordPressのインストール先を確認
  2. “Webサイト/データ”→”ファイルマネージャー”を開いて上記のディレクトリーに移動
  3. wp-config.phpを確認
  4. コンソールでwp-config.phpのコピーを別名で保存(バックアップ用)
  5. ファイルマネージャーでwp-config.phpを右クリック→編集
    • MySQLのホスト名の変更
    • DB名は同じ名前なので変更しない
    • パスワードも変更していないのでそのまま
  6. ファイルマネージャーで”保存”、”閉じる”

WordPressの設定後確認

設定変更後、以下を確認した。

  • WordPressが正常に表示される
  • 新規投稿を書き込み、投稿が反映されること、その内容がMySQLのDBのテーブルに反映されていることを確認
  • 新規投稿を削除し、削除が反映され、テーブルにも削除経緯が反映されることを確認

 

MySQL – 月単位の集計~残高もある場合

例題設定

例題として個人の銀行口座の入出金記録の以下の構造のテーブルを考える。

ここでidは入出金記録の順番に付されていて、変更されないものとする。すなわち、日・月・年などの単位でグルーピングしたとき、idが最も大きいレコードのbalanceがその期の残高となっている。

合計計算のみの場合

月ごとの合計を計算するだけの場合、年月でグルーピングしてSUM関数を使う。

要点は以下の通り。

  • GROUP BY句で、yyyymm形式でフォーマットされた年月単位でグルーピング
  • グループごとにSUM関数で出金額と入金額を合計
  • 年月表示はGROUP BY句のフォーマットと異なってもよい
  • ただしグルーピングされた複数レコード中のどのレコードのdateを使うかを明示するため、MAX関数を使っている。
    • こうしないと(オプション指定なしでは)エラーが発生する

各月の残高も含める場合

月ごとの最終取引の抽出

各月の残高がデータに含まれていて、取引がidの昇順であることがわかっている場合、月ごとにグルーピングされたレコードのうちidが最大のレコードがその月の最後の取引になり、そのbalanceが月の残高になる。

そこで、まず各月の最終取引のidを取得する。

各月最終取引のレコード取得と残高表示

最終取引のidに一致するレコードのみ元のテーブルから取り出すため、INNER JOINを使う。

要点は以下の通り。

  • 最終取引を抽出したクエリーをサブクエリ―として、そのidと等しいレコードのみを元のテーブルから抽出している
  • サブクエリ―にはエイリアスが必須
  • 抽出された最終取引のbalanceを月末残高として表示
  • SELECT句のidは確認用で、表示目的としては不要

月ごとの入出金額を追加

上記のクエリーに、グルーピングされた月ごとの入出金額を追加する。

要点は以下の通り。

  • 月ごとにグルーピングしているサブクエリ―で、SUM関数によって月単位の出金・入金額を計算
  • それらの値をエイリアスを使って元のテーブルで表示

このクエリーのチェックは、前月残高に当月の入出金額を加減して当月残高となることで確認できる。

 

 

MySQL – CSVファイルのインポート

概要

MySQLでCSVファイルをインポートする手順。

要点は以下の通り。

  • CSVファイルはUTF-8(BOMなし)で準備する
  • 改行コードに注意
  • MySQLサーバーとクライアントの両方でローカルのファイル入力の許可が必要
    • クライアントはログイン時のオプションで、サーバー側はグローバル変数で設定する

CSVファイルの準備

CSVファイルを準備し、MySQLで読み込む場所に配置する。CSV作成時の型と文字コードに注意。

型に関する注意点

数字だが001のように表現したい場合は文字列として扱う。

  • Excelの列の形式で適切な型を設定する

日付・時刻形式の空白値に注意。

  • 日付・時刻値の列に空のデータがある場合にMySQLのdate/time/datetimeで読み込むと、NULL値ではなく'0000-00-00''0000-00-00 00:00:00'で埋められてしまう

文字コードの注意点

CSVファイルはUTF-8のBOMなしで準備する。

  • ExcelのシートをCSVファイルとして保存すると、UTF-8(BOMあり)になるので、メモ帳などで開いてBOMなしで保存し直す必要がある

MySQLログイン時のオプション

MySQLにログインするクライアントで、ローカルのファイル入力を許可する必要がある。

ログイン時に以下のオプションのいずれかを付ける

  • --enebale-local-infile
  • --local_infile=1またはon

MySQLログイン後の設定

グローバル変数の確認

SELECTSHOW GLOBAL VARIABLESlocal_infileの値を確認。この値が0/OFFの場合は許可されていない。以下のいずれかで確認できる。

SELECT @@local_infile;

SHOW GLOBAL VARIABLES LIKE 'local_infile';

グローバル変数の設定

SET GLOBALlocal_infileの値を1またはONにセットする。

SET GLOBAL local_infile=1またはON;

インポート実行

テーブルの準備

CSVの各カラムに対応した構造で、空のテーブルを準備しておく。

MySQLのtimestamp型の2038年問題に注意。

LOAD DATAの実行

SQLの例

CSVファイルをテーブルにインポートするSQLの例。

この例では、WindowsのようにCRLFで改行されたファイルを読み込み、区切り文字がカンマのCSVとして、先頭1行を飛ばして読み込む。

FIELDS

TERMINATED BY
CSV保存時に選んだ区切り文字を指定する。1文字である必要はない。デフォルトは'\t'
ENCLOSED BY
フィールドを囲み文字を1文字で指定する。デフォルトは''(空)で、フィールドが区切り文字で囲まれていることを期待しない。OPTIONALLYを付けると文字列のみ囲み文字で囲まれていることを期待する。
ESCAPED BY
エスケープ文字を1文字で指定する。エスケープ文字に続く文字がエスケープシーケンスとして解釈される。デフォルトは'\\''\'一文字を表している。

LINES

STARTING BY
指定したprefix以降のみ行として扱い、それ以外はスキップする。prefixは1文字でなくてもよい。デフォルトは''でprefixを設定していない(常に行頭から読み込む)。
TERMINATED BY
行末の文字列を指定する。デフォルトは'\n'

IGNORE

IGNORE 数値 lines/rowsで、先頭の指定した数の行/列を読み込まない。

 

 

 

MySQL – timestampの2038年問題

timestamp型とdatetime型

MySQLの日付・時刻表現にはtimestamp型とdatetime型がある。

MySQLのtimestamp型は内部表現に整数を用いていて、これが32bit精度の場合には、2038-01-19 03:14:07UTCまでしか表現できない。これは日本のタイムゾーンだと2038-01-19 12:14:07JSTに対応する。

一方datetime型は文字列で日時を表現していて、その範囲は1000年~9999年の間とされている。

timestamp型とdatetime型の有効範囲を比較すると以下のとおり。

timestamp datetime
UTC内部表現で保持 タイムゾーン文字列で保持
1970-01-01 00:00:01UTC~2038-01-19 03:14:07UTC 1000-01-01 00:00:00~9999-12-31 23:59:59

timestamp型の2038年問題

確認環境

timestamp型で上限値より大きな値を登録しようとすると、エラーで登録ができない。このことを確認してみる。環境は以下のとおり。

  • Windows(64bit版)上のVagrant+VirtualBOXで構築したCentOS7
  • MySQLのバージョンは8.0.26

確認用のテーブルは以下のとおり。

上限を超える値の登録は不可

まず上限値一杯の日時を登録してみる。UTCに対して日本のタイムゾーンで表現した2038年1月19日12時14分07秒を登録すると、問題なく登録される。

次に上限値に1秒を加えた日時を登録しようとすると、"Incorrect datetime value"となって登録できない。

timestamp型に対する加算の場合は9999年まで

次に登録された上限一杯の値に対してインターバルを加えていき、問題が生じるまでその境界を探していった。

その結果、9999年12月31日23時59分59秒999999…までは登録可能だが、10000年1月1日0時0分0秒に達することはできず、その場合にはNULLとなることがわかった。

32bitの上限を超えた後も値は保持されているが、その次の上限はdatetime型の上限と一致している。以下の何れかと推測される。

  • 内部的には64bit表現だが、入力時には32bitの上限で、演算時にはdatetime型の上限で抑えている
  • 内部的に32bit表現だが、その上限を超えたときにはdatetime型に内部的に切り替えている

まとめ

  • MySQLのtimestamp型は、2038年の上限値を超える値を登録できない
  • timestamp型に対する加算を行った場合、datetime型と同じ上限まで値を保持できる
    • 加算後にdatetime型の上限を超えた場合の値はNULLになる

64bit整数の場合はtimestampも西暦3000億年弱まで扱い可能だが、一部が64bitシステムでも、データベース、言語、フレームワークの全てが対応していないとまずそうなので、日付時刻を扱う際はdatetime型としておくのが安全なようだ。

 

MySQL – 並べ替え~ORDER BY

準備

以下のテーブルで確認する。

指定方法

ORDER BYで指定したカラムをキーにして並べ替える。デフォルトは昇順でASCを指定したのと同じ。

SELECT カラム群 FROM テーブル ORDER BY カラム [ASC/DESC];

以下の例では、2つ目のnum2で昇順にソートしている。

降順でソートする場合はDESCを指定。以下はnum2カラムの内容で降順にソートしている。

複数カラムでのソート

ORDER BYで複数カラムを指定すると、指定したカラムの順でネストの外側のようにしてソートする。

以下の例では、num1で降順ソートし、その中でnum2で降順ソートしている。

順番を入れ替えた例。まずnum2で降順ソートしてから、その中でnum1でソートしている。

 

MySQL – JOIN – INNER/LEFT/RIGHT/FULL OUTER

準備

以下の2つのテーブルを準備する。

students
学生IDと学生名を保存。
courses
学科IDと学科名を保存。

studentsテーブルの作成とデータ登録のクエリー。

実行結果。

coursesテーブルの作成とデータ登録のクエリー。

実行結果。

JOIN~総当たり

FROM句のテーブルに対して単にJOINでテーブルを指定すると、2つのテーブルの全要素の組み合わせが得られる。

実行結果。

結合テーブル

studentsテーブルとcoursesテーブルの結合テーブルを準備する。各学生の履修学科に関するデータ。

クエリーの実行結果、

INNER JOIN

結合テーブルに基づいて、2つのテーブルの両方に存在するデータを取り出す。studentscoursesの積集合に相当する。INNERは書かなくても内部結合になる。

student_coursesテーブルとstudentsテーブルを結合するクエリーの例。結合の結果、studentsテーブルから学生名を得られる。

実行結果。

3つのテーブルの場合、JOIN ... ON句を並べればよい。以下の例ではcoursesテーブルも結合して、学科名も得ている。

実行結果。student_coursesテーブルは学生IDと学科IDしか持っていないが、studentsテーブル、coursesテーブルと結合することで学生名と学科名を使えるようになる。

LEFT OUTER JOIN

LEFT OUTER JOINは、先に指定されたテーブル(左テーブル)の全レコードに対して、JOINで指定したレコードを対応させる。左テーブルに対応するレコードがないときはNULLになる。LEFT JOINと書いても同じ。

以下のクエリーは、studentsの全学生データに対してstudent_coursesの履修科目データを左結合させる。studentsには登録されているがstudent_coursesにない学生(学科を履修していない学生)についてはNULLになる。

実行結果は以下の通りで、学生IDの伊藤君はstudent_coursesテーブルに存在しないので、course_idNULLになっている。

学科名も欲しい場合は、coursesテーブルを左結合させる。

実行結果。学科を履修していない伊藤君のデータは、course_nameについてもNULLになる。

RIGHT OUTER JOIN

RIGHT JOINは後から指定されたテーブルの全データに対して、先に指定されたテーブルを結合させる。先に指定されたテーブルに対応するレコードがない場合にはNULLになる。RIGHT JOINと書いても同じ。

以下は、student_coursesテーブルのデータにcoursesテーブルのデータを右結合させている。どの学生にも履修されていない学科がある場合はNULLになる。

実行結果。化学はどの学生にも履修されていないのでNULLになっている。

3つ以上のテーブルでは、最も右側のテーブルをRIGHT JOINを連ねる。最後以外のJOINRIGHTがなくても同じ結果になる。

実行結果。履修されていない化学の学生IDと学生名がNULLになっている。

RIGHT JOINのテーブルを入れ替えることで、LEFT JOINで同じ結果を得られる。履修科目に関する上の例をLEFT JOINで書きなおすと以下の通り。

実行結果は同じ。

FULL OUTER JOIN

FULL OUTER JOINは指定されたテーブルの全データを含める。いずれかのデータが存在しない場合はNULLとなる。テーブルのデータ群の和集合に相当する。

MySQLにはFULL JOINが定義されていないので、同等の機能はUNIONで実現する。

  • 2つのテーブルの場合にはLEFT JOINRIGHT JOINの結果をUNION
  • 結合テーブルがある場合には、それぞれのLEFT JOINの結果をUNION
  • UNIONの際に、各SELECTのカラムを同じにしておく

以下は、studentscoursesに対するstudent_coursesLEFT JOINの結果をUNIONで結合している。科目を履修していない学生は学科がNULLとなり、履修されていない科目が学生がNULLとなる。

実行結果。学科を履修していない伊藤君の学科欄はNULLになり、だれにも履修されていない化学は学生欄がNULLになっている。

カラム名の指定によって結果が違う

LEFT OUTER JOINのところで、学生IDと学科IDをstudents.idcouses.idのようにそれぞれのテーブルから指定していた。

一方で、同じIDをstudent_courseのstudent_idcouse_idで指定することもできる。こちらで指定すれば、識別のためのエイリアスを定義しなくていいので便利そうだ。

しかしこれらを使うと、OUTER JOINの際に存在しないIDとして扱われてNULLになってしまう。

上のクエリーを実行した結果が以下で、化学を履修している学生がいないので、student_coursesに該当するデータがなく、student_idcourse_idNULLになっている。

これらを表示させたいときには、LEFT OUTER JOINの例のように、左結合される側(全データが使われる側)のテーブルでカラムを指定するとよい。

 

MySQL – 外部キー制約

外部キーの設定

テーブル構成

学生の履修科目に関する簡単なデータベースを考える。

  • studentsテーブルは学生に関するデータを保存
    • 学生ID(主キー)
    • 学生の名前
  • student_coursesテーブルは学生の履修科目に関するデータを保存
    • 履修科目ID(主キー)
    • 学生ID
    • 科目名

学生テーブル

以下のクエリーでテーブルを作成し、学生データを登録。

登録結果の確認。

履修科目テーブル~外部キーの設定

以下のクエリーで履修科目テーブルを作成。student_idを外部キーとして、studentsテーブルのid(学生ID)を参照している。

外部キーを設定するカラムについて、以下のように定義する。

FOREIGN_KEY (参照するキー) REFERENCES 参照されるテーブル(参照されるキー)

テーブル構造を確認すると、student_idKey欄がMULとなる。このMULについてはぴったりの情報が得られなかったが、以下のようなことらしい。

  • 外部参照するキーにはインデックスが設定される
  • PRIMARY KEYINIQUEが設定されていないインデックスは同じ値を取り得る
  • なのでmultiple keyMULと表示される

外部キー制約の確認

正常な登録

新しいデータをstudent_coursesテーブルに登録する。student_idとして登録済みのstudents.id = 1の学生IDを指定する。

正常に登録されて、students_coursesテーブルは以下のようになる。

存在しないデータの登録はエラー

student_idに、studentsテーブルでは登録されていない値を指定する。

外部キー制約でエラーとなる。

参照されている親のデータは消せない

student_coursesテーブルのデータから参照されている、studentsテーブルのid=1のデータを削除しようとするとエラーになる。

参照されていない親のデータは削除できる

student_coursesのデータから参照されていない、studentsテーブルのid=2のデータは削除できる。

不整合時の挙動~RESTRICTやCASCADEなど

FOREIGN KEY指定時に、参照先のデータの削除時と変更時の挙動を指定できる。

ON DELETE 挙動指定 ON UPDATE 挙動指定

挙動指定には以下の4つがあり、指定しない場合はRESTRICTとなる。

RESTRICT
DELETE, UPDATEともエラーになる。
CASCADE
DELETEでは参照元のデータも削除され、UPDATEでは参照先の変更が参照元のデータに反映される。
SET NULL
DELETE, UPDATEともNULLに置き換わる。
NO ACTION
RESTRICTと同じ挙動。

外部キー制約をCASCADEに変更する

CREATE TABLEの確認

外部キー制約設定後のテーブル定義を以下のコマンドで確認してみる。

SHOW CREATE TABLE テーブル名\G;

  • 最初に実行したクエリーの内容に即している
  • CONSTRAINT `外部制約名`が追加されている

挙動の変更

外部キー制約の挙動をRESTRICTからCASCADEに変更する。外部キーの操作はALTER TABLEで行うが、一度に変更できないので一旦外部キーを削除し、新たな条件で外部キーを追加する。

外部キーの削除

外部キー削除のコマンドは以下の通り。

ALTER TABLE 参照元テーブル DROP FOREIGN KEY 外部制約名;

外部制約名は、先のSHOW CREATE TABLEで確認した内容で指定する。

外部キー削除後もインデックスは残っている。

外部キー削除後のCREATE TABLEを確認すると、student_idにインデックスが設定されている。

外部キーの追加

以下のクエリーで削除・更新時の挙動を指定して外部キーを追加。

クエリー実行。

CREATE TABLE確認。

CASCADEの挙動

ON UPDATE CASCADE

参照先、親テーブルstudentsのデータのid=39に変更する。RESTRICTではエラーになったが、CASCADEの場合は変更が通る。

student_coursesstudent_idも変更されている。

ON DELETE CASCADE

参照先、親テーブルのid=9のレコードを削除する。RESTRICTではエラーになったが、CASCADEの場合は削除される。

student_coursesstudent_id=9のレコードも削除されている。

外部キー制約の追加・削除

外部キーの追加は、ALTER TABLE ... ADD FOREIGIN KEYで行う。被参照キーにインデックスが設定されている必要がある。

外部キーの削除はALTER TABLE ... DROP FOREIGN KEYで行う。制約名はSHOW CREATE TABLEで確認できる。

 

MySQL – テーブルの作成クエリーを表示する

作成済みのテーブルの作成クエリーを表示させることができる。

SHOW CREATE TABLE テーブル名\G

列は1列だが幅が広いので\Gで縦表示している。

 

MySQL – レコード登録・更新のタイムスタンプ

レコードを登録・更新した場合のタイムスタンプを保存するためのテーブル設定。

  • 日時の型はDATETIMEで設定
  • 作成時の初期値を作成時点の日時とするにはDEFAULTを指定
    • DEFAULT CURRENT_TIMESTAMP
  • 更新時に更新時点の日時で書き換える場合はON UPDATEを指定
    • ON UPDATE CURRENT_TIMESTAMP

テーブル作成のSQL例。

SQL実行結果。

テストデータ登録。

2つ目のデータを変更して、更新日時が変更されていることを確認。