Things To Avoid, Episode 1: INSERT IGNORE
Legacy code strikes back
Poleganie na nieudokumentowanych funkcjach języka jest dość ryzykowne. A kiedy miesza się to z INSERT IGNORE, sprawy przybierają naprawdę zły obrót.
Podczas jednej z aktualizacji systemu operacyjnego na początku 2016 roku, Perl został zaktualizowany z wersji 5.14 do 5.18. Niestety, obsługa haszy uległa zmianie, kolejność kluczy w dość specyficznym scenariuszu nie była już utrzymywana, a kod się zepsuł. Masz rację, Perl nigdy nie obiecywał kolejności, ale nasz kod polegał na pewnej specyfice implementacji.
Wstęp z Perl changelog dla v5.18:
Domyślnie dwie odrębne zmienne hash z identycznymi kluczami i wartościami mogą teraz przekazywać swoją zawartość w innej kolejności, gdzie wcześniej była identyczna.
Dlaczego był to problem? Kod przygotowywał listę hashy, które miały być wstawione do bazy danych. Następnie używał pierwszego elementu do przygotowania listy nazw kolumn, które miały być przekazane do zapytań INSERT, które nastąpią później. Jednak każde wysłane zapytanie INSERT używało innych hashy do przygotowania listy wartości do wstawienia. Prowadziło to do następujących sytuacji:
INSERT IGNORE INTO foo (id, data) VALUES (1, '2006-10-11`), (’2007-12-09′, 2), …
Podstawowo, kolejność nazw kolumn i odpowiadających im wartości nie była zachowana. Oczywiście, MySQL normalnie podnosi błąd, gdy ktoś próbuje wstawić datę do kolumny integer. Ale pamiętasz ten cytat z dokumentacji MySQL, prawda? Błędy te nie są podnoszone, gdy INSERT IGNORE jest używany i ma miejsce cicha konwersja danych:
Data consistently inconsistent
Skrypt działał dobrze po aktualizacji Perla (cóż, przynajmniej jego stderr był cichy), ale po kilku dniach dostaliśmy zgłoszenie błędu, że coś jest nie tak z naszymi wewnętrznymi narzędziami, które używają tabel opartych na Perlu. Sądząc po raporcie, coś było dość podejrzane. I nasze obawy potwierdziło szybkie zapytanie SELECT:
Co my tu mamy. Timestamp i wartości całkowite przekonwertowane na ciąg znaków i wstawione jako nazwa użytkownika, lata przechowywane w kolumnach całkowitych (czy 2016 edits count lub rok ostatniej edycji). Niespójność danych w najlepszym wydaniu.
Nie musisz się z tym zgadzać, ale dla mnie żadne dane nie są lepsze niż „dane” takie jak wiersze powyżej.
Dotknięta tabela waży ponad 75 GiB i przechowuje 460 mm wierszy. Możemy je zregenerować, ale to długi proces. Skrypt jest uruchomiony, gdy czytasz tę historię. Cóż, działa już od pięciu tygodni… Wszystko dzięki IGNORE w jednym zapytaniu.
Wyciągnięte wnioski
- INSERT IGNORE to cicha bestia, która tylko czeka na Twój błąd, aby zamienić dane w bezsensowny zestaw wartości.
- Nigdy nie ignoruj błędów. Uczyń je głośnymi. Fail early.
- Poleganie na nieudokumentowanych cechach języka (Perl i kolejność kluczy hash to tylko jeden z wielu przykładów) jest ryzykowną grą.
Zostałeś ostrzeżony. Teraz sprawdź swój kod lub ngrep swój ruch w poszukiwaniu zapytań INSERT IGNORE.