web-dev-qa-db-ja.com

スロットタイムチャレンジ-Doctor Appointmentデータベーススキーマ

私は、オンラインの患者が2つの選択肢を持つ医師の予約システムを作成しようとしています。

  1. 初回訪問(この訪問は20分である必要があります)
  2. フォローアップ訪問(この訪問は10分である必要があります)

制約:

  1. 価格は、最初の/その後の枯れに基づいて異なります
  2. 医者はスロット間に休憩時間があるかもしれません
  3. システムインターフェースは、スロットを予約するときに両方のオプションをサポートします
  4. 最大にするために、スロット間の最小ギャップ時間が必要です。医師の利用状況

これまでに完成したモデルは、次のようなDoctor Slotテーブルの作成に基づいています Doctor Slots Table Schema

DoctorSlotがNでN = 10の場合、以下の例を検討してください。

Case First Time if(N * 2)== Free予約ブロックを1つではなく2つのスロットでブロックする

ケースフォローアップif(N)==無料予約ブロックを1スロット作成

例:医師:9:00から9:10医師:9:10から9:20医師:9:20から9:30

ケース9:00から9:20までの患者への初回ショー

課題:

  • 9:00から9:20までの時間を追加します(医師のスロット間にバッファー時間(医師の休憩)が存在する可能性があります)
  • データベースから1つではなく2つのスロットIDを取得します(このSlotIDは注文で使用されます)
  • ジェネリックモデルを使用する予定のケースに基づいて実行時にユーザーを表示し、それに応じて後で価格を更新する方法
  • ユーザーが最初の時間枠を予約し、次に別のユーザーがフォローアップを予約した場合、ギャップがあり、データベース内のSQL Serverで時間を処理する方法

質問:

  1. 考えられるすべてのシナリオを満たすソリューションを実現するのに最適なデータベーススキーマは何ですか?
  2. SQL Server/ASP.NET POVからの時間エンティティを処理する最良の方法は何ですか?
8
theK

各医師の現在の予定を格納するAppointmentテーブルをお勧めします。このテーブルにいくつかの制約を追加して、アポイントメントの開始時間を10分の時間(たとえば9.00、9.10、9.20)にさえ制限し、EndTimeの後にStartTimeのような他の常識チェックを追加できます。医師は同時に2つの予定を開始することはできません。誰もがワークライフバランスを必要としているため、医師も午前9時から午後5時までしか働かないようにするとします。

CREATE TABLE Appointment (
    DoctorID char(1) NOT NULL,
    [Date] date NOT NULL,
    StartTime time(0) NOT NULL CONSTRAINT CHK_StartTime_TenMinute CHECK (DATEPART(MINUTE, StartTime)%10 = 0 AND DATEPART(SECOND, StartTime) = 0),
    EndTime time(0) NOT NULL CONSTRAINT CHK_EndTime_TenMinute CHECK (DATEPART(MINUTE, EndTime)%10 = 0 AND DATEPART(SECOND, EndTime) = 0),
    Status char(1) NOT NULL,
    UserID char(1) NOT NULL,
    Price int NOT NULL,
    CONSTRAINT PK_Appointment PRIMARY KEY CLUSTERED (DoctorID, [Date], StartTime),
    CONSTRAINT CHK_StartTime_BusinessHours CHECK (DATEPART(HOUR, StartTime) > = 9 AND DATEPART(HOUR, StartTime) < = 16),
    CONSTRAINT CHK_EndTime_BusinessHours CHECK (DATEPART(HOUR, EndTime) > = 9 AND DATEPART(HOUR, DATEADD(SECOND, -1, EndTime)) < = 16),
    CONSTRAINT CHK_EndTime_After_StartTime CHECK (EndTime > StartTime));
CREATE INDEX iDoctor_End ON Appointment (DoctorID, [Date], EndTime);

このテーブルにデータを挿入して、どのように見えるかを確認できます。 3番目の挿入は、制約によって妨げられているため失敗します。医師は、2つの予定を同時に開始することはできません。

INSERT INTO Appointment VALUES ('A', '20170420', '09:00:00', '09:10:00', 'P', '1', '0');
INSERT INTO Appointment VALUES ('A', '20170420', '09:20:00', '09:40:00', 'C', '2', '10');
INSERT INTO Appointment VALUES ('A', '20170420', '09:00:00', '09:20:00', 'C', '2', '10');

