Przejdź do treści

Eksport GA4 do BigQuery: 10 zapytań, które warto znać

Surowe dane GA4 w BigQuery dają odpowiedzi, których nie pokaże interfejs, ale tylko wtedy, gdy umiesz je wyciągnąć. Oto 10 zapytań SQL, od kontroli jakości po lejek i źródła ruchu, z opisem pułapek.

Redakcja Pixel Insider9 min czytaniaPoziom: zaawansowany

  • GA4
  • BigQuery
  • SQL
Spis treści
  1. Jak zbudowane są dane GA4 w BigQuery
  2. Jak bezpiecznie uruchamiać zapytania GA4 w BigQuery
  3. Zapytania 1–3: zdarzenia, użytkownicy, sesje i strony
  4. Zapytania 4–7: strony docelowe, transakcje, duplikaty i produkty
  5. Zapytania 8–10: źródła ruchu, lejek i pierwsze pozyskanie
  6. Dlaczego wyniki z BigQuery różnią się od raportów GA4
  7. Co dalej: od pojedynczych zapytań do tabel i raportów
  8. Najczęściej zadawane pytania

Zapytania SQL do eksportu GA4 w BigQuery pozwalają policzyć użytkowników, sesje, przychód i ścieżki zakupowe na surowych, niepróbkowanych danych zdarzeń, bez progów i limitów interfejsu. Żeby z nich korzystać, musisz zrozumieć trzy rzeczy: dane są zapisane w dziennych tabelach events_RRRRMMDD, parametry zdarzeń siedzą w zagnieżdżonej tablicy event_params, a sesję trzeba złożyć samodzielnie z identyfikatora użytkownika i ga_session_id. Poniżej znajdziesz 10 zapytań, które pokrywają większość codziennych potrzeb analityka e-commerce, wraz z opisem, kiedy ich używać i na co uważać.

Najważniejsze w skrócie

  • Eksport GA4 tworzy osobną tabelę na każdy dzień, więc zakres dat zawsze ograniczaj filtrem _TABLE_SUFFIX, a nie tylko kolumną event_date.
  • Wartość parametru zdarzenia wyciągasz podzapytaniem (SELECT value.string_value FROM UNNEST(event_params) WHERE key = '...').
  • Sesja to połączenie user_pseudo_id i parametru ga_session_id; samo ga_session_id nie jest unikalne między użytkownikami.
  • Źródło i medium sesji najprościej pobrać z rekordu session_traffic_source_last_click, a pierwsze pozyskanie użytkownika z rekordu traffic_source.
  • Wyniki z BigQuery różnią się od raportów GA4 o kilka procent z powodu modelowania, szacowania unikalnych wartości i progów – to normalne.

Jak zbudowane są dane GA4 w BigQuery

Każdy wiersz w tabeli eksportu to jedno zdarzenie: page_view, add_to_cart, purchase albo dowolne zdarzenie niestandardowe. Wiersz zawiera stałe kolumny (event_date, event_timestamp, event_name, user_pseudo_id, rekordy device, geo, ecommerce) oraz tablice, które trzeba rozwinąć: event_params z parametrami zdarzenia, user_properties z właściwościami użytkownika i items z produktami. Jeśli dopiero konfigurujesz połączenie, zacznij od poradnika, jak połączyć GA4 z BigQuery i wybrać rodzaj eksportu.

Tabele mają nazwy z datą, np. events_20261026. Przy eksporcie strumieniowym pojawia się też tabela events_intraday_20261027 z bieżącym dniem. Zapytanie do events_* obejmuje oba typy tabel, dlatego zakres dat wpisany jako cyfry (BETWEEN '20261001' AND '20261026') przy okazji wyklucza tabele intraday – ich sufiks zaczyna się od litery. To prosty sposób, żeby nie policzyć tego samego dnia dwa razy.

Dwie kolumny czasu działają inaczej. event_date to tekst w formacie RRRRMMDD w strefie czasowej usługi GA4, a event_timestamp to liczba mikrosekund w UTC. Jeśli łączysz dane z systemem zamówień, który zapisuje czas lokalny, przelicz znacznik funkcją TIMESTAMP_MICROS() i ustaw strefę Europe/Warsaw.

Jak bezpiecznie uruchamiać zapytania GA4 w BigQuery

