Как делать11 октября 2026 г.9 мин чтения
SQL и PostgreSQLШаг 3 из 9

SQL: JOIN, GROUP BY и агрегаты

Коротко

JOIN соединяет строки двух таблиц по условию: INNER JOIN оставляет только пары, нашедшие друг друга, а LEFT JOIN сохраняет все строки левой таблицы и подставляет NULL, если пары нет. Агрегатные функции (count, sum, avg, min, max) сворачивают много строк в одно значение и пропускают NULL, кроме count(*). GROUP BY считает агрегаты отдельно для каждой группы, WHERE фильтрует строки до группировки, а HAVING — готовые группы после неё.

Пошаговый урокУровень: НачальныйSQLПрактика: ~45–90 мин

После материала вы сможете

  • соединить две и больше таблиц через INNER JOIN и LEFT JOIN и найти строки без пары, например клиентов без заказов
  • посчитать итоги функциями count, sum, avg, min, max и объяснить, как они обрабатывают NULL
  • сгруппировать строки GROUP BY, отфильтровать группы HAVING и объяснить, чем HAVING отличается от WHERE
  • разбить сложный отчёт на шаги с WITH и распознать дубли после JOIN

Что знать заранее

Проверено на

PostgreSQL 17 (17.11, образ postgres:17-alpine)psql 17Docker Engine 29

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Зачем JOIN: данные лежат в разных таблицах
  2. 02INNER JOIN и LEFT JOIN — наглядно
  3. 03Агрегаты: COUNT, SUM, AVG, MIN, MAX и NULL
  4. 04GROUP BY и HAVING
  5. 05Подзапросы и CTE (WITH)
  6. 06Типичные ошибки: дубли после JOIN и COUNT(*) против COUNT(столбец)
  7. 07Упражнения: выручка по товарам и клиенты без заказов

Зачем JOIN: данные лежат в разных таблицах

В уроке «SQL с нуля» мы работали с одной таблицей за раз. Но в учебной базе магазина заказ хранит только номер клиента client_id, а имя клиента лежит в clients. Так задумано: данные о клиенте записаны один раз, а заказы на него ссылаются. Чтобы увидеть заказ вместе с именем, таблицы соединяют запросом с JOIN.

Если контейнер с учебной базой уже остановлен, поднимите его заново тем же скриптом shop.sql:

bash
# shop.sql is the script from the "SQL from scratch" lesson
docker run -d --rm --name shop-db -e POSTGRES_PASSWORD=test postgres:17-alpine
docker exec -i shop-db psql -U postgres < shop.sql
docker exec -it shop-db psql -U postgres -P null=NULL
SQL
SELECT o.id, o.ordered_on, o.status, c.name
FROM orders AS o
JOIN clients AS c ON c.id = o.client_id
ORDER BY o.id;

1

ordered_on
2026-09-02
status
shipped
name
Aigerim Seitova

2

ordered_on
2026-09-05
status
shipped
name
Daniyar Akhmetov

3

ordered_on
2026-09-14
status
paid
name
Aigerim Seitova

4

ordered_on
2026-09-20
status
cancelled
name
Madina Omarova

5

ordered_on
2026-09-28
status
shipped
name
Timur Kim
Реальный вывод psql. Сокращено: первые 5 строк из 10.

Как это читать: для каждой строки orders база ищет строки clients, для которых условие после ON истинно, и склеивает их в одну строку результата. AS o и AS c — короткие псевдонимы таблиц. Документация PostgreSQL считает хорошим стилем указывать таблицу у каждого столбца в запросе с соединением: если в обеих таблицах окажется столбец с одинаковым именем, например id или name, неквалифицированный запрос сломается.

Соединений в одном запросе может быть несколько. Позиция заказа ссылается и на заказ, и на товар, поэтому для чека нужны все четыре таблицы:

SQL
SELECT o.id AS order_id, c.name AS client, p.name AS product,
       oi.quantity, oi.price, oi.quantity * oi.price AS line_total
FROM orders AS o
JOIN clients AS c      ON c.id = o.client_id
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p     ON p.id = oi.product_id
WHERE o.id IN (1, 5)
ORDER BY o.id, p.name;

