Docs /Views /Views
Views

Views in BigQuery

A BigQuery view is a saved query. It stores no data, and every query on the view runs the saved SQL again and bills the bytes it reads from the base tables. What a view costs, when a filter on the view still prunes partitions, and the limits on what a view can contain.

A view is a name for a query. CREATE VIEW saves the SQL, and nothing is computed or stored. When you query the view, BigQuery puts the saved SQL into your query and runs the whole thing. You pay for the bytes that combined query reads from the base tables, exactly as if you had written the SQL yourself. If you want the result stored and refreshed, that is a materialized view. If you want to share a slice of a table without giving access to the table, that is an authorized view.

CREATE VIEW shop.recent_orders AS
SELECT order_id, customer_id, total
FROM shop.orders
WHERE order_date >= '2026-01-01';

What a query on a view costs

A view has no size of its own, so there is nothing to bill for storage. The query bill is for the columns of the base tables that the outer query and the view body reference between them. A view that selects 3 of 20 columns costs the same as a query that selects those 3 columns. If the outer query uses 1 of those 3, only that 1 is read.

We checked this on a public view, bigquery-public-data.crypto_bitcoin.blocks. Asking the view for one column:

one column through a view
123
SELECT size
FROM `bigquery-public-data.crypto_bitcoin.blocks`
WHERE number < 100000
BigQuery dry run

This query will process 1.5 MB when run.

and for every column of the same rows:

SELECT * through the same view
123
SELECT *
FROM `bigquery-public-data.crypto_bitcoin.blocks`
WHERE number < 100000
BigQuery dry run

This query will process 22.2 MB when run.

The first reads 1.6 MB and the second 23.3 MB. The view hides the table, not the cost of SELECT *. See how queries are billed. In Querylab.io, a view opens in a tab with its defining SQL under Query, and its Preview runs the view’s query: free when possible, otherwise it shows a dry-run estimate and an Execute on BigQuery button (Table information).

Two smaller points. SELECT COUNT(*) on a plain SELECT * view over a table processed 0 bytes, the same as on the table. And the job record for a query on a view lists the base tables as its referenced tables, not the view, so INFORMATION_SCHEMA.JOBS shows events and not recent_orders.

Do filters on a view prune partitions?

Often yes. BigQuery plans the view’s SQL and your filter as one query, so a filter on the outer query can reach the table scan. We tested this on a 48.8 MB table with 600,000 rows, partitioned by day into 10 partitions, with views of several shapes. Every query below filters on one day, event_date = '2026-01-03', and was dry-run:

View bodyBytes processed
the table directly, SELECT *4.88 MB, one partition
SELECT * FROM events4.88 MB, one partition
three columns, no filter, for scale19.2 MB
three columns1.92 MB, one partition
GROUP BY event_date, user_id, filter on event_date1.92 MB, one partition
GROUP BY event_date, user_id, filter on the sum19.2 MB, all partitions
UNION ALL of two selects from the table0.96 MB, one partition (9.6 MB without the filter)
a view on a view, two columns1.44 MB, one partition
ROW_NUMBER() OVER (PARTITION BY event_date, user_id ...)4.88 MB, one partition
ROW_NUMBER() OVER (PARTITION BY user_id ...)48.8 MB, all partitions
ORDER BY event_id LIMIT 100048.8 MB, all partitions

The pattern is the usual one in SQL. A filter moves into the view when moving it cannot change the view’s result. A filter on a grouping column can move below the GROUP BY. A filter on a column the window is partitioned by can move below the window, because every window stays whole. A filter on event_date cannot move below a window that is partitioned by user_id only, since rows from other days belong to the same windows and change the row numbers. A filter on the result of an aggregate, or on rows after a LIMIT, never moves. In those cases the view reads the whole table and the result is still correct. Only the bill grows.

So the check is the same as for any partitioned table: look at the byte estimate with the filter on. A view that needs the whole table for every query is a good candidate for a materialized view or a table.

If the base table has require_partition_filter set, a query on the view needs a usable filter too. We saw the same “without a filter over column(s)” error through a view.

The stored schema can go stale

BigQuery records the view’s column list when you create or replace it. If the view is SELECT * FROM events and someone adds a column to events later, the schema BigQuery reports for the view does not change. Queries do. We created a view over a table (a, b), then added c, dropped b and added b2:

  • bq show --schema and INFORMATION_SCHEMA.COLUMNS for the view kept saying a, b.
  • SELECT * FROM view returned a, c, b2.
  • A table created with CREATE TABLE ... AS SELECT * FROM view had a, c, b2.

The data you get is right. The metadata that BI tools, data catalogs and INFORMATION_SCHEMA.COLUMNS read is wrong until you run CREATE OR REPLACE VIEW again. Google documents this limitation. Naming the columns in the view body avoids the surprise.

What a view cannot contain

BigQuery checks these when you create the view. We ran each against BigQuery.

In the view bodyResult
A temporary functionCreating views with temporary user-defined functions is not supported
A query parameter, @pQuery parameters cannot be used inside SQL view bodies
A table without its dataset, FROM eventsTable "events" must be qualified with a dataset. The default dataset of the job does not apply.
A column that doesn’t existUnrecognized name. The view is validated when created.
A persistent function, shop.tax(x)Works. Use this instead of a temporary function.
CURRENT_TIMESTAMP(), SESSION_USER()Works. Queries on the view are never served from the cache.

A view is read-only, so INSERT, UPDATE and DELETE on a view fail with “not allowed for this operation because it currently has type VIEW”. A wildcard table can’t match views: FROM shop.vw_* fails with “Views cannot be queried through prefix”. Views can be nested up to 16 levels, and querying is limited to 15. The SQL of one view can be up to 256 K characters. See user-defined functions for the persistent function pattern and wildcard tables.

CREATE VIEW IF NOT EXISTS skips quietly if the name is taken. CREATE VIEW alone fails with Already Exists. CREATE OR REPLACE VIEW swaps the query and refreshes the stored schema.

Query cache and expiration

The query cache works through views. We ran the same COUNT(*) on a view twice, and the second run was a cache hit. A view that calls CURRENT_TIMESTAMP() or SESSION_USER() was a cache miss both times we ran it, so every query on top of such a view is a full run.

A dataset’s default table expiration applies to views too. A view we created in a dataset with a one-day default had an expiration_timestamp one day out in its DDL. When it passes, the view is deleted like a table and anything that queries it fails.

Views and access

To query a view you normally need read access to the view and to every table it reads. Authorizing the view on the source dataset removes the second requirement, which is how views are used to share part of a table. That is the subject of authorized views.