🕑
SQLで空き時間を取得する
課題
部屋の入退室記録サービスを考えます。
部屋ごとにユーザーが入室時刻・退室時刻を記録します。
全体の営業時間的なものを9:00から20:00とし、その間で誰も居ない時間帯を取得します。
id | room_id | user_id | entry_at | exit_at |
---|---|---|---|---|
1 | 1 | 1 | 2024/01/01 9:00:00 | 2024/01/01 10:00:00 |
2 | 1 | 2 | 2024/01/01 9:00:00 | 2024/01/01 11:00:00 |
3 | 1 | 3 | 2024/01/01 13:00:00 | 2024/01/01 14:00:00 |
4 | 1 | 1 | 2024/01/01 16:00:00 | 2024/01/01 17:00:00 |
5 | 1 | 4 | 2024/01/01 17:00:00 | 2024/01/01 18:00:00 |
6 | 2 | 4 | 2024/01/01 10:00:00 | 2024/01/01 12:00:00 |
7 | 2 | 5 | 2024/01/01 14:00:00 | 2024/01/01 17:00:00 |
8 | 2 | 6 | 2024/01/01 15:00:00 | 2024/01/01 16:00:00 |
9 | 2 | 7 | 2024/01/01 15:00:00 | 2024/01/01 20:00:00 |
テーブル定義
CREATE TABLE room_access (
id SERIAL PRIMARY KEY,
room_id INT NOT NULL,
user_id INT NOT NULL,
entry_at TIMESTAMP NOT NULL,
exit_at TIMESTAMP NOT NULL
)
テストデータ
INSERT INTO room_access (room_id, user_id, entry_at, exit_at)
VALUES
(1,1,'2024-01-01 09:00:00','2024-01-01 10:00:00'),
(1,2,'2024-01-01 09:00:00','2024-01-01 11:00:00'),
(1,3,'2024-01-01 13:00:00','2024-01-01 14:00:00'),
(1,1,'2024-01-01 16:00:00','2024-01-01 17:00:00'),
(1,4,'2024-01-01 17:00:00','2024-01-01 18:00:00'),
(2,4,'2024-01-01 10:00:00','2024-01-01 12:00:00'),
(2,5,'2024-01-01 14:00:00','2024-01-01 17:00:00'),
(2,6,'2024-01-01 15:00:00','2024-01-01 16:00:00'),
(2,7,'2024-01-01 15:00:00','2024-01-01 20:00:00');
期待する出力
このケースで期待する出力は下記のような形となります。
room_id | empty_from | empty_to |
---|---|---|
1 | 2024-01-01 11:00:00 | 2024-01-01 13:00:00 |
1 | 2024-01-01 14:00:00 | 2024-01-01 16:00:00 |
1 | 2024-01-01 18:00:00 | 2024-01-01 20:00:00 |
2 | 2024-01-01 09:00:00 | 2024-01-01 10:00:00 |
2 | 2024-01-01 12:00:00 | 2024-01-01 14:00:00 |
解法
1. 入室時間と前の退室時間のペアを生成
-- 入室時間と前の退室時間のペアを生成
SELECT
room_id,
entry_at,
LAG(exit_at) OVER (PARTITION BY room_id ORDER BY entry_at) AS prev_exit_at
FROM
room_access
WHERE
entry_at >= '2024-01-01 09:00:00' AND exit_at <= '2024-01-01 20:00:00'
room_id | entry_at | prev_exit_at |
---|---|---|
1 | 2024/01/01 9:00:00 | |
1 | 2024/01/01 9:00:00 | 2024/01/01 10:00:00 |
1 | 2024/01/01 13:00:00 | 2024/01/01 11:00:00 |
1 | 2024/01/01 16:00:00 | 2024/01/01 14:00:00 |
1 | 2024/01/01 17:00:00 | 2024/01/01 17:00:00 |
2 | 2024/01/01 10:00:00 | |
2 | 2024/01/01 14:00:00 | 2024/01/01 12:00:00 |
2 | 2024/01/01 15:00:00 | 2024/01/01 17:00:00 |
2 | 2024/01/01 15:00:00 | 2024/01/01 16:00:00 |
2. 各入室時間の前の空き時間を取得
手順1で作成したペアを使用して、各入室時間 (entry_at) の前の空き時間を取得します。
前の退室時間 (prev_exit_at) から入室時間 (entry_at)までの間が空き時間となります。
前の退室時間がない場合(その日の最初の入室)、営業開始時間(9:00)が開始時間として使用されます。
WITH access_periods AS (
-- 入室時間と前の退室時間のペアを生成
SELECT
room_id,
entry_at,
LAG(exit_at) OVER (PARTITION BY room_id ORDER BY entry_at) AS prev_exit_at
FROM
room_access
WHERE
entry_at >= '2024-01-01 09:00:00' AND exit_at <= '2024-01-01 20:00:00'
)
-- 各入室時間の前の空き時間を取得
SELECT
room_id,
COALESCE(prev_exit_at, '2024-01-01 09:00:00') AS empty_start,
entry_at AS empty_end
FROM
access_periods
room_id | empty_start | empty_end |
---|---|---|
1 | 2024/01/01 9:00:00 | 2024/01/01 9:00:00 |
1 | 2024/01/01 10:00:00 | 2024/01/01 9:00:00 |
1 | 2024/01/01 11:00:00 | 2024/01/01 13:00:00 |
1 | 2024/01/01 14:00:00 | 2024/01/01 16:00:00 |
1 | 2024/01/01 17:00:00 | 2024/01/01 17:00:00 |
2 | 2024/01/01 9:00:00 | 2024/01/01 10:00:00 |
2 | 2024/01/01 12:00:00 | 2024/01/01 14:00:00 |
2 | 2024/01/01 17:00:00 | 2024/01/01 15:00:00 |
2 | 2024/01/01 16:00:00 | 2024/01/01 15:00:00 |
3. 各部屋の最後の空き時間を取得
各部屋の最後の退室時間を取得し、営業終了時間(20:00)と組み合わせて、最後の空き時間を取得します。
-- 各部屋の最後の空き時間を取得
SELECT
room_id,
MAX(exit_at) AS empty_start,
'2024-01-01 20:00:00' AS empty_end
FROM
room_access
WHERE
exit_at <= '2024-01-01 20:00:00'
GROUP BY room_id
room_id | empty_start | empty_end |
---|---|---|
1 | 2024/01/01 18:00:00 | 2024-01-01 20:00:00 |
2 | 2024/01/01 20:00:00 | 2024-01-01 20:00:00 |
4. 空き時間の抽出
手順2、3で取得した期間からempty_end > empty_start
を満たす期間を取得します。
これにより、実際に空いている時間帯のみが結果として返されます。
WITH access_periods AS (
-- 入室時間と前の退室時間のペアを生成
SELECT
room_id,
entry_at,
LAG(exit_at) OVER (PARTITION BY room_id ORDER BY entry_at) AS prev_exit_at
FROM
room_access
WHERE
entry_at >= '2024-01-01 09:00:00' AND exit_at <= '2024-01-01 20:00:00'
),
empty_periods AS (
-- 各入室時間の前の空き時間を取得
SELECT
room_id,
COALESCE(prev_exit_at, '2024-01-01 09:00:00') AS empty_start,
entry_at AS empty_end
FROM
access_periods
UNION ALL
-- 各部屋の最後の空き時間を取得
SELECT
room_id,
MAX(exit_at) AS empty_start,
'2024-01-01 20:00:00' AS empty_end
FROM
room_access
WHERE
exit_at <= '2024-01-01 20:00:00'
GROUP BY room_id
)
SELECT
room_id,
empty_start,
empty_end
FROM
empty_periods
WHERE
-- 実際に空いている時間帯のみを抽出
empty_end > empty_start
ORDER BY
room_id, empty_start
room_id | empty_start | empty_end |
---|---|---|
1 | 2024/01/01 11:00:00 | 2024/01/01 13:00:00 |
1 | 2024/01/01 14:00:00 | 2024/01/01 16:00:00 |
1 | 2024/01/01 18:00:00 | 2024/01/01 20:00:00 |
2 | 2024/01/01 9:00:00 | 2024/01/01 10:00:00 |
2 | 2024/01/01 12:00:00 | 2024/01/01 14:00:00 |
総評
コメントがないと何やってるか分かりにくいです。
他の手法として以下のようなものが考えられます。
- 営業時間を秒や分単位で分割したデータを作成して、
EXISTS
で検査する(死ぬほど遅い) - 入退室時刻に+1秒、-1秒を行ったデータを使用して取得する
- ストアドプロシージャを使用する
Discussion