СУБД. Лекция 7

СУБД
Стрелков Никита
Лекция 7

Репликация. Полнотекстовый поиск. JSON.

Организационные моменты

Репликация

Репликация

Репликация

Распространение данных

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

Балансировка нагрузки

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

Реализовать несложное балансирование нагрузки можно, внеся совсем немного изменений в код.

Репликация

Резервное копирование

Репликация ― это ценное подспорье для резервного копирования. Однако подчиненный сервер все же не может использоваться в качестве резервной копии и не является заменой настоящему резервному копированию.

Аварийное переключение на резервный сервер (failover)

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

Репликация

Тестирование новых версий

Очень часто на подчиненный сервер устанавливают новую версию СУБД и перед тем как ставить ее на промышленные серверы, проверяют, что все запросы работают нормально.

Передача изменений в приложение

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

Варианты взаимодействия

Primary-standby (уст. Master-slave)
Подчиненный сервер повторяет состояние главного и не может изменять данные самостоятельно.
Primary-primary
Оба сервера равнозначны и могут обрабатывать запросы как на чтение, так и на изменение данных.

Варианты реализации

Физическая
Передаётся информация о физическом изменении страниц базы данных.
Логическая
Передаётся информация об изменении записей базы данных.
Передача запросов
Передаётся информация о выполненных запросах.

Гарантии репликации

Синхронная
Мастер-сервер не подтверждает транзакцию до того, как реплика не подтвердит получение данных.
Асинхронная
Мастер-сервер не ждёт подтверждения получения данных от реплики.
Majority
Мастер-сервер ждёт подтверждения получения данных от N-реплик.
Семисинхронная (полусинхронная) (MySQL)
Мастер-сервер не подтверждает транзакцию до того, как "живые" реплики не подтвердят получение данных.

Физическая репликация

Общий принцип:

Физическая репликация

Плюсы:

Физическая репликация

Минусы:

Логическая репликация (Slony-I)

Slony использует триггеры PostgreSQL для привязки к событиям INSERT/DELETE/UPDATE и хранимые процедуры для выполнения действий.

Логическая репликация (Logical Decoding)

Общий принцип:

Реализации:

Логическая репликация

Плюсы:

Логическая репликация

Минусы:

Пару слов про кластеры

Общая память

Кластер представляется как одна система (Single-System Image, SSI), то есть эквивалент операционной системы для кластера в целом.

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

Общие диски

Узлы кластера используют единую файловую систему.

Операционная система берет на себя координацию работы с файловой системой и ряд сервисных функций.

Приложение должно явно поддерживать работу в кластере.

Ничего общего

Функции кластера целиком реализуются внутри приложения.

Postgres-XL

Postgres-XL позволяет объединить несколько кластеров PostgreSQL таким образом, чтоб они работали как один инстанс БД.

Для клиента, который подключается в базе, нет никакой разницы, работает он с единственным инстансом PostgreSQL или с кластером Postgres-XL. Postgres-XL предлагает 2 режима распределения таблиц по кластеру: репликация и шардинг.

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

Pgpool II

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

Для клиента, который подключается в базе, нет никакой разницы, работает он с единственным инстансом PostgreSQL или с кластером Pgpool II.

Есть поддержка Failover.

Полнотекстовый поиск

В чем проблема?

Предварительная обработка

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

Словоформы

Запрос: мыла раму
Текст: Мама мыла раму

Запрос: мыть раму
Текст: Мама мыла раму

Запрос: мягкая булка
Текст: Съешь ещё этих мягких французских булок и выпей чаю

Словоформы

Кровать

Глагол, несовершенный вид, переходный, тип спряжения по классификации А. Зализняка — 2b.

Число Лицо Глагол
ед. 1-е я (что делаю) Крую
  2-е ты (что делаешь) Круёшь
  3-е он, она (что делает) Круёт
мн. 1-е мы (что делаем) Круём
  2-е вы (что делаете) Круёте
  3-е они (что делают) Круют

Словари в PostgreSQL

Создавая словари, можно:

