Często spotykamy się z sytuacją „Jeśli nie istnieje, wstaw”. Blog Dana Guzmana zawiera doskonałe informacje na temat tego, jak sprawić, by ten proces był bezpieczny.
Mam podstawową tabelę, która po prostu kataloguje ciąg do liczby całkowitej z SEQUENCE
. W procedurze przechowywanej muszę uzyskać klucz liczby całkowitej dla wartości, jeśli istnieje, lub INSERT
uzyskać wartość wynikową. dbo.NameLookup.ItemName
Kolumna ma wyjątkowość, więc integralność danych nie jest zagrożona, ale nie chcę napotykać wyjątków.
To nie jest IDENTITY
tak, że nie mogę dostać, SCOPE_IDENTITY
a wartość może być NULL
w niektórych przypadkach.
W mojej sytuacji mam do czynienia tylko z INSERT
bezpieczeństwem na stole, więc staram się zdecydować, czy lepiej zastosować MERGE
takie rozwiązanie:
SET NOCOUNT, XACT_ABORT ON;
DECLARE @vValueId INT
DECLARE @inserted AS TABLE (Id INT NOT NULL)
MERGE
dbo.NameLookup WITH (HOLDLOCK) AS f
USING
(SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
ON f.ItemName= new_item.val
WHEN MATCHED THEN
UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
INSERT
(ItemName)
VALUES
(@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s
Mógłbym to zrobić bez użycia MERGE
warunku, INSERT
po którym następuje SELECT
myślenie, że to drugie podejście jest czytelniejsze dla czytelnika, ale nie jestem przekonany, że to „lepsza” praktyka
SET NOCOUNT, XACT_ABORT ON;
INSERT INTO
dbo.NameLookup (ItemName)
SELECT
@vName
WHERE
NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)
DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName
A może jest inny lepszy sposób, którego nie rozważałem
Przeszukałem i odniosłem się do innych pytań. Ten: /programming/5288283/sql-server-insert-if-not-exists-best-practice jest najbardziej odpowiedni, jaki mogłem znaleźć, ale wydaje się, że nie ma on zastosowania do mojego przypadku użycia. Inne pytania do IF NOT EXISTS() THEN
podejścia, które nie uważam za dopuszczalne.
źródło
Odpowiedzi:
Ponieważ używasz Sekwencji, możesz użyć tej samej NASTĘPNEJ WARTOŚCI DLA - którą masz już w Ograniczeniu domyślnym w
Id
polu Klucz podstawowy - aby wygenerować nowąId
wartość z wyprzedzeniem. Generowanie wartości w pierwszej kolejności oznacza, że nie musisz się martwić o jej brakSCOPE_IDENTITY
, co oznacza, że nie potrzebujesz aniOUTPUT
klauzuli, ani robienia dodatkowych,SELECT
aby uzyskać nową wartość; będziesz miał wartość, zanim to zrobiszINSERT
, i nawet nie musisz zadzierać zSET IDENTITY INSERT ON / OFF
:-)To zajmuje część ogólnej sytuacji. Druga część to obsługa problemu dwóch procesów jednocześnie, nie znajdowanie istniejącego wiersza dla dokładnie tego samego łańcucha i kontynuowanie
INSERT
. Chodzi o uniknięcie naruszenia unikatowego ograniczenia, które mogłoby wystąpić.Jednym ze sposobów radzenia sobie z tego rodzaju problemami współbieżności jest wymuszenie, aby ta konkretna operacja była jednowątkowa. Można to zrobić za pomocą blokad aplikacji (które działają między sesjami). Choć są skuteczne, mogą być nieco ciężkie w sytuacji takiej jak ta, w której częstotliwość kolizji jest prawdopodobnie dość niska.
Innym sposobem radzenia sobie z kolizjami jest zaakceptowanie, że czasami się zdarzają, i radzenie sobie z nimi, a nie próba ich uniknięcia. Korzystając z
TRY...CATCH
konstrukcji, możesz skutecznie wychwycić konkretny błąd (w tym przypadku: „unikalne naruszenie ograniczenia”, Msg 2601) i ponownie wykonać,SELECT
aby uzyskaćId
wartość, ponieważ wiemy, że teraz istnieje, ponieważ jest wCATCH
bloku z tym konkretnym błąd. Inne błędy mogą być obsługiwane w typowymRAISERROR
/RETURN
lubTHROW
sposób.Konfiguracja testu: Sekwencja, Tabela i Indeks unikalny
Konfiguracja testu: procedura przechowywana
Test
Pytanie od OP
MERGE
ma różne „problemy” (kilka referencji jest powiązanych w odpowiedzi @ SqlZim, więc nie ma potrzeby kopiowania tych informacji tutaj). I w tym podejściu nie ma dodatkowego blokowania (mniej rywalizacji), więc powinno być lepiej w przypadku współbieżności. Dzięki takiemu podejściu nigdy nie otrzymasz naruszenia Unikalnego Ograniczenia, wszystko bez żadnegoHOLDLOCK
, itp. Jest prawie pewne, że zadziała.Uzasadnieniem tego podejścia jest:
CATCH
bloku w pierwszej kolejności będzie dość niska. Bardziej sensowne jest zoptymalizowanie kodu, który będzie działał przez 99% czasu, zamiast kodu, który będzie działał przez 1% czasu (chyba że nie ma kosztów optymalizacji obu, ale tak nie jest w tym przypadku).Komentarz z odpowiedzi @ SqlZim (wyróżnienie dodane)
Zgodziłbym się z tym pierwszym zdaniem, gdyby zostało zmienione w taki sposób, aby zawierało sformułowanie „i - gdy jest ostrożny”. To, że coś jest technicznie możliwe, nie oznacza, że sytuacja (tj. Zamierzony przypadek użycia) byłaby z tego korzystna.
Problem, który widzę w tym podejściu, polega na tym, że blokuje on więcej niż sugeruje się. Ważne jest, aby ponownie przeczytać cytowaną dokumentację dotyczącą „serializowalnego”, w szczególności następujące (podkreślenie dodane):
Oto komentarz w przykładowym kodzie:
Słowo operacyjne to „zasięg”. Blokada jest podejmowana nie tylko od wartości w
@vName
, ale dokładniej od zakresu odmiejsce, w którym powinna iść ta nowa wartość (tj. między istniejącymi kluczowymi wartościami po obu stronach miejsca, w którym mieści się nowa wartość), ale nie sama wartość. Oznacza to, że inne procesy będą blokowane przed wstawianiem nowych wartości, w zależności od aktualnie badanych wartości. Jeśli wyszukiwanie odbywa się u góry zakresu, wstawianie wszystkiego, co mogłoby zajmować tę samą pozycję, zostanie zablokowane. Na przykład, jeśli istnieją wartości „a”, „b” i „d”, to jeśli jeden proces wykonuje WYBÓR na „f”, wówczas nie będzie można wstawić wartości „g”, a nawet „e” ( ponieważ którykolwiek z nich pojawi się natychmiast po „d”). Jednak wstawienie wartości „c” będzie możliwe, ponieważ nie zostanie ona umieszczona w zakresie „zarezerwowanym”.Poniższy przykład powinien zilustrować to zachowanie:
(Na karcie zapytania (tj. Sesja) # 1)
(Na karcie zapytania (tj. Sesja) # 2)
Podobnie, jeśli istnieje wartość „C”, a wartość „A” jest wybierana (a zatem blokowana), wówczas można wstawić wartość „D”, ale nie wartość „B”:
(Na karcie zapytania (tj. Sesja) # 1)
(Na karcie zapytania (tj. Sesja) # 2)
Szczerze mówiąc, w moim sugerowanym podejściu, w przypadku wyjątku, w Dzienniku transakcji pojawią się 4 wpisy, które nie pojawią się w tym podejściu do „transakcji możliwej do serializacji”. ALE, jak powiedziałem powyżej, jeśli wyjątek zdarzy się 1% (lub nawet 5%) czasu, będzie to miało o wiele mniejszy wpływ niż o wiele bardziej prawdopodobny przypadek początkowego SELECT blokującego tymczasowo operacje WSTAWIANIA.
Innym, aczkolwiek niewielkim, problemem związanym z tym podejściem „możliwa do serializacji transakcja + klauzula OUTPUT” jest to, że
OUTPUT
klauzula (w obecnym użyciu) odsyła dane jako zestaw wyników. Zestaw wyników wymaga większego obciążenia (prawdopodobnie po obu stronach: w SQL Server do zarządzania wewnętrznym kursorem oraz w warstwie aplikacji do zarządzania obiektem DataReader) niż prostyOUTPUT
parametr. Biorąc pod uwagę, że mamy do czynienia tylko z jedną wartością skalarną i że założeniem jest wysoka częstotliwość wykonywania, ten dodatkowy narzut zestawu wyników prawdopodobnie się sumuje.Chociaż
OUTPUT
klauzula może być użyta w taki sposób, aby zwrócićOUTPUT
parametr, wymagałoby to dodatkowych kroków w celu utworzenia tabeli tymczasowej lub zmiennej tabeli, a następnie wybrania wartości z tej tabeli temp / zmiennej tabeli doOUTPUT
parametru.Dalsze wyjaśnienia: Odpowiedź na odpowiedź @ SqlZim (zaktualizowana odpowiedź) na moją odpowiedź na odpowiedź @ SqlZim (w oryginalnej odpowiedzi) na moje oświadczenie dotyczące współbieżności i wydajności ;-)
Przepraszam, jeśli ta część jest trochę za długa, ale w tym momencie jesteśmy po prostu na niuansach obu podejść.
Tak, przyznaję, że jestem stronniczy, choć szczerze mówiąc:
INSERT
kończy się niepowodzeniem z powodu naruszenia Unikalnego Ograniczenia. Nie widziałem tego wymienionego w żadnej z innych odpowiedzi / postów.Jeśli chodzi o podejście „JFDI” @ gbn, post „Ugly Pragmatism For The Win” Michaela J. Swarta oraz komentarz Aarona Bertranda do postu Michaela (dotyczący jego testów pokazujących, które scenariusze zmniejszyły wydajność), a także komentarz na temat „adaptacji Michaela J. Adaptacja Stewarta do procedury JFDI Try Catch @ gbn, stwierdzająca:
W odniesieniu do dyskusji gbn / Michael / Aaron związanej z podejściem „JFDI” niewłaściwe byłoby zrównanie mojej sugestii z podejściem „JFDI” gbn. Ze względu na charakter operacji „Pobierz lub wstaw” istnieje wyraźna potrzeba wykonania tej czynności,
SELECT
aby uzyskaćID
wartość dla istniejących rekordów. Ten WYBÓR działa jakIF EXISTS
sprawdzenie, co czyni to podejście bardziej równoważnym wariantowi „CheckTryCatch” testów Aarona. Ponownie napisany kod Michaela (i twoja ostateczna adaptacja adaptacji Michaela) obejmuje równieżWHERE NOT EXISTS
sprawdzenie tego samego. Dlatego moja sugestia (wraz z ostatecznym kodem Michaela i twoją adaptacją jego końcowego kodu) tak naprawdę nie trafiCATCH
tak często. Mogą to być tylko sytuacje, w których dwie sesje,ItemName
INSERT...SELECT
dokładnie w tym samym momencie, tak że obie sesje otrzymują „prawdziwą”WHERE NOT EXISTS
dokładnie w tym samym momencie, a zatem obie próbują wykonać dokładnieINSERT
w tym samym momencie. Ten bardzo specyficzny scenariusz zdarza się znacznie rzadziej niż wybór istniejącegoItemName
lub wstawienie nowego,ItemName
gdy żaden inny proces nie próbuje tego zrobić dokładnie w tym samym momencie .Z WSZYSTKIMI POWYŻSZYMI UMYSŁAMI: Dlaczego wolę swoje podejście?
Najpierw spójrzmy na to, co blokuje się w podejściu „szeregowalnym”. Jak wspomniano powyżej, „zakres”, który zostaje zablokowany, zależy od istniejących wartości kluczy po obu stronach miejsca, w którym zmieściłaby się nowa wartość klucza. Początkiem lub końcem zakresu może być również odpowiednio początek lub koniec indeksu, jeśli w tym kierunku nie ma żadnej wartości klucza. Załóżmy, że mamy następujący indeks i klucze (
^
reprezentuje początek indeksu, a$
reprezentuje jego koniec):Jeśli sesja 55 spróbuje wstawić kluczową wartość:
A
, następnie zakres nr 1 (od^
doC
) jest zablokowany: sesja 56 nie może wstawić wartościB
, nawet jeśli jest unikalna i poprawna (jeszcze). Ale sesja 56 można wstawić wartościD
,G
orazM
.D
, następnie zakres nr 2 (odC
doF
) jest zablokowany: sesja 56 nie może wstawić wartościE
(jeszcze). Ale sesja 56 można wstawić wartościA
,G
orazM
.M
, następnie zakres # 4 (odJ
do$
) jest zablokowany: sesja 56 nie może wstawić wartościX
(jeszcze). Ale sesja 56 można wstawić wartościA
,D
orazG
.W miarę dodawania kolejnych kluczowych wartości, zakresy między kluczowymi wartościami stają się węższe, co zmniejsza prawdopodobieństwo / częstość wstawienia wielu wartości w tym samym czasie walcząc o ten sam zakres. Trzeba przyznać, że nie jest to poważny problem i na szczęście wydaje się, że problem ten z czasem maleje.
Problem z moim podejściem został opisany powyżej: dzieje się tak tylko wtedy, gdy dwie sesje próbują jednocześnie wprowadzić tę samą wartość klucza. W związku z tym sprowadza się to do tego, co ma większe prawdopodobieństwo wystąpienia: dwie różne, ale bliskie, wartości klucza są próbowane w tym samym czasie, czy próbowana jest ta sama wartość klucza w tym samym czasie? Przypuszczam, że odpowiedź leży w strukturze aplikacji wykonującej wstawki, ale ogólnie mówiąc, bardziej prawdopodobne byłoby założenie dwóch różnych wartości, które akurat dzielą ten sam zakres. Ale jedynym sposobem, aby naprawdę wiedzieć, byłoby przetestowanie obu w systemie operacyjnym.
Następnie rozważmy dwa scenariusze i sposób, w jaki radzi sobie z nimi każde podejście:
Wszystkie żądania dotyczą unikatowych wartości kluczowych:
W tym przypadku
CATCH
blok w mojej sugestii nigdy nie jest wprowadzany, dlatego nie ma „problemu” (tj. 4 wpisów w dzienniku tran i czas potrzebny na to). Ale w podejściu „szeregowalnym”, nawet jeśli wszystkie płytki są unikalne, zawsze będzie istniał potencjał do blokowania innych płytek w tym samym zakresie (choć nie na bardzo długo).Wysoka częstotliwość żądań dla tej samej wartości klucza w tym samym czasie:
W tym przypadku - bardzo niski stopień wyjątkowości pod względem przychodzących żądań nieistniejących wartości klucza -
CATCH
blok w mojej sugestii będzie wprowadzany regularnie. Skutkiem tego będzie to, że każda nieudana wstawka będzie musiała automatycznie przywracać i zapisywać 4 wpisy w Dzienniku transakcji, co jest niewielkim spadkiem wydajności za każdym razem. Ale ogólna operacja nigdy nie powinna zawieść (przynajmniej nie z tego powodu).(Wystąpił problem z poprzednią wersją „zaktualizowanego” podejścia, która pozwalała mu cierpieć z powodu zakleszczeń. Dodano
updlock
wskazówkę, aby rozwiązać ten problem i nie ma już zakleszczeń).ALE w podejściu „serializowalnym” (nawet w zaktualizowanej, zoptymalizowanej wersji) operacja zostanie zakleszczona. Dlaczego? Ponieważserializable
zachowanie zapobiega tylkoINSERT
operacjom w zakresie, który został odczytany, a zatem zablokowany; nie zapobiegaSELECT
operacjom w tym zakresie.serializable
Podejście, w tym przypadku, wydaje się nie mieć dodatkowe obciążenie i może wykonywać nieco lepsze niż to, co ja sugeruję.Podobnie jak w przypadku wielu / większości dyskusji na temat wydajności, ponieważ istnieje tak wiele czynników, które mogą wpłynąć na wynik, jedynym sposobem, aby naprawdę poczuć, jak coś się stanie, jest wypróbowanie go w docelowym środowisku, w którym będzie działać. W tym momencie nie będzie to kwestia opinii :).
źródło
Zaktualizowana odpowiedź
Odpowiedź na @srutzky
Zgadzam się i z tych samych powodów używam parametrów wyjściowych, gdy jestem ostrożny . To mój błąd, że nie użyłem parametru wyjściowego przy pierwszej odpowiedzi, byłem leniwy.
Oto poprawiony procedura z użyciem parametru wyjściowego, dodatkowe optymalizacje, wraz z
next value for
, że @srutzky wyjaśnia w swojej odpowiedzi :Uwaga dotycząca aktualizacji : Dołączenie
updlock
do zaznaczenia spowoduje pobranie odpowiednich blokad w tym scenariuszu. Podziękowania dla @srutzky, który zwrócił uwagę, że może to powodować impasy podczas korzystania tylkoserializable
z Internetuselect
.Uwaga: Może tak nie być, ale jeśli to możliwe, procedura zostanie wywołana z wartością for
@vValueId
, includeset @vValueId = null;
afterset xact_abort on;
, w przeciwnym razie można ją usunąć.W odniesieniu do przykładów @ srutzky dotyczących blokowania zakresu klucza:
@srutzky używa tylko jednej wartości w swojej tabeli i blokuje klawisz „next” / „infinity” w swoich testach, aby zilustrować blokowanie zakresu klawiszy. Podczas gdy jego testy ilustrują to, co dzieje się w takich sytuacjach, uważam, że sposób, w jaki informacje są prezentowane, może prowadzić do fałszywych założeń dotyczących wielkości blokady, jakiej można się spodziewać podczas korzystania
serializable
ze scenariusza przedstawionego w pierwotnym pytaniu.Mimo że postrzegam błąd (być może fałszywie) w sposobie, w jaki przedstawia swoje wyjaśnienia i przykłady blokowania zakresu klucza, są one nadal poprawne.
Po dalszych badaniach znalazłem szczególnie istotny artykuł na blogu z 2011 r. Autorstwa Michaela J. Swarta: Mythbusting: Concurrent Update / Insert Solutions . W nim testuje wiele metod pod kątem dokładności i współbieżności. Metoda 4: Zwiększona izolacja + precyzyjne strojenie blokad opiera się na postie Sama Saffrona wstawiania lub aktualizacji wzorca dla SQL Server i jest jedyną metodą w oryginalnym teście, która spełniła jego oczekiwania (dołączyły później
merge with (holdlock)
).W lutym 2016 roku Michael J. Swart opublikował Ugly Pragmatism For The Win . W tym poście opisuje kilka dodatkowych zmian, które wprowadził do swoich procedur upsert Saffron, aby zmniejszyć blokowanie (które zawarłem w powyższej procedurze).
Po dokonaniu tych zmian Michael nie był zadowolony, że jego procedura zaczęła wyglądać na bardziej skomplikowaną i skonsultował się z kolegą o imieniu Chris. Chris przeczytał wszystkie oryginalne posty z Mythbusters, przeczytał wszystkie komentarze i zapytał o wzór JFDI @ gbn TRY CATCH . Ten wzór jest podobny do odpowiedzi @ srutzky i jest rozwiązaniem, którego Michael użył w tym przypadku.
Michael J Swart:
Moim zdaniem oba rozwiązania są realne. Chociaż nadal wolę zwiększyć poziom izolacji i dostroić blokady, odpowiedź @ srutzky jest również ważna i może, ale nie musi, być bardziej wydajna w twojej konkretnej sytuacji.
Być może w przyszłości i ja dojdę do tego samego wniosku, co Michael J. Swart, ale po prostu jeszcze mnie tam nie ma.
To nie jest moje preferencje, ale oto, jak wyglądałoby moje dostosowanie adaptacji JFDI @ gbn przez Michaela J. Stewarta :
Jeśli wstawiasz nowe wartości częściej niż wybierając istniejące wartości, może to być bardziej wydajne niż wersja @ srutzky . W przeciwnym razie wolałbym wersję @ srutzky od tej.
Komentarze Aarona Bertranda na temat postów Michaela J Swarta prowadzą do odpowiednich testów, które przeprowadził i doprowadziły do tej wymiany. Fragment sekcji komentarzy na temat brzydkiego pragmatyzmu dla zwycięzcy :
i odpowiedź:
Nowe linki:
Oryginalna odpowiedź
Nadal wolę podejście upsam Sama Saffrona niż używanie
merge
, szczególnie w przypadku pojedynczego rzędu.Dostosowałbym tę metodę upsert do następującej sytuacji:
Byłbym zgodny z twoim nazywaniem i, podobnie
serializable
jakholdlock
, wybierz jedno i konsekwentne w jego użyciu. Zwykle używam,serializable
ponieważ jest to ta sama nazwa, co przy określaniuset transaction isolation level serializable
.Przy użyciu
serializable
lubholdlock
blokada zakresu jest podejmowana na podstawie wartości,@vName
która powoduje, że wszelkie inne operacje czekają, czy wybiorą lub wstawią wartości dodbo.NameLookup
tej wartości wwhere
klauzuli.Aby blokada zakresu działała poprawnie,
ItemName
kolumna musi mieć indeks, który obowiązuje również podczas używaniamerge
.Oto co procedura będzie wyglądać głównie następujące whitepapers Erland Sommarskog za obsługę błędów , używając
throw
. Jeślithrow
nie jest to sposób zgłaszania błędów, zmień go tak, aby był zgodny z pozostałymi procedurami:Podsumowując, co dzieje się w powyższej procedurze:
set nocount on; set xact_abort on;
tak jak zawsze to robisz , to jeśli nasza zmienna wejściowais null
lub pusta,select id = cast(null as int)
w wyniku. Jeśli nie jest pusta ani pusta, to weźId
zmienną for, trzymając to miejsce na wypadek, gdyby go nie było. JeśliId
jest, wyślij go. Jeśli go nie ma, włóż go i wyślij nowyId
.W międzyczasie inne wywołania tej procedury, próbujące znaleźć identyfikator dla tej samej wartości, poczekają na zakończenie pierwszej transakcji, a następnie ją wybiorą i zwrócą. Inne wywołania tej procedury lub inne instrukcje szukające innych wartości będą kontynuowane, ponieważ ta nie jest przeszkodą.
Chociaż zgadzam się z @srutzky, że możesz poradzić sobie z kolizjami i połknąć wyjątki dla tego rodzaju problemów, ja osobiście wolę spróbować dopasować rozwiązanie, aby uniknąć tego, gdy to możliwe. W tym przypadku nie wydaje mi się, aby używanie blokad z
serializable
było ciężkim podejściem i byłbym pewien, że dobrze poradziłby sobie z wysoką współbieżnością.Cytat z dokumentacji serwera SQL w tabeli podpowiedzi
serializable
/holdlock
:Wycena z dokumentacji serwera SQL na poziomie izolacji transakcji
serializable
Linki związane z powyższym rozwiązaniem:
Wstaw lub aktualizuj wzorzec dla Sql Server - Sam Saffron
Dokumentacja dotycząca szeregowalnych i innych wskazówek dotyczących tabel - MSDN
Obsługa błędów i transakcji w SQL Server Część pierwsza - Obsługa błędów Jumpstart - Erland Sommarskog
Porada Erlanda Sommarskoga dotycząca @@ rowcount (której nie zastosowałem w tym przypadku).
MERGE
ma niejednoznaczną historię i wydaje się, że potrzeba więcej czasu, aby upewnić się, że kod zachowuje się tak, jak chcesz, przy całej tej składni. Odpowiedniemerge
artykuły:Ciekawy błąd MERGE - Paul White
Wyścig UPSERT ze scaleniem - sqlteam
Zachowaj ostrożność w przypadku instrukcji MERGE programu SQL Server - Aaron Bertrand
Czy mogę zoptymalizować tę instrukcję scalania - Aaron Bertrand
Jeśli korzystasz z widoków indeksowanych i MERGE, przeczytaj to! - Aaron Bertrand
Ostatnie ogniwo, Kendra Little, przeprowadziła przybliżone porównanie w
merge
porównaniuinsert with left join
z zastrzeżeniem, w którym powiedziała: „Nie przeprowadziłem dokładnych testów obciążenia”, ale nadal jest to dobra lektura.źródło