1

client
Aigerim Seitova
product
Ceramic Mug 350 ml
quantity
2
price
2500.00
line_total
5000.00

1

client
Aigerim Seitova
product
Espresso Blend 1 kg
quantity
1
price
12900.00
line_total
12900.00

5

client
Timur Kim
product
Ceramic Mug 350 ml
quantity
1
price
2500.00
line_total
2500.00

5

client
Timur Kim
product
Ethiopia Yirgacheffe 250 g
quantity
1
price
5600.00
line_total
5600.00

5

client
Timur Kim
product
Masala Chai 200 g
quantity
2
price
3800.00
line_total
7600.00
Позиции заказов 1 и 5 с названиями товаров и суммой строки.

INNER JOIN и LEFT JOIN — наглядно

Слово JOIN без уточнения означает INNER JOIN — внутреннее соединение. В результат попадают только пары, нашедшие друг друга. Клиенты, у которых нет ни одного заказа, из такого запроса молча исчезают:

SQL
SELECT c.id, c.name, o.id AS order_id, o.status
FROM clients AS c
JOIN orders AS o ON o.client_id = c.id
ORDER BY c.id, o.id;
INNER JOIN: 10 строк, клиентов 7 и 8 нет — у них нет заказов.
SQL
SELECT c.id, c.name, o.id AS order_id, o.status
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id
ORDER BY c.id, o.id;

1

name
Aigerim Seitova
order_id
1
status
shipped

1

name
Aigerim Seitova
order_id
3
status
paid

1

name
Aigerim Seitova
order_id
9
status
paid

2

name
Daniyar Akhmetov
order_id
2
status
shipped

2

name
Daniyar Akhmetov
order_id
10
status
new

3

name
Madina Omarova
order_id
4
status
cancelled

3

name
Madina Omarova
order_id
7
status
shipped

4

name
Timur Kim
order_id
5
status
shipped

5

name
Aliya Bekova
order_id
6
status
paid

6

name
Ruslan Ivanov
order_id
8
status
new

7

name
Zarina Kaliyeva
order_id
NULL
status
NULL

8

name
Sergey Pak
order_id
NULL
status
NULL
LEFT JOIN: 12 строк. Последние две — клиенты без заказов, столбцы заказа заполнены NULL.

INNER JOIN

  • Только строки, у которых нашлась пара по условию ON
  • Строки без пары в любой из таблиц отбрасываются
  • Отчёт «заказы с именами клиентов»

LEFT JOIN

  • Каждая строка левой таблицы хотя бы один раз
  • Если пары нет, столбцы правой таблицы — NULL
  • Отчёт «все клиенты и их заказы, если есть», поиск строк без пары

Документация описывает LEFT JOIN именно так: сначала выполняется внутреннее соединение, затем для каждой строки левой таблицы без пары добавляется строка с NULL в столбцах правой. Отсюда классический приём — найти строки без пары: оставить только те, где ключ правой таблицы равен NULL.

SQL
SELECT c.id, c.name, c.created_on
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id
WHERE o.id IS NULL
ORDER BY c.id;

7

name
Zarina Kaliyeva
created_on
2026-09-10

8

name
Sergey Pak
created_on
2026-09-30
Клиенты без единого заказа.
SQL
SELECT p.id, p.name
FROM products AS p
LEFT JOIN order_items AS oi ON oi.product_id = p.id
WHERE oi.product_id IS NULL;

9

name
Gift Box
Товар, который ни разу не заказывали.

Есть ещё RIGHT JOIN (то же, что LEFT, но сохраняет правую таблицу) и FULL JOIN (сохраняет строки обеих). На практике RIGHT JOIN обычно переписывают как LEFT JOIN, поменяв таблицы местами, — так запрос легче читать.

SQL
-- Intent: every client plus their paid orders
SELECT c.name, o.id AS order_id, o.status
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id
WHERE o.status = 'paid'
ORDER BY c.id, o.id;

Aigerim Seitova

order_id
3
status
paid

