Database
 sql >> база данни >  >> RDS >> Database

Някои ВСЯКАКВИ агрегатни трансформации са нарушени

ANY агрегатът не е нещо, което можем да напишем директно в Transact SQL. Това е само вътрешна функция, използвана от оптимизатора на заявки и машината за изпълнение.

Аз лично много харесвам ANY агрегат, така че беше малко разочароващо да научим, че е счупен по доста фундаментален начин. Конкретният вкус на „счупен“, за който имам предвид тук, е разнообразието с грешни резултати.

В тази публикация разглеждам две конкретни места, където ANY обобщението обикновено се показва, демонстрира проблема с грешните резултати и предлага заобиколни решения, където е необходимо.

За фон на ANY aggregate, моля, вижте предишната ми публикация Планове за недокументирани заявки:ВСЯКАКВА Агрегат.

1. Един ред на групови заявки

Това трябва да е едно от най-често срещаните ежедневни изисквания за заявка, с много добре познато решение. Вероятно пишете този вид заявка всеки ден, автоматично следвайки шаблона, без наистина да мислите за това.

Идеята е да номерирате входния набор от редове с помощта на ROW_NUMBER функция прозорец, разделена от групиращата колона или колони. Това е обвито в Общ израз на таблица или производна таблица и се филтрира до редове, където изчисленият номер на ред е равен на единица. От ROW_NUMBER рестартира от един за всяка група, това ни дава необходимия един ред за група.

Няма проблем с този общ модел. Типът на един ред на групова заявка, която е предмет на ANY обобщен проблем е този, при който не ни интересува кой конкретен ред е избран от всяка група.

В такъв случай не е ясно коя колона трябва да се използва в задължителния ORDER BY клауза от ROW_NUMBER функция прозорец. В крайна сметка, ние изрично не ни пука кой ред е избран. Един често срещан подход е повторното използване на PARTITION BY колона(и) в ORDER BY клауза. Тук може да възникне проблемът.

Пример

Нека разгледаме пример с набор от данни за играчки:

CREATE TABLE #Data
(
    c1 integer NULL,
    c2 integer NULL,
    c3 integer NULL
);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, NULL, 1),
    (1, 1, NULL),
    (1, 111, 111),
    -- Group 2
    (2, NULL, 2),
    (2, 2, NULL),
    (2, 222, 222);

Изискването е да се върне всеки един пълен ред данни от всяка група, където членството в групата се определя от стойността в колона c1 .

Следвайки ROW_NUMBER модел, можем да напишем заявка като следната (забележете ORDER BY клауза от ROW_NUMBER функцията прозорец съвпада с PARTITION BY клауза):

