Почему в Big Data так часто используют HDFS, Hive и Spark?Когда речь заходит о классическом кластере для больших данных, почти всегда встречается именно эта тройка, и поэтому знание их покрывает значительную часть вакансий 😎. И это не случайность — у каждой технологии своя роль в цепочке.Этапы работы стека🏠 HDFS — хранениеФайловая система, принцип работы которой простой: есть главная нода (Namenode) с метаинформацией — названием файлов, их расположением и т.д., и подчиненные (Datanode), где непосредственно размещаются файлы.Файлы (CSV, JSON, Parquet, ORC) разбиваются на блоки и распределяются по узлам кластера. Есть репликация для защиты от падений. Очень просто масштабируется и есть возможность хранить огромные объёмы (терабайты и петабайты).🔍 Hive — доступ к даннымИзначально появился как движок, который бы позволял писать SQL для запросов к данным, лежащим в HDFS, без необходимости писать Java-приложения.Есть такая полезная вещь, как Hive Metastore, которая хранит метаданные и можно использовать даже без непосредственно Hive (но об этом в следующий раз, когда будем говорить про Iceberg).Довольно легко прикручивается к BI-инструментам .⚙️ Spark — вычисленияНам всем знакомый уже движок для обработки больших объёмов. Поддерживает batch, streaming, ML.Он нативно умеет читать данные напрямую из HDFS или через Hive. Работает в памяти, поэтому быстрее классического MapReduce.Можно строить ETL-пайплайны только с помощью него — умеет читать по jdbc напрямую из разных баз и складывать в хранилище, и более того — может это параллелить в несколько потоковПлюсы и минусы связки✅ Проверенное решение: десятки лет используется в продакшне. Практически везде вы найдете Hadoop ✅ Гибкость: можно хранить любые форматы и обрабатывать разными инструментами.✅ Подходит для разнообразных сценариев: от ETL до аналитики и ML.❌ Не всегда оптимально для ad-hoc аналитики: Hive довольно медленный, поэтому для быстрых запросов лучше подойдут Trino/Impala или же отдельная бд вроде Clickhouse.❌
Поступашки — BIG DATA
@bigdata_postupashki
You can view and join @bigdata_postupashki right away.
Похожие каналы
Все →Последние посты
Sort Merge Join в SparkКогда мы пишем обычный df1.join(df2, "id"), Spark не просто сравнивает строки, как это делает классическая база данных. Под капотом он выбирает конкретную физическую стратегию соединения. И если одна из таблиц не подходит для broadcast, то чаще всего Spark использует Sort Merge Join. Это основной вариант по умолчанию для больших таблиц.Этапы работы Sort Merge Join🔀ShuffleСтроки обеих таблиц перегоняются по ключам так, чтобы одинаковые ключи оказались в одной партиции.🤔SortВнутри каждой партиции данные сортируются по ключу.🔥JoinДва отсортированных набора объединяются. Это работает по принципу merge-шага в алгоритме сортировки слиянием.Плюсы и минусыУ SMJ есть сильная сторона — универсальность. Он подходит для очень больших датасетов и поддерживает все типы соединений, включая outer join. Но у такого подхода есть и минусы: shuffle перегружает сеть и диск, сортировка на больших объёмах требует ресурсов процессора и памяти, а при перекосе ключей одна партиция может стать узким местом и замедлить всю задачу.Настройки и оптимизацияSpark по умолчанию выбирает Sort Merge Join:spark.conf.set("spark.sql.join.preferSortMergeJoin", True)Количество shuffle-партиций задаётся параметром spark.sql.shuffle.partitions. Значение по умолчанию — 200, но для больших таблиц этого часто недостаточно, а для маленьких может быть слишком много:spark.conf.set("spark.sql.shuffle.partitions", 400)Чтобы бороться с перекосами ключей, можно использовать hintsales.hint("skew").join(customers, "customer_id")Важно учитывать сортировку данных. Если подготовить таблицы заранее, Spark сможет пропускать часть работы:sales.repartition("customer_id") \ .sortWithinPartitions("customer_id") \ .write.mode("overwrite").parquet("/data/sales_sorted")В таком случае данные будут сразу распределены и отсортированы по ключу, и последующие join-ы выполнятся быстрее.Как проверить тип join-аПосмотреть физический план:print(result.explain(True))В нём можно увидеть строку:*(3) SortMergeJo

