Чому така оцінка кардинальності приєднання настільки велика?


18

Я переживаю те, що, на мою думку, є неможливо високою оцінкою кардинальності для наступного запиту:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

Орієнтовний план тут . Я працюю над статистичною копією таблиць, тому не можу включити фактичний план. Однак я не думаю, що це дуже актуально для цієї проблеми.

SQL Server оцінює, що 481577 рядків буде повернуто з похідної таблиці "dm". Потім оцінюється, що 4528030000 рядків буде повернуто після того, як буде виконано об'єднання до X_LAST_TABLE, але JOIN_ID є первинним ключем X_LAST_TIME. Я б очікував, що оцінка прихильності приєднатися між 0 і 481577 рядками. Натомість оцінка рядків, як видається, становить 10% від кількості рядків, які я отримав би при перетині з'єднання зовнішньої та внутрішньої таблиць. Математика для цього працює з округленням: 481577 * 94025 * 0,1 = 45280277425, яке округлено до 4528030000.

Я в першу чергу шукаю першопричину такої поведінки. Мене також цікавлять прості обхідні шляхи, але, будь ласка, не пропонуйте змінювати модель даних або використовувати темп-таблиці. Цей запит - це спрощення логіки всередині представлення. Я знаю, що робити COALESCE на декількох стовпцях та приєднуватись до них - це не дуже добра практика. Частина цілі цього питання полягає в тому, щоб з'ясувати, чи потрібно мені рекомендувати переробляти модель даних.

Я тестую на Microsoft SQL Server 2014 з включеним застарілим оцінкою кардинальності. TF 4199 та інші. Я можу дати повний перелік прапорів слідів, якщо це в кінцевому підсумку є актуальним.

Ось найбільш відповідне визначення таблиці:

CREATE TABLE X_LAST_TABLE (
JOIN_ID NUMERIC(18, 0) NOT NULL
    CONSTRAINT PK_X_LAST_TABLE PRIMARY KEY CLUSTERED (JOIN_ID ASC)
);

Я також виписав усі сценарії створення таблиці разом зі своєю статистикою, якщо хтось хоче відтворити проблему на одному зі своїх серверів.

Щоб додати кілька моїх спостережень, використання TF 2312 фіксує оцінку, але це не є для мене варіантом. TF 2301 не фіксує оцінку. Видалення однієї з таблиць фіксує кошторис. Як не дивно, зміна порядку приєднання X_DETAIL_LINK також фіксує оцінку. Змінюючи порядок приєднання, я маю на увазі переписання запиту, а не примушування наказу про приєднання з підказкою. Ось орієнтовний план запитів, коли просто змінюється порядок приєднання.


PS Якщо ви можете будь-яким способом перейти на bigintзамість цього, decimal(18, 0)ви отримаєте переваги: ​​1) використовуйте 8 байт замість 9 для кожного значення та 2) використовуйте порівнянний з байтом тип даних замість упакованого типу даних, що може мати наслідки за час процесора при порівнянні значень.
ЕрікЕ

@ErikE Дякую за пораду, але я це вже знав. На жаль, ми засталися з NUMERIC (18,0) над BIGINT з застарілих причин.
Джо Оббіш

Варто було зняти!
ЕрікЕ

Чи потрібні вам X_DETAIL2і X_DETAIL3таблиці взагалі, якщо значення JOIN_IDне є нульовим X_DETAIL1?
ЕрікЕ

@ErikE Це MCVE, тому запит точно не має сенсу в цій точці.
Джо Оббіш

Відповіді:


14

Я знаю, що робити COALESCEна кількох стовпцях і приєднуватись до них - не надто хороша практика.

Створення хороших оцінок кардинальності та розповсюдження досить складно, коли схема становить 3NF + (з ключами та обмеженнями), а запит є реляційним і в першу чергу SPJG (вибір-проекція-приєднання-група за). Модель СЕ побудована на цих принципах. Чим більше незвичних або нереляційних особливостей у запиті, тим ближче до меж того, з чим можуть працювати рамки кардинальності та вибірковості. Зайдіть занадто далеко, і СЕ здасться та здогадається .

Більшість прикладів MCVE - це прості SPJ (без G), хоча і з переважно зовнішніми еквіхойнами (моделюються як внутрішнє з'єднання плюс анти-напівз'єднання), а не більш простим внутрішнім рівноз'єднанням (або напівз'єднанням). У всіх відносинах є ключі, хоча жодних сторонніх ключів чи інших обмежень немає. Усі, окрім однієї з приєднань, - один-багатьом, що добре.

