Cách tối ưu để ghi và lấy giá trị phụ thuộc thời gian


7

Tôi có một bảng có giao dịch cho xe buýt (người đi lên máy bay). Đưa ra ID tuyến đườngngày , tôi cần tra cứu trong một bảng khác loại dịch vụ mà nó đã làm ngày hôm đó. Lịch trình xe buýt thay đổi nhiều nhất cứ sau 6 tháng hoặc lâu hơn, với hầu hết các năm sẽ không thay đổi.

Hiện tại bảng lịch trình được định nghĩa như vậy:

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
));

Một ví dụ có thể trông giống như:

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

Vì vậy, nếu tôi có giao dịch từ 2015-04-20 cho RouteID 301 , tôi muốn lấy lại "Tiêu chuẩn", nhưng nếu giao dịch là từ 2018-01-20 , thì nó sẽ trả về "Ngưng". Đối với các giao dịch trước 2015-01-01 , nó sẽ trả về NULL (hoặc "" hoặc bất kỳ điều gì khác ngoài kết quả có thể mâu thuẫn với câu trả lời hợp lệ, ví dụ: "Tiêu chuẩn", "Paratrans" hoặc "Ngưng").

Về cơ bản, bảng biểu thị rằng tuyến 301 là tuyến tiêu chuẩn trong khoảng thời gian 2015-01-012016-05-31 (và do đó, bất kỳ giao dịch nào trong khoảng thời gian đó nên được phân loại là "Tiêu chuẩn"), sau đó nó đã bị ngừng vào ngày 2016-06- 01 (cho đến ngày hiện tại, mặc nhiên không có thay đổi lịch trình nào được ghi nhận), trong khi 302 là tuyến Tiêu chuẩn từ 2015-01-01 đến 2016-12-31 , sau đó là tuyến ParaTrans (nó) sau đó.

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

Hiện tại, truy vấn để làm điều này trông như thế này:

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

Câu hỏi

Điều tôi muốn biết là: Đây có phải là sự kết hợp hiệu quả nhất của (a) cấu trúc của Routesbảng và (b) truy vấn để đạt được kết quả này không? Nói cách khác, một truy vấn hiệu quả hơn có thể được sử dụng với cấu trúc hiện có không? Có thể thay đổi bảng tuyến đường cho phép truy vấn hiệu quả hơn không?

Cân nhắc

Bảng giao dịch được nhập từ một nhà cung cấp hàng ngày và do đó, việc thay đổi lược đồ của bảng đó không phải là chuyện nhỏ và nên tránh sử dụng. Quan trọng hơn, việc tra cứu này được sử dụng trên một số bảng và cơ sở dữ liệu bằng cách sử dụng các giao dịch hoặc dữ liệu liên quan đến xe buýt khác có nguồn gốc từ nhiều nhà cung cấp; đây chỉ là một ví dụ duy nhất Chúng tôi có một nhà cung cấp (và do đó là một cơ sở dữ liệu) cho các giao dịch tiền tệ, một nhà cung cấp khác cho số người lái và vẫn là một nhà cung cấp khác cho hiệu suất, v.v., với số tuyến và ngày là định danh duy nhất đáng tin cậy trên tất cả.

Bảng lộ trình có một chỉ số là (RouteID, StartDate). Hiện tại có 56 hàng trong bảng Tuyến và 26 triệu hàng trong bảng giao dịch. Bảng tuyến đường bao gồm 45 tuyến đường và hiện tại không có tuyến đường nào có nhiều hơn 2 hàng hoặc một thay đổi. Không có giới hạn về số lượng thay đổi mà một tuyến đường có thể có, nhưng tôi bao gồm chỉ số này để cho thấy rằng con số có thể vẫn còn nhỏ trong tương lai gần.

Tôi có thể thêm bất kỳ chỉ mục cần thiết nào để tối ưu hóa truy vấn được đề xuất. Câu hỏi liên quan nhiều hơn đến việc tìm ra chiến lược tốt nhất, giả sử tất cả các tối ưu hóa hợp lý được thực hiện cho các chiến lược được xem xét, hơn là tìm kiếm tối ưu hóa tốt nhất cho một chiến lược cụ thể.


db <> fiddle ở đây


Điều đó không có nghĩa là các đề xuất để tối ưu hóa chiến lược hiện tại sẽ không được chào đón nếu bạn cảm thấy đó là lựa chọn tốt nhất. Bên cạnh đó, 56 hàng trong bảng Tuyến đường đại diện cho 45 tuyến, cho đến nay không có tuyến nào có nhiều hơn hai hàng (một thay đổi). Vì vậy, Cross (bên trong) Joins có thể là một chiến lược khả thi, nhưng tôi chưa tìm hiểu kỹ về nó.
cpcodes

Câu trả lời:


5

Bạn có thể tăng hiệu suất thiết lập của mình, như thể hiện trong câu hỏi của bạn, bằng cách thay đổi dbo.Routesbảng thành:

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];

Chìa khóa ở đây là chúng ta đang xác định chỉ mục được nhóm, đó bảng, trên hợp chất của RouteIDStartDate DESC. Điều này cung cấp dữ liệu chính xác theo cách hiệu quả nhất cho truy vấn bạn đã viết. Công cụ báo trước ở đây được chèn vào dbo.Routestuyến đường hiện tại có ngày mới sẽ khiến việc chia trang xảy ra do chúng tôi sẽ điền các hàng theo thứ tự ngày giảm dần. Phải nói rằng, với một số lượng nhỏ các hàng trong bảng Tuyến đường và với việc duy trì chỉ số thường xuyên, điều này không phải là một mối quan tâm lớn.

