Физический план и операции: сканы, джоины, агрегации, сортировка, spill
Физический план является инструментом исполнителя SQL-движка, который преобразует абстрактную запросную логику в конкретный набор операторов и их параметров, исполняемых на данных. Эффективность аналитических запросов в DWH во многом определяется тем, как хорошо спланированы и реализованы сканы, объединения, агрегации, сортировка и управление памятью с spill. Глубокое понимание физического плана позволяет не только добиваться высокой производительности, но и формировать практики мониторинга, профилирования и оптимизации в рамках DevOps-процессов данных.
В контексте больших объёмов данных физический план становится критическим звеном между данными, хранящимися в дата-облаках и файловых форматах, и потребителями аналитики. Различия между сканами, стратегиями соединений, методами агрегаций и поведением spill напрямую влияют на пропускную способность систем, задержку выполнения запросов и устойчивость к пиковым нагрузкам. В этой главе рассматриваются архитектурные принципы, алгоритмы и протоколы реализации основных операций физического плана: сканы данных, джоины, агрегации, сортировка и поведение при выходе данных в вынужденный перенос на диск (spill). Особый акцент сделан на аспектах, критичных для DWH-окружений: векторизация исполнения, распределённая обработка, бюджеты памяти, хранение и форматы столбцовых файлов, а также методики анализа и внедрения изменений в реальных инсталляциях.
-
Основная идея физического плана: превратить логический запрос в набор операторов, оптимально организованных по памяти, распределению и сетевым потокам.
-
Влияние ограничений памяти и внешней памяти: spill как обязательный компромисс и как его минимизировать с сохранением корректности.
-
Роль статистик и предположений в выборе операторов и порядков выполнения.
-
Взаимодействие между планировщиком и исполнителем в распределённых средах и влияния на архитектуру дата-обработки.
-
Примеры практик мониторинга и внедрения оптимизаций в продуктивных средах.
-
Обзор современных инструментов и подходов: от встроенного Explain до профилирования исполнения и тестирования изменений в CI/CD.
Краткое содержание главы
- Принципы формирования физического плана: архитектура исполнителя, память, форматы данных и распределение задач.
- Сканирование данных: полное сканирование, сканы с фильтрами и предикатное падение, партиционирование и чтение столбцов.
- Джоины: типы соединений, выбор стратегии в зависимости от объёмов и распределения данных, влияние бюферов и shuffle/broadcast.
- Агрегации: hash-агрегации и сортируемые агрегации, потоковые и внешние агрегации, влияние на память и spill.
- Сортировка: сортировка в памяти против внешней сортировки, буферы, количество проходов и влияние на задержку.
- Spill и управление памятью: политики spill, тактики снижения spill-эффекта, мониторинг и настройка параметров.
- Интеграция планировщика и исполнителя: статистика, мониторинг, оповещения и подходы к CI/CD для изменений в физическом плане.
Сканирование данных: доступ, фильтрация и чтение
Сканирование - базовая операция, которая определяет, какие данные будут обработаны на следующем этапе исполнения. Эффективность скана во многом зависит от того, насколько хорошо поддерживаются предикаты, фильтры и структура данных. В современных DWH важную роль играют предикатное pushdown и партиционирование, которые позволяют пропускать чтение ненужных фрагментов данных и минимизировать объём передаваемых через план слоёв.
Ключевые принципы:
- predicate pushdown: перенос фильтров на уровень чтения данных, чтение только необходимых столбцов и строк. Это особенно важно при работе с столбцатыми форматами Parquet/ORC, где можно пропускать колонки и читать только те, что участвуют в запросе.
- partition pruning и clustering: организации таблиц по партициям и кластеризованным ключам позволяют быстро сузить диапазоны чтения, особенно в больших таблицах.
- форматы данных: столбцовые форматы и векторизированное чтение ускоряют сканирование, снижая расход CPU и IO. Примеры: Parquet, ORC. В рамках открытых систем стоит отметить Parquet как один из самых распространённых форматов для DWH, и, в контексте российских и открытых решений, ClickHouse продемонстрировал эффективные реализации столбцового чтения и параллельного скана.
- распределённость чтения: в распределённых средах важно уделять внимание размещению данных и вычислений, чтобы избежать лишних shuffle-операций и обеспечить локальное сканирование.
Пошаговая практика оптимизации сканов:
- Убедиться, что статистика обновлена: ANALYZE/ANALYZE TABLE, сбор статистик по столбцам и по партициям. Это критично для качественного выбора планируемых операторов.
- Уточнять выборку столбцов: ограничение чтения до необходимых столбцов, особенно в больших таблицах.
- Рефакторить запросы, чтобы фильтры применялись как можно раньше в цепочке исполнения.
- Рассмотреть использование партиционирования и кластеризации: выбрать партиции, которые чаще всего обслуживают запросы, и поддерживать их актуальными.
- Контролировать формат вывода: если данные подготавливаются для последующих шагов (агрегации, соединения), убедиться, что формат поддержки чтения не приводит к дополнительной переработке данных.
Пример кода: для иллюстрации подхода можно привести базовый EXPLAIN-план, чтобы увидеть, какие сканы выбираются планировщиком. В рамках конкретного движка это будет выглядеть по-разному; приведённый ниже пример демонстрирует общую схему.
-- Пример: общий паттерн объяснения физического плана EXPLAIN (FORMAT JSON) SELECT p.part_id, SUM(s.amount) ## FROM parts p JOIN shipments s ON p.part_id = s.part_id WHERE p.category = 'A' AND s.ship_date >= DATE '2025-01-01';
Ограничение и компромисс:
- слишком агрессивное исключение столбцов и фильтрование на ранних этапах может привести к неочевидным задержкам в позже следующих шагах из-за пересчётов и преобразований. Важно тестировать сценарии на валидных рабочих нагрузках и сравнивать планы с реальными метриками исполнения.
Инструменты и примеры внедрения:
- современные аналитические СУБД и движки часто предоставляют визуальные и текстовые объяснения физических планов, что позволяет сравнивать варианты сканов, оценивать влияние предикатного pushdown и партиционирования. В качестве примера можно упомянуть интеграционную поддержку в Trino/Presto и ClickHouse - они позволяют анализировать физический план и выявлять узкие места на уровне скана и распределения данных.
Джоины: стратегии выполнения и выбор оптимального типа соединений
Соединения между таблицами - один из наиболее дорогостоящих типов операций в DWH, особенно при больших объёмах данных и разреженной/неравномерной расстановке ключей. Эффективность джоин-операций во многом определяется стратегией выполнения, доступностью памяти и размером данных на входах.
Основные типы соединений:
- Nested Loop Join (NLJ): простая реализация, эффективна для малых входов или когда один из входов может быть быстро проиндексирован. Часто оказывается неэффективной на больших объёмах и может приводить к высокой сетевой нагрузке и FLOP.
- Hash Join: базовая стратегия для больших входов без подходящих индексов. В память загружает меньший вход в хеш-таблицу и выполняет сопоставление с большим входом. В условиях ограниченной памяти и возможного spill hash-join может приводить к дополнительной IO-операциям, но остаётся одной из самых производительных в большинстве сценариев.
- Sort-Merge Join: эффективен, когда входы уже отсортированы или их можно отсортировать с разумной стоимостью. Особенно полезен для больших данных и больших числовых ключей; требует порядка на входах, что может потребовать дополнительных расходов на сортировку.
- Distributed/Shuffle Join: в распределённых системах используется обмен данными между нодами (shuffle). Выбор стратегии зависит от распределения данных, доступной сетевой пропускной способности и текущего распределения нагрузки.
- Broadcast Join (или Broadcast Hash Join): когда меньший вход может быть передан на все узлы. Эфективно при малых размерах второго входа и в условиях ограниченного числа узлов. Важно контролировать размер, чтобы не перегрузить сеть.
Как выбрать стратегию:
- Оценка размера входов: если один вход существенно меньше другого, возможно целесообразна стратегия типа NLJ или Broadcast Join.
- Наличие индексов и сортировки по ключу: если входы предварительно отсортированы или легко привести к сортировке, может быть предпочтительнее Single oder Merge-join.
- Распределение данных и сетевые издержки: в больших распределённых кластерах часто предпочтительна стратегия, минимизирующая shuffle, либо кооперативное выполнение, когда данные можно разместить локально (co-located joins).
- Память и spill: Hash Join эффективен, но может потребовать значительный буфер; при нехватке памяти возможно вынужденно прибегнуть к spill-join, что негативно сказывается на задержках.
- Влияние планирования: современные планировщики говорят в пользу более гибких стратегий, включая перестановку джойнов и использование нескольких стратегий в рамках одного запроса.
Практические советы:
- Используйте статистику: точные оценки cardinality и распределение ключей существенно улучшают выбор планировщика.
- Минимизируйте объем shuffle: размещайте данные так, чтобы ключи соединения соответствовали распределению.
- Учитывайте локальные условия: на кластерах с ограниченной сетевой пропускной способностью лучше избегать больших shuffle-операций.
- Контролируйте память: увеличение budged памяти под joins может существенно снизить spill и улучшить latency.
Пример кода: использование EXPLAIN для анализа механизма соединения.
EXPLAIN (FORMAT JSON) SELECT a.id, b.value FROM sales a JOIN customers b ON a.customer_id = b.id WHERE a.date >= DATE '2025-01-01';
Пояснение к типовым сценариям:
- Для малых таблиц можно обходиться Nested Loop, особенно когда один вход уже отсортирован и индексирован.
- Для больших таблиц предпочтителен Hash Join, при условии достаточной памяти и отсутствия частого spill.
- В случаях, когда данные по ключу упорядочены или могут быть легко упорядочены на входе, Merge Join становится эффективной альтернативой, особенно если планировщик может избежать дорогостоящей повторной сортировки.
Интеграционные аспекты:
- В распределённых системах важно учитывать распределение данных по нодам, чтобы минимизировать shuffle и обеспечить локальные соединения. Планировщики должны учитывать расположение колумнарной группировки и партицирования, чтобы снизить сетевые затраты.
- Применение профилирования и мониторинга планов в CI/CD позволяет регламентировать выбор стратегий соединений: дают возможность заранее тестировать влияние изменений в физическом плане и предотвращать регрессию производительности.
Агрегации: обработка группировок и вычисление итогов
Агрегаторы являются узким местом в аналитических запросах, особенно когда речь идёт о больших группировках и сложных вычислениях. В физическом плане применяются разные реализации агрегирования: hash-based агрегации, sort-based агрегации и потоковые (streaming) агрегаты, которые могут работать в сочетании с spill.
Ключевые концепции:
- Hash-based агрегация: создаётся хеш-таблица по группирующим ключам; подход эффективен, когда число групп не превышает доступной памяти. При большом числе уникальных ключей возможно влияние spill и большого потребления RAM.
- Sort-based агрегация: данные сортируются по группировочным ключам, затем выполняется последовательная агрегация по отсортированному потоку. Подход хорошо работает, когда сортировка может быть выполнена эффективно и количество групп велико.
- Стриминг-агрегация (streaming): применяется, когда данные приходят потоками и требуется экономия памяти. Часто применяется в реальном времени или в микродашах.
- Условия spill: при нехватке памяти агрегация может spill на диск, что приводит к дополнительной IO и задержке. Оптимизация памяти и выбор алгоритма агрегации должны учитывать объём групп и характер данных.
- Группинг-sets и rollups: углубляют аналитические возможности, но требуют дополнительной обработки и памяти. Включение таких операций в план может повлиять на выбор стратегии агрегации.
Практические принципы:
- Анализ кардинальности: оценка количества уникальных групп только на этапе планирования, чтобы выбрать стратегию агрегации.
- Применение локальной агрегации в распределённых шагах: если возможно, выполнить агрегацию на ближайшей ноде до объединения результатов.
- Использование промежуточной агрегации и фильтрации: удаление ненужных групп на ранних этапах помогает сэкономить память.
- Влияние форматов данных: столбцовые форматы и векторизированное исполнение ускоряют агрегацию за счёт эффективного доступа к памяти.
Пример кода (упрощённо): индикация различий в физических операциях агрегации через EXPLAIN.
EXPLAIN (FORMAT JSON) SELECT region, SUM(sales) AS total_sales FROM sales_fact GROUP BY region ORDER BY total_sales DESC;
Советы по оптимизации:
- Применение фильтров до агрегации: чем раньше ограничить набор данных, тем меньше памяти требуется.
- Разделение ключей агрегации: если группировка по большому числу ключей вызывает сильную нагрузку, рассмотреть агрегаты по подмножствам или создание предвычисленных агрегатов.
- В случаях больших группировок стоит рассмотреть внешнюю агрегацию или материализованные представления, чтобы повторяемые запросы выполнялись быстрее.
Интересный момент: современные движки часто поддерживают как Hash, так и Sort агрегацию, и выбор между ними может зависеть от параметров памяти, наличия параллелизма и профиля нагрузки. В обработке больших наборов данных это решение напрямую влияет на объем памяти, сложность вычисления и задержку выполнения.
Сортировка: порядок данных и влияние на исполнение
Сортировка играет важную роль не только в ORDER BY, но и в некоторых операциях соединения и оконных функций. В физическом плане сортировка может быть выполнена в памяти или как внешняя (external sort) с использованием spill. Ключевые аспекты: объём данных на входе, доступная память, количество проходов, использование временных файлов и влияние на задержку.
Значимые принципы:
- Векторизация и буферы: векторизованное исполнение и эффективное использование памяти позволяют значительно ускорить сортировку по большему объёму данных.
- Внешняя сортировка: при превышении доступной памяти выполняются несколько проходов сортировки, слияние run-ов, которые приводят к дополнительной задержке, но позволяют обрабатывать данные, которые не помещаются в RAM.
- Связь с агрегациями: сортировка нередко сопутствует агрегациям, особенно когда нужна предварительная сортировка по группирующим ключам. В таком случае можно уменьшить или обойти необходимость в повторной сортировке, используя уже упорядоченный поток.
- Оптимизация параметров: настройка memory budget и параллелизма может снизить число проходов и увеличить пропускную способность.
Практические рекомендации:
- Оптимизируйте порядок выполнения: если сортировка нужна для последующего объединения или топ-N выборок, оцените влияние ранней сортировки на общий план.
- Выбирайте подходящие настройки памяти: увеличение work_mem/vectorsize, при этом оценивая влияние на другие процессы.
- Разрешайте минимизацию spill: если возможно, используйте локальную сортировку на узлах и уменьшение количества проходов за счёт параллелизма.
- Используйте предварительную агрегацию для уменьшения объёма сортируемых строк, когда это возможно.
Пример кода для анализа:
EXPLAIN (FORMAT JSON) SELECT user_id, activity, COUNT(*) FROM activity_log GROUP BY user_id, activity ORDER BY COUNT(*) DESC;
Промежуточные решения и архитектура:
- В некоторых системах ORDER BY может быть вынесено из основного плана в отдельный этап, чтобы позволить параллелизм и локализацию обработки.
- В распределённых средах важно учитывать распределение по узлам и сетевые задержки при сортировке больших потоков данных. Оптимальные планы часто используют частичную сортировку локально и последующее слияние глобальных результатов.
Spill: управление памятью и влияние на производительность
Spill - это перенос части данных из памяти на диск в рамках выполнения операций, когда доступная RAM-буферная область исчерпана. Spill является нормальным механизмом, но его избыточность существенно влияет на задержки и пропускную способность. Эффективное управление spill требует как аппаратной поддержки, так и грамотной политики планирования запросов, мониторинга и настройки параметров.
Ключевые аспекты spill:
- Механизм spill зависит от конкретного типа операции (скан, join, агрегат, сортировка). Он может произойти как во время чтения, так и во время обработки промежуточных результатов.
- Влияние на IO и дисковый трафик: spill создаёт дополнительную IO-нагрузку, может привести к конкуренции за диск и ухудшению latency для запросов с похожей нагрузкой.
- Баланс между памятью и процессорной нагрузкой: иногда увеличение памяти позволяет снизить количество spills и ускорить исполнение, но не всегда это экономически оправдано.
- Профилировка и сигналы тревоги: важно мониторить коэффициент spill и частоту его возникновения, чтобы принимать решения об изменении конфигураций, параллелизма и дизайна запросов.
Лучшие практики управления spill:
- Увеличение доступной памяти под рабочие наборы: настройка параметров памяти (например, размер буфера для операций), чтобы обеспечить большую долю данных в памяти и снизить spills.
- Разделение запросов на этапы: разбиение сложного запроса на более простые шаги, позволяя сохранить данные в памяти на каждом шаге и уменьшить вероятность spill на поздних стадиях.
- Оптимизация по данным: сортировка и агрегации меньшей глубины, предварительная агрегация, фильтрации и фильтр-conditions, чтобы уменьшить общий объём обрабатываемых данных.
- Архитектурные решения: распределение данных по нодам так, чтобы локальные операции выполнялись на близких нодах и уменьшалась необходимость в обменах (shuffle), что сокращает spill вследствие удаления промежуточных перемещений.
Практический подход к мониторингу:
- Включение подробного логирования и метрик: число spills, размер spill-файлов, количество проходов, задержки на разных стадиях исполнения.
- Регулярный анализ планов: анализ изменения физического плана с обновлениями статистики, чтобы предвидеть увеличение spill-рисков и проводить превентивную оптимизацию.
- Автоматизация в CI/CD: тестирование изменений в планах на наборе тестовых нагрузок с эмуляцией пиковых сценариев, чтобы обнаружить регрессии spill и производительности до выпуска.
Интеграционные аспекты:
- Применение внешних хранилищ и форматов данных: spill может существенно зависеть от скорости дисков и конфигурации I/O subsystem. Использование SSD-дисков и оптимизированных файловых систем может снизить влияние spill.
- Гибкость планировщика: современные движки поддерживают адаптивную переработку плана на лету в случае изменения памяти, нагрузки и статистики. В рамках таких решений планировщик может динамически перераспределять ресурсы и менять стратегию, чтобы снизить количество spill.
Интеграция и протоколы физического плана: мониторинг, оптимизация и внедрение
Физический план - не просто набор операторов; он является частью управляемой цепочки анализа, мониторинга и оптимизации. В контексте DWH важно обеспечить прозрачность, возможность профилирования и устойчивость к изменениям нагрузок. Архитектурные подходы включают использование метрик, визуализацию исполнения, статистику и процессы обратной связи, чтобы сотрудники могли быстро выявлять и устранять узкие места.
Ключевые компоненты:
- Статистика и анализ: поддержка актуальных статистик по данным, распределению значений и cardinality. Это основа для качественного выбора планируемых операторов.
- Механизмы Explain: получение детального описания физического плана, включая выбор оператора, порядок выполнения и потенциал spill. В некоторых системах доступно форматирование в JSON для последующего анализа инструментами мониторинга.
- Профилирование исполнения: сбор токенов производительности на уровне операторов и потоков, что позволяет детально выявлять узкие места и оптимизировать конкретные участки плана.
- Инструменты мониторинга: интеграция с системами мониторинга и алертинга (метрики задержки, throughput, использование памяти и диск-IO) для оперативного реагирования на перегрузку и рост задержек.
- CI/CD для планов: внедрение изменений в физический план через автоматизированные тесты и нагрузки, чтобы предотвратить регрессии производительности и обеспечить устойчивость к пиковым нагрузкам.
Практические подходы:
- Внедрить единый стандарт Explain-представления и глобальные метрики для всех моделей исполнения в кластере.
- Задействовать профилирование и трассировку исполнения с целью выявления узких мест и проверки применённых оптимизаций на реальных рабочих нагрузках.
- Установить политики версионирования и отката планов: какие изменения в плане можно вносить, как тестировать и как возвращаться к безопасной конфигурации при регрессиях.
- Встроить проверку производительности в CI/CD: автоматизированное исполнение тестовых наборов нагрузок и сравнение ключевых метрик до и после изменений.
Интеграционные примеры:
- Рассмотреть взаимодействие между системой хранения и вычислениями: ко-location, partition pruning и локальная агрегация на уровне кластерной архитектуры. Пример может быть опорой на один или два ряда пакетов технологий (например, Parquet + ClickHouse или Parquet + PostgreSQL и т.д.) в рамках конкретной инфраструктуры.
- Встроенный мониторинг физических планов в аналитических платформах: использование инструментов визуализации и дашбордов для отслеживания изменений в планах, задержек и spills.
Key takeaways
- Физический план - это исполнительное звено, связывающее логику запроса и конкретные операторы над данными, поэтому его качество напрямую влияет на производительность аналитики.
- Эффективное сканирование зависит от predicate pushdown, партиционирования, форматов данных и управления статистиками; грамотная настройка приводит к значимым сокращениям IO и времени выполнения.
- Выбор стратегии соединения должен опираться на размер входов, наличие индексов, распределение данных и доступную память; гибкость планировщика критична в распределённых средах.
- Агрегации требуют баланса между hash-based и sort-based подходами; оптимизация памяти и ранняя фильтрация данных снижают расходы на вычисления и задержки.
- Сортировка может быть как в памяти, так и внешней; внешняя сортировка приводит к spill и дополнительным проходам, поэтому важны настройки памяти и параллелизма.
- Spill - неизбежное явление при работе с большими данными; управление им требует мониторинга, корректной настройки памяти и продуманной архитектуры обработки данных.
- Интеграция планирования с мониторингом и CI/CD обеспечивает устойчивость к изменениям нагрузок и позволяет быстро реагировать на регрессию производительности.
FAQ
- Что такое физический план и чем он отличается от логического?
- Логический план описывает семантику запроса: какие данные нужно получить и как они взаимосвязаны в рамках SQL-операций. Физический план представляет конкретную реализацию этого запроса в исполнителе, включая выбор операторов, порядок их выполнения, распределение задач и затраты по памяти и I/O. Различие критично: логика может быть эквивалентной, но физический план определяет фактическую производительность и ограничения сред.
- Какие операторы составляют базовый физический план в DWH?
- Основные элементы: сканы данных (полные сканы, индексные сканы, партицированные сканы), джоины (NLJ, hash join, sort-merge join, shuffle-join), агрегации (hash-агрегации, sort-агрегации, streaming), сортировка (in-memory и внешняя), а также операции по управлению памятью и spill. В распределённых системах добавляются элементы распределения и ко-лотирования данных.
- Как определить, что причина задержек - spill?**
- Необходимо смотреть на метрики памяти, частоту spill-операций и объём данных, которые были записаны на диск. В EXPLAIN-плане можно увидеть признаки spill: операции, которые явно помечены как «spill to disk» или «external sort/aggregate». Мониторинг I/O и время ожидания также поможет диагностировать spill как фактор задержек.
- Какие практики помогают снизить количество shuffle в планах?
- Ключевые подходы: размещение данных по ключу распределения схемами partitioning/clustering, co-location данных на нодах, раннее применение фильтров и агрегаций на локальном уровне, использование локальных join-операций и уменьшение пересылки больших наборов данных между узлами.
- Как выбрать между hash join и sort-merge join?
- В большинстве случаев hash-join предпочтителен при больших входах и отсутствии упорядочения, если достаточно памяти для хеш-таблицы. Sort-merge join эффективен, когда входы отсортированы или легко приведены к сортировке, и когда затраты на сортировку ниже, чем стоимость хеш-таблицы. В некоторых системах адаптивно переключаются между стратегиями в зависимости от текущей загрузки и памяти.
- Какие параметры памяти чаще всего влияют на производительность операций агрегации?
- Основные параметры: объем памяти под буферы агрегаций, размер рабочих наборов, параллелизм исполнения и настройки spill. Увеличение доступной памяти снижает риск spill и ускоряет агрегацию, но увеличивает конкуренцию за ресурсы в кластере.
- Какие подходы помогают проводить мониторинг физического плана в продуктивной среде?
- Использование Explain для анализа планов, сбор и анализ профилей исполнения, метрик по задержкам, памяти и IO, визуализация исполнения в дашбордах, регламентированные тесты на нагрузке в CI/CD, а также сохранение версий планов и регрессионный мониторинг изменений.
- Как связь между форматом данных и физическим планом влияет на производительность?
- Форматы данных, например Parquet или ORC, поддерживают предикатное чтение и эффективную компрессию, что позволяет значительно снизить объём читаемых данных. Это влияет на выбор скана, возможность predicate pushdown и общую скорость исполнения.
- Как внедрять оптимизации физического плана в DevOps практику?
- Включать тесты на производительность и регрессионные тесты, сохранять и сравнивать планы до/после оптимизаций, использовать CI/CD для автоматизированного анализа планов и мониторинга выполнения, документировать принципы выбора стратегий и консервативно внедрять изменения в проде.
- Какие современные практики улучшают управляемость физического плана в больших средах?
- Автоматизация профилирования и мониторинга, адаптивная оптимизация исполнения, поддержка ко-локирования данных, внедрение материализованных представлений и агрегатов, а также тесная интеграция с каталогами данных и системами управления метаданными. Эти практики позволяют уменьшать задержки и повышать устойчивость к пиковым нагрузкам.