Виняток становить багато-багато-багато зовнішніх з'єднань між X_DETAIL_1та X_DETAIL_LINK. Єдина функція цього з'єднання в MCVE - потенційно дублювати рядки в X_DETAIL_1. Це незвичайна річ.

Прості предикати рівності (виділення) та скалярні оператори також краще. Наприклад, атрибут / константа порівняння-рівний атрибут / константа зазвичай добре працює в моделі. Відносно "легко" змінювати гістограми та статистику частоти, щоб відобразити застосування таких предикатів.

COALESCEпобудований на CASE, що в свою чергу реалізується внутрішньо як IIF(і це було правдою задовго до IIFпояви в мові Transact-SQL). Моделі СЕ IIFє UNIONдвома взаємовиключними дітьми, кожен з яких складається з проекту щодо вибору на вхідному співвідношенні. Кожен із перерахованих компонентів має підтримку моделі, тому поєднувати їх порівняно просто. Незважаючи на це, чим більше шарів абстракцій, тим менш точним є кінцевий результат - причина, чому більші плани виконання, як правило, менш стабільні та надійні.

ISNULLз іншого боку, властивий двигуну. Він не створюється з використанням більш основних компонентів. ISNULLНаприклад, застосувати ефект до гістограми настільки ж просто, як замінити крок на NULLзначення (та ущільнити за необхідності). Він все ще відносно непрозорий, оскільки скалярні оператори йдуть, і тому краще уникати, де це можливо. Тим не менш, це, як правило, більш оптимізатор (менш оптимізатор-недружній), ніж CASEзамінник на основі.

CE (70 та 120+) дуже складний, навіть за стандартами SQL Server. Це не випадки застосування простої логіки (із секретною формулою) до кожного оператора. СЕ знає про ключові та функціональні залежності; він знає, як оцінити, використовуючи частоти, багатоваріантну статистику та гістограми; і існує абсолютна кількість спеціальних справ, вдосконалень, стримувань і противаг та допоміжних конструкцій. Він часто оцінює, наприклад, приєднується декількома способами (частота, гістограма) і приймає рішення про результат або коригування, виходячи з відмінностей між ними.

Остання основна річ, яку слід охопити: Початкова оцінка кардинальності виконується для кожної операції в дереві запитів знизу вгору. Вибірковість і кардинальність спочатку виводяться для операторів листів (базові відносини). Модифіковані гістограми та інформація про щільність / частоту отримують для батьківських операторів. Чим далі вгору по дереву ми йдемо, тим нижчою є якість оцінок, оскільки помилки, як правило, накопичуються.

Ця єдина початкова комплексна оцінка дає вихідну точку і виникає задовго до того, як будь-який розгляд буде розглянуто підсумковим планом виконання (це трапляється ще до етапу складання тривіального плану). Дерево запитів у цей момент має тенденцію відображати письмову форму запиту досить близько (хоча з видаленими підзапити та застосованими спрощеннями тощо)

Одразу після початкової оцінки SQL Server здійснює евристичне переупорядкування з'єднання, яке, вільно кажучи, намагається перевпорядкувати дерево для розміщення менших таблиць, і першою приєднується висока селективність. Він також намагається розташувати внутрішні з'єднання перед зовнішніми з'єднаннями та перехресними виробами. Його можливості не великі; його зусилля не є вичерпними; і він не враховує фізичні витрати (оскільки вони ще не існують - є лише статистична інформація та інформація метаданих). Евристичний порядок найбільш вдалий на простих внутрішніх деревах еквіоїну. Він існує, щоб забезпечити «кращу» вихідну точку оптимізації на основі витрат.

Чому така оцінка кардинальності приєднання настільки велика?

MCVE має "незвичайне" в основному надлишкове приєднання багатьох до багатьох , а в присудку приєднується еквівалент COALESCE. Дерево Оператора також має внутрішнє з'єднання в минулому , що евристичний приєднатися до перезаказа був не в змозі рухатися вгору по дереву в більш детально визначений становище. Не залишаючи осторонь усіх скалярів та проекцій, дерево з'єднання:

LogOp_Join [ Card=4.52803e+009 ]
    LogOp_LeftOuterJoin [ Card=481577 ]
        LogOp_LeftOuterJoin [ Card=481577 ]
            LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
                LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
                LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
            LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
        LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
    LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]

