Я опублікую відповідь, щоб почати. Моя перша думка полягала в тому, що слід використовувати можливість збереження порядку вкладеного вкладеного циклу разом з кількома допоміжними таблицями, які мають по одному рядку для кожної літери. Трюкова частина мала бути циклічною таким чином, щоб результати були впорядковані по довжині, а також уникнення дублікатів. Наприклад, при перехресному приєднанні до CTE, що включає всі 26 великих літер разом із символом '', ви можете закінчити генерування, 'A' + '' + 'A'
і '' + 'A' + 'A'
це, звичайно, однаковий рядок.
Першим рішенням було де зберігати дані про помічників. Я спробував скористатися таблицею темпів, але це напрочуд негативно вплинуло на продуктивність, хоча дані вміщуються на одній сторінці. Темп-таблиця містила наведені нижче дані:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
Порівняно з використанням CTE, запит зайняв 3X більше часу з кластеризованою таблицею і 4X довше купою. Я не вірю, що проблема полягає в тому, що дані є на диску. Його слід читати в пам'ять як одну сторінку і обробляти в пам'яті за весь план. Можливо, SQL Server може працювати з даними оператора постійного сканування ефективніше, ніж це може бути з даними, що зберігаються на типових сторінках зберігання рядків.
Цікаво, що SQL Server вирішує помістити впорядковані результати з однієї сторінки tempdb таблиці з упорядкованими даними в котушку таблиці:
SQL Server часто ставить результати для внутрішньої таблиці перехресного з'єднання в котушку таблиці, навіть якщо це здається безглуздим. Я думаю, що оптимізатору потрібно трохи попрацювати в цій галузі. Я запустив запит за допомогою, NO_PERFORMANCE_SPOOL
щоб уникнути показника продуктивності.
Одна з проблем використання CTE для зберігання даних про помічників полягає в тому, що дані не гарантовано замовляються. Я не можу подумати, чому оптимізатор вирішив би не замовляти його, і в усіх моїх тестах дані оброблялися в тому порядку, як я написав CTE:
Однак найкраще не ризикувати, особливо якщо є спосіб зробити це без великих накладних витрат. Можна замовити дані у похідній таблиці, додавши зайвий TOP
оператор. Наприклад:
(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)
Це доповнення до запиту повинно гарантувати повернення результатів у правильному порядку. Я очікував, що всі види матимуть великий негативний вплив на продуктивність. Оптимізатор запитів очікував цього також на основі розрахункових витрат:
Дуже дивно, що я не міг помітити жодної статистично значущої різниці у часі чи час виконання процесора з явним замовленням або без нього. Якщо що-небудь, запит, здавалося, працює швидше з ORDER BY
! У мене немає пояснення такої поведінки.
Хитра частина проблеми полягала в тому, щоб з'ясувати, як вставити порожні символи в потрібні місця. Як було сказано раніше, простий CROSS JOIN
призведе до повторення даних. Ми знаємо, що 100000000-й рядок матиме довжину шість символів, оскільки:
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000
але
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000
Тому нам потрібно лише шість разів приєднатися до листа CTE. Припустимо, що ми приєднуємось до CTE шість разів, захопимо по одній букві від кожного CTE та з'єднаємо їх усі разом. Припустимо, лівий лівий лист не порожній. Якщо будь-який з наступних літер порожній, це означає, що рядок довжиною менше шести символів, тому він є дублікатом. Тому ми можемо запобігти дублікатам, знайшовши перший не порожній символ і вимагаючи, щоб усі символи після нього також не були порожніми. Я вирішив відстежити це, призначивши FLAG
стовпець одному з CTE і додавши чек до WHERE
пункту. Це повинно бути зрозумілішим після перегляду запиту. Остаточний запит такий:
WITH FIRST_CHAR (CHR) AS
(
SELECT 'A'
UNION ALL SELECT 'B'
UNION ALL SELECT 'C'
UNION ALL SELECT 'D'
UNION ALL SELECT 'E'
UNION ALL SELECT 'F'
UNION ALL SELECT 'G'
UNION ALL SELECT 'H'
UNION ALL SELECT 'I'
UNION ALL SELECT 'J'
UNION ALL SELECT 'K'
UNION ALL SELECT 'L'
UNION ALL SELECT 'M'
UNION ALL SELECT 'N'
UNION ALL SELECT 'O'
UNION ALL SELECT 'P'
UNION ALL SELECT 'Q'
UNION ALL SELECT 'R'
UNION ALL SELECT 'S'
UNION ALL SELECT 'T'
UNION ALL SELECT 'U'
UNION ALL SELECT 'V'
UNION ALL SELECT 'W'
UNION ALL SELECT 'X'
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
SELECT '', 0 CHR
UNION ALL SELECT 'A', 1
UNION ALL SELECT 'B', 1
UNION ALL SELECT 'C', 1
UNION ALL SELECT 'D', 1
UNION ALL SELECT 'E', 1
UNION ALL SELECT 'F', 1
UNION ALL SELECT 'G', 1
UNION ALL SELECT 'H', 1
UNION ALL SELECT 'I', 1
UNION ALL SELECT 'J', 1
UNION ALL SELECT 'K', 1
UNION ALL SELECT 'L', 1
UNION ALL SELECT 'M', 1
UNION ALL SELECT 'N', 1
UNION ALL SELECT 'O', 1
UNION ALL SELECT 'P', 1
UNION ALL SELECT 'Q', 1
UNION ALL SELECT 'R', 1
UNION ALL SELECT 'S', 1
UNION ALL SELECT 'T', 1
UNION ALL SELECT 'U', 1
UNION ALL SELECT 'V', 1
UNION ALL SELECT 'W', 1
UNION ALL SELECT 'X', 1
UNION ALL SELECT 'Y', 1
UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
CASE
WHEN d6.FLAG = 1 THEN 5
WHEN d5.FLAG = 1 THEN 4
WHEN d4.FLAG = 1 THEN 3
WHEN d3.FLAG = 1 THEN 2
WHEN d2.FLAG = 1 THEN 1
ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);
КТЕ такі, як описано вище. ALL_CHAR
приєднується до п’яти разів, тому що містить рядок для порожнього символу. Останній символ в рядку не повинно бути порожнім тому окреме КТР визначається для нього FIRST_CHAR
. Додатковий стовпчик прапорця в ALL_CHAR
використовується для запобігання дублікатів, як описано вище. Можливо, є більш ефективний спосіб зробити цю перевірку, але, безумовно, є більш неефективні способи зробити це. Одна з моїх спроб LEN()
і POWER()
зробила запит виконуватись у шість разів повільніше, ніж поточна версія.
Підказки MAXDOP 1
та FORCE ORDER
підказки мають важливе значення для того, щоб зберегти порядок у запиті. Анотаційний кошторисний план може бути корисним, щоб зрозуміти, чому з'єднання відбуваються в їх поточному порядку:
Плани запитів часто читаються справа наліво, але запити рядків трапляються зліва направо. В ідеалі, SQL Server вимагає рівно 100 мільйонів рядків від d1
оператора постійного сканування. Коли ви рухаєтеся зліва направо, я очікую, що від кожного оператора потрібно буде запитувати менше рядків. Ми можемо бачити це в реальному плані виконання . Крім того, нижче показаний знімок екрана програми SQL Sentry Plan Explorer:
Ми отримали рівно 100 мільйонів рядків з d1, що добре. Зауважте, що співвідношення рядків між d2 та d3 майже рівно 27: 1 (165336 * 27 = 4464072), що має сенс, якщо ви думаєте про те, як буде працювати перехресне з'єднання. Співвідношення рядків між d1 і d2 становить 22,4, що являє собою деяку витрачену роботу. Я вважаю, що додаткові рядки є з дублікатів (через порожні символи в середині рядків), які не проходять мимо вкладеного оператора приєднання до циклу, який здійснює фільтрацію.
LOOP JOIN
Натяк технічно непотрібний , бо CROSS JOIN
може бути реалізована тільки в вигляді петлі приєднатися до SQL Server. Це NO_PERFORMANCE_SPOOL
полягає у запобіганні непотрібного спушування столу. Якщо натякнути натяк на котушку, на моїй машині запит тривав 3 рази більше.
Час останнього запиту становить близько 17 секунд та загальний час, що минув 18 секунд. Це було під час запуску запиту через SSMS та відкидання набору результатів. Мені дуже цікаво бачити інші методи генерування даних.