BigQuery w modelu na żądanie nalicza opłatę za przeskanowane dane, a nie za liczbę zwróconych wierszy. Dobre nawyki od pierwszego dnia chronią budżet i przyspieszają pracę. Szczegółową strategię opisujemy w tekście o tym, jak trzymać koszty BigQuery pod kontrolą.

  1. Zawsze ogranicz zakres tabel. Filtr _TABLE_SUFFIX sprawia, że BigQuery czyta tylko wskazane dni. Warunek na event_date bez sufiksu nadal skanuje wszystkie tabele.
  2. Wybieraj tylko potrzebne kolumny. BigQuery przechowuje dane kolumnowo, więc SELECT * czyta wszystko, łącznie z ciężką tablicą event_params.
  3. Sprawdź szacunek przed uruchomieniem. Edytor pokazuje w prawym górnym rogu, ile danych przetworzy zapytanie. Jeśli liczba jest zaskakująco duża, popraw filtr.
  4. Nie licz na LIMIT. Ograniczenie liczby wierszy w wyniku nie zmniejsza ilości skanowanych danych.
  5. Zapisuj gotowe zapytania jako widoki lub zapytania zaplanowane. Dzięki temu zespół korzysta z jednej, sprawdzonej definicji metryki zamiast kopiować kod z czatu.

W przykładach używamy tabeli twoj-projekt.analytics_123456789.events_*. Podmień nazwę projektu i identyfikator usługi na własne.

Zapytania 1–3: zdarzenia, użytkownicy, sesje i strony

1. Przegląd zdarzeń z ostatnich 7 dni

To pierwsze zapytanie, które warto uruchomić po każdej zmianie w tagowaniu. Pokazuje, czy wszystkie zdarzenia docierają i czy nie pojawiły się nowe, nieplanowane nazwy, np. Purchase obok purchase.

SELECT
  event_name,
  COUNT(*) AS zdarzenia,
  COUNT(DISTINCT user_pseudo_id) AS uzytkownicy
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN
    FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('Europe/Warsaw'), INTERVAL 7 DAY))
    AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE('Europe/Warsaw'), INTERVAL 1 DAY))
GROUP BY event_name
ORDER BY zdarzenia DESC;

2. Użytkownicy i sesje dzień po dniu

Sesję identyfikuje para user_pseudo_id i ga_session_id. Funkcja CONCAT zwraca wartość pustą, gdy któryś element jest pusty, a COUNT(DISTINCT) pomija takie wiersze, więc zdarzenia bez identyfikatora (np. pingi bez plików cookie) nie zawyżą wyniku.