Thay vì làm điều đó, tôi sẽ xem xét sửa đổi dbo.Routesbảng để bao gồm một EndDatecột. Điều này loại bỏ sự cần thiết phải thực hiện một truy vấn con với TOP(1)ORDER BY .... Cái gì đó như:

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)
);

Lưu ý chỉ số cụm hiện đang bật (RouteID, StartDate ASC).

Bây giờ truy vấn có thể sử dụng một INNER JOIN, thay vì truy vấn con tương quan và trông giống như:

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;

Điều này cho phép SQL Server thực hiện phép nối vòng trong đơn giản để thu được kết quả. Cấp, nếu bạn đang trả lại một số lượng lớn các hàng, sẽ có một loại đáng kể được yêu cầu, có thể sẽ tràn sang tempdb.

Sử dụng MCVE mà tôi đã trình bày bên dưới, chúng tôi có thể so sánh các kế hoạch cho hai biến thể. Kế hoạch đầu tiên là truy vấn ban đầu của bạn với truy vấn con tương quan. Kế hoạch thứ hai là với EndDatecột bao gồm.

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Biến thể thứ 2 có chi phí kế hoạch thấp hơn khoảng 4 lần so với biến thể thứ 1. Các toán tử sắp xếp trong cả hai gói đều yêu cầu 108 MB bộ nhớ và đổ hơn 9.000 trang vào tempdb - tuy nhiên, rất có thể bạn sẽ không yêu cầu toàn bộ kết quả so với nhận một tuyến hoặc có thể là một phạm vi ngày. Nếu bạn thêm bộ lọc cho một tuyến đường, sẽ không có cấp bộ nhớ lớn hoặc tràn sang tempdb.

Dưới đây là một MCVE mẫu với 10.000 hàng tuyến và 1.000.000 hàng giao dịch, có thể được sử dụng để chạy thử nghiệm đối với các thiết kế khác nhau:

Làm điều này trong tempdb để tránh bất kỳ "tai nạn" nào với các bảng thực.

USE tempdb;

Bỏ các bảng nếu chúng tồn tại (điều này hoạt động trên SQL Server 2016+):

DROP TABLE IF EXISTS dbo.Routes;
DROP TABLE IF EXISTS dbo.Transactions;

Tạo dbo.Routesbảng, với một chỉ mục được nhóm trên 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)
);

Chèn 10.000 hàng tuyến:

;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

Tạo dbo.Transactions, với một chỉ mục cụm trên ROUTE_ID, TRANSIT_DAY. Xây dựng chỉ mục được nhóm như thế sẽ tối ưu hóa các truy vấn lọc trên cả tuyến và ngày.

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

Chèn 1.000.000 hàng vào dbo.Transactionsbảng:

;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

Đối với một Routesbảng có một EndDatecột có thể được sử dụng cho các thử nghiệm so sánh, tôi đã sử dụng điều này:

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

Truy vấn cả hai bảng cho một tuyến đường cụ thể:

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;

Kế hoạch cho truy vấn trên:

nhập mô tả hình ảnh ở đây

Chỉ số I / O và thời gian:

Bảng 'Tuyến đường'. Quét số 1000, đọc logic 2142, đọc vật lý 0, đọc trước đọc 0, đọc logic 0, đọc vật lý lob 0, đọc trước đọc 0, đọc trước 0.
Bảng 'Giao dịch'. Quét số 1, đọc logic 7, đọc vật lý 0, đọc trước đọc 0, đọc logic 0, đọc vật lý lob 0, đọc trước đọc 0, đọc trước 0.

 Thời gian thực thi máy chủ SQL:
   Thời gian CPU = 2 ms, thời gian trôi qua = 2 ms.
Thời gian phân tích và biên dịch SQL Server: 
   Thời gian CPU = 0 ms, thời gian trôi qua = 0 ms.

Truy vấn cho tất cả các giao dịch / tuyến đường:

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;

Kế hoạch:

nhập mô tả hình ảnh ở đây

Tràn đầy khó chịu đến tempdb cho toán tử sắp xếp:

nhập mô tả hình ảnh ở đây

Nếu chúng ta sửa đổi các nhóm chỉ số trên dbo.Transactionsđược (TRANSIT_DAY, ROUTE_ID), và lại chạy truy vấn đầy đủ, chúng ta thấy một kế hoạch mà không có loại xấu xí và tràn-to-tempdb:

nhập mô tả hình ảnh ở đây


Rất kỹ lưỡng. Để đảm bảo tôi có nó hoàn toàn, ngày kết thúc phải được đặt thành cùng ngày với ngày lịch biểu mới bắt đầu (vì việc tham gia sử dụng "<" thay vì "<=") và đối với những người vẫn còn hoạt động, ngày kết thúc sẽ được đặt thành một số điểm đủ xa trong tương lai (như "20991231"), đúng không? Nếu vậy, tôi nghĩ bạn có nghĩa là câu lệnh "EndDate = COALESCE (LEAD (r.StartDate) QUÁ (THAM GIA BỞI r.RouteID ORDER BY r.StartDate), GETDATE ())" để tạo bảng thử nghiệm trong tương lai xa thay vì "GETDATE ()". Nhưng bên cạnh đó, một câu trả lời rất tốt. Cảm ơn bạn.
cpcodes

Điều đó hoàn toàn chính xác.
Max Vernon
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.