Я отправлю ответ, чтобы начать. Моя первая мысль была о том, что должна быть возможность воспользоваться сохраняющим порядок характером объединения вложенных циклов вместе с несколькими вспомогательными таблицами, которые имеют одну строку для каждой буквы. Сложная часть будет зацикливаться таким образом, чтобы результаты были упорядочены по длине, а также избегать дубликатов. Например, при перекрестном соединении CTE, который включает в себя все 26 заглавных букв вместе с '', вы можете в конечном итоге генерировать 'A' + '' + 'A'
и, '' + 'A' + 'A'
конечно же, одну и ту же строку.
Первое решение было, где хранить вспомогательные данные. Я попытался использовать временную таблицу, но это оказало удивительно негативное влияние на производительность, даже если данные помещаются на одной странице. Временная таблица содержала следующие данные:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
По сравнению с использованием CTE запрос занимал в 3 раза больше времени для кластеризованной таблицы и в 4 раза больше для кучи. Я не верю, что проблема в том, что данные на диске. Он должен быть прочитан в память как одна страница и обработан в памяти для всего плана. Возможно, 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);
CTE такие же, как описано выше. 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 и отбрасывании результирующего набора. Мне очень интересно увидеть другие методы генерации данных.