How to create a view in BigQuery: CREATE VIEW, cost and limits
A BigQuery view is a saved query that you read like a table. This guide creates one with CREATE VIEW and CREATE OR REPLACE VIEW, reads a view's SQL back from INFORMATION_SCHEMA (BigQuery has no SHOW CREATE VIEW), measures what querying a view costs, sets up an authorized view, compares views with materialized views and tables, and lists the limits that return errors, each checked in BigQuery.
A view in BigQuery is a saved SELECT with a name. It stores the query, not the data: every time someone reads the view, BigQuery runs that query against the tables underneath and bills it as a normal query. You create one with CREATE VIEW, change it with CREATE OR REPLACE VIEW, and read its SQL back from INFORMATION_SCHEMA.
| Task | Statement |
|---|---|
| Create a view | CREATE VIEW dataset.view AS SELECT ... |
| Create, or overwrite if it exists | CREATE OR REPLACE VIEW dataset.view AS SELECT ... |
| Create only if it is missing | CREATE VIEW IF NOT EXISTS dataset.view AS SELECT ... |
| See a view’s SQL | SELECT ddl FROM dataset.INFORMATION_SCHEMA.TABLES WHERE table_name = 'view' |
| Remove a view | DROP VIEW dataset.view |
Note: Originally published in March 2025. Fully rewritten in October 2026: every result below was produced by running the query in BigQuery on 16 October 2026. Statements that create views were validated with a dry run, not executed.
Create a view with CREATE VIEW
Create a view with CREATE VIEW
The examples use the public thelook_ecommerce dataset, so they run in any project. Replace my_project.reporting with a dataset of yours, in the same location as the tables the view reads (here, US).
CREATE OR REPLACE VIEW `my_project.reporting.daily_orders`
OPTIONS (description = 'Orders per day and status, thelook_ecommerce')
AS
SELECT
DATE(created_at) AS order_date,
status,
COUNT(*) AS orders,
COUNT(DISTINCT user_id) AS customers
FROM `bigquery-public-data.thelook_ecommerce.orders`
GROUP BY order_date, status;
The dry run returned the view’s schema and 0 bytes processed: creating a view reads no data.
| column | type |
|---|---|
| order_date | DATE |
| status | STRING |
| orders | INTEGER |
| customers | INTEGER |
Three variants, and how they behave:
CREATE VIEWfails if a view with that name already exists.CREATE OR REPLACE VIEWoverwrites it. This is also how you edit a view in SQL: rewrite the whole statement with the new query.CREATE VIEW IF NOT EXISTSdoes nothing if the view exists. You cannot combine the two: the dry run ofCREATE OR REPLACE VIEW IF NOT EXISTSreturned “CREATE VIEW cannot have both OR REPLACE and IF NOT EXISTS”.
Two other ways to create the same view, per Google’s creating views page (read in October 2026):
- Console: run the query, then Save > Save view, and pick the project, dataset and name. A view saved this way cannot get a description, label or expiration at creation; add them afterwards.
- bq CLI:
bq mk --use_legacy_sql=false --view 'SELECT ...' reporting.daily_orders. Without--use_legacy_sql=falsethe query is read as legacy SQL.
Query a view like a table
SELECT order_date, status, orders, customers
FROM `my_project.reporting.daily_orders`
WHERE order_date = '2024-01-15'
ORDER BY orders DESC;
| order_date | status | orders | customers |
|---|---|---|---|
| 2024-01-15 | Shipped | 11 | 11 |
| 2024-01-15 | Complete | 8 | 8 |
| 2024-01-15 | Cancelled | 6 | 6 |
| 2024-01-15 | Processing | 5 | 5 |
| 2024-01-15 | Returned | 3 | 3 |
The result above is the view’s body run with the same filter, which is what BigQuery executes when you read the view. thelook_ecommerce is generated data, so your counts may differ slightly on a later date.
ORDER BY is allowed inside a view (Google’s own bq mk example uses one), but put the sort in the query that reads the view: that is the result the reader sees.
OWOX Data Marts
See your first report built in real time. 15 minutes.
- Connect your data warehouse
- Pick your metrics
- Get a live Google Sheets report
In the time it takes to write a ticket. Then imagine never writing that ticket again.
Book a DemoWe'll use your actual use case
A view stores the query, so every read re-runs it
A view stores the query, so every read re-runs it
Google’s wording: “the query that defines the view is run each time the view is queried”, and queries are billed on the data “referenced directly or indirectly by the top-level query” (introduction to views, read in October 2026). A view adds no storage cost and no extra charge. It does hide how much data a short-looking query reads.
To show that, we dry-ran queries against a real view in our project, SearchPerformanceViews.AllSearchQueries, which groups Search Console data by month and keyword:
| Query | Bytes processed (dry run) |
|---|---|
SELECT * FROM the view |
1,771,569,219 (1.77 GB) |
| The view’s own SQL, run directly | 1,771,569,219 (1.77 GB) |
SELECT Keyword, Clicks FROM the view WHERE Month = 'September 2026' |
1,307,592,803 (1.31 GB) |
Same with LIMIT 10 added |
1,307,592,803 (1.31 GB) |
Same question on the base table with WHERE data_date BETWEEN '2026-09-01' AND '2026-09-30' |
23,806,125 (23.8 MB) |
What the numbers say:
- Reading the view costs exactly what running its SQL costs.
- Selecting fewer columns helps: BigQuery dropped the columns the outer query does not need.
LIMITsaves nothing.- The month filter barely helped, because the view turns
data_dateinto a textMonth, and a filter on that text cannot skip partitions. Asking the same question on the date column read 55 times less data.
The trap: a view that hides the partition column makes every filtered query a full scan. Keep the raw date column in the view’s output so readers can filter on it.
Read a view’s SQL: there is no SHOW CREATE VIEW
Read a view’s SQL: there is no SHOW CREATE VIEW
SHOW CREATE VIEW is a MySQL statement. In BigQuery the dry run returned “Syntax error: Unexpected keyword CREATE”. The SQL is in INFORMATION_SCHEMA, per dataset:
SELECT table_name, table_type, ddl
FROM `owox-d-ikrasovytskyi-001.SearchPerformanceViews.INFORMATION_SCHEMA.TABLES`
WHERE table_type = 'VIEW';
| table_name | table_type | ddl |
|---|---|---|
| AllSearchQueries | VIEW | CREATE VIEW `owox-d-ikrasovytskyi-001.SearchPerformanceViews.AllSearchQueries` AS SELECT FORMAT_DATE('%B %Y', data_date) AS Month, query as Keyword, SUM(impressions) as Impressions, ... ORDER BY Month DESC, Clicks DESC; |
The ddl column is a ready-to-run CREATE VIEW statement: copy it, change CREATE to CREATE OR REPLACE, edit the query, run it. For the query alone, use INFORMATION_SCHEMA.VIEWS:
SELECT table_name, use_standard_sql, LENGTH(view_definition) AS sql_length
FROM `owox-d-ikrasovytskyi-001.SearchPerformanceViews.INFORMATION_SCHEMA.VIEWS`;
| table_name | use_standard_sql | sql_length |
|---|---|---|
| AllSearchQueries | YES | 416 |
use_standard_sql = YES means the view is GoogleSQL, not legacy SQL. Replace the project and dataset with yours. The region-wide form, region-us.INFORMATION_SCHEMA.VIEWS, needs permission to list tables in every dataset of the project; our account lacked it and got “Access Denied”, so the per-dataset form is the safer default.
Authorized views: share the result, not the table
Authorized views: share the result, not the table
A normal view does not grant access to anything. Someone who queries it needs read access to the view and to every table it reads. An authorized view removes the second requirement: the source dataset trusts the view itself, so users “can’t access the source dataset directly” yet can query the view (authorized views, read in October 2026). Typical use: give the marketing team daily totals without the customer-level table behind them.
Google’s steps:
- Keep the source table in its own dataset.
- Create a second dataset for the view, in the same region as the source.
- Create the view there, selecting only the columns and rows to share.
- Give users
roles/bigquery.jobUseron the project where they run queries. - Give users
roles/bigquery.dataVieweron the view’s dataset, not the source. - On the source dataset, open Sharing > Authorize views and add the view as
PROJECT_ID.DATASET_ID.VIEW_NAME.
Limits from the same page: a dataset’s access list holds up to 2,500 authorized resources (views, datasets and routines together), and only a data owner can change an authorized view’s SQL. The roles are explained in BigQuery IAM roles and permissions.
Views, materialized views and tables
Views, materialized views and tables
| View | Materialized view | Table | |
|---|---|---|---|
| Stores | the query | the query and precomputed results | data |
| Freshness | always current | refreshed automatically after base-table changes | whatever was last loaded |
| Cost to read | the view’s query, every time, for the columns it needs | reads the stored results, plus changes since the last refresh | the columns read |
| Storage cost | none | yes | yes |
| SQL allowed | any SELECT |
a restricted set | n/a |
On refresh, Google’s manage page (read in October 2026) says BigQuery “attempts to start a refresh within 5 minutes of a change in the base table”, and that by default materialized views “are refreshed no more often than every 30 minutes”. You can also refresh one manually at any time. When a materialized view fits, how to build one is in BigQuery materialized views.
Rule of thumb: start with a view. Turn it into a materialized view when the same heavy aggregation is read many times a day, and into a scheduled table when the SQL is too complex for a materialized view.
View limits that return errors
View limits that return errors
Each rule below is from Google’s introduction to views and quotas page (read in October 2026). Where the error text is quoted, it came from a dry run.
| Rule | What happened in the test |
|---|---|
| Table names inside a view must include the dataset | “Table “searchdata_url_impression” must be qualified with a dataset (e.g. dataset.table).” |
No query parameters (@start_date) in the body |
“Query parameters cannot be used inside SQL view bodies” |
| No temporary functions or temporary tables | “Creating views with temporary user-defined functions is not supported” |
| A wildcard query cannot match a view | “Views cannot be queried through prefix.” |
| Views are read-only | INSERT, UPDATE, DELETE go to the base table |
| GoogleSQL and legacy SQL views do not mix | a GoogleSQL query cannot read a legacy SQL view |
| Up to 16 levels of nested views | creating 16 is allowed; querying works up to 15 |
| View SQL up to 256K characters | per view, not counting the views it references |
| Schema is saved at creation | if base columns change, the reported schema is stale until you recreate the view; results stay correct |
A wildcard table inside a view is allowed. This dry run passed, and the body returned real rows:
CREATE OR REPLACE VIEW `my_project.reporting.gsod_1990s` AS
SELECT CONCAT('199', _TABLE_SUFFIX) AS year, COUNT(*) AS readings
FROM `bigquery-public-data.noaa_gsod.gsod199*`
GROUP BY year;
| year | readings |
|---|---|
| 1999 | 2,564,099 |
| 1998 | 2,509,388 |
| 1997 | 2,515,441 |
(The last three years shown, from the body run with ORDER BY year DESC LIMIT 3.) What is not allowed is the reverse: a wildcard table pattern that matches a view’s name.
The parameter rule is the one that hurts most in reporting. The usual workaround is to leave the filter out of the view, expose the column, and let the reading query add WHERE order_date >= @start_date.
Where views go wrong in reports
Where views go wrong in reports
A view gives one definition of “orders per day” a single home. The failure is quiet: someone replaces it with CREATE OR REPLACE VIEW, a column is renamed or a filter changes, and every report that reads it changes on its next refresh, with no error. The job history keeps who ran the CREATE OR REPLACE for 180 days, but nothing tells the reports’ readers that the definition changed.
In OWOX Data Marts the same SQL is published as a Data Mart with a description for each column, and Sheets, Data Studio and AI assistants read that one model. OWOX Data Marts runs your SQL as written, in your own BigQuery project, and does not transform the data.
Turn your data into decisions.
Governed data marts give you the clean foundation ML needs to actually work.
- No AI hallucinations
- Analyst-governed definitions
- Every number traces to SQL
Frequently Asked Questions
What is the difference between a view and a materialized view in BigQuery?
A view stores only its query and runs it in full every time it is read. A materialized view also stores precomputed results, which BigQuery refreshes automatically after base-table changes (it tries to start within 5 minutes, and by default no more often than every 30 minutes), so reads are cheaper but the SQL it accepts is more restricted.
What is the difference between an authorized view and a regular view in BigQuery?
To query a regular view, a user needs access to the view and to every table it reads. An authorized view is trusted by the source dataset, so users with access only to the view's dataset can query it without being able to read the source tables.
How do I list the tables and views in a BigQuery dataset?
Expand the dataset in the BigQuery console, or query dataset.INFORMATION_SCHEMA.TABLES; its table_type column shows BASE TABLE, VIEW or MATERIALIZED VIEW for each object.
How do I modify a view in BigQuery?
Run CREATE OR REPLACE VIEW with the full new query; it overwrites the existing definition. You can take the current statement from the ddl column of INFORMATION_SCHEMA.TABLES, edit it and run it.
How do I see the SQL of a view in BigQuery?
BigQuery has no SHOW CREATE VIEW statement. Query dataset.INFORMATION_SCHEMA.TABLES and read the ddl column, which holds a ready-to-run CREATE VIEW statement, or read view_definition in dataset.INFORMATION_SCHEMA.VIEWS.
What is the difference between a view and a table in BigQuery?
A table stores data and you pay to store it. A view stores only a query, costs nothing to store, and runs that query against its source tables every time it is read, so each read is billed like the full query.
