AWS SDK javaのパッケージとv1, v2の共存について

死ぬほど今更ですがAWS SDK javaについて自分のほしい情報を早引きしたかったのでまとめ。

v1とv2のgroupid・パッケージの違い

v1,v2はgroupidやパッケージが異なっているがどちらがv2なのかそこまで自明ではないので以下にまとめる。

以下のマッピングになっている。

version groupid s3のときの指定例 全部一括で依存する時の指定
v1 com.amazonaws com.amazonaws.aws-java-sdk-s3 com.amazonaws.aws-java-sdk
v2 software.amazon.awssdk software.amazon.awssdk.s3 software.amazon.awssdk.aws-sdk-java

BOMについて

AWS SDKは内部的には別々のライブラリになっており、S3, kinesisなど必要なサービスに関するライブラリのみに依存出来る。 これらのライブラリはパッケージが一部被っている関係で、バラバラのバージョンに依存するとバイナリ互換に関する問題が発生しうる。

BOM (bills of materials)はこの問題に対処するもので、要は一貫性が取れてるライブラリのバージョン番号一覧のようなもの。 依存にBOMを加えつつ各サービスごとのライブラリのバージョン指定を省略することで、いい感じに互換性のあるバージョンを解決してくれるようになっている。

設定例

Apache Maven プロジェクトの設定 - AWS SDK for Java を参考にすると以下のようになる。

BOM指定

<project>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>bom</artifactId>
        <version>2.X.X</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

依存指定

    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>dynamodb</artifactId> <- version指定不要
    </dependency>

bomを使いたくないとき

bomを使いたくない場合は手動で互換性のあるバージョンを指定することになります。

とはいえv1の例v2の例 を見ると基本的に全部一貫して同じバージョンを使っておけば良いのであんまり苦にはならないはず。(一部v1のswfみたいな開発が止まってるやつは古いバージョンになっていたりするっぽい)

心配なときは、以下から最新バージョンを見つけて各サービスの利用すべきバージョンをチェックしてみよう。

V1, V2同時利用について

SDK for Java 1.x と 2.x の並行使用 - AWS SDK for Java によるとv1, v2を一つのアプリケーションで同時に依存しても良さそう。

特に工夫も必要なくサービスごとに別々にv1, v2の依存を追加すればいいっぽい。(パッケージ違うから割と安心してできるはず。)

FOLIOでのrefined活用法 - ドメインを型で形式知化し、契約を型で表すことで認知漏れを防ぐ

FOLIOアドベントカレンダー2021 12日目です。

FOLIOではrefinedというライブラリを採用しているので、活用事例とその目的について考えていこうと思います。

お品書きは以下です。

  • refinedとは
  • ドメイン知識の形式知
  • 契約を型で表すことによる認知漏れリスクへの対策
  • プラットフォームとしてのrefined
  • まとめ

refinedとは

refinedScalaでrefinement typeを表現するためのライブラリで、Haskell同名ライブラリScala版として開発されました。

refinement type(篩型, ふるい型) はざっくりいうと、「値が0以上である」とか「長さが1以上である」といった制約を型で表現し、静的に保証するための概念で、1991年にFreemanらによってRefinement Types for MLで提案されました。

利用方法

refinedでは制約を val a: Int Refined Positive のように、元の型に対して特定の述語を追加する形で表現します。

この制約付きの型に対応する値を生成するためには二種類の方法があります。

1つ目の方法はマクロを使った方法です。

import eu.timepit.refined.auto._

val a: Int Refined Positive = 1
val b: Int Refined Positive = -1 // コンパイル時に値が検証されるためエラー

この方式は便利ですが当然ながらコンパイル時に値が特定可能な場合(数値をリテラルで初期化する場合等)でのみ利用が可能です。

2つ目の方法はランタイムに値を検証するメソッドを呼ぶ方法です。制約を満たさない場合はEitherや例外の形でエラーが返されます。

val i = 10
val a: Either[String, Int Refined Positive] = refineV[Positive](i)

 なお Int Refined Positive の場合は下記のように PosInt.fromPosInt.unsafeFrom のようなメソッドを使うことも可能です。

val i = 10
val a: Either[String, PosInt] = PosInt.from(i)

val j = -10
val b: PosInt = PosInt.unsafeFrom(j) // 制約を満たしていないため実行時例外

