Merge w n8n: append, combine i SQL join - który tryb wybrać i dlaczego

Poznaj mechanikę node'a Merge w n8n. Jak działają pod maską tryby append, combine i dopasowanie po polach? Uniknij pułapek pustych wejść i korupcji danych.

Merge w n8n: append, combine i SQL join - który tryb wybrać i dlaczego

W architekturze opartej na skierowanych grafach acyklicznych (DAG), z jakiej korzysta n8n, rozdzielenie strumienia danych (branching) to operacja prosta i tania obliczeniowo. Schody zaczynają się, gdy musimy te równoległe lub asynchroniczne ścieżki połączyć z powrotem. Node Merge nie jest zwykłym lejkiem, który bezrefleksyjnie zsypuje obiekty do jednego worka. To zaawansowany silnik transformacji, który musi podjąć decyzję, jak dopasować do siebie dwie niezależne tablice JSON, często posiadające różną długość i strukturę.

Zły wybór trybu łączenia w n8n to nie tylko ryzyko błędu wykonania (execution error), ale przede wszystkim cichej korupcji danych, która wyjdzie na jaw dopiero w systemach docelowych – na przykład gdy przypiszesz numer konta bankowego jednego klienta do profilu drugiego. Aby tego uniknąć, nie wystarczy znać interfejsu graficznego. Trzeba zrozumieć, jak platforma operuje na pamięci i strukturach danych w tle.

W tym artykule rozbierzemy na czynniki pierwsze mechanikę node'a Merge. Zajrzymy pod maskę silnika Node.js, na którym opiera się n8n, aby przeanalizować, jak fizycznie alokowane są obiekty podczas trybów Append, Combine oraz łączenia relacyjnego (odpowiednik SQL Join). Zrozumienie tych mechanizmów pozwoli Ci budować deterministyczne, bezpieczne i odporne na błędy workflow. Omówimy również klasyczny problem pustych wejść (empty inputs), który potrafi bez ostrzeżenia zatrzymać całą egzekucję.

Mechanizm pod maską: jak n8n widzi dwa strumienie danych

Zanim przejdziemy do konkretnych trybów, musimy zdefiniować, na czym fizycznie operuje node Merge. Kiedy workflow dociera do punktu łączenia, silnik n8n ładuje do pamięci operacyjnej wyniki z obu wejść (Input 1 i Input 2). W architekturze n8n każde wejście to zawsze tablica obiektów opakowanych w metadane, posiadająca standardową strukturę:

[
  {
    "json": { "id": 101, "name": "System A" },
    "pairedItem": { "item": 0 }
  }
]

Zadaniem node'a Merge jest pobranie tablicy z Input 1 oraz tablicy z Input 2 i wygenerowanie na ich podstawie nowej, pojedynczej tablicy itemsOut. Sposób iteracji po tych dwóch wejściowych kolekcjach determinuje zużycie zasobów (CPU i RAM) oraz logiczną poprawność danych. Każdy tryb węzła Merge to pod maską zupełnie inny algorytm operujący na tych tablicach.

Tryb Append: prosta konkatenacja i problem heterogeniczności

Tryb Append (często realizowany przez ustawienie multiplexing lub po prostu dołączanie danych) to najprostsza możliwa operacja. Pod maską silnik V8 wykonuje akcję zbliżoną do standardowej konkatenacji tablic w JavaScript: [...items1, ...items2]. Złożoność obliczeniowa tego procesu wynosi O(n), gdzie n to suma elementów obu wejść. Obiekty z Input 2 są po prostu dopisywane na końcu obiektów z Input 1.

Kiedy stosujemy ten tryb? Przede wszystkim do agregacji homogenicznych danych z różnych źródeł. Wyobraź sobie, że pobierasz logi błędów z systemu Jira w jednej gałęzi, a z systemu PagerDuty w drugiej. Chcesz zebrać je wszystkie w jedną długą listę, by w kolejnym kroku zapisać je hurtem do bazy danych za pomocą node'a Postgres w trybie Batch Insert.

Główną pułapką trybu Append jest heterogeniczność schematów. Ponieważ n8n nie wymusza silnego typowania pomiędzy nodami, tryb Append bez problemu połączy obiekty o zupełnie różnych kluczach:

[
  { "json": { "userId": 1, "email": "a@b.com" } }, // z Input 1
  { "json": { "user_id": 2, "mail": "c@d.com" } }  // z Input 2
]

Taka struktura wyjściowa jest tykającą bombą dla kolejnych węzłów. Jeśli następny node oczekuje pola $json.email, dla elementów z Input 2 zwróci wartość undefined. Aby zachować higienę danych, przed wejściem do node'a Merge w trybie Append ZAWSZE należy ujednolicić schematy w obu gałęziach, wykorzystując do tego node Edit Fields lub Code, tak aby oba strumienie miały identyczne nazwy kluczy i typy wartości.

Tryb Combine: łączenie po indeksach i pułapka dryfu kolejności