Задачки с собеса в СамокатСобесился на мидла, был лайфкодинг + разговор про технологии, поэтому встреча была довольно долгой, а задачки не особо сложными. Давайте же начнем именно с задачек 🤓Самокат одни из немногих на моей памяти, кто еще используют Scala для разработки на Spark, так что один из этапов это вообще вспомнить её.Однако, вспомнив былую молодость, успешно справился с этим 😎 (благо DF API примерно одинаковое, что в Scala, что на Python)Дано:Таблицы: order, order_line, address, order_status, связи между ними и прочее (все на схеме выше)Задачи:SQL:1) Все заказы клиента customer_id = 42SELECT o.*FROM order AS oWHERE o.customer_id = 42;2) Для каждого заказа — вторая по цене позиция (от самых дорогих)WITH ranked AS ( SELECT ol.*, ROW_NUMBER() OVER (PARTITION BY ol.order_id ORDER BY ol.price DESC) AS rn FROM order_line AS ol)SELECT *FROM rankedWHERE rn = 2;Spark на Scala:Нужно было вывести айдишники покупателей и их адрес: вся задачка сводится к тому, что нужно присоединить координаты position_lon/position_lat к address и вывести customer_id, address_line Т.е. буквально все решается в один joinimport org.apache.spark.sql.functions._val orders = spark.table("order")val address = spark.table("address")val result = orders .join( address, Seq("position_lat", "position_lon"), "inner" ) .select(orders("customer_id"), address("address_line")) .distinct() result.show()Предложил также, что можно было бы округлить координаты, если там большая точность, но этого не требовалось.По итогу собес в кармане — идем дальше 😎@bigdata_postupashki

SQL на стажировку в Т-банкДедлайн 26 августа. Ответы на тест по SQL, условие, а ответы ниже: 1. FROM/JOIN, WHERE, GROUP BY, HAVING, WINDOW, SELECT, ORDER BY, LIMIT/OFFSET2. LEFT JOIN3. COUNT, SUM, AVG4. Возвращает первое ненулевое значение из списка5. Не допускает NULL и определяет уникальность строк6. DROP TABLE7. NULL8. LIKE "__X%" (два подчеркивания)9. Нельзя использовать sum агрегат в where -> нужно перенести в Having10. Подзапрос возвращает тысячи строкА разбор задач по SQL уже на нашем курсе дата инженер и аналитика хард, на которые скидка 50% только до 26 августа. @bigdata_postupashki
Задача с собеседования в Спортмастер В ней проверяют не столько умение решать задачи на sql, сколько ваше умение использовать df api в Spark — по тому, как вы решаете, сразу можно понять, сколько вы знаете про Spark.Условие:Даны таблицы:sales(datetime, shop, art, quantity) — продажи товаров (quantity может быть отрицательным — возвраты).prices(art, price) — цены товаров.Задача: Найти выручку магазина 100 за 2013-01-01.Выручка = quantity * price.Возвраты учитывать.Пример данных:Sales datetime | shop|art| quantity 01-01-13 12:12:12 | 100 |A1 | -2 01-01-13 12:12:13 | 100 |A1 | 3 Prices art|priceA1 |10Собственно, решение тут совсем несложное, как на SQL, так и на Spark DF, поэтому рекомендую сначала самому решить быстро, а потом проверить 🤓Решение:SQL (Spark SQL)SELECT SUM(s.quantity * p.price) AS revenueFROM sales sJOIN prices p USING (art)WHERE s.shop = 100 AND to_date(s.datetime) = '2013-01-01';PySpark DataFrame APIfrom pyspark.sql.functions import col, to_date, lit, sum as sum_# сначала не забываем делать фильтры и селекты, а только после джоины и агрегацииsales = spark.read.table("sales")\ .withColumn("date", to_date("datetime"))\ .filter((col("shop") == 100) & (col("date") == lit("2013-01-01")))\ .select("art", "quantity")prices = spark.read.table("prices").select("art", "price")df = sales.join(prices, "art", "inner")\ .withColumn("amount", col("quantity") * col("price"))\ .agg(sum_("amount").alias("revenue"))А чтобы с легкостью решать такие задачи рекомендую записывать на наш курс по дата инженерии 😎@bigdata_postupashki

