Советы
Как выгрузить сырые данные из Google Ads в Google BigQuery
Анализируя эффективность рекламных кампаний Google Ads в Google Analytics, вы можете столкнуться с семплированием, агрегированием данных и другими ограничениями, обусловленными интерфейсом систем. К счастью, эта проблема легко решается выгрузкой сырых данных из рекламного сервиса в Google BigQuery.
В этой статье вы узнаете, как выгрузить сырые данные из вашего аккаунта Google Ads в BigQuery и определить при этом все UTM-метки кампаний с авторазметкой.
Чтобы связать информацию о рекламных кампаниях с действиями пользователей на сайте, вам понадобится OWOX BI. Запишитесь на демо, и мы подробно расскажем о всех задачах, которые вы можете решить с OWOX BI.
Содержание
- Зачем нужны сырые данные из Google Ads
- Два способа выгрузки сырых данных из Google Ads в BigQuery
- Как настроить выгрузку с помощью Data Transfer
- Как настроить выгрузку с помощью Ads Script
- Как подключить выгрузку из Google Ads к OWOX BI
- Полезные советы


Зачем нужны сырые данные из Google Ads
Сырые данные из Google Ads позволят вам анализировать рекламные кампании с точностью до ключевого слова. Выгрузив их в BigQuery, вы сможете:
- Строить отчеты нужной вам глубины в разрезе любых параметров и не зависеть от ограничений GA.
- Определять эффективность рекламных кампаний на уровне сессии и пользователей.
- Рассчитывать ROI, ROAS, ДРР в разрезе региона, типа пользователя (новый или вернувшийся), устройства и любых других параметров.
- Эффективно управлять рекламными ставками и создавать списки ремаркетинга.
- Объединить данные из Google Ads, Google Analytics и CRM, чтобы оценить эффективность кампаний с учетом маржинальности и выкупаемости ваших товаров.
- Обучать свою ML-модель для более точного планирования.
Чтобы понять, какие кампании, объявления и ключевые слова приводят пользователей на ваш сайт, нужно объединить данные из Google Ads и Analytics в BigQuery. Сделать это можно с помощью потока OWOX BI.
Этот поток передает в GBQ несемплированные данные о поведении пользователей на вашем сайте. Хиты передаются в режиме реального времени, затем на основе этих хитов формируются сессии.
Информацию об источнике трафика OWOX BI берет из UTM-разметки объявлений. Разметка бывает ручной и автоматической.
Допустим, вы разметили объявление вручную и у вас получился такой URL:
www://example. com/? utm_source=facebook&utm_medium=cpc&utm_campaign=utm_metki
В таком случае после подключения OWOX BI у вас в таблице GBQ будут доступны данные об источнике, канале и кампании:
- trafficSource.source — google
- trafficSource.medium — cpc
- trafficSource.campaign — utm_metki
Если же вы включили в рекламном сервисе функцию автоматической разметки, то каждому вашему объявлению присваивается специальный параметр gclid. Он добавляется в URL целевой страницы, когда пользователь нажимает на объявление. Пример такой ссылки:
http://www. example. com/? gclid=TeSter-123
Если вы используете авторазметку, то получить значение source, medium и campaign из gclid без сырых данных не получится — в таблицах BigQuery, которые собирает OWOX BI, эти поля будут пустыми.
Что же делать в таком случае и как получить название кампаний и других параметров, имея только gclid? Настроить автоматическую выгрузку из Google Ads в GBQ.
Примечание: если объявление не размечено вообще, OWOX BI определит переход по ссылке:
- Для не-Google источников как реферальный трафик (например, facebook/referral).
- Для Google источника как прямой трафик (direct/none).
Если в ваших отчетах очень много direct/none трафика, возможно, у вас не включена фильтрация ботов или большое количество неразмеченных объявлений.
Два способа выгрузки сырых данных из Google Ads в BigQuery
Для выгрузки сырых данных из Google Ads мы используем сами и рекомендуем клиентам два метода: коннектор Data Transfer и Ads Script. Какой способ выбрать, зависит от ваших целей и объема данных в рекламном кабинете.
Особенности Data Transfer:
- Нативная интеграция с GBQ.
- Выгрузка исторических данных за любой период без ограничений.
- С 01.01.2020 Data Transfer для Google Ads стал бесплатным. До этого взималась плата 2,5 $ в месяц за выгрузку из каждого аккаунта Google Ads.
Особенности Google Ads Script:
- Бесплатно.
- Нельзя выгрузить исторические данные. Загружается информация только за предыдущий день.
- Требует больше усилий, если нужно настроить выгрузку из большого количества аккаунтов. Необходимо будет вручную вносить изменения в скрипты по каждому аккаунту. При этом высокий риск совершить ошибку.
Что понадобится для настроек
Активные проекты и аккаунты в:
- Google Cloud Platform (GCP)
- Google BigQuery (GBQ)
- OWOX BI
- Google Ads
Доступы уровня:
- Владелец в GCP
- Администратор в GBQ
- Редактирование в OWOX BI. Важно: только тот пользователь, который создавал поток Google Analytics → Google BigQuery, может подключать выгрузку из Google Ads.
- Чтение в Google Ads
Как предоставить права доступа в GBQ

