> ## 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 ? "صفوف" : "صف"}
                </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/ar/reference/functions/regular-functions/array-functions) الأكثر شيوعًا.

<div id="array-basics">
  ## مقدمة إلى المصفوفات
</div>

المصفوفة هي بنية بيانات في الذاكرة تجمع القيم معًا.
نسمّي هذه القيم *عناصر* المصفوفة، ويمكن الإشارة إلى كل عنصر عبر فهرس يحدّد موضعه ضمن هذا التجميع.

يمكن إنشاء المصفوفات في ClickHouse باستخدام الدالة [`array`](/docs/ar/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/ar/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، فسيُختار نوع أشمل من 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/ar/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.
ستكون القيمة المُعادة هي *القيمة الافتراضية* لنوع المصفوفة.
في المثال أدناه، تُعاد سلسلة فارغة لأنها القيمة الافتراضية لنوع البيانات 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` لإرجاع عدد العناصر في Array:

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

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

يمكنك أيضًا استخدام الدالة [`arrayEnumerate`](/docs/ar/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/ar/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/ar/get-started/sample-datasets/ontime)، التي تحتوي على بيانات الرحلات من هيئة إحصاءات النقل.
يمكنك العثور على مجموعة البيانات هذه في [SQL playground](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'.
ما يهمّنا هو معرفة عدد الرحلات التي تنطلق من كل مطار.
تتضمن بياناتنا صفًا واحدًا لكل رحلة، لكن سيكون من الملائم لو أمكننا تجميع البيانات حسب مطار الانطلاق ووضع الوجهات في Array.

ولتحقيق ذلك، يمكننا استخدام الدالة التجميعية [`groupArray`](/docs/ar/reference/functions/aggregate-functions/groupArray)، التي تأخذ قيم العمود المحدد من كل صف وتجمعها في Array.

شغّل الاستعلام أدناه لترى كيف يعمل ذلك:

<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/ar/reference/functions/regular-functions/type-conversion-functions#toStringCutToZero) في الاستعلام أعلاه لإزالة محارف NULL التي تظهر بعد الرمز المكوّن من ثلاثة أحرف لبعض المطارات.

بهذا التنسيق للبيانات، يمكننا بسهولة معرفة ترتيب أكثر المطارات ازدحامًا من خلال إيجاد طول Arrays ‏"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>

رأينا في الاستعلام السابق أن مطار مطار دنفر الدولي كان المطار الذي شهد أكبر عدد من الرحلات المغادرة في اليوم الذي اخترناه.
لنلقِ نظرة الآن على عدد تلك الرحلات التي غادرت في الوقت المحدد، أو تأخرت من 15 إلى 30 دقيقة، أو تأخرت لأكثر من 30 دقيقة.

كثير من دوال المصفوفات في ClickHouse هي ما يُعرف باسم ["الدوال عالية الرتبة"](/docs/ar/reference/functions/regular-functions/overview#higher-order-functions)، وتقبل دالة لامبدا بوصفها المعامل الأول.
وتُعد الدالة [`arrayMap`](/docs/ar/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/ar/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>

في query أعلاه، نمرّر دالة لامبدا بوصفها argument الأول إلى الدالة [`arrayFilter`](/docs/ar/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/ar/reference/functions/regular-functions/array-functions#arraySort) و[`arrayIntersect`](/docs/ar/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` باستخدام تعبير جدول مشترك (CTE)، إذ ينظر إلى جميع الرحلات في 1 يناير 2024، وينشئ لكل مطار انطلاق قائمة مرتبة بكل وجهة فريدة يخدمها ذلك المطار.
في مجموعة النتائج `airport_routes`، على سبيل المثال، قد تكون لدى DEN مصفوفة تحتوي على جميع المدن التي تسير إليها رحلاته، مثل `['ATL', 'BOS', 'LAX', 'MIA', ...]` وهكذا.

في المرحلة الثانية، يأخذ الاستعلام خمسة مطارات محورية أمريكية كبرى (`DEN` و`ATL` و`DFW` و`ORD` و`LAS`) ويقارن كل زوج ممكن منها.
ويفعل ذلك باستخدام `CROSS JOIN`، الذي ينشئ جميع التركيبات الممكنة لهذه المطارات.
ثم يستخدم، لكل زوج، الدالة `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`.
قد ترغب أيضًا في التعرّف على هياكل بيانات ذات صلة مثل أنواع [`tuples`](/docs/ar/reference/data-types/tuple#creating-tuples) و[JSON](/docs/ar/reference/data-types/newjson) و[Map](/docs/ar/reference/data-types/map)، التي تعمل بكفاءة إلى جانب المصفوفات.
تدرّب على تطبيق هذه المفاهيم على مجموعات بياناتك الخاصة، وجرّب استعلامات مختلفة في SQL playground أو على مجموعات بيانات تجريبية أخرى.

تُعدّ المصفوفات ميزة أساسية في 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>
