> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Работа с массивами в ClickHouse

> Краткое руководство по работе с массивами в ClickHouse

export const RunnableCode = ({children, run = false, showStats = true}) => {
  const [results, setResults] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);
  const [showResults, setShowResults] = useState(false);
  const [stats, setStats] = useState(null);
  const [isDark, setIsDark] = useState(false);
  const [hoveredRow, setHoveredRow] = useState(-1);
  const codeRef = useRef(null);
  useEffect(() => {
    if (typeof window !== "undefined") {
      const check = () => setIsDark(document.documentElement.classList.contains("dark"));
      check();
      const observer = new MutationObserver(check);
      observer.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ["class"]
      });
      return () => observer.disconnect();
    }
  }, []);
  useEffect(() => {
    if (codeRef.current) {
      const block = codeRef.current.querySelector(".code-block");
      if (block) {
        block.style.marginBottom = "0";
        block.style.marginTop = "0";
        block.style.borderBottomLeftRadius = "0";
        block.style.borderBottomRightRadius = "0";
      }
    }
  });
  const getSqlText = () => {
    if (!codeRef.current) return "";
    const code = codeRef.current.querySelector("code");
    return (code || codeRef.current).textContent.trim();
  };
  const executeQuery = async () => {
    const sql = getSqlText();
    if (!sql) return;
    setLoading(true);
    setError(null);
    setResults(null);
    setShowResults(true);
    try {
      const cleanQuery = sql.replace(/;$/, "").trim();
      const params = new URLSearchParams({
        query: cleanQuery,
        default_format: "JSONCompact",
        result_overflow_mode: "break",
        read_overflow_mode: "break",
        allow_experimental_analyzer: "1"
      });
      const res = await fetch(`https://sql-clickhouse.clickhouse.com/?${params.toString()}`, {
        method: "POST",
        headers: {
          Authorization: `Basic ${btoa(`demo:`)}`
        }
      });
      const text = await res.text();
      if (!res.ok) {
        setError(text || `HTTP ${res.status}`);
        setLoading(false);
        return;
      }
      const json = JSON.parse(text);
      setResults(json);
      setStats(json.statistics || null);
    } catch (err) {
      setError(err.message || "Ошибка выполнения запроса");
    }
    setLoading(false);
  };
  useEffect(() => {
    if (run) executeQuery();
  }, []);
  const formatRows = n => {
    if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
    if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
    if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
    return String(n);
  };
  const formatBytes = b => {
    if (b >= 1e9) return `${(b / 1e9).toFixed(2)} GB`;
    if (b >= 1e6) return `${(b / 1e6).toFixed(2)} MB`;
    if (b >= 1e3) return `${(b / 1e3).toFixed(2)} KB`;
    return `${b} B`;
  };
  const isNumericType = type => {
    return (/^(UInt|Int|Float|Decimal)/).test(type);
  };
  const isHyperlink = value => {
    return typeof value === "string" && (/^https?:\/\//).test(value);
  };
  const computeColumnExtremes = (meta, data) => {
    const extremes = {};
    for (let i = 0; i < meta.length; i++) {
      if (isNumericType(meta[i].type)) {
        let min = Infinity, max = -Infinity;
        for (const row of data) {
          const v = Number(row[i]);
          if (!isNaN(v)) {
            if (v < min) min = v;
            if (v > max) max = v;
          }
        }
        if (max > -Infinity) {
          extremes[i] = {
            min,
            max
          };
        }
      }
    }
    return extremes;
  };
  const computeColumnWidths = (meta, data) => {
    const lengths = meta.map((col, i) => {
      const headerLen = col.name.length + col.type.length + 1;
      let maxData = 0;
      for (const row of data) {
        const v = row[i];
        const len = v === null ? 4 : String(v).length;
        if (len > maxData) maxData = len;
      }
      return Math.max(headerLen, maxData);
    });
    const total = lengths.reduce((s, l) => s + l, 0);
    return lengths.map(l => `${(l / total * 100).toFixed(1)}%`);
  };
  const copyResultsAsTSV = () => {
    if (!results || !results.meta || !results.data) return;
    const header = results.meta.map(col => col.name).join("\t");
    const rows = results.data.map(row => row.map(cell => cell === null ? "NULL" : String(cell)).join("\t"));
    const tsv = [header, ...rows].join("\n");
    navigator.clipboard.writeText(tsv);
  };
  const borderColor = isDark ? "rgba(255,255,255,0.15)" : "#e5e7eb";
  const bgColor = isDark ? "rgba(255,255,255,0.05)" : "#f9fafb";
  const headerBg = isDark ? "#2a2a2a" : "#f3f4f6";
  const textColor = isDark ? "#e5e7eb" : "#1f2937";
  const mutedColor = isDark ? "#d1d5db" : "#6b7280";
  const accentColor = isDark ? "#FAFF69" : "#323232";
  const accentTextColor = isDark ? "#000" : "#fff";
  const barColor = isDark ? "#35372f" : "#d2d2d2";
  const cellBg = isDark ? "#1f201b" : "#ffffff";
  const cellBgHover = isDark ? "lch(15.8 0 0)" : "#f0f0f0";
  const extremes = results && results.meta && results.data ? computeColumnExtremes(results.meta, results.data) : {};
  const colWidths = results && results.meta && results.data ? computeColumnWidths(results.meta, results.data) : [];
  const getCellBarStyle = (cell, ci, ri) => {
    if (cell === null) return null;
    const colMeta = results.meta[ci];
    if (!isNumericType(colMeta.type) || !extremes[ci] || results.data.length <= 1 || extremes[ci].max <= 0) return null;
    const ratio = 100 * Number(cell) / extremes[ci].max;
    const bg = ri === hoveredRow ? cellBgHover : cellBg;
    return {
      background: `linear-gradient(to right, ${barColor} 0%, ${barColor} ${ratio}%, ${bg} ${ratio}%, ${bg} 100%)`
    };
  };
  const renderCell = (cell, ci) => {
    if (cell === null) {
      return <span style={{
        color: mutedColor,
        fontStyle: "italic"
      }}>NULL</span>;
    }
    const value = String(cell);
    if (isHyperlink(value)) {
      return <a href={value} target="_blank" rel="noopener noreferrer" style={{
        color: accentColor,
        textDecoration: "underline",
        cursor: "pointer"
      }}>
          {value}
        </a>;
    }
    return value;
  };
  return <div className="not-prose" style={{
    margin: "1rem 0",
    width: "100%",
    boxSizing: "border-box",
    contain: "inline-size"
  }}>
      {}
      <div>
        <div ref={codeRef}>{children}</div>

        {}
        <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "6px 12px",
    backgroundColor: headerBg,
    borderWidth: "0 1px 1px 1px",
    borderStyle: "solid",
    borderColor: isDark ? "rgba(255,255,255,0.1)" : "rgba(11,11,11,0.1)",
    borderRadius: "0 0 4px 4px"
  }}>
          <div style={{
    display: "flex",
    alignItems: "center",
    gap: "12px"
  }}>
            {results && <button onClick={() => setShowResults(!showResults)} style={{
    background: "none",
    border: "none",
    cursor: "pointer",
    color: mutedColor,
    fontSize: "12px",
    padding: "2px 4px"
  }}>
                {showResults ? "▼ Скрыть результаты" : "▶ Показать результаты"}
              </button>}
            {showStats && stats && <span style={{
    fontSize: "11px",
    color: mutedColor,
    fontStyle: "italic"
  }}>
                Прочитано {formatRows(stats.rows_read)} строк, {formatBytes(stats.bytes_read)} за {stats.elapsed.toFixed(3)}с
              </span>}
          </div>
          <button onClick={() => executeQuery()} disabled={loading} style={{
    display: "flex",
    alignItems: "center",
    gap: "6px",
    padding: "4px 14px",
    borderRadius: "4px",
    border: "none",
    cursor: loading ? "wait" : "pointer",
    backgroundColor: accentColor,
    color: accentTextColor,
    fontSize: "12px",
    fontWeight: 600
  }}>
            {loading ? <span>Выполняется...</span> : <>
                <span style={{
    fontSize: "10px"
  }}>▶</span>
                <span>Выполнить</span>
              </>}
          </button>
        </div>
      </div>

      {}
      {showResults && <div className="not-prose" style={{
    marginTop: "8px",
    maxHeight: "350px",
    overflow: "auto",
    border: `1px solid ${borderColor}`,
    borderRadius: "4px"
  }}>
          <div>
            {loading && <div style={{
    padding: "24px",
    textAlign: "center",
    color: mutedColor
  }}>Выполнение запроса...</div>}

            {error && <div style={{
    padding: "12px 16px",
    color: "#ef4444",
    backgroundColor: isDark ? "rgba(239,68,68,0.1)" : "#fef2f2",
    fontSize: "13px",
    fontFamily: "monospace",
    whiteSpace: "pre-wrap"
  }}>
                {error}
              </div>}

            {results && results.meta && results.data && <div style={{
    display: "grid",
    gridTemplateColumns: colWidths.join(" "),
    width: "100%",
    fontSize: "13px",
    fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'
  }}>
                {results.meta.map((col, i) => <div key={`h-${i}`} style={{
    position: "sticky",
    top: 0,
    zIndex: 1,
    padding: "6px 12px",
    textAlign: isNumericType(col.type) && results.meta.length > 1 ? "right" : "left",
    backgroundColor: headerBg,
    borderBottom: `1px solid ${borderColor}`,
    color: textColor,
    fontWeight: 600,
    fontSize: "12px",
    whiteSpace: "nowrap",
    overflow: "hidden",
    textOverflow: "ellipsis"
  }}>
                    {col.name}
                    <span style={{
    color: mutedColor,
    fontWeight: 400,
    marginLeft: "4px",
    fontSize: "10px"
  }}>{col.type}</span>
                  </div>)}
                {results.data.map((row, ri) => row.map((cell, ci) => <div key={`${ri}-${ci}`} onMouseEnter={() => setHoveredRow(ri)} onMouseLeave={() => setHoveredRow(-1)} style={{
    padding: "4px 12px",
    color: textColor,
    whiteSpace: "nowrap",
    overflow: "hidden",
    textOverflow: "ellipsis",
    textAlign: isNumericType(results.meta[ci].type) && results.meta.length > 1 ? "right" : "left",
    borderBottom: `1px solid ${borderColor}`,
    backgroundColor: ri === hoveredRow ? cellBgHover : ri % 2 === 0 ? "transparent" : bgColor,
    transition: "background-color 0.1s",
    ...getCellBarStyle(cell, ci, ri)
  }}>
                      {renderCell(cell, ci)}
                    </div>))}
              </div>}

            {results && results.data && <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "4px 12px",
    fontSize: "11px",
    color: mutedColor,
    borderTop: `1px solid ${borderColor}`,
    backgroundColor: headerBg
  }}>
                <span>
                  {results.rows} {results.rows === 1 ? "строка" : results.rows >= 2 && results.rows <= 4 ? "строки" : "строк"}
                </span>
                <button onClick={copyResultsAsTSV} style={{
    background: "none",
    border: "none",
    cursor: "pointer",
    color: mutedColor,
    fontSize: "11px",
    padding: "2px 6px",
    borderRadius: "3px"
  }} onMouseEnter={e => e.target.style.color = textColor} onMouseLeave={e => e.target.style.color = mutedColor}>
                  ⧉ Копировать TSV
                </button>
              </div>}
          </div>
        </div>}
    </div>;
};

