# Klucze naturalne w CosmosDB: szybsze odczyty i prostsze integracje

# Wprowadzenie

Identyfikatory obiektów (ID) odgrywają kluczową rolę w systemach backendowych i rozproszonych. Zwykle do identyfikacji stosujemy klucze zastępcze (surrogate keys) – generowane losowo (np. GUID) lub sekwencyjnie w bazie danych. Istnieje jednak inne podejście, użycie kluczy naturalnych. W tym przypadku id wyliczane są w przewidywalny sposób na podstawie danych biznesowych.

W artykule pokażę, w jakich sytuacjach zastosowanie deterministycznych identyfikatorów może przyspieszyć odczyty i uprościć integracje w naszych aplikacjach.

# Jak deterministyczne ID upraszczają pracę z CosmosDB

## Korzyści przy odczytach

Jeśli generowanie klucza w bazie danych odbywa się po stronie klienta, największym ryzykiem jest duplikacja. W praktyce często stosujemy `Guid.NewGuid()`, co redukuje prawdopodobieństwo kolizji niemal do zera. W bardziej złożonych scenariuszach zdarza się jednak, że powołujemy osobne serwisy do generowania kluczy.

W przypadku pracy z bezserwerową bazą danych NoSQL **Azure Cosmos DB** sytuacja wygląda inaczej. Pole `id` nie jest globalnie unikalne – musi być niepowtarzalne tylko w obrębie jednej partycji. Prawdziwa tożsamość dokumentu to para `(partitionKey, id)`. Jeśli zaprojektujemy ją w przewidywalny sposób, zyskamy prostsze i szybsze operacje.

Przykładem może być system fakturowy w sklepie, w którym kluczem partycji jest identyfikator klienta, a `id` to ten sam identyfikator rozszerzony o numer zamówienia. Dokument o numerze `C123-2025/09/001` w partycji klienta `C123` ma wtedy tożsamość: `partitionKey = "C123", id = "C123-2025/09/001"`. Wyszukanie faktury dla konkretnego zamówienia sprowadza się wówczas do jednoznacznego odczytu o złożoności **O(1)**. W rozwiązaniu wykorzystującym klucz zastępczy należałoby wykonać zapytanie:

```sql
/*/ Przykładowe query w języku NoSQL używanym w CosmosDB /*/
SELECT TOP 1 c
FROM c
WHERE c.orderNumber = "C123-2025/09/001"
```

Oznacza to przeszukiwanie całej partycji to jest przejście przez nią jeden raz aż do znalezienia wyniku. Złożonośc takiego algorytmu to O(n).

## Korzyści przy zapisie

Zalety stosowania kluczy naturalnych widać także przy zapisie. Wyobraźmy sobie, że faktury przechodzą w aplikacji kilka etapów – od szkicu po wersję finalną.

* Przy korzystaniu z identyfikatorów `Guid.NewGuid()` trzeba najpierw wykonać krok A, czyli wygenerować identyfikator albo odczytać go z bazy, a dopiero potem przekazać dalej.
    
* Dodanie tej samej faktury nie jest wtedy idempotentne. Zabezpieczenie przed duplikatami wymaga dodatkowych odczytów i logiki w kodzie.
    

Stosując deterministyczne identyfikatory możemy od razu uruchamiać równolegle różne procesy korzystające z tego samego klucza. Dzięki temu całość działa szybciej i bardziej niezależnie. Unikamy duplikatów, a kod staje się prostszy.

## Kiedy nie stosować kluczy naturalnych

Nie każde pole biznesowe nadaje się do roli identyfikatora:

* **Adres e-mail użytkownika** **czy nazwa produktu** – to pola mutowalne, które w naturalny sposób ulegają zmianom.
    
* **PESEL i inne dane wrażliwe** – nie powinny być używane jako klucze ani powielane w bazie bez uzasadnienia.
    

Użycie takich pól jako kluczy prowadziłoby do kolizji, problemów migracyjnych oraz ryzyka niezamierzonej ekspozycji danych wrażliwych (np. w logach, adresach URL czy systemach integracyjnych). Klucze naturalne warto więc opierać wyłącznie na polach faktycznie unikalnych, niemutowalnych i neutralnych pod względem bezpieczeństwa, np. numerach faktur, przesyłek czy kodach referencyjnych generowanych w systemie źródłowym.

# Klucze naturalne w bazach relacyjnych (SQL)

