28

我正在开发一个律师预约系统,一个人可以在给定日期的给定时间(下一个律师有空的日子)预约。

假设它是针对律师的 ZocDoc。相同的结构,基于时间的约会:http: //goo.gl/djUZb

我正在使用 MySQL 和 PHP。


表架构:

CREATE TABLE `laywer_appointments` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `lawyer_id` INT unsigned,
  `day_of_week` tinyint(3) unsigned DEFAULT '1',
  `slot_date` date DEFAULT NULL,
  `slot_time` time DEFAULT NULL,
  `status` tinyint(4) NOT NULL DEFAULT '0',
  `client_id` int(11) DEFAULT NULL, -- client_id = NULL means free slot
);

第 1 点)

每个律师都有基于星期几的默认时间段(状态 = 0 表示可用)。插入默认插槽时,我不提供日期,只提供 day_of_week。示例数据:

+-----------+-------------+-----------+-----------+
| lawyer_id | day_of_week | slot_time | status    |
+-----------+-------------+-----------+-----------+
| 1         | 1           | 08:00     | 0         |
| 1         | 1           | 08:30     | 0         |
| 1         | 1           | 09:00     | 0         |
| 1         | 1           | 10:30     | 0         |
| 1         | 4           | 14:30     | 0         |
| 1         | 4           | 16:40     | 0         |
| 2         | 1           | 10:20     | 0         |
| 2         | 1           | 14:00     | 0         |
| 2         | 3           | 15:50     | 0         |
+-----------+-------------+-----------+-----------+

第 2 点)

律师可以为特定日期添加时间段即使这一天与他的默认时间段不同,也可以锁定状态=-1)特定日期的默认时间段之一(即他正在开会或生病):

+-----------+-------------+-----------+-----------+-----------+
| lawyer_id | day_of_week | slot_time | slot_date | status    |
+-----------+-------------+-----------+-----------+-----------+
| 1         | 1           | 16:00     | 12/03/13  | 0         |
| 1         | 6           | 11:00     | 26/04/13  | 0         |
| 1         | 6           | 12:00     | 26/04/13  | 0         |
| 2         | 1           | 10:00     | 01/01/13  | -1        |
+-----------+-------------+-----------+-----------+-----------+

第 3 点)

然后我们预约了。在这种情况下,我们填写 slot_date 和 client_id:

+-----------+-------------+-----------+-----------+-----------+
| lawyer_id | day_of_week | slot_time | slot_date | client_id |
+-----------+-------------+-----------+-----------+-----------+
| 1         | 1           | 10:30     | 12/03/13  | 10        |
+-----------+-------------+-----------+-----------+-----------+

例如,通过上述预订并假设仍然是同一天(13 年 12 月 3 日)的 6:30,必须打印的可用空位为:

8:00 - default slot
8:30 - default slot
9:00 - default slot
16:00 - Specific slot inserted in point 2 for 12/03/13

问题:

我必须返回下一个可用日期和相关的空闲时间(默认的、特定的减去锁定的和预订的)。我不能只说“从 2013 年 10 月 10 日星期一开始的返回时间”。

在搜索结果页面中,我将列出所有律师以及每个. 因此,这意味着每次进行搜索时,每位律师都会有不同的时间表。

我不能简单地说“SELECT time FROM [bunch of joins] WHERE date = today”

我提出了这个查询,它忽略了锁定(状态 = -1)或预订(client_id 不为空)的插槽,但当然它不会返回最近一天的空闲时间以及可用时间(或从今天开始):

SELECT p.day_of_week, p.slot_date, p.slot_time
FROM laywer_appointments p
WHERE p.client_id IS NULL AND p.status = 0
     AND p.slot_time NOT IN (
              SELECT s.slot_time FROM laywer_appointments s
              WHERE (s.slot_date IS NOT NULL AND s.client_id IS NOT NULL 
              OR s.status = -1) AND s.day_of_week = p.day_of_week
     )
GROUP BY p.day_of_week, p.slot_date, p.slot_time
ORDER BY p.day_of_week ASC, p.slot_time ASC;

另一个问题:如果今天是 day_of_week = 5,但给定律师的下一个可用 day_of_week 是 2,我该如何查询?

如何返回下一个最接近且可用的 day_of_week 并聚合为从这一天开始的返回时间,而不是所有天?

一种可能的解决方案

我带来的一件事是创建 3 个表而不是一个:

  • default_slots:3 列:lawyer_id、day_of_week、time
  • 插槽:laywer_id、day_of_week、时间、日期、状态
  • 约会:有关预约的所有信息

然后,我会将实际日期的每一天的所有空闲时间段存储在每个律师的时间段表中,最多一年。(取自 default_slots 的时隙)。