> В этом руководстве вы узнаете, как использовать массивы в ClickHouse, а также некоторые из наиболее часто используемых [функций для работы с массивами](/docs/ru/reference/functions/regular-functions/array-functions).

<div id="array-basics">
  ## Введение в массивы
</div>

Массив — это структура данных в памяти, которая объединяет значения.
Эти значения называются *элементами* массива, и к каждому элементу можно обратиться по индексу, который указывает его положение в массиве.

Массивы в ClickHouse можно создавать с помощью функции [`array`](/docs/ru/reference/data-types/array):

```sql theme={null}
array(T)
```

Или, в качестве альтернативы, используя `[]`:

```sql theme={null}
[]
```

Например, можно создать массив чисел:

```sql theme={null}
SELECT array(1, 2, 3) AS numeric_array
```

```response theme={null}
┌─numeric_array─┐
│ [1,2,3]       │
└───────────────┘
```

Или массив строк:

```sql theme={null}
SELECT array('hello', 'world') AS string_array
```

```response theme={null}
┌─string_array──────┐
│ ['hello','world'] │
└───────────────────┘
```

Или массив вложенных типов, например [Tuple](/docs/ru/reference/data-types/tuple):

```sql theme={null}
SELECT array(tuple(1, 2), tuple(3, 4))
```

