# Backend Performance-Analyse: Sell-Funktionen ## Identifizierte Performance-Probleme ### 1. **N+1 Query Problem in `sellAllProducts()`** **Problem:** Die Funktion `sellAllProducts()` macht für jedes Inventory-Item mehrere separate Datenbankabfragen: 1. **Erste Schleife (Zeile 1702-1711):** - `calcRegionalSellPrice()` → macht `TownProductWorth.findOne()` für jedes Item - `getCumulativeTaxPercentWithExemptions()` → macht mehrere Queries pro Item: - `FalukantCharacter.findOne()` - `PoliticalOffice.findAll()` mit Includes - Rekursive SQL-Query für Steuerberechnung - `addSellItem()` → macht `Branch.findOne()` und `DaySell.findOne()`/`create()` für jedes Item 2. **Zweite Schleife (Zeile 1714-1724):** - `RegionData.findOne()` für jedes Item - `getCumulativeTaxPercent()` → rekursive SQL-Query für jedes Item - `calcRegionalSellPrice()` → erneut `TownProductWorth.findOne()` für jedes Item **Beispiel:** Bei 10 Items werden gemacht: - 10x `TownProductWorth.findOne()` (2x pro Item) - 10x `RegionData.findOne()` - 10x `getCumulativeTaxPercentWithExemptions()` (mit mehreren Queries) - 10x `getCumulativeTaxPercent()` (rekursive SQL) - 10x `addSellItem()` (mit 2 Queries pro Item) - = **~70+ Datenbankabfragen für 10 Items** ### 2. **Ineffiziente `addSellItem()` Implementierung** **Problem:** - Wird für jedes Item einzeln aufgerufen - Macht `Branch.findOne()` für jedes Item (könnte gecacht werden) - `DaySell.findOne()` und `create()`/`update()` für jedes Item **Lösung:** Batch-Operation implementieren, die alle DaySell Einträge auf einmal verarbeitet. ### 3. **Doppelte Berechnungen in `sellAllProducts()`** **Problem:** - Preis wird zweimal berechnet (Zeile 1705 und 1718) - Steuer wird zweimal berechnet (Zeile 1706 und 1717) - `calcRegionalSellPrice()` wird zweimal aufgerufen mit denselben Parametern ### 4. **Fehlende Indizes** **Potenzielle fehlende Indizes:** - `falukant_data.town_product_worth(product_id, region_id)` - sollte unique sein - `falukant_data.inventory(stock_id, product_id, quality)` - für schnelle Lookups - `falukant_data.knowledge(character_id, product_id)` - für Knowledge-Lookups - `falukant_data.political_office(character_id)` - für Steuerbefreiungen ### 5. **Ineffiziente `getCumulativeTaxPercentWithExemptions()`** **Problem:** - Lädt alle PoliticalOffices jedes Mal neu, auch wenn sich nichts geändert hat - Macht komplexe rekursive SQL-Query für jedes Item separat - Könnte gecacht werden (z.B. pro User+Region Kombination) ## Empfohlene Optimierungen ### 1. **Batch-Loading für `sellAllProducts()`** ```javascript async sellAllProducts(hashedUserId, branchId) { // ... existing code ... // Batch-Load alle benötigten Daten VOR den Schleifen const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))]; const productIds = [...new Set(inventory.map(item => item.productType.id))]; // 1. Lade alle TownProductWorth Einträge auf einmal const townWorths = await TownProductWorth.findAll({ where: { productId: { [Op.in]: productIds }, regionId: { [Op.in]: regionIds } } }); const worthMap = new Map(); townWorths.forEach(tw => { worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent); }); // 2. Lade alle RegionData auf einmal const regions = await RegionData.findAll({ where: { id: { [Op.in]: regionIds } } }); const regionMap = new Map(regions.map(r => [r.id, r])); // 3. Berechne Steuern für alle Regionen auf einmal const taxMap = new Map(); for (const regionId of regionIds) { const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId); taxMap.set(regionId, tax); } // 4. Berechne Preise und Steuern in einer Schleife const sellItems = []; for (const item of inventory) { const regionId = item.stock.branch.regionId; const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50; const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0; const pricePerUnit = calcRegionalSellPrice(item.productType, knowledgeVal, regionId, worthPercent); const cumulativeTax = taxMap.get(regionId); // ... rest of calculation ... sellItems.push({ branchId: item.stock.branch.id, productId: item.productType.id, quantity: item.quantity }); } // 5. Batch-Update DaySell Einträge await this.addSellItemsBatch(sellItems); // ... rest of code ... } ``` ### 2. **Batch-Operation für `addSellItem()`** ```javascript async addSellItemsBatch(sellItems) { // Gruppiere nach (regionId, productId, sellerId) const grouped = new Map(); for (const item of sellItems) { const branch = await Branch.findByPk(item.branchId); if (!branch) continue; const key = `${branch.regionId}-${item.productId}-${item.sellerId}`; if (!grouped.has(key)) { grouped.set(key, { regionId: branch.regionId, productId: item.productId, sellerId: item.sellerId, quantity: 0 }); } grouped.get(key).quantity += item.quantity; } // Batch-Update oder Create for (const [key, data] of grouped) { const [daySell, created] = await DaySell.findOrCreate({ where: { regionId: data.regionId, productId: data.productId, sellerId: data.sellerId }, defaults: { quantity: data.quantity } }); if (!created) { daySell.quantity += data.quantity; await daySell.save(); } } } ``` ### 3. **Caching für `getCumulativeTaxPercentWithExemptions()`** ```javascript // Cache für Steuerberechnungen (z.B. 5 Minuten) const taxCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten async function getCumulativeTaxPercentWithExemptions(userId, regionId) { const cacheKey = `${userId}-${regionId}`; const cached = taxCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.value; } // ... existing calculation ... taxCache.set(cacheKey, { value: tax, timestamp: Date.now() }); return tax; } ``` ### 4. **Optimierte `calcRegionalSellPrice()`** ```javascript async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) { // Wenn worthPercent nicht übergeben wurde UND wir es nicht aus dem Cache haben, // dann hole es aus der DB if (worthPercent === null) { const townWorth = await TownProductWorth.findOne({ where: { productId: product.id, regionId: regionId } }); worthPercent = townWorth?.worthPercent || 50; } // ... rest of calculation ... } ``` ### 5. **Datenbank-Indizes hinzufügen** ```sql -- Index für town_product_worth (sollte unique sein) CREATE UNIQUE INDEX IF NOT EXISTS idx_town_product_worth_product_region ON falukant_data.town_product_worth(product_id, region_id); -- Index für inventory Lookups CREATE INDEX IF NOT EXISTS idx_inventory_stock_product_quality ON falukant_data.inventory(stock_id, product_id, quality); -- Index für knowledge Lookups CREATE INDEX IF NOT EXISTS idx_knowledge_character_product ON falukant_data.knowledge(character_id, product_id); -- Index für political_office Lookups CREATE INDEX IF NOT EXISTS idx_political_office_character ON falukant_data.political_office(character_id); ``` ## Geschätzter Performance-Gewinn - **Vorher:** ~70+ Queries für 10 Items - **Nachher:** ~15-20 Queries für 10 Items (Batch-Loading + Caching) - **Geschätzte Verbesserung:** 70-80% weniger Datenbankabfragen ## Priorität 1. **Hoch:** Batch-Loading für `sellAllProducts()` (größter Impact) 2. **Hoch:** Batch-Operation für `addSellItem()` 3. **Mittel:** Caching für Steuerberechnungen 4. **Mittel:** Datenbank-Indizes 5. **Niedrig:** Doppelte Berechnungen entfernen