BigQuery

BigQuery’de GA4 oturum sayısını doğru hesaplamak: arayüzle neden hiç tutmuyor?

ga_session_id tek başına oturum değildir. GA4 BigQuery export verisinden oturumu doğru saymanın yolu ve arayüzle aradaki farkın gerçek kaynakları.

GA4 BigQuery export verisinde oturum sayısını doğru hesaplamak için user_pseudo_id ile ga_session_id değerlerini birleştirip bu anahtarı tekil olarak saymanız gerekir, çünkü ga_session_id tek başına yalnızca kullanıcı içinde tekildir. Bu doğru sayı bile arayüzle birebir tutmaz: GA4 arayüzü oturumları HyperLogLog++ ile tahmin eder, bazı raporlarda eşik uygular, Consent Mode ile modellenmiş veri ekler ve gün sınırlarını sizin sorgunuzdan farklı ele alır. Hedef birebir eşleşme değil, farkın her kalemini açıklayabilen bir oturum modelidir.

Bu yazıda önce en sık yapılan hatayı, sonra farkın kaynaklarını, en sonda da sahada işe yarayan bir oturum tablosu modelini ele alıyorum.

ga_session_id aslında ne tutuyor?

GA4’te oturum, session_start event’i ile başlar ve bu oturuma ait tüm event’lere ga_session_id parametresi eklenir. Bu değer bir sıra numarası değil, oturumun başladığı anın Unix zaman damgasıdır, saniye cinsinden. Yani 1788350400 gibi bir değer gördüğünüzde bu, oturumun hangi saniyede başladığını söyler.

Buradan çıkan sonuç basit ama sık atlanıyor: günde birkaç yüz bin oturum alan bir sitede, aynı saniyede oturum başlatan onlarca kullanıcı olabilir. Hepsinin ga_session_id değeri aynıdır. COUNT(DISTINCT ga_session_id) yazdığınızda bu oturumları tek bir oturum gibi sayarsınız ve sonuç gerçek değerin altında kalır. Trafik arttıkça hata da büyür, dolayısıyla küçük bir test property’sinde fark etmediğiniz sorun canlı property’de ciddi bir sapmaya dönüşür.

Oturumun gerçek kimliği iki alanın birleşimidir: user_pseudo_id cihazı ve tarayıcıyı, ga_session_id o cihazdaki belirli oturumu temsil eder. Resmi GA4 BigQuery export şeması da ga_session_id alanını event parametreleri içinde tanımlar, yani ona ulaşmak için event_params dizisini UNNEST etmeniz gerekir.

session_start event’ini saymak neden yanıltır?

İlk akla gelen alternatif session_start event’lerini saymak olur. Bu yaklaşım da güvenilir değildir. Measurement Protocol ile gönderilen event’ler, consent sonrası geç tetiklenen etiketler, yanlış yapılandırılmış GTM kurulumları veya server-side tarafta filtrelenen istekler yüzünden bazı oturumlarda session_start hiç bulunmaz. Bazı durumlarda ise tek oturum için birden fazla session_start görebilirsiniz. Oturumu event’in varlığına değil, anahtarın tekilliğine dayandırmak daha sağlamdır.

Doğru sayım: anahtarı kurmak ve karşılaştırmak

Aşağıdaki sorgu aynı veri üzerinde üç farklı sayımı yan yana koyar: tam tekil sayım, arayüzün kullandığı yönteme yakın bir HyperLogLog++ tahmini ve hatalı naif sayım. Bunları bir arada görmek, ekibe farkı anlatırken tartışmayı hızla sonlandırır.

DECLARE start_date STRING DEFAULT '20260801';
DECLARE end_date STRING DEFAULT '20260831';