Как настроить выгрузку с помощью Data Transfer
Шаг 1. Создайте проект в Google Cloud Platform


Шаг 2. Включите API BigQuery





Шаг 3. Активируйте Data Transfer API

Шаг 4. Подготовьте набор данных в GBQ

Шаг 5. Настройте передачу данных из Google Ads
В разделе «Настройки расписания» вы можете оставить значение по умолчанию «Начать» (то есть сейчас) или установить нужные вам дату и время для начала выгрузки. В поле «Повтор» выберите, как часто выполнять выгрузку: ежедневно, еженедельно, ежемесячно по требованию и т. д.


В результате вы получите в GBQ большое количество сырых данных, с которыми сможете работать: таблицы по кампаниям, аудиториям, общие (кастомные) таблицы, по ключевым словам, по конверсиям. Например, если вы захотите построить какой-нибудь кастомный дашборд, то сможете вытянуть неагрегированные данные из этих таблиц.
Как настроить выгрузку с помощью Ads Script



Скопируйте скрипт, приведенный ниже. В строках BIGQUERY_PROJECT_ID, BIGQUERY_DATASET_ID и Your email замените имеющиеся значения на свои: название проекта, набора данных в GBQ и email. Вставьте текст скрипта в текстовый редактор.
/**
* @name Export Data to BigQuery
*
* @overview The Export Data to BigQuery script sets up a BigQuery
* dataset and tables, downloads a report from AdWords and then
* loads the report to BigQuery.
*
* @author AdWords Scripts Team [adwords-scripts@googlegroups.com]
*
* @version 1.3
*/
var CONFIG = {
BIGQUERY_PROJECT_ID: 'BQ project name',
BIGQUERY_DATASET_ID: AdWordsApp.currentAccount().getCustomerId().replace(/-/g, '_'),
// Truncate existing data, otherwise will append.
TRUNCATE_EXISTING_DATASET: false,
TRUNCATE_EXISTING_TABLES: true,
// Lists of reports and fields to retrieve from AdWords.
REPORTS: [],
RECIPIENT_EMAILS: [
'Your email'
]
};
var report = {
NAME: 'CLICK_PERFORMANCE_REPORT', //https://developers.google.com/adwords/api/docs/appendix/reports/click-performance-report
CONDITIONS: '',
FIELDS: {'AccountDescriptiveName': 'STRING',
'AdFormat': 'STRING',
'AdGroupId': 'STRING',
'AdGroupName': 'STRING',
'AoiCountryCriteriaId': 'STRING',
'CampaignId': 'STRING',
'CampaignLocationTargetId': 'STRING',
'CampaignName': 'STRING',
'CampaignStatus': 'STRING',
'Clicks': 'INTEGER',
'ClickType': 'STRING',
'CreativeId': 'STRING',
'CriteriaId': 'STRING',
'CriteriaParameters': 'STRING',
'Date': 'DATE',
'Device': 'STRING',
'ExternalCustomerId': 'STRING',
'GclId': 'STRING',
'KeywordMatchType': 'STRING',
'LopCountryCriteriaId': 'STRING',
'Page': 'INTEGER'
},
DATE_RANGE: new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, "")+','+new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, ""),
DATE: new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, "")
};
//Regular export
CONFIG.REPORTS.push(JSON.parse(JSON.stringify(report)));
//One-time historical export
//for(var i=2;i<91;i++){
// report.DATE_RANGE = new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, "")+','+new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, "");
// report.DATE = new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, "");
// CONFIG.REPORTS.push(JSON.parse(JSON.stringify(report)));
//}
/**
* Main method
*/
function main() {
createDataset();
for (var i = 0; i < CONFIG.REPORTS.length; i++) {
var reportConfig = CONFIG.REPORTS[i];
createTable(reportConfig);
}
var jobIds = processReports();
waitTillJobsComplete(jobIds);
sendEmail(jobIds);
}
/**
* Creates a new dataset.
*
* If a dataset with the same id already exists and the truncate flag
* is set, will truncate the old dataset. If the truncate flag is not
* set, then will not create a new dataset.
*/
function createDataset() {
if (datasetExists()) {
if (CONFIG.TRUNCATE_EXISTING_DATASET) {
BigQuery.Datasets.remove(CONFIG.BIGQUERY_PROJECT_ID,
CONFIG.BIGQUERY_DATASET_ID, {'deleteContents' : true});
Logger.log('Truncated dataset.');
} else {
Logger.log('Dataset %s already exists. Will not recreate.',
CONFIG.BIGQUERY_DATASET_ID);
return;
}
}
// Create new dataset.
var dataSet = BigQuery.newDataset();
dataSet.friendlyName = CONFIG.BIGQUERY_DATASET_ID;
dataSet.datasetReference = BigQuery.newDatasetReference();
dataSet.datasetReference.projectId = CONFIG.BIGQUERY_PROJECT_ID;
dataSet.datasetReference.datasetId = CONFIG.BIGQUERY_DATASET_ID;
dataSet = BigQuery.Datasets.insert(dataSet, CONFIG.BIGQUERY_PROJECT_ID);
Logger.log('Created dataset with id %s.', dataSet.id);
}
/**
* Checks if dataset already exists in project.
*
* @return {boolean} Returns true if dataset already exists.
*/
function datasetExists() {
// Get a list of all datasets in project.
var datasets = BigQuery.Datasets.list(CONFIG.BIGQUERY_PROJECT_ID);
var datasetExists = false;
// Iterate through each dataset and check for an id match.
if (datasets.datasets != null) {
for (var i = 0; i < datasets.datasets.length; i++) {
var dataset = datasets.datasets[i];
if (dataset.datasetReference.datasetId == CONFIG.BIGQUERY_DATASET_ID) {
datasetExists = true;
break;
}
}
}
return datasetExists;
}
/**
* Creates a new table.
*
* If a table with the same id already exists and the truncate flag
* is set, will truncate the old table. If the truncate flag is not
* set, then will not create a new table.
*
* @param {Object} reportConfig Report configuration including report name,
* conditions, and fields.
*/
function createTable(reportConfig) {
var tableName = reportConfig.NAME+reportConfig.DATE;
if (tableExists(tableName)) {
if (CONFIG.TRUNCATE_EXISTING_TABLES) {
BigQuery.Tables.remove(CONFIG.BIGQUERY_PROJECT_ID,
CONFIG.BIGQUERY_DATASET_ID, tableName);
Logger.log('Truncated table %s.', tableName);
} else {
Logger.log('Table %s already exists. Will not recreate.',
tableName);
return;
}
}
// Create new table.
var table = BigQuery.newTable();
var schema = BigQuery.newTableSchema();
var bigQueryFields = [];
// Add each field to table schema.
var fieldNames = Object.keys(reportConfig.FIELDS);
for (var i = 0; i < fieldNames.length; i++) {
var fieldName = fieldNames[i];
var bigQueryFieldSchema = BigQuery.newTableFieldSchema();
bigQueryFieldSchema.description = fieldName;
bigQueryFieldSchema.name = fieldName;
bigQueryFieldSchema.type = reportConfig.FIELDS[fieldName];
bigQueryFields.push(bigQueryFieldSchema);
}
schema.fields = bigQueryFields;
table.schema = schema;
table.friendlyName = tableName;
table.tableReference = BigQuery.newTableReference();
table.tableReference.datasetId = CONFIG.BIGQUERY_DATASET_ID;
table.tableReference.projectId = CONFIG.BIGQUERY_PROJECT_ID;
table.tableReference.tableId = tableName;
table = BigQuery.Tables.insert(table, CONFIG.BIGQUERY_PROJECT_ID,
CONFIG.BIGQUERY_DATASET_ID);
Logger.log('Created table with id %s.', table.id);
}
/**
* Checks if table already exists in dataset.
*
* @param {string} tableId The table id to check existence.
*
* @return {boolean} Returns true if table already exists.
*/
function tableExists(tableId) {
// Get a list of all tables in the dataset.
var tables = BigQuery.Tables.list(CONFIG.BIGQUERY_PROJECT_ID,
CONFIG.BIGQUERY_DATASET_ID);
var tableExists = false;
// Iterate through each table and check for an id match.
if (tables.tables != null) {
for (var i = 0; i < tables.tables.length; i++) {
var table = tables.tables[i];
if (table.tableReference.tableId == tableId) {
tableExists = true;
break;
}
}
}
return tableExists;
}
/**
* Process all configured reports
*
* Iterates through each report to: retrieve AdWords data,
* backup data to Drive (if configured), load data to BigQuery.
*
* @return {Array.<string>} jobIds The list of all job ids.
*/
function processReports() {
var jobIds = [];
// Iterate over each report type.
for (var i = 0; i < CONFIG.REPORTS.length; i++) {
var reportConfig = CONFIG.REPORTS[i];
Logger.log('Running report %s', reportConfig.NAME);
// Get data as csv
var csvData = retrieveAdwordsReport(reportConfig);
//Logger.log(csvData);
// Convert to Blob format.
var blobData = Utilities.newBlob(csvData, 'application/octet-stream');
// Load data
var jobId = loadDataToBigquery(reportConfig, blobData);
jobIds.push(jobId);
}
return jobIds;
}
/**
* Retrieves AdWords data as csv and formats any fields
* to BigQuery expected format.
*
* @param {Object} reportConfig Report configuration including report name,
* conditions, and fields.
*
* @return {string} csvData Report in csv format.
*/
function retrieveAdwordsReport(reportConfig) {
var fieldNames = Object.keys(reportConfig.FIELDS);
var query = 'SELECT ' + fieldNames.join(', ') +
' FROM ' + reportConfig.NAME + '' + reportConfig.CONDITIONS +
' DURING ' + reportConfig.DATE_RANGE;
Logger.log(query);
var report = AdWordsApp.report(query);
var rows = report.rows();
var csvRows = [];
// Header row
csvRows.push(fieldNames.join(','));
// Iterate over each row.
while (rows.hasNext()) {
var row = rows.next();
var csvRow = [];
for (var i = 0; i < fieldNames.length; i++) {
var fieldName = fieldNames[i];
var fieldValue = row[fieldName].toString();
var fieldType = reportConfig.FIELDS[fieldName];
// Strip off % and perform any other formatting here.
if (fieldType == 'FLOAT' || fieldType == 'INTEGER') {
if (fieldValue.charAt(fieldValue.length - 1) == '%') {
fieldValue = fieldValue.substring(0, fieldValue.length - 1);
}
fieldValue = fieldValue.replace(/,/g,'');
if (fieldValue == '--' || fieldValue == 'Unspecified') {
fieldValue = ''
}
}
// Add double quotes to any string values.
if (fieldType == 'STRING') {
if (fieldValue == '--') {
fieldValue = ''
}
fieldValue = fieldValue.replace(/"/g, '""');
fieldValue = '"' + fieldValue + '"'
}
csvRow.push(fieldValue);
}
csvRows.push(csvRow.join(','));
}
Logger.log('Downloaded ' + reportConfig.NAME + ' with ' + csvRows.length +
' rows.');
return csvRows.join('\n');
}
/**
* Creates a BigQuery insertJob to load csv data.
*
* @param {Object} reportConfig Report configuration including report name,
* conditions, and fields.
* @param {Blob} data Csv report data as an 'application/octet-stream' blob.
*
* @return {string} jobId The job id for upload.
*/
function loadDataToBigquery(reportConfig, data) {
// Create the data upload job.
var job = {
configuration: {
load: {
destinationTable: {
projectId: CONFIG.BIGQUERY_PROJECT_ID,
datasetId: CONFIG.BIGQUERY_DATASET_ID,
tableId: reportConfig.NAME + reportConfig.DATE
},
skipLeadingRows: 1
}
}
};
var insertJob = BigQuery.Jobs.insert(job, CONFIG.BIGQUERY_PROJECT_ID, data);
Logger.log('Load job started for %s. Check on the status of it here: ' +
'https://bigquery.cloud.google.com/jobs/%s', reportConfig.NAME,
CONFIG.BIGQUERY_PROJECT_ID);
return insertJob.jobReference.jobId;
}
/**
* Polls until all jobs are 'DONE'.
*
* @param {Array.<string>} jobIds The list of all job ids.
*/
function waitTillJobsComplete(jobIds) {
var complete = false;
var remainingJobs = jobIds;
while (!complete) {
if (AdWordsApp.getExecutionInfo().getRemainingTime() < 5){
Logger.log('Script is about to timeout, jobs ' + remainingJobs.join(',') +
' are still incomplete.');
}
remainingJobs = getIncompleteJobs(remainingJobs);
if (remainingJobs.length == 0) {
complete = true;
}
if (!complete) {
Logger.log(remainingJobs.length + ' jobs still being processed.');
// Wait 5 seconds before checking status again.
Utilities.sleep(5000);
}
}
Logger.log('All jobs processed.');
}
/**
* Iterates through jobs and returns the ids for those jobs
* that are not 'DONE'.
*
* @param {Array.<string>} jobIds The list of job ids.
*
* @return {Array.<string>} remainingJobIds The list of remaining job ids.
*/
function getIncompleteJobs(jobIds) {
var remainingJobIds = [];
for (var i = 0; i < jobIds.length; i++) {
var jobId = jobIds[i];
var getJob = BigQuery.Jobs.get(CONFIG.BIGQUERY_PROJECT_ID, jobId);
if (getJob.status.state != 'DONE') {
remainingJobIds.push(jobId);
}
}
return remainingJobIds;
}
/**
* Sends a notification email that jobs have completed loading.
*
* @param {Array.<string>} jobIds The list of all job ids.
*/
function sendEmail(jobIds) {
var html = [];
html.push(
'<html>',
'<body>',
'<table width=800 cellpadding=0 border=0 cellspacing=0>',
'<tr>',
'<td colspan=2 align=right>',
"<div style='font: italic normal 10pt Times New Roman, serif; " +
"margin: 0; color: #666; padding-right: 5px;'>" +
'Powered by AdWords Scripts</div>',
'</td>',
'</tr>',
"<tr bgcolor='#3c78d8'>",
'<td width=500>',
"<div style='font: normal 18pt verdana, sans-serif; " +
"padding: 3px 10px; color: white'>Adwords data load to " +
"Bigquery report</div>",
'</td>',
'<td align=right>',
"<div style='font: normal 18pt verdana, sans-serif; " +
"padding: 3px 10px; color: white'>",
AdWordsApp.currentAccount().getCustomerId(),
'</tr>',
'</table>',
'<table width=800 cellpadding=0 border=1 cellspacing=0>',
"<tr bgcolor='#ddd'>",
"<td style='font: 12pt verdana, sans-serif; " +
'padding: 5px 0px 5px 5px; background-color: #ddd; ' +
"text-align: left'>Report</td>",
"<td style='font: 12pt verdana, sans-serif; " +
'padding: 5px 0px 5px 5px; background-color: #ddd; ' +
"text-align: left'>JobId</td>",
"<td style='font: 12pt verdana, sans-serif; " +
'padding: 5px 0px 5x 5px; background-color: #ddd; ' +
"text-align: left'>Rows</td>",
"<td style='font: 12pt verdana, sans-serif; " +
'padding: 5px 0px 5x 5px; background-color: #ddd; ' +
"text-align: left'>State</td>",
"<td style='font: 12pt verdana, sans-serif; " +
'padding: 5px 0px 5x 5px; background-color: #ddd; ' +
"text-align: left'>ErrorResult</td>",
'</tr>',
createTableRows(jobIds),
'</table>',
'</body>',
'</html>');
MailApp.sendEmail(CONFIG.RECIPIENT_EMAILS.join(','),
'Adwords data load to Bigquery Complete', '',
{htmlBody: html.join('\n')});
}
/**
* Creates table rows for email report.
*
* @param {Array.<string>} jobIds The list of all job ids.
*/
function createTableRows(jobIds) {
var html = [];
for (var i = 0; i < jobIds.length; i++) {
var jobId = jobIds[i];
var job = BigQuery.Jobs.get(CONFIG.BIGQUERY_PROJECT_ID, jobId);
var errorResult = ''
if (job.status.errorResult) {
errorResult = job.status.errorResult;
}
html.push('<tr>',
"<td style='padding: 0px 10px'>" +
job.configuration.load.destinationTable.tableId + '</td>',
"<td style='padding: 0px 10px'>" + jobId + '</td>',
"<td style='padding: 0px 10px'>" + job.statistics.load?job.statistics.load.outputRows:0 + '</td>',
"<td style='padding: 0px 10px'>" + job.status.state + '</td>',
"<td style='padding: 0px 10px'>" + errorResult + '</td>',
'</tr>');
}
return html.join('\n');
}
Перед тем, как запустить скрипт, обязательно нажмите на кнопку «Просмотр» в правом нижнем углу, чтобы проверить. Если в нем есть ошибки, система напишет об этом и укажет, в какой именно строке, как на скриншоте:



Напомним, что при использовании Data Transfer вы получаете большое количество сырых неагрегированных данных. С Ads Script же вы будете иметь информацию только об определенных полях.
В таблицы OWOX BI с данными о сессиях попадают следующие поля из этой выгрузки:
- GclId
- CampaignId
- CampaignName
- AdGroupId
- AdGroupName
- CriteriaId
- CriteriaParameters
- KeywordMatchType
Как подключить выгрузку из Google Ads к OWOX BI
Если у вас еще нет потока Google Analytics → Google BigQuery в OWOX BI, ознакомьтесь с инструкцией как его создать.
Затем зайдите в свой проект OWOX BI, откройте этот поток. Перейдите на вкладку «Настройка» и в разделе «Сбор данных о сессиях» нажмите «Изменить настройки»:



Полезные советы
Активировать загрузку и указать период вы можете с помощью кнопки «Schedule Backfill» во вкладке «Переносы», выбрав нужный вам трансфер:

Совет 2. Если вы хотите проверить количество аккаунтов Google Ads, за которые GCP будет брать оплату, вам необходимо определить количество ExternalCustomerID в таблице «Customer» с помощью запроса:
SELECT
ExternalCustomerId
FROM `project_name.dataset_name.Customer_*`
WHERE _PARTITIONTIME >= "2019-01-01 00:00:00" AND _PARTITIONTIME < "2019-07-10 00:00:00"
group by 1
Даты в запросе можно редактировать.
Совет 3. Вы можете самостоятельно обращаться к загруженным данным с помощью SQL-запросов. Вот, например, запрос для определения эффективности кампаний из таблиц «Campaign» и «CampaignBasicStats», полученных методом Data Transfer:
SELECT
{source language="sql"}
c.ExternalCustomerId,
c.CampaignName,
c.CampaignStatus,
SUM(cs.Impressions) AS Impressions,
SUM(cs.Interactions) AS Interactions,
{/source}
(SUM(cs.Cost) / 1000000) AS Cost
FROM
`[DATASET].Campaign_[CUSTOMER_ID]` c
LEFT JOIN
{source language="sql"}
{source language="sql"}
`[DATASET].CampaignBasicStats_[CUSTOMER_ID]` cs
ON
(c.CampaignId = cs.CampaignId
AND cs._DATA_DATE BETWEEN
DATE_ADD(CURRENT_DATE(), INTERVAL -31 DAY) AND DATE_ADD(CURRENT_DATE(), INTERVAL -1 DAY))
WHERE
c._DATA_DATE = c._LATEST_DATE
GROUP BY
1, 2, 3
ORDER BY
Impressions DESC
P. S. Если вам нужна помощь с выгрузкой и объединением данных в Google BigQuery, мы готовы помочь. Запишитесь на демо — обсудим детали.