Aigerim Seitova

order_id
9
status
paid

Aliya Bekova

order_id
6
status
paid
Ошибка: осталось 3 строки, клиенты без оплаченных заказов пропали.
SQL
SELECT c.name, o.id AS order_id, o.status
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id AND o.status = 'paid'
ORDER BY c.id, o.id;

Aigerim Seitova

order_id
3
status
paid

Aigerim Seitova

order_id
9
status
paid

Daniyar Akhmetov

order_id
NULL
status
NULL

Madina Omarova

order_id
NULL
status
NULL

Timur Kim

order_id
NULL
status
NULL

Aliya Bekova

order_id
6
status
paid

Ruslan Ivanov

order_id
NULL
status
NULL

Zarina Kaliyeva

order_id
NULL
status
NULL

Sergey Pak

order_id
NULL
status
NULL
Условие в ON: все 8 клиентов, у Aigerim два оплаченных заказа — 9 строк.

Агрегаты: COUNT, SUM, AVG, MIN, MAX и NULL

Агрегатная функция вычисляет одно значение по многим строкам. Без GROUP BY весь результат — одна группа, и запрос возвращает ровно одну строку.

SQL
SELECT count(*)        AS products,
       count(weight_g) AS with_weight,
       min(price)      AS cheapest,
       max(price)      AS most_expensive,
       round(avg(price), 2) AS avg_price,
       avg(weight_g)   AS avg_weight
FROM products;

9

with_weight
5
cheapest
2500.00
most_expensive
18500.00
avg_price
8366.67
avg_weight
360.0000000000000000
Реальный вывод: товаров 9, а вес указан у 5.

Здесь видно главное правило: агрегаты пропускают NULL. count(*) считает строки, а count(weight_g) — только строки, где вес задан. avg(weight_g) — среднее по пяти товарам с известным весом: (1000 + 250 + 250 + 100 + 200) / 5 = 360, а не сумма, делённая на 9. Это почти всегда то, что нужно, но об этом важно помнить, когда NULL означает «ноль», а не «неизвестно».

count(*)

Что считает
Все строки группы
Результат на orders
10

count(promo_code)

Что считает
Строки, где значение не NULL
Результат на orders
3

count(DISTINCT promo_code)

Что считает
Разные значения, кроме NULL
Результат на orders
2
SQL
SELECT count(*) AS orders, count(promo_code) AS with_promo,
       count(DISTINCT promo_code) AS distinct_promos
FROM orders;

--  orders | with_promo | distinct_promos
-- --------+------------+-----------------
--      10 |          3 |               2
-- (1 row)
Проверка таблицы выше на учебной базе.

Вторая ловушка: по документации все агрегаты, кроме count, возвращают NULL, если строк нет вовсе. Сумма пустого набора — не ноль. У заказа 10 позиций пока нет:

SQL
SELECT sum(quantity * price) AS revenue
FROM order_items
WHERE order_id = 10;
SELECT coalesce(sum(quantity * price), 0) AS revenue
FROM order_items
WHERE order_id = 10;
SELECT count(*) AS lines
FROM order_items
WHERE order_id = 10;

--  revenue
-- ---------
--     NULL
-- (1 row)
--
--  revenue
-- ---------
--        0
-- (1 row)
--
--  lines
-- -------
--      0
-- (1 row)
sum по пустому набору — NULL, coalesce превращает его в 0, count возвращает 0.

Ещё один полезный инструмент — FILTER: он ограничивает строки для одного конкретного агрегата, не трогая остальные. Так одним запросом считают несколько показателей:

SQL
SELECT count(*) AS orders,
       count(*) FILTER (WHERE status = 'cancelled') AS cancelled,
       count(*) FILTER (WHERE promo_code IS NOT NULL) AS with_promo
FROM orders;

10

cancelled
1
with_promo
3
Всего заказов, отменённых и с промокодом — за один проход.

GROUP BY и HAVING

GROUP BY делит строки на группы с одинаковым значением и считает агрегаты для каждой группы отдельно. Выручка по категориям — это соединение позиций с заказами и товарами, фильтр по статусу и группировка по категории. Выручкой в уроке считаем оплаченные и отгруженные заказы: новые ещё не оплачены, отменённые не состоялись.

