03 Migrasi yang malas
Biarkan ClickHouse menginfer schema export BigQuery sendiri, ingest tanpa diubah, dan lihat migrasi yang berfungsi tapi biasa-biasa saja.
Langkah pertama yang jujur
Hal paling menggoda untuk dilakukan terhadap sumber data baru apa pun adalah mengambil schema yang sudah diberikan dan memakainya begitu saja. Itulah yang dilakukan modul ini: tidak ada redesign, tidak ada opini soal tipe data, tidak ada apa pun yang ditanam khusus untuk lab ini. Apa pun yang diinfer ClickHouse dari export Parquet BigQuery sendiri adalah persis apa yang dibuat dan diload.
Schema: hasil generate, bukan tulisan tangan
DDL di bawah bukan sesuatu yang ditulis tangan oleh siapa pun. DDL ini keluar langsung dari
menjalankan DESCRIBE TABLE terhadap bucket dari modul 02 dan membungkus setiap pasangan
nama/tipe yang diinfer menjadi definisi kolom -- inferensi yang sama persis yang sudah Anda
lihat:
CREATE DATABASE IF NOT EXISTS bq;
CREATE TABLE bq.events_naive
(
`event_date` Nullable(String),
`event_timestamp` Nullable(Int64),
`event_name` Nullable(String),
`event_params` Array(Tuple( key Nullable(String), value Tuple( string_value Nullable(String), int_value Nullable(Int64), float_value Nullable(Float64), double_value Nullable(Float64)))),
`event_previous_timestamp` Nullable(Int64),
`event_value_in_usd` Nullable(Float64),
`event_bundle_sequence_id` Nullable(Int64),
`event_server_timestamp_offset` Nullable(Int64),
`user_id` Nullable(String),
`user_pseudo_id` Nullable(String),
`privacy_info` Tuple( analytics_storage Nullable(Int64), ads_storage Nullable(Int64), uses_transient_token Nullable(String)),
`user_properties` Array(Tuple( key Nullable(Int64), value Tuple( string_value Nullable(Int64), int_value Nullable(Int64), float_value Nullable(Int64), double_value Nullable(Int64), set_timestamp_micros Nullable(Int64)))),
`user_first_touch_timestamp` Nullable(Int64),
`user_ltv` Tuple( revenue Nullable(Float64), currency Nullable(String)),
`device` Tuple( category Nullable(String), mobile_brand_name Nullable(String), mobile_model_name Nullable(String), mobile_marketing_name Nullable(String), mobile_os_hardware_model Nullable(Int64), operating_system Nullable(String), operating_system_version Nullable(String), vendor_id Nullable(Int64), advertising_id Nullable(Int64), language Nullable(String), is_limited_ad_tracking Nullable(String), time_zone_offset_seconds Nullable(Int64), web_info Tuple( browser Nullable(String), browser_version Nullable(String))),
`geo` Tuple( continent Nullable(String), sub_continent Nullable(String), country Nullable(String), region Nullable(String), city Nullable(String), metro Nullable(String)),
`app_info` Tuple( id Nullable(String), version Nullable(String), install_store Nullable(String), firebase_app_id Nullable(String), install_source Nullable(String)),
`traffic_source` Tuple( medium Nullable(String), name Nullable(String), source Nullable(String)),
`stream_id` Nullable(Int64),
`platform` Nullable(String),
`event_dimensions` Tuple( hostname Nullable(String)),
`ecommerce` Tuple( total_item_quantity Nullable(Int64), purchase_revenue_in_usd Nullable(Float64), purchase_revenue Nullable(Float64), refund_value_in_usd Nullable(Float64), refund_value Nullable(Float64), shipping_value_in_usd Nullable(Float64), shipping_value Nullable(Float64), tax_value_in_usd Nullable(Float64), tax_value Nullable(Float64), unique_items Nullable(Int64), transaction_id Nullable(String)),
`items` Array(Tuple( item_id Nullable(String), item_name Nullable(String), item_brand Nullable(String), item_variant Nullable(String), item_category Nullable(String), item_category2 Nullable(String), item_category3 Nullable(String), item_category4 Nullable(String), item_category5 Nullable(String), price_in_usd Nullable(Float64), price Nullable(Float64), quantity Nullable(Int64), item_revenue_in_usd Nullable(Float64), item_revenue Nullable(Float64), item_refund_in_usd Nullable(Float64), item_refund Nullable(Float64), coupon Nullable(String), affiliation Nullable(String), location_id Nullable(String), item_list_id Nullable(String), item_list_name Nullable(String), item_list_index Nullable(String), promotion_id Nullable(String), promotion_name Nullable(String), creative_name Nullable(String), creative_slot Nullable(String)))
)
ENGINE = MergeTree
ORDER BY event_timestamp
SETTINGS allow_nullable_key = 1;Setiap kolom adalah Nullable. event_date adalah String, bukan Date.
event_timestamp adalah Int64 mentah berisi microsecond, bukan DateTime.
user_properties diinfer sebagai tuple all-Int64 hanya karena export ini tidak pernah
mengisi field itu -- itu adalah bentuk export BigQuery dan data nyata workshop ini, bukan
penyederhanaan yang dibuat khusus untuk lab.
Mengapa allow_nullable_key bukan opsional
Jalankan CREATE TABLE ini tanpa setting itu dan langsung ditolak: kolom nullable BigQuery
tidak bisa membentuk sort key sendiri, dan event_timestamp -- satu-satunya kolom yang
disortir schema naive ini -- adalah Nullable(Int64) karena itulah yang diinfer dari
sumbernya. Ini adalah friksi yang langsung ditemui kopi yang setia, tipe-demi-tipe, dan ini
adalah bagian dari bentuk schema BigQuery, bukan sesuatu yang ditanam untuk workshop ini.
event_date, yang duduk tepat di sebelahnya, punya masalah kebalikannya yang tersembunyi di
dalam non-masalah: ia adalah String dalam bentuk %Y%m%d, sehingga min(), max(), dan
perbandingan range di atasnya kebetulan keluar dengan benar -- urutan leksikografis dan
urutan kalender sepakat untuk format yang tepat itu. Itu keberuntungan, bukan desain. Layout
tanggal lain apa pun dalam string yang sama akan merusak ini secara diam-diam, tanpa error
yang memperingatkan Anda.
Load dengan ClickPipes
Tabelnya sudah ada; sekarang isi dari bucket yang sama yang dibaca modul 02 secara langsung. Di console service Anda:
-
Buka Data sources di menu kiri, lalu mulai ClickPipe baru (diberi label kira-kira Create ClickPipe atau Set up a ClickPipe, bergantung pada versi console Anda).