数値テーブルがあるとしましょう。あなたが他の多くの人がそれを作成する方法を説明していない場合。他のすべてが失敗した場合、これはあなたのためにそれを作成する可能性がありますが、それはおそらく最良の方法ではありません。

CREATE TABLE Numbers (Number int PRIMARY KEY CLUSTERED);
DECLARE @number int = 0;
WHILE @number < 1000
BEGIN
    INSERT INTO Numbers VALUES (@number);
    SET @number += 1;
END 

ここで、特定のドクターの空きスロットを確認したい場合は、必要なのは、どのドクターか、およびスロットが探している長さを指定することだけです。

DECLARE @doctorID char(1) = 'A';
DECLARE @length tinyint = 20;
WITH Slots AS (
    SELECT StartTime = DATEADD(MINUTE, ((DATEPART(MINUTE, GETDATE())/10)+1+Number)*10, DATEADD(HOUR, DATEPART(HOUR, GETDATE()), CONVERT(smalldatetime, CONVERT(date, GETDATE())))),
           EndTime = DATEADD(MINUTE, @length, DATEADD(MINUTE, ((DATEPART(MINUTE, GETDATE())/10)+1+Number)*10, DATEADD(HOUR, DATEPART(HOUR, GETDATE()), CONVERT(smalldatetime, CONVERT(date, GETDATE())))))
      FROM Numbers)
SELECT TOP 15 DoctorID = @doctorID,
    s.StartTime,
    s.EndTime
  FROM Slots AS s
  WHERE NOT EXISTS (SELECT 1 
                      FROM Appointment AS a
                      WHERE (CONVERT(time(0), s.StartTime) < a.EndTime AND CONVERT(time(0), s.EndTime) > a.StartTime)
                        AND a.DoctorID = @doctorID
                        AND a.[Date] = CONVERT(date, s.StartTime))
    AND DATEPART(HOUR, s.StartTime) >= 9
    AND DATEPART(HOUR, DATEADD(MINUTE, -1, s.EndTime)) <= 16
ORDER BY s.StartTime;

それは少し厄介に見えますので、誰かがその日付ロジックを改善して提案を喜んで受け入れることができれば。

医師が休憩を希望する場合は、予約として休憩を入力すると予約できなくなります。

テーブルの制約は重複しない予定を強制しないことに注意してください。これは可能ですが、より複雑です。これが私のシステムである場合、最終的にアポイントメントが挿入時に既存のアポイントメントと重複しないことを確認するために、いくつかのシステム(トリガーなど)について考えますが、それはあなた次第です。

5
mendosi

以下は、MariaDB/MySQL用の mendosi のコードの機能的に同等な(およびIMOの方が読みやすい)バージョンです。

Numbersテーブルがない場合は作成します。

CREATE TABLE Numbers (number INT UNSIGNED PRIMARY KEY);

DELIMITER //

CREATE PROCEDURE populateNumbers()
BEGIN
    SET @x = 0;
    WHILE @x < 1024 DO
        INSERT INTO Numbers VALUES (@x);
        SET @x = @x + 1;
    END WHILE;
    SET @x = NULL;
END; //

DELIMITER ;

CALL populateNumbers;
DROP PROCEDURE populateNumbers;

Appointmentテーブルの十分なスキーマを以下に示します。すぐに、INSERTトリガーも追加して、新しいエントリが既存のエントリと競合しないようにします。

CREATE TABLE Appointment (
    doctorID    INT UNSIGNED    NOT NULL,
    `date`      DATE            NOT NULL,
    startTime   TIME(0)         NOT NULL,
    endTime     TIME(0)         NOT NULL,

    CONSTRAINT PRIMARY KEY (doctorID, `date`, startTime),

    CONSTRAINT mustStartOnTenMinuteBoundary CHECK (
        EXTRACT(MINUTE FROM startTime) % 10 = 0
        AND EXTRACT(SECOND FROM startTime) = 0
    ),
    CONSTRAINT mustEndOnTenMinuteBoundary CHECK (
        EXTRACT(MINUTE FROM endTime) % 10 = 0
        AND EXTRACT(SECOND FROM endTime) = 0
    ),
    CONSTRAINT cannotStartBefore0900 CHECK (
        EXTRACT(HOUR FROM startTime) >= 9
    ),
    CONSTRAINT cannotEndAfter1700 CHECK (
        EXTRACT(HOUR FROM (startTime - INTERVAL 1 SECOND)) < 17
    ),
    CONSTRAINT mustEndAfterStart CHECK (
        endTime > startTime
    )
);