WITH events AS (
  SELECT
    user_pseudo_id,
    (SELECT value.int_value FROM UNNEST(event_params)
      WHERE key = 'ga_session_id') AS ga_session_id,
    privacy_info.analytics_storage AS analytics_storage
  FROM `project.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN start_date AND end_date
),
keyed AS (
  SELECT
    *,
    CONCAT(user_pseudo_id, '.', CAST(ga_session_id AS STRING)) AS session_key
  FROM events
)
SELECT
  COUNT(DISTINCT session_key) AS sessions_exact,
  HLL_COUNT.EXTRACT(HLL_COUNT.INIT(session_key, 12)) AS sessions_hll,
  COUNT(DISTINCT ga_session_id) AS sessions_naive,
  COUNTIF(session_key IS NULL) AS events_without_key,
  COUNTIF(analytics_storage = 'No') AS events_consent_denied
FROM keyed;

Sorguda dikkat edilmesi gereken birkaç ayrıntı var. CONCAT fonksiyonu argümanlardan biri NULL olduğunda NULL döndürür ve COUNT(DISTINCT ...) NULL değerleri saymaz. Bu istediğimiz davranıştır, ama kaç event’in bu yüzden dışarıda kaldığını görmek için events_without_key sütununu ayrıca tutuyoruz. Ayrıca events_* joker karakteri intraday tablolarını da yakalar, fakat bu tabloların _TABLE_SUFFIX değeri intraday_20260801 biçiminde olduğu için tarih aralığı filtresi onları doğal olarak dışarıda bırakır. Filtreyi kaldırırsanız aynı günü iki kez okumuş olursunuz.

HyperLogLog++ satırında hassasiyet olarak 12 kullandım. Google, arayüzdeki oturum metriği için bu seviyeyi kullandığını belgeliyor. Bu hassasiyette göreli standart hata yaklaşık yüzde 1,6 civarındadır, yani büyük hacimli bir property’de yalnızca tahmin yönteminden kaynaklanan birkaç yüz, hatta birkaç bin oturumluk fark tamamen olağandır.

Arayüzle BigQuery neden ayrışır?

Farkın tek bir nedeni olmadığı için tek bir düzeltmesi de yoktur. Aşağıdaki tablo sahada en sık karşılaştığım kaynakları, tipik etkilerini ve ne yapılması gerektiğini özetliyor. Büyüklükler kurulumdan kuruluma değişir, bunları mutlak değer değil yön gösteren bir referans olarak okuyun.

Fark kaynağıTipik etkiNe yapmalı
HyperLogLog++ tahminiYüzde 1-2 civarı, her iki yöndeHLL sorgusuyla arayüz değerini yeniden üretin
Consent Mode modellemesiOnay oranına göre yüzde 5-30 arası arayüz lehineRaporlama kimliğini ve modellemenin açık olup olmadığını kontrol edin
Veri eşikleriKüçük segmentlerde satırların tamamen kaybolmasıKarşılaştırmayı eşik uygulanmayan toplam düzeyde yapın
Intraday ve gecikmeli event’lerSon 1-3 günde BigQuery eksik görünürSon 72 saati kesinleşmemiş kabul edin
Gece yarısını geçen oturumlarGünlük toplamlarda BigQuery fazla görünürOturumu başlangıç tarihine atayın

Advanced Consent Mode kullanan bir sitede analytics depolaması reddedildiğinde GA4 çerezsiz ping’ler gönderir. Bu ping’ler BigQuery’ye düşer ancak user_pseudo_id genellikle boştur ve oturumu birbirine bağlayacak bir kimlik yoktur. Arayüz ise raporlama kimliği ayarına göre bu boşluğu davranışsal modelleme ile doldurabilir. Sonuç olarak onay oranı düşük olan pazarlarda arayüz, BigQuery’nin asla gösteremeyeceği oturumları raporlar. BigQuery bu veriyi hiçbir zaman içermez, dolayısıyla SQL tarafında bunu düzeltmeye çalışmak anlamsızdır. Yapılması gereken, privacy_info.analytics_storage alanını izleyip farkın ne kadarının onaydan geldiğini ayrı bir metrik olarak raporlamaktır.

Eşikler ve tahmin

Google signals veya demografik boyutlar devredeyken arayüz, kullanıcıların tanınmasını engellemek için bazı satırları gizler. BigQuery’de eşik yoktur. Küçük bir kampanya veya şehir bazında karşılaştırma yapıyorsanız arayüzdeki eksik satırlar farkı olduğundan büyük gösterir. Karşılaştırmayı her zaman önce property toplamında yapın, segment düzeyine ancak toplam açıklandıktan sonra inin.

Zaman ve gün sınırları: intraday, günlük tablolar ve gece yarısı

BigQuery export’u iki tür tablo üretir. events_intraday_YYYYMMDD gün içinde sürekli dolan ve kesinleşmemiş veridir. Gün tamamlandıktan sonra events_YYYYMMDD tablosu oluşur ve intraday tablo silinir. GA4 ayrıca 72 saate kadar gecikmeli gelen event’leri, örneğin çevrimdışı kalıp sonradan veri gönderen uygulama oturumlarını, geçmiş günlük tablolara ekleyebilir. Bu yüzden dünkü tabloyu sabahki raporda kesin kabul etmek, iki gün sonra değişen rakamlarla karşılaşmak anlamına gelir.

Tablolar property saat dilimine göre bölünür. event_date alanı da bu saat dilimindedir, ancak event_timestamp UTC mikrosaniye cinsindendir. İkisini karıştıran sorgular özellikle UTC+3 gibi sıfırdan uzak saat dilimlerinde gün başı ve gün sonu oturumlarını yanlış güne atar.

Gece yarısı ayrı bir sorundur. GA4, Universal Analytics’ten farklı olarak gece yarısında oturumu bölmez. 23:50’de başlayıp 00:15’te biten bir oturumun event’leri iki ayrı günlük tabloya dağılır. Her günü kendi içinde tekil sayıp toplarsanız bu oturum iki kez sayılır. Arayüz de tarih boyutuyla kırıldığında aynı oturumu iki günde gösterebilir, ama tarih aralığının toplamında bir kez sayar. Sizin modeliniz de bu ayrımı açıkça yapmalıdır.

Bir oturum metriği, farkını açıklayamadığınız sürece doğru değildir, yalnızca tesadüfen yakındır. Arayüzle yüzde 3 fark edip nedenini bilen bir tablo, yüzde 0,5 fark edip nedenini bilmeyen bir tablodan her zaman daha değerlidir.

Önerilen oturum tablosu modeli

Her analizde event tablosundan oturumu yeniden türetmek hem pahalı hem de tutarsızlığa açıktır. Farklı ekipler farklı filtrelerle farklı sayılar üretir. Bunun yerine tek bir oturum tablosu kurup tüm raporları bu tablodan beslemek gerekir. Pratikte işe yarayan model şu adımlarla kurulur:

  1. Event tablosundan session_key üretin ve anahtarı NULL olan event’leri ayrı bir consent veya kalite tablosuna yönlendirin.
  2. Oturumu anahtar bazında gruplayın, başlangıç ve bitiş zamanını MIN(event_timestamp) ve MAX(event_timestamp) ile hesaplayın.
  3. Oturum tarihini başlangıç zaman damgasından, property saat dilimine çevirerek türetin.
  4. Tabloyu bu oturum tarihine göre partition’layın ve user_pseudo_id ile cluster’layın.
  5. Her gün son üç günün partition’larını silip yeniden yazın, böylece gecikmeli event’ler de modele girer.

Tablonun sütun seti ise olabildiğince yalın tutulmalı. Benim başlangıç noktası olarak kullandığım minimum alanlar şunlar:

  • session_key: user_pseudo_id ve ga_session_id birleşiminden oluşan birincil anahtar
  • session_date: property saat dilimine göre oturumun başladığı gün
  • session_start_ts ve session_end_ts: UTC zaman damgaları, süre hesabı için
  • is_engaged: session_engaged parametresinin oturum içindeki herhangi bir event’te 1 olup olmadığı
  • landing_page: oturumun ilk page_view event’indeki page_location
  • source_medium: export’taki session_traffic_source_last_click alanından ya da ilk event’in trafik parametrelerinden türetilen kaynak
  • event_count ve has_conversion: temel yoğunluk ve dönüşüm göstergeleri

Attribution mantığını bu tabloya gömmemeye özen gösterin. Kaynak bilgisini ham haliyle saklayıp attribution kurallarını bir üst katmanda uygulamak, kural değiştiğinde tüm geçmişi yeniden hesaplamak zorunda kalmamanızı sağlar. Aynı şekilde oturum tablosunu kullanıcı tablosuyla karıştırmayın. Kullanıcı düzeyindeki metrikler ayrı bir tabloda, oturum tablosundan türetilerek hesaplanmalıdır.

Engaged session tanımı da bu tabloda netleşmelidir. Arayüz bir oturumu 10 saniyeden uzun sürdüyse, en az bir key event içerdiyse veya en az iki sayfa görüntülemesi olduysa engaged kabul eder. Export’ta bu kararın sonucu session_engaged parametresi olarak gelir, ancak her event’te bulunmaz. Oturum içindeki herhangi bir event’te değer 1 ise oturumu engaged işaretlemek, arayüzdeki engagement rate ile en yakın sonucu verir. Bu sütunu kendi süre hesabınızdan türetmeye çalışırsanız, özellikle tek sayfalık ve sekmesi arka planda kalan oturumlarda sistematik bir sapma görürsünüz.

Model canlıya alınmadan önce basit bir doğrulama yapın: oturum tablosundaki toplam event_count değeri, aynı tarih aralığında anahtarı NULL olmayan event sayısına eşit olmalıdır. Eşit değilse ya gruplamada bir satır kaybediyor ya da tarih atamasında aynı oturumu iki partition’a yazıyorsunuz demektir.

Arayüzle mutabakatı nasıl rapor etmeli?

Modeli kurduktan sonra ilk iş, arayüzle düzenli bir mutabakat raporu oluşturmaktır. Haftalık olarak aynı tarih aralığı için arayüzdeki oturum sayısını, oturum tablonuzdaki tam sayımı ve HLL tahminini yan yana koyun. Buna consent reddedilen event oranını ve anahtarı NULL olan event sayısını ekleyin. Fark bir hafta içinde aniden yükselirse genellikle neden tabloların birinde görünür: yeni bir consent banner’ı, bozulan bir GTM tetikleyicisi veya export’un günlük limitine takılan bir property.

Standart property’lerde günlük export limiti olduğunu da unutmayın. Limit aşıldığında BigQuery’ye eksik veri gider ve bu, tablodaki hiçbir kalemle açıklanamayan ani bir düşüş olarak görünür. Mutabakat raporu bu tür sessiz veri kayıplarını fark etmenin en ucuz yoludur.

Son olarak, farkı paydaşlara anlatırken tek bir rakam vermek yerine bir aralık verin. BigQuery’deki sayı gözlemlenen oturumların alt sınırı, arayüzdeki sayı ise modellenmiş verilerle genişletilmiş bir tahmindir. İkisi birbirinin alternatifi değil, aynı gerçekliğin iki farklı görünümüdür. Hangi kararın hangi rakamla verileceğini baştan netleştirmek, ay sonunda iki ekibin iki farklı sayıyla toplantıya girmesini engeller.

Sık sorulan sorular

ga_session_id neden tek başına oturum sayısı için yeterli değil?
ga_session_id oturumun başladığı anın saniye cinsinden zaman damgasıdır ve yalnızca kullanıcı bazında tekildir. Aynı saniyede başlayan farklı kullanıcıların oturumları aynı değeri alır, bu yüzden oturum anahtarı user_pseudo_id ile birlikte kurulmalıdır.
BigQuery’deki oturum sayısı GA4 arayüzündeki sayıyla birebir tutmalı mı?
Hayır, birebir tutması beklenmemelidir. Arayüz HyperLogLog++ tahmini, eşikler ve Consent Mode modellemesi kullanır, BigQuery ise ham event verisini verir. Birkaç puanlık fark normaldir, önemli olan farkın nedenini açıklayabilmektir.
Gece yarısını geçen oturumlar nasıl sayılmalı?
Oturumu başladığı güne atamak en tutarlı yaklaşımdır. Günlük tablolarda ayrı ayrı sayıp toplarsanız aynı oturum iki kez sayılır, bu yüzden önce oturum anahtarı bazında tekilleştirip sonra tarihe bağlamak gerekir.