Распределение данных

Обзор типов распределения данных

Распределение данных (distribution) — одна из самых важных концепций, лежащих в основе ADB. Она означает хранение данных каждой таблицы на различных сегментах кластера. При этом чем более равномерно данные распределяются между сегментами, тем выше производительность всего кластера. Для этого сегменты должны содержать примерно одинаковые порции данных.

Все таблицы в ADB распределяются между сегментами. При создании или изменении таблицы можно выбрать одну из доступных политик распределения данных (distribution policy) — для определения алгоритма распределения табличных данных между сегментами. Возможные политики приведены в таблице ниже.

Политики распределения данных
Тип Описание Пример

DISTRIBUTED BY (<column(-s)>)

Хеш-распределение. Конкретный сегмент выбирается на основе хешей, которые рассчитываются по полям, указанным в скобках. Рекомендуется использовать для таблиц, имеющих первичные ключи (PRIMARY KEY) либо столбцы с уникальными значениями (UNIQUE) — эти столбцы могут быть использованы в качестве ключа распределения (поля, на основе значений которого выбирается сегмент)

CREATE TABLE test_distr_key (a INT PRIMARY KEY, b TEXT)
DISTRIBUTED BY (a);

DISTRIBUTED REPLICATED

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

CREATE TABLE test_distr_replicated (a TEXT, b TEXT)
DISTRIBUTED REPLICATED;

DISTRIBUTED RANDOMLY

Случайное распределение данных с использованием алгоритма round-robin. Поскольку система выбирает сегменты случайным образом, равномерность распределения данных между ними не гарантируется. Рекомендуется для иных случаев — когда в таблицах нет столбцов с уникальными значениями, а размер таблиц достаточно большой

CREATE TABLE test_distr_random (a TEXT, b INT)
DISTRIBUTED RANDOMLY;

Рисунок ниже показывает в упрощенном виде, как выбрать наиболее оптимальную политику распределения данных.

distribution dark
Выбор политики распределения данных
distribution light
Выбор политики распределения данных

Рекомендации по равномерному распределению данных

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

  1. Всегда явно указывать политику распределения данных при создании таблицы. При отсутствии выражения DISTRIBUTED распределение данных в таблице в последующем будет регулироваться конфигурационным параметром gp_create_table_random_default_distribution. А это в отдельных случаях может привести к выбору произвольного столбца в качестве ключа распределения — если в параметре не указано значение ON. Дополнительную информацию можно получить в документации Greenplum.

  2. Не выбирать в качестве ключей распределения столбцы, данные в которых распределяются неравномерно:

    • Даты и временные метки.

    • Поля, содержащие множество значений NULL.

    • Поля с кастомными типами данных, для которых неизвестно дальнейшее распределение значений.

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

    При необходимости можно настраивать пользовательские хеш-функции для вычисления ключа распределения. Для этого заполните аргумент <opclass> в выражении DISTRIBUTED BY — именем соответствующего оператора (operator). Более подробную информацию о синтаксисе можно найти в статье Таблицы.

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

    SELECT gp_segment_id, * FROM test_distr_key;

    Результат команды:

     gp_segment_id | a |   b
    ---------------+---+-------
                 4 | 1 | first
    (1 row)

    Возвращаемое поле gp_segment_id соответствует номеру сегмента, хранимому в поле content системной таблицы pg_catalog.gp_segment_configuration. Эта таблица содержит подробную информацию о всех сегментах кластера. По умолчанию поле скрыто. Оно может быть получено для всех таблиц, за исключением тех, для которых выбрана политика DISTRIBUTED REPLICATED (так как в этом случае данные дублируются на всех сегментах).

    ВНИМАНИЕ
    Не рекомендуется использовать системное поле gp_segment_id в запросах к большим таблицам на регулярной основе.
  4. При необходимости можно изменить выбранный ранее тип распределения данных одним из описанных способов.

    • Изменение политики:

      ALTER TABLE test_distr_replicated SET DISTRIBUTED RANDOMLY;
    • Перераспределение данных без изменения ключа распределения:

       ALTER TABLE test_distr_key SET WITH (REORGANIZE=TRUE);
  5. Правильное распределение данных влияет на скорость выполнения запросов, особенно запросов JOIN. Для обеспечения максимальной эффективности запросов рекомендуется:

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

    • Учитывать, что соединение таблиц с использованием ключей распределения самое быстрое, поскольку одинаковые данные хранятся на одних и тех же сегментах — даже если эти данные принадлежат различным таблицам. Когда таблицы используют общий ключ распределения, соединение и сортировка по этому ключу выполняется локально — без необходимости распределения кортежей данных по другим сегментам. А локальные операции всегда быстрее, чем распределенные.

    • Использовать политику DISTRIBUTED REPLICATED для небольших и часто используемых таблиц-справочников. Это повышает производительность запросов, позволяя избежать копирования таблицы во все сегменты (так называемый Broadcast Motion).

    • Использовать одинаковые типы данных в полях, на основе которых выполняется соединение таблиц в запросах JOIN.

Примеры

