יישום ייבוא בכמות גדולה: כאשר 150,000 פריטים מפילים את האתר

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

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

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

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

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
יישום ייבוא בכמות גדולה: כאשר 150,000 פריטים מפילים את האתר
בינוני
~5 ימים

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

שאלות נפוצות

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

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

כאשר 150,000 פריטים מפילים את האתר

תארו לעצמכם: לקוח מביא רשימת מחירים עם 150,000 מוצרים. אתם מנסים לטעון הכל בבת אחת — והאתר נתקע, מסד הנתונים מפסיק להגיב. זו בקשה קלאסית לייבוא המוני. במשך 5 שנים, יישמנו יותר מ-80 פרויקטים עם נפחים של עד 500,000 פריטים. צברנו ניסיון במניעת טעינה איטית, שאילתות N+1 וקריסות מסד נתונים. הנה כיצד להשיג יציבות עם מנות (chunks), תורים וטעינה מוקדמת של מילונים.

נפח שיטה זמן עיבוד
עד 1,000 פריטים סינכרוני בתוך הבקשה שניות
1,000 – 50,000 משימת תור אחת עם מנות דקות
50,000 – 500,000 פיצול: N משימות מקבילות 10–60 דקות
מעל 500,000 הכנסה בקבוצות + צינור עיבוד נפרד שעות

אילו בעיות אנחנו פותרים

טעינה איטית: INSERT/UPDATE שורה אחר שורה עבור 100,000 שורות לוקח שעות. שאילתות N+1 בעת פתרון מילונים הורגות את הביצועים. קריסות מסד נתונים עקב טרנזקציות לא אופטימליות. אובדן נתונים בשגיאות באמצע הייבוא. כל הכאבים הללו נפתרים עם ארכיטקטורה נכונה. שירותי הפיתוח שלנו יכולים לפתור בעיות אלה.

כיצד ייבוא המוני משפיע על הביצועים

הגורם המרכזי הוא נפח הנתונים. עבור 1,000 פריטים, עיבוד סינכרוני מספיק; עבור 100,000, נדרש תור אסינכרוני עם מנות.

שיטה זמן עבור 100,000 רשומות עומס על מסד הנתונים
סינכרוני שורה אחר שורה ~30 דקות גבוה (500 שאילתות/שנייה)
אסינכרוני עם מנות + upsert ~5 דקות נמוך (50 שאילתות/שנייה)

Upsert בקבוצות מהיר פי 10 משאילתות שורה אחר שורה — הניסיון שלנו מאשר זאת.

למה מנות + תור הם המפתח ליציבות

קובץ עם 100,000 שורות לא מעובד במשימה אחת. אנחנו מפצלים אותו למנות של 500 שורות, כל מנה כמשימה נפרדת בתור bulk-import. עובדים (2–4) מעבדים במקביל מבלי לגעת בתור הראשי.

class BulkImportDispatcher {
    private const CHUNK_SIZE = 500;

    public function dispatch(ImportFile $file): void {
        $import = ImportRun::create([
            'file_id' => $file->id,
            'status' => 'dispatching',
            'total' => 0,
        ]);

        $chunkIndex = 0;
        foreach ($file->parser()->chunks(self::CHUNK_SIZE) as $chunk) {
            ProcessImportChunkJob::dispatch($import->id, $chunkIndex, $chunk)
                ->onQueue('bulk-import');
            $chunkIndex++;
        }

        $import->update([
            'status' => 'processing',
            'total_chunks' => $chunkIndex,
        ]);
    }
}

יישום טכני: ממנות ועד Upsert

Upsert בקבוצות במקום INSERT/UPDATE שורה אחר שורה

כלי הביצועים המרכזי הוא class BulkImportDispatcher { private const CHUNK_SIZE = 500; public function dispatch(ImportFile $file): void { $import = ImportRun::create([ 'file_id' => $file->id, 'status' => 'dispatching', 'total' => 0, ]); $chunkIndex = 0; foreach ($file->parser()->chunks(self::CHUNK_SIZE) as $chunk) { ProcessImportChunkJob::dispatch($import->id, $chunkIndex, $chunk) ->onQueue('bulk-import'); $chunkIndex++; } $import->update([ 'status' => 'processing', 'total_chunks' => $chunkIndex, ]); } } (upsert). Laravel תומך בזה דרך INSERT ... ON CONFLICT DO UPDATE. פעולת upsert אחת עבור 500 שורות ב-PostgreSQL לוקחת ~50–200 אלפיות השנייה, לעומת 500 × 5 אלפיות השנייה = 2500 אלפיות השנייה עבור שאילתות שורה אחר שורה.

class ProcessImportChunkJob implements ShouldQueue {
    public int $timeout = 120;

    public function handle(): void
    {
        $rows = [];

        foreach ($this->chunk as $item) {
            $rows[] = [
                'sku' => $item['sku'],
                'name' => $item['name'],
                'price' => $item['price'],
                'qty' => $item['qty'],
                'category_id' => $this->resolveCategory($item['category']),
                'source_id' => $this->import->source_id,
                'updated_at' => now(),
                'created_at' => now(),
            ];
        }

        Product::upsert(
            $rows,
            uniqueBy: ['sku'],
            update: ['name', 'price', 'qty', 'category_id', 'updated_at']
        );

        DB::table('import_runs')
            ->where('id', $this->importId)
            ->increment('processed_chunks');
    }
}

טעינה מוקדמת של מילונים לזיכרון

