ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

PostgreSQLはトランザクション内で制約を一旦外して戻すことができる

はじめに

こんな tweet を拝見しました。

遅延制約(DEFERRABLE)を使えば制約を一旦外す必要がないのですが、技術的な可否が気になり、調べた結果をまとめます。

ドキュメント調査

PostgreSQL 11 のマニュアルには以下の記載があります。

DDLコマンドの中には、現在はTRUNCATEとテーブルを書き換える形のALTER TABLEだけですが、MVCCセーフでないものがあります。 これは、DDLコマンドをコミットする前に取得したスナップショットを使っていると、切り詰めまたは書き換えのコミット後に、同時実行トランザクションに対してテーブルが空に見えることを意味しています。

13.5. 警告

この具体例は以下の記事を参照ください。
PostgreSQL のトランザクション & MVCC & スナップショットの仕組み

マニュアルには『テーブルを書き換える形のALTER TABLE』とあり、『制約を書き換えるだけの ALTER TABLE』は対象外に思えますが、動作検証で試してみます。『テーブルが空に見える』ということなので、空に見えなければOKとします。

動作検証

PostgreSQL 11.4 で確認しました。

検証手順は遅延制約について解説した記事での手順をベースにしています。
PostgreSQLで遅延制約を使って一意制約カラムを一括更新する - Qiita

これに対して制約のDROP/ADDを追加するとともに、MVCCセーフであるか(テーブルが空に見えないか)?を検証する手順としました。

手順では2つのトランザクションを用いており、TX1/TX2で記載しています。

-- TX1

=# CREATE TABLE posts (id integer, CONSTRAINT posts_pk PRIMARY KEY (id));

=# INSERT INTO posts VALUES(1);
=# INSERT INTO posts VALUES(2);
=# INSERT INTO posts VALUES(3);

=# BEGIN ISOLATION LEVEL SERIALIZABLE;

=# SELECT txid_current();
-- TX2

=# BEGIN ISOLATION LEVEL SERIALIZABLE;

=# SELECT txid_current();

=# ALTER TABLE posts DROP CONSTRAINT posts_pk;

=# UPDATE posts SET id = id + 1;

=# ALTER TABLE posts ADD CONSTRAINT posts_pk PRIMARY KEY (id);

=# COMMIT;

=# SELECT * FROM posts ORDER BY posts;
 id
----
  2
  3
  4
(3 rows)
-- TX1

=# SELECT * FROM posts ORDER BY posts;
 id
----
  1
  2
  3
(3 rows)

PostgreSQLトランザクション内で「一旦制約を外す ⇒ 操作する ⇒ 制約を戻す」ことが可能であるとわかりました。
また、SELECT結果からトランザクションの順序 (TX1 → TX2) も想定通りであることが確認できました。

注意点

動作検証のTX1の最後の状態でpsqlの\dコマンド(オブジェクトの概要表示)を対象のテーブルに使うとエラーになります。

=# \d posts
ERROR:  cache lookup failed for index 0
STATEMENT:  SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),
    pg_catalog.pg_get_constraintdef(con.oid, true), contype, condeferrable, condeferred, i.indisreplident, c2.reltablespace
  FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
    LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))
  WHERE c.oid = '16537' AND c.oid = i.indrelid AND i.indexrelid = c2.oid
  ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname;

今回の記事の目的からは若干外れるので深追いしていませんが、PRIMARY KEY 制約を DROP/ADD したことで index が再生成され、トランザクション分離レベルに従わない関数によってエラーを起こしたのではないかとみています。
PostgreSQLの一部の関数はトランザクション分離レベルに従わない - ぱと隊長日誌

まとめ

PostgreSQLは制約の操作(ADD/DROP)もトランザクションに含めることができます。
ただし、全てのDDLコマンドや関数がMVCCセーフとは限らない点に注意が必要です。
実務では制約を操作するのではなく、遅延制約での実現を検討するのが良いでしょう。

Excelの動作が重たいのは「折り返して全体を表示する」設定が原因かも

