CockroachDB分散型SQL実践:2026年MySQLからNewSQLへのオンライン移行完全ガイド

编程语言

MySQLのシャーディングで疲弊していませんか?

シャードキーの選択ミスでホットスポット、クロスシャードJOINのパフォーマンス災害、分散トランザクションをメッセージキューで継ぎ接ぎ、スケールアウトにダウンタイムのデータ移行が必要——MySQLクラスタがTB規模に達すると、シャーディングの運用コストはビジネス価値を遥かに超えます。2026年、NewSQLデータベースがよりエレガントな解答を提供します:CockroachDB、ネイティブ分散、強整合性、PostgreSQLプロトコル互換のSQLデータベース。

本記事ではMySQLからCockroachDBへのオンラインゼロダウンタイム移行を、アーキテクチャ原理から実践ステップ、分散トランザクションからマルチリージョンデプロイまで完全ガイドします。


CockroachDBアーキテクチャコア

概念 説明 MySQLとの比較
Range 範囲でデータをシャーディングする基本単位(デフォルト64MB) MySQLのPartitionに類似、ただし自動管理
Replica 各Rangeのデフォルト3レプリカ、Raftコンセンサス使用 MySQL MGRの類似機構、ただしより成熟
Lease Holder Rangeの読み書きリーダー、ローカル読み取りを処理 MySQLのプライマリに類似、ただしより細粒度
DistSQL 分散SQLエンジン、クエリを自動並列化 MySQLは手動シャーディング+ミドルウェアが必要
HLC ハイブリッド論理時計、グローバル時系列を提供 MySQLはGTIDに依存、クロスシャードでグローバル時系列なし

分散トランザクションACID保証

CockroachDBはParallel Commitsプロトコルで分散トランザクションを実装し、2PCのコーディネータ単一障害点を排除:

トランザクションフロー:
1. Write Intentをすべての関連Rangeに書き込み
2. PREPAREとCOMMITをすべてのRangeに並列送信
3. いずれかのノードがクラッシュ時、他ノードがWrite Intentから復旧
4. トランザクションレイテンシ = 1回のネットワークラウンドトリップ(2PCの2回ではなく)

問題分析:MySQLからCockroachDBへの移行チャレンジ

  1. SQL互換性:CockroachDBはPostgreSQL互換、MySQL構文の書き換えが必要
  2. 自動増分ID:MySQLのAUTO_INCREMENTは分散環境で非連続、UUIDまたはSEQUENCEを使用
  3. 外部キーカスケード:サポートするが推奨されない(パフォーマンス問題)
  4. ストレージエンジン差異:InnoDBのクラスタインデックス vs CockroachDBのLSM-tree
  5. 文字セット:MySQLのutf8mb4はCockroachDBのUTF8(デフォルト)に対応

ステップバイステップ:MySQLオンライン移行

Step 1:CockroachDBのインストール

# Linux
curl -L https://binaries.cockroachdb.com/cockroach-v24.3.0.linux-amd64.tgz | tar -xz
sudo cp cockroach-v24.3.0.linux-amd64/cockroach /usr/local/bin/

# Docker
docker pull cockroachdb/cockroach:v24.3.0

# シングルノード起動(開発テスト)
cockroach start-single-node --insecure --listen-addr=localhost:26257 --store=path=/data/cockroach

Step 2:本番クラスタの起動

# ノード1
cockroach start \
  --insecure \
  --store=path=/data/cockroach1 \
  --listen-addr=192.168.1.10:26257 \
  --http-addr=192.168.1.10:8080 \
  --join=192.168.1.10:26257,192.168.1.11:26257,192.168.1.12:26257 \
  --background

# クラスタの初期化(初回のみ)
cockroach init --insecure --host=192.168.1.10:26257

Step 3:MOLT移行ツールの使用

# MOLTのインストール
curl -L https://binaries.cockroachdb.com/molt/v1.0.0/molt_linux_amd64.tar.gz | tar -xz
sudo cp molt /usr/local/bin/

# 移行可能性の評価
molt assess \
  --source-type=mysql \
  --source="root:password@tcp(mysql-host:3306)/mydb" \
  --target="postgresql://root@cockroach-host:26257/mydb?sslmode=disable"

# スキーマのエクスポート
molt dump-schema \
  --source-type=mysql \
  --source="root:password@tcp(mysql-host:3306)/mydb" \
  --output-dir=./schema

# スキーマの変換(MySQL→CockroachDB)
molt convert-schema \
  --input-dir=./schema \
  --output-dir=./converted-schema

# CockroachDBにスキーマを適用
cockroach sql --insecure --host=cockroach-host < ./converted-schema/schema.sql

