{"id":10470,"date":"2026-09-19T07:20:00","date_gmt":"2026-09-19T05:20:00","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=10470"},"modified":"2026-10-26T13:15:43","modified_gmt":"2026-10-26T12:15:43","slug":"reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges","status":"publish","type":"post","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/","title":{"rendered":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges"},"content":{"rendered":"<p>A small query result does not necessarily mean a small BigQuery bill. With on-demand query processing, the relevant quantity is the data processed, not the number of returned rows. Selecting the required daily tables and fields can therefore save much more than adding a result limit.<\/p>\n<p>This article concerns queries against the GA4 export. Capacity-based BigQuery pricing uses compute capacity instead of a direct per-query byte charge; storage and data ingestion have separate cost rules. A dry run helps estimate a query, but does not promise an exact final invoice.<\/p>\n<figure class=\"lw-diagram\"><img decoding=\"async\" src=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/diagrams\/dia-10470-review-20261024.png\" width=\"1120\" height=\"580\" loading=\"lazy\" alt=\"Constant suffix range selects 31 of 90 example daily tables; a row-date filter alone leaves all 90 in scope\"><figcaption>Illustration with matching daily data only. Table counts are not an exact cost ratio.<\/figcaption><\/figure>\n<h2>LIMIT is not a reliable scan limit<\/h2>\n<p>Clustering groups stored data by selected columns so that suitable filters can skip unrelated blocks. For a non-clustered table, <code>SELECT * ... LIMIT 10<\/code> does not reduce the bytes read from the selected columns. The ten returned rows can require a large scan. Table preview or <code>bq head<\/code> is better suited to an initial inspection without running a query.<\/p>\n<p>There is an exception: a clustered table can sometimes stop scanning after enough blocks have supplied the limited result. That does not make <code>LIMIT<\/code> a general spending guard. Sorting with <code>ORDER BY<\/code> also requires computation and is not automatically free, even when the selected input columns remain unchanged.<\/p>\n<h2>Table selection and row filtering do different jobs<\/h2>\n<p>For <code>events_*<\/code>, <code>_TABLE_SUFFIX<\/code> contains the part after <code>events_<\/code>. A constant date range restricts the daily tables scanned. The following query selects August 2026; the project and dataset names are placeholders.<\/p>\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"sql\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">SQL<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-sql\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-keyword\">SELECT<\/span> event_date, event_name, user_pseudo_id\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">FROM<\/span> <span class=\"cbb-variable\">`project.analytics_12345.events_*`<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">WHERE<\/span> _TABLE_SUFFIX <span class=\"cbb-keyword\">BETWEEN<\/span> <span class=\"cbb-string\">'20260801'<\/span> <span class=\"cbb-keyword\">AND<\/span> <span class=\"cbb-string\">'20260831'<\/span>;<\/span><\/code><\/pre><\/figure>\n<p>Replacing the suffix condition with <code>event_date BETWEEN '20260801' AND '20260831'<\/code> filters rows, but does not itself restrict the wildcard table list. The results coincide only when the matched tables contain the expected daily data and no extra matching sources introduce duplicates or other rows. The suffix range excludes <code>intraday_...<\/code>; the row-date condition alone does not.<\/p>\n<p>A suffix filter derived dynamically from another table may fail to prune the scan. Suffix pruning also does not bypass all metadata checks: an incompatible schema or a view matched by the wildcard can still make the query fail. The shortest useful table list and a sufficiently specific prefix simplify this problem. Current-day streaming data belongs in a deliberately separate query.<\/p>\n<h2>Nested fields can be selected individually<\/h2>\n<p><code>event_params<\/code> is an array of records with a key and typed value fields. It is not an indivisible block that every parameter query must read in full. The following example refers to <code>key<\/code> and <code>value.string_value<\/code>, rather than requesting every numeric value field and the whole record.<\/p>\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"sql\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">SQL<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-sql\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-keyword\">SELECT<\/span> event_timestamp,\n<\/span><span class=\"cbb-line\">  (<span class=\"cbb-keyword\">SELECT<\/span> p.value.string_value\n<\/span><span class=\"cbb-line\">   <span class=\"cbb-keyword\">FROM<\/span> <span class=\"cbb-keyword\">UNNEST<\/span>(event_params) <span class=\"cbb-keyword\">AS<\/span> p\n<\/span><span class=\"cbb-line\">   <span class=\"cbb-keyword\">WHERE<\/span> p.<span class=\"cbb-keyword\">key<\/span> = <span class=\"cbb-string\">'page_location'<\/span>) <span class=\"cbb-keyword\">AS<\/span> page_location\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">FROM<\/span> <span class=\"cbb-variable\">`project.analytics_12345.events_*`<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">WHERE<\/span> _TABLE_SUFFIX <span class=\"cbb-keyword\">BETWEEN<\/span> <span class=\"cbb-string\">'20260801'<\/span> <span class=\"cbb-keyword\">AND<\/span> <span class=\"cbb-string\">'20260831'<\/span>;<\/span><\/code><\/pre><\/figure>\n<p>The scalar subquery assumes at most one <code>page_location<\/code> key per event. A missing key returns <code>NULL<\/code>; duplicate keys cause an error and require an explicit data-quality or selection rule. Filtering for a key does not create a separately stored <code>page_location<\/code> column: the relevant key and string-value data still have to be inspected.<\/p>\n<p>Selecting individual fields also applies to <code>device<\/code>, <code>geo<\/code>, <code>items<\/code> and <code>user_properties<\/code>. These fields can be essential to an analysis. The useful rule is to read the required fields, not to discard entire structures as generally unnecessary. Filters, joins and sorting may require fields beyond the final output list.<\/p>\n<h2>A free estimate, not an exact bill<\/h2>\n<p>The query editor validates SQL and estimates processed bytes. The command-line equivalent runs without executing the query:<\/p>\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap wp-block-code-beautifier-block-code\" data-lang=\"plain\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">Text<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-plain\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\">bq query --use_legacy_sql=false --dry_run &lt; query.sql<\/span><\/code><\/pre><\/figure>\n<p><code>query.sql<\/code> contains a complete query with the actual project, dataset and dates. No fixed byte result applies to every GA4 property. For clustered tables, the estimate can exceed the final scan because the skipped blocks are determined during execution. A dry run is not a guarantee of successful execution; scripts and dynamic SQL have additional validation limits.<\/p>\n<p>The actual on-demand charge also depends on the applicable price, free allowance, rounding and minimum billed data. BigQuery documents a 10 MB minimum per referenced table and per query. Result-cache hits can avoid query charges, but queries across multiple wildcard tables do not support cached results. Slot-based capacity costs cannot be calculated precisely from the byte estimate.<\/p>\n<h2>Clustering and a reusable summary table<\/h2>\n<p>A table containing only the required fields and summaries can reduce repeated work when many reports use the same fields or aggregates. The saving must exceed the refresh and storage costs; there is no universal threshold of three manual executions. The example below creates an empty table for event counts grouped by date, event name and pseudonymous identifier. It does not populate the table or calculate user\/session counts.<\/p>\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"sql\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">SQL<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-sql\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-keyword\">CREATE<\/span> <span class=\"cbb-keyword\">TABLE<\/span> <span class=\"cbb-keyword\">IF<\/span> <span class=\"cbb-keyword\">NOT<\/span> <span class=\"cbb-keyword\">EXISTS<\/span> <span class=\"cbb-variable\">`project.analytics_12345.daily_user_metrics`<\/span>\n<\/span><span class=\"cbb-line\">(\n<\/span><span class=\"cbb-line\">  event_date <span class=\"cbb-type\">DATE<\/span>,\n<\/span><span class=\"cbb-line\">  event_name <span class=\"cbb-type\">STRING<\/span>,\n<\/span><span class=\"cbb-line\">  user_pseudo_id <span class=\"cbb-type\">STRING<\/span>,\n<\/span><span class=\"cbb-line\">  total_events <span class=\"cbb-type\">INT64<\/span>\n<\/span><span class=\"cbb-line\">)\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">PARTITION<\/span> <span class=\"cbb-keyword\">BY<\/span> event_date\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">CLUSTER<\/span> <span class=\"cbb-keyword\">BY<\/span> event_name, user_pseudo_id\n<\/span><span class=\"cbb-line\"><span class=\"cbb-function\">OPTIONS<\/span> (require_partition_filter = <span class=\"cbb-constant\">TRUE<\/span>);<\/span><\/code><\/pre><\/figure>\n<p>Loading the table requires converting the exported <code>event_date<\/code> string with <code>PARSE_DATE('%Y%m%d', event_date)<\/code> and grouping by the intended date, event name and identifier to calculate <code>total_events<\/code>.<\/p>\n<p>The dataset must exist in the appropriate location. <code>IF NOT EXISTS<\/code> does not repair a pre-existing table with a different schema or clustering. Partitions divide the table into sections by date. The partition-filter option requires subsequent queries to include a suitable condition on that date; an unnecessarily broad date range can still read substantial data.<\/p>\n<p>Clustering can skip storage blocks for suitable filters. Up to four clustering columns are supported, and their order matters. Placing <code>event_name<\/code> first suits workloads that frequently filter by that field; identifier-only queries may need another order. Small tables or small daily partitions may gain little. These choices need measurements against the actual report workload.<\/p>\n<div class=\"lw-faq\"><h2>Questions and answers<\/h2>\n<h3>Can an expensive query be rejected before execution?<\/h3>\n<p>For on-demand queries, <code>--maximum_bytes_billed<\/code> sets a per-query bound on the estimated billed bytes. An estimate above that bound causes rejection without executing the query. A clustered query can be rejected even when its eventual scan would have been smaller. Project and user daily quotas provide additional controls, but Google describes custom quotas as approximate rather than an absolute spending guarantee.<\/p>\n<h3>How should late GA4 updates reach the summary table?<\/h3>\n<p>GA4 can update daily tables for up to three days after the event date. A daily refresh can rebuild the four completed dates before the current date in the property&#8217;s timezone. The oldest date is included again after the full three-day update window. This is different from merely appending yesterday or including today among four dates.<\/p>\n<p>Before replacing existing summaries, the refresh must verify that all expected source tables are available and read the replacement data successfully. Missing tables are not evidence of zero events. A staging table holds the replacement data before the target changes. A transaction applies the deletion and insertion together, or leaves both unapplied on failure. A staging table followed by a transaction for the date-scoped <code>DELETE<\/code> and <code>INSERT<\/code> avoids leaving the target empty after a failed insert. An appropriately scoped <code>MERGE<\/code> also needs a rule for target rows that disappeared from the source.<\/p>\n<p>A table&#8217;s existence does not prove that its export is complete. Export delays, historical corrections and deletions need separate monitoring and a repair path beyond the rolling window. Refresh jobs should also avoid overlapping executions. Neither partitioning nor a suffix filter supplies those correctness checks automatically.<\/p><\/div>\n<div class=\"lw-quellen\">\n<h2>Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/cloud.google.com\/bigquery\/pricing\" target=\"_blank\" rel=\"noopener noreferrer\">BigQuery pricing<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/analytics\/devguides\/collection\/ga4\" target=\"_blank\" rel=\"noopener noreferrer\">Google Analytics 4 developer guide<\/a><\/li>\n<li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/clustered-tables\" target=\"_blank\" rel=\"noopener noreferrer\">Introduction to clustered tables<\/a><\/li>\n<li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/best-practices-costs\" target=\"_blank\" rel=\"noopener noreferrer\">Google: estimate and control costs<\/a><\/li><li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/querying-wildcard-tables\" target=\"_blank\" rel=\"noopener noreferrer\">Google: wildcard table queries<\/a><\/li><li><a href=\"https:\/\/support.google.com\/analytics\/answer\/7029846?hl=en\" target=\"_blank\" rel=\"noopener noreferrer\">Google: GA4 BigQuery export schema<\/a><\/li><li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/best-practices-performance-nested\" target=\"_blank\" rel=\"noopener noreferrer\">Google: nested and repeated fields<\/a><\/li><li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/best-practices-performance-compute\" target=\"_blank\" rel=\"noopener noreferrer\">Google: query computation<\/a><\/li><li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/custom-quotas\" target=\"_blank\" rel=\"noopener noreferrer\">Google: custom query quotas<\/a><\/li><li><a href=\"https:\/\/docs.cloud.google.com\/bigquery\/docs\/multi-statement-queries\" target=\"_blank\" rel=\"noopener noreferrer\">Google: multi-statement queries<\/a><\/li><\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>On-demand query costs depend on scanned data, not result rows. Constant table-suffix filters and selected fields can reduce scans; a free dry run estimates bytes but does not guarantee the final charge.<\/p>\n","protected":false},"author":1,"featured_media":30789,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[91333,91258],"class_list":["post-10470","post","type-post","status-publish","format-standard","hentry","category-digital-analytics","tag-bigquery","tag-google-analytics-4"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"Why LIMIT does not make a query cheaper, why event_date does not prune tables, and how a free dry run answers the cost question exactly.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"Why LIMIT does not make a query cheaper, why event_date does not prune tables, and how a free dry run answers the cost question exactly.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-19T05:20:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-26T12:15:43+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/11\/klarheit-20261111-10470.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Lukas Wojcik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Lukas Wojcik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/\"},\"author\":{\"name\":\"Lukas Wojcik\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"headline\":\"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges\",\"datePublished\":\"2026-09-19T05:20:00+00:00\",\"dateModified\":\"2026-10-26T12:15:43+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/\"},\"wordCount\":1155,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/klarheit-20261111-10470.png\",\"keywords\":[\"BigQuery\",\"Google Analytics 4\"],\"articleSection\":[\"Digital Analytics\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/\",\"name\":\"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/klarheit-20261111-10470.png\",\"datePublished\":\"2026-09-19T05:20:00+00:00\",\"dateModified\":\"2026-10-26T12:15:43+00:00\",\"description\":\"Why LIMIT does not make a query cheaper, why event_date does not prune tables, and how a free dry run answers the cost question exactly.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/klarheit-20261111-10470.png\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/klarheit-20261111-10470.png\",\"width\":1200,\"height\":630,\"caption\":\"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges | Lukas Wojcik","description":"Why LIMIT does not make a query cheaper, why event_date does not prune tables, and how a free dry run answers the cost question exactly.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/","og_locale":"en_US","og_type":"article","og_title":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges | Lukas Wojcik","og_description":"Why LIMIT does not make a query cheaper, why event_date does not prune tables, and how a free dry run answers the cost question exactly.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/","og_site_name":"Lukas Wojcik - Blog","article_published_time":"2026-09-19T05:20:00+00:00","article_modified_time":"2026-10-26T12:15:43+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/11\/klarheit-20261111-10470.png","type":"image\/png"}],"author":"Lukas Wojcik","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Lukas Wojcik","Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#article","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/"},"author":{"name":"Lukas Wojcik","@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"headline":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges","datePublished":"2026-09-19T05:20:00+00:00","dateModified":"2026-10-26T12:15:43+00:00","mainEntityOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/"},"wordCount":1155,"commentCount":0,"publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/11\/klarheit-20261111-10470.png","keywords":["BigQuery","Google Analytics 4"],"articleSection":["Digital Analytics"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/","name":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#primaryimage"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/11\/klarheit-20261111-10470.png","datePublished":"2026-09-19T05:20:00+00:00","dateModified":"2026-10-26T12:15:43+00:00","description":"Why LIMIT does not make a query cheaper, why event_date does not prune tables, and how a free dry run answers the cost question exactly.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#primaryimage","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/11\/klarheit-20261111-10470.png","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/11\/klarheit-20261111-10470.png","width":1200,"height":630,"caption":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges"},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/reducing-ga4-query-costs-in-bigquery-checking-data-volume-and-estimating-charges\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Reducing GA4 Query Costs in BigQuery: Checking Data Volume and Estimating Charges"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/10470","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=10470"}],"version-history":[{"count":7,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/10470\/revisions"}],"predecessor-version":[{"id":27285,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/10470\/revisions\/27285"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media\/30789"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=10470"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/categories?post=10470"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=10470"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}