Корректный выбор ключа распределения

 

  1. Создайте две таблицы со структурой, описанной ниже:

    CREATE TABLE t1 (id INT, msg TEXT) DISTRIBUTED BY(id);
    CREATE TABLE t2 (id INT, msg TEXT) DISTRIBUTED BY(id);
  2. Добавьте в таблицы следующие данные:

    INSERT INTO t1 (id, msg) VALUES (1, 'text1'), (2, 'text2');
    INSERT INTO t2 (id, msg) VALUES (1, 'text3'), (2, 'text4');
  3. Убедитесь, что данные с одинаковым значением ключа распределения в обеих таблицах сохранены на одном сегменте:

    SELECT gp_segment_id, * FROM t1;

    Результат команды:

     gp_segment_id | id |  msg
    ---------------+----+-------
                 4 |  1 | text1
                 9 |  2 | text2
    (2 rows)
    SELECT gp_segment_id, * FROM t2;

    Результат команды ниже. Идентификаторы сегментов идентичны приведенным ранее:

     gp_segment_id | id |  msg
    ---------------+----+-------
                 9 |  2 | text4
                 4 |  1 | text3
    (2 rows)
  4. Используйте команду EXPLAIN для вывода плана JOIN-соединения двух таблиц.

    EXPLAIN SELECT * FROM t1 JOIN t2 ON t1.id = t2.id;

    Результат команды:

                                       QUERY PLAN
    ---------------------------------------------------------------------------------
     Gather Motion 16:1  (slice1; segments: 16)  (cost=0.00..862.00 rows=2 width=20)
       ->  Hash Join  (cost=0.00..862.00 rows=1 width=20)
             Hash Cond: (t1.id = t2.id)
             ->  Seq Scan on t1  (cost=0.00..431.00 rows=1 width=10)
             ->  Hash  (cost=431.00..431.00 rows=1 width=10)
                   ->  Seq Scan on t2  (cost=0.00..431.00 rows=1 width=10)
     Optimizer: Pivotal Optimizer (GPORCA)
    (7 rows)

    Вывод команды показывает, что для этого запроса не требуется никаких перемещений данных (Motion), кроме Gather Motion — это стандартное действие, означающее отправку результатов запроса из сегментов в Master Server.

Некорректный выбор ключа распределения

 

     Различные типы данных в ключах

  1. Создайте дополнительную таблицу:

    CREATE TABLE t3 (id INT, msg TEXT) DISTRIBUTED BY(msg);
  2. Добавьте данные в таблицу:

    INSERT INTO t3 (id, msg) VALUES (1, 'text5'), (2, 'text6');
  3. Получите список сегментов, в которых хранятся данные таблицы:

    SELECT gp_segment_id, * FROM t3;

    Результат команды:

     gp_segment_id | id |  msg
    ---------------+----+-------
                 2 |  1 | text5
                10 |  2 | text6
    (2 rows)

    Обратите внимание, что идентификаторы сегментов отличаются от тех, что выводились для таблицы t1 в предыдущем примере.

  4. Используйте команду EXPLAIN для вывода плана JOIN-соединения таблиц t3 и t1.

    EXPLAIN SELECT * FROM t1 JOIN t3 ON t1.id = t3.id;

    Результат команды:

                                                  QUERY PLAN
    -------------------------------------------------------------------------------------------------------
     Gather Motion 16:1  (slice2; segments: 16)  (cost=0.00..862.00 rows=2 width=20)
       ->  Hash Join  (cost=0.00..862.00 rows=1 width=20)
             Hash Cond: (t1.id = t3.id)
             ->  Seq Scan on t1  (cost=0.00..431.00 rows=1 width=10)
             ->  Hash  (cost=431.00..431.00 rows=2 width=10)
                   ->  Broadcast Motion 16:16  (slice1; segments: 16)  (cost=0.00..431.00 rows=2 width=10)
                         ->  Seq Scan on t3  (cost=0.00..431.00 rows=1 width=10)
     Optimizer: Pivotal Optimizer (GPORCA)
    (8 rows)

    По сравнению с предыдущим примером, в выводе команды добавляется действие Broadcast Motion. Во время этой операции происходит копирование таблицы во все сегменты кластера — поскольку в них нет необходимых данных для выполнения JOIN. Такое поведение некритично для таблиц малого размера. Однако в случае соединения больших таблиц оно может вызвать серьезные проблемы с производительностью. Чтобы устранить Broadcast Motion в нашем примере, достаточно изменить ключ распределения в таблице t3 с msg на id.

 
     Выбор неуникального поля в качестве ключа

  1. Создайте дополнительную таблицу:

    CREATE TABLE t4 (id INT, msg TEXT) DISTRIBUTED BY(id);
  2. Добавьте данные в таблицу. Обратите внимание, что значения первого столбца (выбранного в качестве ключа распределения) неуникальны:

    INSERT INTO t4 (id, msg) VALUES (1, 'text7'), (1, 'text8');
  3. Получите список сегментов, в которых хранятся данные таблицы:

    SELECT gp_segment_id, * FROM t4;

    Результат команды:

     gp_segment_id | id |  msg
    ---------------+----+-------
                 4 |  1 | text7
                 4 |  1 | text8
    (2 rows)

    Вывод команды наглядно демонстрирует, что обе записи таблицы хранятся на одном сегменте, поскольку для них задано одно и то же значение ключа распределения. На практике крайне важно избегать таких ситуаций, так как они могут вызвать увеличение нагрузки на отдельных сегментах. Чтобы устранить проблему в нашем примере, достаточно указать в качестве ключа распределения другой, уникальный столбец таблицы. Если это невозможно — изменить политику распределения данных на DISTRIBUTED RANDOMLY.

Нашли ошибку? Выделите текст и нажмите Ctrl+Enter чтобы сообщить о ней