Свершилось! Поступашки открывают набор на новую линейку продвинутых карьерных курсов 🎉 Мечтаешь стать крутым специалистом и с легкость тащить рабочие задачи и собесы, получив конкурентное преимущество? Хочешь овладеть знаниями и навыками для работы в крупной компании как Яндекс, Тинькофф или ВК? Узнал себя? Тогда записывайся у администратора на любой из курсов (если андроид - смотрим через яндекс браузер):➡️ машинное обучение хард➡️ бэкенд хард➡️ аналитика хард➡️ алгоритмы хардКурсы продвинутые и рассчитаны на тех, у кого уже есть БАЗА, для тех, кто хочет затронуть более сложные темы и идеально подготовиться к собесам, для тех, кто претендует на что-то большее чем просто джуниор. Если вы только в начале своего пути, советуем курсы старт, на которые тоже до 08.08 действует скидка. Все курсы стартует 17.08. Курсы заточены на практику, вся теория будет разобрана на конкретных задачах и кейсах, с которыми сталкиваются на работе и на собесах. Ничего нудного и скучного! Изучаем только то, что тебе реально понадобится и залетаем на первую работу! Хочешь подробностей? На нашем сайте можно найти программу и отзывы на каждый курс.Помимо этого на курсах тебя ждут:- продвинутые пет проекты и мини проекты, которые пойдут в портфолио;- разбор реальных тестовых заданий бигтехов;- разбор актуального контеста на стажировку в Яндекс и Тинькофф;- банк реальных технических вопрос с собесов;- разбор всех задач с алгособесов Яндекса! А после прохождения курса тебя ждет пробный собес с подробной консультацией и сопровождением, рефералкой в Яндекс или в другие топовые компании! 📊 Цена 12'000р 7'200р при покупке до 8 августа включительно! Хочешь купить несколько курсов сразу? Дадим хорошую скидку!Для вопросов и покупок пишем администратору и не тянем с этим: на каждом курсе количество мест ограничено!Не забываем и про линейку старт, на которую тоже только до 08.08 действует скидка 40%!➡️ алгоритмы старт ➡️ аналитика старт ➡️ машинное обучение старт ➡️ бэкенд разработка старт ЗАПИСАТЬС
Как устроен Trino — разбор для дата-инженеровНаверняка многие из вас слышали про эту технологию в последнее время, её часто видно на митапах, поэтому давайте разберемся, почему же Trino сейчас становится таким популярным?Trino (раньше назывался PrestoSQL) — федеративный SQL-движок для запросов к разным источникам: S3, Iceberg, Hive, PostgreSQL, Kafka и др. Он не хранит данные, а исполняет запросы напрямую — туда, где данные лежат. Сейчас часто используют его именно в Lakehouse архитектуре, как основной инструмент взаимодействия с данными.Давайте же разберемся, из чего он состоит.Coordinator — планирует запросыГлавный процесс в Trino: принимает SQL-запрос, строит логический и физический план, применяет оптимизации (pushdown, partition pruning, join-стратегии), разбивает план на фрагменты и распределяет их воркерамCoordinator чем-то похож на Spark Driver — но проще: не участвует в shuffle, не управляет состоянием, не держит данные. Его задача — распланировать и отдать.Workers — исполняют задачиВоркеры получают части плана и исполняют их: читают данные, фильтруют, джойнят, считают. Stateless — не кэшируют и не сохраняют контекст между запросами. Просто получили задачу, сделали, отдали результат.В отличие от executors в Spark, Trino workers не привязаны к приложению или сессии. Один запрос — одна жизнь.Catalogs — подключение к источникамTrino сам по себе ничего не знает про таблицы. Всё взаимодействие с данными идёт через каталоги. Каталог — это конфигурация, в которой описано, где лежат данные, как их читать и какие метаданные использовать (HMS, Iceberg REST, JDBC и т. д.).Это одно из ключевых отличий от Spark. В Spark чаще всего используется один Hive Metastore как основной каталог, а подключения к внешним базам делаются отдельно — через JDBC внутри SparkSession. В Trino — наоборот: всё подключается через каталоги. Хочешь работать с PostgreSQL? Подключаешь каталог postgres.properties. Нужно S3 с Iceberg? Заводишь каталог iceberg.properties. Trino обрабатывает всё един
Как Spark оптимизирует запросыSpark вроде бы понятный — прочитал parquet, отфильтровал по статусу, посчитал count — всё просто. Но иногда одну и ту же команду можно написать так, что отработает за секунду, а в другом случае будет работать часы.Почему? Давайте разберемсяSpark не магическая коробка, которая непонятно как молотит сотни гигабайт данных, а использует конкретные хорошо известные нам техники.Я бы хотел остановиться на трех из них и показать, как не сломать их1. Predicate pushdownSpark может передавать фильтры (WHERE) сразу в источник — parquet, ORC, JDBC. То есть не грузить весь файл, а сразу читать только нужные строки.Пример:df = spark.read.parquet("s3://...").filter("status = 'active'")Когда работает:— фильтр простой (=, >, <)— источник поддерживает pushdown (паркет, ORC, JDBC)— фильтр указан явно, а не через UDFКогда не работает:—фильтр через UDF—фильтр через выражение (df["a"] + df["b"] > 5)—JSON и CSV (там pushdown нет вообще)Проверить можно через план запроса df.explain(True). Ищите PushedFilters.2. Column pruningЕсли тебе нужны только user_id и amount, Spark может прочитать только эти поля. Особенно важно, если таблица на 300+ колонок.Пример:df = df.select("user_id", "amount")Когда работает:— n.select(...), без *— формат — parquet, ORC, JDBCКогда не работает:— select("*")— ты передал df в .rdd.map(...), .apply(...) или UDFПоэтому, когда работаете с большими данными, рекомендую первое время вообще не использовать SELECT *, потому что в таблице может быть 100/200/100500 колонок, и тянуть их все влечет проблемы с производительностью. Вряд ли вам нужны все сто колонокВ explain будет Scan parquet [user_id, amount], если всё хорошо.3. Partition pruningПоследнее по списку, но не по значимостиЕсли данные разложены по папкам (event_date=...), Spark может читать только нужные партиции, а не все подряд.Пример:df = spark.read.parquet("s3://events/").filter("event_date = '2025-07-30'")аналогично через Spark SQLspark.sql("SELECT id, date FROM table WHERE date =

Новая линейка карьерных курсов стартует уже в это воскресение 🎉 Мечтаешь стать крутым специалистом и с легкость тащить собесы, но не хватает фундамента? Хочешь овладеть знаниями и навыками для работы в крупной компании как Яндекс, Тинькофф или ВК? Узнал себя? Тогда записывайся у администратора на любой из курсов (если андроид - смотрим через яндекс браузер):➡️ дата сайнс (глубокое обучение)➡️ фронтенд➡️ дата инженер➡️ математика для карьерыВсе курсы стартует 03.08. Курсы заточены на практику, вся теория будет разобрана на конкретных задачах и кейсах, с которыми сталкиваются на работе и на собесах. Ничего нудного и скучного! Изучаем только то, что тебе реально понадобится и залетаем на первую работу! Хочешь подробностей? На нашем сайте можно найти программу и отзывы на каждый курс.Помимо этого на курсах тебя ждут:- пет проекты и мини проекты, которые пойдут в портфолио;- разбор реальных тестовых заданий бигтехов;- разбор актуального контеста на стажировку в Яндекс и Тинькофф;- банк реальных технических вопрос с собесов;- разбор всех задач с алгособесов Яндекса! А после прохождения курса тебя ждет пробный собес с подробной консультацией и сопровождением, рефералкой в Яндекс или в другие топовые компании! 📊 Цена на сайте, только при покупке до 2 августа включительно скидка 40%! Хочешь купить несколько курсов сразу? Дадим хорошую скидку!Для вопросов и покупок пишем администратору и не тянем с этим: на каждом курсе количество мест ограничено!Не забываем и про линейку старт, на которую тоже только до 2 августа действует скидка 40%!➡️ алгоритмы старт ➡️ аналитика старт ➡️ машинное обучение старт ➡️ бэкенд разработка старт ЗАПИСАТЬСЯ
Задача с собеседования.Вы могли заметить, что довольно давно не было постов тут, а дело тут вот в чем — я уволился и искал новую работу, поэтому много свободного времени уделял на подготовку к собеседованиям. Сейчас, наконец, все устаканилось, может быть, я позже расскажу подробнее как это было, но пока, благодаря этому я смог вернуться к вам с разными задачами и темами собесов, которые сейчас актуальны 😎 Поэтому первый пост про задачку, которую мне дали на первом этапе собеседования, довольно простая задачка на алгоритмы и знание питона. Кроме решения необходимо было, как и везде оценить сложность и предложить, как его улучшить.От себя могу сказать, что задача совершенно несложная (уровень изи на литкоде), проверяет просто то, что вы умеет что-то писать на питоне. Собственно, если вы не собеситесь в Яндекс, то и вряд ли на DE у вас будут жестить по алгоритмам.Итак, что нужно:Даны два массива и число target. Нужно найти такие пары чисел (по одному из каждого массива), сумма которых даёт target. Вернуть надо их индексы.Пример:a = [1, 3, 5]b = [2, 4, 6]target = 7Ответ:(0, 1)(1, 0)(2, 2)Решение:Вариант 1: В лоб, через переборfor i in range(len(a)): for j in range(len(b)): if a[i] + b[j] == target: print (i, j)Просто, понятно, но O(n²)Вариант 2: Добавим хеш-таблицу, т.к. поиск в ней по ключу — О(1)b_map = {value: j for j, value in enumerate(b)}for i, value in enumerate(a): needed = target - value if needed in b_map: print (i, b_map[needed])Уже гораздо лучше, сложность уже О(n). Но самые внимательные из вас могли подумать и заметить: а что, если для одного числа из первого массива подходит несколько вариантов из второго. Вот тут пригодится такая очень удобная тема, как defaultdict. Вариант 3: Через defaultdictb_map = defaultdict(list)for j, value in enumerate(b): b_map[value].append(j)for i, value in enumerate(a): needed = target - value if needed in b_map: result.append((i, b_map[needed]))print(result)Этот ва
Ребятаа, там открылся донабор на стажировку в Т-банк, поэтому выкладываем сюда разбор SQL задач 😎Обратите внимание также на то, что формулировки задач могут несильно отличаться, например, могут попросить не имя товара, а айдишник и подобное, поэтому будьте внимательны!РешенияЗадача 1: SELECT t1.name FROM ( SELECT b.buyer_id, b.name, SUM(o.quantity * pr.price) as total_sum from buyers b JOIN orders o on b.buyer_id = o.buyer_id JOIN products pr on o.product_id = pr.product_id GROUP by b.buyer_id, b.name ORDER BY total_sum DESC LIMIT 1 )t1Задача 2:SELECT p.product_name FROM products p LEFT JOIN orders o ON p.product_id = o.product_id AND o.order_date BETWEEN '2023-01-01' AND '2023-12-31'WHERE o.product_id IS NULL;Задача 3:SELECT SUM(o.quantity * p.price) AS total_sum FROM orders o JOIN buyers b ON o.buyer_id = b.buyer_id JOIN products p ON o.product_id = p.product_id WHERE b.city = 'Москва' AND o.order_date BETWEEN '2024-01-01' AND '2024-12-31';@bigdata_postupashki
Интересная задачка с собеседования Вы работаете со Spark, делаете преобразования над таблицами, но задача валится по OutOfMemory, как решить эту проблему без увеличения общего количества памяти?Решение1. Отрегулировать количество партицийДелается это несколькими способами:- spark.sql.shuffle.partitions: <количество партиций>За что отвечает данный параметр: перед запуском shuffle операции Spark все данные распределит в количество партиций, равное числу в этом параметр(по дефолту оно 200). Поэтому, если данных у вас ну уж очень много, то партиции могут стать очень жирными и не влезать в память => надо их количество увеличить- repartition(<количество партиций>)Происходит тоже самое, но нужно это делать вручную перед shuffle(например, перед join-ом)2. Использование Broadcast Join Broadcast Join работает по очень просто принципу: перемещает меньший датасет(его размер можно установить до 8 Gb) к партициям большого и соединяте, что позволяет избежать shuffle операции.Но если один из датасетов > 8 гигов, лучше его отключить с помощью параметра spark.sql.autoBroadcastJoinThreshold: -13. Уменьшение количества executorДопустим, у нас 10 экзекьюторов по 8 гигов и 5 ядер каждый, и при выполнении на них спарк задачи происходит OOM, тогда, можно сделать вместо 10 — 5, но выделить на каждый не по 8 гигов памяти, а 16, и 10 ядер. Общая параллельность не изменится, но партиции начнут влезать в память.А как бы вы решали данную задачку пишите в комменты 😎Чтобы узнать больше о работе Spark подписывайтесь на канал, ставьте реакции, а также записывайтесь на наши курсы@bigdata_postupashki
ETL vs ELTДрузья, после небольшого перерывчика начинаем новую серию постов, в которых мы будем рассматривать всякие теоретические аспекты, которые помогут вам как просто лучше разобраться, так и круто отвечать на собесах 😎Сегодня у нас на разборе самая база в Больших данных, шесть крутых букв — ETL и ELT. Куда бы вы не попали, везде работа с данными и хранилища будут устроены в одной из этих парадигм. Ну давайте же разбираться!ETL Происходит от трех слов(Extract, Transform, Load) – это классический подход к обработке данных, когда данные извлекаются из источников (из различных БД, CRM систем и прочих 1С-ов), затем обрабатываются на промежуточном уровне (Informatica, Debezium, SAP DS) и только после этого загружаются в основное хранилище данных, например, Greenplum, Clickhouse или Postgres. Такой подход удобен для сценариев, где важны контроль качества данных, строгие схемы и подготовленные агрегированные данные. Звучит круто, но очевидно, у данного метода есть недостатки, и они довольно очевидны: возникает узкое место в трансформации данных, т.к. приходится сразу чистить от дубликатов, приводить к одной схеме и т.д. И кроме этого эта промежуточная зона должна успевать обрабатывать огромное количество данных!Поэтому возник другой подход к выстраиванию процессов.ELT Как можно понять — это перестановка предыдущих слов: Extract, Load, Transform. ELT более гибкий метод, при котором сырые данные сначала загружаются в хранилище, а потом уже обрабатываются внутри него и загружаются в целевое хранилище. Такой подход характерен для облачных решений и аналитических платформ, например, в Yandex Cloud/Snowflake/Amazon S3, а также наш любимый Hadoop, где данные можно загружать без предварительной очистки, преобразовывать их внутри, а после этого загружать в тот же Greenplum, Clickhouse(но бывает и без загрузки, мы позже поговорим про подход Data Lakehouse). Плюсы тут понятны: данные у нас всегда доступны, особенно при отслеживании истории(этому помогает SCD, или же Slow Changing
СКИДКИ, СКИДКИ, СКИДКИ!!Мечтаешь стать крутым специалистом и с легкость тащить собесы, но не хватает фундамента? Хочешь овладеть знаниями и навыками для работы в крупной компании как Яндекс, Тинькофф или ВК? Специально и исключительно для подписчиков нашего канала в честь начала весны Поступашки объявляют ФИНАЛЬНЫЕ скидки в 40% до 3 марта! Любой курс можно приобрести всего лишь за 5400 рублей:➡️ аналитика➡️ машинное обучение старт ➡️ машинное обучение хард➡️ бэкенд разработка ➡️ фронтенд разработка➡️ инженер данныхПрограмма и Подробности. Для записи и всех вопросов пишем администратору: @menshe_treh

Свершилось! Поступашки открывают набор на новую линейку математических курсов 🎓 Хочешь поступить в ШАД, Ai Masters, или ААА? А может ты мечтаешь тащить собесы и поступить в крутую магу, но тебе не хватает фундамента? Узнал себя? Тогда записывайся у администратора на любой из курсов:➡️ алгоритмы старт 08.03➡️ теория вероятностей старт 16.03➡️ линейная алгебра старт 23.03➡️ математический анализ старт 30.03Наши курсы заточены на практику и конкретные задачи, вся теория будет разобрана на конкретных задачах и примерах, которые будут на экзаменах и на собесах. Ничего нудного и скучного! Изучаем только то, что вам реально понадобится! Хочешь подробностей? На нашам сайте можно найти программу и отзывы на каждый курс.Помимо кучи авторских задач мы даем доступ к уникальной закрытой базе заданий ШАДа, разбор реального контеста в ШАД, разбор ВСЕХ задач с собеседований в ШАД, Ai Masters, ААА! Более того, вы получите эксклюзивные материалы для проверяющих с собесов, пробный экзамен, инсайды, персональные рекомендации, собес с подробной консультацией и дальнейшим сопровождением вплоть до поступления в место мечты!📊 Цена очень доступная: 20'000рублей 9’000 рублей за каждый курс с учетом скидки (для подписчиков нашего ТГК до 26 февраля в честь старта продаж доступна скидка в 55% при покупке любого курса). Далее базовая цена повышается до 20’000 рублей за курс. Для вопросов и покупок пишем администратору и не тянем с этим: на каждом курсе количество мест ограничено!