טבלאות ציר אינטראקטיביות לניתוח אתרים

כאשר נפחי הנתונים באתר גדלים, דוחות סטנדרטיים מתחילים להיכשל: מנהלים ממתינים לייצואים, מסד הנתונים נטען יתר על המידה, והניתוח נמשך זמן רב. אנו בונים טבלאות ציר אינטראקטיביות עם צבירה בצד השרת, המעבדות באופן מיידי אפילו מיליוני רשומות. הצוות שלנו מספק פרויקטים סוהריים—מבחירת ספריות ועד להגדרת לוגיקת השרת—ומבטיח ביצועים אמינים ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
טבלאות ציר אינטראקטיביות לניתוח אתרים
מורכב
~5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

מדוע ספריות מוכנות לא עומדות בעומס של נתונים גדולים

תארו לעצמכם אתר מסחר אלקטרוני עם 10 מיליון הזמנות. מנהל צריך דוח מכירות רבעוני מקובץ לפי קטגוריה וחודש. ייצוא אקסל רגיל לוקח 30 דקות ומכביד על מסד הנתונים. טבלת ציר עם צבירה בצד השרת מחזירה נתונים ב-200 אלפיות השנייה ומאפשרת למשתמש לשנות חיתוכים בזמן אמת. בפועל, ספריות מוכנות מתקשות עם עומס כזה: react-pivottable נחנקת על 200k+ שורות, AG Grid דורשת רישיון ארגוני למצב שרת ($2k לשנה), ו-Flexmonster לא תומכת בשאילתות אד-הוק מול מסדי נתונים יחסיים. אנו משלבים ספריות עם צבירה בצד השרת שלנו כדי למלא את הפערים הללו. החיסכון ברישיונות יכול לעלות על $10k לשנה עבור צוות של 5 מפתחים.

ויקיפדיה מגדירה טבלת צור ככלי לניתוח נתונים שמצבר נתונים לפי ממדים. בפרויקטים מסחריים, לעיתים קרובות נדרשת התאמה אישית – דבר שפתרונות מוכנים לא מספקים.

איך אנו בונים צבירה בצד השרת עבור מיליוני שורות

עבור נתונים גדולים, תצורת הציר עוברת לשרת. אנו משתמשים ב-ClickHouse או PostgreSQL עם אינדקסים. כל שינוי ציר שולח שאילתה, השרת מחזיר צבירות ב-100–500 אלפיות השנייה. אנו שומרים תוצאות במטמון לפי מפתח תצורה. להלן דוגמה ליצירת SQL מתצורה.

// Сервер — генерация SQL из конфига
function buildPivotQuery(config: PivotConfig, dateRange: [Date, Date]): string {
  const rowsExpr = config.rows.map(r => `"${r}"`).join(', ');
  const colsExpr = config.cols.map(c => `"${c}"`).join(', ');
  const valExpr = config.values[0]; // упрощение
  const aggExpr = {
    sum: `SUM("${valExpr}")`,
    count: `COUNT(*)`,
    avg: `AVG("${valExpr}")::numeric(18,2)`,
    min: `MIN("${valExpr}")`,
    max: `MAX("${valExpr}")`,
  }[config.aggFn];
  return `
    SELECT ${rowsExpr}, ${colsExpr}, ${aggExpr} AS value
    FROM events
    WHERE created_at BETWEEN $1 AND $2
    GROUP BY ${rowsExpr}, ${colsExpr}
    ORDER BY ${rowsExpr}, ${colsExpr}
  `;
}

מדריך שלב אחר שלב

  1. ניתוח מבנה הנתונים. זיהוי שדות לקיבוץ (שורות, עמודות) ומידות (סכום, ממוצע).
  2. עיצוב שאילתות SQL. יצירת שאילתות דינמיות עם GROUP BY.
  3. הגדרת ClickHouse/PostgreSQL. יצירת אינדקסים, הגדרת מטמון תוצאות.
  4. אינטגרציה עם הלקוח. שליחת שאילתה על כל שינוי תצורה.

השוואה: צבירה בצד הלקוח מול צבירה בצד השרת

מאפיין צד הלקוח צד השרת
מספר רשומות מקסימלי ~200k ללא הגבלה
זמן תגובה מיידי 100–500 אלפיות השנייה
עלות תשתית נמוכה (חינם) גבוהה יותר (שרת + מטמון ~$100 לחודש)
גמישות רק נתונים שנטענו מראש כל שאילתת SQL
מדרגיות מוגבל לדפדפן עד מיליארדי שורות

צבירה בצד הלקוח מהירה פי 5 על נתונים קטנים, אבל צד השרת מתאים למיליארדי שורות. הבחירה תלויה בנפח הנתונים ובצרכי האינטראקטיביות.

איך ליישם צבירה בצד הלקוח: מדריך שלב אחר שלב

  1. סינון נתונים – שמירה רק על רשומות שתואמות למסננים שהופעלו.
  2. קיבוץ – הפקת מפתחות לשורות ועמודות לפי ערכי השדות שנבחרו.
  3. צבירה – חישוב הערך לכל תא (סכום, ספירה, ממוצע, מינימום, מקסימום).
  4. עיבוד טבלה – פלט טבלת HTML עם סיכומים וכותרת קבועה.