まず、特定のタイムスロットを新しい予定として割り当てることができるかどうかを決定する関数を定義します。

DELIMITER //

CREATE FUNCTION slotIsAvailable(
    doctorID            INT,
    slotStartDateTime   DATETIME,
    slotEndDateTime     DATETIME
) RETURNS BOOLEAN NOT DETERMINISTIC
BEGIN
    RETURN CASE WHEN EXISTS (
        -- This table will contain records iff the slot clashes with an existing appointment
        SELECT TRUE
        FROM Appointment AS a
        WHERE
                CONVERT(slotStartDateTime, TIME) < a.endTime   -- These two conditions will both hold iff the slot overlaps
            AND CONVERT(slotEndDateTime,   TIME) > a.startTime -- with the existing appointment that it's being compared to
            AND a.doctorID = doctorID
            AND a.date = CONVERT(slotStartDateTime, DATE)
    ) THEN FALSE ELSE TRUE
    END;
END; //

DELIMITER ;

次に、衝突する予定が保存されないようにするために、前述のINSERTトリガーを示します。

DELIMITER //

CREATE TRIGGER ensureNewAppointmentsDoNotClash
    BEFORE INSERT ON Appointment
    FOR EACH ROW
BEGIN
    IF NOT slotIsAvailable(
        NEW.doctorID,
        CAST( CONCAT(NEW.date, ' ', NEW.startTime)  AS DATETIME ),
        CAST( CONCAT(NEW.date, ' ', NEW.endTime)    AS DATETIME )
    ) THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Appointment clashes with an existing appointment!';
    END IF;
END; //

DELIMITER ;

Appointmentテーブルが適切に設定されたので、いくつかの有効なサンプルエントリを挿入できます。

INSERT INTO Appointment VALUES (1, '2019-10-06', '09:20', '09:30');
INSERT INTO Appointment VALUES (1, '2019-10-06', '09:40', '09:50');
INSERT INTO Appointment VALUES (1, '2019-10-06', '11:00', '11:20');
INSERT INTO Appointment VALUES (1, '2019-10-06', '11:20', '11:40');
INSERT INTO Appointment VALUES (1, '2019-10-06', '11:40', '12:00');
INSERT INTO Appointment VALUES (1, '2019-10-06', '13:00', '14:00');
INSERT INTO Appointment VALUES (1, '2019-10-06', '16:00', '16:40');

無効な予定エントリを挿入しようとすると、ensureNewAppointmentsDoNotClashトリガーの結果としてエラーがスローされます。実際、このトリガーは、主キー制約がチェックされる前でもエラーをスローするため、冗長と見なされる可能性があります。私のソリューションでは、複合主キーを使用するのではなく、AppointmentテーブルのIDフィールドを選択しました。

ここで、指定されたドクターを使用して、指定された長さの利用可能なタイムスロットの結果セットを取得する手順を示します。以前に定義し、slotIsAvailableトリガーでも使用したINSERT関数を使用していることに注意してください。

-- The ID of the doctor to book the appointment with.
SET @doctorID = 1;

-- The moment from which to start searching for availble time slots
SET @searchStart = CURRENT_TIMESTAMP;

-- The duration of the appointment to book, in minutes.
SET @duration = 20;

WITH
    SlotStart AS (
        -- This table will list all the 10-minute-aligned timestamps that occur after `@searchStart`
        SELECT
            CONVERT(@searchStart, DATE)
            + INTERVAL (EXTRACT(HOUR FROM @searchStart)) HOUR
            + INTERVAL ( EXTRACT(MINUTE FROM @searchStart) DIV 10 + number + 1 ) * 10 MINUTE
        AS startDateTime
        FROM Numbers
    ),
    Slot AS (
        SELECT
            startDateTime,
            startDateTime + INTERVAL @duration MINUTE   AS endDateTime
        FROM SlotStart
    ),
    AvailableSlot AS (
        SELECT
            @doctorID   AS doctorID,
            startDateTime,
            endDateTime
        FROM Slot AS s
        WHERE
                slotIsAvailable(@doctorID, s.startDateTime, s.endDateTime)
            AND EXTRACT(HOUR FROM s.startDateTime) >= 9
            AND EXTRACT(HOUR FROM (s.endDateTime - INTERVAL 1 MINUTE)) <= 16
    )
