Proč jsou aplikace pomalé?

Většina performance problémů spadá do tří kategorií:

  1. Špatné datové struktury - O(n) místo O(1) operace
  2. Chybějící cache - Opakované výpočty a DB dotazy
  3. Neoptimalizované DB queries - Chybějící indexy, N+1 problém

V praxi jsme dosáhli zrychlení z 2 hodin na 3 minuty jen správnou kombinací těchto technik.

1. Datové struktury: List vs HashSet vs Dictionary

Problém: O(n) lookup v Listu

Představte si, že máte milion záznamů a pro každý potřebujete ověřit, zda existuje v jiném seznamu:

// ❌ ŠPATNĚ - O(n) pro každý lookup
List<string> existingIds = GetExistingIds(); // 100k záznamů

foreach (var record in records) // 1M záznamů
{
    if (existingIds.Contains(record.Id)) // O(n) = O(100k)
    {
        // zpracování
    }
}
// Celkem: O(n × m) = O(100 miliard operací)

Řešení: HashSet pro O(1) lookup

// ✅ SPRÁVNĚ - O(1) pro každý lookup
HashSet<string> existingIds = new HashSet<string>(GetExistingIds());

foreach (var record in records)
{
    if (existingIds.Contains(record.Id)) // O(1)
    {
        // zpracování
    }
}
// Celkem: O(n) = O(1 milion operací)

Výsledek: Zrychlení až 100 000× pro velké datasety.

Dictionary pro key-value lookup

// ❌ ŠPATNĚ - Opakované hledání v listu
foreach (var order in orders)
{
    var customer = customers.FirstOrDefault(c => c.Id == order.CustomerId);
    // O(n) pro každý order
}

// ✅ SPRÁVNĚ - Dictionary s O(1) lookup
var customerDict = customers.ToDictionary(c => c.Id);
foreach (var order in orders)
{
    var customer = customerDict[order.CustomerId]; // O(1)
}

2. Caching: Nedělejte stejnou práci dvakrát

In-memory cache s Dictionary

private static readonly Dictionary<string, Product> _cache = new();

public Product GetProduct(string id)
{
    if (_cache.TryGetValue(id, out var cached))
        return cached;

    var product = _db.Products.Find(id);
    _cache[id] = product;
    return product;
}

Distribuovaný cache s Redis

public async Task<Product> GetProductAsync(string id)
{
    var cacheKey = $"product:{id}";
    var cached = await _redis.GetAsync<Product>(cacheKey);

    if (cached != null)
        return cached;

    var product = await _db.Products.FindAsync(id);
    await _redis.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
    return product;
}

Kdy cachovat?

  • Často čtená, zřídka měněná data
  • Výpočetně náročné operace
  • Externí API volání

Cache invalidation

"There are only two hard things in Computer Science: cache invalidation and naming things." - Phil Karlton

Strategie:

  • TTL (Time To Live) - Automatická expirace po X minutách
  • Write-through - Invalidace při každém zápisu
  • Event-driven - Invalidace pomocí message queue

3. Databázové optimalizace

Indexy - základní nástroj

-- Bez indexu: Full table scan O(n)
SELECT * FROM Orders WHERE CustomerId = 123;
-- Scan: 1,000,000 rows

-- S indexem: Index seek O(log n)
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
SELECT * FROM Orders WHERE CustomerId = 123;
-- Scan: 3 rows (index seek)

Execution plány

Vždy analyzujte execution plan před optimalizací:

-- SQL Server
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

-- Nebo
EXPLAIN ANALYZE SELECT * FROM Orders WHERE CustomerId = 123;

Hledejte:

  • Table Scan - Chybí index
  • Key Lookup - Zvažte covering index
  • Sort - Můžete změnit index?

N+1 problém

// ❌ N+1 problém - 1 + N dotazů
var orders = db.Orders.ToList(); // 1 dotaz
foreach (var order in orders)
{
    var customer = db.Customers.Find(order.CustomerId); // N dotazů
}

// ✅ Eager loading - 1 dotaz
var orders = db.Orders
    .Include(o => o.Customer)
    .ToList();

Batch processing

// ❌ ŠPATNĚ - 1000 roundtripů
foreach (var item in items)
{
    await db.SaveAsync(item);
}

// ✅ SPRÁVNĚ - 1 roundtrip
db.BulkInsert(items);

Reálný případ: Banka - z 2 hodin na 3 minuty

Systém generování smluv pro banku. Původně trval 2+ hodiny:

  1. Analýza: 95% času v opakovaném vyhledávání v Listu
  2. Fix 1: List → HashSet pro lookup = 10× zrychlení
  3. Fix 2: Dictionary cache pro opakované DB dotazy = 3× zrychlení
  4. Fix 3: Batch insert místo jednotlivých = 2× zrychlení

Výsledek: 2 hodiny → 3 minuty (40× zrychlení)

Checklist pro optimalizaci

  1. Profilujte před optimalizací (měřte, ne hádejte)
  2. Zkontrolujte datové struktury (List → HashSet/Dictionary)
  3. Přidejte cache pro opakované operace
  4. Analyzujte DB execution plány
  5. Použijte batch processing pro velké objemy
  6. Změřte po optimalizaci

Potřebujete pomoc s optimalizací? Ozvěte se nám - máme zkušenosti s 100× zrychlením enterprise systémů.