Cách hiệu quả nhất để truy xuất COUNT truy vấn phụ được nhóm theo bảng trên cùng?


7

Cho lược đồ sau

CREATE TABLE categories
(
    id UNIQUEIDENTIFIER PRIMARY KEY,
    name NVARCHAR(50)
);

CREATE TABLE [group]
(
    id UNIQUEIDENTIFIER PRIMARY KEY
);

CREATE TABLE logger
(
    id UNIQUEIDENTIFIER PRIMARY KEY,
    group_id UNIQUEIDENTIFIER,
    uuid CHAR(17)
);

CREATE TABLE data
(
    id UNIQUEIDENTIFIER PRIMARY KEY,
    logger_uuid CHAR(17),
    category_name NVARCHAR(50),
    recorded_on DATETIME
);

Và các quy tắc sau đây

  1. Mỗi databản ghi tham chiếu a loggervà acategory
  2. Mỗi người loggersẽ luôn có mộtgroup
  3. Mỗi groupcó thể có nhiều loggers
  4. Tôi chỉ muốn đếm dữ liệu gần đây nhất được ghi lại

category_namekhông phải là duy nhất trên mỗi hàng, đó chỉ là một cách để liên kết một bản ghi dữ liệu nhất định trong một danh mục, idthực sự chỉ là một khóa thay thế.

Điều gì sẽ là cách tối ưu để đạt được kết quả như thế nào

category_id | logger_group_count
--------------------------------
12345          4
67890          2
.....          ...

tức là tính không. của các nhóm cho mỗi thể loại mà một logger đã ghi dữ liệu?

Như một cú đâm ban đầu tôi đã nghĩ ra:

SELECT g.id, COUNT(DISTINCT(a.id)) AS logger_group_count 
FROM categories g
  LEFT OUTER JOIN data d ON d.category_name = g.name
  INNER JOIN logger s ON s.uuid = d.logger_uuid
  INNER JOIN group a ON a.id = s.group_id
GROUP BY g.id

Nhưng cực kỳ chậm (~ 45 giây), datacó 400k + hồ sơ - đây là kế hoạch truy vấn và đây là một mẹo để chơi.

