Перед проведением экспериментов нужно запустить контейнер с подключением к PostgreSQL
python3 upstart.py -s psql
Пример создания массива
-- создаем таблицу, у которой значения являются массивами CREATE TABLE holiday_picnic ( holiday varchar(50) -- строковое значение sandwich text[], -- массив side text[] [], -- многомерный массив dessert text ARRAY, -- массив beverage text ARRAY[4] -- массив из 4-х элементов ); -- вставляем значения массивов в таблицу INSERT INTO holiday_picnic VALUES ('Labor Day', '{"roast beef","veggie","turkey"}', '{ {"potato salad","green salad"}, {"chips","crackers"} }', '{"fruit cocktail","berry pie","ice cream"}', '{"soda","juice","beer","water"}' );
Есть много встроенных типов данных
Name | Storage Size | Representation | Description |
---|---|---|---|
point | 16 bytes | Point on a plane | (x,y) |
line | 32 bytes | Infinite line (not fully implemented) | ((x1,y1),(x2,y2)) |
lseg | 32 bytes | Finite line segment | ((x1,y1),(x2,y2)) |
box | 32 bytes | Rectangular box | ((x1,y1),(x2,y2)) |
path | 16+16n bytes | Closed path (similar to polygon) | ((x1,y1),...) |
path | 16+16n bytes | Open path | [(x1,y1),...] |
polygon | 40+16n | Polygon (similar to closed path) | ((x1,y1),...) |
circle | 24 bytes | Circle | <(x,y),r> (center point and radius) |
Пример, зачем это нужно: https://habr.com/post/245015/
Хранить более эффективно, чем в виде строк
CREATE TYPE week AS ENUM ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun');
Команда pg_database_size вычисляет размер БД в байтах
SELECT pg_size_pretty(pg_database_size(current_database()));
Результат
pg_size_pretty ---------------- 3000 MB (1 row)
Команда формирует список таблиц, которые были созданы пользователем
SELECT table_name FROM information_schema.tables WHERE table_schema NOT IN ('information_schema','pg_catalog');
Результат:
table_name
------------
links
ratings
(2 rows)
```shell
Описание таблицы можно получить при помощи команды \d
```shell
\d ratings
Результат:
Table "movie.ratings"
Column | Type | Collation | Nullable | Default
-----------+------------------+-----------+----------+---------
userid | bigint | | |
movieid | bigint | | |
rating | double precision | | |
timestamp | bigint | | |
Можно узнать размер таблицы
SELECT pg_size_pretty(pg_relation_size('movie.ratings'));
Результат:
pg_size_pretty
----------------
45 MB
(1 row)
Или полный размер данных (вместе с индексами и т.д.)
SELECT pg_size_pretty(pg_total_relation_size('movie.ratings'));
Результат:
pg_size_pretty
----------------
45 MB
(1 row)
Размер данных в конкретном столбце
SELECT pg_size_pretty(SUM(pg_column_size(userId)))
FROM movie.ratings;
Результат:
pg_size_pretty
----------------
397 MB
(1 row)
Запрос, который выводит информацию об активных запросах.
SELECT
pid,
age(query_start, clock_timestamp()),
usename, query, backend_type
FROM pg_stat_activity
WHERE
query != '<IDLE>'
AND query NOT ILIKE '%pg_stat_activity%';
Результат:
pid | age | usename | query | backend_type
-----+-----+----------+-------+---------------------
66 | | | | autovacuum launcher
68 | | postgres | | background worker
64 | | | | background writer
63 | | | | checkpointer
65 | | | | walwriter
(5 rows)
Если запрос висит слишком долго, его стоит прибить командой
SELECT pg_terminate_backend(procpid);
С помощью команды \timing можно определить время выполнения запроса
\timing
Результат:
Timing is on.
Сформируем запрос:
SELECT
movieId,
COUNT(*) num_rating
FROM movie.ratings
WHERE
ratings.movieID > 100000
GROUP BY 1
LIMIT 10;
Результат:
movieid | num_rating
---------+------------
100001 | 2
100003 | 6
100006 | 6
100008 | 28
100010 | 88
100013 | 18
100015 | 4
100017 | 50
100032 | 30
100034 | 64
(10 rows)
Time: 1494.318 ms (00:01.494)
Ускорить запрос можно с помощью создания индексов. Индексы можно создавать на лету
CREATE INDEX ON movie.ratings(movieId);
Результат:
Time: 37427.672 ms (00:37.428)
После того, как индекс создан - запросы начинают выполнятся бодрее, время сокращается в сотни раз
SELECT
movieId,
COUNT(*) num_rating
FROM movie.ratings
WHERE
ratings.movieID > 100000
GROUP BY 1
Результат:
CREATE INDEX
Time: 38493.878 ms (00:38.494)
Выполним запрос ещё раз:
SELECT
movieId,
COUNT(*) num_rating
FROM movie.ratings
WHERE
ratings.movieID > 100000
GROUP BY 1
LIMIT 10;
Результат:
movieid | num_rating
---------+------------
100001 | 2
100003 | 6
100006 | 6
100008 | 28
100010 | 88
100013 | 18
100015 | 4
100017 | 50
100032 | 30
100034 | 64
(10 rows)
Time: 5.289 ms
Хранимые процедуры - это функции, которые определяются пользователем. Их можно создавать для более гибкого препроцессинга данных.
CREATE OR REPLACE FUNCTION
imdb_url(imdb_id VARCHAR)
RETURNS VARCHAR AS
$$
BEGIN RETURN
CONCAT(
CAST('http://www.imdb.com/' as VARCHAR), CAST(imdb_id as VARCHAR)
) ;
END;
$$
LANGUAGE plpgsql;
Результат:
CREATE FUNCTION
Time: 3.637 ms
Применяем функцию:
SELECT imdb_url(movie.links.imdbId)
FROM movie.links
LIMIT 10;
Результат:
imdb_url
-----------------------------
http://www.imdb.com/0114709
http://www.imdb.com/0113497
http://www.imdb.com/0113228
http://www.imdb.com/0114885
http://www.imdb.com/0113041
http://www.imdb.com/0113277
http://www.imdb.com/0114319
http://www.imdb.com/0112302
http://www.imdb.com/0114576
http://www.imdb.com/0113189
(10 rows)
Time: 1.478 ms
Мы создали хранимую процедуру, в которой приклеиваем к id оставшуюся часть URL. Хранимые процедуры можно делать и более сложными и использовать их для препроцессинга данных, или внутри триггеров.
Оператор EXPLAIN демонстрирует этапы выполнения запроса и может быть использован для оптимизации.
EXPLAIN
SELECT
userId, COUNT(*) num_rating
FROM movie.links
LEFT JOIN movie.ratings
ON links.movieid=ratings.movieid
GROUP BY 1
LIMIT 10;
Результат:
QUERY PLAN
--------------------------------------------------------------------------------------
Limit (cost=1880431.03..1880431.13 rows=10 width=16)
-> HashAggregate (cost=1880431.03..1880749.83 rows=31880 width=16)
Group Key: ratings.userid
-> Hash Right Join (cost=1323.47..1620188.15 rows=52048576 width=8)
Hash Cond: (ratings.movieid = links.movieid)
-> Seq Scan on ratings (cost=0.00..903196.76 rows=52048576 width=16)
-> Hash (cost=750.43..750.43 rows=45843 width=8)
ETL (Extract, Transform, Load) - общее название для процессов загрузки сырых данных в БД, а также выгрузки результатов для дальнейшего использования. Для импорта и экспорта данных используется команда copy.
Загружать данные в Postgres можно из CSV файлов, ниже пример который загружает данные из csv:
Тут надо переключиться из psql
терминал bash:
python3 upstart.py -s bash
На первом этапе создаём табличку с данными
psql --host $APP_POSTGRES_HOST -U postgres -c '
CREATE TABLE IF NOT EXISTS ratings (
userId bigint,
movieId bigint,
rating float(25),
timestamp bigint
);'
И на втором этапе заливаем CSV в созданную таблицу:
psql --host $APP_POSTGRES_HOST -U postgres \
-c "\\copy ratings FROM '/usr/share/data_store/raw_data/ratings.csv' DELIMITER ',' CSV HEADER"
Примечание: перед тем, как загружать данные, нужно очень внимательно изучить csv файл - чтобы точно узнать типы данных и их размерности
Выгрузку данных можно производить с помощью команды copy
psql --host $APP_POSTGRES_HOST -U postgres \
-c "\\copy (SELECT * FROM ratings LIMIT 100) TO '/usr/share/data_store/raw_data/ratings_file.csv' WITH CSV HEADER DELIMITER as ',';"
ETL процессы позволяют использовать Postgres (или другие БД) как средство вычисления: получаем данные, обрабатываем внутри Postgres используя мощный движок вычислений и выгружаем результат для дальнейшего использования - например, в алгоритмах машинного обучения.
Дамп - это сохранение состояния базы в текстовом виде.
Используется в качестве чекпоинта (точки восстановления) базы.
Для создания дампов в Postgres используется утилита pg_dump
pg_dump -h $APP_POSTGRES_HOST \
-U postgres \
-t public.ratings > /usr/share/data_store/raw_data/ratings_parted_dump.sql
Восстановление из дампа происходит аналогично
psql -h $APP_POSTGRES_HOST -U postgres dbname < ratings_parted_dump.sql
В промышленных системах дамп базы - это регулярная задача, требующая автоматизации.
Шардинг (иногда шардирование) — техника работы с данными, суть которой в разделении (партиционирование) данных на отдельные части (по отдельным серверам или отдельным таблицам)
Находясь в контейнере, подключимся к psql
python3 upstart.py -s psql
Создаём партиционированную таблицу с рейтингами
CREATE TABLE movie.ratings_parted (
userId bigint,
movieId bigint,
rating float(25),
timestamp bigint
);
Создаём шард -табличку с ограничениями на одно из полей - ключ шарда.
CREATE TABLE movie.ratings_parted_0 (
CHECK ( userId % 10 = 0 )
) INHERITS (movie.ratings_parted);
Чтобы заливка происходила правильно, нужно создать дополнительное правило-триггер
CREATE RULE ratings_insert_0 AS ON INSERT TO movie.ratings_parted
WHERE ( userId % 10 = 0 )
DO INSTEAD INSERT INTO movie.ratings_parted_0 VALUES ( NEW.* );
Проверим, как все работает
INSERT INTO movie.ratings_parted (
SELECT *
FROM movie.ratings
WHERE userid=10
);
Проверяем результат
SELECT COUNT (*)
FROM movie.ratings_parted
Ещё одна проверка
SELECT COUNT (*)
FROM movie.ratings_parted_0
Загадка: что будет, если выполнить запрос
INSERT INTO movie.ratings_parted (
SELECT *
FROM movie.ratings
WHERE userid=11
);
Аналогичным образом таблицы можно разнести по разным инстансам БД.