```response theme={null}
┌─[(1, 2), (3, 4)]─┐
│ [(1,2),(3,4)]    │
└──────────────────┘
```

У вас может возникнуть соблазн создать массив из значений разных типов вот так:

```sql theme={null}
SELECT array('Hello', 'world', 1, 2, 3)
```

Однако элементы массива всегда должны иметь общий супертип — наименьший тип данных, который может без потерь представлять значения двух или более разных типов, что позволяет использовать их вместе.
Если общего супертипа нет, при попытке сформировать массив возникнет исключение:

```sql theme={null}
Received exception:
Code: 386. DB::Exception: There is no supertype for types String, String, UInt8, UInt8, UInt8 because some of them are String/FixedString/Enum and some of them are not: In scope SELECT ['Hello', 'world', 1, 2, 3]. (NO_COMMON_TYPE)
```

при создании массивов на лету ClickHouse выбирает наиболее узкий тип, подходящий для всех элементов.
Например, если вы создаёте массив из целых чисел и чисел с плавающей точкой, выбирается супертип Float:

```sql theme={null}
SELECT [1::UInt8, 2.5::Float32, 3::UInt8] AS mixed_array, toTypeName([1, 2.5, 3]) AS array_type;
```

```response theme={null}
┌─mixed_array─┬─array_type─────┐
│ [1,2.5,3]   │ Array(Float64) │
└─────────────┴────────────────┘
```