Tôi muốn chắc chắn rằng tôi đang tìm ra hầu hết các truy vấn trước khi tôi bắt đầu xem xét những thứ khác, ví dụ như việc sử dụng phần cứng, v.v. Chi phí Azure SQL có thể tăng đáng kể (mặc dù bạn có thể chỉ cần thêm một chút nước trái cây .


2
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện . Sử dụng phòng trò chuyện đó để xử lý sự cố thêm và đảm bảo luôn cập nhật nội dung câu hỏi.
Paul White 9

Câu trả lời:


8

Bạn đang sử dụng phiên bản SQL Server mới hơn nên kế hoạch thực tế cung cấp cho bạn nhiều thông tin. Xem dấu hiệu thận trọng trên các SELECTnhà điều hành? Điều đó có nghĩa là SQL Server đã tạo một cảnh báo có thể ảnh hưởng đến hiệu năng truy vấn. Bạn nên luôn luôn nhìn vào:

<Warnings>
<PlanAffectingConvert ConvertIssue="Seek Plan" Expression="[s].[logger_uuid]=CONVERT_IMPLICIT(nchar(17),[d].[uuid],0)" />
<PlanAffectingConvert ConvertIssue="Seek Plan" Expression="CONVERT_IMPLICIT(nvarchar(100),[d].[name],0)=[g].[name]" />
</Warnings>

Có hai loại chuyển đổi dữ liệu gây ra bởi lược đồ của bạn. Dựa trên các cảnh báo tôi nghi ngờ rằng tên đó thực sự là một NVARCHAR(100)logger_uuidlà một NCHAR(17). Lược đồ bảng được đăng trong câu hỏi có thể không chính xác. Bạn nên hiểu nguyên nhân gốc rễ tại sao các chuyển đổi này xảy ra và khắc phục nó. Một số loại chuyển đổi loại dữ liệu ngăn chặn tìm kiếm chỉ mục, dẫn đến các vấn đề ước tính cardinality và gây ra các vấn đề khác.

Một điều quan trọng khác để kiểm tra là số liệu thống kê chờ đợi. Bạn có thể thấy những người trong các chi tiết của các SELECTnhà điều hành là tốt. Đây là XML cho các số liệu thống kê chờ đợi của bạn và thời gian dành cho truy vấn:

<WaitStats>
<Wait WaitType="RESOURCE_GOVERNOR_IDLE" WaitTimeMs="49515" WaitCount="3773" />
<Wait WaitType="SOS_SCHEDULER_YIELD" WaitTimeMs="57164" WaitCount="2466" />
</WaitStats>
<QueryTimeStats ElapsedTime="67135" CpuTime="10007" />

Tôi không phải là người trên đám mây nhưng có vẻ như truy vấn của bạn không thể tham gia đầy đủ vào CPU . Điều đó có thể liên quan đến lớp Azure hiện tại của bạn. Truy vấn chỉ cần khoảng 10 giây CPU khi thực thi nhưng phải mất 67 giây. Tôi tin rằng 50 giây thời gian đó đã được sử dụng để tiết kiệm và 7 giây thời gian đó được trao cho bạn nhưng được sử dụng cho các truy vấn khác đang chạy đồng thời. Tin xấu là truy vấn chậm hơn có thể là do tầng của bạn. Tin tốt là mọi sự giảm CPU đều có thể dẫn đến giảm 5 lần thời gian chạy. Nói cách khác, nếu bạn có thể nhận được truy vấn để sử dụng 1 giây CPU thì bạn có thể thấy thời gian chạy khoảng 5 giây.

Tiếp theo, bạn có thể xem thuộc tính Thống kê thời gian thực trong chi tiết nhà điều hành của bạn để xem thời gian sử dụng CPU. Gói của bạn sử dụng chế độ hàng, vì vậy thời gian CPU cho một toán tử là tổng thời gian mà toán tử đó cũng như các con của nó sử dụng. Đây là một kế hoạch tương đối đơn giản, do đó, không mất nhiều thời gian để phát hiện ra rằng quét chỉ mục được nhóm logger_datasử dụng thời gian CPU là 6527 ms. Phép nối vòng lặp gọi nó sử dụng 10006 ms thời gian CPU, vì vậy tất cả CPU của truy vấn của bạn được sử dụng ở bước đó. Một manh mối khác cho thấy có gì đó không ổn ở bước đó có thể được tìm thấy bằng cách xem xét độ dày của các mũi tên tương đối:

mũi tên dày

Rất nhiều hàng được trả về từ toán tử đó, vì vậy nó đáng để xem xét chi tiết. Nhìn vào số lượng hàng thực tế để quét chỉ mục được nhóm, bạn có thể thấy rằng 14088885 hàng đã được trả về và 14100798 hàng đã được đọc. Tuy nhiên, số lượng thẻ bảng chỉ là 484804 hàng. Theo trực giác có vẻ khá kém hiệu quả, phải không? Quét chỉ mục cụm trả về nhiều hơn nhiều so với số lượng hàng trong bảng. Một số kế hoạch khác với loại tham gia hoặc phương thức truy cập khác trên bảng có thể sẽ hiệu quả hơn.

Tại sao SQL Server đọc và trả về nhiều hàng như vậy? Chỉ số cụm nằm ở phía bên trong của một vòng lặp lồng nhau. Có 38 hàng được trả về bởi phía bên ngoài của vòng lặp (quét trên loggerbảng) để quét trên logger_datathực hiện 38 lần. 484804 * 38 = 18422514 khá gần với số lượng hàng đã đọc. Vậy tại sao SQL Server lại chọn một kế hoạch như vậy mà cảm thấy không hiệu quả? Nó thậm chí còn ước tính rằng nó sẽ thực hiện 57 lần quét bảng, vì vậy có thể cho rằng kế hoạch mà bạn có được hiệu quả hơn so với nghi ngờ.

Bạn có thể đã tự hỏi tại sao có một TOPnhà điều hành trong kế hoạch của bạn. SQL Server đã giới thiệu một mục tiêu hàng khi tạo kế hoạch truy vấn cho truy vấn của bạn. Điều này có thể chi tiết hơn bạn muốn, nhưng phiên bản ngắn là SQL Server không phải lúc nào cũng cần trả về tất cả các hàng từ quét chỉ mục cụm. Đôi khi nó có thể dừng sớm nếu nó chỉ cần một số hàng cố định và nó tìm thấy những hàng đó trước khi nó kết thúc quá trình quét. Việc quét không tốn kém nếu nó có thể dừng sớm để chi phí cho nhà điều hành được giảm giá theo công thức khi có mục tiêu hàng. Nói cách khác, SQL Server dự kiến ​​sẽ quét chỉ mục được nhóm 57 lần, nhưng nó nghĩ rằng nó sẽ tìm thấy một hàng duy nhất mà nó cần rất nhanh. Nó chỉ cần một hàng duy nhất từ ​​mỗi lần quét do sự hiện diện củaTOP nhà điều hành.

Bạn có thể làm cho truy vấn của mình nhanh hơn bằng cách khuyến khích trình tối ưu hóa truy vấn chọn một gói không quét logger_databảng 38 lần. Điều này có thể đơn giản như loại bỏ các chuyển đổi loại dữ liệu. Điều đó có thể cho phép SQL Server thực hiện tìm kiếm chỉ mục thay vì quét. Nếu không, hãy sửa các chuyển đổi và tạo chỉ mục bao phủ cho logger_data:

CREATE INDEX IX ON logger_data (category_name, logger_uuid);

Trình tối ưu hóa truy vấn chọn một kế hoạch dựa trên chi phí. Việc thêm chỉ mục này khiến cho không thể có được kế hoạch chậm mà nhiều lần quét trên logger_data vì sẽ rẻ hơn khi truy cập vào bảng thông qua tìm kiếm chỉ mục thay vì quét chỉ mục theo cụm.

Nếu bạn không thể thêm chỉ mục, bạn có thể xem xét thêm gợi ý truy vấn để vô hiệu hóa việc giới thiệu mục tiêu hàng : USE HINT('DISABLE_OPTIMIZER_ROWGOAL')). Bạn chỉ nên làm điều này nếu bạn cảm thấy thoải mái với khái niệm mục tiêu hàng và hiểu chúng. Thêm gợi ý đó sẽ dẫn đến một kế hoạch khác, nhưng tôi không thể nói nó sẽ hiệu quả như thế nào.


