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:
/*/ 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.:
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):
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 |