<Accordion title="Создание массивов разных типов">
  Вы можете использовать настройку `use_variant_as_common_type`, чтобы изменить описанное выше поведение по умолчанию.
  Это позволяет использовать тип [Variant](/docs/ru/reference/data-types/variant) в качестве результирующего типа для функций `if`/`multiIf`/`array`/`map`, когда для типов аргументов нет общего типа.

  Например:

  ```sql theme={null}
  SELECT
      [1, 'ClickHouse', ['Another', 'Array']] AS array,
      toTypeName(array)
  SETTINGS use_variant_as_common_type = 1;
  ```

  ```response theme={null}
  ┌─array────────────────────────────────┬─toTypeName(array)────────────────────────────┐
  │ [1,'ClickHouse',['Another','Array']] │ Array(Variant(Array(String), String, UInt8)) │
  └──────────────────────────────────────┴──────────────────────────────────────────────┘
  ```

  Затем вы также можете извлекать типы из массива по имени типа:

  ```sql theme={null}
  SELECT
      [1, 'ClickHouse', ['Another', 'Array']] AS array,
      array.UInt8,
      array.String,
      array.`Array(String)`
  SETTINGS use_variant_as_common_type = 1;
  ```

  ```response theme={null}
  ┌─array────────────────────────────────┬─array.UInt8───┬─array.String─────────────┬─array.Array(String)─────────┐
  │ [1,'ClickHouse',['Another','Array']] │ [1,NULL,NULL] │ [NULL,'ClickHouse',NULL] │ [[],[],['Another','Array']] │
  └──────────────────────────────────────┴───────────────┴──────────────────────────┴─────────────────────────────┘
  ```
</Accordion>

Использование индекса с `[]` — удобный способ доступа к элементам массива.
В ClickHouse важно помнить, что индекс массива всегда начинается с **1**.
Это может отличаться от других языков программирования, к которым вы привыкли, где индексация массивов начинается с нуля.

Например, имея массив, вы можете выбрать его первый элемент, написав:

```sql theme={null}
WITH array('hello', 'world') AS string_array
SELECT string_array[1];
```

```response theme={null}
┌─arrayElement⋯g_array, 1)─┐
│ hello                    │
└──────────────────────────┘
```

Также можно использовать отрицательные индексы.
Так можно выбирать элементы относительно последнего:

```sql theme={null}
WITH array('hello', 'world') AS string_array
SELECT string_array[-1];
```

```response theme={null}
┌─arrayElement⋯g_array, -1)─┐
│ world                     │
└───────────────────────────┘
```

Несмотря на то что индексация в массивах начинается с 1, вы всё равно можете обратиться к элементу в позиции 0.
Возвращаемым значением будет *значение по умолчанию* для типа Array.
В примере ниже возвращается пустая строка, так как это значение по умолчанию для типа данных String:

```sql theme={null}
WITH ['hello', 'world', 'arrays are great aren\'t they?'] AS string_array
SELECT string_array[0]
```

```response theme={null}
┌─arrayElement⋯g_array, 0)─┐
│                          │
└──────────────────────────┘
```

<div id="array-functions">
  ## Функции для работы с массивами
</div>

ClickHouse предоставляет множество полезных функций для работы с массивами.
В этом разделе мы рассмотрим некоторые из самых полезных — от самых простых до более сложных.

<div id="length-arrayEnumerate-indexOf-has-functions">
  ### функции length, arrayEnumerate, indexOf и has\*
</div>

Функция `length` возвращает количество элементов в массиве:

```sql theme={null}
WITH array('learning', 'ClickHouse', 'arrays') AS string_array
SELECT length(string_array);
```

```response theme={null}
┌─length(string_array)─┐
│                    3 │
└──────────────────────┘
```

