Внутреннее устройство Teradata и оптимизация SQL-запросов

Курс посвящен архитектуре массивно-параллельной обработки Teradata [dwhpro.com](https://www.dwhpro.com/teradata-sql-tuning-guide/) и методам написания высокопроизводительных запросов. Вы изучите работу оптимизатора [docs.teradata.com](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Request-and-Transaction-Processing/Query-Rewrite-Statistics-and-Optimization/Query-Optimizers), анализ EXPLAIN-планов и стратегии индексирования [medium.com](https://medium.com/@guruprasadnookala65/mastering-teradata-performance-tuning-best-practices-for-sql-optimization-834dccbaa375).

1. Архитектура Teradata: массивно-параллельная обработка и распределение данных

Архитектура Teradata: массивно-параллельная обработка и распределение данных

Представьте, что вам нужно найти одну конкретную транзакцию среди 50 миллиардов записей в базе данных крупного банка. Традиционная реляционная база данных будет последовательно сканировать жесткие диски, что займет часы. Teradata справляется с этой задачей за доли секунды. Секрет такой производительности кроется не в использовании суперкомпьютеров с невероятной тактовой частотой, а в фундаментальном принципе разделяй и властвуй. Этот принцип в мире баз данных называется массивно-параллельной обработкой (Massively Parallel Processing, MPP).

Что такое массивно-параллельная обработка (MPP)?

Если вам нужно пересчитать гору из миллиона монет, вы можете делать это в одиночку целый день. Но если вы позовете 1000 друзей, разделите монеты поровну и попросите каждого посчитать свою кучку, работа будет выполнена за пару минут. Именно так работает MPP-архитектура.

В основе Teradata лежит концепция Shared-Nothing (архитектура без разделения ресурсов). Это означает, что каждый процессор в системе имеет собственную выделенную оперативную память и собственный жесткий диск. Процессоры не конкурируют за ресурсы и не ждут друг друга, что позволяет системе масштабироваться практически бесконечно.

> Архитектура без разделения ресурсов (Shared-Nothing) означает, что каждый узел системы полностью независим и автономен, что исключает возникновение единой точки отказа на уровне аппаратных ресурсов и позволяет достичь линейной масштабируемости. > > Документация Greenplum

Для наглядности сравним традиционную архитектуру симметричной мультипроцессорности (SMP) и MPP.

| Характеристика | SMP (Традиционные БД) | MPP (Teradata) | | --- | --- | --- | | Разделение ресурсов | Все процессоры делят общую память и диски | Каждый процессор имеет свои память и диски | | Масштабируемость | Ограничена (добавление процессоров вызывает очереди к памяти) | Линейная (добавление узлов пропорционально ускоряет работу) | | Узкое место | Системная шина и дисковая подсистема | Пропускная способность сети между узлами | | Идеально для... | OLTP (быстрые короткие транзакции) | OLAP (сложная аналитика на петабайтах данных) |

Анатомия системы: PE, BYNET и AMP

Как сотни независимых процессоров работают как единый слаженный механизм? Архитектура Teradata состоит из трех главных компонентов, каждый из которых выполняет строго свою роль.

Parsing Engine (PE): Мозг системы

Parsing Engine — это диспетчер и координатор. Когда вы отправляете SQL-запрос, он попадает именно сюда. PE не хранит данные пользователей, его задача — понять, что вы хотите, и составить оптимальный план действий.

PE выполняет четыре шага:

  • Проверяет синтаксис запроса и права доступа пользователя.
  • Оптимизирует запрос (выбирает самый быстрый путь получения данных).
  • Генерирует пошаговый план выполнения.
  • Отправляет команды рабочим процессорам и собирает от них финальный результат.
  • BYNET: Нервная система

    Чтобы мозг мог управлять мышцами, нужна нервная система. В Teradata эту роль играет BYNET (Banyan Network) — высокоскоростная внутренняя сеть, связывающая все компоненты.

    BYNET умеет не только передавать сообщения, но и аппаратно сортировать данные на лету. Если вы делаете запрос с сортировкой ORDER BY, данные от рабочих процессоров сливаются через BYNET, и к моменту попадания в PE они уже отсортированы.

    Access Module Processor (AMP): Рабочие мускулы

    Access Module Processor — это рабочая лошадка Teradata. Именно AMP физически читает и пишет данные на свои виртуальные диски (vDisks). Каждый AMP полностью изолирован: он ничего не знает о данных, которые лежат на соседнем AMP.

    Если в системе 100 AMP, и вы загружаете таблицу из 100 миллионов строк, каждый AMP получит примерно по 1 миллиону строк. Когда приходит команда на поиск, все 100 AMP одновременно начинают искать нужную информацию в своем миллионе записей.

    Магия хеширования: как Teradata распределяет данные

    Чтобы параллельная обработка была эффективной, данные должны быть распределены равномерно. Если 90% данных окажется на одном AMP, система будет работать со скоростью этого единственного перегруженного процессора.

    Для равномерного распределения Teradata использует алгоритм хеширования, основанный на Первичном индексе (Primary Index, PI). При создании таблицы разработчик указывает колонку (или набор колонок), которая станет PI.

    Процесс записи новой строки выглядит так:

  • PE берет значение Первичного индекса из новой строки.
  • Значение пропускается через математическую хеш-функцию, которая выдает 32-битное число — Row Hash.
  • Система определяет номер корзины (Bucket) с помощью операции взятия остатка от деления:
  • Где: * — номер корзины в карте хешей (Hash Map). * — 32-битное число, сгенерированное хеш-функцией. * — общее количество корзин в системе (в современных версиях Teradata это ).

  • Карта хешей указывает, какому AMP принадлежит вычисленная корзина. Строка отправляется по BYNET на этот AMP и сохраняется на его диске.
  • Рассмотрим на конкретном примере с числами. Допустим, мы добавляем клиента в таблицу:

    Если CustomerID является Первичным индексом, Teradata берет число 1001 и хеширует его. Допустим, хеш-функция выдает число 87654321. При делении на остаток (Bucket) равен 615089. Карта хешей говорит, что корзина 615089 закреплена за AMP №14. Строка летит на AMP №14 и записывается там.

    Выполнение SQL-запроса под капотом

    Теперь, когда мы знаем, как данные хранятся, давайте посмотрим, как они извлекаются. Существует два основных сценария: точечный поиск и полное сканирование.

    Сценарий 1: Точечный поиск (Point Query)

    Пользователь ищет конкретного клиента:

  • PE получает запрос и видит, что поиск идет по Первичному индексу (CustomerID = 1001).
  • PE мгновенно пропускает 1001 через хеш-функцию и вычисляет, что данные лежат на AMP №14.
  • PE отправляет точечное сообщение через BYNET: "AMP №14, дай мне строку с хешем 87654321". Остальные 99 AMP в системе в это время отдыхают или обслуживают другие запросы.
  • AMP №14 находит строку на своем диске и возвращает ее в PE.
  • PE отдает результат пользователю.
  • Время выполнения такого запроса составляет миллисекунды, независимо от того, лежит в таблице тысяча строк или сто миллиардов.

    Сценарий 2: Полное сканирование (Full Table Scan)

    Пользователь хочет узнать, сколько клиентов живет в Москве:

    Поскольку колонка City не является Первичным индексом, PE не знает, на каких AMP лежат нужные строки.

  • PE отправляет широковещательное сообщение (Broadcast) через BYNET: "Всем AMP! Срочно посчитайте клиентов из Москвы на своих дисках!".
  • Все 100 AMP одновременно начинают сканировать свои диски. Если в таблице 100 миллионов строк, каждому AMP нужно просмотреть всего 1 миллион.
  • Каждый AMP находит своих московских клиентов, считает их и отправляет промежуточный итог (например, AMP №1 нашел 5000, AMP №2 нашел 4800 и т.д.).
  • BYNET суммирует эти числа по пути к PE.
  • PE получает финальную цифру и отдает ее пользователю.
  • Именно благодаря этому механизму аналитические запросы над гигантскими массивами данных выполняются с поразительной скоростью. Вся тяжелая работа распределяется между независимыми процессорами.

    Итоги

    Teradata использует архитектуру Shared-Nothing* (MPP), где каждый процессор (AMP) имеет собственную память и диск, что обеспечивает линейную масштабируемость системы. * Жизненный цикл запроса управляется тремя компонентами: Parsing Engine (мозг/диспетчер), BYNET (сеть/транспорт) и AMP (рабочие узлы/хранилище). * Данные распределяются по AMP с помощью алгоритма хеширования на основе Первичного индекса (Primary Index). Это гарантирует равномерную загрузку всех узлов. * При поиске по Первичному индексу в работе участвует только один AMP, что обеспечивает миллисекундный отклик. При поиске по другим колонкам все AMP работают параллельно, сканируя только свою часть данных.

    2. Стратегии индексирования: Primary, Secondary и Hash индексы

    Стратегии индексирования: Primary, Secondary и Hash индексы

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

    В предыдущей статье мы выяснили, что Teradata использует алгоритм хеширования для распределения данных по независимым процессорам (AMP). Этот механизм опирается на один ключевой столбец. Однако реальные аналитические запросы редко ограничиваются поиском только по одному атрибуту. Чтобы избежать полного сканирования при поиске по другим критериям, применяются различные стратегии индексирования.

    Первичный индекс (Primary Index): Фундамент распределения

    Первичный индекс (Primary Index, PI) — это обязательный элемент архитектуры Teradata. В отличие от традиционных реляционных баз данных, где первичный ключ служит только для обеспечения уникальности строк, PI в Teradata выполняет физическую функцию: он определяет, на каком конкретном AMP будет храниться строка.

    Существует два вида первичных индексов:

  • Уникальный первичный индекс (Unique Primary Index, UPI)
  • Неуникальный первичный индекс (Non-Unique Primary Index, NUPI)
  • UPI: Идеальный баланс

    Если столбец назначен как UPI, система гарантирует, что каждое значение в нем уникально. Это идеальный сценарий для массивно-параллельной обработки.

    Допустим, у нас есть таблица клиентов банка из 100 миллионов записей, и мы назначаем Account_ID в качестве UPI. Поскольку каждый номер счета уникален, хеш-функция сгенерирует 100 миллионов уникальных хешей. Если в системе работает 100 AMP, каждый процессор получит ровно по 1 миллиону строк. Нагрузка распределена идеально.

    NUPI: Риск перекоса данных

    Иногда в таблице нет уникального столбца, подходящего для распределения. Например, в таблице транзакций розничной сети мы можем выбрать Store_ID (номер магазина) в качестве NUPI.

    Это решение таит в себе опасность, называемую перекосом данных (Data Skew). Алгоритм хеширования всегда отправляет одинаковые значения на один и тот же AMP. Если флагманский магазин в центре Москвы генерирует 500 000 чеков в день, а маленький магазин в спальном районе — всего 5 000 чеков, то AMP, отвечающий за московский магазин, получит в 100 раз больше данных.

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

    | Характеристика | UPI (Уникальный) | NUPI (Неуникальный) | | --- | --- | --- | | Уникальность значений | Строго обязательна | Допускаются дубликаты | | Распределение данных | Максимально равномерное | Возможен перекос (Data Skew) | | Скорость точечного поиска | Доли секунды (работает 1 AMP) | Быстро, но 1 AMP возвращает несколько строк | | Пример использования | Номер паспорта, ID транзакции | Дата покупки, ID филиала |

    Вторичные индексы (Secondary Indexes): Альтернативные маршруты

    Что делать, если данные распределены по Account_ID, но оператору колл-центра нужно найти клиента по номеру телефона? Без дополнительных структур Parsing Engine отправит команду всем AMP сканировать свои диски. Для таблицы в несколько терабайт это займет минуты.

    Здесь на помощь приходит Вторичный индекс (Secondary Index, SI). Физически это отдельная скрытая субтаблица, которую Teradata создает и поддерживает автоматически.

    Уникальный вторичный индекс (USI)

    Уникальный вторичный индекс (Unique Secondary Index, USI) используется для столбцов с уникальными значениями (например, номер телефона или email).

    Поиск по USI — это всегда двухэтапная операция, в которой участвуют ровно два AMP:

  • Вы ищете клиента с номером +7-999-123-45-67.
  • Parsing Engine хеширует этот номер и понимает, что запись вторичного индекса лежит на AMP №45.
  • AMP №45 читает индексную субтаблицу. В ней написано: "Клиент с таким телефоном имеет базовый хеш, который хранится на AMP №12".
  • Запрос перенаправляется на AMP №12, который мгновенно извлекает полную строку клиента.
  • Время выполнения такого запроса составляет около 10-20 миллисекунд. Мы избежали сканирования 100 миллионов строк, обратившись всего к двум процессорам.

    Неуникальный вторичный индекс (NUSI)

    Если мы часто ищем клиентов по городу (City), мы можем создать Неуникальный вторичный индекс (Non-Unique Secondary Index, NUSI).

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

    При запросе SELECT * FROM Customers WHERE City = 'Казань'; система задействует все AMP. Однако вместо того, чтобы читать 100 ГБ базовой таблицы, каждый AMP читает свою крошечную индексную субтаблицу размером, например, 50 МБ. Найдя нужные указатели в индексе, AMP точечно извлекает строки из базовой таблицы. Это снижает нагрузку на дисковую подсистему в сотни раз.

    Хеш-индексы (Hash Indexes): Специализированный инструмент

    Хеш-индекс (Hash Index) в Teradata — это продвинутая структура, которая позволяет не просто находить строки, но и полностью исключить обращение к базовой таблице при определенных запросах.

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

    Если аналитик выполнит запрос SELECT Name, Email FROM Customers WHERE City = 'Самара';, оптимизатор Teradata заметит, что все запрашиваемые данные (Name, Email) уже физически лежат внутри хеш-индекса Customer_City_Idx.

    Такой индекс называется покрывающим индексом (Covering Index). Система прочитает только компактную структуру хеш-индекса и вообще не будет трогать тяжелую базовую таблицу. Если базовая таблица содержит 150 столбцов и весит 5 ТБ, а хеш-индекс содержит всего 3 столбца и весит 50 ГБ, выигрыш в производительности будет колоссальным.

    Цена оптимизации: почему нельзя индексировать всё

    Узнав о мощи индексов, начинающие разработчики часто совершают классическую ошибку — создают индексы для каждого столбца, который когда-либо появлялся в условии WHERE.

    > Индексы подобны лекарствам: в правильных дозах они спасают систему, в чрезмерных — убивают её производительность. > > Ральф Кимбалл, эксперт по проектированию хранилищ данных

    Каждый вторичный или хеш-индекс — это физическая таблица на жестком диске. Когда вы добавляете новую строку в базовую таблицу (INSERT), Teradata обязана обновить все связанные индексы.

    Рассмотрим математику процесса. Допустим, запись одной строки в базовую таблицу занимает 5 миллисекунд. Обновление одного индекса требует еще 3 миллисекунды. Время записи = Базовое время + (Количество индексов × Время обновления индекса).

    Если на таблице нет индексов, вставка 10 000 строк займет 50 секунд. Если на таблице висит 5 индексов, время записи одной строки вырастает до миллисекунд. Вставка тех же 10 000 строк займет уже 200 секунд. Скорость загрузки данных падает в 4 раза.

    Поэтому в аналитических хранилищах данных (OLAP), таких как Teradata, где каждую ночь загружаются терабайты новой информации, количество индексов строго лимитируют. Индексы создаются только для тех столбцов, которые критически важны для самых тяжелых и частых отчетов.

    Итоги

    * Первичный индекс (PI) обязателен для каждой таблицы и определяет физическое распределение данных по AMP. Уникальный PI гарантирует равномерную загрузку, а неуникальный может привести к перекосу данных (Data Skew). * Уникальный вторичный индекс (USI) позволяет находить конкретную строку за миллисекунды, задействуя всего два AMP, минуя полное сканирование таблицы. * Неуникальный вторичный индекс (NUSI) задействует все AMP, но кардинально ускоряет поиск за счет сканирования компактной индексной субтаблицы вместо огромной базовой таблицы. * Хеш-индексы могут выступать в роли покрывающих индексов, отдавая результаты запроса напрямую из своей структуры без обращения к базовым данным. * Любой индекс замедляет операции записи (INSERT, UPDATE, DELETE), поэтому их создание требует строгого баланса между скоростью чтения и скоростью загрузки данных.