SQL
SELECT p.category,
       sum(oi.quantity)            AS units,
       sum(oi.quantity * oi.price) AS revenue
FROM order_items AS oi
JOIN orders AS o   ON o.id = oi.order_id
JOIN products AS p ON p.id = oi.product_id
WHERE o.status IN ('paid', 'shipped')
GROUP BY p.category
ORDER BY revenue DESC;

coffee

units
9
revenue
77500.00

equipment

units
2
revenue
27400.00

tea

units
3
revenue
10800.00

tableware

units
3
revenue
7500.00
Выручка по категориям. Категории gifts нет: Gift Box не продавался.

Правило группировки: в SELECT можно выводить только столбцы из GROUP BY и агрегаты. Если вывести столбец, у которого внутри группы много значений, база не знает, какое выбрать, и отказывается выполнять запрос:

SQL
SELECT p.category, p.name, sum(oi.quantity) AS units
FROM order_items AS oi
JOIN products AS p ON p.id = oi.product_id
GROUP BY p.category;

-- ERROR:  column "p.name" must appear in the GROUP BY clause or be used in an aggregate function
-- LINE 1: SELECT p.category, p.name, sum(oi.quantity) AS units
--                            ^

Исключение одно: если сгруппировать по первичному ключу, остальные столбцы этой таблицы можно выводить — они однозначно определяются ключом. Поэтому GROUP BY c.id достаточно, чтобы вывести c.name. В примерах мы пишем GROUP BY c.id, c.name — так запрос понятнее и переносим на другие СУБД.

HAVING фильтрует уже готовые группы. Найдём клиентов с двумя и более заказами, не считая отменённых:

SQL
SELECT c.name, count(*) AS orders
FROM clients AS c
JOIN orders AS o ON o.client_id = c.id
WHERE o.status <> 'cancelled'
GROUP BY c.id, c.name
HAVING count(*) >= 2
ORDER BY orders DESC, c.name;

Aigerim Seitova

orders
3

Daniyar Akhmetov

orders
2
У Madina два заказа, но один отменён — она не прошла фильтр.

Разница между WHERE и HAVING следует из порядка выполнения. WHERE решает, какие строки войдут в группы, поэтому агрегата в нём ещё не существует:

SQL
SELECT client_id, count(*)
FROM orders
WHERE count(*) >= 2
GROUP BY client_id;

-- ERROR:  aggregate functions are not allowed in WHERE
-- LINE 3: WHERE count(*) >= 2
--               ^
FROM и JOINWHEREGROUP BY и HAVINGSELECTORDER BY и LIMIT
Логический порядок выполнения SELECT по документации PostgreSQL: строки фильтруются до группировки, группы — после.

Документация советует ставить в WHERE всё, что не требует агрегата: так база не тратит работу на группировку строк, которые потом всё равно отбросит. В HAVING остаются только условия на агрегаты.

При группировке все NULL попадают в одну группу. Клиентка без города образует отдельную строку:

SQL
SELECT city, count(*) AS clients
FROM clients
GROUP BY city
ORDER BY clients DESC, city;

Almaty

clients
3

Astana

clients
2

Karaganda

clients
1

Shymkent

clients
1

NULL

clients
1
NULL в city — отдельная группа из одного клиента.

Подзапросы и CTE (WITH)

Подзапрос — SELECT внутри другого запроса. Агрегат нельзя написать в WHERE, но можно сравнить со значением, которое посчитал отдельный подзапрос. Этот приём показан и в учебнике PostgreSQL:

SQL
SELECT name, price
FROM products
WHERE price > (SELECT avg(price) FROM products)
ORDER BY price DESC;

Hand Grinder

price
18500.00

Gift Box

price
15000.00

Espresso Blend 1 kg

price
12900.00

French Press 600 ml

price
8900.00
Товары дороже средней цены каталога (8366.67 ₸).

