SELECT and INSERT queries to be performed on a table in Google BigQuery, including public datasets. The table structure is inferred from the BigQuery table schema automatically.
Reading uses the BigQuery REST API (tabledata.list), so only native tables can be read (views, materialized views and external tables cannot). Writing uses streaming inserts (tabledata.insertAll), which requires billing to be enabled for the project.
Syntax
Arguments
The
project, dataset, table and access_token arguments can also be given in the key = value form; positional arguments fill these slots in this order, and specifying an argument both positionally and as a key (or the same key twice) is an error.
The following arguments can be specified in the key = value form (or as keys of a named collection):
Authentication
Exactly one authentication method must be provided. BigQuery does not allow anonymous access, so credentials are required even for public datasets.- Access token. Any valid OAuth 2.0 access token, for example, from
gcloud auth print-access-token. Tokens expire quickly (typically after one hour), so this method is best for interactive use. - Service account key (recommended for servers). Pass the content of a key file created in Google Cloud IAM with the
service_account_keyargument. ClickHouse signs a JWT with the key and exchanges it for an access token, refreshing it automatically. - Refresh token. Pass
client_id,client_secretandrefresh_token, for example, taken from~/.config/gcloud/application_default_credentials.jsonaftergcloud auth application-default login.
BigQuery table engine or CREATE TABLE ... AS bigquery(...)) is registered as a dependency of the collection, so DROP NAMED COLLECTION is blocked while the table exists.
Data type mapping
Notes:
- BigQuery
DATETIMEhas no time zone; it is mapped toDateTime64(6, 'UTC')so that the displayed value does not depend on the server time zone. - A
NULLABLERECORDis mapped toNullable(Tuple(...)), so a whole-recordNULLis preserved asNULLinstead of collapsing to aTupleof default values. ANULL(or empty) array becomes an empty array, becauseArraycannot be insideNullablein ClickHouse. A BigQuery array cannot containNULLelements (ARRAY<T>is equivalent toARRAY<T NOT NULL>), so the element type of aREPEATEDfield is notNullable(Array(T), orArray(Tuple(...))for aRECORDelement); aNULLelement in atabledata.listresponse is rejected as malformed input. - Reading and writing
Nullable(Tuple(...))columns through thebigquerytable function works without extra settings. Creating a persistentBigQuery-engine table that contains such a column (whether the structure is inferred or declared explicitly) requires theenable_nullable_tuple_typesetting, as for anyNullable(Tuple)column. When declaring columns explicitly, aRECORDfield may instead be declared as a plainTuple(...)to avoid the setting, at the cost of coercing a whole-recordNULLto a default tuple; the only accepted difference from the inferred type is dropping aNullablethat wraps aRECORD’sTuple, and only at that same record — the nullability cannot be moved to a different (inner or outer) record. GEOGRAPHYis mapped to Geometry. BigQuery transfers aGEOGRAPHYvalue as WKT text, which is parsed into the matching alternative ofGeometry(aVariantofPoint,MultiPoint,Ring,LineString,MultiLineString,PolygonandMultiPolygon) on read, and serialized back to WKT on write. AGEOMETRYCOLLECTIONand an empty geometry (such asPOINT EMPTY) have noGeometrycounterpart, so reading a row that contains such a value raises an error. BecauseVariantholds aNULLby itself, aNULLABLEGEOGRAPHYfield is mapped toGeometryand not toNullable(Geometry), andNULLstill round-trips.JSONis mapped toStringrather than to the JSON data type, because the ClickHouseJSONtype accepts only an object ({...}) at the top level, while a BigQueryJSONvalue can be any JSON value — a scalar, an array, ornull— so a table containing such values could not be read. In addition,JSONcannot be wrapped inNullable, so an SQLNULLin aNULLABLEcolumn would not be preserved. TheStringmapping is lossless; top-level objects can be converted withCAST(value AS JSON).BIGNUMERICvalues with more than 38 digits in the integer part do not fit intoDecimal(76, 38)and produce an error.TIMESTAMPandDATEvalues outside of the range ofDateTime64/Date32(years 1900-2299) are not supported.RANGEcolumns are read-only.tabledata.insertAllexpects aRANGE<T>value as a structured{start, end}object, which cannot be reconstructed from theStringmapping, so inserting into aRANGEcolumn raises an error.INT64values are sent totabledata.insertAllas decimal strings, because the API parses JSON numbers as doubles and would otherwise corrupt values outside[-2^53 + 1, 2^53 - 1].
Examples
Read a public dataset using a token fromgcloud:
Limitations
- Only native BigQuery tables can be read. Views and external tables require running a BigQuery query job, which this function does not do.
RANGEcolumns can be read (asString) but not written: inserting into aRANGEcolumn raises an error.- A
GEOGRAPHYvalue that is aGEOMETRYCOLLECTIONor an empty geometry cannot be represented by theGeometrytype, so reading a row containing one raises an error. Writing aNULLGeometryinto aREQUIREDGEOGRAPHYfield, or as an element of aREPEATEDGEOGRAPHYfield, is rejected, because BigQuery accepts noNULLthere. - Predicates are not pushed down:
tabledata.listonly lists the rows of a table and has no filtering parameter at all (it takes pagination, column selection and format options), and filtering would require running a BigQuery query job, which this function does not do. AWHEREcondition is therefore applied in ClickHouse after the rows have been downloaded; use column selection to reduce the transferred data. - A
LIMIT, on the other hand, does reduce the amount of data read. Pages are requested lazily, withmaxResultsset tomax_block_size, and no further page is requested once the query has enough rows. For a trivialLIMIT n(noWHERE,GROUP BY,ORDER BY, andnbelowmax_block_size) ClickHouse lowersmax_block_sizeton, so exactly one request for exactlynrows is made; otherwise the read stops at the first page boundary past the limit, overshooting it by less than one page. - The read is pinned to the schema seen at query analysis time by passing the explicit list of columns to
tabledata.list. For a very wide read whose column list would exceed the request URL length limit (for exampleSELECT *from a table with thousands of columns), the query is rejected rather than read without a pin (an unpinned read could be misaligned by a concurrent schema change); select fewer columns so the list fits. The same URL length limit is checked before every paginated request (each page carries an opaquepageToken), so a read whose later pages would not fit the limit is rejected with the same error instead of failing part way through. - If the BigQuery table is altered after its schema has been read, the query is rejected instead of silently returning or writing mismatched data: the live schema is re-fetched and compared with the analyzed one right before a read, and again before an
INSERTstreams its first row. The remaining window (a schema change between that check and the requests that follow it) cannot be closed, because the schema and the data are fetched by separate REST requests. - The comparison is against the schema snapshot the query was analyzed with, which is taken when the table function resolves its structure or, for a persistent table (a
BigQueryengine table, or a table created withCREATE TABLE ... AS bigquery(...), which persists its columns the same way), on its first read or write afterCREATE,ATTACH, or a server restart. Table metadata persists the mapped ClickHouse columns, not the BigQuery schema, so a schema change made while the table was detached (or the server was down) is adopted by the next query rather than rejected: the declared columns are still validated against the live schema, and the rows are decoded with it, so a change that keeps the mapped ClickHouse types (STRINGtoBYTES, for example) is read with the new type’s rules under the same column type. - Rows written with streaming inserts land in the BigQuery streaming buffer and may take a while to become visible to subsequent reads.
- A large
INSERTis sent totabledata.insertAllin batches: at most 500 rows per request, and also split so that each request stays under BigQuery’s 10 MB request-size limit (a single row larger than that limit is rejected with a clear error). - Writes are not atomic, and a single
tabledata.insertAllrequest may itself partially succeed: BigQuery can commit some rows of a request while rejecting the others withinsertErrors. Requests are also committed independently of each other, so a later batch may be rejected after earlier batches have been accepted. In both cases the query reports an error, but the already-committed rows remain in BigQuery. To limit duplication, each row is sent with a stableinsertIdderived from the query id and the row’s ordinal position in the stream, which BigQuery uses for best-effort deduplication within its streaming-insert window. Aquery_idlonger than BigQuery’s 128-characterinsertIdlimit is hashed to a fixed-length prefix, which stays stable for thatquery_id. Because theinsertIddepends on the ordinal position, deduplication is reliable only when the rerun produces the rows in the same order: a transport-level retry of a batch is always safe, and re-running the sameINSERTwith the samequery_iddeduplicates only if it presents the rows in the same order (for example a single-threaded insert, or an otherwise deterministic ordering — setmax_threads = 1andmax_insert_threads = 1for a parallelINSERT ... SELECTwhose chunk order could otherwise change between attempts).