Einwilligungsstatus je Ereignis im GA4-BigQuery-Export auslesen

Inhalt
Die Einwilligung taucht in GA4-Berichten als Zusammenfassung auf, wenn überhaupt. Im BigQuery-Export taucht sie je Ereignis auf, in einem verschachtelten Satz neben jeder Zeile, der den Zustand hält, welcher im Augenblick der Aufzeichnung dieses einen Ereignisses galt.
Das ist ein deutlich besserer Gegenstand zum Arbeiten, und er bringt eine Falle mit, die eine falsche Zahl erzeugt statt eines Fehlers.

Wo er steht
Der Satz heißt privacy_info und hält die Speicherzustände als Zeichenketten, nicht als Wahrheitswerte. Weil er verschachtelt ist, braucht er Punktzugriff; weil es Zeichenketten sind, braucht er Vergleiche in Anführungszeichen; und weil sie leer sein können, braucht er vor jeder Quote eine Entscheidung darüber, was ein fehlender Wert bedeutet.
SELECT
event_date,
privacy_info.analytics_storage AS analytics,
privacy_info.ads_storage AS ads,
COUNT(*) AS ereignisse
FROM `projekt.analytics_XXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260801' AND '20260831'
GROUP BY 1, 2, 3
ORDER BY 1, 4 DESC
-- vor jedem Filter laufen lassen. Der Zweck ist zu sehen,
-- wie viele Zeilen in den dritten Topf fallen, denn diese
-- Zahl entscheidet, ob die Falle unten ueberhaupt greift
Erst gruppieren und dann filtern ist hier die ganze Disziplin. Das kostet eine Abfrage, auf einem einzelnen Monat fast nichts, und beantwortet die Frage, von der jede weitere Abfrage abhängt.
Die Falle
Zwei Filter, die sich wie Gegensätze lesen, sind keine. analytics_storage = 'No' wählt Ereignisse mit ausdrücklicher Ablehnung. analytics_storage != 'Yes' wählt Ablehnungen plus alles ohne festgehaltenen Zustand – denn in SQL ergibt ein Vergleich gegen einen Leerwert wieder einen Leerwert und nicht wahr, sodass der erste Filter die leeren Zeilen still fallen lässt und der zweite sie behält.
Eine Einwilligungsquote auf dem ersten fällt höher aus als eine auf dem zweiten, auf denselben Daten, für denselben Tag. Von außen ist keine der beiden offensichtlich falsch; beide liefern einen plausiblen Prozentsatz. Welche stimmt, hängt davon ab, was die Leerwerte tatsächlich sind, und das ist eine Frage an die Umsetzung, nicht an SQL.
Leerwerte bedeuten meistens, dass beim Auslösen des Ereignisses kein Einwilligungssignal beim Tag ankam – ein Treffer vor der Entscheidung am Banner, eine Seite, auf der die Einwilligungsplattform nicht lud, ein serverseitiges Ereignis, das ohne durchgereichten Zustand gebaut wurde. Das sind drei verschiedene Lagen mit drei verschiedenen Bedeutungen, und sie mit ausdrücklichen Ablehnungen in einen Topf zu werfen, überzeichnet die Ablehnung.
Was damit möglich wird
Weil der Zustand je Ereignis und nicht je Bestand vorliegt, lässt er sich mit allem anderen auf der Zeile kreuzen.
nach Seite welche Seiten am haeufigsten abgelehnt werden
meist die, auf denen das Banner mit dem Inhalt
um dieselbe Bildschirmflaeche ringt
nach Geraet Mobil und Rechner unterscheiden sich weit
genug, dass eine Gesamtzahl keines von beiden
beschreibt
nach Quelle Verkehr aus einem Link innerhalb einer App
verhaelt sich anders als ein Direktbesuch
nach Stunde eine Auslieferung, die das Banner zerlegt,
zeigt sich als Stufe zu einer bestimmten
Stunde - was ein Tageswert vollstaendig
glaettet
Der Schnitt nach Seite lohnt sich am schnellsten, weil er aus einer Zahl, mit der niemand etwas anfangen kann, eine Liste konkreter Seiten macht.
Zwei Dinge, die über die Zeile zu wissen sind
Ein einzelner Besuch kann Ereignisse in mehr als einem Zustand enthalten. Wer ein Banner mitten in einer Sitzung annimmt, hinterlässt unter derselben Pseudokennung eine Spur abgelehnter Ereignisse, gefolgt von erteilten – was richtig ist und jede Abfrage leise zerlegt, die einen Zustand je Nutzer annimmt.
Und das Exportschema wächst. Der Ereignistabelle wurden schon Felder hinzugefügt und werden wieder welche hinzugefügt, was das Argument gegen SELECT * in allem Nachgelagerten ist: Eine unangekündigt eintreffende Spalte ist harmlos für eine Abfrage, die ihre Spalten benennt, und ein gebrochener Ladeauftrag für eine, die es nicht tut.
Fragen und Antworten
Warum enthält ein Export manchmal keine einzige Ablehnung?
Meist, weil der Consent Mode im einfachen Modus läuft. Dort lädt das Tag erst nach der Einwilligung; wer ablehnt, sendet nichts, und im Export stehen nur Ereignisse mit erteilter Einwilligung oder ohne festgehaltenen Zustand. Eine Einwilligungsquote lässt sich aus einem solchen Export nicht bilden, sie muss dann aus den Aufzeichnungen der Einwilligungsplattform kommen.
Wie lassen sich die Leerwerte den drei Lagen aus dem Artikel zuordnen?
Über die übrigen Felder derselben Zeile, denn jede der drei Lagen hinterlässt ein eigenes Muster:
- Treffer vor der Entscheidung am Banner ballen sich am Anfang einer Sitzung. Werden die Ereignisse je Sitzung nach event_timestamp geordnet, stehen die Leerwerte vorn, gefolgt von Ereignissen mit festgehaltenem Zustand.
- Eine Seite, auf der die Einwilligungsplattform nicht lud, zeigt sich im Schnitt nach Seite oder nach Stunde: Leerwerte gehäuft auf bestimmten Seiten oder Seitenvorlagen oder als Stufe ab dem Zeitpunkt einer Auslieferung.
- Serverseitige Ereignisse ohne durchgereichten Zustand lassen sich am Ereignisnamen erkennen, wenn nur der Server sie sendet; sie sind dann durchgehend leer, unabhängig von Seite und Uhrzeit.
Für die Abfrage selbst hilft es, den dritten Topf sichtbar zu halten, statt ihn dem Vergleich zu überlassen: COALESCE(privacy_info.analytics_storage, 'leer') macht aus dem Leerwert eine eigene Ausprägung, die in jeder Gruppierung und jedem Filter ausdrücklich erscheint. Die Entscheidung, was „leer“ bedeutet, fällt dann einmal an einer Stelle und nicht stillschweigend in jedem WHERE.
Warum grenzt die Abfrage über _TABLE_SUFFIX ab und nicht allein über event_date?
Aus zwei Gründen. Erstens rechnet BigQuery Abfragen im On-Demand-Tarif nach gelesenen Daten ab, und nur ein Filter auf _TABLE_SUFFIX bestimmt, welche Tagestabellen überhaupt gelesen werden; ein Filter allein auf event_date liest zunächst jede Tabelle, die der Platzhalter erfasst, und verwirft die Zeilen erst danach.
Zweitens erfasst events_* auch die Tabellen des Streaming-Exports, events_intraday_…, deren Suffix mit intraday_ beginnt. Ein Bereich aus Datumswerten wie ‘20260801’ bis ‘20260831’ schließt sie aus. Reicht ein Zeitraum bis an den laufenden Tag heran, liest ein Filter nur auf event_date dagegen deren vorläufige Zeilen mit.