Группировать можно и по выражению. Функция date_trunc('month', …) обрезает дату до первого числа месяца, а ::date приводит результат к типу date. В GROUP BY здесь использовано имя выходного столбца month — PostgreSQL это разрешает.

SQL
SELECT date_trunc('month', o.ordered_on)::date AS month,
       count(DISTINCT o.id)                    AS orders,
       sum(oi.quantity * oi.price)             AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
WHERE o.status IN ('paid', 'shipped')
GROUP BY month
ORDER BY month;

2026-09-01

orders
4
revenue
55500.00

2026-10-01

orders
3
revenue
67700.00
Выручка по месяцам: сентябрь и октябрь 2026.

Когда отчёт строится в несколько шагов, удобно WITH. Документация описывает CTE (common table expression) как вспомогательный запрос, который ведёт себя как временная таблица на время одного запроса. Сначала посчитаем сумму каждого заказа, затем по этим суммам — выручку и средний чек по клиентам:

SQL
WITH order_totals AS (
    SELECT o.id, o.client_id, sum(oi.quantity * oi.price) AS total
    FROM orders AS o
    JOIN order_items AS oi ON oi.order_id = o.id
    WHERE o.status IN ('paid', 'shipped')
    GROUP BY o.id, o.client_id
)
SELECT c.name,
       count(*)              AS orders,
       sum(t.total)          AS revenue,
       round(avg(t.total))   AS avg_check
FROM order_totals AS t
JOIN clients AS c ON c.id = t.client_id
GROUP BY c.id, c.name
ORDER BY revenue DESC;

Aigerim Seitova

orders
3
revenue
58200.00
avg_check
19400

Aliya Bekova

orders
1
revenue
25800.00
avg_check
25800

Timur Kim

orders
1
revenue
15700.00
avg_check
15700

Daniyar Akhmetov

orders
1
revenue
13000.00
avg_check
13000

Madina Omarova

orders
1
revenue
10500.00
avg_check
10500
Шаг 1 — суммы заказов, шаг 2 — итоги по клиентам. Средний чек округлён.
SQL
SELECT name FROM products
WHERE id NOT IN (SELECT product_id FROM order_items);
SELECT name FROM products
WHERE id NOT IN (1, 2, 3, 4, 5, 6, 7, 8, NULL);
SELECT p.name FROM products AS p
WHERE NOT EXISTS (
    SELECT 1 FROM order_items AS oi WHERE oi.product_id = p.id
);

--    name
-- ----------
--  Gift Box
-- (1 row)
--
--  name
-- ------
-- (0 rows)
--
--    name
-- ----------
--  Gift Box
-- (1 row)
Три запроса подряд, реальный вывод — в комментариях.

Типичные ошибки: дубли после JOIN и COUNT(*) против COUNT(столбец)

Соединение «один ко многим» размножает строки: каждый заказ повторяется столько раз, сколько в нём позиций. Если после этого посчитать count(*), получится число позиций, а не заказов:

SQL
-- Wrong: counts order lines, not orders
SELECT c.name, count(*) AS orders
FROM clients AS c
JOIN orders AS o       ON o.client_id = c.id
JOIN order_items AS oi ON oi.order_id = o.id
GROUP BY c.id, c.name
ORDER BY c.id;

Aigerim Seitova

orders
5

Daniyar Akhmetov

orders
2

Madina Omarova

orders
3

Timur Kim

orders
3

Aliya Bekova

orders
1

Ruslan Ivanov

orders
1
Ошибка: у Aigerim не 5 заказов, а 3 — в них 5 позиций.
SQL
SELECT c.name,
       count(DISTINCT o.id)        AS orders,
       sum(oi.quantity * oi.price) AS total
FROM clients AS c
JOIN orders AS o       ON o.client_id = c.id
JOIN order_items AS oi ON oi.order_id = o.id
GROUP BY c.id, c.name
ORDER BY c.id;

Aigerim Seitova

orders
3
total
58200.00

Daniyar Akhmetov

orders
1
total
13000.00

Madina Omarova

orders
2
total
29000.00

Timur Kim

orders
1
total
15700.00

Aliya Bekova