余談ですが PosIntのような型はRefinedTypeOps を継承すれば作成可能です。FOLIOでは正の値(>0)を表現する PosInt の他に、非負の値(>= 0)を表現する NonNegInt 、これらの BigDecimal 版である PosBigDecimal , NonNegBigDecimal などをよく利用しています。

その他の使い方については refinedのREADMEさらなる型安全性を求めて ~ Refinement TypeをScalaで実現する ~ - Visional Engineering Blog にとても分かりやすい解説があります。

ドメイン知識の形式知

FOLIOの中でも筆者が所属するチームは口座残高や株式売買に関わるシステムを担当することが多く、現金残高・証券残高・証券残高評価額・平均取得単価・約定単価・約定数量・約定金額・実現損益・実現損益前日比・含み損益・含み損益前日比など一目には違いが分かりにくい”お金や数量を表現する値”がたくさん存在しています。

複雑なドメインと戦いながらロボアド基盤をリプレースするときにした工夫 でも(20ページ目などで)発表したのですが、これらの概念を全体的に把握するだけでも一定のハードルがあります。

また、これらの概念を理解したとしても、それらが実際に取る値を理解するという次のハードルがあります。 証券残高評価額が非負の値を取るとしても、証券残高評価額の前日比は正負両方の値を取ります。現金残高は一見すると非負の値を取りそうですが業務フローによってはシステム的に負の残高を許容することもあります。

こういった知識を持たないまま開発を行うと、想定していない計算が行われたり重要なエッジケースがテストされないまま本番稼働を迎えるリスクがあります。

refinedを使うと以下のように暗黙的に持っていた不変条件に関する知識を宣言的に明示することができます。

case class OrderAmount(value: PosBigDecimal) // この場合は注文金額は0にはならない
case class OrderAmount(value: NonNegBigDecimal) // この場合は注文金額は0になりうる

上記のようなクラスを新しく作成する場合にドメインに関する知識を宣言的に表明することで、ドメイン層のクラスをチームで共有可能な知識置き場にすることができます。

契約を型で表すことによる認知漏れリスクへの対策

前述したコードの書き方は不変条件の表明を通した、いわゆる契約プログラミングの実践の一例と言えると思います。

Scalaには契約プログラミング用のメソッドとしてrequireやensuringなどが用意されている ので、これらの手法とrefinedを使った手法では何が違うのか考えてみたいと思います。

契約を型で表すことのメリット

refinedを使って契約を型で表現する手法では、fromやunsafeFromのような契約の検査を行うコードを明示的に書く必要があります。これはプライマリコンストラクタ等に require(value > 0) を記述するような手法と異なる点です。

後者の方式でも契約違反が発生した場合に後続の処理を停止させることは可能です。一方でこの処理が契約違反になる可能性があるかどうかについてはプログラマが注意を払う必要があります。

case class Fee(value: BigDecimal) {
  require(value > 0)
}

case class Cash(value: BigDecimal) {
  require(value >= 0)

  def calc(f: Fee): Cash =  Cash(value - f.value) // これは契約を満たさない可能性があるが、プログラマがそれを認識しているとは限らない
}

例のcalcメソッドのような処理を書く際に不変条件を意識できなかったとすると、これはエッジケースの認知漏れにつながる可能性があります。 もしかすると考慮漏れが発生しており設計・仕様へのフィードバックが必要な状態かもしれませんし、あるいは仕様の(暗黙の)範囲内で(設計書には書いてないけど)エラーハンドリングを行う必要があるかもしれません。

脱線: 負にならないとされているものが負になったらどうしようもないしエラーをハンドリングする必要があるのか?という疑問がありそうなので少しだけ背景の解説を入れておきます。例えば全口座を処理するバッチではエラーがあった場合に即座にプロセスを終了させると全口座の処理がストップしてしまいます。こうなると復旧が長時間化し、障害規模も単一口座から全口座へ広がってしまいます。これを避けるため該当口座の処理をskipし次口座の処理を開始、全口座処理終了後にskipされた口座についてエラーを報知するといったエラーハンドリングを行う場合があります。*1

バグや仕様の不整合でしか起こり得ないケースなのか、データによってはあり得るケースなのかによって対応は分かれると思いますが、どちらにせよそういうエッジケースがあるということを認知しなければ対応は始まりません。ここを認知しなければエラーハンドリングや適切なテストも行われず、仕様の考慮漏れを起こしたまま本番環境にデプロイされてしまう可能性も出てきてしまいます。