Словари для Русского языка

В PostgreSQL по-умолчанию нет словарей для Русского языка, но можно воспользоваться словарями из пакета myspell-ru:

# Устанавливаем словари
sudo apt install myspell-ru
cd /usr/share/postgresql/9.5/tsearch_data
DICT=/usr/share/hunspell/ru_RU
# Копируем словари с преобразованием в UTF-8
sudo iconv -f koi8-r -t utf-8 -o russian.affix $DICT.aff
sudo iconv -f koi8-r -t utf-8 -o russian.dict  $DICT.dic
    

Подключаем словари

CREATE TEXT SEARCH DICTIONARY russian_ispell (
    TEMPLATE = ispell,
    DictFile = russian,
    AffFile = russian,
    StopWords = russian
);


CREATE TEXT SEARCH CONFIGURATION ru (COPY=russian);


ALTER TEXT SEARCH CONFIGURATION ru
    ALTER MAPPING FOR hword, hword_part, word
    WITH russian_ispell, russian_stem;

Проверяем словари

SELECT to_tsvector('russian', 'мама мыла раму');
       to_tsvector
-------------------------
 'мам':1 'мыл':2 'рам':3
(1 строка)


SELECT to_tsvector('ru', 'мама мыла раму');
             to_tsvector
-------------------------------------
 'мама':1 'мыло':2 'мыть':2 'рама':3
(1 строка)

Проверяем словари

SELECT to_tsvector('Съешь ещё этих мягких французских');
                  to_tsvector
------------------------------------------------
 'ещё':2 'мягк':4 'съеш':1 'французск':5 'эт':3
(1 строка)


SET default_text_search_config = 'ru';


SELECT to_tsvector('Съешь ещё этих мягких французских');
                      to_tsvector
-------------------------------------------------------
 'ещё':2 'мягкий':4 'съешь':1 'французский':5 'этих':3
(1 строка)

Собственно поиск

SELECT plainto_tsquery('мягкая булка') @@ to_tsvector(
    'Съешь ещё этих мягких французских булок и выпей чаю');
?column?
----------
t
(1 строка)


SELECT ts_headline('мама мыла милу', to_tsquery('мама'));
      ts_headline
-----------------------
 <b>мама</b> мыла милу
(1 строка)


SELECT ts_rank(to_tsvector(
    'Съешь ещё этих мягких французских булок и выпей чаю'
    ), to_tsquery('мягкая & булка'));
  ts_rank
-----------
 0.0985009
(1 строка)

Веса

SELECT
   setweight(to_tsvector('мама'), 'A') ||
   setweight(to_tsvector('мыла'), 'B') ||
   setweight(to_tsvector('раму'), 'D') @@ to_tsquery('мама:AB');
?column?
----------
t
(1 строка)


SELECT
   setweight(to_tsvector('мама'), 'A') ||
   setweight(to_tsvector('мыла'), 'B') ||
   setweight(to_tsvector('раму'), 'D') @@ to_tsquery('мама:BC');
?column?
----------
f
(1 строка)

Всё вместе

CREATE TABLE documents (
  id    SERIAL PRIMARY KEY,
  title TEXT,
  body  TEXT,
  fts   TSVECTOR
);

CREATE INDEX documents_fts USING GIN ON documents (fts);

INSERT INTO documents (title, body) VALUES
('Хокку', E'Голос модема!\nКак он напоминает\nПенье цикады.');

UPDATE documents
SET fts = setweight(to_tsvector(title), 'A')
       || setweight(to_tsvector(body), 'B');

SELECT * FROM documents WHERE fts @@ to_tsquery('модем');

Географические данные

PostGIS

Установка PostGIS

apt install postgis postgresql-9.5-postgis-scripts
service postgresql restart

Настойка базы данных

CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;

PostGIS: Пример запроса

Расстояние между двумя точками

SELECT ST_Distance(
  ST_GeomFromText('POINT(37.6726 55.7448)'),  -- Moscow
  ST_GeomFromText('POINT(30.3715 59.9401)')); -- St Petersburg

