记录和检索时间相关值的最佳方法

cpc*_*des 7 database-design sql-server sql-server-2014

我有有一张桌子交易总线(登机乘客)。给定路线 ID日期,我需要在另一个表中查找当天正在执行的服务类型巴士时刻表最多每 6 个月左右更改一次,大多数年份保持不变。

目前调度表的定义如下:

CREATE TABLE [dbo].[Routes](
    [ID] [int] NOT NULL,
    [RouteID] [int] NOT NULL,
    [Type] [varchar](50) NOT NULL,
    [StartDate] [datetime] NOT NULL,
PRIMARY KEY CLUSTERED 
(
    [ID] ASC
));
Run Code Online (Sandbox Code Playgroud)

一个示例可能如下所示:

ID  RouteID  Type          StartDate
--  -------  ------------  ----------
 1      301  Standard      2015-01-01
 2      301  Discontinued  2016-06-01
 3      302  Standard      2015-01-01
 4      302  ParaTrans     2017-01-01
Run Code Online (Sandbox Code Playgroud)

所以,如果我有一个2015-04-20RouteID 301交易,我想取回“标准”,但如果交易是从2018-01-20 开始,它应该返回“停止”。对于2015-01-01之前的交易,它应该返回 NULL(或“”,或任何可能与有效答案冲突的结果,即“标准”、“Paratrans”或“停止”)。

基本上,该表表示路线3012015-01-012016-05-31之间的标准路线(因此该期间的任何交易都应归类为“标准”),然后于2016-06-停止01(通过当天,隐式,因为没有注意到以后的时间表更改),而302是从2015-01-012016-12-31的标准路线,然后是 ParaTrans(it) 路线。

Route   Type          Start       End
-----   ----          -----       ---
301
        Standard      2015-01-01  2016-05-31
        Discontinued  2016-06-01  Present
302
        Standard      2015-01-01  2016-12-31
        ParaTrans     2017-01-01  Present
Run Code Online (Sandbox Code Playgroud)

目前,执行此操作的查询如下所示:

SELECT
    TRANSIT_DAY, 
    ROUTE_ID, 
    (SELECT TOP (1) Type FROM Routes
     WHERE (RouteID = dbo.DAILY_SALES_DETAIL.ROUTE_ID) 
     AND (StartDate <= dbo.DAILY_SALES_DETAIL.TRANSIT_DAY)
     ORDER BY StartDate DESC) AS NCTD_MODE 
FROM dbo.DAILY_SALES_DETAIL
Run Code Online (Sandbox Code Playgroud)

问题

我想知道的是:这是(a)Routes表结构和(b)查询的最有效组合以实现此结果吗?换句话说,是否可以对现有结构使用更有效的查询?更改路由表是否可以实现更高效的查询?

注意事项

交易表每天从供应商处导入,因此更改该表的架构并非易事,最好避免。更重要的是,这种查找使用来自多个供应商的事务或其他总线相关数据跨多个表和数据库使用;这只是一个例子。我们有一个供应商(因此有一个数据库)用于货币交易,另一个用于乘客人数,还有另一个用于性能,等等,路线编号和日期是唯一可靠一致的标识符。

路由表的索引为(RouteID, StartDate)。目前Route表有56行,事务表有26M行。路由表由45条路由组成,目前没有超过2行或1条变化的路由。一条路线可以有多少变化没有限制,但我包括这个统计数据是为了表明在可预见的未来,这个数字可能仍然很小。

我可以添加任何必需的索引以优化建议的查询。问题更多是关于找到最佳策略,假设对所考虑的策略进行了所有合理的优化,而不是找到特定策略的最佳优化。


db<>在这里摆弄

Han*_*non 5

您可以通过将dbo.Routes表更改为以下内容来提高设置的性能,如您的问题所示:

CREATE TABLE dbo.Routes(
      RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , CONSTRAINT PK_Routes
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate DESC)
) WITH (DATA_COMPRESSION = PAGE)
ON [PRIMARY];
Run Code Online (Sandbox Code Playgroud)

这里的关键是我们在和的复合上定义聚集索引,也就是表。这以对您编写的查询最有效的方式提供数据。此处需要注意的是,为具有新日期的现有路由插入将导致页面拆分发生,因为我们将按日期降序填充行。话虽如此,Route 表中的行数很少,而且偶尔会有索引维护,这应该不是什么大问题。RouteIDStartDate DESCdbo.Routes

而不是这样做,我会考虑修改dbo.Routes表以包含一EndDate列。这消除了使用TOP(1)和执行子查询的需要ORDER BY ...。就像是:

CREATE TABLE dbo.Routes(
      RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , EndDate datetime NOT NULL
    , CONSTRAINT PK_Routes
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate ASC)
);
Run Code Online (Sandbox Code Playgroud)

请注意,聚集索引现在位于(RouteID, StartDate ASC)

查询现在可以使用INNER JOIN, 而不是相关子查询,并且看起来像:

SELECT
      t.TRANSIT_DAY
    , t.ROUTE_ID
    ,  NCTD_MODE = r.Type 
FROM Transactions t
    INNER JOIN dbo.Routes r ON t.ROUTE_ID = r.RouteID 
        AND t.TRANSIT_DAY >= r.StartDate 
        AND t.TRANSIT_DAY < r.EndDate
ORDER BY t.TRANSIT_DAY
    , t.ROUTE_ID;
Run Code Online (Sandbox Code Playgroud)

这允许 SQL Server 执行简单的内部循环连接以获得结果。当然,如果您要返回大量行,则需要进行大量排序,这可能会溢出到 tempdb。

使用我在下面展示的 MCVE,我们可以比较这两种变体的计划。第一个计划是带有相关子查询的原始查询。第二个计划是包含EndDate列。

在此处输入图片说明

在此处输入图片说明

第二个变体的计划成本比第一个变体低约 4 倍。两个计划中的排序运算符都请求 108MB 内存并将超过 9,000 页溢出到 tempdb - 但是您不太可能请求整个结果集而不是获取单个路由,或者可能是日期范围。如果为单个路由添加过滤器,则不会有大量内存授予或溢出到 tempdb。

以下是具有 10,000 个路由行和 1,000,000 个事务行的示例MCVE,可用于针对各种设计运行测试:

在 tempdb 中执行此操作以避免任何真实表的“事故”。

USE tempdb;
Run Code Online (Sandbox Code Playgroud)

删除表(如果存在)(这适用于 SQL Server 2016+):

DROP TABLE IF EXISTS dbo.Routes;
DROP TABLE IF EXISTS dbo.Transactions;
Run Code Online (Sandbox Code Playgroud)

创建dbo.Routes表,聚簇索引位于RouteID, StartDate DESC

CREATE TABLE dbo.Routes(
        RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , CONSTRAINT PK_Routes
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate DESC)
);
Run Code Online (Sandbox Code Playgroud)

插入 10,000 个路由行:

;WITH src AS (
    SELECT t.n
    FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9))t(n)
)
, src2 AS (
SELECT RouteID = (s1.n * 1000) + (s2.n * 100) + (s3.n * 10)
    , Type = REPLICATE(CHAR(65 + CONVERT(int, CRYPT_GEN_RANDOM(1) % 26)), 50)
FROM src s1
    CROSS JOIN src s2
    CROSS JOIN src s3
    CROSS JOIN src s4
)
INSERT INTO dbo.Routes (RouteID, [Type], StartDate)
SELECT s.RouteID
    , s.Type
    , StartDate = DATEADD(DAY, ROW_NUMBER() OVER (PARTITION BY RouteID ORDER BY s.RouteID) - 1, '1997-01-01T00:00:00')
FROM src2 s
Run Code Online (Sandbox Code Playgroud)