一方でrefinedを使って契約を型で表現する手法では、ある値がNonNegBigDecimalなのかPosBigDecimalなのかを何らかの検証を通して保証する必要があります。

case class Fee(value: PosBigDecimal)

case class Cash(value: NonNegBigDecimal) {

  def calc(c: Cash, f: Fee): Cash =  {
    // NonNegかどうかの検査をしないとCashのインスタンスを作れない。
   // プログラマは負になる場合が仕様の範囲でありえるかどうか考え、エラーハンドルを行うか異常終了させるか検討する必要が出てくる。
    // 下記コードでは契約違反の場合は例外になる。
    val nonNeg = NonNegBigDecimal.unsafeFrom(c.value.value - f.value.value)
    Cash(nonNeg)
  }
}

このようなコードスタイルの場合、仕様上ありえない(バグ以外では成功する)のか、データとしては不正なものの厳密にはあり得ること(バグではなく仕様の範囲内)なのかをプログラマが考えるきっかけがうまれます。

計算結果がマイナスになりうるといったエッジケースを認知することさえできれば、エラーハンドリング用コードの追加は容易ですし、異常終了でよいとしても復旧用マニュアルの整備を含めた事前のリスク対応索の整備が可能です。(検討を行った結果、仕様上ありえないことが分かったならその理由をコメントに書いてunsafeFromを呼び出してもよいでしょう。) いずれにせよどういう事象がありえるのか?の分析を行う機会が得られれば何らかの対応を取ることが可能になります。

つまり、このような手法を使うことで

  • エッジケースがそもそもあるんじゃないか?といった点に気づくきっかけを得られる
  • (上記を通して)設計段階でも開発段階でもテスト段階でも気づかず、エッジケースが本番で発生し障害になるというリスクを低減できる

といった効果が得られると期待しています。

もちろんrequire等を使った契約プログラミングの「誤った計算が行われた状態で処理が進んでしまうことがない」といった利点もそのまま享受できます。

nullとOptionの関係に近いかもしれない

上記で例に上げたエッジケース(マイナスになりうる)などは設計段階でもたいていは拾われますし、熟練のプログラマなら「ドメインオブジェクトなんだから不変条件あるし、それのチェックは当然」 と当たり前のように気づけるかもしれません。なにより境界値分析などでテストケースに上がる部分でもあるので、上述のようなリスクが本番環境で顕在化することは実際にはあまり多くはないのかもしれません。

一方で、「必ず目を配りながらコードを書く必要がある」、「うっかり考慮を忘れると本番でエラーが発生しバッチ処理が異常終了する」といったことはプログラマとしては可能な限り避けたいものです。

これらと同じような問題を避ける手法として「nullをOptionで表現する」手法があると考えています。nullも設計段階で拾うべきですし熟練のプログラマならNullPointerExceptionなんて出さないかもしれませんし、テストケースで確実にテストされるべきですが、実際にはNullPointerExceptionで涙を流した人は多いのではないでしょうか。

値がないことをnullの代わりにOptionで表現することで予期せぬ場所で例外が出るのを防ぐことができたり、nullになりうる箇所が制限されることでデバッグや障害時の原因調査を迅速に行うことが可能です。同様のことは事前条件として value != null を書くことでも実現できますが、「値はnullではない」・「値はnullになりうる」という契約をOptionのような型で表現することで(mapとかgetOrElseみたいな面倒なコードと引き換えに)安全なコードを手に入れることができますし、「ここ値ない可能性ありますけどどうしましょう?」といった相談も自然にできるようになると思います。

nullをOptionにしてもテストシナリオの分析やテストが不要になるわけではないのと同様に、refinedで契約を型として表してもそれらが不要になるわけではありませんが上記と同じようなメリットを得られるのではないかと考えています。

余談: 顕在的契約計算あるいはHybrid Type Checkingについて

ちなみにrefinement typeを利用して契約計算を行う手法を顕在的契約計算やHybrid Type Checkingというようです。

後者の論文では動的な契約の検査の欠点として

dynamic checking provides only limited coverage – specifications are only checked on data values and code paths of actual executions. Thus, dynamic checking often results in incomplete and late (possibly post-deployment) detection of defects.