Зверніть увагу, несправна остаточна оцінка вже діє. Він друкується як Card=4.52803e+009і зберігається внутрішньо як значення подвійної точності з плаваючою точкою 4,5280277425e + 9 (4528027742,5 в десятковій частині).

Виведена таблиця в оригінальному запиті видалена, а прогнози нормалізовані. Представлення SQL дерева, на якому було виконано початкову оцінку кардинальності та селективності:

SELECT 
    PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
    ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk 
    ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2 
    ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3 
    ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst 
    ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)

(В бік відхилення повтор COALESCEтакож присутній у кінцевому плані - один раз у кінцевому Скалярному Скалярі та один раз на внутрішній стороні внутрішнього з'єднання).

Помітьте остаточне приєднання. Це внутрішнє з'єднання є (за визначенням) декартовим твором X_LAST_TABLEта попереднім об'єднаним результатом із застосованим виділенням (присудок приєднання) lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID). Кардинальність декартового продукту просто 481577 * 94025 = 45280277425.

Для цього нам потрібно визначити і застосувати вибірковість присудка. Поєднання непрозорого розгорнутого COALESCEдерева (з точки зору UNIONта IIF, пам’ятайте) разом із впливом на ключову інформацію, отримані гістограми та частоти попередніх "незвичайних" здебільшого надлишкових комбінованих "багато-багато" комбінованих засобів означає, що СЕ не в змозі отримати прийнятну оцінку будь-яким із звичайних способів.

Як результат, він входить у логіку «Вгадай». Логіка здогадок помірно складна, з випробуваними алгоритмами здогадок "утворених" та "не так освічених". Якщо не знайдено кращої основи для здогадки, модель використовує здогадку в крайньому випадку, яке для порівняння рівності: sqllang!x_Selectivity_Equal= фіксована 0,1 селективність (10% здогадка):

Стек викликів

-- the moment of doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal

Результат - 0,1 селективність для декартового продукту: 481577 * 94025 * 0,1 = 4528027742,5 (~ 4,552803e + 009), як згадувалося раніше.

Переписує

Коли проблематичне з'єднання прокоментується , робиться краща оцінка, оскільки уникається "здогадки про останню інстанцію" (ключова інформація зберігається 1-М приєднаннями). Якість оцінки все ще низька, оскільки COALESCEпредикат приєднання зовсім не сприятливий для СЕ. Переглянута оцінка робить по крайней мере виглядати більш розумним людям, я вважаю.

Коли запит записується із зовнішнім приєднанням до X_DETAIL_LINK останнього , евристичний порядок може поміняти його на остаточне внутрішнє з'єднання X_LAST_TABLE. Поставлення внутрішнього з'єднання безпосередньо поруч із зовнішнім завданням проблеми дає обмеженим можливостям раннього впорядкування можливість покращити остаточну оцінку, оскільки наслідки здебільшого надлишкового "незвичайного" зовнішнього з'єднання багатьох-багатьох наступають після складної оцінки вибірковості для COALESCE. Знову ж таки, оцінки трохи кращі, ніж тверді здогадки, і, ймовірно, не витримають рішучого перехресного допиту в суді.

Упорядкування суміші внутрішніх і зовнішніх з'єднань є складним та трудомістким (навіть повна оптимізація на етапі 2 лише намагається обмежити підмножину теоретичних кроків).

Вкладений ISNULLу відповідь Макса Вернона вдається уникнути фіксованої здогадки, але остаточна оцінка - це неправдоподібні нульові рядки (підвищені до одного ряду для пристойності). Це також може бути фіксованою здогадкою в 1 рядок для всіх статистичних підрахунків, які має розрахунок.

Я б очікував, що оцінка прихильності приєднатися між 0 і 481577 рядками.

Це розумне очікування, навіть якщо можна визнати, що оцінка кардинальності може відбуватися в різний час (під час оптимізації, заснованої на витратах) на фізично різних, але логічно та семантично однакових підрядках - остаточний план є свого роду зшитим найкращим кращий (за групою пам’яті). Відсутність загальнопланової гарантії узгодженості не означає, що індивідуальне приєднання повинне бути здатним плавати на респектабельність, я це розумію.

