A different dialect

Reading BigQuery errors

10 min
What you'll learn
  • to read the anatomy of a BigQuery error: the class, the culprit, the [line:character] position
  • to decode Unrecognized name and the Did you mean hint
  • to unpick the wall of text that is No matching signature: which argument did not fit, and why
  • to name the class of a refusal from its text, find the place it broke and choose the first thing to check

A packet marked "refused"

A packet surfaced in the station's queue: queries that earlier operators sent to the archive years ago. All of them marked "refused". The commandant wants to know what they were asking about: perhaps the operators were looking for the same thing you are.

You open the first one — and see not data but the archive's answer: a few lines of text, coordinates, a list of some signatures. The archive was not silent. It was explaining what was wrong — in its own language.

QUERY: I have no translator for the language of refusals — I have you. Five queries in the packet, and the refusals differ. Let us take all five apart.

A returned-mail alcove: a rack of old capsules sealed with red rejection wax. The cadet has opened one and it projects a diagnostic hologram in four bands of light, a thin beam pointing at the exact spot in the archive columns. The robot cat holds a seal in its paw.
The archive's refusal is not a wall but an explanation: the error class, the culprit, the position and the hint are read part by part.

Refusal #1: Unrecognized name

The first query from the packet:

SELECT name, colonny
FROM fa_users
ORDER BY user_id;

The archive's answer:

Unrecognized name: colonny; Did you mean colony? [at 1:14]

Read it piece by piece — the order works for any BigQuery error:

  1. Class: Unrecognized name — no such name (a column or an ) was found in the query's scope.
  2. Culprit: colonny — the name exactly as you wrote it.
  3. Hint: Did you mean colony? — BigQuery looks for a similar name and more often than not guesses right. But it is a heuristic: with a two-letter typo there may be no hint at all, and sometimes it offers a similar column that is not the one you wanted. Check against the schema, not on faith.
  4. Coordinates: [at 1:14] — line 1, character 14 of YOUR query. In a forty-line query that is the main navigator.

The fix is obvious: colony. The cell below is the repaired one. Now work the refusal archive yourself: put the typo colonny back, run it and compare the error text with the one we took apart.

How to read a BigQuery errorUnrecognized name: colonny;Did you mean colony? at [1:14]what happenedthe hint — often the fix itselfwhere exactly: line and column
An error has three useful parts: what happened, the hint, and the position in your query
The working version. Break it again — swap colony for colonny — and read the archive's refusal with your own eyes.

Refusal #2: Table not found

SELECT action, COUNT(*) AS n
FROM fa_logs_20291113
GROUP BY action;
Table not found: fa_logs_20291113 [at 2:6]

The class speaks for itself: there is no such table. A migrant usually has three reasons for it:

  • a typo in the name — as with a column, only the hint fires less often;
  • the wrong or project: the full address is project.dataset.table; it is easy to forget the backticks in it, and then FROM my-project.logs.events falls apart into the meaningless subtraction my - project;
  • the table really is not there: our log is sharded by day — fa_logs_20291110, ..._11, ..._12. The query above asks for the shard of 13 November, which the archive never got to write.

And yes: table and dataset names in BigQuery are case-sensitive — FA_LOGS_20291112 and fa_logs_20291112 are different names to the archive (column and function names are not).

The last recorded day is 12 November:

The log's last shard: three logins and one sync — on 12 November 2029 the archive was still alive.

Refusal #3: No matching signature — a wall of text that can be read

The third query in the packet wanted to add up the revenue:

SELECT SUM(status) AS paid_total
FROM fa_orders
WHERE status = 'paid';
No matching signature for aggregate function SUM
  Argument types: STRING
  Signature: SUM(INT64)
    Argument 1: Unable to coerce type STRING to expected type INT64
  Signature: SUM(UINT64)
    Argument 1: Unable to coerce type STRING to expected type UINT64
  ...
  Signature: SUM(NUMERIC)
    Argument 1: Unable to coerce type STRING to expected type NUMERIC
  ... [at 1:8]

It looks like a wall of text, but its structure is rigid:

  1. Class: No matching signature — the function exists, but NOT with argument types like these.
  2. What you passed: Argument types: STRING.
  3. The list of signatures — every variant the function accepts and, for each of them, which argument did not fit (Unable to coerce ...). That is not noise, it is the list of what is allowed. The first question to ask of it is not "how do I cast the type" but "did the right argument even go into the function". Casting a type makes sense only when the argument is the correct one and the signature genuinely does not accept it.