概要

Excelが重たい理由は様々指摘されていますが、何をしても改善しないときは「折り返して全体を表示する」設定がONになっているからかもしれません。本エントリではこの事象についての検証結果をまとめます。

テスト環境

  • Excel 2013 (15.0.5189.1000)

他のバージョンでも同様の事象が再現することを確認しています。

  • CPU AMD A10-5800K 3.8GHz
  • メモリ 24.0GB

CPUこそ古いですが、メモリはそれなりに積んでいます。

テストファイル

「折り返しOFF」と「折り返しON」の2バージョンを作成します。

  1. Excelファイルを「空白のブック」で新規作成する。
  2. (「折り返しON」のみ)A-K列の「折り返して全体を表示する」設定をONにする。
  3. A-K列、1-1048576行の全セルを 'a' (1文字)で埋める。
  4. Excelブック (*.xlsx) 形式で保存する。

テスト項目・結果

(1) ファイルサイズ
ファイルのプロパティから「サイズ」の値を確認する。

(2) オープン
ファイルをダブルクリックしてから表が表示されるまでの時間を計測する。
3~5回目の計測値を平均して結果とする。

(3) セル移動
ファイルを開いた直後にctrl+↓キーで最下行に移動する時間を計測する。
3回の計測値を平均して結果とする。

(4) 並べ替え
ファイルを開いた直後に並べ替え(昇順)を実行し、完了する時間を計測する。
3回の計測値を平均して結果とする。

折り返し設定 ファイルサイズ オープン セル移動 並べ替え
折り返しOFF 33.4MB 20秒 0秒 3秒
折り返しON 34.0MB 22秒 39秒 40秒

まとめ

折り返しOFFの場合に比べ、折り返しONの場合には処理時間が大幅に延びています。単純に同じ動作を繰り返す場合は処理速度の改善するケースがありましたが、実際の利用シーンでは様々な操作を行う中でたびたび待たされる状況が発生しました。

Excelのバージョンによってはファイルを開いて操作可能となるまでに長い時間を待たされるケースもあるようです。
別の環境でファイルの種類を「Excel 97-2003 ブック (*.xls)」にしても同様の事象が再現することを確認しました。
セルに設定する文字列が1文字でも事象を確認できており、実際の表示の折り返し有無とは無関係でした。

現在のところ、解決策は見つかっていません。回避策として、不要な「折り返して全体を表示する」設定はOFFにすることをお勧めします。

集合に含まれない組み合わせをクエリで抽出する

今回のクエリの説明例として、データベーススペシャリスト平成27年春期 午前Ⅱ 問11 の問題・解答を引用します。

<問題>
庭に訪れた野鳥の数を記録する"観測"表がある。観測のたびに通番を振り、鳥名と観測数を記録している。AVG関数を用いて鳥名別に野鳥の観測数の平均値を得るために、一度でも訪れた野鳥については、観測されなかったときの観測数を0とするデータを明示的に挿入する。SQL文のaに入る字句はどれか。ここで、通番は初回を1として、観測のタイミングごとにカウントアップされる。

<解答>

CREATE TABLE 観測 (
  通番 INTEGER,
  鳥名 CHAR(20),
  観測数 INTEGER,
PRIMARY KEY(通番, 鳥名))

INSERT INTO 観測
SELECT DISTINCT obs1.通番, obs2.鳥名, 0
  FROM 観測 AS obs1, 観測 AS obs2
 WHERE NOT EXISTS (
   SELECT * FROM 観測 AS obs3
    WHERE obs1.通番 = obs3.通番
      AND obs2.鳥名 = obs3.鳥名)

このクエリの目的は試験問題の解説記事に記載されています。

"観測"表に存在しないすべての(通番,鳥名)の組み合わせについて、観測数を0にしてレコードを新規追加する

平成27年春期問11 SQL文のaに入る字句はどれか|データベーススペシャリスト.com

