יישמנו סכמת אחסון לתוצאות עיבוד של חנות מקוונת עם 500,000 מוצרים. הדרישות המרכזיות: לא לאבד את היסטוריית השינויים ולשלוף במהירות את הנתונים העדכניים ביותר ללא כפילויות. להלן אנו מציגים פתרון PostgreSQL באמצעות JSONB ולוגיקת upsert שהפחית את זמן שאילתות התכונות ב-60% וביטל כפילויות בסריקות יומיות של 50,000 עמודים. בנוסף, קיצצנו את עלויות האחסון ב-40% והאצנו את טעינת הנתונים ב-70%.
במשך 8 שנים, השלמנו יותר מ-120 פרויקטים בעיבוד נתונים ובאינטגרציה. בעיה אופיינית היא אחסון כאוטי: כפילויות, שאילתות איטיות והיסטוריה חסרה. במאמר זה, אנו מפרקים פתרון מוכח.
בעיות שאנו פותרים
בעיה נפוצה אחת היא כפילויות בסריקות חוזרות: נתונים זהים מוכנסים כשורות חדשות. בעיה נוספת היא שאילתות איטיות על שדות לא מובנים: שאילתות על תכונות מוצר ללא אינדקס לקחו שניות. בעיה שלישית היא היסטוריה חסרה: בעת החלפה, לא נראה מתי מחיר השתנה. הפתרון שלנו מטפל בכל שלוש הבעיות.
איך אנחנו עושים את זה: מקרה בוחן עם קטלוג של 500 אלף מוצרים
תכננו סכמה דו-מפלסית: נתונים גולמיים לניפוי באגים ומוצרים מנורמלים לשאילתות מהירות. המרכיב המרכזי הוא עמודת data מסוג JSONB. היא מאחסנת את כל התכונות הלא סטנדרטיות: צבעים, גדלים, תמונות נוספות. אינדקס GIN על עמודה זו מבטיח ביצועי שאילתות עבור מסננים כמו data->>'color' = 'red' אפילו על מיליוני רשומות.
לעדכונים אנו משתמשים ב-upsert: בעת עיבוד חוזר, אנו מכניסים או מעדכנים את השורה בהתבסס על (site_id, external_id) ייחודי. זה מבטיח ללא כפילויות וחותמות זמן עדכניות.
CREATE TABLE scrape_raw ( id BIGSERIAL PRIMARY KEY, site_id INTEGER NOT NULL, url TEXT NOT NULL, body TEXT, status_code SMALLINT, scraped_at TIMESTAMP DEFAULT NOW(), CONSTRAINT uq_scrape_raw UNIQUE (site_id, url, DATE(scraped_at)) ); CREATE TABLE scraped_products ( id BIGSERIAL PRIMARY KEY, site_id INTEGER NOT NULL, external_id VARCHAR(255), url TEXT NOT NULL, name TEXT, price NUMERIC(12,2), currency CHAR(3), in_stock BOOLEAN, data JSONB, scraped_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), CONSTRAINT uq_scraped_product UNIQUE (site_id, external_id) ); CREATE INDEX idx_scraped_products_site ON scraped_products (site_id); CREATE INDEX idx_scraped_products_data ON scraped_products USING gin(data); שלבים לתכנון סכמת האחסון
- ניתוח תחום. קבע אילו נתונים החנות זקוקה להם: מחירים, מלאי, מאפיינים. זהה שדות חובה לעומת שדות משתנים.
- עיצוב סכמה. שדות נפוצים (מחיר, שם, SKU) נכנסים לעמודות נפרדות. השאר נכנסים לעמודת JSONB
CREATE TABLE scrape_raw ( id BIGSERIAL PRIMARY KEY, site_id INTEGER NOT NULL, url TEXT NOT NULL, body TEXT, status_code SMALLINT, scraped_at TIMESTAMP DEFAULT NOW(), CONSTRAINT uq_scrape_raw UNIQUE (site_id, url, DATE(scraped_at)) ); CREATE TABLE scraped_products ( id BIGSERIAL PRIMARY KEY, site_id INTEGER NOT NULL, external_id VARCHAR(255), url TEXT NOT NULL, name TEXT, price NUMERIC(12,2), currency CHAR(3), in_stock BOOLEAN, data JSONB, scraped_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), CONSTRAINT uq_scraped_product UNIQUE (site_id, external_id) ); CREATE INDEX idx_scraped_products_site ON scraped_products (site_id); CREATE INDEX idx_scraped_products_data ON scraped_products USING gin(data);. זה מספק גמישות מבלי לפגוע בביצועים. - יישם לוגיקת upsert. כתוב INSERT ... ON CONFLICT DO UPDATE. המפתח הייחודי הוא
data. זה מבטיח הסרת כפילויות בכל סריקה. - אינדקסים. אינדקס GIN על
dataלשאילתות מהירות על כל תכונה. B-tree עלsite_idו-external_idלביצועי join. - בדיקות ואופטימיזציה. טען 100,000 רשומות, מדוד זמני INSERT ו-SELECT. כוון ל-<100 אלפיות שנייה בשאילתות טיפוסיות.
- תיעוד והדרכה. העבר את תיאור הסכמה ודוגמאות שאילתות לצוות הלקוח. ערוך סדנה.
למה JSONB במקום טבלה נפרדת?
בעבר, השתמשנו ב-EAV (Entity-Attribute-Value) לאחסון שדות שרירותיים. זה הוביל לשאילתות N+1 ו-joins מורכבים. JSONB עם אינדקס GIN מציע את אותן יכולות אך עם שאילתה אחת, ללא joins, ופחות אחסון. עבור שדות נפוצים (מחיר, שם) אנו שומרים עמודות מנורמלות — זה מפשט סינון ללא אינדקס JSON. גישה זו קיצצה את עלויות האחסון ב-40% בהשוואה ל-EAV.
| גישה | ביצועי שאילתות | גמישות | מורכבות תחזוקה |
|---|---|---|---|
| HTML גולמי | נמוכים | גבוהה | בינונית |
| רלציוני מנורמל | גבוהים לשדות נפוצים | נמוכה (סכמה קבועה) | גבוהה |
| JSONB | גבוהים (עם אינדקס GIN) | גבוהה מאוד | נמוכה |
תיעוד PostgreSQL JSONB מאשר ש-JSONB מהיר פי 2-3 מ-EAV בסינון תכונות.
מידע נוסף על ביצועי JSONB
ההשוואה בוצעה על 500,000 רשומות. JSONB עם אינדקס GIN הראה זמן שאילתה ממוצע של 12 אלפיות שנייה לעומת 45 אלפיות שנייה עבור EAV.איך להימנע מכפילויות בעיבוד חוזר?
השתמש ב-upsert. דוגמה ב-Python:
def save_product(conn, site_id: int, product: dict): conn.execute(""" INSERT INTO scraped_products (site_id, external_id, url, name, price, currency, in_stock, data, scraped_at) VALUES (%(site_id)s, %(external_id)s, %(url)s, %(name)s, %(price)s, %(currency)s, %(in_stock)s, %(data)s::jsonb, NOW()) ON CONFLICT (site_id, external_id) DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price, in_stock = EXCLUDED.in_stock, data = EXCLUDED.data, updated_at = NOW(), scraped_at = NOW() """, {**product, 'site_id': site_id, 'data': json.dumps(product.get('extra', {}))}) גישה זו מבטיחה שורה אחת לכל מוצר, ו-def save_product(conn, site_id: int, product: dict): conn.execute(""" INSERT INTO scraped_products (site_id, external_id, url, name, price, currency, in_stock, data, scraped_at) VALUES (%(site_id)s, %(external_id)s, %(url)s, %(name)s, %(price)s, %(currency)s, %(in_stock)s, %(data)s::jsonb, NOW()) ON CONFLICT (site_id, external_id) DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price, in_stock = EXCLUDED.in_stock, data = EXCLUDED.data, updated_at = NOW(), scraped_at = NOW() """, {**product, 'site_id': site_id, 'data': json.dumps(product.get('extra', {}))}) מספקת היסטוריית עדכונים.
טעויות אופייניות
| טעות | השלכות | פתרון |
|---|---|---|
| חוסר באילוץ ייחודיות | כפילויות בעיבוד חוזר | הוסף updated_at |
| שימוש בשדה טקסט ל-JSON | אין אינדקסים, שאילתות איטיות | השתמש ב-JSONB עם אינדקס GIN |
אין עמודת UNIQUE (site_id, external_id) |
לא ניתן לעקוב אחר עדכניות | הוסף scraped_at |
מה כלול בעבודה
- עיצוב סכמה המותאם לתחום שלך (נתונים גולמיים, מוצרים, קטגוריות).
- יישום לוגיקת upsert למניעת כפילויות.
- הגדרת אינדקסים (GIN, B-tree) לשאילתות מהירות.
- תיעוד המבנה והפעולות.
- הדרכת צוות על עבודה עם JSONB.
- תמיכה למשך שבועיים לאחר המסירה.
במשך 8 שנים, צברנו ניסיון בפתרון משימות דומות: יותר מ-120 פרויקטים, מחנויות קטנות ועד מרקטפלייסים עם מיליוני מוצרים. אנו מבטיחים איכות ואופטימיזציה ל-Core Web Vitals.
לוחות זמנים ויצירת קשר
סכמה בסיסית עם upsert ואינדקסים — 1-2 ימי עבודה. פתרון מלא עם תיעוד והדרכה — עד 5 ימים. צור קשר כדי לקבל הערכה לפרויקט שלך. קבל ייעוץ על עיצוב סכמה לפרויקט שלך. אנו עוזרים לך להימנע מטעויות נפוצות ולהאיץ את הפיתוח.







