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への移行チャレンジ
- SQL互換性:CockroachDBはPostgreSQL互換、MySQL構文の書き換えが必要
- 自動増分ID:MySQLのAUTO_INCREMENTは分散環境で非連続、UUIDまたはSEQUENCEを使用
- 外部キーカスケード:サポートするが推奨されない(パフォーマンス問題)
- ストレージエンジン差異:InnoDBのクラスタインデックス vs CockroachDBのLSM-tree
- 文字セット: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互換境界の理解が前提です。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format
- Base64エンコード/デコード:/ja/encode/base64
- cURLからコード:/ja/dev/curl-to-code
- SQLフォーマッター:/ja/dev/sql-format
ブラウザローカルツールを無料で試す →
#CockroachDB#分布式SQL#NewSQL#分布式事务#多区域#在线迁移#高可用#一致性