記事ではクエリについてかなり丁寧に説明されていますが、それでもピンときにくい点があるかと思います。そこで、このクエリの分かりにくい点を解説し、集合に含まれない組み合わせをクエリで抽出する方法を説明します。

<観測表>

通番 鳥名 観測数
1 ヒバリ 1
1 メジロ 2
2 スズメ 1

SQLの実行順序は以下のとおり決まっています。
FROM → WHERE → GROUP BY → HAVING → SELECT
この順に沿ってクエリを読み解きます。

(1) FROM

FROM 観測 AS obs1, 観測 AS obs2

FROM句に複数のテーブル名が指定されており、クロス結合が行われます。その結果、以下のワークテーブルが作られます。

obs1.通番 obs1.鳥名 obs1.観測数 obs2.通番 obs2.鳥名 obs2.観測数
1 ヒバリ 1 1 ヒバリ 1
1 ヒバリ 1 1 メジロ 2
1 ヒバリ 1 2 スズメ 1
1 メジロ 2 1 ヒバリ 1
1 メジロ 2 1 メジロ 2
1 メジロ 2 2 スズメ 1
2 スズメ 1 1 ヒバリ 1
2 スズメ 1 1 メジロ 2
2 スズメ 1 2 スズメ 1

クエリで使われるのは "obs1.通番", "obs2.鳥名" のみなので、説明の為に簡略化します。本来はSELECT句で評価(射影)されることに注意してください。

obs1.通番 obs2.鳥名
1 ヒバリ
1 メジロ
1 スズメ
1 ヒバリ
1 メジロ
1 スズメ
2 ヒバリ
2 メジロ
2 スズメ

ここでポイントになるのが、obs1, obs2 という個別の表が1つのワークテーブルになり、元のテーブル名が "obs1.通番" のようにワークテーブルの列名の一部になったということです。このようにイメージしたほうが以降の処理を考えやすくなります。

ここで得られた結果は (通番, 鳥名) の全ての組み合わせとなっています。

(2) WHERE

WHERE NOT EXISTS (
  SELECT * FROM 観測 AS obs3
   WHERE obs1.通番 = obs3.通番
     AND obs2.鳥名 = obs3.鳥名)

WHERE句に obs1, obs2, obs3 と複数のテーブル名が登場して混乱しそうになりますが、obs1, obs2 はワークテーブルへと変化したことを思い出してください。ワークテーブルが work という名前とすれば、以下の形に変わります。

WHERE NOT EXISTS (
  SELECT * FROM 観測 AS obs3
   WHERE work.通番 = obs3.通番
     AND work.鳥名 = obs3.鳥名)

これは単純な相関サブクエリの形をしていることが分かります。

この相関サブクエリの意味を紐解きます。

WHERE句の評価は1行ずつ行われます。ここではワークテーブルの (1, スズメ) を評価しているとします。これを相関サブクエリに値として代入します。

WHERE NOT EXISTS (
  SELECT * FROM 観測 AS obs3
   WHERE 1 = obs3.通番
     AND 'スズメ' = obs3.鳥名)

このWHERE句は観測表(obs3)に (1, スズメ) が存在しないときに成立します。これが問題文の「観測されなかったとき」に対応します。

つまり、ワークテーブルの (通番, 鳥名) の全ての組み合わせから観測表に存在しない組み合わせを選択しています。

obs1.通番 obs2.鳥名
1 スズメ
1 スズメ
2 ヒバリ
2 メジロ

(3) SELECT

SELECT DISTINCT obs1.通番, obs2.鳥名, 0

このSELECT句より以下の結果が得られます。

obs1.通番 obs2.鳥名 -
1 スズメ 0
2 ヒバリ 0
2 メジロ 0


今回のクエリは「集合に含まれない組み合わせ」を抽出するため、全ての組み合わせ(FROM句)から既に存在する組み合わせを引き算する(WHERE句)ことで実現していました。

<参考文献>

SQLの実行順序やFROM句をクロス結合として考えることの参考にしました。