4

Bắt đầu bằng cách đảm bảo mỗi bảng có tất cả các khóa ứng cử viên được khai báo và các khóa ngoại được thi hành:

CREATE TABLE dbo.categories
(
    id uniqueidentifier NOT NULL
        CONSTRAINT [UQ dbo.categories id]
        UNIQUE NONCLUSTERED,
    [name] nvarchar(50) NOT NULL 
        CONSTRAINT [PK dbo.categories name]
        PRIMARY KEY CLUSTERED
);

-- Choose a better name for this table
CREATE TABLE dbo.[group]
(
    id uniqueidentifier NOT NULL
        CONSTRAINT [PK dbo.group id]
        PRIMARY KEY CLUSTERED
);

CREATE TABLE dbo.logger
(
    id uniqueidentifier 
        CONSTRAINT [UQ dbo.logger id]
        UNIQUE NONCLUSTERED,
    group_id uniqueidentifier NOT NULL
        CONSTRAINT [FK dbo.group id]
        FOREIGN KEY (group_id)
        REFERENCES [dbo].[group] (id),
    uuid char(17) NOT NULL
        CONSTRAINT [PK dbo.logger uuid]
        PRIMARY KEY CLUSTERED
);

CREATE TABLE dbo.logger_data
(
    id uniqueidentifier 
        CONSTRAINT [PK dbo.logger_data id]
        PRIMARY KEY NONCLUSTERED,
    logger_uuid char(17) NOT NULL
        CONSTRAINT [FK dbo.logger_data uuid]
        FOREIGN KEY (logger_uuid)
        REFERENCES dbo.logger (uuid),
    category_name nvarchar(50) NOT NULL
        CONSTRAINT [dbo.logger_data name]
        FOREIGN KEY (category_name)
        REFERENCES dbo.categories ([name]),
    recorded_on datetime NOT NULL,

    INDEX [dbo.logger_data logger_uuid recorded_on] 
        CLUSTERED (logger_uuid, recorded_on)
);

Tôi cũng đã thêm một chỉ mục cụm không duy nhất logger_datavào logger_uuid, recorded_on.

Sau đó, chú ý nhiệm vụ lớn nhất trong kế hoạch thực hiện của bạn là quét 484.836 hàng trong bảng dữ liệu. Vì bạn chỉ quan tâm đến việc đọc gần đây nhất cho một logger cụ thể và hiện tại chỉ có 48 logger, nên hiệu quả hơn là thay thế quét toàn bộ bằng 48 lần tìm kiếm đơn lẻ:

SELECT 
    category_id = C.id, 
    logger_group_count = COUNT_BIG(DISTINCT L.group_id)
FROM dbo.logger AS L
CROSS APPLY 
(
    -- Latest reading per logger
    SELECT TOP (1) 
        LD.recorded_on,
        LD.category_name
    FROM  dbo.logger_data AS LD
    WHERE LD.logger_uuid = L.uuid
    ORDER BY 
        LD.recorded_on DESC
) AS LDT1
JOIN dbo.categories AS C
    ON C.[name] = LDT1.category_name
GROUP BY
    C.id
ORDER BY
    C.id;

Kế hoạch thực hiện là:

Kế hoạch dự kiến

dbfiddle

Bạn cũng nên vá ví dụ của mình từ 2017 RTM sang bản cập nhật tích lũy mới nhất.


0

Tại sao bạn cần tham gia nhóm?

Tại sao là hạng mục g?