SELECT
  PARSE_DATE('%Y%m%d', event_date) AS data,
  COUNT(DISTINCT user_pseudo_id) AS uzytkownicy,
  COUNT(DISTINCT CONCAT(user_pseudo_id, '.', CAST(
    (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id')
    AS STRING))) AS sesje
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
GROUP BY data
ORDER BY data;

Pełne odtworzenie metryk sesji, łącznie z sesjami z zaangażowaniem i współczynnikiem zaangażowania, pokazujemy w artykule o sesjach i źródłach ruchu w SQL.

3. Najczęściej oglądane strony

Parametr page_location zawiera pełny adres razem z parametrami UTM i identyfikatorami kliknięć. Wyrażenie regularne obcina wszystko od znaku zapytania, dzięki czemu jedna strona nie rozpada się na setki wierszy.

SELECT
  REGEXP_REPLACE(
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location'),
    r'\?.*$', '') AS strona,
  COUNT(*) AS odslony,
  COUNT(DISTINCT user_pseudo_id) AS uzytkownicy
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
  AND event_name = 'page_view'
GROUP BY strona
ORDER BY odslony DESC
LIMIT 50;

Zapytania 4–7: strony docelowe, transakcje, duplikaty i produkty

4. Strony docelowe sesji

Strona docelowa to pierwsza odsłona w sesji. Funkcja okna ROW_NUMBER() numeruje odsłony według czasu, a klauzula QUALIFY zostawia tylko pierwszą.

WITH odslony AS (
  SELECT
    CONCAT(user_pseudo_id, '.', CAST(
      (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id')
      AS STRING)) AS sesja_id,
    event_timestamp,
    REGEXP_REPLACE(
      (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location'),
      r'\?.*$', '') AS strona
  FROM `twoj-projekt.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
    AND event_name = 'page_view'
),
pierwsze AS (
  SELECT sesja_id, strona
  FROM odslony
  WHERE sesja_id IS NOT NULL
  QUALIFY ROW_NUMBER() OVER (PARTITION BY sesja_id ORDER BY event_timestamp) = 1
)
SELECT strona AS strona_docelowa, COUNT(*) AS sesje
FROM pierwsze
GROUP BY strona_docelowa
ORDER BY sesje DESC
LIMIT 50;

BigQuery wymaga, żeby zapytanie z QUALIFY miało też klauzulę WHERE, GROUP BY lub HAVING – tutaj tę rolę pełni filtr pustych identyfikatorów sesji. W wersji produkcyjnej dodaj grupowanie według typu urządzenia z rekordu device.category, bo strony docelowe na telefonach i komputerach często wyglądają zupełnie inaczej.

5. Transakcje i przychód dziennie

Rekord ecommerce jest wypełniany dla zdarzeń zakupowych. Liczba transakcji to liczba unikalnych transaction_id, a przychód pochodzi z pola purchase_revenue w walucie lokalnej usługi.

SELECT
  PARSE_DATE('%Y%m%d', event_date) AS data,
  COUNT(DISTINCT ecommerce.transaction_id) AS transakcje,
  ROUND(SUM(ecommerce.purchase_revenue), 2) AS przychod,
  ROUND(SAFE_DIVIDE(SUM(ecommerce.purchase_revenue),
    COUNT(DISTINCT ecommerce.transaction_id)), 2) AS srednia_wartosc_zamowienia
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
  AND event_name = 'purchase'
GROUP BY data
ORDER BY data;

Porównaj wynik z systemem sklepu. Jeśli suma przychodu jest wyraźnie wyższa niż w panelu sklepu, a liczba transakcji się zgadza, najpewniej masz zduplikowane zdarzenia zakupu.

6. Duplikaty transakcji

To zapytanie wyłapuje identyfikatory transakcji, które pojawiły się więcej niż raz, np. po odświeżeniu strony podziękowania.

SELECT
  ecommerce.transaction_id,
  COUNT(*) AS liczba_zdarzen,
  MIN(TIMESTAMP_MICROS(event_timestamp)) AS pierwsze,
  MAX(TIMESTAMP_MICROS(event_timestamp)) AS ostatnie
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
  AND event_name = 'purchase'
GROUP BY ecommerce.transaction_id
HAVING COUNT(*) > 1
ORDER BY liczba_zdarzen DESC;

Odstęp między pierwszym a ostatnim zdarzeniem podpowiada przyczynę: kilka sekund to zwykle podwójne wywołanie tagu, kilka godzin – powrót na stronę potwierdzenia z historii przeglądarki. Jak usunąć problem u źródła, opisujemy w tekście o duplikatach transakcji w GA4.

7. Przychód według produktów

Tablicę items rozwijasz przez UNNEST w klauzuli FROM. Każdy produkt z zamówienia staje się wtedy osobnym wierszem.

SELECT
  i.item_name AS produkt,
  SUM(i.quantity) AS sztuki,
  ROUND(SUM(i.item_revenue), 2) AS przychod
FROM `twoj-projekt.analytics_123456789.events_*`, UNNEST(items) AS i
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
  AND event_name = 'purchase'
GROUP BY produkt
ORDER BY przychod DESC
LIMIT 50;

Jakość wyniku zależy od tego, co sklep wysyła w warstwie danych. Jeśli nazwy produktów są niespójne albo brakuje item_id, wróć do specyfikacji dataLayer dla e-commerce GA4.

Zapytania 8–10: źródła ruchu, lejek i pierwsze pozyskanie

8. Źródło i medium sesji z przychodem

Rekord session_traffic_source_last_click zawiera atrybucję sesji według ostatniego kliknięcia i jest dostępny dla każdego zdarzenia. Podrekord cross_channel_campaign łączy kampanie Google Ads i kampanie opisane UTM-ami, więc najbliżej mu do wymiaru „Źródło/medium sesji” z interfejsu.

SELECT
  CONCAT(
    IFNULL(session_traffic_source_last_click.cross_channel_campaign.source, '(not set)'),
    ' / ',
    IFNULL(session_traffic_source_last_click.cross_channel_campaign.medium, '(not set)')
  ) AS zrodlo_medium,
  COUNT(DISTINCT CONCAT(user_pseudo_id, '.', CAST(
    (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id')
    AS STRING))) AS sesje,
  COUNT(DISTINCT IF(event_name = 'purchase', ecommerce.transaction_id, NULL)) AS transakcje,
  ROUND(SUM(IF(event_name = 'purchase', ecommerce.purchase_revenue, 0)), 2) AS przychod
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
GROUP BY zrodlo_medium
ORDER BY sesje DESC;

Pola atrybucji sesji dodano do eksportu w 2024 roku, więc w starszych tabelach będą puste. Dużo wartości „(not set)” w nowych danych to sygnał problemów z tagowaniem, które omawiamy w poradniku o przyczynach „(not set)” w GA4.

9. Lejek zakupowy na poziomie sesji

Zapytanie oznacza każdą sesję flagami: czy był widok produktu, dodanie do koszyka, rozpoczęcie płatności i zakup. To lejek otwarty, czyli bez wymogu kolejności kroków, co zwykle wystarcza do oceny, gdzie odpada najwięcej osób.

WITH sesje AS (
  SELECT
    CONCAT(user_pseudo_id, '.', CAST(
      (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id')
      AS STRING)) AS sesja_id,
    LOGICAL_OR(event_name = 'view_item') AS produkt,
    LOGICAL_OR(event_name = 'add_to_cart') AS koszyk,
    LOGICAL_OR(event_name = 'begin_checkout') AS platnosc,
    LOGICAL_OR(event_name = 'purchase') AS zakup
  FROM `twoj-projekt.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20261001' AND '20261026'
  GROUP BY sesja_id
)
SELECT
  COUNT(*) AS wszystkie_sesje,
  COUNTIF(produkt) AS z_produktem,
  COUNTIF(koszyk) AS z_koszykiem,
  COUNTIF(platnosc) AS z_platnoscia,
  COUNTIF(zakup) AS z_zakupem,
  ROUND(100 * SAFE_DIVIDE(COUNTIF(zakup), COUNTIF(koszyk)), 1) AS koszyk_do_zakupu_proc
FROM sesje
WHERE sesja_id IS NOT NULL;

10. Kanał pierwszego pozyskania a przychód

Rekord traffic_source opisuje źródło, z którego użytkownik trafił do serwisu po raz pierwszy, i nie zmienia się w kolejnych wizytach. To dobry punkt wyjścia do pytania, które kanały przyprowadzają klientów wartych najwięcej. Rekord nie jest wypełniany w tabelach intraday, więc pracuj na tabelach dziennych.

SELECT
  CONCAT(traffic_source.source, ' / ', traffic_source.medium) AS pierwsze_pozyskanie,
  COUNT(DISTINCT user_pseudo_id) AS uzytkownicy,
  COUNT(DISTINCT IF(event_name = 'purchase', ecommerce.transaction_id, NULL)) AS transakcje,
  ROUND(SUM(IF(event_name = 'purchase', ecommerce.purchase_revenue, 0)), 2) AS przychod,
  ROUND(SAFE_DIVIDE(SUM(IF(event_name = 'purchase', ecommerce.purchase_revenue, 0)),
    COUNT(DISTINCT user_pseudo_id)), 2) AS przychod_na_uzytkownika
FROM `twoj-projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260701' AND '20260930'
GROUP BY pierwsze_pozyskanie
HAVING uzytkownicy >= 100
ORDER BY przychod DESC;

Załóżmy hipotetycznie, że kanał google / cpc przyprowadził w kwartale 8000 użytkowników i 160 000 zł przychodu, a newsletter 1500 użytkowników i 60 000 zł. Przychód na użytkownika wynosi wtedy 20 zł i 40 zł, co jest argumentem do rozmowy o inwestycji w bazę mailingową. Pamiętaj, że to analiza korelacyjna; o tym, czy kanał faktycznie generuje dodatkową sprzedaż, rozstrzygają testy i modele opisane w podtemacie o atrybucji i inkrementalności.

Dlaczego wyniki z BigQuery różnią się od raportów GA4

Niewielkie rozbieżności między SQL a interfejsem są normalne i wynikają z innego sposobu przetwarzania danych. Zanim uznasz zapytanie za błędne, sprawdź przyczyny z tabeli.

Przyczyna Co się dzieje Jak to sprawdzić
Szacowanie unikalnych wartości Interfejs liczy użytkowników i sesje algorytmem przybliżonym, BigQuery dokładnie Różnica rzędu kilku procent przy dużym ruchu
Modelowanie zgód Raporty mogą zawierać dane modelowane przy Consent Mode, eksport ma tylko zebrane zdarzenia Porównaj dni przed i po zmianie banera
Progi danych Interfejs ukrywa wiersze z małą liczbą użytkowników, BigQuery ich nie ukrywa Wiersze widoczne tylko w SQL
Strefa czasowa event_date jest lokalne, event_timestamp w UTC Wyniki godzinowe przesunięte o 1–2 godziny
Tożsamość raportowania Interfejs może łączyć urządzenia przez User-ID, eksport opiera się na user_pseudo_id Więcej użytkowników w SQL niż w GA4
Późne zdarzenia Tabela dzienna może się aktualizować jeszcze przez kilka dni Wynik za wczoraj rośnie po ponownym uruchomieniu

Jeśli różnica jest większa niż kilka procent, poszukaj błędów w samym zapytaniu: brakującego filtra sufiksu, podwójnego liczenia przez UNNEST(items) w zapytaniu o sesje albo połączenia z tabelą intraday. Ogólne przyczyny niezgodności między narzędziami omawiamy w artykule o tym, dlaczego GA4, Google Ads i Meta pokazują inną liczbę konwersji. Wpływ trybów zgody na kompletność danych opisuje tekst o Consent Mode w trybie podstawowym i zaawansowanym.

Co dalej: od pojedynczych zapytań do tabel i raportów

Pojedyncze zapytania sprawdzają się w analizach ad hoc, ale przy regularnym raportowaniu lepiej zbudować z nich tabele pośrednie: dzienną tabelę sesji, tabelę transakcji i tabelę produktów. Zapytanie zaplanowane dopisuje do nich co rano jeden dzień, a raporty korzystają z małych, zagregowanych tabel zamiast skanować surowy eksport.

Na takich tabelach łatwo postawić dashboard e-commerce w Looker Studio albo policzyć wskaźniki rentowności opisane w artykule o ROAS, POAS i MER. Jeśli SQL jest dla Ciebie nowy, część zapytań możesz wygenerować z opisu w języku polskim – możliwości i ograniczenia tej metody opisujemy w tekście o Gemini w BigQuery. Wszystkie poradniki o pracy z eksportem znajdziesz w podtemacie BigQuery i SQL, a opis samego narzędzia na stronie BigQuery.

Najczęściej zadawane pytania

Czy zapytania do eksportu GA4 w BigQuery są płatne?

Tak, w modelu na żądanie BigQuery nalicza opłatę za ilość przeskanowanych danych, ale pierwszy 1 TiB miesięcznie jest bezpłatny. Dla małego lub średniego sklepu dobrze napisane zapytania z filtrem dat zwykle mieszczą się w tym limicie. Koszty rosną głównie przy zapytaniach bez filtra _TABLE_SUFFIX i przy dashboardach podpiętych bezpośrednio do surowych tabel.

Jak wyciągnąć wartość parametru zdarzenia w BigQuery?

Parametry zdarzeń są zapisane w tablicy event_params, więc wartość pobierasz podzapytaniem z UNNEST, np. (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location'). Dla liczb użyj value.int_value lub value.double_value. Jeśli nie wiesz, w którym polu jest wartość, sprawdź najpierw kilka wierszy z rozwiniętą tablicą.

Dlaczego liczba użytkowników w BigQuery jest inna niż w GA4?

Interfejs GA4 szacuje liczbę unikalnych użytkowników algorytmem przybliżonym, może uwzględniać dane modelowane i łączyć urządzenia przez User-ID. Eksport zawiera tylko zebrane zdarzenia i identyfikator user_pseudo_id, liczony dokładnie. Różnice rzędu kilku procent są normalne, większe zwykle oznaczają błąd w zapytaniu lub zakresie dat.

Czy w BigQuery są dane historyczne sprzed połączenia z GA4?

Nie. Eksport obejmuje dane od dnia utworzenia połączenia i GA4 nie przesyła historii wstecz. Dlatego połączenie warto uruchomić jak najwcześniej, nawet jeśli analizy w SQL planujesz dopiero za kilka miesięcy. Starsze dane możesz najwyżej pobrać w formie zagregowanej przez API, ale nie będą to surowe zdarzenia.

Jak policzyć sesje w eksporcie GA4?

Połącz user_pseudo_id z wartością parametru ga_session_id i policz unikalne kombinacje funkcją COUNT(DISTINCT ...). Samo ga_session_id to znacznik czasu rozpoczęcia sesji, więc u różnych użytkowników może się powtarzać. Zdarzenia bez identyfikatora użytkownika, np. pingi wysyłane bez zgody, nie utworzą sesji w takim liczeniu.