קוד פונקציית הצבירה המלא (TypeScript)
type AggregateFunction = 'sum' | 'count' | 'avg' | 'min' | 'max';

interface PivotConfig {
  rows: string[];
  cols: string[];
  values: string[];
  aggFn: AggregateFunction;
  filters: Record<string, string[]>;
}

interface PivotResult {
  rowKeys: string[][];
  colKeys: string[][];
  data: Map<string, Map<string, number>>;
}

function computePivot(rawData: Record<string, any>[], config: PivotConfig): PivotResult {
  const { rows, cols, values, aggFn, filters } = config;

  const filtered = rawData.filter(row =>
    Object.entries(filters).every(([field, allowed]) =>
      !allowed.length || allowed.includes(String(row[field]))
    )
  );

  const rowKeySet = new Set<string>();
  const colKeySet = new Set<string>();
  const accumulator = new Map<string, Map<string, number[]>>();

  filtered.forEach(row => {
    const rowKey = rows.map(r => String(row[r] ?? '(пусто)')).join('||');
    const colKey = cols.map(c => String(row[c] ?? '(пусто)')).join('||');
    rowKeySet.add(rowKey);
    colKeySet.add(colKey);

    const numVal = values.reduce((sum, v) => sum + (Number(row[v]) || 0), 0);

    if (!accumulator.has(rowKey)) accumulator.set(rowKey, new Map());
    const colMap = accumulator.get(rowKey)!;
    if (!colMap.has(colKey)) colMap.set(colKey, []);
    colMap.get(colKey)!.push(numVal);
  });

  const aggregated = new Map<string, Map<string, number>>();

  accumulator.forEach((colMap, rowKey) => {
    const row = new Map<string, number>();
    colMap.forEach((vals, colKey) => {
      let result: number;
      switch (aggFn) {
        case 'sum':
          result = vals.reduce((a, b) => a + b, 0);
          break;
        case 'count':
          result = vals.length;
          break;
        case 'avg':
          result = vals.reduce((a, b) => a + b, 0) / vals.length;
          break;
        case 'min':
          result = Math.min(...vals);
          break;
        case 'max':
          result = Math.max(...vals);
          break;
      }
      row.set(colKey, result);
    });
    aggregated.set(rowKey, row);
  });

  return {
    rowKeys: Array.from(rowKeySet).sort().map(k => k.split('||')),
    colKeys: Array.from(colKeySet).sort().map(k => k.split('||')),
    data: aggregated,
  };
}
קוד רכיב PivotTable המלא (React+TypeScript)
function PivotTable({ result, config, format }: { result: PivotResult; config: PivotConfig; format?: (val: number) => string; }) {
  const fmt = format ?? (v => v.toLocaleString('ru-RU'));
  const rowTotals = result.rowKeys.map(rk => {
    const rowKey = rk.join('||');
    let total = 0;
    result.colKeys.forEach(ck => {
      total += result.data.get(rowKey)?.get(ck.join('||')) ?? 0;
    });
    return total;
  });
  const grandTotal = rowTotals.reduce((a, b) => a + b, 0);
  return (
    <div className="overflow-auto max-h-[600px]">
      <table className="text-sm border-collapse w-full">
        <thead className="sticky top-0 bg-white z-10">
          <tr>
            {config.rows.map(r => (
              <th key={r} className="border px-3 py-2 text-left bg-gray-50 font-medium">{r}</th>
            ))}
            {result.colKeys.map(ck => (
              <th key={ck.join('/')} className="border px-3 py-2 text-right bg-gray-50 font-medium whitespace-nowrap">
                {ck.join(' / ')}
              </th>
            ))}
            <th className="border px-3 py-2 text-right bg-blue-50 font-semibold">Итого</th>
          </tr>
        </thead>
        <tbody>
          {result.rowKeys.map((rk, ri) => {
            const rowKey = rk.join('||');
            return (
              <tr key={rowKey} className="hover:bg-gray-50">
                {rk.map((label, i) => (
                  <td key={i} className="border px-3 py-1.5 font-medium">{label}</td>
                ))}
                {result.colKeys.map(ck => {
                  const val = result.data.get(rowKey)?.get(ck.join('||'));
                  return (
                    <td key={ck.join('/')} className="border px-3 py-1.5 text-right tabular-nums">
                      {val != null ? fmt(val) : '—'}
                    </td>
                  );
                })}
                <td className="border px-3 py-1.5 text-right tabular-nums font-medium bg-blue-50">
                  {fmt(rowTotals[ri])}
                </td>
              </tr>
            );
          })}
        </tbody>
        <tfoot>
          <tr className="font-semibold bg-gray-100">
            <td colSpan={config.rows.length} className="border px-3 py-2">Итого</td>
            {result.colKeys.map(ck => {
              const colTotal = result.rowKeys.reduce((sum, rk) => {
                return sum + (result.data.get(rk.join('||'))?.get(ck.join('||')) ?? 0);
              }, 0);
              return (
                <td key={ck.join('/')} className="border px-3 py-2 text-right tabular-nums">{fmt(colTotal)}</td>
              );
            })}
            <td className="border px-3 py-2 text-right tabular-nums bg-blue-100">{fmt(grandTotal)}</td>
          </tr>
        </tfoot>
      </table>
    </div>
  );
}

