Spis treści
- Jak zbudowane są dane GA4 w BigQuery
- Jak bezpiecznie uruchamiać zapytania GA4 w BigQuery
- Zapytania 1–3: zdarzenia, użytkownicy, sesje i strony
- Zapytania 4–7: strony docelowe, transakcje, duplikaty i produkty
- Zapytania 8–10: źródła ruchu, lejek i pierwsze pozyskanie
- Dlaczego wyniki z BigQuery różnią się od raportów GA4
- Co dalej: od pojedynczych zapytań do tabel i raportów
- 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_idi parametruga_session_id; samoga_session_idnie 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 rekordutraffic_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ą.
- Zawsze ogranicz zakres tabel. Filtr
_TABLE_SUFFIXsprawia, że BigQuery czyta tylko wskazane dni. Warunek naevent_datebez sufiksu nadal skanuje wszystkie tabele. - Wybieraj tylko potrzebne kolumny. BigQuery przechowuje dane kolumnowo, więc
SELECT *czyta wszystko, łącznie z ciężką tablicąevent_params. - 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.
- Nie licz na
LIMIT. Ograniczenie liczby wierszy w wyniku nie zmniejsza ilości skanowanych danych. - 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.