SQL: внутреннее соединение двух больших таблиц - PullRequest
23 голосов
/ 17 ноября 2009

У меня есть две массивные таблицы с примерно 100 миллионами записей в каждой, и я боюсь, что мне нужно было выполнить внутреннее соединение между ними. Теперь обе таблицы очень просты; вот описание:

Таблица BioEntity:

  • BioEntityId (int)
  • Имя (nvarchar 4000, хотя это перебор)
  • TypeId (int)

Таблица EGM (фактически, дополнительная таблица, полученная в результате операций массового импорта):

  • EMGId (int)
  • PId (int)
  • Имя (nvarchar 4000, хотя это перебор)
  • TypeId (int)
  • LastModified (дата)

Мне нужно получить соответствующее Имя, чтобы связать BioEntityId с PId, находящимся в таблице EGM. Первоначально я пытался сделать все с одним внутренним объединением, но запрос, казалось, занимал слишком много времени, и лог-файл базы данных (в простом режиме восстановления) сумел уничтожить все доступное дисковое пространство (это чуть более 200 ГБ, если база данных занимает 18 ГБ) и запрос не будет выполнен через два дня ожидания, если я не ошибаюсь. Мне удалось предотвратить рост журнала (всего 33 МБ сейчас), но запрос выполнялся безостановочно уже 6 дней, и не похоже, что он скоро остановится.

Я использую его на довольно приличном компьютере (4 ГБ ОЗУ, Core 2 Duo (E8400) 3GHz, Windows Server 2008, SQL Server 2008), и я заметил, что компьютер иногда зависает каждые 30 секунд (да или нет) ) на пару секунд. Это делает его довольно трудным для чего-то другого, что действительно действует мне на нервы.

Теперь вот запрос:

 SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
 FROM EGM INNER JOIN BioEntity 
 ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId

Я вручную настроил некоторые индексы; и EGM, и BioEntity имели некластеризованный индекс покрытия, содержащий TypeId и Name. Однако запрос выполнялся в течение пяти дней, и он не заканчивался , поэтому я попытался запустить помощник по настройке базы данных, чтобы заставить его работать. Он предложил удалить мои старые индексы и вместо этого создать статистику и два кластеризованных индекса (по одному на каждую таблицу, просто содержащий TypeId, который я нахожу довольно странным - или просто тупым - но я все равно попробовал).

Он работает уже 6 дней, и я до сих пор не знаю, что делать ... Есть идеи, ребята? Как я могу сделать это быстрее (или, по крайней мере, конечно)?

Обновление: - Хорошо, я отменил запрос и перезагрузил сервер, чтобы снова запустить и запустить ОС - Я перезапускаю рабочий процесс с вашими предлагаемыми изменениями, в частности, обрезая поле nvarchar до гораздо меньшего размера и заменяя «как» на «=». Это займет не менее двух часов, поэтому я буду публиковать дальнейшие обновления позже

Обновление 2 (13:00 по Гринвичу, 18.11.09): - Предполагаемый план выполнения показывает 67% затрат на сканирование таблиц с последующим совпадением хэшей на 33%. Далее идет параллелизм 0% (не странно ли это? Это первый раз, когда я использую примерный план выполнения, но этот конкретный факт просто поднял мою бровь), 0% совпадения хэша, больше параллелизма 0%, 0% вершины, 0 % table insert и, наконец, еще 0% выберите. Кажется, что индексы, как и ожидалось, дерьмовые, поэтому я буду делать ручные индексы и откажусь от дерьмовых предложенных.

Ответы [ 16 ]

17 голосов
/ 17 ноября 2009

Я не эксперт по настройке SQL, но объединение сотен миллионов строк в поле VARCHAR не кажется хорошей идеей в любой системе баз данных, которую я знаю.

Вы можете попробовать добавить столбец целых чисел к каждой таблице и вычислить хеш в поле ИМЯ, который должен получить возможные совпадения с разумным числом, прежде чем движок должен будет просмотреть фактические данные VARCHAR.

7 голосов
/ 18 ноября 2009

Во-первых, соединения по 100M строк вовсе не являются необоснованными или необычными.

Однако я подозреваю, что причина плохой производительности, которую вы видите, может быть связана с предложением INTO. При этом вы не только выполняете объединение, вы также записываете результаты в новую таблицу. Ваше наблюдение по поводу огромного размера файла журнала в основном подтверждает это.

Одна вещь, которую нужно попробовать: удалить INTO и посмотреть, как он работает. Если производительность приемлема, то для устранения медленной записи вы должны убедиться, что ваш файл журнала БД находится на отдельном физическом томе из данных. Если это не так, то при чтении данных и записи журнала головки дисков будут зависать (много запросов), и ваш перфоманс падает (возможно, от 1/40 до 1/60 от того, что могло бы быть иначе ).

7 голосов
/ 17 ноября 2009

Для огромных объединений, иногда явный выбор loop join ускоряет процесс:

SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
FROM EGM 
INNER LOOP JOIN BioEntity 
    ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId

Как всегда, публикация вашего приблизительного плана выполнения может помочь нам дать более точные ответы.

РЕДАКТИРОВАТЬ: Если оба входа отсортированы (они должны быть, с индексом покрытия), вы можете попробовать MERGE JOIN :

SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
FROM EGM 
INNER JOIN BioEntity 
    ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId
OPTION (MERGE JOIN)
6 голосов
/ 18 ноября 2009

Может быть немного оффтоп, но: «Я заметил, что компьютер иногда зависает каждые 30 секунд (в течение нескольких секунд»).

Такое поведение характерно для дешевого массива RAID5 (или, возможно, для одного диска) при копировании (а ваш запрос в основном копирует данные) гигабайтов информации.