Here the intent was a sum over total, while SUM was handed the string column status: the operator mixed up the columns. BigQuery, unlike MySQL, never coerces a string to a number in silence:

8901.16 credits across ten paid orders — the supply station's revenue for the week.

Refusals #4 and #5: types in a function and in an operator

The last two queries in the packet break the same way — on types. They are shown side by side for comparison, and the coordinates in the messages refer to each query separately, as if you were running it alone:

-- #4: a short order number — "the last two digits"
SELECT order_id, SUBSTR(order_id, 2, 2) AS short_id
FROM fa_orders;
-- No matching signature for function SUBSTR
--   Argument types: INT64, INT64, INT64
--   Signature: SUBSTR(STRING, INT64, [INT64])
--     Argument 1: Unable to coerce type INT64 to expected type STRING
--   ... [at 1:18]

-- #5: the docking fee out of a string config
SELECT order_id, total + '25' AS with_docking_fee
FROM fa_orders;
-- No matching signature for operator +
--   Argument types: NUMERIC, STRING
--   ... [at 1:18]

Both refusals come from one migrant habit: hoping for an implicit cast. MySQL forgave SUBSTR over a number; many languages forgive number + 'string'. BigQuery is strict: cast explicitly. In #4 the intent is a string one — so CAST(order_id AS STRING); in #5 it is numeric — so CAST('25' AS NUMERIC).

Both fixes in one cell:

Number → string for SUBSTR, string → number for the addition. Both CASTs are safe: the types are known in advance, so SAFE_CAST is not needed.
Three error classes, three different movesnameUnrecognized namea typo or a foreign columntableNot found: Tablethe three-part addresstypeNo matching signaturean argument of the wrong typethe class of the error names the first thing to check
The class of the error names the first thing to check

Refusals the sandbox will never show you

The five refusals we took apart came down to three classes — an unknown name, a missing table and a signature mismatch: the archive (and our sandbox) finds them BEFORE reading any data, which is why such refusals are free. Production BigQuery has refusals of other levels too — the kind that leave a migrant staring:

  • billing and quotas: Quota exceeded and its relatives — the query hit the project's limits or a disabled billing account; the fix is not in the SQL but in the project settings;
  • the partition filter: Cannot query over table ... without a filter over column(s) ... that can be used for partition elimination — the table's owner has banned expensive queries that carry no partition filter; what that machinery is, the chapter "Money and scale" explains;
  • the resource limit: Resources exceeded — the query is too greedy for the memory it was given; a frequent culprit is ORDER BY over an entire log with no LIMIT.

They cannot be reproduced in the sandbox — there is neither billing nor quotas here. Remember the fact itself: if the text of a refusal is not about names and not about types, read it as an invoice or as a ban, not as a bug in your SQL.

Check yourself
An operator has sent you the query SELECT SUM(total) AS credits_total FROM fa_payments and the refusal Unrecognized name: total [at 1:12]. What is the reason, and what is the minimal fix?
Check yourself
The archive's refusal: No matching signature for operator + Argument types: NUMERIC, STRING ... on the query SELECT total + '25' AS with_fee FROM fa_orders. Which fix is minimal and keeps the meaning?
Practice: solve the tasks
Solved 0 of 3 · any 2 is enough to pass
Key takeaways
ClassWhat it meansThe first fix
Unrecognized name: x; Did you mean y?the name was not found; the hint is a heuristiccheck the name against the schema; [row:col] leads you to the spot
Table not foundno such table: a typo, the wrong /project, the casethe full address in backticks; does the shard exist?
No matching signaturethe function exists, the types are wrongfind your intent in the list of signatures and cast the type explicitly
Quota exceeded, Cannot query over table without a filter ...not an SQL bug: quotas, billing, a ban on expensive queriesthe project settings; the partition filter (chapter 5)
  • the coordinates [at line:character] are the navigator through your own query
  • BigQuery does not cast types in silence: an explicit CAST is part of the

Documentation: error messages, query syntax.

The chapter is closed: you speak the dialect, you know the price of words and you can read a refusal. But QUERY has already found something in the logs that it did not understand: DATE_DIFF counted a whole day where two hours had passed. The next chapter is the type system and the four kinds of time: the places where Postgres intuition lies most quietly.