ייצוא לאקסל

אנו משתמשים ב-// Сервер — генерация SQL из конфига function buildPivotQuery(config: PivotConfig, dateRange: [Date, Date]): string { const rowsExpr = config.rows.map(r => `"${r}"`).join(', '); const colsExpr = config.cols.map(c => `"${c}"`).join(', '); const valExpr = config.values[0]; // упрощение const aggExpr = { sum: `SUM("${valExpr}")`, count: `COUNT(*)`, avg: `AVG("${valExpr}")::numeric(18,2)`, min: `MIN("${valExpr}")`, max: `MAX("${valExpr}")`, }[config.aggFn]; return ` SELECT ${rowsExpr}, ${colsExpr}, ${aggExpr} AS value FROM events WHERE created_at BETWEEN $1 AND $2 GROUP BY ${rowsExpr}, ${colsExpr} ORDER BY ${rowsExpr}, ${colsExpr} `; } כדי ליצור .xlsx בצד הלקוח:

import ExcelJS from 'exceljs';
async function exportToExcel(result: PivotResult, config: PivotConfig) {
  const wb = new ExcelJS.Workbook();
  const ws = wb.addWorksheet('Сводная таблица');
  const headers = [...config.rows, ...result.colKeys.map(k => k.join(' / ')), 'Итого'];
  ws.addRow(headers).font = { bold: true };
  result.rowKeys.forEach(rk => {
    const rowKey = rk.join('||');
    const row = [...rk];
    result.colKeys.forEach(ck => {
      row.push(String(result.data.get(rowKey)?.get(ck.join('||')) ?? ''));
    });
    ws.addRow(row);
  });
  const buffer = await wb.xlsx.writeBuffer();
  const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'pivot.xlsx';
  a.click();
}

איך לבחור בין צבירה בצד הלקוח לצבירה בצד השרת?

אם הנתונים שלכם עד 200k שורות ואינם דורשים סינון מורכב, צד הלקוח מהיר וזול יותר. עבור מיליוני שורות ושאילתות אד-הוק, צבירה בצד השרת היא חובה. אנו עוזרים ללקוחות לבחור את הארכיטקטורה האופטימלית. עם ניסיון של 7+ שנים ו-50+ פרויקטי ציר מוצלחים, אנו מבטיחים פתרון מותאם אישית שעונה על צרכי הביצועים שלכם.

שלבי פיתוח ולוח זמנים משוער

שלב משך תיאור
ניתוח נתונים 2–3 ימים לימוד מבנה הנתונים, דרישות למדדים וסינון
עיצוב סכמת צבירה 1–2 ימים הגדרת שדות, מדדים, אינדקסים עבור ClickHouse/PostgreSQL
פיתוח ממשק משתמש 1–2 שבועות יישום קונפיגורטור גרור-ושחרר, טבלה עם סיכומים וכותרת דביקה
צבירה בצד השרת (במידת הצורך) 1–2 שבועות הגדרת ClickHouse, מטמון, יצירת SQL
ייצוא לאקסל/CSV 2–3 ימים אינטגרציה עם exceljs, בדיקת עיצוב
בדיקות עומס 2–3 ימים בדיקה עם 10M שורות, אופטימיזציה של שאילתות N+1

ציר בצד הלקוח עבור עד 50k שורות עם קונפיגורטור גרור-ושחרר וייצוא לאקסל: 2–3 שבועות. מצב שרת עם ClickHouse או PostgreSQL, מטמון תוצאות ותמיכה במספר ערכים: תוספת של 1–2 שבועות. פתרון מפתח כולל כל השלבים עולה מ-$5k ומועבר תוך 4–6 שבועות.

היקף העבודה (כלול)

  • ניתוח נתונים ועיצוב סכמת צבירה
  • פיתוח ממשק משתמש עם גרור-ושחרר (React, TypeScript, Tailwind)
  • צבירה בצד השרת על ClickHouse/PostgreSQL עם מטמון
  • ייצוא לאקסל/CSV, הדפסה
  • בדיקות עומס עד 10M שורות
  • תיעוד והעברת קוד מקור
  • 3 חודשי תמיכה לאחר השקה עם זמן תגובה מובטח

אנו נעריך את הפרויקט שלכם תוך יום אחד. צרו קשר כדי לדון בנתונים שלכם, במדדים ולמצוא את הפתרון הטוב ביותר. קבלו ייעוץ והצעת מחיר מותאמת אישית עוד היום. אנו גם מציעים אחריות שביעות רצון – אם הפתרון לא עומד ביעדי הביצועים המוסכמים, אנו נמשיך לשפר אותו עד שיעמוד בהם.