【问题标题】:Database Design: Building Hours with Tenants数据库设计:与租户建立时间
【发布时间】:2012-05-26 11:02:12
【问题描述】:

我正在努力设计一个表示建筑物及其租户开放时间的数据库架构。以下是详细信息/要求:

建筑物:

  • 一般来说,每栋楼的正常营业时间取决于季节(夏季、冬季等)
  • 每栋建筑都有不同的营业时间。
  • 许多节假日、活动等将覆盖正常营业时间

租户:

  • 租户在建筑物内运营,因此应受到建筑时间的限制。
  • 正常时间也取决于季节,但与其建筑物的季节性时间不同。
  • 每个租户都有不同的营业时间

理想情况下,我希望能够查询以下内容:

  • 建筑物或租户现在是否开放
  • 今天的建筑时间是几点
  • 接下来 X 天的建筑时间是多少

到目前为止,我的(未完成的)工作是这三个表,但我仍然无法创建有效的解决方案。

[Season]
id
building_id
title
start_date
end_date

[Schedule]
id
season_id
day_of_week (0-6)
open_time
close_time

[Override]
id
schedule_id
date
is_closed
is_holiday

感谢大家的时间和意见。所有答案都在开发/完善解决方案。 Catcall 存储单个日期的想法是我们开发模型和管理/管理界面的最简单方法。