Вы также можете использовать функцию [`arrayEnumerate`](/docs/ru/reference/functions/regular-functions/array-functions#arrayEnumerate), чтобы получить массив индексов элементов:

```sql theme={null}
WITH array('learning', 'ClickHouse', 'arrays') AS string_array
SELECT arrayEnumerate(string_array);
```

```response theme={null}
┌─arrayEnumerate(string_array)─┐
│ [1,2,3]                      │
└──────────────────────────────┘
```

Если вы хотите найти индекс конкретного значения, можно использовать функцию `indexOf`:

```sql theme={null}
SELECT indexOf([4, 2, 8, 8, 9], 8);
```

```response theme={null}
┌─indexOf([4, 2, 8, 8, 9], 8)─┐
│                           3 │
└─────────────────────────────┘
```

Обратите внимание, что эта функция вернёт первый найденный индекс, если в массиве есть несколько одинаковых значений.
Если элементы массива отсортированы по возрастанию, можно использовать функцию [`indexOfAssumeSorted`](/docs/ru/reference/functions/regular-functions/array-functions#indexOfAssumeSorted).

Функции `has`, `hasAll` и `hasAny` полезны, чтобы определить, содержит ли массив заданное значение.
Рассмотрим следующий пример:

```sql theme={null}
WITH ['Airbus A380', 'Airbus A350', 'Airbus A220', 'Boeing 737', 'Boeing 747-400'] AS airplanes
SELECT
    has(airplanes, 'Airbus A350') AS has_true,
    has(airplanes, 'Lockheed Martin F-22 Raptor') AS has_false,
    hasAny(airplanes, ['Boeing 737', 'Eurofighter Typhoon']) AS hasAny_true,
    hasAny(airplanes, ['Lockheed Martin F-22 Raptor', 'Eurofighter Typhoon']) AS hasAny_false,
    hasAll(airplanes, ['Boeing 737', 'Boeing 747-400']) AS hasAll_true,
    hasAll(airplanes, ['Boeing 737', 'Eurofighter Typhoon']) AS hasAll_false
FORMAT Vertical;
```

```response theme={null}
has_true:     1
has_false:    0
hasAny_true:  1
hasAny_false: 0
hasAll_true:  1
hasAll_false: 0
```

<div id="exploring-flight-data-with-array-functions">
  ## Анализ данных о рейсах с помощью функций для работы с массивами
</div>

До сих пор примеры были довольно простыми.
Полезность массивов по-настоящему раскрывается при работе с реальным набором данных.

Мы будем использовать [набор данных ontime](/docs/ru/get-started/sample-datasets/ontime), который содержит данные о рейсах из Бюро транспортной статистики США.
Этот набор данных доступен в [Песочнице ClickHouse](https://sql.clickhouse.com/?query_id=M4FSVBVMSHY98NKCQP8N4K).

Мы выбрали этот набор данных, потому что массивы часто хорошо подходят для работы с временными рядами и помогают упростить
иначе довольно сложные запросы.

<Tip>
  Нажмите кнопку "play" ниже, чтобы запускать запросы прямо в документации и сразу видеть результат.
</Tip>

<div id="grouparray">
  ### groupArray
</div>

В этом наборе данных много столбцов, но мы сосредоточимся лишь на некоторых из них.
Выполните запрос ниже, чтобы посмотреть, как выглядят наши данные:

<RunnableCode>
  ```sql theme={null}
  -- SELECT
  -- *
  -- FROM ontime.ontime LIMIT 100

  SELECT
      FlightDate,
      Origin,
      OriginCityName,
      Dest,
      DestCityName,
      DepTime,
      DepDelayMinutes,
      ArrTime,
      ArrDelayMinutes
  FROM ontime.ontime LIMIT 5
  ```
</RunnableCode>

Рассмотрим 10 самых загруженных аэропортов США за случайно выбранный день, например '2024-01-01'.
Нас интересует, сколько рейсов вылетает из каждого аэропорта.
В наших данных каждой строке соответствует отдельный рейс, но было бы удобно сгруппировать данные по аэропорту вылета и собрать пункты назначения в массив.

Для этого можно использовать агрегатную функцию [`groupArray`](/docs/ru/reference/functions/aggregate-functions/groupArray), которая берет значения указанного столбца из каждой строки и объединяет их в массив.

Выполните запрос ниже, чтобы посмотреть, как это работает:

<RunnableCode>
  ```sql theme={null}
  SELECT
      FlightDate,
      Origin,
      groupArray(toStringCutToZero(Dest)) AS Destinations
  FROM ontime.ontime
  WHERE Origin IN ('ATL', 'ORD', 'DFW', 'DEN', 'LAX', 'JFK', 'LAS', 'CLT', 'SFO', 'SEA') AND FlightDate='2024-01-01'
  GROUP BY FlightDate, Origin
  ORDER BY length(Destinations)
  ```
</RunnableCode>

[`toStringCutToZero`](/docs/ru/reference/functions/regular-functions/type-conversion-functions#toStringCutToZero) в приведенном выше запросе используется для удаления нулевых символов, которые встречаются после трехбуквенного кода некоторых аэропортов.

Когда данные представлены в таком формате, мы можем легко определить порядок самых загруженных аэропортов, вычислив длину собранных массивов "Destinations":

<RunnableCode>
  ```sql highlight={7} theme={null}
  WITH
      '2024-01-01' AS date,
      busy_airports AS (
      SELECT
      FlightDate,
      Origin,
      groupArray(toStringCutToZero(Dest)) AS Destinations
      FROM ontime.ontime
      WHERE Origin IN ('ATL', 'ORD', 'DFW', 'DEN', 'LAX', 'JFK', 'LAS', 'CLT', 'SFO', 'SEA')
      AND FlightDate = date
      GROUP BY FlightDate, Origin
      ORDER BY length(Destinations)
      )
  SELECT
      Origin,
      length(Destinations) AS outward_flights
  FROM busy_airports
  ORDER BY outward_flights DESC
  ```
</RunnableCode>

<div id="arraymap">
  ### arrayMap и arrayZip
</div>

В предыдущем запросе мы увидели, что Denver International Airport был аэропортом с наибольшим числом вылетающих рейсов в выбранный нами день.
Давайте посмотрим, сколько из этих рейсов вылетели вовремя, сколько задержались на 15–30 минут и сколько — более чем на 30 минут.

Многие функции для работы с массивами в ClickHouse являются так называемыми ["функциями высшего порядка"](/docs/ru/reference/functions/regular-functions/overview#higher-order-functions) и принимают лямбда-функцию в качестве первого параметра.
Функция [`arrayMap`](/docs/ru/reference/functions/regular-functions/array-functions#arrayMap) — один из примеров такой функции высшего порядка; она возвращает новый массив на основе переданного массива, применяя лямбда-функцию к каждому элементу исходного массива.

Выполните приведённый ниже запрос, который использует функцию `arrayMap`, чтобы увидеть, какие рейсы были задержаны, а какие вылетели вовремя.
Для пар пунктов отправления/назначения он показывает бортовой номер и статус каждого рейса:

<RunnableCode>
  ```sql theme={null}
  WITH arrayMap(
                d -> if(d >= 30, 'DELAYED', if(d >= 15, 'WARNING', 'ON-TIME')),
                groupArray(DepDelayMinutes)
      ) AS statuses

  SELECT
      Origin,
      toStringCutToZero(Dest) AS Destination,
      arrayZip(groupArray(Tail_Number), statuses) as tailNumberStatuses
  FROM ontime.ontime
  WHERE Origin = 'DEN'
    AND FlightDate = '2024-01-01'
    AND DepTime IS NOT NULL
    AND DepDelayMinutes IS NOT NULL
  GROUP BY ALL
  ```
</RunnableCode>

В приведённом выше запросе функция `arrayMap` принимает одноэлементный массив `[DepDelayMinutes]` и применяет к нему лямбда-функцию `d -> if(d >= 30, 'DELAYED', if(d >= 15, 'WARNING', 'ON-TIME'`, чтобы отнести значение к определённой категории.
Затем первый элемент результирующего массива извлекается с помощью `[DepDelayMinutes][1]`.
Функция [`arrayZip`](/docs/ru/reference/functions/regular-functions/array-functions#arrayZip) объединяет массив `Tail_Number` и массив `statuses` в один массив.

<div id="arrayfilter">
  ### arrayFilter
</div>

Теперь рассмотрим только количество рейсов с задержкой 30 минут и более для аэропортов `DEN`, `ATL` и `DFW`:

<RunnableCode>
  ```sql highlight={4} theme={null}
  SELECT
      Origin,
      OriginCityName,
      length(arrayFilter(d -> d >= 30, groupArray(ArrDelayMinutes))) AS num_delays_30_min_or_more
  FROM ontime.ontime
  WHERE Origin IN ('DEN', 'ATL', 'DFW')
      AND FlightDate = '2024-01-01'
  GROUP BY Origin, OriginCityName
  ORDER BY num_delays_30_min_or_more DESC
  ```
</RunnableCode>

В запросе выше мы передаём лямбда-функцию в качестве первого аргумента функции [`arrayFilter`](/docs/ru/reference/functions/regular-functions/array-functions#arrayFilter).
Эта лямбда-функция принимает значение задержки в минутах (`d`) и возвращает `1`, если условие выполняется, и `0` — в противном случае.

```sql theme={null}
d -> d >= 30
```

<div id="arraysort-and-arrayintersect">
  ### arraySort и arrayIntersect
</div>

Далее разберёмся, какие пары крупных аэропортов США имеют больше всего общих пунктов назначения, с помощью функций [`arraySort`](/docs/ru/reference/functions/regular-functions/array-functions#arraySort) и [`arrayIntersect`](/docs/ru/reference/functions/regular-functions/array-functions#arrayIntersect).
`arraySort` принимает массив и по умолчанию сортирует его элементы по возрастанию, хотя в неё также можно передать лямбда-функцию, чтобы задать порядок сортировки.
`arrayIntersect` принимает несколько массивов и возвращает массив с элементами, которые присутствуют во всех этих массивах.

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

<RunnableCode>
  ```sql highlight={4,12} theme={null}
  WITH airport_routes AS (
      SELECT 
          Origin,
          arraySort(groupArray(DISTINCT toStringCutToZero(Dest))) AS destinations
      FROM ontime.ontime
      WHERE FlightDate = '2024-01-01'
      GROUP BY Origin
  )
  SELECT 
      a1.Origin AS airport1,
      a2.Origin AS airport2,
      length(arrayIntersect(a1.destinations, a2.destinations)) AS common_destinations
  FROM airport_routes a1
  CROSS JOIN airport_routes a2
  WHERE a1.Origin < a2.Origin
      AND a1.Origin IN ('DEN', 'ATL', 'DFW', 'ORD', 'LAS')
      AND a2.Origin IN ('DEN', 'ATL', 'DFW', 'ORD', 'LAS')
  ORDER BY common_destinations DESC
  LIMIT 10
  ```
</RunnableCode>

Запрос работает в два основных этапа.
Сначала он создаёт временный набор данных `airport_routes` с помощью Common Table Expression (CTE): в нём рассматриваются все рейсы за 1 января 2024 года, и для каждого аэропорта вылета строится отсортированный список всех уникальных пунктов назначения, которые он обслуживает.
Например, в результирующем наборе `airport_routes` для DEN может быть массив со всеми городами, в которые из него выполняются рейсы, например `['ATL', 'BOS', 'LAX', 'MIA', ...]`, и так далее.

На втором этапе запрос берёт пять крупных узловых аэропортов США (`DEN`, `ATL`, `DFW`, `ORD` и `LAS`) и сравнивает все возможные пары.
Для этого используется перекрёстное соединение, которое создаёт все комбинации этих аэропортов.
Затем для каждой пары применяется функция `arrayIntersect`, чтобы найти пункты назначения, присутствующие в списках обоих аэропортов.
Функция `length` подсчитывает, сколько у них общих пунктов назначения.

Условие `a1.Origin < a2.Origin` гарантирует, что каждая пара появляется только один раз.
Без него в результатах были бы и JFK-LAX, и LAX-JFK как отдельные строки, хотя это одно и то же сравнение.
Наконец, запрос сортирует результаты так, чтобы показать пары аэропортов с наибольшим числом общих пунктов назначения, и возвращает только первые 10.
Это позволяет увидеть, у каких крупных хабов маршрутные сети пересекаются сильнее всего. Это может указывать на конкурентные рынки, где несколько авиакомпаний обслуживают одни и те же пары городов, или на хабы, обслуживающие схожие географические регионы и потенциально подходящие в качестве альтернативных пересадочных узлов для пассажиров.

<div id="arrayReduce">
  ### arrayReduce
</div>

Пока мы рассматриваем задержки, давайте используем ещё одну функцию высшего порядка для массивов — `arrayReduce`, чтобы найти среднюю и максимальную задержку
для каждого маршрута из международного аэропорта Денвера:

<RunnableCode>
  ```sql highlight={5-6} theme={null}
  SELECT
      Origin,
      toStringCutToZero(Dest) AS Destination,
      groupArray(DepDelayMinutes) AS delays,
      round(arrayReduce('avg', groupArray(DepDelayMinutes)), 2) AS avg_delay,
      round(arrayReduce('max', groupArray(DepDelayMinutes)), 2) AS worst_delay
  FROM ontime.ontime
  WHERE Origin = 'DEN'
      AND FlightDate = '2024-01-01'
      AND DepDelayMinutes IS NOT NULL
  GROUP BY Origin, Destination
  ORDER BY avg_delay DESC
  ```
</RunnableCode>

В примере выше мы использовали `arrayReduce`, чтобы найти среднюю и максимальную задержки для различных рейсов с вылетом из `DEN`.
`arrayReduce` применяет агрегатную функцию, указанную в первом параметре функции, к элементам переданного массива, указанного во втором параметре.

<div id="arrayJoin">
  ### arrayJoin
</div>

Обычные функции в ClickHouse возвращают столько же строк, сколько получают на вход.
Однако есть одна интересная и необычная функция, которая нарушает это правило, — `arrayJoin`.

`arrayJoin` «разворачивает» массив, создавая отдельную строку для каждого его элемента.
Это похоже на SQL-функции `UNNEST` или `EXPLODE` в других базах данных.

В отличие от большинства функций для работы с массивами, которые возвращают массивы или скалярные значения, `arrayJoin` кардинально меняет результирующий набор, увеличивая число строк.

Рассмотрим запрос ниже, который возвращает массив значений от 0 до 100 с шагом 10.
Этот массив можно интерпретировать как разные значения задержки: 0 минут, 10 минут, 20 минут и так далее.

<RunnableCode>
  ```sql theme={null}
  WITH range(0, 100, 10) AS delay
  SELECT delay
  ```
</RunnableCode>

С помощью `arrayJoin` можно написать запрос, который покажет, сколько было задержек вплоть до указанного числа минут между двумя аэропортами.
Запрос ниже строит гистограмму распределения задержек рейсов из Denver (DEN) в Miami (MIA) за 1 января 2024 года с использованием накопительных бакетов задержки:

<RunnableCode>
  ```sql theme={null}
  WITH range(0, 100, 10) AS delay,
      toStringCutToZero(Dest) AS Destination

  SELECT
      'Up to ' || arrayJoin(delay) || ' minutes' AS delayTime,
      countIf(DepDelayMinutes >= arrayJoin(delay)) AS flightsDelayed
  FROM ontime.ontime
  WHERE Origin = 'DEN' AND Destination = 'MIA' AND FlightDate = '2024-01-01'
  GROUP BY delayTime
  ORDER BY flightsDelayed DESC
  ```
</RunnableCode>

В запросе выше мы возвращаем массив задержек с помощью выражения CTE (выражения `WITH`).
`Destination` преобразует код пункта назначения в строку.

Мы используем `arrayJoin`, чтобы развернуть массив задержек в отдельные строки.
Каждое значение из массива `delay` становится отдельной строкой с псевдонимом `del`,
и в результате мы получаем 10 строк: одну для `del=0`, одну для `del=10`, одну для `del=20` и т. д.
Для каждого порога задержки (`del`) запрос подсчитывает, сколько рейсов имели задержку, большую или равную этому порогу,
с помощью `countIf(DepDelayMinutes >= del)`.

У `arrayJoin` также есть SQL-эквивалент — `ARRAY JOIN`.
Ниже для сравнения приведён тот же запрос, но с использованием этого SQL-эквивалента:

<RunnableCode>
  ```sql theme={null}
  WITH range(0, 100, 10) AS delay, 
       toStringCutToZero(Dest) AS Destination

  SELECT    
      'Up to ' || del || ' minutes' AS delayTime,
      countIf(DepDelayMinutes >= del) flightsDelayed
  FROM ontime.ontime
  ARRAY JOIN delay AS del
  WHERE Origin = 'DEN' AND Destination = 'MIA' AND FlightDate = '2024-01-01'
  GROUP BY ALL
  ORDER BY flightsDelayed DESC
  ```
</RunnableCode>

<div id="next-steps">
  ## Следующие шаги
</div>

Поздравляем! Вы узнали, как работать с массивами в ClickHouse: от базового создания массивов и индексирования до мощных функций, таких как `groupArray`, `arrayFilter`, `arrayMap`, `arrayReduce` и `arrayJoin`.
Чтобы продолжить изучение темы, обратитесь к полному справочнику по функциям для работы с массивами и познакомьтесь с дополнительными функциями, такими как `arrayFlatten`, `arrayReverse` и `arrayDistinct`.
Также вам может быть полезно узнать о связанных структурах данных, таких как [`Tuple`](/docs/ru/reference/data-types/tuple#creating-tuples), типы [JSON](/docs/ru/reference/data-types/newjson) и [Map](/docs/ru/reference/data-types/map), которые хорошо сочетаются с массивами.
Потренируйтесь применять эти концепции к собственным наборам данных и поэкспериментируйте с различными запросами в Песочнице ClickHouse или на других демонстрационных наборах данных.

Массивы — одна из ключевых возможностей ClickHouse, позволяющая выполнять эффективные аналитические запросы. Чем увереннее вы будете работать с функциями для массивов, тем сильнее они будут упрощать сложные агрегации и анализ временных рядов.
Если хотите узнать о массивах еще больше, рекомендуем посмотреть видео ниже на YouTube от Mark, нашего штатного эксперта по данным:

<Frame>
  <iframe src="https://www.youtube.com/embed/7jaw3J6U_h8?si=6NiEJ7S1odU-VVqX" title="Видеоплеер YouTube" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen />
</Frame>
