Migracja danych z legacy systemu - najtrudniejszy etap transformacji, który decyduje o wszystkim
Nowy system jest gotowy. Phoenix LiveView, PostgreSQL, piękny interfejs, testy automatyczne, CI/CD. Wszystko działa idealnie. Na danych testowych.
Teraz trzeba przenieść 15 lat prawdziwych danych ze starego systemu. 500 000 klientów, 2 miliony zamówień, 8 milionów pozycji faktur, 200 000 produktów. Dane, które żyją w bazie Access, w tabelach SQL Server bez relacji, w Excelach na dysku sieciowym i w głowie Tomka.
To jest moment, w którym 60% projektów migracyjnych wpada w poważne problemy. Nie dlatego, że nowy system jest zły. Dlatego, że dane ze starego systemu nie pasują do nowego - i nikt tego nie sprawdził wcześniej.
Dlaczego migracja danych jest najtrudniejsza
Dane legacy to nie dane - to archeologia
Stary system rósł organicznie przez 15 lat. Każdy programista, każda zmiana, każde "tymczasowe rozwiązanie" zostawiło ślad w strukturze danych:
Kolumna status z 14 wartościami, z których 3 to "legacy z czasów starego systemu" i nikt nie wie, co oznaczają. status = 7 - co to jest? Tomek mówi: "to było używane przez moduł, który usunęliśmy w 2018, ale rekordy zostały".
Pole uwagi zamiast kolumny. Numer konta bankowego klienta? W polu uwagi. Preferowany kurier? W polu uwagi. Informacja o alergii na gluten? Też w polu uwagi. 15 lat danych niestrukturalnych wciśniętych w jedno pole tekstowe.
Duplikaty wszędzie. Klient "Jan Kowalski" istnieje 4 razy: "Jan Kowalski", "jan kowalski", "KOWALSKI JAN", "Kowalski Jan (stary)". Każdy z innym adresem. Który jest aktualny?
Brak integralności referencyjnej. Zamówienie odwołuje się do klienta o ID 4523. Klient 4523 nie istnieje w tabeli klientów. Został usunięty ręcznie z bazy w 2019 roku, ale zamówienia zostały.
Dane w wielu miejscach. Adres klienta jest w tabeli klienci, ale też w każdym zamówieniu (skopiowany w momencie składania). Który jest prawidłowy? Ten z tabeli klientów (aktualizowany) czy ten z zamówienia (historyczny)?
Typy danych jako string. Cena: "120,50 zł". Data: "15-sty-2023". NIP: "522-123-45-67" albo "5221234567" albo "PL5221234567" albo puste pole z komentarzem "sprawdzić".
Dane to serce firmy
Możesz wymienić system. Możesz wymienić serwer. Możesz wymienić zespół programistów. Ale nie możesz stracić danych. 15 lat historii zamówień, relacji z klientami, cen, faktur - to jest pamięć firmy. Utrata nawet fragmentu tych danych może oznaczać:
- Faktury, których nie możesz odtworzyć (wymóg prawny: 5 lat przechowywania)
- Klienci, których kontakty giną (utracona sprzedaż)
- Historia zamówień, której nie da się odtworzyć (analityka, reklamacje)
- Stany magazynowe, które się nie zgadzają (chaos operacyjny)
Fazy migracji danych
Faza 1: Audit - co mamy
Zanim ruszymy jakiekolwiek dane, musimy zrozumieć co mamy. To jest faza detektywistyczna - otwieramy starą bazę i dokumentujemy:
Inwentaryzacja tabel i plików.
Baza SQL Server "ERP_PROD":
- dbo.Klienci (487 231 wierszy, ostatnia zmiana: wczoraj)
- dbo.Klienci_old (23 445 wierszy, ostatnia zmiana: 2019)
- dbo.Zamowienia (2 134 567 wierszy)
- dbo.Zamowienia_arch (890 234 wierszy, "archiwum z 2020")
- dbo.tmp_import_2021 (15 000 wierszy, "tymczasowa, nie usuwać!")
- dbo.Tomek_test (3 wiersze)
Pliki na dysku sieciowym:
- \\server\dane\cenniki\cennik_aktualny_v3_FINAL.xlsx
- \\server\dane\cenniki\cennik_2024_Krysia.xlsx
- \\server\dane\klienci_eksport_styczen.csv
Pliki w systemie:
- C:\ERP\config\grupy_rabatowe.xml
- C:\ERP\data\szablony_faktur\*.dotProfiling danych - automatyczna analiza jakości:
-- Ile klientów nie ma emaila?
SELECT COUNT(*) FROM Klienci WHERE email IS NULL OR email = '';
-- Wynik: 34 567 (7.1%)
-- Ile unikatowych formatów NIP?
SELECT DISTINCT
CASE
WHEN nip LIKE '___-___-__-__' THEN 'XXX-XXX-XX-XX'
WHEN nip LIKE '__________' THEN 'XXXXXXXXXX'
WHEN nip LIKE 'PL%' THEN 'PL...'
WHEN nip IS NULL OR nip = '' THEN 'BRAK'
ELSE 'INNY: ' + nip
END AS format,
COUNT(*) as ilosc
FROM Klienci
GROUP BY ...
-- Wynik:
-- XXX-XXX-XX-XX: 234 567
-- XXXXXXXXXX: 89 432
-- PL...: 12 345
-- BRAK: 145 678
-- INNY: 5 209 (np. "do sprawdzenia", "xxx", "00000")Mapa zależności - które tabele odwołują się do których:
Klienci ←── Zamowienia ←── Pozycje_zamowien ──→ Produkty
↑ ↑ ↑
│ │ │
└── Faktury ──┘──── Pozycje_faktur ──────────────┘
↑
Cennik (który cennik?)Wynik fazy 1: dokument z pełną mapą danych, statystykami jakości i listą problemów do rozwiązania. Typowy czas: 1-2 tygodnie.
Faza 2: Planowanie - co z tym zrobimy
Na podstawie auditu podejmujemy decyzje:
Co migrujemy, co zostawiamy?
Nie wszystko musi być przeniesione. Tabela Tomek_test z 3 wierszami? Nie. Archiwum zamówień sprzed 10 lat? Zależy od wymogu prawnego - jeśli faktury muszą być przechowywane 5 lat, zamówienia starsze niż 5 lat można zarchiwizować osobno (CSV + backup), nie migrować do nowego systemu.
Zasada: migruj dane aktywne (używane na co dzień) i dane wymagane prawnie (faktury, dokumenty księgowe). Resztę archiwizuj.
Jak mapujemy stary schemat na nowy?
Stary system ma tabelę Klienci z 47 kolumnami, z których 12 jest nieużywanych od lat. Nowy system ma moduł Accounts.Customer z 15 polami. Mapowanie:
STARY NOWY
Klienci.Nazwa → customers.company_name
Klienci.Imie → customers.contact_first_name
Klienci.Nazwisko → customers.contact_last_name
Klienci.NIP → customers.tax_id (po normalizacji)
Klienci.Tel1 → customers.phone (po czyszczeniu)
Klienci.Tel2 → (pominięte - 98% pustych)
Klienci.Fax → (pominięte - to jest 2026)
Klienci.Uwagi → (parsowane - wyciągnąć nr konta, kuriera)
Klienci.Status → customers.active (mapowanie 14 wartości → boolean)
Klienci.KodRabatowy → customers.discount_group_id (nowa tabela)
Klienci.DataUtworzenia → customers.inserted_atJak czyścimy dane?
Dla każdego problemu z fazy 1 - strategia:
| Problem | Strategia | Automatyzacja |
|---|---|---|
| Duplikaty klientów | Deduplikacja po NIP + fuzzy matching na nazwę | 80% auto, 20% ręcznie |
| NIP w różnych formatach | Normalizacja do 10 cyfr + walidacja checksum | 100% auto |
| Brak emaila (7%) | Uzupełnij z pliku Excela Krysi lub zostaw NULL | 50% auto |
| Pole "uwagi" z danymi | Regex extracting (nr konta, kurier, itp.) | 70% auto |
| Osierocone zamówienia | Przypisz do klienta "Usunięty" lub pomiń | 100% auto |
| Ceny jako string | Parse + konwersja na Decimal | 100% auto |
| Status 14 wartości | Tabela mapowania → boolean active | 100% auto |
Faza 3: ETL - Extract, Transform, Load
ETL to trzy kroki przenoszenia danych:
Extract (wyciągnij) - pobierz dane ze starego systemu. SQL dump, CSV export, API call, czytanie plików Excel.
Transform (przekształć) - oczyść, znormalizuj, zmapuj na nowy schemat. To jest najtrudniejszy krok.
Load (załaduj) - wstaw przetworzone dane do nowej bazy PostgreSQL.
W Elixirze budujemy pipeline ETL jako osobną aplikację:
defmodule Migration.Pipeline do
def run do
# Extract
legacy_customers = Migration.Extract.customers_from_sql_server()
legacy_orders = Migration.Extract.orders_from_sql_server()
excel_data = Migration.Extract.customers_from_excel("cennik_Krysia.xlsx")
# Transform
customers =
legacy_customers
|> Migration.Transform.deduplicate_by_nip()
|> Migration.Transform.normalize_nip()
|> Migration.Transform.normalize_phone()
|> Migration.Transform.extract_from_uwagi()
|> Migration.Transform.merge_excel_data(excel_data)
|> Migration.Transform.map_status_to_active()
|> Migration.Transform.validate_all()
# Load
Migration.Load.insert_customers(customers)
# Walidacja po załadowaniu
Migration.Validate.customer_count_matches()
Migration.Validate.no_orphaned_orders()
Migration.Validate.all_nips_valid()
end
endKażdy krok jest:
- Testowalny - testy jednostkowe na każdą transformację
- Powtarzalny - uruchamiamy pipeline wielokrotnie na danych testowych
- Logowany - wiemy dokładnie co się stało z każdym rekordem
- Odwracalny - możemy wyczyścić nową bazę i uruchomić ponownie
Faza 4: Walidacja - czy dane się zgadzają
Najważniejsza faza, którą większość projektów pomija. Po załadowaniu danych sprawdzamy czy wszystko się zgadza:
Zliczanie rekordów:
-- Stary system
SELECT COUNT(*) FROM Klienci WHERE Status != 'USUNIETY';
-- Wynik: 341 786
-- Nowy system
SELECT COUNT(*) FROM customers WHERE active = true;
-- Wynik: 341 786 ✓ ZGADZA SIĘ
-- Ale też:
SELECT COUNT(*) FROM customers;
-- Wynik: 341 902 (116 więcej - to klienci z deduplikacji, których
-- połączyliśmy, ale zachowaliśmy z active=false)Sumy kontrolne na kwotach:
-- Stary system: suma wszystkich faktur z 2025
SELECT SUM(CAST(REPLACE(REPLACE(Kwota, ' ', ''), ',', '.') AS DECIMAL(18,2)))
FROM Faktury WHERE YEAR(Data) = 2025;
-- Wynik: 12 456 789.45
-- Nowy system
SELECT SUM(total) FROM invoices WHERE date >= '2025-01-01' AND date < '2026-01-01';
-- Wynik: 12 456 789.45 ✓ CO DO GROSZALosowa weryfikacja:
Wylosuj 100 klientów ze starego systemu.
Dla każdego sprawdź w nowym:
- Czy dane osobowe się zgadzają?
- Czy zamówienia się zgadzają?
- Czy faktury się zgadzają?
- Czy stany rozrachunków się zgadzają?
Wynik: 97/100 OK, 3 rozbieżności (do ręcznego sprawdzenia)Walidacja logiki biznesowej:
# Każde zamówienie musi mieć klienta
assert Repo.aggregate(
from(o in Order, where: is_nil(o.customer_id)), :count
) == 0
# Każda faktura musi mieć poprawny NIP
invalid_nips = Repo.all(
from(i in Invoice, where: not fragment("? ~ '^[0-9]{10}$'", i.tax_id))
)
assert invalid_nips == []
# Suma pozycji zamówienia = wartość zamówienia
mismatched = Repo.all(
from o in Order,
join: items in assoc(o, :items),
group_by: o.id,
having: sum(items.price * items.quantity) != o.total,
select: o.id
)
assert mismatched == []Faza 5: Migracja produkcyjna
Po wielokrotnym przetestowaniu pipeline'a na kopiach danych - czas na migrację produkcyjną:
Strategia 1: Big bang (nie rekomendowana)
- Piątek wieczór: zatrzymaj stary system
- Noc: uruchom migrację (4-8 godzin)
- Sobota rano: walidacja
- Poniedziałek: start na nowym systemie
Ryzyko: jeśli coś pójdzie nie tak w nocy, poniedziałek jest katastrofą.
Strategia 2: Przyrostowa (rekomendowana)
- Dane historyczne (zamówienia, faktury) migrowane z wyprzedzeniem
- Codziennie: synchronizacja nowych danych delta
- Dzień D: finalna synchronizacja (ostatnie zmiany), przełączenie
- Czas niedostępności: minuty, nie godziny
Tydzień -4: Migracja danych historycznych (2020-2024)
Tydzień -3: Walidacja, poprawki, druga migracja historycznych
Tydzień -2: Migracja danych bieżących (2025-2026)
Tydzień -1: Delta sync codziennie + walidacja
Dzień D: Ostatnia delta sync + przełączenie
Czas niedostępności: 30-60 minut
Dzień D+1: Weryfikacja w nowym systemie
Dzień D+7: Stary system read-only (backup)
Dzień D+30: Stary system wyłączonyStrategia 3: Równoległa (najlepsza, ale najdroższa)
- Oba systemy działają jednocześnie
- Dane synchronizowane w obie strony (Foreign Data Wrappers)
- Użytkownicy stopniowo przechodzą na nowy system
- Stary system wyłączany, gdy nikt go nie używa
Zero przestoju. Zero ryzyka. Ale wymaga synchronizacji dwukierunkowej, co jest technicznie złożone.
Typowe problemy i jak je rozwiązujemy
Duplikaty klientów
Najczęstszy problem. Jeden klient ma 2-5 rekordów w starej bazie.
Automatyczna deduplikacja:
defmodule Migration.Transform.Dedup do
def deduplicate_by_nip(customers) do
customers
|> Enum.group_by(&normalize_nip(&1.nip))
|> Enum.map(fn {_nip, group} ->
case group do
[single] -> single
multiple -> merge_duplicates(multiple)
end
end)
end
defp merge_duplicates(duplicates) do
# Wybierz rekord z najnowszą datą modyfikacji jako "master"
master = Enum.max_by(duplicates, & &1.updated_at)
# Uzupełnij brakujące pola z pozostałych rekordów
Enum.reduce(duplicates, master, fn dup, acc ->
%{acc |
email: acc.email || dup.email,
phone: acc.phone || dup.phone,
address: acc.address || dup.address
}
end)
end
end80% duplikatów jest rozwiązywanych automatycznie (ten sam NIP = ten sam klient). Pozostałe 20% (różne NIP-y, ale ta sama firma - zmiana NIP, przejęcie, przekształcenie) wymaga ręcznej decyzji.
Tworzymy interfejs w LiveView, w którym operator przegląda podejrzane duplikaty i decyduje: "połącz", "zostaw osobno", "usuń":
┌─────────────────────────────────────────────────┐
│ Podejrzany duplikat #47/234 │
├─────────────────────┬───────────────────────────┤
│ Rekord A │ Rekord B │
│ Jan Kowalski Sp.z.o │ JK KOWALSKI SP ZOO │
│ NIP: 5221234567 │ NIP: 5221234567 │
│ ul. Marszałkowska 1 │ Marszałkowska 1/2 │
│ tel: 601-234-567 │ tel: (brak) │
│ email: jan@firma.pl │ email: kowalski@gmail.com │
│ Zamówienia: 47 │ Zamówienia: 3 │
│ Ostatnie: 2026-01 │ Ostatnie: 2024-03 │
├─────────────────────┴───────────────────────────┤
│ [Połącz → A] [Połącz → B] [Zostaw osobno] │
└─────────────────────────────────────────────────┘Dane w polu "uwagi"
Lata wciskania danych w jedno pole tekstowe:
"Konto bankowe: 12 1234 5678 9012 3456 7890 1234
Preferowany kurier: DPD
UWAGA: nie dzwonić przed 10:00
Rabat 5% uzgodniony z Markiem - faktura za netto
adres dostawy inny: Krucza 15/3, Warszawa"Pipeline ekstrakcji:
defmodule Migration.Transform.ParseUwagi do
def extract(uwagi) when is_binary(uwagi) do
%{
bank_account: extract_bank_account(uwagi),
preferred_courier: extract_courier(uwagi),
delivery_notes: extract_notes(uwagi),
discount_info: extract_discount(uwagi),
alt_address: extract_address(uwagi),
remaining: clean_remaining(uwagi) # to, czego nie sparsowaliśmy
}
end
defp extract_bank_account(text) do
case Regex.run(~r/(?:konto|rachunek|bank)[:\s]*(\d{2}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4})/i, text) do
[_, account] -> String.replace(account, ~r/[\s-]/, "")
nil -> nil
end
end
defp extract_courier(text) do
cond do
text =~ ~r/dpd/i -> "DPD"
text =~ ~r/inpost/i -> "InPost"
text =~ ~r/dhl/i -> "DHL"
text =~ ~r/ups/i -> "UPS"
true -> nil
end
end
endLogujemy każdą ekstrakcję z confidence score. To, czego nie sparsowaliśmy automatycznie (remaining), trafia do kolejki ręcznej weryfikacji.
Daty i formaty liczbowe
Stary system polskojęzyczny z datami w formacie "15-sty-2023", cenami "1 234,56 zł", wagami "2.5 kg":
defmodule Migration.Transform.Formats do
@polish_months %{
"sty" => 1, "lut" => 2, "mar" => 3, "kwi" => 4,
"maj" => 5, "cze" => 6, "lip" => 7, "sie" => 8,
"wrz" => 9, "paź" => 10, "lis" => 11, "gru" => 12
}
def parse_polish_date(str) do
case Regex.run(~r/(\d{1,2})-(\w{3})-(\d{4})/, str) do
[_, day, month, year] ->
month_num = @polish_months[String.downcase(month)]
Date.new!(String.to_integer(year), month_num, String.to_integer(day))
nil ->
# Spróbuj inne formaty
parse_other_formats(str)
end
end
def parse_polish_decimal(str) do
str
|> String.replace(~r/[^\d,.-]/, "") # usuń "zł", "PLN", spacje
|> String.replace(",", ".") # polska notacja → angielska
|> Decimal.new()
end
endTesty na każdy format:
test "parsuje polskie daty" do
assert parse_polish_date("15-sty-2023") == ~D[2023-01-15]
assert parse_polish_date("1-paź-2024") == ~D[2024-10-01]
assert parse_polish_date("31-gru-2025") == ~D[2025-12-31]
end
test "parsuje polskie kwoty" do
assert parse_polish_decimal("1 234,56 zł") == Decimal.new("1234.56")
assert parse_polish_decimal("100,00") == Decimal.new("100.00")
assert parse_polish_decimal("1234.56 PLN") == Decimal.new("1234.56")
endHasła użytkowników
Stary system przechowuje hasła w MD5 albo plain text. Nie możesz ich przenieść jako Argon2, bo nie znasz haseł w postaci jawnej (chyba że plain text - wtedy znasz, ale nie powinieneś).
Strategia:
defmodule Migration.Transform.Passwords do
def migrate_password(user) do
case user.password_format do
:plain_text ->
# Mamy hasło w jawnej postaci - zahashuj Argon2
%{user | password_hash: Argon2.hash_pwd_salt(user.password)}
:md5 ->
# Nie znamy hasła. Przenieś hash MD5.
# Przy pierwszym logowaniu: sprawdź MD5, zahashuj Argon2
%{user | legacy_hash: user.password_hash, password_hash: nil}
:unknown ->
# Wymuś reset hasła
%{user | password_hash: nil, force_reset: true}
end
end
endPrzy logowaniu w nowym systemie:
def authenticate(email, password) do
user = Repo.get_by(User, email: email)
cond do
# Nowy hash Argon2 - normalne logowanie
user.password_hash && Argon2.verify_pass(password, user.password_hash) ->
{:ok, user}
# Legacy hash MD5 - zweryfikuj i przekonwertuj
user.legacy_hash && md5(password) == user.legacy_hash ->
# Sukces! Przekonwertuj na Argon2
user
|> User.changeset(%{password_hash: Argon2.hash_pwd_salt(password), legacy_hash: nil})
|> Repo.update!()
{:ok, user}
# Wymuszone resetowanie hasła
user.force_reset ->
{:error, :password_reset_required}
true ->
{:error, :invalid_credentials}
end
endUżytkownicy logują się starym hasłem. System w tle konwertuje na Argon2. Po miesiącu większość haseł jest w bezpiecznym formacie. Ci, którzy się nie logowali, dostaną email z resetem.
Narzędzia, których używamy
Foreign Data Wrappers - czytanie starej bazy bez kopiowania
PostgreSQL FDW pozwala odpytywać starą bazę (SQL Server, MySQL, Oracle) bezpośrednio z nowego PostgreSQL:
CREATE EXTENSION tds_fdw; -- dla SQL Server
CREATE SERVER legacy_server
FOREIGN DATA WRAPPER tds_fdw
OPTIONS (servername 'stary-serwer.firma.pl', database 'ERP_PROD');
CREATE USER MAPPING FOR CURRENT_USER
SERVER legacy_server
OPTIONS (username 'readonly', password '...');
IMPORT FOREIGN SCHEMA dbo
LIMIT TO (Klienci, Zamowienia, Faktury)
FROM SERVER legacy_server
INTO legacy;
-- Teraz mogę zapytać starą bazę z PostgreSQL
SELECT * FROM legacy.Klienci WHERE NIP = '5221234567';Idealne do:
- Porównywania danych po migracji
- Przyrostowej synchronizacji (delta)
- Debugowania rozbieżności
Ecto Multi - transakcyjna migracja
Każda paczka danych migrowana w jednej transakcji:
def migrate_customer_batch(legacy_customers) do
Ecto.Multi.new()
|> Ecto.Multi.insert_all(:customers, Customer, transform_batch(legacy_customers))
|> Ecto.Multi.run(:validate, fn repo, %{customers: {count, _}} ->
if count == length(legacy_customers), do: {:ok, count}, else: {:error, :count_mismatch}
end)
|> Repo.transaction()
endAlbo cała paczka się wstawia, albo żadna. Nie ma sytuacji "połowa klientów zmigrowana, połowa nie".
Raport migracji
Po każdym uruchomieniu pipeline'a generujemy raport:
╔══════════════════════════════════════════════════╗
║ RAPORT MIGRACJI - 2026-02-26 14:30 ║
╠══════════════════════════════════════════════════╣
║ ║
║ Klienci: ║
║ Źródło: 487 231 ║
║ Zmigrowane: 341 786 (aktywni) ║
║ Duplikaty: 12 345 (połączone) ║
║ Pominięte: 133 100 (usunięci w legacy) ║
║ Błędy: 0 ║
║ ║
║ Zamówienia: ║
║ Źródło: 2 134 567 ║
║ Zmigrowane: 2 134 234 ║
║ Pominięte: 333 (brak klienta) ║
║ Błędy: 0 ║
║ ║
║ Faktury: ║
║ Źródło: 1 567 890 ║
║ Zmigrowane: 1 567 890 ║
║ Suma netto: ✓ ZGODNA (234 567 890.12 PLN) ║
║ Błędy: 0 ║
║ ║
║ Czas wykonania: 47 minut ║
║ Status: SUKCES ║
╚══════════════════════════════════════════════════╝Ile to trwa i ile kosztuje
Typowy harmonogram
| Faza | Czas | Co się dzieje |
|---|---|---|
| Audit danych | 1-2 tygodnie | Analiza jakości, mapa zależności |
| Planowanie migracji | 1 tydzień | Mapowanie, strategia czyszczenia |
| Budowa pipeline ETL | 2-4 tygodnie | Kod, testy, automatyzacja |
| Testy na kopii danych | 1-2 tygodnie | 3-5 uruchomień, poprawki |
| Interfejs do ręcznej weryfikacji | 1 tydzień | Duplikaty, edge case'y |
| Ręczna weryfikacja | 1-2 tygodnie | Operator przegląda wątpliwe dane |
| Migracja produkcyjna | 1-2 dni | Finalna migracja + walidacja |
| Łącznie | 8-14 tygodni |
Typowy koszt
Migracja danych to zwykle 20-30% budżetu całego projektu. Przy projekcie za 300 000 PLN - migracja kosztuje 60 000-90 000 PLN.
Firmy, które próbują zaoszczędzić na migracji ("wrzucimy dane ręcznie" albo "zrobimy prosty import CSV"), płacą za to wielokrotnie więcej:
- Miesiące ręcznego przepisywania
- Błędy w danych, które wychodzą przez kolejne miesiące
- Utracone dane, których nikt nie zauważył
- Rozbieżności, które podważają zaufanie do nowego systemu
Najważniejsza zasada
Migracja danych to nie jednorazowy skrypt. To proces z testami, walidacją i planem B.
Każdy krok jest powtarzalny. Każdy krok jest testowalny. Każdy krok ma walidację. I jeśli coś pójdzie nie tak - wracamy do starego systemu, naprawiamy pipeline i próbujemy ponownie.
Stary system działa do momentu, aż nowy jest w 100% zwalidowany. Nie "pewnie działa". Nie "wygląda OK". 100% rekordów, co do grosza, co do jednego zamówienia.
Planujesz migrację ze starego systemu i boisz się o dane? Porozmawiajmy - zaczynamy od bezpłatnego auditu jakości Twoich danych. Powiemy Ci dokładnie, z jakimi problemami się zmierzysz i ile to będzie kosztować.