orders
1
total
25800.00

Ruslan Ivanov

orders
1
total
9600.00
count(DISTINCT o.id) считает заказы. У Daniyar 1, а не 2: заказ 10 без позиций выпал при INNER JOIN с order_items.

Обратите внимание на вторую деталь в результате: внутреннее соединение с позициями отбросило пустой заказ 10. Если пустые заказы важны для отчёта, нужен LEFT JOIN и coalesce для суммы.

Вторая частая ошибка — count(*) после LEFT JOIN. Для клиента без заказов в результате всё равно есть одна строка (с NULL в столбцах заказа), и count(*) посчитает её. Считать нужно столбец правой таблицы:

SQL
SELECT c.name, count(o.id) AS orders, count(*) AS rows_in_group
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id
GROUP BY c.id, c.name
ORDER BY c.id;

Aigerim Seitova

orders
3
rows_in_group
3

Daniyar Akhmetov

orders
2
rows_in_group
2

Madina Omarova

orders
2
rows_in_group
2

Timur Kim

orders
1
rows_in_group
1

Aliya Bekova

orders
1
rows_in_group
1

Ruslan Ivanov

orders
1
rows_in_group
1

Zarina Kaliyeva

orders
0
rows_in_group
1

Sergey Pak

orders
0
rows_in_group
1
У клиентов без заказов count(o.id) = 0, а count(*) = 1.

Сумма или количество больше ожидаемого

Причина
JOIN «один ко многим» размножил строки
Исправление
count(DISTINCT …) или сначала агрегировать в WITH, потом соединять

Клиенты без заказов пропали из отчёта

Причина
INNER JOIN вместо LEFT JOIN
Исправление
LEFT JOIN от таблицы, строки которой нужны все

LEFT JOIN ведёт себя как INNER

Причина
Условие на правую таблицу стоит в WHERE
Исправление
Перенести условие в ON

У клиента без заказов 1 заказ

Причина
count(*) после LEFT JOIN
Исправление
count(столбец правой таблицы)

Итог пустой, а не 0

Причина
sum по пустому набору — NULL
Исправление
coalesce(sum(…), 0)

must appear in the GROUP BY clause

Причина
В SELECT столбец вне группы
Исправление
Добавить в GROUP BY или обернуть в агрегат

Упражнения: выручка по товарам и клиенты без заказов

Упражнения выполняются на той же учебной базе. Выручка — сумма quantity * price по заказам в статусах paid и shipped. Ответы проверены на PostgreSQL 17.11; ожидаемый вывод — в комментариях к решению.

Дальше по траектории — запуск PostgreSQL для своего проекта и проектирование схемы: как выбрать таблицы и связи, чтобы такие отчёты оставались простыми.

Как проверено4 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    shop.sql (seed из урока sql-select) и команды перезапуска

    тот же файл, что в sql-select; загружен в docker postgres:17-alpine (17.11), контейнер kb-pg-db0103; команды запуска проверены в sql-select (контейнер kb-pg-db0103-setup)

  • запущено, работает

    Запросы урока (24 шт.: INNER/LEFT JOIN, ON vs WHERE, агрегаты и NULL, FILTER, GROUP BY/HAVING, ошибки группировки и агрегата в WHERE, подзапрос, date_trunc, CTE, NOT IN vs NOT EXISTS, дубли после JOIN)

    каждый запрос — отдельный .sql, `docker exec -i kb-pg-db0103 psql -U postgres -X -P null=NULL < file` на свежезагруженной базе; таблицы и комментарии с выводом собраны в статью скриптом из файлов вывода; сокращённые таблицы помечены

  • запущено, работает

    GROUP BY по первичному ключу (c.id) с выводом c.name

    отдельный запрос x_pk_group.sql выполнен без ошибки — утверждение в тексте подтверждено

  • запущено, работает

    Решения 6 упражнений + неправильный вариант упр. 2 (условие в WHERE)

    выполнены на учебной базе; неправильный вариант вернул 0 строк, как сказано в подсказке; сумма упр. 1 (55 500) совпала с отчётом по месяцам

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

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

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

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