Tryb Combine (często nazywany Zippingiem) łączy obiekty na podstawie ich indeksu w tablicy. Silnik n8n iteruje równolegle po obu wejściach i dla każdego indeksu i wykonuje operację scalania obiektów JSON. Pod maską wygląda to w uproszczeniu tak:

const mergedJson = { ...items1[i].json, ...items2[i].json };

Jeśli klucze w obu obiektach się pokrywają, wartości z Input 2 nadpiszą wartości z Input 1. Tryb ten jest niezwykle wydajny pamięciowo, ponieważ nie wymaga przeszukiwania tablic ani budowania map haszujących. Kiedy z niego korzystać? Wtedy, gdy w jednej gałęzi transformujesz dane (np. wysyłasz tekst do LLM w celu analizy sentymentu), a w drugiej przepuszczasz oryginalne obiekty bez zmian, i chcesz na końcu połączyć wynik z oryginalnym rekordem.

Niestety, Combine niesie ze sobą gigantyczne ryzyko operacyjne, które nazywamy dryfem kolejności (order drift). Tryb ten zakłada, że element o indeksie 0 w Input 1 to logicznie ten sam byt, co element o indeksie 0 w Input 2. Wystarczy jednak, że w jednej z gałęzi użyjesz node'a Filter, który usunie choć jeden element, albo node'a HTTP Request z opcją Continue On Fail, który zwróci pusty obiekt dla błędnego zapytania. Wtedy indeksy przesuwają się względem siebie.

Jeśli tablice stracą synchronizację, n8n bez żadnego ostrzeżenia połączy dane Jana Kowalskiego z numerem telefonu Piotra Nowaka. Dodatkowo, jeśli tablice mają różną długość, n8n odetnie nadmiarowe elementy z dłuższej tablicy (lub wypełni je wartościami null, w zależności od konfiguracji). Z tego powodu tryb Combine stosujemy WYŁĄCZNIE wtedy, gdy mamy absolutną, gwarantowaną architektonicznie pewność, że liczba i kolejność itemów w obu gałęziach nie uległa zmianie.

Dopasowanie po polach (SQL Join): relacyjne łączenie w pamięci RAM

Aby uniknąć ryzyka związanego z dryfem kolejności, najlepszym rozwiązaniem jest użycie trybu Match by fields (często konfigurowanego jako Keep Key Matches). Działa on dokładnie tak, jak klauzula JOIN w relacyjnych bazach danych. Wskazujemy klucz identyfikujący z Input 1 (np. customer_id) oraz klucz z Input 2 (np. id).

Pod maską n8n nie wykonuje tu prostego łączenia indeksów. Silnik buduje w pamięci operacyjnej hash mapę dla jednego z wejść (najczęściej tego podłączonego do Input 2), a następnie iteruje po Input 1, błyskawicznie wyszukując dopasowania po zdefiniowanym kluczu. Złożoność czasowa spada dzięki temu z O(n*m) do O(n+m), jednak kosztuje to znacznie więcej pamięci RAM.

Konfigurując ten tryb, decydujesz o typie złączenia (Join Type):

  • Keep Key Matches (Inner Join): na wyjściu pojawią się tylko te obiekty, które znalazły dopasowanie w obu strumieniach.
  • Keep Non-Matches (Left/Right Anti Join): zwraca elementy, które NIE znalazły odpowiednika w drugiej gałęzi. Idealne do detekcji brakujących rekordów przed operacją INSERT.
  • Keep All (Full Outer Join): łączy dopasowane elementy, a dla brakujących zostawia puste pola. Zwraca połączony zbiór wszystkich danych.

Należy uważać na relacje jeden-do-wielu (1:N). Jeśli w Input 2 znajdzie się kilka obiektów z tym samym kluczem, n8n zachowa się specyficznie w zależności od wersji – zazwyczaj dopasuje pierwszy znaleziony element lub zduplikuje rekord z Input 1 dla każdego dopasowania z Input 2. Zawsze weryfikuj unikalność kluczy przed wejściem do Merge, używając node'a Item Lists (Remove Duplicates).

Zmora pustych wejść (Empty Inputs) i cykl życia egzekucji

Jednym z najczęstszych problemów, z jakimi borykają się inżynierowie budujący zaawansowane workflow w n8n, jest permanentne zawieszenie się egzekucji na node'ie Merge. Wynika to z mechaniki działania grafu skierowanego. Node w n8n uruchamia się dopiero wtedy, gdy WSZYSTKIE jego aktywne wejścia otrzymają dane.

Wyobraźmy sobie scenariusz: gałąź A pobiera listę nowych klientów (5 elementów). Gałąź B pobiera listę klientów do usunięcia. Jeśli w danym dniu nie ma żadnych klientów do usunięcia, node w gałęzi B zwraca 0 elementów (pustą tablicę). W tym momencie gałąź B kończy swoje wykonanie (silnik n8n uznaje ją za zakończoną bez propagacji danych dalej). Node Merge oczekuje na sygnał z gałęzi B. Ponieważ sygnał nigdy nie nadejdzie, Merge nigdy się nie uruchomi. Cały workflow zatrzymuje się w tym miejscu, a status egzekucji może wisieć jako niedokończony lub zakończyć się sukcesem bez wykonania końcowych kroków.