+-----------+-------------+-----------+-----------+-----------+
| lawyer_id | day_of_week | slot_time | slot_date | status    |
+-----------+-------------+-----------+-----------+-----------+
| 1         | 1           | 16:00     | 12/03/13  | 0         |
| 1         | 1           | 16:00     | 12/03/13  | 0         |
| 1         | 2           | 08:00     | 13/03/13  | 0         |
| 1         | 2           | 09:00     | 13/03/13  | 0         |
... next week
| 1         | 1           | 16:00     | 19/03/13  | 0         |
| 1         | 1           | 16:00     | 19/03/13  | 0         |
| 1         | 2           | 08:00     | 20/03/13  | 0         |
| 1         | 2           | 09:00     | 20/03/13  | 0         |
... up to an year
| 1         | 1           | 16:00     | 20/03/14  | 0         |
| 1         | 1           | 16:00     | 20/03/14  | 0         |
| 1         | 2           | 08:00     | 21/03/14  | 0         |
| 1         | 2           | 09:00     | 21/03/14  | 0         |
+-----------+-------------+-----------+-----------+-----------+

我还将有一些每周运行的 cron 作业,在表槽中添加另一周的空闲槽记录,并删除过去的记录以减少表大小和未使用的数据。

律师还可以将时间直接锁定到插槽中,以及添加特定时间(第 2 点)。

对于列表,这将是获得等于或大于今天的空闲时间的日期的插槽的问题,因为每个日期的每个时间都会有一行

对这个解决方案的影响: 1)第一天我们将有 2500 名律师(第二个月大约 6000 名)。假设 8 个可能的时段/每天 X 20 天工作/月 X 12 个月 =每位律师 1920 条时段记录。

2500 层 x 1920 条记录 = 第一天的 480 万条记录。(第二个月~12M)

这些记录将一直被更新、插入和删除。slot 表有一些索引,所以我无法想象在一个有 12M+ 记录和一些索引的表上不断进行写操作。每秒更新的索引对我来说并不明智。

我真的无法提供合理且可扩展的解决方案。我的解决方案只有一张表可以工作,但我根本想不出查询的方式。并且非规范化的槽表将是巨大的,同时需要不断的写入操作。

有小费吗?

4

2 回答 2

22

我做了与您尝试做的类似的事情,所以我了解它有多复杂:)

这是在 MSSQL 中完成的,因此您需要将其转换为 MySql。

约会的例子

这些是我们最终得到的表格:

时隙:

我们将每个员工的默认时隙和修改后的时隙都存储在此表中(我们在此表中有一个名为“SlotType”的列。SlotType 1 = DEFAULT TIMESLOTS & SlotType 2 = MODIFIED TIMESLOTS)。如果您查看上图中的“2013 年 4 月 30 日星期二”,您会看到我们修改了当天的时间段,只显示该特定工作人员的上午 9 点预约。

休息日:

这是关闭日的列表 - 例如,一名工作人员在他的生日和圣诞节不工作。

约会:

这是已预订(或等待预订确认)的约会列表。

获取可用约会的 SQL 查询:

为了检查约会,我们在存储过程中使用了以下 SQL。它检查指定日期的一名工作人员的任命。我们使用的最终存储过程在一周中的每一天通过页面上的每个员工循环来获取所有约会。使用此查询来获取未来 7 天的 10 名员工约会 = 总共 70 次查询 & 大约需要 300 毫秒,每个表中有 100 万条记录。我们正在通过 ajax 加载约会,因此我们可以使用 300 毫秒,并且可能会更改它以通过 ajax 分别获取每个员工的约会(因此一次 7 个查询)以在未来进一步提高性能。

DECLARE @MyDate date, @MyDayName nvarchar(10);
IF @StartDate IS NULL
    SET @StartDate = GETDATE();
SET @MyDate = CAST(@StartDate AS date);
SET @MyDayName = DATENAME(dw, @MyDate );