SELECT c.id, COUNT(DISTINCT(s.group_id)) AS logger_group_count 
FROM categories c
JOIN data d 
  ON d.category_name = c.name
JOIN logger s 
  ON s.uuid = d.logger_uuid
GROUP BY c.id  

Tôi hy vọng trong cuộc sống thực, bạn đang khai báo các khóa ngoại.

Bạn nên có một chỉ mục trên mỗi cột tham gia.


0

Các vấn đề là:

  1. Improper data type: Nếu kiểu dữ liệu INTcó nghĩa là trang dữ liệu ít hơn và không index fragmentation, nếu đó là NewSequentialIDphương tiện more data pageno index fragmentation, với UNIQUEIDENTIFIERbạn có cả hai vấn đề. Vì vậy, kiểu dữ liệu INT là lựa chọn lý tưởng.
  2. Data type and length of both column should be same in relationship column: ví dụ, a.category_name = g.NAME quét chỉ mục Logger_data Clustered trong kế hoạch đề xuất cả độ dài cột phải là 50 hoặc 100, để Trình tối ưu hóa không phải mất thời gian thực hiện Convert_Implicit Thậm chí tốt hơn, nên xác định mối quan hệ với kiểu dữ liệu int như CategoryID int`.
  3. Nếu truy vấn này rất quan trọng và thường xuyên sử dụng thì bạn có thể nghĩ ra Denormalization, trong ví dụ của bạn tôi không thể nói như thế nào?

Hãy thử truy vấn bên dưới,

    SELECT g.id
    ,sum(CASE 
            WHEN rn = 1
                THEN 1
            ELSE 0
            END)
FROM categories g
INNER JOIN (
    SELECT d.category_name
        ,ROW_NUMBER() OVER (
            PARTITION BY d.category_name
            ,s.group_id ORDER BY s.group_id
            ) rn
    FROM data d
    INNER JOIN logger s ON s.uuid = d.logger_uuid
        --INNER JOIN [group] a ON a.id = s.group_id
    ) a ON a.category_name = g.NAME
GROUP BY g.id

Tôi thích @Paparazziý tưởng vì vậy tôi đã kết hợp nó.

Tôi nghĩ rằng kế hoạch là tốt hơn so với của bạn. Với hiệu chỉnh trên và điều chỉnh chỉ số, nó sẽ thực hiện tốt hơn nữa.

bạn cần sửa ở đây

ROW_NUMBER()over(partition by d.category_name,a.id order by s.group_id )rn 

order by s.group_id, nó sẽ là order by DateOrIDcolumn descbản ghi mới nhất. Với mẫu của bạn, tôi không thể tìm ra cách tìm bản ghi mới nhất.

Cũng lưu ý partition by d.category_nameđiều này nên có partition by d.CatgoryID.


0

Nhờ một câu trả lời tuyệt vời từ @JoeObbish, tôi có thể hiểu rõ hơn về kế hoạch truy vấn và tìm ra nơi mà nó đang vật lộn và những chỉ mục nào tôi có thể sử dụng để cải thiện nó. Ở giữa điều này, các bài viết mục tiêu đã thay đổi một chút vì tôi quên đề cập rằng tôi cần điều này chỉ có thể áp dụng cho lần đọc mới nhất từ mỗi logger, ví dụ nếu logger_aghi dữ liệu bên dưới category_x @ 11:50category_y @ 11:51tôi chỉ muốn tính điều này là category_y.

Đây là SQL kết quả

;WITH logger_data AS (
  SELECT 
    category_name,
    logger_uuid,
    recorded_on,
    RN = ROW_NUMBER() OVER (PARTITION BY logger_uuid ORDER BY recorded_on DESC)
  FROM data
)
SELECT c.id, count(DISTINCT l.group_id) FROM categories c
INNER JOIN logger_data d on d.category_name = c.name
INNER JOIN logger l ON l.uuid = d.logger_uuid
WHERE RN = 1
GROUP BY c.id

Đây vẫn là một truy vấn đắt tiền, tuy nhiên, với các chỉ mục sau được áp dụng

CREATE CLUSTERED INDEX ix_latest ON "dbo"."data"
(
    logger_uuid,
    recorded_on DESC
)
GO
CREATE CLUSTERED INDEX ix_groups ON "dbo"."logger"
(
    group_id
)

Đi từ ~ 25 đến ~ 3 giây và cho một bảng có ~ 500k hàng. Khá hài lòng với điều này, tôi nghĩ có lẽ còn nhiều chỗ để cải thiện nhưng vì nó đứng này là đủ tốt.

Đây là kế hoạch cuối cùng , bất kỳ đề xuất / cải tiến nào khác đều được chào đón.

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.