הפעולה היקרה ביותר היא שאילתות מסד נתונים לפתרון תלויות. הפתרון: טעינת כל המילונים לזיכרון לפני העיבוד.

class ImportContext {
    private array $categoryMap;
    private array $supplierMap;
    private array $existingSkus;

    public function preload(int $sourceId): void {
        $this->categoryMap = Category::pluck('id', 'name_normalized')->all();
        $this->supplierMap = Supplier::pluck('id', 'code')->all();
        $this->existingSkus = Product::where('source_id', $sourceId)
            ->pluck('id', 'sku')->all();
    }

    public function resolveCategoryId(string $name): ?int {
        return $this->categoryMap[mb_strtolower(trim($name))] ?? null;
    }

    public function productExists(string $sku): bool {
        return isset($this->existingSkus[$sku]);
    }
}

משימה סופית: צבירת תוצאות

אנחנו משתמשים ב-Model::upsert() — המנגנון המובנה של Laravel לארגון משימות עם קריאת השלמה.

Bus::batch(
    collect($chunks)->map(fn($chunk, $i) => new ProcessImportChunkJob($importId, $i, $chunk))
)->then(function (Batch $batch) use ($importId) {
    ImportRun::find($importId)->update([
        'status' => 'completed',
        'completed_at' => now(),
    ]);
    PostImportPipeline::dispatch($importId);
})->onQueue('bulk-import')->dispatch();

צינור עיבוד לאחר ייבוא

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

class PostImportPipeline {
    public function handle(int $importId): void {
        $productIds = ImportedProduct::where('import_id', $importId)->pluck('product_id');
        Product::whereIn('id', $productIds)->each(function (Product $p) {
            $p->update(['in_stock' => $p->qty > 0]);
        });
        Product::whereIn('id', $productIds)->searchable();
        FilterValueRebuilder::dispatch($productIds);
    }
}

ניטור והגבלת עומס

בממשק הניהול, המפעיל רואה התקדמות בזמן אמת: מספר פריטים שעובדו, שגיאות, זמן נותר. הנתונים נלקחים מטבלת class ProcessImportChunkJob implements ShouldQueue { public int $timeout = 120; public function handle(): void { $rows = []; foreach ($this->chunk as $item) { $rows[] = [ 'sku' => $item['sku'], 'name' => $item['name'], 'price' => $item['price'], 'qty' => $item['qty'], 'category_id' => $this->resolveCategory($item['category']), 'source_id' => $this->import->source_id, 'updated_at' => now(), 'created_at' => now(), ]; } Product::upsert( $rows, uniqueBy: ['sku'], update: ['name', 'price', 'qty', 'category_id', 'updated_at'] ); DB::table('import_runs') ->where('id', $this->importId) ->increment('processed_chunks'); } } . אנחנו מקדישים את התור class ImportContext { private array $categoryMap; private array $supplierMap; private array $existingSkus; public function preload(int $sourceId): void { $this->categoryMap = Category::pluck('id', 'name_normalized')->all(); $this->supplierMap = Supplier::pluck('id', 'code')->all(); $this->existingSkus = Product::where('source_id', $sourceId) ->pluck('id', 'sku')->all(); } public function resolveCategoryId(string $name): ?int { return $this->categoryMap[mb_strtolower(trim($name))] ?? null; } public function productExists(string $sku): bool { return isset($this->existingSkus[$sku]); } } עם 2–4 עובדים, ומשאירים את התור Bus::batch() ללא שינוי. ייבוא כבד רץ בלילה דרך מתזמן המשימות. כל שגיאה מתועדת, והמפעיל יכול להפעיל מחדש רק מנות שנכשלו — מקרה טיפוסי לאתרי מסחר אלקטרוני עם קטלוגים גדולים. אנחנו תומכים גם באינטגרציה עם 1C ו-CommerceML, מקורות נתונים פופולריים במסחר אלקטרוני רוסי. צרו קשר לייעוץ — אנחנו יכולים להגדיר מנגנון זה עבור הפרויקט שלכם.

טעויות נפוצות בתכנון ייבוא
  1. גודל מנה שגוי: קטן מדי (הרבה משימות, תקורה של תור) או גדול מדי (פסק זמן, עומס זיכרון). הגודל האופטימלי הוא 500–1000 רשומות.
  2. חוסר באינדקסים על שדות ייחודיים (SKU, מק"ט) — upsert מואט לסריקת טבלה מלאה. בדקו אינדקסים לפני ההרצה.
  3. התעלמות ממצבי deadlock בכתיבה מקבילה לאותה טבלה — השתמשו בנעילות שורות או בעיבוד רציף בתוך מחיצה.
  4. שגיאות פענוח לא מטופלות — תמיד אמתו ודלגו על שורות לא תקינות עם תיעוד.

היקף העבודה ולוח זמנים

  • פיתוח מפצל מנות ו-upsert בקבוצות.
  • טעינה מוקדמת של מילונים ומשימה סופית.
  • דוח תוצאות (מספר פריטים שעובדו, שגיאות).
  • תיעוד להתקנה והרצה.
  • הדרכת מפעילים.
  • אחריות לפעילות יציבה — 3 חודשי תמיכה.
  • עבודה על ידי מהנדסי Laravel מוסמכים.

לוח זמנים: מ-3 עד 5 ימים במפתח פתוח. קבלו ייעוץ לפרויקט שלכם. צרו קשר להערכה.

ייבוא נתונים — ויקיפדיה