SELECT *
    FROM AvailableSlot
    WHERE
            CONVERT(startDateTime, DATE) = CONVERT(@searchStart, DATE)
        AND CONVERT(endDateTime,   DATE) = CONVERT(@searchStart, DATE)
    ORDER BY startDateTime ASC;

上記のクエリは、上記のAppointmentのサンプルレコードと@searchStart に等しい '2019-10-06 06:00'、利回り:

+----------+---------------------+---------------------+
| doctorID | startDateTime       | endDateTime         |
+----------+---------------------+---------------------+
|        1 | 2019-10-06 09:00:00 | 2019-10-06 09:20:00 |
|        1 | 2019-10-06 09:50:00 | 2019-10-06 10:10:00 |
|        1 | 2019-10-06 10:00:00 | 2019-10-06 10:20:00 |
|        1 | 2019-10-06 10:10:00 | 2019-10-06 10:30:00 |
|        1 | 2019-10-06 10:20:00 | 2019-10-06 10:40:00 |
|        1 | 2019-10-06 10:30:00 | 2019-10-06 10:50:00 |
|        1 | 2019-10-06 10:40:00 | 2019-10-06 11:00:00 |
|        1 | 2019-10-06 12:00:00 | 2019-10-06 12:20:00 |
|        1 | 2019-10-06 12:10:00 | 2019-10-06 12:30:00 |
|        1 | 2019-10-06 12:20:00 | 2019-10-06 12:40:00 |
|        1 | 2019-10-06 12:30:00 | 2019-10-06 12:50:00 |
|        1 | 2019-10-06 12:40:00 | 2019-10-06 13:00:00 |
|        1 | 2019-10-06 14:00:00 | 2019-10-06 14:20:00 |
|        1 | 2019-10-06 14:10:00 | 2019-10-06 14:30:00 |
|        1 | 2019-10-06 14:20:00 | 2019-10-06 14:40:00 |
|        1 | 2019-10-06 14:30:00 | 2019-10-06 14:50:00 |
|        1 | 2019-10-06 14:40:00 | 2019-10-06 15:00:00 |
|        1 | 2019-10-06 14:50:00 | 2019-10-06 15:10:00 |
|        1 | 2019-10-06 15:00:00 | 2019-10-06 15:20:00 |
|        1 | 2019-10-06 15:10:00 | 2019-10-06 15:30:00 |
|        1 | 2019-10-06 15:20:00 | 2019-10-06 15:40:00 |
|        1 | 2019-10-06 15:30:00 | 2019-10-06 15:50:00 |
|        1 | 2019-10-06 15:40:00 | 2019-10-06 16:00:00 |
|        1 | 2019-10-06 16:40:00 | 2019-10-06 17:00:00 |
+----------+---------------------+---------------------+
1
Jivan Pal

まず、なぜあなたのタイムスロットは固定長なのですか?現在のデザインでも、start_time = 9:00およびend_time = 9:20のスロットを使用でき、IDを1つにすることができます。さらに、次のようにブリッジテーブルを作成して、試験のタイプ(タイプ1 =最初の試験、タイプ2 =対照試験など)をスロットの長さに接続できます。
exam_type slot_length_in_minutes
1 20
2 10
3 15
この場合、試験の時間を予約するときに、UIである種の選択ボックス(コンボボックス)から試験の種類を選択し、適切なサイズの空きスロットを検索します。

また、予約テーブルの構造を教えてくれなかったので、それをお手伝いします。しかし、私はあなたがそこにある種のタイムスロットIDを持っていると思います、そしてまた選択された医者のIDも...?その場合、タイムスロットと予約に参加するだけで、完全なタイムテーブルを表示できます。お役に立てば幸いです。そうでない場合は、遠慮なく質問してください。

0
Miloš