# オンラインデータ移行(CDC方式、ゼロダウンタイム)
molt migrate-data \
  --source-type=mysql \
  --source="root:password@tcp(mysql-host:3306)/mydb" \
  --target="postgresql://root@cockroach-host:26257/mydb?sslmode=disable" \
  --mode=cdc \
  --batch-size=10000 \
  --concurrency=8

Step 4:SQL構文の書き換え

-- MySQL: AUTO_INCREMENT → CockroachDB: SERIAL
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

-- MySQL: LIMIT offset, count → CockroachDB: LIMIT count OFFSET offset
SELECT * FROM orders LIMIT 20 OFFSET 10;

-- MySQL: GROUP_CONCAT → CockroachDB: STRING_AGG
SELECT dept, STRING_AGG(name, ',') FROM employees GROUP BY dept;

-- MySQL: ON DUPLICATE KEY UPDATE → CockroachDB: UPSERT
UPSERT INTO users (id, name) VALUES (1, 'Alice');

完全コード:アプリケーション移行例

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/lib/pq"
)

type Order struct {
    ID        int
    UserID    int
    Amount    float64
    Status    string
    CreatedAt time.Time
}

type OrderRepo struct {
    db *sql.DB
}

func NewOrderRepo(connStr string) (*OrderRepo, error) {
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil, fmt.Errorf("open db: %w", err)
    }
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(5 * time.Minute)
    return &OrderRepo{db: db}, nil
}

func (r *OrderRepo) CreateOrder(ctx context.Context, order *Order) error {
    query := `
        INSERT INTO orders (user_id, amount, status, created_at)
        VALUES ($1, $2, $3, $4)
        RETURNING id`
    return r.db.QueryRowContext(ctx, query,
        order.UserID, order.Amount, order.Status, order.CreatedAt,
    ).Scan(&order.ID)
}

func (r *OrderRepo) TransferOrder(ctx context.Context, fromUserID, toUserID int, amount float64) error {
    tx, err := r.db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("begin tx: %w", err)
    }
    defer tx.Rollback()

    deductQuery := `UPDATE accounts SET balance = balance - $1 WHERE user_id = $2 AND balance >= $1`
    result, err := tx.ExecContext(ctx, deductQuery, amount, fromUserID)
    if err != nil {
        return fmt.Errorf("deduct: %w", err)
    }
    if n, _ := result.RowsAffected(); n == 0 {
        return fmt.Errorf("insufficient balance")
    }

    creditQuery := `UPDATE accounts SET balance = balance + $1 WHERE user_id = $2`
    if _, err := tx.ExecContext(ctx, creditQuery, amount, toUserID); err != nil {
        return fmt.Errorf("credit: %w", err)
    }

    logQuery := `INSERT INTO transfer_logs (from_user, to_user, amount, created_at) VALUES ($1, $2, $3, NOW())`
    if _, err := tx.ExecContext(ctx, logQuery, fromUserID, toUserID, amount); err != nil {
        return fmt.Errorf("log: %w", err)
    }

    return tx.Commit()
}

func main() {
    repo, err := NewOrderRepo("postgresql://root@localhost:26257/mydb?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer repo.db.Close()

    ctx := context.Background()
    err = repo.TransferOrder(ctx, 1, 2, 100.50)
    if err != nil {
        log.Fatal("Transfer failed:", err)
    }
    fmt.Println("Transfer succeeded")
}

マルチリージョンデプロイ設定

ALTER RANGE default CONFIGURE ZONE USING num_replicas = 5, constraints = '{"+us-east-1":2, "+eu-west-1":2, "+ap-southeast-1":1}';

ALTER DATABASE mydb CONFIGURE ZONE USING num_replicas = 5, constraints = '{"+us-east-1":2, "+eu-west-1":2, "+ap-southeast-1":1}';

ALTER TABLE users CONFIGURE ZONE USING num_replicas = 5, lease_preferences = '[[+us-east-1]]';

ALTER PARTITION us_east OF TABLE orders CONFIGURE ZONE USING constraints = '{"+us-east-1":2, "+eu-west-1":1}';
ALTER PARTITION eu_west OF TABLE orders CONFIGURE ZONE USING constraints = '{"+eu-west-1":2, "+us-east-1":1}';

落とし穴ガイド

落とし穴1:SERIAL IDが連続ではない

-- ❌ 期待:1, 2, 3, 4...
-- ✅ 実際:SERIALはunique_rowid()を使用、大きな数を生成
-- 解決:連続IDが必要な場合はSEQUENCEを使用
CREATE SEQUENCE order_seq;
CREATE TABLE orders (id BIGINT DEFAULT nextval('order_seq') PRIMARY KEY, ...);

落とし穴2:外部キーカスケードのパフォーマンス

-- ❌ カスケード更新はクロスRangeトランザクションをトリガー
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id) ON UPDATE CASCADE
);

