Понедельник, 9 утра, сообщение в рабочем чате: "Всё сломалось, почините". Согласитесь, неприятная ситуация, особенно когда это ваш первый месяц работы, а сломалось что-то в функционале, с которым вы ещё ни разу не контактировали, да и не трогал его уже никто месяцами.

Моя реакция в тот момент
Моя реакция в тот момент

Так что случилось?

Сразу после просмотра логов всё становится ясно (нет): Timeout error от MS SQL сервера в модуле, который вызывается из клиентского приложения и обрабатывает документы за определённый период, переводит их в систему планирования и просчитывает различные временные и денежные показатели, после чего это всё становится доступным в другом модуле для различной оптимизации и логистики.

Так как тупо увеличить максимально возможное время выполнения процедур в настройках сервера это подход для слабаков, пришлось копать. Спустя несколько тестов и анализов планов выполнения, проблема найдена - справочник, получаемый в формате xml, записывается в табличную переменную. После очередного обновления статистики собрался новый план выполнения для процедуры, который максимально не эффективно джойнит эту переменную с основной выборкой. Моментальное решение - перейти на temp table, заменив символ @ на # в названии создаваемой таблицы (то что они по разному создаются в коде, одно через declare, другое через create, мы опускаем, чтобы не портить заголовок). Тестируем, получаем время выполнения в 1-2 секунды, радуемся, льём в прод, все счастливы (особенно я).

Я в глазах пользователей после этой истории (по моему скромному представлению)
Я в глазах пользователей после этой истории (по моему скромному представлению)

А в чём разница?

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

No DDL is allowed on table variables. So if you have a large rowset which needs to be queried often, you may want to use #table when possible.

Но помимо отсутствия DDL, разница всё же более серьёзная:

#TABLE

@TABLE

Хранение в памяти

Хранится в tempdb

Хранится в tempdb, но имеет функционал передачи как переменная

Возможность менять структуру после создания

Да

Нет

Возможность создания индексов и ограничений

Да

Только первичный ключ в момент создания

Доступ

Внутри сессии

Внутри пакета транзакций одной сессии

Время жизни

Пока активна сессия

Пока действует один пакет транзакций

Использование в функциях

Нет

Да

Удобства с использовании

Можно создать и заполнить через команду select into

Не нужно проверять на наличие и удалять при перезапуске куска кода во время разработки

Параллельность

Да

Нет

Имеет статистику

Да

Нет

В целом - это почти вся общая информация, которую легко найти, загуглив "ms sql temp table vs table variable", но понять причину конкретно моей ситуации это не помогает, ведь мне не пришлось создавать индексы или использовать прочие фокусы временных таблиц, достаточно было просто поменять 1 символ и всё.

Под капотом

Когда-то давно, вопрос про различие этих двух механизмов привёл меня на достаточно старый пост. Один из отрывков гласил:

Many of the execution plans involving table variables will show a single row estimated as the output from them. Inspecting the table variable properties shows that SQL Server believes the table variable has zero rows

However the results shown in the previous section do show an accurate rows count in sys.partitions. The issue is that on most occasions the statements referencing table variables are compiled while the table is empty

И действительно, план выполнения процедуры с временной переменной гласил - скорее всего в вашей таблице 1 запись, так что запрос строим так, а не иначе. План выполнения был сгенерирован так, будто мы джойним таблицу всего с 1 строкой, так что можно не особо напрягаться за оптимизацию.

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

Заместо итога

На этом у меня пока всё. Это была моя первая попытка писательства, так-что прошу сильно не кидаться тапками за скупость языка или речевые обороты. Решил попробовать себя в этой стезе, так как услышал, что это помогает в обобщении своих знаний и поднятии навыков в целом. Спасибо за прочтение.

Комментарии (13)


  1. gleb_l
    03.11.2021 17:36
    +1

    Первичный ключ был задан в исходной табличной переменной? Если это справочник, то, полагаю, он должен там быть.


    1. zomblzum Автор
      04.11.2021 21:00

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


  1. KaiOvas
    03.11.2021 18:30
    +2

    Интересно понять что в XML справочнике и сколько строк он в таблице занимал. Когда нужно справочниких хранить которые актуальны некоторый промежуток времени (час-сутки-неделя-месяц) то я делал постоянную таблицу(таблицы) и туда их заносил. Конечно нужно слегка подумать над первичным ключем и правильной реализацией "протухания" сравочника но передавать его в запросе из клиента, насколько я понимаю это ваша ситуация, я бы точно не стал. Если справочник используется не для одноразовой выборки то он точно должен быть положен в постоянную таблицу.


    1. zomblzum Автор
      04.11.2021 20:54

      Сейчас он действительно лежит в постоянной таблице, что сильно облегчает жизнь. До этого это была таблица с полем XML, в котором хранились разные справочники, и ключевое поле по названию. Обновление информации в таблице справочников происходило джобом из 1с сервиса раз в какое-то время. Видимо потребность в этой информации изначально была не очень большой (да и число записей изначально не предполагалось особо большим), но потом наросло. Функционал же - определение синонимов для объектов: код и название основного объекта, код и название замены.


  1. hmpd
    04.11.2021 13:27
    +2

    Да, главная проблема с табличными переменными - это Estimated Number of Rows = 1. Если потом в плане идет какой-нибудь большой Join или Key Lookup, пиши пропало.

    С временными таблицами в tempdb можно делать практически что угодно. Особенно удобно, что SQL Server по ним сам статистику успевает строить - Execution Plans получаются приличные. Если план строится не очень хороший, можно поэкспериментировать с индексами по временной таблице.


  1. ldvo
    04.11.2021 21:04
    +1

    Только первичный ключ в момент создания

    Не только - еще можно добавить уникальный ключ. Итого два индекса в момент создания


    1. zomblzum Автор
      04.11.2021 21:06

      Спасибо за уточнение, дополнительный индекс действительно можно добавить, указав Unique constraint, тем самым заставить сервер создать для нас индекс.


    1. Jovanny
      12.11.2021 22:34
      +1

      Да любое количество индексов. С каждой новой версией этот функционал расширялся. Начиная с SQL Server 2016 (более старой версии нет в наличии) этот код работает

      DECLARE @t TABLE
      (c1 int INDEX IX_c1, 
       c2 int INDEX IX_c2, 
       c3 int INDEX IX_c3,
       INDEX IX_c1_c2_c3 (c1, c2, c3)
      );


  1. Victory98
    06.11.2021 09:26

    Отличная статья, полезно!


    1. zomblzum Автор
      06.11.2021 09:26
      -1

      Спасибо большое!


    1. LuckyStarr
      06.11.2021 15:00

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


      1. zomblzum Автор
        06.11.2021 15:11

        Но действительно совпадение, никому из знакомых не давал ссылки, сам вторых аккаунтов не имею. Может так понравилось человеку?)


  1. m0ntana
    08.11.2021 22:50
    +1

    Добрый вечер, спасибо за статью. Эта проблема была решена с помощью deferred compilation in SQL Server 2019. Please find details in https://www.mssqltips.com/sqlservertip/5662/table-variable-deferred-compilation-in-sql-server/