-
Pilih Google Cloud Storage sebagai source.

-
Atur autentikasi ke public, tanpa kredensial -- bucket-nya menerima anonymous read, sama dengan
NOSIGNyang Anda berikan kegcs()di modul 02 -- dan paste path-nya:https://storage.googleapis.com/ch-workshop-bq-migration/ga4-events/*.parquet
-
Atur format file ke Parquet.

-
Di langkah destination, arahkan pipe ke tabel yang baru Anda buat,
bq.events_naive, bukan membiarkannya membuat tabel baru. Kolom Parquet yang masuk cocok dengannya berdasarkan nama.
-
Di langkah terakhir, klik Create ClickPipe -- tidak ada tombol "start" terpisah, ini sekaligus membuat dan menjalankannya -- lalu tunggu status-nya berubah menjadi Completed.


Provisioning, belum Completed -- ini adalah pipe beberapa detik setelah dibuat. Beri waktu sedikit lebih lama dan periksa lagi statusnya sebelum melanjutkan.
Periksa apakah sudah masuk
SELECT count() FROM bq.events_naive;Harapkan 4,295,584 -- hitungan yang sama yang didapat modul 02 saat membaca bucket secara langsung, kali ini dari tabel yang dimiliki ClickHouse di disk.
Jalankan query funnel
SELECT event_name, count() AS n
FROM bq.events_naive
WHERE event_name IN ('view_item', 'add_to_cart', 'begin_checkout', 'purchase')
GROUP BY event_name
ORDER BY n DESC;Empat angka yang sama seperti modul 02, dalam urutan ORDER BY n DESC yang sama, karena ini
data yang sama:
event_name | n |
|---|---|
view_item | 386,068 |
add_to_cart | 58,543 |
begin_checkout | 38,757 |
purchase | 5,692 |
Yang berubah adalah tempat data itu tinggal: query ini membaca baris yang sudah dimiliki ClickHouse di disk, bukan Parquet yang harus diambil dan diparsing ulang lewat network setiap kali dipanggil. Diukur pada service yang sama dengan 0.178s di modul 02:
Rasa lambat dari modul 02 sudah hilang.
Di mana Anda berakhir
Ini adalah migrasi yang berfungsi. Tabelnya ada, menyimpan setiap baris yang diekspor BigQuery, dan query funnel di atas mengembalikan jawaban yang benar. Itulah seluruh modul ini: tidak ada yang mengoptimalkan apa pun, tidak ada yang memilih satu tipe pun, dan tetap berfungsi.
Ini juga biasa-biasa saja, dengan sengaja. Setiap kolom masih Nullable. Sort key-nya
adalah satu Int64 timestamp tanpa hubungan apa pun dengan cara data ini benar-benar diquery
siapa pun. Kolom event_params dan items masih berupa array of tuples bersarang, persis
seperti diekspor BigQuery. Tidak ada satu pun dari itu yang sudah menimbulkan biaya, karena
belum ada yang mengajukan pertanyaan yang lebih sulit ke tabel ini selain "berapa banyak
baris." Ini migrasi yang berfungsi, dan biasa-biasa saja -- itulah keseluruhan poin modul
ini, bukan setup untuk sebuah kejutan.
Lihat seberapa besar tabel naive-nya
"Belum ada yang menimbulkan biaya" adalah klaim yang bisa Anda periksa. Lihat berapa biaya migrasi yang biasa-biasa saja dan belum di-tuning ini dalam byte:
SELECT
sum(data_compressed_bytes) AS compressed_bytes,
sum(data_uncompressed_bytes) AS uncompressed_bytes
FROM system.parts
WHERE active AND database = 'bq' AND table = 'events_naive';Diukur pada service ClickHouse Cloud saat menyiapkan workshop ini -- perkirakan service Anda sendiri berada di kisaran yang sama, tidak harus identik bit-demi-bit:
Tidak ada yang memilih tipe, codec, atau sort key untuk mendapatkan angka itu -- itu adalah apa pun yang dilakukan kompresi default ClickHouse terhadap schema hasil inferensi BigQuery sendiri, pada baris yang belum pernah ditanya pertanyaan lebih sulit dari "berapa banyak." Modul 04 meminta Anda menyimpan 4,295,584 baris yang sama, tanpa kehilangan informasi apa pun, dalam byte yang lebih sedikit dari ini.
Selesai jika
bq.events_naive menyimpan 4,295,584 baris, query funnel mengembalikan empat angka yang
sama seperti modul 02, Anda bisa menjelaskan dalam satu kalimat mengapa
allow_nullable_key diperlukan, dan Anda sudah melihat berapa byte compressed yang
dibutuhkan tabel naive-nya. Lanjut ke
04 Mengecilkan tabel jika
Anda sudah siap.
02 Query di tempat
Baca export GA4 langsung dari object storage tanpa ingestion sama sekali, dan rasakan sendiri mengapa ini bukan cara yang tepat untuk menjalankan dashboard.
04 Mengecilkan tabel
Lima perubahan konkret pada schema naive, dijalankan satu per satu, masing-masing dengan jumlah byte terkompresi yang membuktikan apakah itu membantu.