--NOTES:
--@SlotType = 1 (DEFAULT TIMESLOTS), 2 (MODIFIED TIMESLOTS)

    --***CHECK TO SEE IF DOCTOR IS CLOSED TODAY***
    IF NOT EXISTS (SELECT [ClosedDays].[ID] FROM [ClosedDays] WHERE [ClosedDays].[StaffID] = @StaffID AND [ClosedDays].[BusinessID] = @BusinessID AND [ClosedDays].[Active] = 1 AND @MyDate BETWEEN [ClosedDays].[StartDate] AND [ClosedDays].[EndDate])
    BEGIN
        --***THE DOCTOR IS NOT CLOSED TODAY SO GET THE AVAILABLE TIMESLOTS***
        --***CHECK TO SEE IF DOCTOR IS HAS MODIED TIMESLOTS TODAY***
        IF NOT EXISTS (SELECT [TimeSlots].[ID], @MyDate AS SlotDate FROM [TimeSlots] WHERE  [TimeSlots].[StaffID] = @StaffID AND [TimeSlots].[BusinessID] = @BusinessID AND [TimeSlots].[Active] = 1 AND [TimeSlots].[SlotType] = 2 AND [TimeSlots].[SlotDay] = @MyDayName AND @MyDate BETWEEN [TimeSlots].[StartDate] AND [TimeSlots].[EndDate] AND [TimeSlots].[ID] NOT IN (SELECT [Appointments].[TimeSlotID] FROM [Appointments]) )
            BEGIN
                --***THE DOCTOR HAS NO MODIFIED TIMESLOTS FOR TODAY USE THE DEFAULT ONES***
                SELECT [TimeSlots].[ID] AS SlotID, [TimeSlots].[StaffID], [TimeSlots].[BusinessID], CONVERT(nvarchar(10), @MyDate, 103) AS SlotDate, [TimeSlots].[SlotDay], LTRIM(RIGHT(CONVERT(nvarchar(10), [TimeSlots].[SlotTime], 100), 7))AS SlotTime FROM [TimeSlots]  
                WHERE  [TimeSlots].[StaffID] = @StaffID AND [TimeSlots].[BusinessID] = @BusinessID AND [TimeSlots].[Active] = 1 AND [TimeSlots].[SlotType] = 1 AND [TimeSlots].[SlotDay] = @MyDayName AND @MyDate BETWEEN [TimeSlots].[StartDate] AND [TimeSlots].[EndDate] AND NOT EXISTS (SELECT [Appointments].[TimeSlotID] FROM [Appointments] WHERE [Appointments].[TimeSlotID] = [TimeSlots].[ID])
            END
            ELSE
            BEGIN
                --***THE DOCTOR HAS MODIFIED TODAYS TIMESLOTS SO USE THE MODIFIED TIMESLOTS***
                SELECT [TimeSlots].[ID] AS SlotID, [TimeSlots].[StaffID], [TimeSlots].[BusinessID], CONVERT(nvarchar(10), @MyDate, 103) AS SlotDate, [TimeSlots].[SlotDay], LTRIM(RIGHT(CONVERT(nvarchar(10), [TimeSlots].[SlotTime], 100), 7))AS SlotTime FROM [TimeSlots]  
                WHERE  [TimeSlots].[StaffID] = @StaffID AND [TimeSlots].[BusinessID] = @BusinessID AND [TimeSlots].[Active] = 1 AND [TimeSlots].[SlotType] = 2 AND [TimeSlots].[SlotDay] = @MyDayName AND @MyDate BETWEEN [TimeSlots].[StartDate] AND [TimeSlots].[EndDate] AND NOT EXISTS (SELECT [Appointments].[TimeSlotID] FROM [Appointments] WHERE [Appointments].[TimeSlotID] = [TimeSlots].[ID])
            END
    END
    ELSE
    BEGIN
            --***NO APPOINTMENTS WERE FOUND***
            --***DUMMY QUERY TO RETURN NO RECORDS***
            SELECT [TimeSlots].[ID] AS SlotID, [TimeSlots].[StaffID], [TimeSlots].[BusinessID], CONVERT(nvarchar(10), @MyDate, 103) AS SlotDate, [TimeSlots].[SlotDay], LTRIM(RIGHT(CONVERT(nvarchar(10), [TimeSlots].[SlotTime], 100), 7))AS SlotTime FROM [TimeSlots]  
            WHERE  [TimeSlots].[ID] = -0
    END

希望这是有道理的,如果其他人对如何优化它有一些想法,请告诉我!

于 2013-04-27T23:37:26.680 回答
3

你是对的,你会有一张大桌子。但尚不清楚您的应用程序是否会因此而失败。MySQL(和所有 DBMS 软件)旨在允许快速访问大型表。

良好的专用 MySQL 服务器硬件(具有 64 位操作系统、两个或四个快速处理器、大量 RAM 和出色的文件 I/O——SAS 接口快速磁盘)和正确配置的服务器软件将处理此工作负载。

您可能希望将 slot_time 和 slot_date 合并到单个 DATETIME 或 TIMESTAMP 字段中,可以对其进行索引以便于搜索。如果您选择使用 TIMESTAMP 数据项,如果您做对了,您将获得一些不错的时区处理优势。

您可能想弄清楚如何使用一种方案对大表进行分区,该方案允许您在该月或一周过去后离线获取一个月甚至一周的数据。

有 2,500 名律师在使用您的系统,您会想把这件事做好。为什么不花点钱找一个像样的数据库管理员呢?他们每小时的费用低于大多数律师。Sheeri Cabral 写了一篇关于如何找到的很好的总结。http://www.sheeri.org/how-to-find-a-dba/

于 2013-03-26T01:27:05.777 回答