Bulk operations
Copy, clone, move, drop or truncate many tables at once. Pick a project, dataset or table, choose an action, filter by name and check the plan. Querylab.io writes the SQL into a new tab and you decide whether to run it.
Every action produces a script in a new query tab named after it (Drop 12 tables). Nothing runs until you press Run.
Opening it
- Right-click a project, dataset or table in the schema tree, or a table’s tab → Bulk table actions…. The target step is skipped.
- ⌘ K → Bulk table actions…, then search for a target. The entry is also in the list before you type anything.
There is no multi-select in the tree: the set of tables is the target plus a name filter.
The three steps
- Target: a project, dataset or table. Results are grouped (Recent, Projects, Datasets, Tables) and the part of each name you typed is in bold. Recent holds the targets you last opened a script for.
- Action: one of the five above. On a project you can’t write to, such as
bigquery-public-data, Move, Drop and Truncate are greyed out with the reason. - Review: a name filter, a destination for Copy, Clone and Move, a plan line and the list of what will happen to each object.
On a phone the dialog fills the screen, a close button sits at the top, and the footer with Open script stays at the bottom whatever the length of the list. There are no key hints there.
Esc goes back one step and closes the dialog from the first. Enter or ⌘ Enter opens the script from the review step. ↑ and ↓ scroll the list there.
The review step
The plan line sits at the top: 365 to drop · 4 skipped. Below it the objects are in groups you can open and close:
- Will copy / clone / move / drop / truncate: what the script touches.
- Skipped: matches the action can’t handle, one reason per row.
- Couldn’t list: datasets whose tables could not be read, with the error and a Retry button. The script leaves them out and says so in its header.
The script button reads Open script, 365 and stays off while the lists are loading and when nothing is left to run.
Destination
Copy, Clone and Move ask where the tables go. Type project.dataset in the Destination field and pick from the list. The button stays off until the destination is a dataset you can see and write to. There is no placeholder to fill in later: the script has the real name in every statement.
- Same location. BigQuery copies tables only within one location. The destination’s location shows under the field. If a source is somewhere else, the field says so, naming both (
proj.archive is in EU. The tables are in US.), and the button stays off. A multi-region such asUSand a region such asus-central1count as different locations. - Names already taken. Copy and Clone skip a table when the destination already has one with that name, and say so. The list is read fresh when you pick the destination. Move skips it too, see Move.
- Not the same dataset. A table already in the destination dataset is skipped, for Move too. Copying a table onto itself and then dropping the original would delete it.
- Read-only destinations. Public data and linked datasets (Analytics Hub) can’t be written to, and the field says so.
Name filter
The menu next to the filter picks how the text is compared with a table name:
| Mode | Matches |
|---|---|
| Contains | Any name with your text in it. The default for Copy and Clone. |
| Starts with | Names that begin with your text. The default for Move, Drop and Truncate. |
| Exact | The name and nothing else. Capital letters count. |
| Pattern | * is any text and ? one character. The whole name has to match. |
An empty filter matches every table. Contains, Starts with and Pattern ignore capitals.
What gets skipped
Views, materialized views, external tables and snapshots are left out of the script, so a script can’t stop halfway after it has already dropped other tables. Clone is the exception: it takes snapshots, which is how you restore one. A table whose type BigQuery did not report is skipped too, because the app won’t guess.
For Copy, Clone and Move, tables with the same name in two datasets of one project are both skipped. They would land under the same name in the destination. Pick one dataset as the target to get them.
For Move, Drop and Truncate, tables in a linked dataset are skipped too, because nothing in a linked dataset can be changed. The app learns that a dataset is linked from the dataset list, or from its details when you chose the dataset itself as the target. Tables of a linked dataset can still be copied or cloned out.
Date-sharded tables
Tables named events_20240101, events_20240102 and so on show as one row, events_*, with the shard count and date range. A family is every table with the same base name and a date suffix, as BigQuery’s events_* selects them, so events_202401 and events_20240101 count as one. Open the row to see every shard. The script has one statement per shard, so events_2023 on a dataset with 365 daily shards drops all 365. In the schema tree the same group is one entry, and choosing it as the target means the whole group.
What you get
-- Clone 3 tables from ace-analytics.warehouse to ace-analytics.archive (US) -- Filter: starts with "dim_". Skipped: 1 (1 view). -- Generated by Querylab.io on 2026-10-30. Not run yet. CREATE TABLE `ace-analytics.archive.dim_customer` CLONE `ace-analytics.warehouse.dim_customer`; CREATE TABLE `ace-analytics.archive.dim_date` CLONE `ace-analytics.warehouse.dim_date`; CREATE TABLE `ace-analytics.archive.dim_product` CLONE `ace-analytics.warehouse.dim_product`;
The header says what the script is for, where it goes, the filter, how many objects were skipped and why, and any dataset it could not list.
Copy tables writes a CREATE TABLE … COPY … per table: a full copy, billed as storage. Clone tables writes CREATE TABLE … CLONE …: zero-copy, you pay only for what diverges. Drop tables writes DROP TABLE IF EXISTS … and Truncate tables writes TRUNCATE TABLE …. Both are guarded when you run them, see Confirming a drop.
Move
BigQuery can’t wrap DDL in a transaction, so Move is a count, a copy, a check and a drop, in that order, and it is not atomic. Run executes the whole tab and stops at the first statement that fails, so a failure in the first three blocks means no drop runs.
-- Move 2 tables from ace-analytics.warehouse to ace-analytics.archive (US) -- Filter: starts with "dim_". Skipped: none. -- Not moved with the tables: IAM grants on each table, row access policies, views that read the old names. -- The check compares row counts, not contents. -- Generated by Querylab.io on 2026-10-30. Not run yet. -- 0. Count the rows of every table first. A table that cannot be counted stops the script before anything is copied. DECLARE rows_1 INT64 DEFAULT (SELECT COUNT(*) FROM `ace-analytics.warehouse.dim_customer`); DECLARE rows_2 INT64 DEFAULT (SELECT COUNT(*) FROM `ace-analytics.warehouse.dim_date`); -- 1. Copy CREATE TABLE IF NOT EXISTS `ace-analytics.archive.dim_customer` COPY `ace-analytics.warehouse.dim_customer`; CREATE TABLE IF NOT EXISTS `ace-analytics.archive.dim_date` COPY `ace-analytics.warehouse.dim_date`; -- 2. Check. Each ASSERT stops the script if a copy has a different row count. COUNT(*) on a whole table reads 0 bytes. ASSERT (SELECT COUNT(*) FROM `ace-analytics.archive.dim_customer`) = rows_1 AS 'dim_customer: row counts differ, nothing was dropped'; ASSERT (SELECT COUNT(*) FROM `ace-analytics.archive.dim_date`) = rows_2 AS 'dim_date: row counts differ, nothing was dropped'; -- 3. Drop the originals. Runs only if every count, copy and check above passed. DROP TABLE `ace-analytics.warehouse.dim_customer`; DROP TABLE `ace-analytics.warehouse.dim_date`;
- The first block reads the row count of every source into a variable, before anything is copied. A table that can’t be counted stops the script there, with nothing copied. The counts read 0 bytes. Move also leaves out, with a reason, any table whose metadata says it requires a partition filter, because it can’t be counted without one. The table list does not always carry that flag, so the first block is what catches the rest.
- Each check compares the copy with the count read at the start. It compares row counts, not contents, and the review step says so.
- A table that already exists in the destination is skipped, as in Copy. Move never takes a table that was already there when the script was made for its own copy, so it can’t drop an original on the strength of it.
IF NOT EXISTSlets you run the script again after a failure in the copy or check blocks: tables already copied are not copied again, and their counts are checked. A script that got as far as the drops can’t be run again from the top, because the first block can no longer count a table that is gone. Check what is left and generate a new script.- A move by copy does not carry everything. IAM grants on each table, row access policies and views that read the old names stay behind. The review step and the header both say so.
- The tab opens with a note: compare a table you care about. Its link opens Data compare with one source table on the left and its copy in the destination on the right. The copy does not exist before the first run, so Compare finds it only after the copy block has run, or on a second run.
Confirming a drop
Drop, Truncate and Move tabs are marked when they are made. Pressing Run on such a tab (the Run button, ⌘Enter, run selection or run statement) opens a dialog instead of running, when the SQL being sent has a DROP TABLE or TRUNCATE TABLE statement in it. Type the dataset’s name (the project’s id, if you chose a project) to enable the red button. It says how many tables go, Drop 365 tables. Cancel runs nothing.
- The dialog counts the statements that are about to run, so a selection of three drops says
Drop 3 tables. - It asks every time, whatever the count.
- The mark is decided from the first line of the script, the
-- Drop 365 tables from …header, as it is now. Change that line and the tab is your own script, and nothing asks. Undo it and the tab is guarded again. The Format and pipe conversion actions keep that line. - If the script can’t be read, or it runs
EXECUTE IMMEDIATEorCALL, which can drop tables the parser can’t see, the dialog asks anyway and says it couldn’t check. - Scheduling a marked tab that drops or truncates is refused, because a schedule would run it with nobody to type the name.
- A dropped table can be restored within the time travel window of its dataset, up to 7 days.
What the guard does not cover:
- It protects the tab, not the SQL. The Prism AI agent can read a tab and run SQL through
execute_query, which is governed by the AI write-approval setting, not by this guard. - A script copied into another tab, or a tab reopened from History, has no mark.
DROP SCHEMA … CASCADEis not counted: the guard looks forDROP TABLEandTRUNCATE TABLE.
The check reads the script with the SQL parser. A DROP built inside EXECUTE IMMEDIATE is a string to it, and is not found as a statement. The dialog asks anyway, and says it couldn’t check, for any marked script that has EXECUTE IMMEDIATE, CALL or a syntax error.
Checking before you move or drop
To find out whether two tables actually differ before you move or drop, see Data compare. Per-table CREATE TABLE DDL for a single table is in its details tab, not here.