З іншого боку, якщо ми закінчимося здогадками в останню чергу , надія вже втрачена, то навіщо турбуватися. Ми спробували всі відомі хитрощі, і відмовилися. Якщо нічого іншого, дика остаточна оцінка є чудовим попереджувальним знаком того, що не все пройшло всередині СЕ під час складання та оптимізації цього запиту.

Коли я спробував MCVE, 120+ CE створив нульову (= 1) рядкову підсумкову оцінку (як вкладена ISNULL) для оригінального запиту, що так само неприйнятно для мого мислення.

Справжнє рішення, ймовірно, передбачає зміну дизайну, щоб дозволити просте приєднання рівних без COALESCEабо ISNULL, в ідеалі зовнішніх ключів та інших обмежень, корисних для складання запитів.


10

Я вважаю, що Compute Scalarоператор, отриманий в COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)результаті приєднання, X_LAST_TABLE.JOIN_IDє першопричиною проблеми. Історично склалося, що обчислити скалярів важко було точно 1 , 2 .

Оскільки ви навели мінімально повний приклад, який можна перевірити (спасибі!) З точною статистикою, я можу переписати запит таким чином, щоб об'єднання більше не вимагало CASEфункціональності, в яке COALESCEрозгортаються, в результаті чого набагато точніші оцінки рядків і, мабуть, більше точна загальна вартість Дивіться додаток наприкінці. :

SELECT COALESCE(dm.d1ID, dm.d2ID, dm.d3ID)
FROM
(
    SELECT d1ID = d1.JOIN_ID
        , d2ID = d2.JOIN_ID
        , d3ID = d3.JOIN_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst 
    ON (dm.d1ID IS NOT NULL AND dm.d1ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NOT NULL AND dm.d2ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NULL AND dm.d3ID IS NOT NULL AND dm.d3ID = lst.JOIN_ID);

Хоча xID IS NOT NULLтехнічно технічно не потрібно, оскільки ID = JOIN_IDне буде приєднуватися до нульових значень, я включив їх, оскільки це більш чітко зображує наміри.

План 1 і План 2

План 1:

введіть тут опис зображення

План 2:

введіть тут опис зображення

Новий запит виграє (?) Від паралелізації. Також слід зазначити, що новий запит має оцінну кількість вихідних рядків 1, що насправді може бути гіршим наприкінці дня, ніж оцінка 4528030000 для початкового запиту. Вартість піддерева для оператора вибору за новим запитом надходить за 243210, тоді як початкові тактові частоти - 536.535, що явно менше. Сказавши це, я не вірю, що перша оцінка є десь близькою до реальності.


Додаток 1.

Після подальшої консультації з різними людьми щодо The Heap ™, спричиненої дискусією з @Lamak, мені здається, що мої спостережувальні запити вище спрацьовують жахливо, навіть з паралелізмом. Рішення , яке дозволяє як продуктивність хорошою і хороші оцінки потужності полягає в заміні COALESCE(x,y,z)з ISNULL(ISNULL(x, y), z), як в:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT ISNULL(ISNULL(d1.JOIN_ID, d2.JOIN_ID), d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

COALESCECASEоптимізатором запитів перетворюється в оператор "під обкладинками". Таким чином, оцінювачу кардинальності важче виявити надійну статистику для стовпців, закопаних всередину COALESCE. ISNULLбудучи внутрішньою функцією, набагато "відкритіше" для оцінювача кардинальності. Не варто також нічого ISNULLоптимізувати, якщо ціль, як відомо, не зводиться до нуля.

План ISNULLваріанту виглядає так:

введіть тут опис зображення

(Вставте версію плану тут ).

FYI, реквізит до Sentry One за їх чудовий Провідник плану, який я використовував для створення графічних планів вище.


-1

Відповідно до вашої умови приєднання, таблицю можна впорядкувати за такою кількістю манер, що "змінившись на один конкретний спосіб" фіксуйте результат.

Припустимо, що приєднання лише до однієї таблиці дає правильний результат.

SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID

Тут замість X_DETAIL_1, ви можете використовувати або X_DETAIL_2або X_DETAIL_3.

Тому призначення двох таблиць не зрозуміло.

Це як би ви розбили стіл X_DETAIL_1на ще дві частини.

Швидше за все , « є помилка , де ви заповнення цих таблиць. » В ідеалі X_DETAIL_1, X_DETAIL_2і X_DETAIL_3повинні містити однакову кількість рядків.

Але одна або кілька таблиць містять небажану кількість рядків.

Вибачте, якщо я помиляюся

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.