Phép nối băm giữa các bảng chính / chi tiết tạo ra ước tính cardinality quá thấp


9

Khi tham gia bảng chính vào bảng chi tiết, làm cách nào tôi có thể khuyến khích SQL Server 2014 sử dụng ước tính cardinality của bảng (chi tiết) lớn hơn làm ước tính cardinality của đầu ra tham gia?

Ví dụ: khi nối các hàng chính 10K thành các hàng chi tiết 100K, tôi muốn SQL Server ước tính liên kết ở các hàng 100K - giống như số lượng hàng chi tiết ước tính. Tôi nên cấu trúc các truy vấn và / hoặc bảng và / hoặc chỉ mục của mình như thế nào để giúp công cụ ước tính của SQL Server tận dụng thực tế là mọi hàng chi tiết luôn có một hàng chính tương ứng? (Có nghĩa là một liên kết giữa chúng sẽ không bao giờ làm giảm ước tính cardinality.)

Dưới đây là chi tiết. Cơ sở dữ liệu của chúng tôi có một cặp bảng chính / chi tiết: VisitTargetcó một hàng cho mỗi giao dịch bán hàng và VisitSalecó một hàng cho mỗi sản phẩm trong mỗi giao dịch. Đó là mối quan hệ một-nhiều: một hàng VisitTarget cho trung bình 10 hàng VisitSale.

Các bảng trông như thế này: (Tôi chỉ đơn giản hóa các cột có liên quan cho câu hỏi này)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

Vì lý do hiệu suất, chúng tôi đã không chuẩn hóa một phần bằng cách sao chép các cột lọc phổ biến nhất (ví dụ SaleDate) từ bảng chính vào từng hàng của bảng chi tiết và sau đó chúng tôi đã thêm các chỉ mục che phủ trên cả hai bảng để hỗ trợ tốt hơn các truy vấn được lọc theo ngày. Điều này hoạt động rất tốt để giảm I / O khi chạy các truy vấn được lọc theo ngày, nhưng tôi nghĩ cách tiếp cận này đang gây ra các vấn đề ước tính về tim mạch khi nối các bảng tổng thể và chi tiết với nhau.

Khi chúng ta tham gia hai bảng này, các truy vấn trông như thế này:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

Bộ lọc ngày trên bảng chi tiết ( VisitSale) là dự phòng. Nó ở đó để kích hoạt I / O tuần tự (còn gọi là toán tử Index Seek) trên bảng chi tiết cho các truy vấn được lọc theo phạm vi ngày.

Kế hoạch cho các loại truy vấn này trông như thế này:

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

Một kế hoạch thực tế của một truy vấn có cùng vấn đề có thể được tìm thấy ở đây .

Như bạn có thể thấy, ước tính cardinality cho phép nối (tooltip ở phía dưới bên trái trong hình) là trên 4x quá thấp: ước tính thực tế là 2.1M so với 0.5M. Điều này gây ra các vấn đề về hiệu năng (ví dụ như đổ vào tempdb), đặc biệt khi truy vấn này là truy vấn con được sử dụng trong truy vấn phức tạp hơn.

Nhưng các ước tính đếm hàng cho mỗi nhánh của phép nối gần với số lượng hàng thực tế. Nửa trên của tham gia là 100K thực tế so với 164K ước tính. Nửa dưới của liên kết là 2,1M hàng thực tế so với ước tính 3,7 triệu. Phân phối xô băm cũng có vẻ tốt. Những quan sát này gợi ý cho tôi rằng số liệu thống kê là ổn đối với mỗi bảng và vấn đề là ước tính của số lượng tham gia.

Lúc đầu, tôi nghĩ rằng vấn đề là SQL Server hy vọng rằng các cột SaleDate trong mỗi bảng là độc lập, trong khi thực sự chúng giống hệt nhau. Vì vậy, tôi đã thử thêm một so sánh bình đẳng cho ngày Bán hàng vào điều kiện tham gia hoặc mệnh đề WHERE, vd

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

hoặc là

WHERE vt.SaleDate = vs.SaleDate

Điều này đã không làm việc. Nó thậm chí còn làm cho ước tính cardinality tồi tệ hơn! Vì vậy, SQL Server không sử dụng gợi ý bình đẳng đó hoặc một cái gì đó khác là nguyên nhân gốc rễ của vấn đề.

Có bất kỳ ý tưởng nào về cách khắc phục sự cố và hy vọng khắc phục sự cố ước tính cardinality này không? Mục tiêu của tôi là để tính chính xác của phép nối chính / chi tiết được ước tính giống như ước tính cho đầu vào lớn hơn ("bảng chi tiết") của phép nối.

Nếu có vấn đề, chúng tôi đang chạy SQL Server 2014 Enterprise SP2 CU8 bản dựng 12.0.5557.0 trên Windows Server. Không có cờ theo dõi được kích hoạt. Mức độ tương thích cơ sở dữ liệu là SQL Server 2014. Chúng tôi thấy hành vi tương tự trên nhiều Máy chủ SQL khác nhau, do đó dường như không phải là sự cố cụ thể của máy chủ.

Có một tối ưu hóa trong Công cụ ước tính Cardinality của SQL Server 2014 , đó chính xác là hành vi tôi đang tìm kiếm:

Tuy nhiên, CE mới sử dụng thuật toán đơn giản hơn, giả sử rằng có một liên kết một-nhiều giữa một bảng lớn và một bảng nhỏ. Điều này giả định rằng mỗi hàng trong bảng lớn khớp chính xác với một hàng trong bảng nhỏ. Thuật toán này trả về kích thước ước tính của đầu vào lớn hơn như là số lượng tham gia.

Lý tưởng nhất là tôi có thể có được hành vi này, trong đó ước tính cardinality cho phép nối sẽ giống như ước tính cho bảng lớn, mặc dù bảng "nhỏ" của tôi vẫn trả về hơn 100 nghìn hàng!

Câu trả lời:


6

Giả sử rằng không có cải thiện nào có thể đạt được bằng cách thực hiện một số thứ để thống kê hoặc sử dụng CE kế thừa, thì cách đơn giản nhất xung quanh vấn đề của bạn là thay đổi INNER JOINthành LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

Nếu bạn có khóa ngoại giữa các bảng, bạn luôn lọc trên cùng một SaleDatephạm vi cho cả hai bảng và SaleDateluôn khớp giữa các bảng thì kết quả truy vấn của bạn sẽ không thay đổi. Việc sử dụng một phép nối ngoài như thế này có vẻ kỳ lạ, nhưng hãy nghĩ rằng nó thông báo cho trình tối ưu hóa truy vấn rằng phép nối vào VisitTargetbảng sẽ không bao giờ giảm số lượng hàng được truy vấn trả về. Thật không may, khóa ngoại không thay đổi ước tính cardinality nên đôi khi bạn cần phải dùng đến các thủ thuật như thế này. (Đề xuất Microsoft Connect: Giúp ước tính tối ưu hóa chính xác hơn bằng cách sử dụng siêu dữ liệu ).

Có thể việc viết truy vấn ở dạng này sẽ không hoạt động tốt tùy thuộc vào những gì khác xảy ra trong truy vấn sau khi tham gia. Bạn có thể thử sử dụng bảng tạm thời để giữ kết quả trung gian của tập kết quả với ước tính số lượng quan trọng nhất.

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.