创建dbo.Transactions, 上的聚集索引ROUTE_ID, TRANSIT_DAY。像这样构建聚集索引可以优化对路线和日期进行过滤的查询。

CREATE TABLE dbo.Transactions(
     TRANSIT_DAY datetime NOT NULL
    , ROUTE_ID int NOT NULL
    , CONSTRAINT PK_Transactions
        PRIMARY KEY CLUSTERED
        (ROUTE_ID, TRANSIT_DAY)
);
Run Code Online (Sandbox Code Playgroud)

dbo.Transactions表中插入 1,000,000 行:

;WITH src AS (
    SELECT t.n
    FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9))t(n)
)
INSERT INTO dbo.Transactions (TRANSIT_DAY, ROUTE_ID)
SELECT DATEADD(DAY, CONVERT(int, CRYPT_GEN_RANDOM(1)), '1997-01-01') + DATEADD(MILLISECOND, ABS(CONVERT(int, CRYPT_GEN_RANDOM(4))), '00:00:00')
    , r.RouteID
FROM dbo.Routes r
CROSS JOIN src s1
CROSS JOIN src s2
Run Code Online (Sandbox Code Playgroud)

对于Routes带有EndDate可用于比较测试的列的表,我使用了这个:

CREATE TABLE dbo.RoutesEndDate(
      RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , EndDate datetime NOT NULL
    , CONSTRAINT PK_RoutesEndDate
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate ASC)
);

INSERT INTO dbo.RoutesEndDate (RouteID, [Type], StartDate, EndDate)
SELECT r.RouteID
    , R.Type
    , R.StartDate
    , EndDate = COALESCE(LEAD(r.StartDate) OVER (PARTITION BY r.RouteID ORDER BY r.StartDate), GETDATE())
FROM dbo.Routes r
Run Code Online (Sandbox Code Playgroud)

查询特定路由的两个表:

SELECT
      t.TRANSIT_DAY
    , t.ROUTE_ID
    ,  NCTD_MODE = (
        SELECT TOP (1) Type 
        FROM Routes r
        WHERE (r.RouteID = t.ROUTE_ID) AND (r.StartDate <= t.TRANSIT_DAY)
        ORDER BY r.StartDate DESC
        ) 
FROM Transactions t
WHERE t.ROUTE_ID = 750
ORDER BY t.TRANSIT_DAY
    , t.ROUTE_ID;
Run Code Online (Sandbox Code Playgroud)

上述查询的计划:

在此处输入图片说明

I/O 和时间统计:

表“路线”。扫描计数1000,逻辑读2142,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。
表“交易”。扫描计数 1,逻辑读 7,物理读 0,预读 0,lob 逻辑读 0,lob 物理读 0,lob 预读 0。

 SQL Server 执行时间:
   CPU 时间 = 2 毫秒,经过时间 = 2 毫秒。
SQL Server 解析和编译时间: 
   CPU 时间 = 0 毫秒,经过时间 = 0 毫秒。

查询所有交易/路线:

SELECT
      t.TRANSIT_DAY
    , t.ROUTE_ID
    ,  NCTD_MODE = (
        SELECT TOP (1) Type 
        FROM Routes r
        WHERE (r.RouteID = t.ROUTE_ID) AND (r.StartDate <= t.TRANSIT_DAY)
        ORDER BY r.StartDate DESC
        ) 
FROM Transactions t
ORDER BY t.TRANSIT_DAY
    , t.ROUTE_ID;
Run Code Online (Sandbox Code Playgroud)

计划:

在此处输入图片说明

令人讨厌的溢出到 tempdb 的排序运算符:

在此处输入图片说明

如果我们将聚集索引修改dbo.Transactions(TRANSIT_DAY, ROUTE_ID),并重新运行完整查询,我们会看到一个没有丑陋排序和溢出到临时数据库的计划:

在此处输入图片说明