【问题讨论】:

    标签: database-design database-schema


    【解决方案1】:

    您的桌子设计有了一个很好的开端。

    建造时间(你的模型到目前为止)

    您的[Override] 表可以调整以简化数据维护和查询。

    假设您的业务规则是覆盖总是针对(一整天)一天,那么您希望在季节级别而不是计划级别进行覆盖。这使您不必担心一周中的哪一天会发生覆盖。通过在覆盖表中包含 start_date 和 end_date 列,您也可以更轻松地进行概括。此外,您的标志(is_closedis_holiday)可以简化为单个枚举列(closed_reason)。这将允许您在将来添加新的原因,而无需更改您的架构和查询。

    租户时间(下一步)

    您的租户时间表模型应该与您的建筑时间相似。根据您预计租户时间与构建时间不同的频率,您可以在租户表中包含一个标志以指示 uses_building_schedule。这表明没有租户计划条目。

    对于时间表与他们所居住的建筑物不同的租户,只需使用相同的季节/时间表/覆盖结构。

    由于您有一条规定租户工时必须受建筑工时限制,因此您需要添加程序代码来强制执行此规则。我会在查询时进行,而不是在数据维护时进行。换句话说,在查询租户的营业时间时,我会查看他们是否有营业时间,然后将这些营业时间限制为建筑营业时间。

    【讨论】:

    • Building Hours 下的好建议;所有这些对我来说都很有意义。关于您对 Tenant Hours 的评论,您认为我需要创建三个单独的表,还是应该与构建表结合使用?对于这些限制,我计划使用表单验证(输入时间时)来强制租户时间适应建筑时间。
    • @kml - 要结合建筑物和租户的明细表,您必须反转关系(如 Branko 建议的那样)或在某个新表下的子类型建筑物和租户(即:THINGS_THAT_HAVE_SCHEDULES )。在我看来,这有点过于复杂了。您已经有不同的业务规则,并且您将来很可能为每种类型的日程安排不同的列。我会把这两种时间表分开。它可能不那么“优雅”(即过度设计),但随着时间的推移会更容易打理。
    【解决方案2】:

    我不确定它在多大程度上符合您的要求,但让我提供一个想法:

    该模型具有以下特点:

    • 建筑物或租户有时间表(可能会或可能不会与其他建筑物/租户共享)。
    • 计划是一组间隔。
    • 每个间隔在计划中都有一个优先级:具有较高优先级的间隔“覆盖”具有较低优先级的间隔。看看 SCHEDULE_ITEM 是如何受到约束的,因此任何时间间隔都不能两次属于同一个计划,并且相对于属于同一计划的其他时间间隔不能具有不明确的优先级(UNIQUE 约束U1)。
    • 时间间隔可以在时间表之间共享,例如,您可以设置一个涵盖年度事件的单个时间间隔,然后在多个时间表之间共享。如果事件发生变化,您只需在一处更新即可。

    此模型的问题在于它的处理量很大,并且 DBMS 无法帮助您避免数据中出现一些无意义的情况(您需要在应用程序级别执行此操作)。另一方面,它非常灵活且功能强大。

    在给定日期查找建筑物或租户的小时数并非易事:您需要按照优先级的顺序将时间表下的所有时间间隔相交。要找到给定租户的小时数,您需要先找到她的建筑物的小时数,然后将它们相交。

    示例

    一个时间表......

    • 全年上午 9 点至下午 5 点,
    • 5 月至 10 月的上午 8 点至下午 6 点除外,
    • 周六上午 9 点至下午 1 点除外,
    • 周日没有营业时间
    • 1 月 1 日、7 月 4 日和 12 月 25 日不营业

    ...可以这样表示:

    SCHEDULE_ID     PRIORITY    MONTH_START     MONTH_END       DAY_OF_MONTH_START      DAY_OF_MONTH_END        DAY_OF_WEEK_START       DAY_OF_WEEK_END     HOUR_START      HOUR_END
    1               1           NULL            NULL            NULL                    NULL                    NULL                    NULL                9 AM            5 PM
    1               2           5               10              NULL                    NULL                    NULL                    NULL                8 AM            6 PM
    1               3           NULL            NULL            NULL                    NULL                    6                       6                   9 AM            1 PM
    1               4           NULL            NULL            NULL                    NULL                    7                       7                   NULL            NULL
    1               5           1               1               1                       1                       NULL                    NULL                NULL            NULL
    1               6           7               7               4                       4                       NULL                    NULL                NULL            NULL
    1               7           12              12              25                      25                      NULL                    NULL                NULL            NULL
    

    【讨论】:

      【解决方案3】:

      让我们换个角度看问题。建筑物和租户的营业时间并没有本质上的不同。也就是说,价值可能会有所不同,但建筑物是否开放的想法与租户是否开放并没有本质上的不同。我创建了一个单独的模式“小时”来实现这个想法。

      -- Essentially a supertype, but I couldn't think of a good noun to 
      -- describe it.  So I named the table "x".  It holds all the attributes 
      -- common to both buildings and tenants. (That is, not very much.)
      --
      create table hours.x (
        x_id integer primary key,
        x_name varchar(35) not null,
        x_type char(1) check (x_type in ('b','t')),
        unique (x_id, x_type)
      );
      
      insert into hours.x values 
      (1, 'First building', 'b'),
      (2, 'Second building', 'b'),
      (3, 'First tenant', 't'),
      (4, 'Second tenant', 't'),
      (5, 'Third tenant', 't');
      
      create table hours.buildings (
        bldg_id integer primary key,
        x_type char(1) not null default 'b' check (x_type = 'b'),
        foreign key (bldg_id, x_type) references hours.x (x_id, x_type),
        other_columns char(1) not null default 'x'
      );
      
      insert into hours.buildings (bldg_id) values 
      (1), (2);
      
      create table hours.tenants (
        tenant_id integer primary key,
        x_type char(1) not null default 't' check (x_type = 't'),
        foreign key (tenant_id, x_type) references hours.x (x_id, x_type),
        bldg_id integer not null references hours.buildings (bldg_id),
        other_columns char(1) not null default 'x'
      );
      
      insert into hours.tenants (tenant_id, bldg_id) values
      (3, 1), (4, 1), (5, 2);
      
      
      -- Operating hours records a half open interval [opening_time, closing_time).
      -- If a queried time matches the opening time, the building or tenant is open.
      -- But if it matches the closing time, it's *not* open.  Examples later.
      create table hours.op_hours (
        x_id integer not null references hours.x (x_id),
        opening_time timestamp not null,
        closing_time timestamp not null,
        check (opening_time < closing_time),
        check (
          -- Open and close on the same date,
          ( opening_time::date = closing_time::date ) or
          -- or close at midnight following the opening.
          ( 
            (opening_time::date + interval '1' day = closing_time::date) and 
            (cast(closing_time as time) = '00:00')
          )
        )
      );
      
      insert into hours.op_hours values
      -- Bldg 1 is normally Mon-Sat, 9:00 to 5:00. Closed on Jan 1, holiday
      (1, '2012-01-02 09:00', '2012-01-02 17:00'),
      (1, '2012-01-03 09:00', '2012-01-03 17:00'),
      (1, '2012-01-04 09:00', '2012-01-04 17:00'),
      (1, '2012-01-05 09:00', '2012-01-05 17:00'),
      (1, '2012-01-06 09:00', '2012-01-06 17:00'),
      (1, '2012-01-07 09:00', '2012-01-07 17:00'),
      -- Closed on Jan 8, a Sunday.
      (1, '2012-01-09 09:00', '2012-01-09 17:00'),
      
      -- Bldg 2 is normally Mon-Fri, 7:30 to midnight.
      (2, '2012-01-02 07:30', '2012-01-03 00:00'),
      (2, '2012-01-03 07:30', '2012-01-04 00:00'),
      (2, '2012-01-04 07:30', '2012-01-05 00:00'),
      (2, '2012-01-05 07:30', '2012-01-06 00:00'),
      (2, '2012-01-06 07:30', '2012-01-07 00:00'),
      -- Closed on Jan 7 and 8, weekend.
      (2, '2012-01-09 07:30', '2012-01-10 00:00'),
      
      -- "First" tenant is open 9:00 to noon and 1:00 to 4:00, Mon-Fri.
      (3, '2012-01-02 09:00', '2012-01-02 12:00'),
      (3, '2012-01-03 09:00', '2012-01-03 12:00'),
      (3, '2012-01-04 09:00', '2012-01-04 12:00'),
      (3, '2012-01-05 09:00', '2012-01-05 12:00'),
      (3, '2012-01-06 09:00', '2012-01-06 12:00'),
      (3, '2012-01-02 13:00', '2012-01-02 16:00'),
      (3, '2012-01-03 13:00', '2012-01-03 16:00'),
      (3, '2012-01-04 13:00', '2012-01-04 16:00'),
      (3, '2012-01-05 13:00', '2012-01-05 16:00'),
      (3, '2012-01-06 13:00', '2012-01-06 16:00'),
      
      -- "Second" tenant is open when the building is open.
      (4, '2012-01-02 09:00', '2012-01-02 17:00'),
      (4, '2012-01-03 09:00', '2012-01-03 17:00'),
      (4, '2012-01-04 09:00', '2012-01-04 17:00'),
      (4, '2012-01-05 09:00', '2012-01-05 17:00'),
      (4, '2012-01-06 09:00', '2012-01-06 17:00'),
      (4, '2012-01-07 09:00', '2012-01-07 17:00'),
      -- Closed on Jan 8, a Sunday.
      (4, '2012-01-09 09:00', '2012-01-09 17:00'),
      
      -- "Third" tenant is open Mon-Thu 7:30 to 9:30, Fri until midnight.
      (5, '2012-01-02 07:30', '2012-01-02 21:30'),
      (5, '2012-01-03 07:30', '2012-01-03 21:30'),
      (5, '2012-01-04 07:30', '2012-01-04 21:30'),
      (5, '2012-01-05 07:30', '2012-01-05 21:30'),
      (5, '2012-01-06 07:30', '2012-01-07 00:00'),
      -- Closed on Jan 7 and 8, weekend.
      (5, '2012-01-09 07:30', '2012-01-09 21:30');
      

      应该清楚的是,这种记录每个建筑物和每个租户每天的营业时间的结构将适应任何季节、假期、活动等的定义。可以使用相当简单的存储过程生成默认数据。而且所需的查询非常简单。 (能够看到一些东西并看到它是正确的,这是很有价值的。)

      -- Is building 1 open at 9:00 am on Jan 3? (Queries for tenants are essentially
      -- identical. Returns the id number if it's open, but that could be massaged 
      -- into an "is_open" derived column with Boolean values.)
      select x_id
      from hours.op_hours
      where x_id = 1
        and opening_time <= '2012-01-03 09:00' 
        and '2012-01-03 09:00' < closing_time;
      
      -- How about on Jan 1? (Returns an empty set if closed. See above.)
      select x_id
      from hours.op_hours
      where x_id = 1
        and opening_time <= '2012-01-01 09:00' 
        and '2012-01-01 09:00' < closing_time;
      
      -- Which tenants, regardless of building, are open on Jan 4 at 9:00 am?
      select op_hours.x_id
      from hours.op_hours
      inner join hours.tenants on hours.tenants.tenant_id = hours.op_hours.x_id 
      where opening_time <= '2012-01-04 09:00' 
        and '2012-01-04 09:00' < closing_time;
      

      请注意,这些查询中的每一个都在半开区间上运行。您不能使用 BETWEEN 运算符,因为它在闭合区间上运行。

      这个结构满足哪些要求?

      • 租户营业时间必须是建筑物营业时间的一个子集。除非您的 dbms 支持断言,否则无法以声明方式完成。可以使用数据库中的程序代码来完成。

      【讨论】:

        猜你喜欢
        • 2012-09-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-04-08
        • 1970-01-01
        • 2017-09-16
        • 2019-10-02
        • 1970-01-01
        相关资源
        最近更新 更多