ということが挙げられています。動的な契約の検査は実行時にしか行われず、また与えられた値に対してのみの検査となるため、テストは通っていてもデプロイした後に契約を満たさない値が与えられてエラーが出る可能性があるということのようです。対照的に静的な型検査では網羅的な検査が事前に行われるためこのようなことは通常起こりません。

すべてを静的型で検査するぞ!うおー型安全!!!と行きたいところですが、残念ながら表現力豊かな型システムでは、ある型がある述語を満足するかを静的に検査するのが難しい(決定可能でなくなる)ケースがあったりリーズナブルな時間内に型検査を終わらせることが難しくなるようです。

しかし、明らかに型が合っているプログラムについては短時間で型検査を終わらせることは可能ですし、逆に明らかに型が合わないプログラムをエラーと判定することも短時間で可能そうです。 そこでHybridなアプローチとして静的に検証可能な範囲では静的な検査、実行時に行う必要があるような検査では動的な検査(契約違反なら例外)を挿入することで両者の欠点を補いたいというモチベーションでHybrid Type Checkingが提案されていそうです。

このようなハイブリッドなアプローチを行うことで

  • 静的に検査可能な部分は静的に検査
    • 型検査を通した網羅性の担保
    • 実行時の検査実行によるオーバーヘッド削減
  • 動的な検査の強制

といったメリットを享受することが可能そうです。

refinedを使ったコードの場合

  • 静的にチェック可能な関係は静的にチェックされる
    • 例えば NonNegIntPosInt は代入可能だが逆は不可といった単純なサブタイピング関係はコンパイル時にチェックされる
  • それ以外のものは動的なチェックとなるが検査は強制される
    • 例えばNonNegIntのfromやunsafeFromのどちらでも実行時の検査が行われる

といった特徴を持っていますので、これも一種の顕在的契約計算の理論に基づいたプログラミング方法といえるかもしれません。(顕在的契約プログラミングみたいな単語を連想しましたがググった感じそういう用語はなさそうでした。)

