Google Sheets#
Querona connects to Google Sheets with a built-in provider. One connection covers as many spreadsheets as you list: each becomes a schema inside one virtual database, and its tabs become that schema’s tables — the same shape the Excel provider uses, where a workbook is a schema and a worksheet is a table. Because they share a database, you can join across spreadsheets in a single query. The connection is read-only.
SELECT id, name, price FROM [Sales 2026].[Sheet1] WHERE price > 100;
Note
There is no SQL push-down — Google Sheets has no SQL surface. Querona reads a whole tab
and evaluates every filter, join and aggregate itself, so a WHERE clause narrows the result
but not the amount of data fetched from Google.
Prerequisites#
Querona connects as a Google service account. If you do not already have one and its JSON key, follow How to create a Google service account and its JSON key first — it walks through creating the account, enabling the API, downloading the key, and handling it safely.
Three things must be true on the Google side, or every query fails:
The Google Sheets API must be enabled on the Google Cloud project that owns the service account you connect with — console.cloud.google.com/apis/library/sheets.googleapis.com.
You need the service account’s JSON key file — downloaded from APIs & Services → Credentials in the Google Cloud console, then with key type JSON. Google offers that file once, at creation: it cannot be downloaded again, only replaced by a new key. This step does not apply if the engine authenticates through Application Default Credentials instead — see Authentication below.
Every spreadsheet you list must be shared with the service account’s email address (the
client_emailfield of its JSON key), Viewer is enough. Sharing is per file — adding a spreadsheet to an existing connection means sharing that one too.
Important
Forgetting to share a spreadsheet is by far the most common failure, and it surfaces as an HTTP 403. The service account can never own or create a spreadsheet of its own — an existing spreadsheet always has to be shared with it first. Testing the connection checks every spreadsheet in the list, so an unshared one is reported before you import anything.
Important
The service account needs no IAM roles — grant none. A spreadsheet is a Google Workspace asset rather than a Cloud resource, and per Google’s own guidance IAM roles configured in the Google Cloud console do not grant access to Workspace assets such as Sheets. Giving the account project roles like Viewer, Editor or Owner buys no access to any spreadsheet at all; it only widens what that account can do elsewhere in the project. Sharing the file is the whole mechanism.
Warning
The JSON key is a long-lived credential — anyone holding the file can act as the service account until the key is deleted. Never commit it to source control, and rotate it on a schedule.
Connection setup#
Go to the section and click ADD CONNECTION, then pick the Google Sheets provider. The provider form carries the read options only, and every one of them is optional — see Configuration options below.
The spreadsheets themselves go on the connection’s Source URIs screen, opened from the provider-parameters screen — the same shared data-sources screen the File provider uses for its files. Add one entry per spreadsheet, pasting the URI straight from your browser’s address bar — or click IMPORT and paste the whole list at once, separated by semicolons or line breaks. See also Create a connection.
EXEC sp_addlinkedserver
@server = 'gs',
@srvproduct = 'Google Sheets',
@provstr = '{"dataSourcesConfiguration":"[{\"uriPath\":\"https://docs.google.com/spreadsheets/d/<spreadsheet-id>/edit\"},{\"uriPath\":\"https://docs.google.com/spreadsheets/d/<another-spreadsheet-id>/edit\"}]"}';
CREATE DATABASE [sheetdb] WITH CONNECTION = [gs];
EXEC sp_import_metadata @database = 'sheetdb';
USE sheetdb;
SELECT id, name, price FROM [Sales 2026].[Sheet1] WHERE price > 100;
This example authenticates through Application Default Credentials. To authenticate through
a database scoped credential instead, see the exact sequence below
— the credential lives inside sheetdb, so it can only be created after the database
and must exist before the metadata import.
dataSourcesConfiguration lists the spreadsheets the connection exposes — one schema each, with
that spreadsheet’s tabs as its tables. It is the engine’s shared data-source list, the same setting the
File provider fills with file and folder URIs: a JSON array of objects, each carrying one uriPath.
Paste a spreadsheet’s link exactly as your browser’s address bar shows it — Querona takes the spreadsheet
id out of it for you, so the /edit, #gid= and ?usp=sharing decorations do no harm. A link is
required: a bare spreadsheet id on its own is rejected, as are links to Drive folders, to other kinds of
Google document, and published-to-web addresses. The provider cannot list or browse spreadsheets (the
Sheets API has no such operation), so the list is always yours to supply.
Note
@provstr is a JSON object whose values are strings, so the list is a JSON array inside a
string — its own quotes escaped as \", exactly as in the example above. The GUI writes that
escaping for you.
For a connection over a single spreadsheet, @datasrc is a shorthand — give it that spreadsheet’s URI
and it seeds a one-entry list for you:
EXEC sp_addlinkedserver
@server = 'gs',
@srvproduct = 'Google Sheets',
@datasrc = 'https://docs.google.com/spreadsheets/d/<spreadsheet-id>/edit';
Each spreadsheet becomes one schema, named after the spreadsheet’s own title, normalized to fit the
128-character sysname limit. Rename that schema in Querona whenever the title makes an awkward one:
the new name survives every later metadata import, because a schema is keyed internally by the
spreadsheet’s permanent id rather than by the name on show. The same key means retitling a workbook in
Google Drive neither renames nor forks its schema, and two spreadsheets that happen to share a title stay
two separate schemas.
Note
The schema is never dbo, so qualify your queries — [<spreadsheet>].[<tab>] — or set the
database’s default schema.
Authentication#
A connection authenticates as a Google service account, in one of two ways:
A DATABASE SCOPED CREDENTIAL holding the service account’s JSON key, named through the
credentialconfiguration key (see below). To obtain that key, see How to create a Google service account and its JSON key.Application Default Credentials when no credential is named — the ambient Google credentials of the machine running the Querona engine (for example
GOOGLE_APPLICATION_CREDENTIALS, or on a Google Cloud VM the instance’s attached service account). This avoids handling a key file at all, at the cost of every connection on that engine sharing one identity.
A credential belongs to exactly one virtual database, and the provider resolves it on the database that owns the connection — so the order matters: create the linked server (naming the credential), create the database, create the credential inside that database, and only then import metadata:
EXEC sp_addlinkedserver
@server = 'gs',
@srvproduct = 'Google Sheets',
@provstr = '{"dataSourcesConfiguration":"[{\"uriPath\":\"https://docs.google.com/spreadsheets/d/<spreadsheet-id>/edit\"}]","credential":"gs_cred"}';
CREATE DATABASE [sheetdb] WITH CONNECTION = [gs];
USE sheetdb;
CREATE DATABASE SCOPED CREDENTIAL gs_cred
WITH IDENTITY = '<service-account-email>', SECRET = '<service-account-json-key>';
EXEC sp_import_metadata @database = 'sheetdb';
The SECRET is the service account’s entire JSON key file, as one string. Naming a credential
that does not exist in the owning database fails the metadata import with an error explaining
exactly this.
Either way, the connection only ever requests the read-only
https://www.googleapis.com/auth/spreadsheets.readonly scope — the narrowest scope that can
still read a spreadsheet’s values and metadata.
Configuration options#
Set these through @provstr as JSON when creating the linked server, or through the matching
fields in the GUI — the read options on the provider form, the spreadsheets on the connection’s
Source URIs screen:
Option |
GUI field |
Default |
Meaning |
|---|---|---|---|
|
Source URIs |
(required) |
The spreadsheets this connection exposes, as a JSON array of objects — one schema each. Each
object needs a |
|
Database scoped credential |
(none) |
Name of the |
|
Type detection |
|
|
|
Scan rows limit |
|
How many rows of a tab are sampled per column when |
|
Use first row as column headers |
|
Whether a tab’s first row supplies the column names. Set to |
How columns get their types#
Metadata import reads a tab’s first row for column names, then samples up to scanRowsLimit
data rows per column to infer a type:
A column whose sampled values agree gets that type: whole numbers become
bigint, decimals and percentages becomefloat, checkboxes becomebit, and cells formatted as a date or time become a real date/time type.A column whose sampled values disagree — or offer no signal at all — is imported as text (
nvarchar(max)), so nothing is silently dropped.An elapsed-time column — formatted as
[h]:mm:ss, for example25:30:00for 25 hours 30 minutes — becomes a number of days, not a time value. SQL Server’stimetype cannot hold 24 hours or more and has no interval type, so the elapsed time arrives as its fractional-day value; multiply it by 24 to get hours.Error cells (
#DIV/0!,#N/A, and the like) and blank cells are read asNULL.
Set typeDetection=none to bypass inference entirely and import every column as text — useful
when a tab’s real-world data disagrees with what sampling guesses.
Limits worth knowing#
A query reads a whole tab — filters run inside Querona after the read, not inside Google Sheets, so a narrow
WHEREclause does not reduce what is fetched.Google’s Sheets API quota is 300 requests per minute per project and 60 per minute per user; heavy, repeated querying of the same tab can be throttled.
A tab name is capped at 100 characters.
A spreadsheet is capped at 10 million cells in total.
The provider cannot list or browse spreadsheets — you always supply the list. The Sheets API has no operation that enumerates them.
Metadata import costs roughly one API call per spreadsheet plus one per tab, so a connection listing many spreadsheets is likelier to meet the per-minute quota above.
See also#
Files and data services — the Files and data services overview