Схемы шардинга в PostgreSQL

92d3f6ecd0dbe87908cf246ef8c4bc3a.jpg

Привет, Хабр!

Шардинг — это процесс разделения горизонтальных частей данных на разные БД, или шарды, которые могут располагаться на различных серверах или даже в разных геолокациях. Шардинг не просто режет данные на части, но организует их таким образом, чтобы каждый шард мог быть оптимизирован для выполнения определенного набора задач

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

В этой статье кратко рассмотрим то, как реализуются различные схемы шардина в PostgreSQL

Реализация схем шардинга

Горизонтальный шардинг

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

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

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

Допустим, есть таблица с заказами orders, которую нужно партиционировать по годам:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    order_date DATE NOT NULL,
    customer_id INT NOT NULL,
    amount DECIMAL NOT NULL
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2020 PARTITION OF orders FOR VALUES FROM ('2020-01-01') TO ('2021-01-01');
CREATE TABLE orders_2021 PARTITION OF orders FOR VALUES FROM ('2021-01-01') TO ('2022-01-01');
-- и так далее для каждого года

При вставке данных в таблицу orders, PostgreSQL автоматически направит данные в соответствующую партицию на основе значения order_date.

Если есть таблица пользователей, которую нужно партиционировать по стране пользователя, можно сделать это следующим образом:

CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    country TEXT NOT NULL
) PARTITION BY LIST (country);

CREATE TABLE users_usa PARTITION OF users FOR VALUES IN ('USA');
CREATE TABLE users_canada PARTITION OF users FOR VALUES IN ('Canada');
CREATE TABLE users_germany PARTITION OF users FOR VALUES IN ('Germany');
-- и так далее для каждой страны

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

CREATE FUNCTION orders_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
    IF (NEW.order_date < '2021-01-01') THEN
        INSERT INTO orders_2020 VALUES (NEW.*);
    ELSIF (NEW.order_date >= '2021-01-01' AND NEW.order_date < '2022-01-01') THEN
        INSERT INTO orders_2021 VALUES (NEW.*);
    END IF;
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER orders_before_insert
BEFORE INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION orders_insert_trigger();

Перед вставкой в таблицу orders триггер проверяет дату заказа и перенаправляет запись в соответствующую партицию.

Вертикальный шардинг

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

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

Предположим, есть таблица users, которая содержит большое количество столбцов, некоторые из которых используются гораздо чаще, чем другие. Для оптимизации можно разделить эту таблицу на две: одна будет содержать часто используемые данные например, user_id, name, email, а другая — менее часто используемые данные например, address, phone_number и т.д.

-- Таблица для часто используемых данных
CREATE TABLE users_common (
    user_id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL
);

-- Таблица для менее часто используемых данных
CREATE TABLE users_details (
    user_id INTEGER NOT NULL REFERENCES users_common(user_id),
    address TEXT,
    phone_number TEXT,
    CONSTRAINT pk_users_details PRIMARY KEY (user_id)
);

Для реализации вертикального шардинга на нескольких серверах можно использовать FDW, который позволяет PostgreSQL взаимодействовать с данными, хранящимися за пределами текущей базы данных. Используя FDW, можно создать логическое разделение столбцов на разных серверах.

Допустим, хочется хранить детальную информацию о пользователях на отдельном сервере.

  1. Устанавливаем расширение postgres_fdw на основном сервере:

CREATE EXTENSION postgres_fdw;
  1. Создаем серверную конфигурацию для доступа к внешнему серверу:

CREATE SERVER foreign_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'foreign_host', dbname 'foreign_db', port '5432');
  1. Создаем пользовательское отображение для внешнего сервера:

CREATE USER MAPPING FOR local_user
SERVER foreign_server
OPTIONS (user 'foreign_user', password 'foreign_password');
  1. Создаем внешнюю таблицу, которая будет представлять собой вертикальный шард на внешнем сервере:

CREATE FOREIGN TABLE users_details_foreign (
    user_id INTEGER NOT NULL,
    address TEXT,
    phone_number TEXT
) SERVER foreign_server
OPTIONS (schema_name 'public', table_name 'users_details');

Функциональный шардинг

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

Например, разделим данные пользователей и заказов по разным схемам в рамках одной базы данных

CREATE SCHEMA users_data;
CREATE SCHEMA orders_data;

Создадим таблицы в соответствующих схемах

-- Таблица пользователей в схеме users_data
CREATE TABLE users_data.users (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now()
);

-- Таблица заказов в схеме orders_data
CREATE TABLE orders_data.orders (
    order_id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users_data.users(user_id),
    order_date DATE NOT NULL,
    amount DECIMAL(10, 2) NOT NULL
);

Для доступа к данным в разных схемах, просто укзазываем полное имя таблицы, включая имя схемы:

-- Вставка данных в таблицу пользователей
INSERT INTO users_data.users (username, email) VALUES ('user1', 'user1@example.com');

-- Вставка данных в таблицу заказов
INSERT INTO orders_data.orders (user_id, order_date, amount) VALUES (1, '2023-01-01', 100.00);

Можно так же юзать FDW:

CREATE EXTENSION postgres_fdw;

Создадим сервер FDW:

CREATE SERVER remote_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'remote_host', dbname 'remote_db', port '5432');

Создадим пользователя:

CREATE USER MAPPING FOR CURRENT_USER
SERVER remote_server
OPTIONS (user 'remote_user', password 'remote_password');

Создадим внешнюю таблицу:

CREATE FOREIGN TABLE users_data.users_remote (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now()
) SERVER remote_server OPTIONS (schema_name 'public', table_name 'users');

В завершение хочу порекомендовать бесплатный вебинар курса BI-аналитика.

© Habrahabr.ru