BigQuery: аналитика на петабайтах
Киллер-фича Google Cloud: SQL по терабайтам за секунды, без единого сервера. И главная ловушка новичка — счёт, который выставляют не за время запроса, а за просканированные байты.
BigQuery — serverless-хранилище аналитических данных: вы пишете обычный SQL, Google сам поднимает тысячи машин на несколько секунд, а платите вы за объём данных, который запрос прочитал.
Зачем нужен ещё один SQL, если есть Cloud SQL
Cloud SQL — это OLTP-база: тысячи мелких запросов вида «дай заказ №42», «обнови статус». Она отлично держит нагрузку приложения и абсолютно беспомощна, когда аналитик пишет:
SELECT country, COUNT(*), SUM(amount)
FROM events
WHERE event_date BETWEEN '2020-01-01' AND '2026-07-01'
GROUP BY country;
По таблице на 3 миллиарда строк такой запрос в PostgreSQL будет идти минут сорок и попутно положит продакшен. BigQuery выполнит его секунд за пять — и не потому, что «Google быстрее», а потому что устроен принципиально иначе.
Два архитектурных решения делают всю разницу:
- Разделение хранения и вычислений. Данные лежат в распределённой файловой системе, а вычислительные мощности берутся из общего пула на время запроса. Нет «сервера БигКвери», который вы арендуете. Есть запрос — есть машины, нет запроса — нет счёта за вычисления.
- Колоночное хранилище. Строки не хранятся целиком. Каждая колонка лежит отдельным сжатым массивом.
Колоночное хранилище: почему это меняет всё
Представьте таблицу событий с 60 колонками, где 55 из них — тяжёлый JSON-payload, user-agent и прочий мусор. Строчная база (PostgreSQL) для запроса «посчитай события по странам» вынуждена прочитать каждую строку целиком: страна лежит вперемешку со всем остальным на одной странице диска.
BigQuery хранит колонку country отдельным файлом. Нужен только country — читается только он. Соседние 59 колонок не трогаются вообще. Плюс однотипные значения в колонке сжимаются в разы лучше, чем разнородная строка.
Отсюда — главное правило BigQuery, из которого следуют все остальные:
Вы платите за просканированные байты, а объём скана определяется тем, какие колонки вы перечислили в
SELECTи какие партиции отсекли вWHERE.
Сколько это стоит
| За что | Порядок цены | Бесплатный лимит |
| Запросы (on-demand) | ≈ $6,25 за просканированный ТиБ | 1 ТиБ в месяц |
| Активное хранение | ≈ $0,02 за ГиБ/мес | 10 ГиБ |
| Долговременное хранение | ≈ $0,01 за ГиБ/мес | — |
| Streaming inserts | ≈ $0,01 за 200 МиБ | — |
Долговременное хранение включается само: если таблицу (или конкретную партицию) не меняли 90 дней, цена за неё падает вдвое. Ничего настраивать не нужно.
А теперь арифметика, ради которой написан этот урок. Таблица событий весит 2 ТиБ. Аналитик пишет:
SELECT * FROM analytics.events LIMIT 10;
Кажется, что это «посмотреть десять строчек» — операция на копейку. На самом деле: * означает «прочитать все колонки», отсутствие WHERE — «все партиции». Просканировано 2 ТиБ, к оплате около $12,50. За просмотр десяти строк.
LIMIT не уменьшает скан. Это самая контринтуитивная вещь во всём BigQuery. Лимит применяется после того, как данные прочитаны и обработаны движком: он ограничивает размер результата, а не объём чтения. Запомните это раз и навсегда — на этом сгорел не один бюджет.
Как смотреть данные бесплатно
- Вкладка Preview в консоли (и
bq head) читает данные напрямую, минуя движок запросов, — бесплатно. Именно так и надо «просто посмотреть, что в таблице». SELECT COUNT(*)не читает колонки — тоже почти бесплатно, метаданные.- Кэш результатов: повторный идентичный запрос к неизменившейся таблице отдаётся из кэша бесплатно (кэш живёт 24 часа). Он ломается, если в запросе есть недетерминированные функции вроде
CURRENT_TIMESTAMP().
Три приёма, чтобы не разориться
1. Dry run: узнать цену до запуска
BigQuery умеет посчитать объём скана, не выполняя запрос. Это бесплатно и занимает доли секунды — привыкайте делать это перед любым тяжёлым запросом.
bq query --use_legacy_sql=false --dry_run \
'SELECT * FROM `my-project.analytics.events`'
Результат:
Query successfully validated. Assuming the tables are not modified,
running this query will process 2199023255552 bytes of data.
2 199 023 255 552 байт — это ровно 2 ТиБ, то есть примерно $12,50. А теперь то же самое, но аккуратно:
bq query --use_legacy_sql=false --dry_run \
'SELECT event_name, COUNT(*) AS c
FROM `my-project.analytics.events`
WHERE event_time >= TIMESTAMP("2026-07-01")
GROUP BY event_name'
Результат:
Query successfully validated. Assuming the tables are not modified,
running this query will process 1476395008 bytes of data.
1,4 ГиБ вместо 2 ТиБ — в полторы тысячи раз меньше. Разница только в том, что мы назвали нужные колонки и отсекли партиции.
2. Предохранитель maximum_bytes_billed
Запрос, который собирается прочитать больше разрешённого, просто не запустится. Это ставят и на конкретный запрос, и на весь проект — обязательная гигиена, если к данным есть доступ у команды.
# не дороже ~1 ГиБ скана: всё, что больше, — упадёт с ошибкой, а не со счётом
bq query --use_legacy_sql=false --maximum_bytes_billed=1073741824 \
'SELECT country, COUNT(*) FROM `my-project.analytics.events` GROUP BY country'
3. Партиционирование и кластеризация
Партиционирование физически разрезает таблицу на куски по значению одной колонки (чаще всего — по дате). Запрос с фильтром по этой колонке читает только нужные куски. Таблица за пять лет, запрос за один день — сканируется 1/1800 данных.
CREATE TABLE analytics.events (
event_time TIMESTAMP,
user_id STRING,
event_name STRING,
country STRING,
payload JSON
)
PARTITION BY DATE(event_time)
CLUSTER BY event_name, country
OPTIONS (
require_partition_filter = TRUE,
partition_expiration_days = 400
);
Здесь сделано четыре важных вещи:
PARTITION BY DATE(event_time)— по одной партиции на календарный день.CLUSTER BY— внутри партиции данные ещё и физически отсортированы поevent_nameиcountry. Фильтр по этим полям позволяет движку пропускать целые блоки. Кластеризация — это «второй этаж» отсечения, уже внутри партиции.require_partition_filter = TRUE— золотая настройка. Запрос без фильтра по партиции просто не выполнится: BigQuery вернёт ошибку вместо счёта на $12,50. Ставьте её на каждой большой таблице.partition_expiration_days— партиции старше 400 дней удаляются сами, вы не платите за древние данные.
Ошибка при отсутствующем фильтре выглядит так — и это лучшая ошибка в вашей жизни:
Cannot query over table 'analytics.events' without a filter over column(s)
'event_time' that can be used for partition elimination.
Из кода запросы выглядят обычно — важно лишь передавать параметры, а не склеивать SQL строками:
from google.cloud import bigquery
client = bigquery.Client()
job_config = bigquery.QueryJobConfig(
# предохранитель: не дороже 1 ГиБ скана
maximum_bytes_billed=1_073_741_824,
query_parameters=[
bigquery.ScalarQueryParameter("day", "DATE", "2026-07-01"),
],
)
sql = """
SELECT event_name, COUNT(*) AS c
FROM `my-project.analytics.events`
WHERE DATE(event_time) = @day
GROUP BY event_name
ORDER BY c DESC
"""
for row in client.query(sql, job_config=job_config).result():
print(row.event_name, row.c)
Как это работает
Когда вы жмёте «Выполнить», происходит примерно следующее. Планировщик разбирает SQL и строит дерево выполнения. Данные лежат колонками в сжатом формате Capacitor поверх распределённой файловой системы Colossus. Из общего пула выделяются слоты — единицы вычислений; каждый слот берёт свой кусок колонок и считает частичный результат. Промежуточные данные между этапами (например, для GROUP BY) прогоняются через shuffle по сети, которая внутри дата-центра работает на сотнях гигабит. Результаты сливаются и отдаются вам.
Практические следствия этой архитектуры:
- BigQuery не для OLTP. Точечный
UPDATE ... WHERE id = 42здесь выполним, но это дорогая перезапись блоков, да ещё и с квотами на количество DML-операций. Оперативные данные приложения живут в Cloud SQL, а в BigQuery попадают уже как поток событий. - Нет индексов в привычном смысле. Ускоряют не индексы, а партиционирование, кластеризация и отказ от лишних колонок.
- Скорость почти не зависит от размера таблицы — зависит от объёма скана. Запрос по 1 ГиБ из таблицы в 100 ТиБ будет быстрым.
- JOIN гигантских таблиц дорог по shuffle. Маленький справочник BigQuery сам «размножит» по узлам (broadcast join), а вот два многотерабайтных монстра честно поедут по сети.
Частые ошибки
SELECT *на большой таблице. Убивает всю выгоду колоночного хранения: вы читаете все 60 колонок ради двух. Перечисляйте колонки явно — всегда.- Верить, что
LIMIT 10дёшев. Не дёшев: лимит режет результат, а не скан. Чтобы «посмотреть данные», пользуйтесь Preview илиbq head— они бесплатны. - Таблица без партиционирования. Каждый запрос читает всю историю. Партиция по дате — первое, что делают с любой таблицей событий.
- Партиционирование есть, а фильтра нет. Партиции не работают магически: без
WHEREпо партиционной колонке сканируется всё. Ставьтеrequire_partition_filter = TRUEи спите спокойно. - Фильтр по обёрнутой колонке. Условие вида
WHERE CAST(event_time AS STRING) LIKE '2026-07%'ломает отсечение партиций: BigQuery не может понять, какие куски нужны, и читает всё. Фильтруйте по колонке напрямую. - Нет
maximum_bytes_billed. Один невнимательный аналитик с доступом к продовым данным способен за минуту сжечь месячный бюджет. Предохранитель на уровне проекта стоит поставить в первый же день. - Стриминг вместо батча. Streaming inserts удобны, но платны за каждый мегабайт. Если данные не нужны в реальном времени, грузите файлами из Cloud Storage — загрузка бесплатна.
- Использовать BigQuery как основную базу приложения. Он не даёт ни низкой задержки на точечный запрос, ни транзакций в привычном смысле. Это хранилище для аналитики.
Итоги
- BigQuery — serverless-аналитика: хранение и вычисления разделены, серверов нет, SQL обычный.
- Колоночное хранение означает, что цена запроса зависит от перечисленных колонок и отсечённых партиций, а не от размера таблицы.
- Оплата — примерно $6,25 за просканированный ТиБ; первый 1 ТиБ в месяц и 10 ГиБ хранения бесплатны.
SELECT *дорог, аLIMITне спасает: он режет результат, а не объём чтения.- Три обязательных привычки:
--dry_runперед тяжёлым запросом,maximum_bytes_billedкак предохранитель,PARTITION BYплюсrequire_partition_filterна каждой большой таблице. - Кластеризация — второй уровень отсечения внутри партиции; долговременное хранение (90 дней без изменений) само делает данные вдвое дешевле.
- BigQuery не заменяет Cloud SQL: оперативные данные — там, аналитика — здесь.