WITH 
    Numbered AS 
    (
        SELECT 
            D.*, 
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM #Data AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

Както е представено, тази заявка се изпълнява успешно, с правилни резултати. Резултатите са технически недетерминирани тъй като SQL Server може валидно да върне всеки един от редовете във всяка група. Независимо от това, ако изпълните тази заявка сами, е много вероятно да видите същия резултат като аз:

Планът за изпълнение зависи от използваната версия на SQL Server и не зависи от нивото на съвместимост на базата данни.

На SQL Server 2014 и по-стари, планът е:

За SQL Server 2016 или по-нова версия ще видите:

И двата плана са безопасни, но по различни причини. Отличен сорт планът съдържа ANY агрегат, но Отличен сорт реализацията на оператора не показва грешката.

По-сложният план за SQL Server 2016+ не използва ANY агрегат изобщо. Сортиране поставя редовете в реда, необходим за операцията за номериране на редове. Сегментът операторът задава флаг в началото на всяка нова група. Проектът последователност изчислява номера на реда. И накрая, Филтър операторът предава само тези редове, които имат изчислен номер на ред един.

Бъгът

За да получим неправилни резултати с този набор от данни, трябва да използваме SQL Server 2014 или по-стара версия и ANY агрегатите трябва да бъдат внедрени в Stream Aggregate или Eager Hash Aggregate оператор (Flow Distinct Hash Match Aggregate не произвежда грешка).

Един от начините да насърчите оптимизатора да избере Stream Aggregate вместо Отлично сортиране е да добавите клъстериран индекс, за да осигурите подреждане по колона c1 :

CREATE CLUSTERED INDEX c ON #Data (c1);

След тази промяна планът за изпълнение става:

ANY агрегатите се виждат в Свойства прозорец, когато Агрегат на потока е избран оператор:

Резултатът от заявката е:

Това е грешно . SQL Server върна редове, които не съществуват в изходните данни. Няма изходни редове, където c2 = 1 и c3 = 1 например. Като напомняне, изходните данни са:

Планът за изпълнение погрешно изчислява отделно ANY агрегати за c2 и c3 колони, пренебрегвайки нулевите стойности. Всеки агрегат независимо връща първия не-нула стойност, която среща, давайки резултат, където стойностите за c2 и c3 идват от различни изходни редове . Това не е искано от оригиналната спецификация на SQL заявка.

Същият грешен резултат може да се получи с или без клъстерирания индекс чрез добавяне на OPTION (HASH GROUP) намек за създаване на план с Eager Hash Aggregate вместо Stream Aggregate .

Условия

Този проблем може да възникне само при няколко ANY присъстват агрегати, а обобщените данни съдържат нулеви стойности. Както бе отбелязано, проблемът засяга само Stream Aggregate и Eager Hash Aggregate оператори; Различно сортиране и Различен поток не са засегнати.

SQL Server 2016 нататък полага усилия да избегне въвеждането на множество ANY агрегати за модела на заявка за номериране на всеки един ред на група, когато изходните колони са нулеви. Когато това се случи, планът за изпълнение ще съдържа Сегмент , Проект за последователност и Филтър оператори вместо агрегат. Тази форма на план винаги е безопасна, тъй като няма ANY се използват агрегати.

Възпроизвеждане на грешката в SQL Server 2016+

Оптимизаторът на SQL Server не е перфектен при откриване кога колона първоначално е ограничена да бъде NOT NULL може все пак да генерира нулева междинна стойност чрез манипулации на данни.

За да възпроизведем това, ще започнем с таблица, където всички колони са декларирани като NOT NULL :

IF OBJECT_ID(N'tempdb..#Data', N'U') IS NOT NULL
BEGIN
    DROP TABLE #Data;
END;
 
CREATE TABLE #Data
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 integer NOT NULL
);
 
CREATE CLUSTERED INDEX c ON #Data (c1);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, 1, 1),
    (1, 2, 2),
    (1, 3, 3),
    -- Group 2
    (2, 1, 1),
    (2, 2, 2),
    (2, 3, 3);

Можем да произвеждаме нулеви стойности от този набор от данни по много начини, повечето от които оптимизаторът може успешно да открие, и така избягваме въвеждането на ANY агрегати по време на оптимизация.

Един от начините за добавяне на нули, които се случват да се изплъзнат под радара, е показан по-долу:

SELECT
    D.c1,
    OA1.c2,
    OA2.c3
FROM #Data AS D
OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2;

Тази заявка произвежда следния изход:

Следващата стъпка е да използвате тази спецификация на заявката като изходни данни за стандартната заявка „всеки един ред на група“:

WITH
    SneakyNulls AS 
    (
        -- Introduce nulls the optimizer can't see
        SELECT
            D.c1,
            OA1.c2,
            OA2.c3
        FROM #Data AS D
        OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
        OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2
    ),
    Numbered AS 
    (
        SELECT
            D.c1,
            D.c2,
            D.c3,
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM SneakyNulls AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

На всяка версия на SQL Server, което създава следния план:

Агрегатът на потока съдържа множество ANY агрегати, а резултатът е грешен . Нито един от върнатите редове не се появява в изходния набор от данни:

db<>fiddle онлайн демонстрация

Заобиколно решение

Единственото напълно надеждно решение, докато тази грешка не бъде отстранена, е да се избегне моделът, при който ROW_NUMBER има същата колона в ORDER BY клауза, както е в PARTITION BY клауза.

Когато не ни интересува кое по един ред е избран от всяка група, жалко е, че ORDER BY клаузата изобщо е необходима. Един от начините да избегнете проблема е да използвате константа за време на изпълнение като ORDER BY @@SPID във функцията прозорец.

2. Недетерминирана актуализация

Проблемът с няколко ANY агрегатите на входове с нулеви стойности не е ограничено до всеки един ред на групова заявка. Оптимизаторът на заявки може да въведе вътрешен ANY съвкупност при редица обстоятелства. Един от тези случаи е недетерминирана актуализация.

недетерминиран update е мястото, където изразът не гарантира, че всеки целеви ред ще бъде актуализиран най-много веднъж. С други думи, има множество изходни реда за поне един целеви ред. Документацията изрично предупреждава за това:

Бъдете внимателни, когато посочвате клаузата FROM, за да предоставите критериите за операцията за актуализиране.
Резултатите от оператор UPDATE са недефинирани, ако изразът включва клауза FROM, която не е посочена по такъв начин, че да е налична само една стойност за всяка поява на колона, която се актуализира, че е, ако изразът UPDATE не е детерминистичен.

За да се справи с недетерминирана актуализация, оптимизаторът групира редовете по ключ (индекс или RID) и прилага ANY агрегати към останалите колони. Основната идея там е да изберете един ред от множество кандидати и да използвате стойности от този ред, за да извършите актуализацията. Има очевидни паралели с предишния ROW_NUMBER проблем, така че не е изненада, че е доста лесно да се демонстрира неправилна актуализация.

За разлика от предишния проблем, SQL Server понастоящем не предприема специални стъпки за да избегнете множество ANY агрегати върху колони с нулеви стойности при извършване на недетерминирана актуализация. Следователно следното се отнася до всички версии на SQL Server , включително SQL Server 2019 CTP 3.0.

Пример

DECLARE @Target table
(
    c1 integer PRIMARY KEY, 
    c2 integer NOT NULL, 
    c3 integer NOT NULL
);
 
DECLARE @Source table 
(
    c1 integer NULL, 
    c2 integer NULL, 
    c3 integer NULL, 
 
    INDEX c CLUSTERED (c1)
);
 
INSERT @Target 
    (c1, c2, c3) 
VALUES 
    (1, 0, 0);
 
INSERT @Source 
    (c1, c2, c3) 
VALUES 
    (1, 2, NULL),
    (1, NULL, 3);
 
UPDATE T
SET T.c2 = S.c2,
    T.c3 = S.c3
FROM @Target AS T
JOIN @Source AS S
    ON S.c1 = T.c1;
 
SELECT * FROM @Target AS T;

db<>fiddle онлайн демонстрация

Логично, тази актуализация винаги трябва да генерира грешка:Таблицата на целевата таблица не позволява нулеви стойности в нито една колона. Който и съвпадащ ред е избран от изходната таблица, опит за актуализиране на колона c2 или c3 до нула трябва възникне.

За съжаление, актуализацията е успешна и крайното състояние на целевата таблица не е в съответствие с предоставените данни:

Съобщих за това като за грешка. Работата е да се избегне писането на недетерминиран UPDATE изрази, така че ANY не са необходими агрегати за разрешаване на неяснотата.

Както споменахме, SQL Server може да въведе ANY агрегати при повече обстоятелства от двата примера, дадени тук. Ако това се случи, когато обобщената колона съдържа нулеви стойности, има потенциал за грешни резултати.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Създаване на модел на данни за споделено пътуване

  2. Научете за конкатенацията в SQL с примери

  3. Какво е DBMS? – Изчерпателно ръководство за системи за управление на бази данни

  4. Разбиране на транзакциите в SQL

  5. Модел на данни за приложение за маратонско обучение