PostGIS: Сфероиды

Что значит "SRID=4326"?

SELECT ST_Distance(
  ST_GeogFromText('SRID=4326;POINT(37.6726 55.7448)'),
  ST_GeogFromText('SRID=4326;POINT(30.3715 59.9401)'));
   st_distance
-----------------
 636751.38845948
(1 строка)

PostGIS: Пример запроса

EXPLAIN SELECT * FROM bookings.airports WHERE ST_DWithin(
    Geography(ST_SetSRID(ST_POINT(longitude, latitude), 4326)),
    ST_GeogFromText('SRID=4326;POINT(37.6726 55.7448)'),
    100 * 1000);

PostGIS: Пример запроса

CREATE INDEX idx_airports ON bookings.airports USING GIST
    (Geography(ST_SetSRID(ST_POINT(longitude, latitude), 4326)));

EXPLAIN SELECT * FROM bookings.airports WHERE ST_DWithin(
    Geography(ST_SetSRID(ST_POINT(longitude, latitude), 4326)),
    ST_GeogFromText('SRID=4326;POINT(37.6726 55.7448)'),
    100 * 1000);

Поиграться можно с базой: https://edu.postgrespro.ru/bookings.pdf

Хранение слабоструктурированных данных

Хранение слабоструктурированных данных

Зачем?

Хранение слабоструктурированных данных

Как хранить такие данные?

JSON: Пример запроса

CREATE TABLE movies_json (
  id INT PRIMARY KEY,
  data JSONB
);

INSERT INTO movies_json (id, data)
SELECT m.id, json_build_object(
  'title', title,
  'year', year,
  'genres', (
    SELECT array_to_json(array_agg(name))
    FROM movie_genres mg JOIN genres g ON (mg.genre_id = g.id)
    WHERE mg.movie_id = m.id
  ),
  'tags', (
    SELECT array_to_json(COALESCE(array_agg(distinct name), array[]::text[]))
    FROM movie_tags mt JOIN tags t ON (mt.tag_id = t.id)
    WHERE mt.movie_id = m.id
  )
)
FROM movies m;

JSON: Пример запроса

SELECT data FROM movies_json
WHERE data->>'title' = 'Zombieland';
{
  "tags": [
    "Abigail Breslin",
    "atlanta",
    "awkward romance",
    "banjo",
    "Bill Murray",
    "Woody Harrelson",
    "zombies"
  ],
  "year": 2009,
  "title": "Zombieland",
  "genres": [
    "Horror",
    "Action",
    "Comedy"
  ]
}

JSON: Операции

A @>B, B <@ A
A является подмножеством B
A ?| array [B, C]
Проверяет, есть ли в A ключи/значения B или C
A ?& array [B, C]
Проверяет, есть ли в A ключи/значения B и C
A -> B, A ->> B
Получение из A элемента по ключу B
A #> '{B, C}', A #>> '{B, C}'
Получает из A элемент по пути A -> B -> C

JSON: Пример запроса

SELECT data->>'title', data->>'year', data->'genres'
FROM movies_json
WHERE data @> '{"title": "Zombieland"}';

CREATE INDEX idx_movies_json ON movies_json
USING GIN (data jsonb_path_ops);

VS

SELECT data->>'title', data->>'year', data->'genres'
FROM movies_json
WHERE data ->> 'title' = 'Zombieland';

CREATE INDEX idx_movies_json_title ON movies_json
((data ->> 'title') text_pattern_ops);

JSON: Пример запроса

SELECT data ->> 'title'
FROM movies_json
WHERE data @> '{"genres": ["Comedy"], "tags": ["zombie"]}';

SELECT data ->> 'title'
FROM movies_json j
WHERE data @> '{"genres": ["Comedy"]}'
  AND EXISTS (
    SELECT value
    FROM jsonb_array_elements_text(j.data -> 'tags')
    WHERE value LIKE 'zombie%'
  );
Стрелков Никита
E-mail: nikita.strelkov@gmail.com
Спасибо за внимание!