W klasycznych bazach relacyjnych (SQL Server, PostgreSQL, MySQL) również spotykamy się z deterministycznymi kluczami, choć częściej mówi się o nich jako o **kluczach naturalnych**. Przykładami mogą być numery faktur, kody ISBN czy numery VIN. Są to pola, które jednoznacznie identyfikują rekord i nie zmieniają się w czasie.

Alternatywą są **klucze techniczne** (np. `INT IDENTITY` albo `GUID`), które są generowane niezależnie od logiki biznesowej. W takim podejściu zwykle dodatkowo zabezpiecza się pola biznesowe przez indeksy `UNIQUE`, aby zapobiec duplikatom.

W SQL wybór między kluczem naturalnym a technicznym jest głównie decyzją modelowania danych i zarządzania unikalnością, podczas gdy w Cosmos DB deterministyczne ID mają bezpośredni wpływ także na wydajność i koszt operacji (O(1) odczyty zamiast pełnych skanów partycji).

# Jak tworzyć deterministyczne ID

## **Konkatenacja** **pól biznesowych**

Najprostsza metoda tworzenia deterministycznych identyfikatorów to konkatenacja pól biznesowych. Identyfikator powstaje z połączenia kilku unikalnych atrybutów, np.:

```csharp
public static string GetKeyOrder(string customerId, string orderNumber)
    => $"{customerId}:{orderNumber}";
```

Takie rozwiązanie jest wyjątkowo proste i czytelne – już po samym ID można rozpoznać, do czego się odnosi. Ma jednak i słabe strony. Długość identyfikatora zależy bezpośrednio od wartości pól, więc nie ma gwarancji stałego formatu i długości. Trzeba też uważać na separatory i sposób łączenia danych, aby uniknąć kolizji lub niejednoznaczności.

## Użycie funkcji skrótu (hash function)

Drugim podejściem jest **wyliczanie hasha z kluczy**. Zamiast przechowywać całe wartości, można je złączyć i przepuścić przez funkcję skrótu (hash function):

```sql
public static string HashFrom(params string[] parts)
{
    using var sha = SHA256.Create();
    var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(string.Join("|", parts)));
    return Convert.ToHexString(bytes);
}
```

Hash ma zawsze stałą długość i nie ujawnia wprost danych biznesowych. Dzięki temu łatwiej go przechowywać, indeksować i przenosić między systemami. Jest to podejście stosowane m.in. w **Git**, gdzie identyfikator commita powstaje właśnie jako hash (`SHA-1` lub `SHA-256`) obliczony z treści commita i metadanych. Dzięki temu commit o tej samej zawartości zawsze ma ten sam identyfikator, a repozytoria mogą łatwo synchronizować dane i wykrywać duplikaty. Z drugiej strony takie ID jest trudniejsze do odczytania przez człowieka, a w teorii istnieje niewielkie ryzyko kolizji.

Przy wyborze algorytmu haszowania warto brać pod uwagę nie tylko kwestie bezpieczeństwa, lecz także skalę danych. W mniejszych zbiorach wystarczą szybsze i prostsze algorytmy, które zapewniają akceptowalnie małe ryzyko kolizji. Przy dużych wolumenach danych lepiej postawić na silniejsze funkcje, takie jak `SHA-256` czy `SHA-512`, które minimalizują ryzyko kolizji kosztem większych nakładów obliczeniowych. Algorytm musimy dobrać świadomie do charakterystyki systemu, zamiast traktować go jako rozwiązanie uniwersalne.

# Podsumowanie

Deterministyczne identyfikatory nie są rozwiązaniem uniwersalnym, ale w odpowiednich scenariuszach potrafią znacząco uprościć backend. W Cosmos DB pozwalają unikać kosztownych skanów, a w systemach rozproszonych wspierają idempotencję i prostsze integracje. Dzięki temu stają się jednym z tych detali architektonicznych, które zwracają się wielokrotnie w trakcie rozwoju systemu.

Dobrze zaprojektowane ID może być różnicą między tanim odczytem a kosztownym skanem całej bazy.

## Tabela wyboru sposobu nadawania kluczy w CosmosDB

| Podejście | Zalety | Wady |
| --- | --- | --- |
| Klucz techniczny (GUID, IDENTITY) | Łatwy do wygenerowania, stabilny w czasie, stała długość | Wymaga zapytań O(n), trudny do czytania |
| Klucz naturalny - prosty | O(1) w CosmosDB, wsparcie indempotencji | Problemy przy mutowalnych polach, zmienna długość kluczy, widoczne dane |
| Klucz naturalny - hash | O(1) w CosmosDB, stała długość, ukrycie danych | Trudny do czytania, koszt obliczeniowy |