Подробнее о проблеме - вы не можете разделить ваш запрос на более мелкие блоки? Как имена, начинающиеся с A, B и т. Д. Или идентификаторы в определенных диапазонах? Это может существенно уменьшить транзакционные / блокирующие накладные расходы.

4 голосов
/ 17 ноября 2009

Я бы попробовал удалить оператор 'LIKE'; поскольку вы, кажется, не делаете подстановочных знаков.

3 голосов
/ 17 ноября 2009

Как рекомендовано, я бы добавил хеш-имя, чтобы сделать объединение более разумным. Я бы настоятельно рекомендовал изучить возможность назначения идентификатора во время импорта пакетов с помощью поиска, если это возможно, поскольку это избавит от необходимости выполнять соединение позднее (и, возможно, неоднократно придется выполнять такое неэффективное соединение).

Я вижу, у вас есть этот индекс для TypeID - это очень помогло бы, если бы оно было избирательным. Кроме того, добавьте столбец с хэшем имени к тому же индексу:

SELECT EGM.Name
       ,BioEntity.BioEntityId
INTO AUX 
FROM EGM 
INNER JOIN BioEntity  
    ON EGM.TypeId = BioEntity.TypeId -- Hopefully a good index
    AND EGM.NameHash = BioEntity.NameHash -- Should be a very selective index now
    AND EGM.name LIKE BioEntity.Name
2 голосов
/ 17 ноября 2009

Еще одно предложение, которое я могу предложить, - попытаться получить подмножество данных вместо обработки всех 100 миллионов строк одновременно для настройки вашего запроса. Таким образом, вам не нужно тратить так много времени на ожидание, чтобы увидеть, когда ваш запрос будет завершен. Тогда вы могли бы рассмотреть возможность проверки плана выполнения запроса, который также может дать представление о существующей проблеме.

1 голос
/ 19 ноября 2009

Я бы попытался решить проблему нестандартно, может быть, есть какой-то другой алгоритм, который мог бы выполнять работу намного лучше и быстрее, чем база данных. Конечно, все зависит от характера данных, но есть довольно быстрый алгоритм поиска строк (Boyer-Moore, ZBox и т. Д.) Или другой алгоритм анализа данных (MapReduce?). При тщательном создании экспорта данных можно было бы согните проблему, чтобы соответствовать более изящному и более быстрому решению. Кроме того, можно было бы лучше распараллелить проблему, и с помощью простого клиента использовать циклы простоя систем вокруг вас, есть структура, которая может помочь с этим.

выводом этого может быть список refid-кортежей, которые вы могли бы использовать для гораздо более быстрого извлечения полных данных из базы данных.

Это не мешает вам экспериментировать с индексом, но если вам придется ждать результаты в течение 6 дней, я думаю, что оправдывает затраченные ресурсы на изучение других возможных вариантов.

мои 2 цента

1 голос
/ 18 ноября 2009

Почему нварчар? Лучше всего, если вам НЕ НУЖНА (или вы ожидаете, что вам понадобится) поддержка юникода, просто используйте varchar. Если вы думаете, что самое длинное имя меньше 200 символов, я бы сделал этот столбец varchar (255). Я вижу сценарии, в которых рекомендованное вам хеширование будет дорогостоящим (кажется, что эта база данных интенсивно вставляется). Однако при таком большом размере, частоте и случайном характере имен ваши индексы быстро фрагментируются в большинстве сценариев, где вы индексируете хеш (зависит от хеша) или имя.

Я бы изменил столбец имени, как описано выше, и сделал бы кластеризованный индекс TypeId, EGMId / BioentityId (суррогатный ключ для любой таблицы). Тогда вы можете легко присоединиться к TypeId, и «грубое» объединение по имени будет иметь меньше циклов. Чтобы увидеть, как долго может выполняться этот запрос, попробуйте выполнить его для очень небольшого подмножества ваших TypeIds, и это должно дать вам оценку времени выполнения (хотя он может игнорировать такие факторы, как размер кэша, объем памяти, скорость передачи жесткого диска).

Редактировать: если это непрерывный процесс, вы должны применить ограничение внешнего ключа между вашими двумя таблицами для будущих импортов / дампов. Если это не происходит, хэширование, вероятно, является лучшим.

1 голос
/ 18 ноября 2009

Повторю несколько предыдущих постов здесь (которые я буду голосовать) ...

Насколько избирателен TypeId? Если у вас есть только 5, 10 или даже 100 различных значений в ваших 100M + строках, индекс ничего не сделает для вас - особенно если вы все равно выбираете все строки.

Я бы посоветовал создать столбец CHECKSUM (Имя) в обеих таблицах. Возможно, сделайте этот постоянный вычисляемый столбец:

CREATE TABLE BioEntity
 (
   BioEntityId  int
  ,Name         nvarchar(4000)
  ,TypeId       int
  ,NameLookup  AS checksum(Name) persisted
 )

, а затем создайте индекс примерно так (я бы использовал кластеризованный, но даже некластеризованный помог бы):

CREATE clustered INDEX IX_BioEntity__Lookup on BioEntity (NameLookup, TypeId)

(Проверьте BOL, существуют правила и ограничения для построения индексов для вычисляемых столбцов, которые могут применяться в вашей среде.)

Выполнено для обеих таблиц, это должно обеспечить очень избирательный индекс для поддержки вашего запроса, если он будет изменен следующим образом:

SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
 FROM EGM INNER JOIN BioEntity 
 ON EGM.NameLookup = BioEntity.NameLookup
  and EGM.name = BioEntity.Name
  and EGM.TypeId = BioEntity.TypeId

В зависимости от многих факторов, он все равно будет работать долго (не в последнюю очередь потому, что вы копируете, сколько данных в новую таблицу?), Но это должно занять меньше дней.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...