Jak rozwiązać ten problem architektonicznie? Na node'ach poprzedzających Merge (np. Filter, HTTP Request, Postgres) należy wejść w ustawienia (Settings) i aktywować opcję Always Output Data. Sprawi to, że w przypadku braku wyników, node nie przerwie gałęzi, lecz wygeneruje jeden pusty element (tzw. empty item):

[
  { "json": {} }
]

Dzięki temu sygnał dotrze do node'a Merge. Musisz jednak pamiętać, że Merge potraktuje ten pusty obiekt jako pełnoprawny rekord. W trybie Append doda pusty obiekt do tablicy wyjściowej. W trybie Match by fields prawdopodobnie nie znajdzie dopasowania (bo pole klucza będzie równe undefined). Dlatego tuż za nodem Merge często umieszcza się node Filter z warunkiem sprawdzającym, czy dany klucz istnieje (np. {{ $json.id }} is not empty), aby oczyścić strumień z technicznych "pustych" obiektów.

Kiedy NIE używać node'a Merge (Offloading do bazy danych)

Chociaż node Merge jest potężny, jego piętą achillesową jest zużycie pamięci w środowisku Node.js. Jeśli w self-hosted n8n próbujesz połączyć relacyjnie (Match by fields) dwie tablice liczące po 50 000 rekordów, ryzykujesz wyczerpaniem pamięci sterty (Heap Out Of Memory) i restartem kontenera z n8n.

Dla tak dużych wolumenów danych, n8n nie powinien pełnić roli silnika bazodanowego. W takich przypadkach najlepszą praktyką (Best Practice) jest zrzucenie ciężaru obliczeniowego (offloading) na zewnętrzną bazę danych. Zamiast łączyć dane w pamięci n8n:

  1. Zapisz strumień z Input 1 do tymczasowej tabeli w bazie (np. PostgreSQL) używając node'a Postgres w trybie Batch Insert.
  2. Zapisz strumień z Input 2 do innej tabeli tymczasowej.
  3. Wykonaj node Postgres z zapytaniem SELECT ... FROM table1 JOIN table2 ON ... i pobierz gotowy, połączony wynik z powrotem do n8n.

Relacyjne bazy danych są optymalizowane pod kątem operacji JOIN na poziomie dysku i indeksów, radząc sobie z milionami rekordów w ułamku sekundy, podczas gdy n8n zablokowałoby w tym czasie główny wątek (Event Loop).

Checklista architekta n8n: który tryb wybrać?

Decyzja o sposobie łączenia danych w n8n powinna być podyktowana strukturą danych i gwarancjami, jakie dają poprzednie kroki w workflow. Podsumowując, stosuj poniższe kryteria decyzyjne:

  • Wybierz Append, jeśli chcesz po prostu wydłużyć listę wyników, a struktura obiektów w obu gałęziach jest identyczna (lub ujednoliciłeś ją przed wejściem do Merge).
  • Wybierz Combine TYLKO wtedy, gdy masz 100% pewności, że żadna z gałęzi nie zmieniła kolejności ani liczby elementów (brak filtrowania, brak błędów HTTP). Jest to tryb najwyższego ryzyka.
  • Wybierz Match by fields (SQL Join) zawsze, gdy łączysz obiekty reprezentujące te same byty biznesowe (np. wzbogacasz dane klienta o dane z CRM). To najbezpieczniejszy tryb, odporny na zmiany kolejności.
  • Pamiętaj o Always Output Data na węzłach przed Merge, jeśli istnieje ryzyko, że którakolwiek z gałęzi może zwrócić 0 elementów. W przeciwnym razie workflow zatrzyma się bez błędu.
  • Monitoruj rozmiar danych. Przy łączeniu dziesiątek tysięcy rekordów, przenieś logikę z node'a Merge w n8n do zapytania SQL w docelowej bazie danych.

Świadome korzystanie z node'a Merge to różnica między workflow, które działa tylko podczas testów, a takim, które bezawaryjnie przetwarza dane na produkcji.


Summary in English

This article provides an in-depth technical analysis of the Merge node in n8n, focusing on its internal mechanics and memory management within the Node.js environment. It explores the operational differences and architectural implications of the Append, Combine (zipping), and Match by fields (SQL Join) modes. The text highlights critical pitfalls, such as order drift in Combine mode and schema heterogeneity in Append mode. Furthermore, it addresses the common issue of execution halts caused by empty inputs in DAG-based workflows, offering practical solutions like the 'Always Output Data' setting. Finally, it provides best practices for performance optimization, including when to offload heavy data joins to an external relational database.

#n8n #automatyzacja #workflow #integracje #selfhosting #dane