[03] Работа с SQL в ClickHouse - rklepov/OTUS-ClickHouse-2025-03 GitHub Wiki
При выполнении первой части задания (меню) использовалась языковая модель DeepSeek.
1. Создание БД
CREATE DATABASE IF NOT EXISTS restaurant_db
ENGINE = Atomic
Ok.
USE restaurant_db
Connecting to database restaurant at localhost:9000 as user default.
Connected to ClickHouse server version 25.3.2.
2. Создание таблицы
CREATE OR REPLACE TABLE menu
(
`dish_id` UInt32 NOT NULL COMMENT 'Уникальный идентификатор блюда',
`dish_name` String NOT NULL COMMENT 'Название блюда',
`category` LowCardinality(String) NOT NULL COMMENT 'Категория блюда (основное, десерт и т.д.)',
`price` Decimal(10, 2) NOT NULL COMMENT 'Цена блюда в рублях',
`description` String NULL COMMENT 'Описание блюда и ингредиентов (может отсутствовать)'
)
ENGINE = MergeTree
ORDER BY dish_id
COMMENT 'Меню ресторана с основными блюдами и их характеристиками'
Ok.
SELECT
name,
type,
comment
FROM system.columns
WHERE (database = 'restaurant_db') AND (`table` = 'menu')
┌─name────────┬─type───────────────────┬─comment─────────────────────────────────────────────┐
1. │ dish_id │ UInt32 │ Уникальный идентификатор блюда │
2. │ dish_name │ String │ Название блюда │
3. │ category │ LowCardinality(String) │ Категория блюда (основное, десерт и т.д.) │
4. │ price │ Decimal(10, 2) │ Цена блюда в рублях │
5. │ description │ Nullable(String) │ Описание блюда и ингредиентов (может отсутствовать) │
└─────────────┴────────────────────────┴─────────────────────────────────────────────────────┘
3. CRUD операции
Вставка данных
INSERT INTO menu VALUES
(1, 'Стейк Рибай', 'Горячие блюда', 1250.00, 'Говяжий стейк с соусом Демиглас'),
(2, 'Салат Цезарь', 'Салаты', 450.00, 'Салат с курицей, крутонами и соусом Цезарь'),
(3, 'Тыквенный суп-пюре', 'Супы', 350.00, 'Нежный суп-пюре с тыквой и сливками'),
(4, 'Лосось на гриле', 'Горячие блюда', 890.00, 'Филе лосося с лимонным соусом'),
(5, 'Брускетта с томатами', 'Закуски', 290.00, 'Тосты с помидорами, базиликом и чесноком'),
(6, 'Тирамису', 'Десерты', 390.00, 'Классический итальянский десерт'),
(7, 'Паста Карбонара', 'Паста', 550.00, 'Спагетти с беконом, яйцом и пармезаном'),
(8, 'Вегетарианская пицца', 'Пицца', 680.00, 'Пицца с овощами и моцареллой'),
(9, 'Мохито', 'Напитки', 350.00, 'Освежающий коктейль с лаймом и мятой'),
(10, 'Чизкейк', 'Десерты', 420.00, 'Классический чизкейк с ягодным соусом'),
(11, 'Секретное блюдо', 'Горячие блюда', 999.00, NULL); -- Пример с NULL в описании
Запрос данных
Количество и средняя цена блюд по категориям.
SELECT
category,
count(*) AS cntByCategory,
round(avg(price), 2) AS avgPrice
FROM menu
GROUP BY category
ORDER BY
cntByCategory DESC,
category ASC
┌─category──────┬─cntByCategory─┬─avgPrice─┐
1. │ Горячие блюда │ 3 │ 1046.33 │
2. │ Десерты │ 2 │ 405 │
3. │ Закуски │ 1 │ 290 │
4. │ Напитки │ 1 │ 350 │
5. │ Паста │ 1 │ 550 │
6. │ Пицца │ 1 │ 680 │
7. │ Салаты │ 1 │ 450 │
8. │ Супы │ 1 │ 350 │
└───────────────┴───────────────┴──────────┘
Изменение данных
Повысим цену на горячие блюда на 10% и повторим предыдущий запрос.
ALTER TABLE menu
(UPDATE price = price * 1.1 WHERE category IN ['Горячие блюда'])
Ok.
SELECT
category,
count(*) AS cntByCategory,
round(avg(price), 2) AS avgPrice
FROM menu
GROUP BY category
ORDER BY
cntByCategory DESC,
category ASC
┌─category──────┬─cntByCategory─┬─avgPrice─┐
1. │ Горячие блюда │ 3 │ 1150.97 │
2. │ Десерты │ 2 │ 405 │
3. │ Закуски │ 1 │ 290 │
4. │ Напитки │ 1 │ 350 │
5. │ Паста │ 1 │ 550 │
6. │ Пицца │ 1 │ 680 │
7. │ Салаты │ 1 │ 450 │
8. │ Супы │ 1 │ 350 │
└───────────────┴───────────────┴──────────┘
Удаление данных
Удаляем десерты дороже 400р.
ALTER TABLE restaurant_db.menu
(DELETE WHERE (category = 'Десерты') AND (price > 400.))
Ok.
Проверяем состояние мутаций
SELECT
`table`,
mutation_id,
create_time,
substr(command, 1, 7) AS cmd,
is_done
FROM system.mutations
WHERE (`table` = 'menu') AND (database = 'restaurant_db')
┌─table─┬─mutation_id────┬─────────create_time─┬─cmd─────┬─is_done─┐
1. │ menu │ mutation_2.txt │ 2025-04-26 09:58:04 │ (UPDATE │ 1 │
2. │ menu │ mutation_3.txt │ 2025-04-26 10:10:31 │ (DELETE │ 1 │
└───────┴────────────────┴─────────────────────┴─────────┴─────────┘
4. Изменение состава полей
Добавление новых
ALTER TABLE menu
ADD COLUMN `calories` Nullable(UInt32) COMMENT 'Калорийность блюда в ккал',
ADD COLUMN `is_vegetarian` Bool DEFAULT false COMMENT 'Флаг вегетарианского блюда (true/false)',
ADD COLUMN `preparation_time` UInt16 DEFAULT 10 COMMENT 'Время приготовления в минутах'
Ok.
SELECT
name,
type,
comment,
default_expression
FROM system.columns
WHERE (database = 'restaurant_db') AND (`table` = 'menu')
┌─name─────────────┬─type───────────────────┬─comment─────────────────────────────────────────────┬─default_expression─┐
1. │ dish_id │ UInt32 │ Уникальный идентификатор блюда │ │
2. │ dish_name │ String │ Название блюда │ │
3. │ category │ LowCardinality(String) │ Категория блюда (основное, десерт и т.д.) │ │
4. │ price │ Decimal(10, 2) │ Цена блюда в рублях │ │
5. │ description │ Nullable(String) │ Описание блюда и ингредиентов (может отсутствовать) │ │
6. │ calories │ Nullable(UInt32) │ Калорийность блюда в ккал │ │
7. │ is_vegetarian │ Bool │ Флаг вегетарианского блюда (true/false) │ false │
8. │ preparation_time │ UInt16 │ Время приготовления в минутах │ 10 │
└──────────────────┴────────────────────────┴─────────────────────────────────────────────────────┴────────────────────┘
Удаление существующих
ALTER TABLE restaurant_db.menu
(DROP COLUMN is_vegetarian),
(DROP COLUMN description)
Ok.
SELECT
name,
type,
comment,
default_expression
FROM system.columns
WHERE (database = 'restaurant_db') AND (`table` = 'menu')
┌─name─────────────┬─type───────────────────┬─comment───────────────────────────────────┬─default_expression─┐
1. │ dish_id │ UInt32 │ Уникальный идентификатор блюда │ │
2. │ dish_name │ String │ Название блюда │ │
3. │ category │ LowCardinality(String) │ Категория блюда (основное, десерт и т.д.) │ │
4. │ price │ Decimal(10, 2) │ Цена блюда в рублях │ │
5. │ calories │ Nullable(UInt32) │ Калорийность блюда в ккал │ │
6. │ preparation_time │ UInt16 │ Время приготовления в минутах │ 10 │
└──────────────────┴────────────────────────┴───────────────────────────────────────────┴────────────────────┘
5. Выборка из Sample dataset
SELECT
pickup_date,
cab_type,
passenger_count,
trip_distance,
fare_amount,
pickup_ct2010,
dropoff_ct2010
FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{0..2}.gz', 'TabSeparatedWithNames')
LIMIT 10
┌─pickup_date─┬─cab_type─┬─passenger_count─┬─trip_distance──────┬─fare_amount─┬─pickup_ct2010─┬─dropoff_ct2010─┐
1. │ 2015-07-07 │ yellow │ 2 │ 2.59 │ 14.5 │ Manhattan │ Manhattan │
2. │ 2015-07-07 │ yellow │ 1 │ 2.4 │ 9 │ Queens │ Queens │
3. │ 2015-07-07 │ yellow │ 1 │ 5.13 │ 20 │ Manhattan │ Manhattan │
4. │ 2015-07-07 │ yellow │ 1 │ 1.2 │ 7 │ Manhattan │ Manhattan │
5. │ 2015-07-08 │ yellow │ 5 │ 2.17 │ 8.5 │ Manhattan │ Manhattan │
6. │ 2015-07-08 │ yellow │ 2 │ 5.4 │ 20.5 │ Manhattan │ Brooklyn │
7. │ 2015-07-08 │ yellow │ 1 │ 1.6600000000000001 │ 8.5 │ Manhattan │ Manhattan │
8. │ 2015-07-08 │ yellow │ 1 │ 1.6600000000000001 │ 8 │ Manhattan │ Manhattan │
9. │ 2015-07-08 │ yellow │ 1 │ 1.1 │ 9.5 │ Manhattan │ Manhattan │
10. │ 2015-07-08 │ yellow │ 1 │ 5.14 │ 22.5 │ Brooklyn │ Manhattan │
└─────────────┴──────────┴─────────────────┴────────────────────┴─────────────┴───────────────┴────────────────┘
10 rows in set. Elapsed: 0.926 sec.
6. Материализация таблицы
Здесь в целом следуем инструкциям со страницы New York Taxi Data, однако для дальнейшей практики с партициями сразу делаем таблицу партицированной (по месяцу поездки). Также для большей наглядности отключаем таймаут на удаление неактивных партов (old_parts_lifetime
).
CREATE DATABASE nyc_taxi;
CREATE OR REPLACE TABLE nyc_taxi.trips_small
(
`trip_id` UInt32,
`pickup_datetime` DateTime,
`dropoff_datetime` DateTime,
`pickup_longitude` Nullable(Float64),
`pickup_latitude` Nullable(Float64),
`dropoff_longitude` Nullable(Float64),
`dropoff_latitude` Nullable(Float64),
`passenger_count` UInt8,
`trip_distance` Float32,
`fare_amount` Float32,
`extra` Float32,
`tip_amount` Float32,
`tolls_amount` Float32,
`total_amount` Float32,
`payment_type` Enum('CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4, 'UNK' = 5),
`pickup_ntaname` LowCardinality(String),
`dropoff_ntaname` LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(pickup_datetime)
PRIMARY KEY (pickup_datetime, dropoff_datetime)
SETTINGS old_parts_lifetime = 0;
INSERT INTO nyc_taxi.trips_small
SELECT
trip_id,
pickup_datetime,
dropoff_datetime,
pickup_longitude,
pickup_latitude,
dropoff_longitude,
dropoff_latitude,
passenger_count,
trip_distance,
fare_amount,
extra,
tip_amount,
tolls_amount,
total_amount,
payment_type,
pickup_ntaname,
dropoff_ntaname
FROM s3(
'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{0..2}.gz',
'TabSeparatedWithNames'
);
7. Практика с партициями
Начальное состояние
Состояние партиций таблицы сразу после загрузки из s3:
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name─────────┬───rows─┬─size──────┬─active─┐
1. │ trips_small │ 201507 │ 201507_1_1_0 │ 258211 │ 10.51 MiB │ 1 │
2. │ trips_small │ 201507 │ 201507_4_4_0 │ 258275 │ 10.51 MiB │ 1 │
3. │ trips_small │ 201507 │ 201507_7_7_0 │ 61957 │ 2.60 MiB │ 1 │
4. │ trips_small │ 201508 │ 201508_2_2_0 │ 557407 │ 22.45 MiB │ 1 │
5. │ trips_small │ 201508 │ 201508_5_5_0 │ 556737 │ 22.41 MiB │ 1 │
6. │ trips_small │ 201508 │ 201508_8_8_0 │ 556491 │ 22.41 MiB │ 1 │
7. │ trips_small │ 201509 │ 201509_3_3_0 │ 250451 │ 10.20 MiB │ 1 │
8. │ trips_small │ 201509 │ 201509_6_6_0 │ 250229 │ 10.20 MiB │ 1 │
9. │ trips_small │ 201509 │ 201509_9_9_0 │ 250559 │ 10.21 MiB │ 1 │
└─────────────┴───────────┴──────────────┴────────┴───────────┴────────┘
Вручную инициируем слияние:
OPTIMIZE TABLE nyc_taxi.trips_small FINAL
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name─────────┬────rows─┬─size──────┬─active─┐
1. │ trips_small │ 201507 │ 201507_1_7_1 │ 578443 │ 23.22 MiB │ 1 │
2. │ trips_small │ 201508 │ 201508_2_8_1 │ 1670635 │ 65.27 MiB │ 1 │
3. │ trips_small │ 201509 │ 201509_3_9_1 │ 751239 │ 30.04 MiB │ 1 │
└─────────────┴───────────┴──────────────┴─────────┴───────────┴────────┘
Отключение партиции
Проверяем общее количество записей по месяцам до отсоединения партиции:
WITH toYYYYMM(pickup_datetime) AS month
SELECT
toString(month),
count(*) AS monthlyRides
FROM nyc_taxi.trips_small
GROUP BY month
ORDER BY month ASC
┌─toString(month)─┬─monthlyRides─┐
1. │ 201507 │ 578443 │
2. │ 201508 │ 1670635 │
3. │ 201509 │ 751239 │
└─────────────────┴──────────────┘
Отсоединяем партицию за август 2015г. и повторяем запрос:
ALTER TABLE nyc_taxi.trips_small
(DETACH PARTITION 201508)
Ok.
WITH toYYYYMM(pickup_datetime) AS month
SELECT
toString(month),
count(*) AS monthlyRides
FROM nyc_taxi.trips_small
GROUP BY month
ORDER BY month ASC
┌─toString(month)─┬─monthlyRides─┐
1. │ 201507 │ 578443 │
2. │ 201509 │ 751239 │
└─────────────────┴──────────────┘
Дополнительно подтверждаем, что партиция 201508
действительно была отсоединена:
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name─────────┬───rows─┬─size──────┬─active─┐
1. │ trips_small │ 201507 │ 201507_1_7_1 │ 578443 │ 23.22 MiB │ 1 │
2. │ trips_small │ 201509 │ 201509_3_9_1 │ 751239 │ 30.04 MiB │ 1 │
└─────────────┴───────────┴──────────────┴────────┴───────────┴────────┘
Подключение партиции
Создаём временную таблицу:
CREATE OR REPLACE TABLE nyc_taxi.tmp_trips AS nyc_taxi.trips_small
Ok.
Переносим отсоединённую партицию во временную таблицу перемещая директорию на диске (через shell):
root@3329e3f61ff3:~# mv \
/var/lib/clickhouse/data/nyc_taxi/trips_small/detached/201508_2_8_1/ \
/var/lib/clickhouse/data/nyc_taxi/tmp_trips/detached/
Подключаем партицию во временную таблицу:
ALTER TABLE nyc_taxi.tmp_trips
(ATTACH PARTITION '201508')
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name─────────┬────rows─┬─size──────┬─active─┐
1. │ tmp_trips │ 201508 │ 201508_1_1_0 │ 1670635 │ 65.27 MiB │ 1 │
2. │ trips_small │ 201507 │ 201507_1_7_1 │ 578443 │ 23.22 MiB │ 1 │
3. │ trips_small │ 201509 │ 201509_3_9_1 │ 751239 │ 30.04 MiB │ 1 │
└─────────────┴───────────┴──────────────┴─────────┴───────────┴────────┘
Изменяем данные во временной таблице. Удаляем поездки в/из аэропорт(а):
ALTER TABLE nyc_taxi.tmp_trips
(DELETE WHERE (pickup_ntaname IN ['Airport']) OR (dropoff_ntaname IN ['Airport']))
Ok.
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name───────────┬────rows─┬─size──────┬─active─┐
1. │ tmp_trips │ 201508 │ 201508_1_1_0_2 │ 1551802 │ 60.31 MiB │ 1 │
2. │ trips_small │ 201507 │ 201507_1_7_1 │ 578443 │ 23.22 MiB │ 1 │
3. │ trips_small │ 201509 │ 201509_3_9_1 │ 751239 │ 30.04 MiB │ 1 │
└─────────────┴───────────┴────────────────┴─────────┴───────────┴────────┘
Переносим изменённые данные из партиции временной таблицы обратно в основную таблицу (через архив):
root@3329e3f61ff3:~# tar -czvf updated_trips_201508.tgz \
-C /var/lib/clickhouse/data/nyc_taxi/tmp_trips/ 201508_3_3_0
201508_3_3_0/
201508_3_3_0/primary.cidx
201508_3_3_0/trip_id.bin
201508_3_3_0/trip_id.cmrk2
. . . . .
201508_3_3_0/metadata_version.txt
root@3329e3f61ff3:~# tar -xzvf updated_trips_201508.tgz \
-C /var/lib/clickhouse/data/nyc_taxi/trips_small/detached/
201508_3_3_0/
201508_3_3_0/primary.cidx
201508_3_3_0/trip_id.bin
201508_3_3_0/trip_id.cmrk2
. . . . .
201508_3_3_0/metadata_version.txt
Подключаем изменённую партицию к основной таблице:
ALTER TABLE nyc_taxi.trips_small
(ATTACH PARTITION '201508')
Ok.
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name───────────┬────rows─┬─size──────┬─active─┐
1. │ tmp_trips │ 201508 │ 201508_3_3_0 │ 1551802 │ 60.31 MiB │ 1 │
2. │ trips_small │ 201507 │ 201507_1_7_1 │ 578443 │ 23.22 MiB │ 1 │
3. │ trips_small │ 201508 │ 201508_10_10_0 │ 1551802 │ 60.31 MiB │ 1 │
4. │ trips_small │ 201509 │ 201509_3_9_1 │ 751239 │ 30.04 MiB │ 1 │
└─────────────┴───────────┴────────────────┴─────────┴───────────┴────────┘
Удаляем партицию временной таблицы:
ALTER TABLE nyc_taxi.tmp_trips
(DROP PARTITION '201508')
Ok.
SELECT
`table`,
partition,
name,
rows,
formatReadableSize(bytes_on_disk) AS size,
active
FROM system.parts
WHERE database = 'nyc_taxi'
┌─table───────┬─partition─┬─name───────────┬────rows─┬─size──────┬─active─┐
1. │ trips_small │ 201507 │ 201507_1_7_1 │ 578443 │ 23.22 MiB │ 1 │
2. │ trips_small │ 201508 │ 201508_10_10_0 │ 1551802 │ 60.31 MiB │ 1 │
3. │ trips_small │ 201509 │ 201509_3_9_1 │ 751239 │ 30.04 MiB │ 1 │
└─────────────┴───────────┴────────────────┴─────────┴───────────┴────────┘
Как итог данные в партиции 201508
основной таблицы были изменены:
WITH toYYYYMM(pickup_datetime) AS month
SELECT
toString(month),
count(*) AS monthlyRides
FROM nyc_taxi.trips_small
GROUP BY month
ORDER BY month ASC
┌─toString(month)─┬─monthlyRides─┐
1. │ 201507 │ 578443 │
2. │ 201508 │ 1551802 │
3. │ 201509 │ 751239 │
└─────────────────┴──────────────┘