-- ✅ カスケードを使用せず、アプリケーションで処理
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id) ON DELETE RESTRICT
);

落とし穴3:大トランザクションのタイムアウト

-- ❌ 10万行を単一トランザクションで更新、5分タイムアウト超過
BEGIN;
UPDATE large_table SET status = 'processed' WHERE created_at < '2026-01-01';
COMMIT;

-- ✅ バッチ更新
BEGIN;
UPDATE large_table SET status = 'processed' WHERE created_at < '2026-01-01' LIMIT 1000;
COMMIT;

落とし穴4:SELECT FOR UPDATEによるRangeロック競合

-- ❌ Range全体をロック
SELECT * FROM orders WHERE user_id = 1 FOR UPDATE;

-- ✅ AS OF SYSTEM TIMEで履歴スナップショットを読み取り
SELECT * FROM orders AS OF SYSTEM TIME '-5s' WHERE user_id = 1;

落とし穴5:Zone Configurationの無視による高読み取りレイテンシ

-- ❌ デフォルト3レプリカランダム分布、クロスリージョン読み取りレイテンシ200ms+
-- ✅ lease_preferencesを設定してローカル読み取り
ALTER TABLE users CONFIGURE ZONE USING lease_preferences = '[[+us-east-1]]';

エラートラブルシューティング

# エラーメッセージ 原因 解決方法
1 TransactionRetryWithProtoRefreshError トランザクション競合、リトライ必要 CRDBトランザクションリトライラッパーを使用、または並行度を下げる
2 transaction deadline exceeded トランザクションが5分タイムアウト超過 大トランザクションを小バッチに分割
3 duplicate key value violates unique constraint SERIAL ID衝突(稀) UUIDまたは明示的SEQUENCEを使用
4 foreign key violation FK制約チェック失敗 参照データの存在確認、挿入順序の確認
5 value type tuple doesn't match type of column MySQL暗黙型変換非対応 明示的CAST型変換
6 unimplemented: this syntax is not supported MySQL固有構文非対応 互換性ドキュメント確認、PG構文に書き換え
7 memory budget exceeded クエリ中間結果がメモリ制限超過 LIMIT追加、JOIN順序最適化、メモリ設定増加
8 node is draining ノードがシャットダウン中 ドレーン完了待ちまたは他ノードに接続
9 replica not available レプリカ不足(ノードダウン) 生きているノード>=クオーラムを保証、レプリカ復旧待ち
10 incompatible schema change オンラインスキーマ変更競合 前のスキーマ変更完了を待つ

高度な最適化

1. 履歴データ読み取りでロック競合を回避

SELECT * FROM orders AS OF SYSTEM TIME INTERVAL '-5s'
WHERE status = 'pending';

2. バルクインサート最適化

IMPORT INTO orders (id, user_id, amount, status)
CSV DATA (
    'nfs://data/orders_part1.csv',
    'nfs://data/orders_part2.csv'
) WITH skip = 1;

3. 自動リトライトランザクションラッパー

func RetryTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
    for i := 0; i < 5; i++ {
        tx, err := db.BeginTx(ctx, nil)
        if err != nil {
            return err
        }
        err = fn(tx)
        if err == nil {
            return tx.Commit()
        }
        tx.Rollback()
        if !isRetryableError(err) {
            return err
        }
        time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
    }
    return fmt.Errorf("max retries exceeded")
}

比較分析

次元 MySQLシャーディング CockroachDB TiDB Spanner
分散トランザクション ミドルウェア必要 ネイティブACID ネイティブACID ネイティブACID
SQL互換 MySQL PostgreSQL MySQL PostgreSQL
オンラインスケーリング ダウンタイム必要 自動Rebalance 自動Rebalance 自動
マルチリージョン 自構築必要 ネイティブ対応 限定対応 ネイティブ対応
整合性 結局整合 強整合(Serializable) RC/SI 強整合
運用複雑度 低(マネージド)
コスト
オープンソース ✅(BSL)

まとめ:MySQLからCockroachDBへの移行は単なる「データベースの切り替え」ではなく、「シャーディング+ミドルウェア」から「ネイティブ分散SQL」へのアーキテクチャアップグレードです。キーパス:MOLTツールでオンライン移行→非互換SQLの書き換え→マルチリージョンZone設定→大トランザクションのバッチ処理→トランザクションリトライ設定。CockroachDBの強整合性と自動Rebalanceで運用コストを80%削減できますが、分散トランザクションメカニズムとSQL互換境界の理解が前提です。


オンラインツール推奨

ブラウザローカルツールを無料で試す →

#CockroachDB#分布式SQL#NewSQL#分布式事务#多区域#在线迁移#高可用#一致性