ただし実際にコードを書いてみると静的にチェック可能な部分は思ったより少ないかもしれません。(例えばPosInt + PosInt = PosInt であるということすら静的には保証できません。 参考: refinedでPosInt + PosInt を PosIntにしたい - だいたいよくわからないブログ

こういったものは研究が進んで実装的にこなれてきて業務的に使いやすい物が産まれるまで時間がかかりますし、現段階では無闇に静的にチェックする範囲を広げると辛いかもしれませんが、時代が進んでいくとこういった研究の恩恵も享受できるかもしれません。

プラットフォームとしてのrefined

ここまで、不変条件を宣言的に型として表明しそれを保証する利点を述べてきました。

しかし、refinedを使わずともファクトリーメソッドで必ずチェックするようにすればvalue object自体が契約を表明する型になるんでないの?という声もありそうです。 実際、ファクトリーメソッドでEitherを返すようにすればCashのインスタンスを作ろうとした段階でチェックが必要なことに気がつけます、

一応

  • refinedであれば静的な保証ですむ場合がある(例えばNonNegBigDecimalのvalueを別のクラスに詰め替える場合は動的な検査は不要)
  • より宣言的であり、ファクトリーメソッドの実装を見に行く必要がない
  • 値域を表明するための方法(Pos等以外にもGreater[5]やRegex["[0-9]+"]など)が確立されており、実装者による表現方法のブレが少ない

といったメリットがあると思いますし、refinedを使ってもファクトリーメソッドが使えなくなるわけではないのでapplyを直接呼び出すのが気になる人はrefinedとファクトリーメソッドの併用することも可能です。

とはいえ、refinedを使わずに自前でfrom, unsafeFrom相当のファクトリーメソッドを作成することはもちろん可能ですし十分実用可能な範囲だと思います。

じゃあrefined使う意味ないよねってことになると寂しいので、こういった保証にrefinedを使う副次的な利点としてrefined自体がScalaライブラリのエコシステム内で既に受け入れられているという点も挙げておきます。

例えば

といった有名どころのライブラリでrefinedを使ったクラスのサポートが行われています。

そのためjsonのデシリアライズ時やdbからのselect時にフィールドを一つずつ検査するコード(あるいは自前のファクトリーメソッドを自動でいい感じに呼んでくれる仕組み)を自分で書かずとも自動で検証を行ってくれます。

こういった点は自前でファクトリーメソッドを作る方法に比べたときの既存のライブラリを使うメリットと言えそうです。

コーディングスタイルによってはファクトリーメソッドで検証するから上記は不要だ!という人もいるかもしれませんが、ある程度マッピングを簡略化できるので便利に使える人もいるかも知れません。

まとめ

この記事ではScalaのrefinedというライブラリについて、およびFOLIOでの実用例について解説しました。

主なメリットは3つです。

  • ドメイン知識の形式知
    • 似たような値がたくさんあるドメインに対する知識を型で表現するだけでなく、その不変条件も型で表現することでドメイン上の暗黙知形式知に転換し、チームで共有可能にする。
  • 契約を型で表すことによる認知漏れリスクへの対策
    • 契約プログラミングにおけるランタイムの安全性に加え、エラーハンドリング自体がそもそも不足してしまうといったエッジケースの「認知漏れ」に起因するリスクを低減する。
  • プラットフォームとしてのrefinedの利便性
    • 様々なライブラリと連携した一括検査によって生産性を向上させる。

だいぶ長々と書きましたが、型で表現されてたら便利かもね!程度の話でした。

*1:もちろん明らかに何かおかしい場合は止めたほうがいいので全部そうするとは限りません。

MySQLでのVARCHARサイズ変更を行うALTER TABLEについて

これは何

MySQLにはVARCHARの文字数を拡張するDDLをオンラインで行うための最適化が存在する。

一方でこの最適化は内部のバイナリ表現に依存した限定的な最適化であり、意図せずオフラインDDLとして実行される可能性がある。

本ページではこの最適化の仕組みと安全にサイズ変更を行うための対策について解説する。

結論

結論1

MySQL 5.6以前では文字数が何文字だろうとVARCHARサイズ変更に関する最適化は行われないのでオフラインDDLになる。

そのため変更したい場合は停止メンテ等を行うかpt-online-schema-change等の利用を検討すること。

結論2

MySQL 5.7以降ではVARCHARサイズとCHARSETに依存してオンラインで拡張可能なサイズが変わるので、将来の拡張を考えた上でサイズを検討しよう。

結論3

以下のように ALGORITHM=INPLACE, LOCK=NONEDDL末尾につけることで、DDLをオンラインで実行できるか検証できる。(algorithm, lockについては MySQLでカラム追加などのalter table中にクエリがブロックされるかなどについてのメモ - だいたいよくわからないブログ を参照)

エラーがでたらオフラインDDLになってしまうため停止メンテ等を行うかpt-online-schema-changeの利用を検討すること。

ALTER TABLE table_name CHANGE COLUMN colum_name colum_name VARCHAR(63), ALGORITHM=INPLACE, LOCK=NONE;

背景

MySQL :: MySQL 5.7 Reference Manual :: 14.13.1 Online DDL Operations によるとMySQL 5.7以降では

Extending VARCHAR column sizeIn Place=yes, Rebuilds Table=no, Permits Concurrent DML=yes, Only Modifies Metadata=yes となっている。

つまりread/writeはブロックされず、DDL自体も高速に終了することが期待できる。

しかし The number of length bytes required by a VARCHAR column must remain the same. といった記載もあり、対象範囲が自明ではない。以下ではこれらの挙動について解説する

前提: VARCHARについて

VARCHAR は 1 バイトまたは 2 バイトのlength prefixが付いたデータとして格納される。length prefixは値に含まれるバイト数を示す。 ( つまり${byte数prefix}${実際の文字列} という表現になる)

MySQL :: MySQL 5.7 Reference Manual :: 11.3.2 The CHAR and VARCHAR Types

このとき文字列のバイト数に依存して、length prefixのbyte数も以下のように変化する。

  • 1~255byteの場合は1byte (00~FF)
  • 256byte以上の場合は2byteで表現される。(0100~FFFF)
  • MySQLの1行のサイズ上限があるため65535byteより大きなVARCHARはそもそも定義することができないのでlength prefixが3byteになることはない

また DDLで指定する VARCHAR(255)などの括弧内の数字はbyte数ではなく文字数なので、VARCHAR(255)のlength prefixが1byteであるかどうかは文字コードに依存する。(latin1なら1文字1byteなのでVARCHART(255)のlength prefixは1byte、utf8mb4なら1文字4byteなのでVARCHART(255)のlength prefixは2byteになる)

確認内容

MySQL 5.6

MySQL 5.6のドキュメントには Extending VARCHAR column size についての記載はない。

see: MySQL :: MySQL 8.0 Reference Manual :: 17.12.1 Online DDL Operations

実際に下記のDDLで検証したところサイズの拡大・縮小ともにALGORITHM=INPLACEではエラーとなり、ALGORITHM=COPYが要求された。

ALGORITHM=INPLACE is not supported. Reason: Cannot change column type INPLACE. Try ALGORITHM=COPY.

ここから考えると、MySQL 5.6ではVARCHARに関する最適化はなく表中の Changing the column data type 相当のオフラインDDLになっていると想定される。

-- size=2にセット
ALTER TABLE foo MODIFY COLUMN foo_id VARCHAR(2);

-- size=2 to 3 => エラーになる
ALTER TABLE foo CHANGE COLUMN foo_id foo_id VARCHAR(3), ALGORITHM=INPLACE, LOCK=NONE;

-- size=2 to 1 => エラーになる
ALTER TABLE foo CHANGE COLUMN foo_id foo_id VARCHAR(1), ALGORITHM=INPLACE, LOCK=NONE;

MySQL 5.7

MySQL 5.7ではMySQL :: MySQL 5.7 Reference Manual :: 14.13.1 Online DDL Operationsにあるとおり、VARCHARのサイズ拡張はオンラインDDLとして実行可能であるという記載がある。

しかし、この最適化には以下のような注意点がある。

  • カラムサイズの縮小に関しては対応されていない。(縮小の場合はオフラインDDLになる)
  • length prefixのbyte数は同じである必要がある(異なる場合はオフラインDDLになる)
カラムサイズの縮小について

サイズの縮小時に最適化されない旨は上記の公式ドキュメント内に記載がある

Decreasing VARCHAR size using in-place ALTER TABLE is not supported. Decreasing VARCHAR size requires a table copy (ALGORITHM=COPY).

実際に MySQL 5.7で同様の構文でサイズを縮小しようとしたところMySQL 5.6と同様のエラーが発生することが確認できた。

length prefixのサイズ制限について

前提の項で挙げたとおりVARCHARのbyte数が1~255の場合と 256以上の場合ではlength prefixのバイト数が異なる。

そのため VARCHAR(1) => VARCHAR(2)の場合はオンラインDDLになるが、VARCHAR(3) => VARCHAR(1000)の場合はオフラインDDLになってしまうといった問題が発生しうる。

さらにこれはバイト数での話であり、「文字数を意味するVARCHAR(N)のN」について考えたい場合は文字コードについても考える必要がある。

具体的には以下のようにオンラインDDLで実行されるかのしきい値が変わる。

  • CHARSET = latin1 のときは1文字1byteなので
    • VARCHAR(1)~VARCHAR(255)はlength prefixのサイズが1
    • VARCHAR(256)~はlength prefixのサイズが2
  • CHARSET = utf8mb4 のときは1文字4byteなので
    • VARCHAR(1)~VARCHAR(63)はlength prefixのサイズが1
    • VARCHAR(64)~はlength prefixのサイズが2

安全なマイグレーションの手引き

まず不用意にオフラインDDLになることをさけるため、 LOCK=none を付与することをおすすめする。

read/writeがブロックされるalter tableになっていたら実行前にエラーになってくれる。

またMySQL 8.xのドキュメントを見てもVARCHARのサイズ拡張に対応したアルゴリズムはINPLACEだけなので、 ALGORITHM=INPLACE もつけてよいだろう。(将来的になにか素敵なアルゴリズムが出た場合は見直す必要が出るかもしれないが、とはいえ現状でもブロックなしでtableコピーもないので気にするほどではない・・はず)

MySQL :: MySQL 8.0 Reference Manual :: 17.12.1 Online DDL Operations

エラーになった場合は諦めるか停止メンテ、あるいはpt-online-schema-change等の利用を検討するのが良いだろう。

refinedでPosInt + PosInt を PosIntにしたい

結論

オーバーフローしたら条件満たさないから無理

結論以外

refinedScalaでrefinement typeを表現するためのライブラリです。

このライブラリの中に「1以上のInt」を表すPosIntという型があるのですが、refinedは PosInt + PosInt = PosIntといった推論を(通常は)行わないので、2つのPosIntを足す場合は自分で検査を行う必要があります。

val a = PosInt.unsafeFrom(1)
val b = PosInt.unsafeFrom(2)
val c: Either[String, PosInt] = PosInt.from(a.value + b.value) 

一方でrefinedでは catsのSemigroupインスタンス が提供されているので、 以下のようにPosIntが返ってくるPosInt同士の計算を行うことができます。

import eu.timepit.refined.cats._
import cats.syntax.semigroup._

val c: PosInt =  a.value |+| b.value

標準で上記のようなメソッドが用意されていないのはオーバーフローによりPosInt同士の加算が負になる = PosIntでなくなる可能性があるためです。 (see https://github.com/fthomas/refined/issues/217#issuecomment-258535569 )

実際、refinedで提供されているSemigroupのインスタンスでは オーバーフローしないような実装 が含まれています。

これが求めるものかどうかはユースケース次第かとは思います。筆者がよく使うシチュエーションではPosInt.fromを使って万が一オーバフローが発生したら例外が出てくれる方が嬉しいので上記インスタンスを積極的に利用することはしていません。

ということで用法用量を守って正しくお使いください。

なおこの記事は来週のアドベントカレンダー用の記事の端材を使って執筆されました。

MySQLでカラム追加などのalter table中にクエリがブロックされるかなどについてのメモ

これは何

MySQLのカラム追加などのDDL, alter table時にそれぞれの操作がどれくらい危険なのか、負荷は?といった情報の見方を毎回忘れるのでメモしたもの

まとめ

  • 表を見なくても ALTER TABLE の末尾に ALGORITHM=INPLACE,LOCK=NONE; をつけてしまえば、動作を決められる(不可能な場合はエラーになる)ので、それで実験してみるとよい
  • 公式document に詳細に記載があるのでそれを見ると良い。
    • ただし必ずしもその表通りになるわけではないため表の下に記載されている注意書きをよく読む必要がある。
  • 例えば varcharのサイズ変更 = Extending VARCHAR column size は表内では INPLACE=yes と記載されているが length を 255 から 256 に拡張する場合は ALGORITHM=COPY しか利用できないといった制限が記載されているので見落とさないようにすべき。(MySQL8.0 公式doc
  • 余談だが上記の事象については MySQLでのVARCHARサイズ変更を行うALTER TABLEについて - だいたいよくわからないブログ に理由を記載している。

Alter Tableの性能特性についての公式ドキュメントの見方

にある表からやりたい操作を探せば良い。

表にあるそれぞれの意味と解釈は

項目 説明
インプレース In Place yesだと既存データを新規ファイルにコピーせずに、既存ファイル内でalter tableを実行する。Noだと新規データファイルにデータをコピーするのでディスク容量を消費する。またsharedロックが取得され書き込みがブロックされる。(yesだとalgorithm=INPLACE, noだとalgorithm=COPYとなる)
テーブルの再構築 Rebuilds Table yesだと既存データの書き換えが走るのでテーブル行数に応じて時間がかかったりディスクIOが高まったりする
同時 DML の許可 Permits Concurrent DML yesだとinsert/update/deleteなどの更新が行える(lock=none)
メタデータの変更のみ Only Modifies Metadata yesだとメタデータ更新のみでalter tableがすぐ完了する

より詳細な解説は以下の記事を参照すると良い。

mita2db.hateblo.jp

まとめ欄にも記載したが、表だけではなく表の下の解説まで読むことをおすすめする。読み飛ばすとALGORITHM=INPLACEで実行されると思っていたのにALGORITHM=COPYが発動してしまう(ALGORITHMについては後述)といったことがありえる。

algorithmの違い

algorithm=COPY

  • 空テーブルを作成
  • 全データ変換しながらコピー
  • テーブルを新しいものに入れ替え という手順でのマイグレーション

基本的に古い時代の物だがいくつかのケースではCOPYが採用される。

使われるケースは例えば

  • 主キーの削除(削除&追加を1行で行うのは問題ないが、削除だけを行う操作を行うと)
  • データ型の変更

などになる。

  • データをコピーするためデータ件数によってはdisk IO負荷が高くなり、ディスク容量も必要で、実行時間も長くなることがあるので注意

algorithm=INPLACE

  • テーブルを作り直す
  • secondary indexを作り直す
  • テーブル作り直し中の更新操作をログに記録
  • テーブル作り直し中の更新操作を作り直したテーブルへapplyする

という手順でのマイグレーション

MySQL 5.xでALGORITHMを指定せずにADD COLUMNをした場合は大抵の場合は inplace になる。

誕生の経緯などは第30回 InnoDBオンラインDDLについて | gihyo.jpを参照

  • 裏でテーブルを作り直すのでデータ件数によってはdisk IO負荷が高くなり、実行時間も長くなることがあるので注意

algorithm=INSTANT

メタデータのみを書き換えて、テーブルデータを全く変更しないモード

対応する操作は以下

  • テーブル末尾へのカラム追加
  • MODIFY COLUMN
  • etc

基本的にはMySQL 8以降の機能。

MySQL 8.0.12からInstant ADD COLUMNという機能が追加されました。

MySQLのInstant ADD COLUMNをちゃんと調べてみる - y-asaba@hatenablog

auroraでは今までサポートされていなかったが 2021/11/18にMySQL 8.x系互換のaurora MySQL v3がリリースされた ので現在は利用可能のよう。

速度感の違いとしては

  • COPY 21.70 秒
  • INPLACE 6.54 秒
  • INSTANT 0.05秒

といった参考値が挙げられている。 (もちろん諸条件によって変化する) MySQL 8.0 の INSTANT DDL について – スマートスタイル技術ブログ

カラム追加をalgorithm=INSTANTで実行する際の条件については MySQL :: MySQL 8.0 Reference Manual :: 17.12.1 Online DDL Operationsから (Syntax and Usage Notes > Adding a column )を参照

aurora fast DDL

仕組みは不明だがテーブルコピーなしで瞬時にカラム追加できる。

条件は以下

  • NULL を許容する列 (デフォルト値を持たない) のテーブル末尾への追加
  • パーティション分割されてない
  • レコードサイズがページサイズの半分以下

公式 には

Currently, Aurora lab mode must be enabled to use fast DDL for Aurora MySQL. We don't recommend using fast DDL for production DB clusters. For information about enabling Aurora lab mode, see Amazon Aurora MySQL lab mode.

との記載があり、さらに aurora MySQL v3からはInstantで行くぞと書いてあるためこちらについて詳しくなる必要はあまりなさそう。

Alter Table時のlockモードとalgorithmの指定

意図されていないlockモードやalgorithmでの動作を防ぎたい場合は

ALTER TABLE test MODIFY text varchar(20), ALGORITHM=INPLACE,LOCK=NONE;

のように明示的に指定してやればよい。

もし想定と異なるモードで実行される状態であればエラーで停止するようになっている。

第30回 InnoDBオンラインDDLについて | gihyo.jp

LOCKについて

ページ上部に記載した表を見て意図したものを選ぶのがよい。

オプション 説明
NONE read ○, write○ (何もブロックしない)
SHARED read ○, write × (writeがブロックされる)
EXCLUSIVE read ×, write × (read/writeがブロックされる)
DEFAULT 使用可能なもっとも低いレベルのロックを使用
LOCK句省略 DEFAULTと同じ

algorithmについて

オプション 説明
COPY テーブルコピー方式
INPLACE インプレース方式
INSTANT インスタント方式
DEFAULT 一番負荷が少ない方式を採用
ALGORITHM句省略 DEFAULTと同じ

マイグレーションの速度感

インスタンスサイズや設定によるので概算でしかないが以下が参考になる。

https://www.percona.com/live/e18/sites/default/files/slides/Deep%20Dive%20on%20Amazon%20Aurora%20-%20FileId%20-%20160328.pdf の p.31より r3.large ~ r3.8xlarge

mysql テーブルサイズ 実行時間目安 sec 実行時間の大体オーダー感
5.6 10GB 900 ~ 3960 15 min ~ 70 min
5.6 50GB 4680 ~ 23400 1.3h ~ 6.5h
5.6 100GB 14400 ~ 53460 4h ~ 15h
5.7 10GB 1080 ~ 1600 15 min ~ 30 min
5.7 50GB 5040 1.4h
5.7 100GB 9720 2.7h

繰り返すがinstanceサイズやレコードサイズ、mysqlの設定などあらゆる要素で変動するので、この時間で終わると思ってはいけない。

テーブルサイズの調べ方

MySQLでDBとテーブルのサイズを確認するSQL #MySQL - Qiitaを参考にした。

SELECT  
    table_name, 
    table_rows,
    floor((data_length+index_length)/1024/1024)/1024 AS dataGB  #総容量(インデックス込み)
FROM 
    information_schema.tables  
WHERE
    table_schema=database()  
ORDER BY
    table_name;