CREATE TABLE waitlist (
— 主キー
waitlist_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
— 誰がどの部屋タイプを希望しているか
user_id BIGINT UNSIGNED NOT NULL,
room_type_id BIGINT UNSIGNED NOT NULL,
— 希望宿泊期間
check_in_date DATE NOT NULL,
check_out_date DATE NOT NULL,
— 希望人数
num_guests INT UNSIGNED NOT NULL,
— ステータス(待機中・通知済み・期限切れ)
status ENUM(‘waiting’, ‘notified’, ‘expired’) DEFAULT ‘waiting’,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
— 有効期限(この日時を過ぎたら自動で期限切れ)
expires_at TIMESTAMP NOT NULL,
— 外部キー
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (room_type_id) REFERENCES room_types(room_type_id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_room_type_dates (room_type_id, check_in_date, check_out_date),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
🔒 4. ダブルブッキング防止
予約処理のトランザクション
START TRANSACTION;
— 1. 予約期間の空室チェック(悲観的ロック)
SELECT
ra.availability_date,
ra.total_capacity,
ra.reserved_count,
(ra.total_capacity – ra.reserved_count) as available
FROM room_availability ra
WHERE ra.room_id = 101
AND ra.availability_date >= ‘2024-12-01’
AND ra.availability_date < ‘2024-12-03’ — チェックアウト日は含まない
AND ra.is_closed = FALSE
FOR UPDATE; — 行ロック
— 2. 全日程で空きがあるかチェック
— (アプリケーション側で判定)
— available > 0 がすべての日付で満たされるか
— 3. 空きがない場合はROLLBACK
— 空きがある場合は予約を作成
— 4. 予約レコードを作成
INSERT INTO reservations (user_id, room_id, check_in_date, check_out_date, num_guests, total_price)
VALUES (123, 101, ‘2024-12-01’, ‘2024-12-03’, 2, 30000.00);
SET @reservation_id = LAST_INSERT_ID();
— 5. 空室カウントを更新
UPDATE room_availability
SET reserved_count = reserved_count + 1
WHERE room_id = 101
AND availability_date >= ‘2024-12-01’
AND availability_date < ‘2024-12-03’;
COMMIT;
🛡️ ダブルブッキング防止の仕組み
FOR UPDATE:読み取り時に行ロック
トランザクション:複数の更新をアトミックに実行
reserved_count:予約済み数をカウント
範囲チェック:全日程で空きがあることを確認
制約による二重チェック
— 予約の重複を検出するトリガー
DELIMITER //
CREATE TRIGGER check_overlapping_reservations
BEFORE INSERT ON reservations
FOR EACH ROW
BEGIN
DECLARE overlap_count INT;
SELECT COUNT(*) INTO overlap_count
FROM reservations
WHERE room_id = NEW.room_id
AND reservation_status NOT IN (‘cancelled’)
AND (
(NEW.check_in_date >= check_in_date AND NEW.check_in_date < check_out_date)
OR
(NEW.check_out_date > check_in_date AND NEW.check_out_date <= check_out_date)
OR
(NEW.check_in_date <= check_in_date AND NEW.check_out_date >= check_out_date)
);
IF overlap_count > 0 THEN
SIGNAL SQLSTATE ‘45000’
SET MESSAGE_TEXT = ‘Overlapping reservation detected’;
END IF;
END //
DELIMITER ;
🔍 5. 空室検索の最適化
空室検索クエリ
— 2024年12月1日〜3日で2名宿泊可能な部屋を検索
SELECT
rt.type_name,
r.room_number,
rt.base_price,
rt.max_occupancy,
COUNT(DISTINCT ra.availability_date) as available_days
FROM room_types rt
INNER JOIN rooms r ON rt.room_type_id = r.room_type_id
INNER JOIN room_availability ra ON r.room_id = ra.room_id
WHERE rt.hotel_id = 1
AND rt.max_occupancy >= 2
AND ra.availability_date >= ‘2024-12-01’
AND ra.availability_date < ‘2024-12-03’
AND (ra.total_capacity – ra.reserved_count) > 0
AND ra.is_closed = FALSE
AND r.is_active = TRUE
GROUP BY rt.type_name, r.room_id, r.room_number, rt.base_price, rt.max_occupancy
HAVING available_days = DATEDIFF(‘2024-12-03’, ‘2024-12-01’) — 全日程空いている
ORDER BY rt.base_price;
— 特定日の料金を取得
SELECT
r.room_id,
r.room_number,
rt.base_price,
IFNULL(pr.price_multiplier, 1.00) as multiplier,
IFNULL(pr.fixed_price, rt.base_price * IFNULL(pr.price_multiplier, 1.00)) as daily_price
FROM rooms r
INNER JOIN room_types rt ON r.room_type_id = rt.room_type_id
LEFT JOIN pricing_rules pr ON rt.room_type_id = pr.room_type_id
AND pr.is_active = TRUE
AND (
— 曜日ルール
(pr.rule_type = ‘weekday’ AND DAYOFWEEK(‘2024-12-01’) BETWEEN 2 AND 6)
OR (pr.rule_type = ‘weekend’ AND DAYOFWEEK(‘2024-12-01’) IN (1, 7))
— シーズンルール
OR (pr.rule_type = ‘season’ AND ‘2024-12-01’ BETWEEN pr.valid_from AND pr.valid_until)
)
WHERE r.room_id = 101;
🔄 7. キャンセル処理
キャンセル処理のトランザクション
START TRANSACTION;
— 1. 予約をキャンセル状態に更新
UPDATE reservations
SET reservation_status = ‘cancelled’
WHERE reservation_id = 12345;
— 2. キャンセル料を計算(例:前日までは無料、当日は50%)
SET @cancellation_fee = (
CASE
WHEN DATEDIFF((SELECT check_in_date FROM reservations WHERE reservation_id = 12345), CURDATE()) >= 1
THEN 0.00
ELSE (SELECT total_price FROM reservations WHERE reservation_id = 12345) * 0.5
END
);
— 3. キャンセル履歴を記録
INSERT INTO cancellations (reservation_id, cancellation_reason, cancellation_fee, refund_amount)
SELECT
12345,
‘お客様都合’,
@cancellation_fee,
total_price – @cancellation_fee
FROM reservations
WHERE reservation_id = 12345;
— 4. 空室カウントを戻す
UPDATE room_availability ra
INNER JOIN reservations res ON ra.room_id = res.room_id
SET ra.reserved_count = ra.reserved_count – 1
WHERE res.reservation_id = 12345
AND ra.availability_date >= res.check_in_date
AND ra.availability_date < res.check_out_date;
COMMIT;
📊 8. レポートとダッシュボード
稼働率レポート
— 月別稼働率
SELECT
DATE_FORMAT(ra.availability_date, ‘%Y-%m’) as month,
COUNT(*) as total_days,
SUM(ra.reserved_count) as reserved_days,
ROUND(SUM(ra.reserved_count) / COUNT(*) * 100, 2) as occupancy_rate
FROM room_availability ra
WHERE ra.availability_date >= ‘2024-01-01’
AND ra.availability_date < ‘2025-01-01’
GROUP BY DATE_FORMAT(ra.availability_date, ‘%Y-%m’)
ORDER BY month;
売上レポート
— 月別売上
SELECT
DATE_FORMAT(r.check_in_date, ‘%Y-%m’) as month,
COUNT(*) as total_reservations,
SUM(r.total_price) as total_revenue,
AVG(r.total_price) as avg_price_per_reservation
FROM reservations r
WHERE r.reservation_status IN (‘confirmed’, ‘checked_in’, ‘checked_out’)
AND r.check_in_date >= ‘2024-01-01’
AND r.check_in_date < ‘2025-01-01’
GROUP BY DATE_FORMAT(r.check_in_date, ‘%Y-%m’)
ORDER BY month;
✅ 9. 完全な設計書作成
📄 設計書に含めるべき内容
システム概要:目的、対象ユーザー、主要機能
ER図:エンティティとリレーションシップの全体像
テーブル定義書:各テーブルの詳細仕様
インデックス設計:パフォーマンス最適化
トランザクション設計:重要な処理の流れ
セキュリティ:アクセス制御、データ保護
バックアップ戦略:データ保護方針
スケーラビリティ:将来の拡張性
テーブル定義書の例
【テーブル名】reservations(予約)
【説明】
ホテルの予約情報を管理するテーブル。
顧客が部屋を予約した際の全情報を保持する。
【カラム定義】
┌────────────────────┬──────────────┬──────┬──────┬─────────────┐
│ カラム名 │ データ型 │ NULL │ キー │ 説明 │
├────────────────────┼──────────────┼──────┼──────┼─────────────┤
│ reservation_id │ BIGINT │ NO │ PRI │ 予約ID │
│ user_id │ BIGINT │ NO │ FK │ ユーザーID │
│ room_id │ BIGINT │ NO │ FK │ 部屋ID │
│ check_in_date │ DATE │ NO │ IDX │ チェックイン日 │
│ check_out_date │ DATE │ NO │ IDX │ チェックアウト日 │
│ num_guests │ INT │ NO │ │ 宿泊人数 │
│ total_price │ DECIMAL(10,2)│ NO │ │ 合計金額 │
│ reservation_status │ ENUM │ NO │ IDX │ 予約ステータス │
└────────────────────┴──────────────┴──────┴──────┴─────────────┘
【ビジネスルール】
– check_out_date は check_in_date より後の日付でなければならない
– num_guests は部屋タイプの max_occupancy を超えてはならない
– キャンセル後は reservation_status が ‘cancelled’ に更新される
CREATE TABLE reviews (
review_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
reservation_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
hotel_id BIGINT UNSIGNED NOT NULL,
room_type_id BIGINT UNSIGNED NOT NULL,
— 総合評価
overall_rating INT UNSIGNED NOT NULL CHECK (overall_rating BETWEEN 1 AND 5),
— 項目別評価
cleanliness_rating INT UNSIGNED CHECK (cleanliness_rating BETWEEN 1 AND 5),
service_rating INT UNSIGNED CHECK (service_rating BETWEEN 1 AND 5),
location_rating INT UNSIGNED CHECK (location_rating BETWEEN 1 AND 5),
facility_rating INT UNSIGNED CHECK (facility_rating BETWEEN 1 AND 5),
value_rating INT UNSIGNED CHECK (value_rating BETWEEN 1 AND 5),
title VARCHAR(200),
comment TEXT,
is_verified BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (reservation_id) REFERENCES reservations(reservation_id) ON DELETE RESTRICT,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (hotel_id) REFERENCES hotels(hotel_id) ON DELETE CASCADE,
FOREIGN KEY (room_type_id) REFERENCES room_types(room_type_id) ON DELETE CASCADE,
UNIQUE KEY unique_reservation_review (reservation_id),
INDEX idx_hotel (hotel_id),
INDEX idx_room_type (room_type_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— ホテルの平均評価を取得
SELECT
h.hotel_id,
h.hotel_name,
COUNT(*) as review_count,
ROUND(AVG(r.overall_rating), 2) as avg_overall,
ROUND(AVG(r.cleanliness_rating), 2) as avg_cleanliness,
ROUND(AVG(r.service_rating), 2) as avg_service,
ROUND(AVG(r.location_rating), 2) as avg_location
FROM hotels h
LEFT JOIN reviews r ON h.hotel_id = r.hotel_id
GROUP BY h.hotel_id, h.hotel_name;
追加課題 2発展
会員ランク制度を追加してください。
要件:
宿泊回数に応じてランクが上がる(ブロンズ→シルバー→ゴールド→プラチナ)
ランクに応じた割引率を適用
ランクアップ時に通知
【解答】
— 会員ランクマスタ
CREATE TABLE membership_ranks (
rank_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
rank_name VARCHAR(50) NOT NULL,
min_stays INT UNSIGNED NOT NULL, — 必要宿泊回数
discount_rate DECIMAL(4, 2) DEFAULT 0.00, — 割引率
priority_booking BOOLEAN DEFAULT FALSE, — 優先予約
late_checkout BOOLEAN DEFAULT FALSE, — レイトチェックアウト
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 初期データ
INSERT INTO membership_ranks (rank_name, min_stays, discount_rate, priority_booking, late_checkout) VALUES
(‘ブロンズ’, 0, 0.00, FALSE, FALSE),
(‘シルバー’, 5, 0.05, FALSE, FALSE),
(‘ゴールド’, 15, 0.10, TRUE, FALSE),
(‘プラチナ’, 30, 0.15, TRUE, TRUE);
— ユーザーの会員情報
CREATE TABLE user_memberships (
user_id BIGINT UNSIGNED PRIMARY KEY,
rank_id INT UNSIGNED NOT NULL DEFAULT 1,
total_stays INT UNSIGNED DEFAULT 0,
total_spent DECIMAL(12, 2) DEFAULT 0.00,
member_since DATE NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (rank_id) REFERENCES membership_ranks(rank_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— チェックアウト時にランクを更新するトリガー
DELIMITER //
CREATE TRIGGER after_checkout_update_rank
AFTER UPDATE ON reservations
FOR EACH ROW
BEGIN
DECLARE current_stays INT;
DECLARE new_rank_id INT;
IF NEW.reservation_status = ‘checked_out’ AND OLD.reservation_status != ‘checked_out’ THEN
— 宿泊回数を更新
UPDATE user_memberships
SET total_stays = total_stays + 1,
total_spent = total_spent + NEW.total_price
WHERE user_id = NEW.user_id;
— 現在の宿泊回数を取得
SELECT total_stays INTO current_stays
FROM user_memberships
WHERE user_id = NEW.user_id;
— 新しいランクを決定
SELECT rank_id INTO new_rank_id
FROM membership_ranks
WHERE min_stays <= current_stays
ORDER BY min_stays DESC
LIMIT 1;
— ランクを更新
UPDATE user_memberships
SET rank_id = new_rank_id
WHERE user_id = NEW.user_id;
END IF;
END //
DELIMITER ;