AlpinaShop porta tres mòduls generant dades sense mirar-se-les. Les comandes s'acumulen a alpinashop-pedidos, les visites queden als registres del balancejador, les imatges se serveixen des del CDN i les cistelles s'abandonen a Firestore. La Lucía, l'analista, té una llibreta amb preguntes que ningú no ha respost: quina motxilla es ven més a Catalunya, si la campanya de tardor va moure l'agulla, per què el 30 % de les cistelles es queden al pas d'enviament, quant val de mitjana una comanda i com ha evolucionat aquest número en dos anys.
La reacció instintiva de molta gent davant d'aquest problema és obrir una connexió a la base de dades de producció i començar a escriure SELECT. A 02-03 ja vam evitar el pitjor creant la rèplica de lectura alpinashop-pedidos-replica-informes, perquè els informes no competissin amb les compres. Però aquesta rèplica és un pedaç. Una consulta que recorri dos anys de línies de comanda per agrupar per categoria i mes continuarà trigant minuts i continuarà sent incòmoda d'escriure, perquè PostgreSQL no està construït per a això. PostgreSQL està construït per respondre "dona'm la comanda 48213" en un mil·lisegon, no per respondre "dona'm la mitjana de totes les comandes dels últims dos anys agrupades per sis dimensions".
En aquesta lliçó entendràs per què aquestes dues preguntes requereixen màquines diferents, crearàs el conjunt de dades alpinashop_analitica a alpinashop-datos, hi carregaràs l'històric de comandes, escriuràs el SQL que respon de debò a les preguntes de la Lucía i —això és el que separa qui sap fer servir BigQuery de qui rep una factura sorpresa— aprendràs exactament com es paga i com s'evita pagar de més.
Contingut
- Per què un magatzem analític no és "una base de dades més gran"
- Com funciona BigQuery per dins: columnes, separació i Dremel
- Cloud SQL davant de BigQuery: la taula que cal interioritzar
- Estructura: projecte, conjunt de dades, taula i vista
- Creació del conjunt de dades
alpinashop_analitica - Tipus de dades,
STRUCTiARRAY: la desnormalització com a virtut - Creació de les taules d'AlpinaShop
- Carregar dades: des de Cloud Storage, en streaming i sense carregar-les
- Portar l'històric des de Cloud SQL amb consultes federades
- SQL de negoci: les preguntes de la Lucía, respostes
- El model de cost i com no arruïnar-se
- Particionat i agrupació en clústers: l'abans i el després mesurats
- Vistes materialitzades, memòria cau de resultats i quotes
- Emmagatzematge lògic davant de físic
- Control d'accés: conjunt de dades, taula, columna i fila
INFORMATION_SCHEMA: auditar qui gasta i en què- BigQuery ML, esmentat i ajornat
- Per què un magatzem analític no és "una base de dades més gran"
Hi ha dues famílies de càrrega de treball sobre dades, i confondre-les és l'error conceptual més car d'aquesta professió.
OLTP (processament de transaccions en línia) és el que fa alpinashop-pedidos. Moltes operacions molt petites, concurrents, que llegeixen i escriuen unes poques files identificades per clau primària, amb garanties transaccionals estrictes. "Insereix aquesta comanda", "actualitza l'estoc del SKU MOCH-40L-AZ", "dona'm la cistella de l'usuari 8821".
OLAP (processament analític en línia) és el que vol la Lucía. Poques consultes, molt grans, que llegeixen milions de files, toquen poques columnes de cadascuna, agreguen, agrupen i ordenen, i no escriuen res.
La diferència no és de mida, és de forma. I aquesta forma determina com convé desar els bytes al disc.
Imagina't la taula lineas_pedido amb 8 milions de files i aquestes columnes: pedido_id, sku, nombre_producto, categoria, cantidad, precio_unitario, descuento, iva, fecha, pais_envio, metodo_pago, email_cliente, notas.
- PostgreSQL desa les dades per files: tots els camps de la línia 1, després tots els de la línia 2, etc. És perfecte per a "dona'm la línia 1 sencera", perquè és contigua al disc.
- La pregunta de la Lucía, en canvi, és
SELECT categoria, SUM(cantidad * precio_unitario) FROM lineas_pedido GROUP BY categoria. Només necessita tres de les tretze columnes. Però per llegir aquestes tres, PostgreSQL ha d'arrossegar del disc les tretze, perquè estan entrellaçades. Llegeix deu vegades més bytes dels que fa servir.
BigQuery desa les dades per columnes: tots els valors de categoria junts, tots els de cantidad junts. La consulta anterior llegeix exclusivament tres blocs de disc i no toca els altres deu. I com que cada bloc conté valors del mateix tipus i molt repetitius —categoria tindrà una dotzena de valors diferents en vuit milions de files—, es comprimeix brutalment bé.
Emmagatzematge per FILES (OLTP) [p1|MOCH-40L|Motxilla 40L|mochilas|2|89.90|...] [p1|CRAM-12P|Grampons|...] [p2|...] └──────────── per llegir "categoria" cal travessar-ho tot ────────────┘ Emmagatzematge per COLUMNES (OLAP) pedido_id: [p1, p1, p2, p2, p3, ...] sku: [MOCH-40L, CRAM-12P, ...] categoria: [mochilas, mochilas, mochilas, crampones, ...] <-- es comprimeix a gairebe res cantidad: [2, 1, 1, 3, ...]
D'aquí es deriven les dues regles que governen tota la resta a BigQuery:
- Llegir poques columnes és barat; llegir-les totes és car. Per això
SELECT *és, literalment, la manera més ràpida de gastar diners. - Filtrar per una columna no evita llegir-la. Si filtres per
WHERE pais_envio = 'ES', BigQuery ha de llegir tota la columnapais_envioper saber quines files compleixen la condició. L'única manera de no llegir dades és que estiguin particionades o agrupades en clústers, cosa que veurem a l'apartat 12.
- Com funciona BigQuery per dins: columnes, separació i Dremel
Tres decisions d'arquitectura expliquen per què BigQuery es comporta com es comporta.
Emmagatzematge columnar propi (Capacitor). Les dades es desen en un format columnar comprimit al sistema de fitxers distribuït de Google (Colossus), replicat automàticament. Tu no veus fitxers, no gestiones índexs, no fas VACUUM i no defineixes tablespaces. No hi ha res per administrar.
Separació total d'emmagatzematge i càlcul. Aquesta és la més important i la que canvia la mentalitat. A Cloud SQL, el disc i la CPU són a la mateixa màquina: si necessites més CPU, pagues també més màquina, i si la base de dades està aturada, la màquina continua encesa i facturant. A BigQuery no hi ha màquina.
- L'emmagatzematge es factura per GB al mes que hi ha desats. Existeix sempre.
- El càlcul es factura per consulta executada (o per capacitat reservada). Si ningú no consulta, el càlcul costa zero.
Conseqüència immediata per a AlpinaShop: pots tenir 2 TB d'històric desats i, si la Lucía se'n va de vacances, pagar únicament l'emmagatzematge —uns pocs euros— sense apagar res. I a l'inrevés: una consulta pot mobilitzar centenars de màquines durant 8 segons i després tornar-les. Aquesta elasticitat és impossible en un model amb servidor.
Execució distribuïda (motor Dremel). Quan llances una consulta, el planificador la descompon en un arbre d'execució. Les fulles de l'arbre —els workers— llegeixen fragments de columnes en paral·lel, cadascun fa la seva part de l'agregació, i els resultats intermedis pugen per l'arbre barrejant-se en una capa de shuffle en memòria fins a produir el resultat final.
flowchart TD
Q["Consulta SQL de la Lucia<br/>SUM vendes GROUP BY categoria"]
P["Planificador<br/>arbre d execucio"]
S["Capa de shuffle<br/>en memoria"]
W1["Worker 1<br/>llegeix fragment columnar"]
W2["Worker 2"]
W3["Worker 3"]
Wn["... Worker N"]
C["Colossus<br/>emmagatzematge columnar"]
R["Resultat"]
Q --> P
P --> W1 & W2 & W3 & Wn
C --> W1 & W2 & W3 & Wn
W1 & W2 & W3 & Wn --> S
S --> R
La unitat d'aquest càlcul s'anomena slot: un slot és, aproximadament, una porció de CPU amb la seva memòria associada. Una consulta fa servir tants slots com el planificador consideri i n'hi hagi de disponibles. Tornarem als slots a l'apartat 11, perquè són la clau del model de preus per capacitat.
- Cloud SQL davant de BigQuery: la taula que cal interioritzar
| Dimensió | Cloud SQL (alpinashop-pedidos) |
BigQuery (alpinashop_analitica) |
|---|---|---|
| Propòsit | OLTP: transaccions, la botiga funcionant | OLAP: anàlisi, informes, taulers de control |
| Emmagatzematge | Per files | Per columnes, comprimit |
| Unitat de treball típica | Llegir/escriure 1-100 files | Llegir 10⁶-10⁹ files, escriure'n 0 |
| Latència d'una consulta | 1-50 ms | 1-30 s (segons, no mil·lisegons) |
| Concurrència objectiu | Milers de connexions simultànies | Desenes de consultes simultànies |
| Escriptura | INSERT/UPDATE fila a fila, constant |
Càrrega per lots o streaming; UPDATE puntual i car |
| Transaccions ACID | Sí, és la seva raó de ser | Transaccions multiinstrucció limitades; no és la seva funció |
| Índexs | Els defineixes tu (B-tree, GIN…) | No existeixen; hi ha partició i agrupació en clústers |
| Claus foranes | Sí, amb integritat referencial | Declarables com a no forçades; no les valida |
| Escalat | Vertical (més CPU/RAM) + rèpliques | Automàtic i invisible |
| Model de cost | Per hora d'instància encesa | Per bytes consultats (o slots reservats) + GB emmagatzemats |
| Cost si ningú no el fa servir | El de la instància, íntegre | Només l'emmagatzematge |
| Modelatge recomanat | Normalitzat (3FN) | Desnormalitzat, amb STRUCT i ARRAY |
La lectura correcta d'aquesta taula no és "BigQuery és millor". És: són eines complementàries i AlpinaShop necessita totes dues. La botiga continuarà comprant contra Cloud SQL. La Lucía analitzarà contra BigQuery. La feina d'aquesta lliçó i de les següents és construir la canonada que porta les dades de la primera a la segona.
- Estructura: projecte, conjunt de dades, taula i vista
La jerarquia de BigQuery es recolza en la jerarquia de recursos que ja coneixes de 01-04:
Organitzacio alpinashop.example
└── Projecte alpinashop-datos <-- aqui viu l analitica
└── Dataset alpinashop_analitica <-- contenidor amb ubicacio i permisos
├── Taula pedidos
├── Taula lineas_pedido
├── Taula productos
├── Taula visitas
├── Vista v_ventas_mensuales
└── Rutina (UDF, procediment emmagatzemat)Quatre precisions que eviten problemes més endavant:
- El projecte és la unitat de facturació. Les consultes que llanci la Lucía es carreguen al projecte on s'executen, no al projecte on viu la taula. Això permet, per exemple, que màrqueting consulti dades d'
alpinashop-datospagant des del seu propi projecte. Per a AlpinaShop, tot s'executa i es factura aalpinashop-datos, que ja té la seva etiquetacentro-coste. - El conjunt de dades és la unitat d'organització i de permisos. És on es concedeixen rols, i és el que es comparteix.
- La ubicació del conjunt de dades és immutable. Es decideix en crear-lo i no es pot canviar: per moure'l cal crear-ne un altre i copiar-hi les dades. Triarem
europe-west1, coherent amb la resta de la infraestructura d'AlpinaShop i amb l'exigència del RGPD de mantenir les dades personals de clients europeus a la UE. - No es pot fer un
JOINentre conjunts de dades d'ubicacions diferents. Si demà algú creaalpinashop_marketingaus-central1, no el podrà creuar ambalpinashop_analitica. És l'error de disseny irreversible més comú a BigQuery. Fixa la ubicació com a estàndard de l'equip des del primer dia.
El nom complet d'una taula és projecte.dataset.taula, i en SQL estàndard s'escriu entre accents greus quan el projecte porta guions:
Una nota de nomenclatura: els identificadors de conjunt de dades i de taula admeten lletres, números i guions baixos, però no guions. Per això el projecte és alpinashop-datos (amb guió) i el conjunt de dades és alpinashop_analitica (amb guió baix).
- Creació del conjunt de dades
alpinashop_analitica
alpinashop_analiticaTreballarem amb l'eina bq, que ve inclosa a la CLI de gcloud i a Cloud Shell des de 01-06.
gcloud config set project alpinashop-datos
# Crear el dataset d analitica
bq --location=europe-west1 mk \
--dataset \
--description="Magatzem analitic d AlpinaShop: comandes, cataleg i navegacio" \
--default_table_expiration=0 \
--label=entorno:produccion \
--label=equipo:datos \
--label=centro-coste:analitica \
alpinashop-datos:alpinashop_analiticaOpció per opció:
--location=europe-west1: la ubicació immutable. Va abans de la subcomandamk, no després; és un error habitual.--dataset: indica que el recurs que s'ha de crear és un conjunt de dades i no una taula.--default_table_expiration=0: sense caducitat automàtica. Si hi posessis2592000(30 dies en segons), tota taula creada aquí s'autodestruiria als 30 dies. És utilíssim en un conjunt de dades de proves i catastròfic en producció, així que convé ser explícit.- Les tres etiquetes repliquen l'esquema
entorno/equipo/centro-costeque AlpinaShop va fixar a 01-04, perquè el cost d'analítica es pugui aïllar a l'informe de facturació.
Convé crear també un conjunt de dades de treball amb caducitat, perquè les taules temporals de les anàlisis no s'acumulin per sempre:
bq --location=europe-west1 mk --dataset \
--description="Area de treball temporal; les taules caduquen als 7 dies" \
--default_table_expiration=604800 \
--label=entorno:desarrollo --label=equipo:datos \
alpinashop-datos:alpinashop_scratchVerificació:
bq ls --format=prettyjson --datasets alpinashop-datos
bq show --format=prettyjson alpinashop-datos:alpinashop_analiticaA la sortida de bq show fixa't en "location": "europe-west1". Si diu US, has creat el conjunt de dades al lloc equivocat: esborra'l ara, abans de posar-hi dades, perquè després ja no hi ha marxa enrere barata.
# Nomes si t has equivocat d ubicacio i el dataset esta buit
bq rm -r -f -d alpinashop-datos:alpinashop_analitica
- Tipus de dades,
STRUCT i ARRAY: la desnormalització com a virtut
STRUCT i ARRAY: la desnormalització com a virtutEls tipus escalars de BigQuery són pocs i deliberadament estrictes:
| Tipus | Ús a AlpinaShop | Nota |
|---|---|---|
STRING |
sku, categoria, pais_envio |
UTF-8, sense longitud màxima declarada |
INT64 |
cantidad, pedido_id |
Enter de 64 bits; no hi ha INT d'altres mides |
NUMERIC |
precio_unitario, total_pedido |
Decimal exacte, 38 dígits, 9 decimals: el tipus dels diners |
FLOAT64 |
Mètriques aproximades, ràtios | Coma flotant: mai per a imports |
BOOL |
es_regalo |
|
DATE |
fecha_pedido |
Sense hora |
TIMESTAMP |
momento_evento |
Instant absolut en UTC |
DATETIME |
Dates civils sense zona | Menys habitual; prefereix TIMESTAMP |
GEOGRAPHY |
Anàlisi geoespacial | Punts, línies, polígons |
JSON |
Càrregues semiestructurades | Consultable amb operadors natius |
BYTES |
Binari | Rar en analítica |
Regla que estalvia disgustos: els imports van en NUMERIC, mai en FLOAT64. Amb FLOAT64, sumar 1,10 € i 2,20 € pot donar 3.3000000000000003, i aquest cèntim fantasma apareixerà en un informe de direcció en el pitjor moment possible.
I ara la part que distingeix el modelatge a BigQuery del modelatge relacional clàssic: els tipus imbricats.
ARRAY<T>: una llista ordenada de valors del tipusTdins d'una única cel·la.STRUCT<...>: un registre amb camps anomenats dins d'una única cel·la.- Es combinen:
ARRAY<STRUCT<sku STRING, cantidad INT64, precio NUMERIC>>és "una llista de línies de comanda dins de la fila de la comanda".
A PostgreSQL, una comanda amb tres línies són quatre files repartides en dues taules unides per clau forana. A BigQuery pot ser una sola fila que conté, a dins, les seves tres línies.
Per què això és millor aquí? Perquè en un motor distribuït, un JOIN entre dues taules grans obliga a moure dades entre màquines per la capa de shuffle, i això és el que és car. Si les línies ja viatgen enganxades a la seva comanda, el JOIN desapareix: es converteix en un UNNEST, que és una operació local dins de cada worker, sense xarxa pel mig.
-- Exemple d'una sola fila amb estructura imbricada
SELECT
'PED-2026-0042' AS pedido_id,
DATE '2026-03-14' AS fecha_pedido,
STRUCT('ES' AS pais, 'Barcelona' AS ciudad) AS envio,
[
STRUCT('MOCH-40L-AZ' AS sku, 1 AS cantidad, NUMERIC '89.90' AS precio_unitario),
STRUCT('FRON-300L' AS sku, 2 AS cantidad, NUMERIC '34.50' AS precio_unitario)
] AS lineas;Aquesta fila conté una comanda completa. envio és un STRUCT (un registre), i lineas és un ARRAY<STRUCT<...>> (una llista de registres). Tot el que cal per analitzar la comanda és en un sol lloc del disc.
La regla pràctica de modelatge: normalitza en OLTP per evitar duplicitat en escriure; desnormalitza en OLAP per evitar JOIN en llegir. L'emmagatzematge és barat (uns cèntims per GB al mes); el shuffle no ho és.
Dit això, no cal portar-ho a l'extrem. Per a AlpinaShop mantindrem pedidos i lineas_pedido com a taules separades —perquè és com arriben de PostgreSQL i facilita la càrrega incremental— i imbricarem allà on aporti de debò: el bloc d'enviament dins de la comanda, i els esdeveniments dins de la sessió de navegació. És un punt intermedi honest i molt comú a la pràctica.
- Creació de les taules d'AlpinaShop
Crearem les quatre taules amb DDL en SQL, que és més llegible i versionable que un JSON d'esquema. Aquestes sentències s'executen a BigQuery Studio, la interfície de consulta de la consola (menú BigQuery), o des de bq query --use_legacy_sql=false.
-- Capcalera de comanda. Particionada per data i agrupada en clusters per pais i estat.
CREATE TABLE `alpinashop-datos.alpinashop_analitica.pedidos`
(
pedido_id STRING NOT NULL OPTIONS(description="Identificador de negoci, p. ex. PED-2026-0042"),
fecha_pedido DATE NOT NULL OPTIONS(description="Data de confirmacio de la comanda"),
momento_pedido TIMESTAMP OPTIONS(description="Instant exacte en UTC"),
cliente_id STRING OPTIONS(description="Identificador pseudonimitzat del client"),
email_cliente STRING OPTIONS(description="DADA PERSONAL: acces restringit per politica de columna"),
canal STRING OPTIONS(description="web | movil | telefono"),
estado STRING OPTIONS(description="confirmado | enviado | entregado | devuelto | cancelado"),
envio STRUCT<
pais STRING,
provincia STRING,
ciudad STRING,
codigo_postal STRING,
metodo STRING,
coste NUMERIC
> OPTIONS(description="Bloc d enviament imbricat"),
metodo_pago STRING,
cupon STRING,
subtotal NUMERIC NOT NULL,
descuento NUMERIC,
iva NUMERIC,
total_pedido NUMERIC NOT NULL OPTIONS(description="Import final cobrat, IVA inclos")
)
PARTITION BY fecha_pedido
CLUSTER BY estado, canal
OPTIONS(
description="Capcaleres de comanda d AlpinaShop, replicades des de Cloud SQL alpinashop-pedidos",
partition_expiration_days=NULL,
require_partition_filter=TRUE
);Els tres detalls que importen:
PARTITION BY fecha_pedidodivideix físicament la taula en una partició per dia. Una consulta que filtri per data només llegirà les particions necessàries.CLUSTER BY estado, canalordena les dades dins de cada partició per aquestes columnes, en aquest ordre. Els filtres perestadopodran saltar-se blocs sencers.require_partition_filter=TRUEés la millor decisió defensiva de tota la lliçó: rebutja qualsevol consulta que no filtri perfecha_pedido. UnSELECT * FROM pedidosfallarà amb un error explícit en comptes d'escanejar dos anys de dades. És un cinturó de seguretat barat que evita factures absurdes.
-- Linies de comanda. Mateixa clau de particio perque els JOIN filtrin igual.
CREATE TABLE `alpinashop-datos.alpinashop_analitica.lineas_pedido`
(
pedido_id STRING NOT NULL,
linea_num INT64 NOT NULL,
fecha_pedido DATE NOT NULL OPTIONS(description="Desnormalitzada des de pedidos per poder particionar"),
sku STRING NOT NULL,
cantidad INT64 NOT NULL,
precio_unitario NUMERIC NOT NULL,
descuento_linea NUMERIC,
importe_linea NUMERIC NOT NULL OPTIONS(description="cantidad * precio_unitario - descuento_linea")
)
PARTITION BY fecha_pedido
CLUSTER BY sku
OPTIONS(description="Detall de linies de comanda", require_partition_filter=TRUE);
-- Cataleg de productes. Taula petita: ni particio ni agrupacio en clusters.
CREATE TABLE `alpinashop-datos.alpinashop_analitica.productos`
(
sku STRING NOT NULL,
nombre STRING NOT NULL,
categoria STRING NOT NULL OPTIONS(description="mochilas | crampones | tiendas | frontales | ropa | cuerdas"),
subcategoria STRING,
marca STRING,
precio_catalogo NUMERIC,
coste_compra NUMERIC OPTIONS(description="CONFIDENCIAL: marge comercial"),
peso_gramos INT64,
activo BOOL,
fecha_alta DATE
)
OPTIONS(description="Cataleg mestre de productes, sincronitzat des de l ERP");Nota de disseny sobre lineas_pedido: hem duplicat fecha_pedido des de la capçalera. En un model relacional això seria una redundància censurable. Aquí és imprescindible, perquè una taula només es pot particionar per una columna pròpia. Sense aquesta duplicació, qualsevol JOIN entre comandes i línies obligaria a escanejar la taula de línies sencera. És l'exemple perfecte de per què les regles del modelatge OLTP no es traslladen literalment.
I la taula de navegació, que sí que aprofita la imbricació a fons:
-- Sessions de navegacio amb els seus esdeveniments imbricats.
CREATE TABLE `alpinashop-datos.alpinashop_analitica.visitas`
(
sesion_id STRING NOT NULL,
fecha DATE NOT NULL,
inicio TIMESTAMP NOT NULL,
cliente_id STRING OPTIONS(description="NULL si el visitant no ha iniciat sessio"),
dispositivo STRUCT<tipo STRING, navegador STRING, sistema STRING>,
origen STRUCT<fuente STRING, medio STRING, campana STRING>,
pais STRING,
eventos ARRAY<STRUCT<
momento TIMESTAMP,
tipo STRING, -- vista_pagina | ver_producto | anadir_carrito | iniciar_pago | compra
ruta STRING,
sku STRING,
valor NUMERIC
>> OPTIONS(description="Tots els esdeveniments de la sessio, en ordre cronologic")
)
PARTITION BY fecha
CLUSTER BY pais, sesion_id
OPTIONS(description="Sessions de navegacio del cataleg web", require_partition_filter=TRUE);Una sessió de 40 clics és una fila, no 40. Comptar quantes sessions van arribar a iniciar_pago però no a compra —la pregunta de la cistella abandonada que portava la Lucía— serà una operació local, sense JOIN i sense shuffle.
- Carregar dades: des de Cloud Storage, en streaming i sense carregar-les
Hi ha quatre maneres de posar dades a BigQuery, i triar l'equivocada és una font clàssica de cost i de dolor.
| Mètode | Latència | Cost | Quan fer-lo servir |
|---|---|---|---|
| Càrrega per lots des de Cloud Storage | Minuts | Gratis (no consumeix slots sota demanda) | Bolcats diaris, històric, reprocessos |
| Streaming (Storage Write API) | Segons | Per GB inserit | Esdeveniments que es necessiten "ara" |
| Taula externa / BigLake | Cap: no es carrega | Es paga en consultar, i és més lent | Dades que viuen al bucket i es consulten poc |
| Consulta federada a Cloud SQL | Directa | Es paga el càlcul | Portar dades operatives fresques i petites |
Càrrega per lots des de Cloud Storage
És el cavall de batalla i és gratis, cosa que sorprèn molta gent. Google no cobra la ingesta per lots: cobra l'emmagatzematge resultant i les consultes posteriors.
Suposem que el bolcat nocturn de l'històric de comandes deixa fitxers al bucket. Formats admesos: CSV, JSON delimitat per línies (NDJSON), Avro, Parquet i ORC.
# Carrega d un CSV amb capcalera, sense autodetectar res: esquema explicit
bq load \
--source_format=CSV \
--skip_leading_rows=1 \
--null_marker='\N' \
--field_delimiter=',' \
--time_partitioning_field=fecha_pedido \
--clustering_fields=estado,canal \
alpinashop-datos:alpinashop_analitica.pedidos \
gs://alpinashop-catalogo/exportaciones/2026/03/14/pedidos-*.csv \
./esquema_pedidos.json- El comodí
pedidos-*.csvpermet carregar desenes de fitxers en una sola operació, i BigQuery els processa en paral·lel. És molt millor que un fitxer gegant. --null_marker='\N'tradueix la marca de nul que fa servirpg_dumpen format CSV; sense això acabaries amb la cadena literal\Na les cel·les.- Passar l'esquema en un fitxer JSON és preferible a
--autodetect. L'autodetecció endevina, i endevinar amb diners és mala idea: és capaç d'inferirFLOAT64per a una columna d'imports oSTRINGper a una data amb un format estrany.
Comparació honesta de formats:
| Format | Esquema inclòs | Comprimit | Paral·lelitzable | Veredicte |
|---|---|---|---|---|
| CSV | No | Només externament (gzip: no paral·lelitzable) | Sí si no està comprimit | Universal, fràgil amb comes i salts de línia |
| JSON (NDJSON) | No | Igual que CSV | Sí | Bé per a dades imbricades; verbós i pesat |
| Avro | Sí | Sí, per blocs | Sí, fins i tot comprimit | La millor opció per a càrregues repetides |
| Parquet | Sí | Sí, columnar | Sí | Excel·lent; ideal si ja fas servir Spark (04-03) |
| ORC | Sí | Sí | Sí | Vàlid; habitual en migracions des de Hive |
Recomanació per a AlpinaShop: Avro o Parquet als processos automàtics, perquè porten l'esquema a dins i no hi ha ambigüitat de tipus ni sorpreses amb les comes dels noms de producte. CSV només per al que arriba de tercers, com el fitxer del transportista que veurem a 04-05.
Insercions en streaming
Quan la dada ha d'estar disponible en segons —el tauler de la campanya de tardor en temps real— es fa servir la Storage Write API, que substitueix l'antiga tabledata.insertAll i és més barata i amb millors garanties.
# Publicacio d un esdeveniment de compra en streaming cap a BigQuery.
# Requereix: pip install google-cloud-bigquery-storage
from google.cloud import bigquery
client = bigquery.Client(project="alpinashop-datos")
taula = "alpinashop-datos.alpinashop_analitica.pedidos"
files = [
{
"pedido_id": "PED-2026-0042",
"fecha_pedido": "2026-03-14",
"momento_pedido": "2026-03-14T10:22:31Z",
"cliente_id": "CLI-8821",
"canal": "web",
"estado": "confirmado",
"envio": {"pais": "ES", "provincia": "Barcelona",
"ciudad": "Barcelona", "codigo_postal": "08013",
"metodo": "estandar", "coste": "4.90"},
"subtotal": "158.90",
"total_pedido": "192.27",
}
]
errors = client.insert_rows_json(taula, files)
if errors:
# MAI ignoris aquest retorn: les fallades de streaming son silencioses
raise RuntimeError(f"Files rebutjades per BigQuery: {errors}")Dos advertiments importants sobre el streaming:
- Costa diners per GB inserit, mentre que la càrrega per lots és gratis. Si la dada pot esperar una hora, no la transmetis en streaming: acumula-la al bucket i carrega-la per lots. És una de les optimitzacions de cost més rendibles i més ignorades.
- Per a AlpinaShop, la via real no serà aquest codi a l'aplicació Flask, sinó Pub/Sub → BigQuery (04-04) o Pub/Sub → Dataflow → BigQuery (04-02). Escriure directament a BigQuery des del web acobla la botiga al magatzem analític: si BigQuery té una incidència, no volem que la botiga deixi de vendre.
Taules externes i BigLake
De vegades la dada ja és a alpinashop-catalogo i no compensa duplicar-la. Una taula externa deixa els fitxers on són i BigQuery els llegeix en el moment de la consulta.
-- Taula externa sobre els fitxers del transportista, sense carregar-los
CREATE OR REPLACE EXTERNAL TABLE `alpinashop-datos.alpinashop_analitica.ext_envios_transportista`
OPTIONS (
format = 'CSV',
uris = ['gs://alpinashop-catalogo/exportaciones/*/*/*/envios-*.csv'],
skip_leading_rows = 1,
field_delimiter = ';'
);Avantatges: zero duplicació, zero cost d'ingesta, la dada sempre reflecteix el bucket. Inconvenients: consultes més lentes, sense partició ni agrupació en clústers, i sense memòria cau de resultats. Fes-les servir per a dades consultades de tant en tant o com a zona d'aterratge abans d'una càrrega real.
BigLake és l'evolució de les taules externes: afegeix control d'accés fi —permisos a nivell de columna i de fila sobre dades que viuen al bucket— sense que l'analista necessiti permís sobre el bucket. Es recolza en una connexió de BigQuery amb el seu propi compte de servei, que és qui accedeix a Cloud Storage. Per a AlpinaShop, això significa que la Lucía podrà consultar els CSV d'exportació sense tenir storage.objectViewer sobre alpinashop-catalogo, cosa que encaixa amb el mínim privilegi de 03-04.
- Portar l'històric des de Cloud SQL amb consultes federades
L'històric de comandes viu a alpinashop-pedidos. Per portar-lo hi ha una via elegant: EXTERNAL_QUERY, la consulta federada, que executa SQL directament a PostgreSQL i retorna el resultat a BigQuery com si fos una taula.
Primer, la connexió. La crea la Marta una sola vegada:
# 1) Crear la connexio de BigQuery cap a Cloud SQL
bq mk --connection \
--connection_type=CLOUD_SQL \
--properties='{"instanceId":"alpinashop-prod:europe-west1:alpinashop-pedidos-replica-informes","database":"tienda","type":"POSTGRES"}' \
--connection_credential='{"username":"informes_lectura","password":"REEMPLAZAR"}' \
--location=europe-west1 \
conn-pedidos-postgres
# 2) Veure el compte de servei que BigQuery ha creat per a aquesta connexio
bq show --connection --location=europe-west1 alpinashop-datos.conn-pedidos-postgresFixa't en dues decisions deliberades:
- Apuntem a la rèplica de lectura
alpinashop-pedidos-replica-informes, no a la instància principal. És exactament per al que es va crear a 02-03: que els informes no toquin la base de dades que atén les compres. - Fem servir l'usuari
informes_lectura, que només téSELECT. La contrasenya real ha de sortir de Secret Manager (db-password-catalogoés la del catàleg; per a això es crearà el seu propi secret), mai d'un fitxer al repositori, tal com vam fixar a 03-06.
Ara la càrrega de l'històric, en una sola sentència:
-- Bolcat inicial de l'historic de capcaleres de comanda des de PostgreSQL
INSERT INTO `alpinashop-datos.alpinashop_analitica.pedidos`
(pedido_id, fecha_pedido, momento_pedido, cliente_id, email_cliente,
canal, estado, envio, metodo_pago, cupon, subtotal, descuento, iva, total_pedido)
SELECT
p.pedido_id,
DATE(p.creado_en) AS fecha_pedido,
p.creado_en AS momento_pedido,
p.cliente_id,
p.email AS email_cliente,
p.canal,
p.estado,
STRUCT(p.pais, p.provincia, p.ciudad,
p.codigo_postal, p.metodo_envio, p.coste_envio) AS envio,
p.metodo_pago,
p.cupon,
p.subtotal,
p.descuento,
p.iva,
p.total
FROM EXTERNAL_QUERY(
'alpinashop-datos.europe-west1.conn-pedidos-postgres',
'''SELECT pedido_id, creado_en, cliente_id, email, canal, estado,
pais, provincia, ciudad, codigo_postal, metodo_envio, coste_envio,
metodo_pago, cupon, subtotal, descuento, iva, total
FROM pedidos
WHERE creado_en >= '2024-01-01' '''
) AS p;Com llegir-ho:
- La cadena que va dins d'
EXTERNAL_QUERYés SQL de PostgreSQL, no de BigQuery. S'executa allà, amb la sintaxi d'allà. Les cometes triples eviten haver d'escapar les cometes simples internes. - Filtrar dins d'aquesta cadena (
WHERE creado_en >= ...) és crític: com menys files creuin la xarxa, millor. Filtrar fora, a BigQuery, ho portaria tot igualment. - El
STRUCT(...)construeix el bloc imbricatenvioa partir de les sis columnes planes que retorna PostgreSQL. Aquí es veu la traducció entre els dos models.
Quan NO fer servir consultes federades: per a càrregues grans i repetides. EXTERNAL_QUERY posa càrrega a la instància de PostgreSQL i no paral·lelitza; serveix per al bolcat inicial i per portar taules petites i fresques (el mestre de productes, per exemple). La sincronització diària de l'històric es farà amb el patró exportar → carregar orquestrat a 04-06, o amb CDC, que veurem a 04-05.
- SQL de negoci: les preguntes de la Lucía, respostes
BigQuery fa servir GoogleSQL, un dialecte estàndard ANSI amb extensions. Si saps SQL, en saps el 90 %. Anem amb les preguntes reals.
Vendes per categoria i mes
SELECT
FORMAT_DATE('%Y-%m', l.fecha_pedido) AS mes,
pr.categoria,
COUNT(DISTINCT l.pedido_id) AS pedidos,
SUM(l.cantidad) AS unidades,
ROUND(SUM(l.importe_linea), 2) AS ventas_eur
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido` AS l
JOIN `alpinashop-datos.alpinashop_analitica.productos` AS pr
ON pr.sku = l.sku
WHERE l.fecha_pedido BETWEEN DATE '2025-01-01' AND DATE '2026-12-31'
GROUP BY mes, pr.categoria
ORDER BY mes, ventas_eur DESC;El WHERE sobre fecha_pedido no és cosmètic: és el que activa la poda de particions i el que fa que require_partition_filter=TRUE no rebutgi la consulta. Sense ell, BigQuery llegiria la taula sencera.
Cistella mitjana i la seva evolució
SELECT
DATE_TRUNC(fecha_pedido, MONTH) AS mes,
COUNT(*) AS num_pedidos,
ROUND(AVG(total_pedido), 2) AS ticket_medio,
ROUND(APPROX_QUANTILES(total_pedido, 100)[OFFSET(50)], 2) AS mediana,
ROUND(APPROX_QUANTILES(total_pedido, 100)[OFFSET(90)], 2) AS percentil_90
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido >= DATE '2025-01-01'
AND estado NOT IN ('cancelado', 'devuelto')
GROUP BY mes
ORDER BY mes;APPROX_QUANTILES calcula percentils de manera aproximada però moltíssim més barata que el càlcul exacte sobre milions de files. Incloure la mediana al costat de la mitjana no és un caprici estadístic: si una comanda corporativa de 4.000 € entra al març, la mitjana es dispara i la mediana no es immuta. Ensenyar només la mitjana és com s'enganya sense voler un comitè de direcció.
Productes sense vendes
-- Productes actius que no han venut res els ultims 90 dies
SELECT
pr.sku, pr.nombre, pr.categoria, pr.precio_catalogo, pr.fecha_alta
FROM `alpinashop-datos.alpinashop_analitica.productos` AS pr
WHERE pr.activo = TRUE
AND pr.sku NOT IN (
SELECT DISTINCT sku
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido`
WHERE fecha_pedido >= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)
)
ORDER BY pr.fecha_alta;Compte amb NOT IN i els nuls: si la subconsulta retornés algun sku nul, NOT IN retorna zero files sense avisar. En producció és més segur LEFT JOIN ... WHERE l.sku IS NULL o NOT EXISTS.
Rànquing de productes amb funcions de finestra
-- Top 3 de productes per categoria, amb la seva quota dins de la categoria
WITH ventas AS (
SELECT
pr.categoria,
pr.sku,
pr.nombre,
SUM(l.importe_linea) AS ventas_eur
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido` AS l
JOIN `alpinashop-datos.alpinashop_analitica.productos` AS pr USING (sku)
WHERE l.fecha_pedido >= DATE '2026-01-01'
GROUP BY pr.categoria, pr.sku, pr.nombre
)
SELECT
categoria,
RANK() OVER (PARTITION BY categoria ORDER BY ventas_eur DESC) AS puesto,
nombre,
ROUND(ventas_eur, 2) AS ventas_eur,
ROUND(100 * ventas_eur
/ SUM(ventas_eur) OVER (PARTITION BY categoria), 1) AS cuota_pct
FROM ventas
QUALIFY puesto <= 3
ORDER BY categoria, puesto;Dues joies aquí:
OVER (PARTITION BY categoria)calcula sobre cada grup sense col·lapsar les files, que és justament el que unGROUP BYno pot fer.QUALIFYfiltra pel resultat d'una funció de finestra directament, sense necessitat d'embolcallar-ho tot en una altra subconsulta. És una extensió de GoogleSQL i estalvia moltíssim soroll.
UNNEST: l'embut de conversió sobre dades imbricades
-- Embut de conversio del mes: de sessio a compra
WITH sesion_flags AS (
SELECT
sesion_id,
LOGICAL_OR(e.tipo = 'ver_producto') AS vio_producto,
LOGICAL_OR(e.tipo = 'anadir_carrito') AS anadio_carrito,
LOGICAL_OR(e.tipo = 'iniciar_pago') AS inicio_pago,
LOGICAL_OR(e.tipo = 'compra') AS compro
FROM `alpinashop-datos.alpinashop_analitica.visitas`,
UNNEST(eventos) AS e
WHERE fecha BETWEEN DATE '2026-03-01' AND DATE '2026-03-31'
GROUP BY sesion_id
)
SELECT
COUNT(*) AS sesiones,
COUNTIF(vio_producto) AS vieron_producto,
COUNTIF(anadio_carrito) AS anadieron_carrito,
COUNTIF(inicio_pago) AS iniciaron_pago,
COUNTIF(compro) AS compraron,
ROUND(100 * COUNTIF(compro) / NULLIF(COUNTIF(inicio_pago), 0), 1)
AS pct_pago_a_compra
FROM sesion_flags;UNNEST(eventos) converteix l'array d'esdeveniments de cada sessió en files, i la coma que hi ha al davant és un CROSS JOIN implícit amb la fila pare: cada esdeveniment conserva accés a sesion_id, pais i la resta. És local a cada worker, sense shuffle. L'últim camp respon directament a la pregunta de la cistella abandonada que portava la Lucía des del mòdul 3, i NULLIF(..., 0) evita la divisió per zero quan un dia no hi ha pagaments iniciats.
- El model de cost i com no arruïnar-se
Aquest apartat val per si sol el preu de la lliçó. BigQuery és meravellós i també és el servei amb què més gent s'emporta un ensurt a la factura.
Hi ha dos models de càlcul, i es poden barrejar per projecte:
| Model | Com es paga | Ordre de magnitud (verificar a la documentació oficial) | Quan convé |
|---|---|---|---|
| Sota demanda | Per bytes llegits per cada consulta | ~5-6 $ per TB escanejat | Ús irregular, exploració, començar |
| Edicions (capacitat) | Per slots reservats per temps | ~0,04-0,10 $ per slot-hora segons edició | Ús constant i predictible, cost fix |
Les edicions actuals són Standard, Enterprise i Enterprise Plus, amb reserves de slots que poden ser d'escalat automàtic (pagues els slots que es fan servir, amb un mínim) o compromeses a un o tres anys amb descompte. Cadascuna afegeix funcions: Standard cobreix el bàsic, Enterprise afegeix govern i CMEK, Enterprise Plus afegeix recuperació avançada i residència estricta.
Per a AlpinaShop la decisió és sota demanda, i cal dir-ho amb els seus números: un equip de tres persones consultant uns pocs centenars de gigabytes al mes gasta uns pocs euros. Reservar slots tindria un cost fix mensual molt superior. La regla pràctica del sector és migrar a capacitat quan la despesa sota demanda supera de manera estable el cost de la reserva equivalent, cosa que sol passar a partir de desenes de terabytes consultats al mes.
Ara, el que de debò cal fer cada dia.
Estimar abans d'executar: --dry-run
bq query --use_legacy_sql=false --dry_run \
'SELECT categoria, SUM(importe_linea)
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido` l
JOIN `alpinashop-datos.alpinashop_analitica.productos` p USING (sku)
WHERE l.fecha_pedido >= "2026-01-01"
GROUP BY categoria'Retorna una cosa com:
Query successfully validated. Assuming the tables are not modified, running this query will process 428934112 bytes of data.
428 MB. A ~5 $/TB, uns 0,002 $. Cap problema. Però el mateix càlcul sobre una consulta mal escrita pot retornar 900 GB i costar 4,50 $ cada vegada que algú premi executar. Deu analistes refrescant un tauler cada cinc minuts converteixen això en una factura de quatre xifres al mes.
A BigQuery Studio no cal la comanda: la interfície mostra a dalt a la dreta, en temps real mentre escrius, "Aquesta consulta processarà X". Ensenya a mirar aquí a tothom que rebi accés. És el costum més rendible que pots instal·lar en un equip de dades.
Per què SELECT * és car
Es paga pels bytes de les columnes llegides. SELECT * sobre pedidos llegeix les catorze columnes; SELECT pedido_id, total_pedido en llegeix dues. Diferència habitual: entre cinc i vint vegades.
-- MALAMENT: llegeix totes les columnes de totes les particions
SELECT * FROM `alpinashop-datos.alpinashop_analitica.pedidos`;
-- TAMBE MALAMENT: el LIMIT no redueix el que es llegeix, nomes el que es mostra
SELECT * FROM `alpinashop-datos.alpinashop_analitica.pedidos` LIMIT 10;
-- BE: dues columnes i una particio
SELECT pedido_id, total_pedido
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido = DATE '2026-03-14';
-- Per fer un cop d'ull a les dades, fes servir la VISTA PREVIA, que es GRATIS
-- bq head -n 10 alpinashop-datos:alpinashop_analitica.pedidosLIMIT no redueix el cost. És el malentès número u. Per inspeccionar dades fes servir bq head o la pestanya Vista prèvia de la consola: llegeixen directament l'emmagatzematge i no costen res.
I una utilitat que sí que ajuda quan vols moltes columnes menys una:
SELECT * EXCEPT(email_cliente, cupon)
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido = CURRENT_DATE();
- Particionat i agrupació en clústers: l'abans i el després mesurats
Ja vam declarar partició i agrupació en clústers en crear les taules. Ara mesurem què compren exactament.
Partició: divideix la taula en trossos pel valor d'una columna (data, rang enter, o el temps d'ingesta). BigQuery descarta particions senceres sense llegir-les si el WHERE ho permet.
Agrupació en clústers: ordena físicament les dades dins de cada partició per fins a quatre columnes. Permet saltar-se blocs dins de la partició.
L'experiment, amb una taula de comandes de dos anys (suposem 40 GB):
# A) Taula SENSE particionar, filtrant per data
bq query --use_legacy_sql=false --dry_run \
'SELECT SUM(total_pedido) FROM `alpinashop-datos.alpinashop_scratch.pedidos_plana`
WHERE fecha_pedido = "2026-03-14"'
# -> Aquesta consulta processara 3.221.225.472 bytes (3,0 GB: llegeix TOTA la columna)
# B) Taula PARTICIONADA per fecha_pedido, mateix filtre
bq query --use_legacy_sql=false --dry_run \
'SELECT SUM(total_pedido) FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido = "2026-03-14"'
# -> Aquesta consulta processara 4.194.304 bytes (4 MB: una sola particio)De 3 GB a 4 MB: 750 vegades menys. I no ha canviat ni una lletra del SQL, només la definició física de la taula. A escala d'un any de consultes diàries, aquesta diferència és la que separa una factura de tres euros d'una de dos mil.
L'agrupació en clústers no apareix al --dry-run perquè el seu estalvi només es coneix en executar (el planificador no sap per endavant quants blocs podrà saltar). Es mesura al resultat real:
-- Executar i despres consultar els bytes realment facturats
SELECT
job_id,
query,
total_bytes_processed,
total_bytes_billed,
ROUND(total_bytes_billed / POW(1024,3), 2) AS gb_facturados,
TIMESTAMP_DIFF(end_time, start_time, MILLISECOND) AS ms
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
AND job_type = 'QUERY'
ORDER BY creation_time DESC
LIMIT 20;Guia de decisió ràpida:
| Situació | Què fer |
|---|---|
| Taula amb columna de data i consultes per rang | Particionar per aquesta data, sempre |
Filtres freqüents per columnes d'alta cardinalitat (sku, cliente_id) |
Agrupar-les en clústers |
| Taula de menys d'~1 GB | Cap de les dues: no compensa la complexitat |
| Més de 4.000 particions previstes | Particionar per mes en comptes de per dia |
| Vols impedir escanejos complets | require_partition_filter=TRUE |
Un matís sobre l'ordre a CLUSTER BY estado, canal: l'ordre importa. L'agrupació en clústers ordena primer per estado i dins per canal. Un filtre només per canal aprofita molt menys l'agrupació que un filtre per estado. Posa primer la columna per la qual més filtres.
- Vistes materialitzades, memòria cau de resultats i quotes
Tres mecanismes més per pagar menys, en ordre d'esforç creixent.
Memòria cau de resultats (gratis i automàtica)
Si executes exactament la mateixa consulta sobre dades que no han canviat, BigQuery retorna el resultat de la memòria cau: cost zero i resposta immediata, durant unes 24 hores. S'invalida en modificar qualsevol taula implicada.
Es trenca sense voler amb més facilitat de la que sembla: n'hi ha prou que la consulta faci servir CURRENT_TIMESTAMP() o funcions no deterministes, o que el text canviï en un caràcter. Si un tauler fa servir WHERE fecha >= CURRENT_DATE() - 7, mai no farà bé la memòria cau. Convé fixar dates explícites quan l'informe ho permeti.
Vistes i vistes materialitzades
Una vista és SQL desat: no ocupa espai i s'executa —i es paga— sencera cada vegada.
CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_ventas_mensuales` AS
SELECT
DATE_TRUNC(l.fecha_pedido, MONTH) AS mes,
pr.categoria,
SUM(l.importe_linea) AS ventas_eur,
SUM(l.cantidad) AS unidades
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido` AS l
JOIN `alpinashop-datos.alpinashop_analitica.productos` AS pr USING (sku)
WHERE l.fecha_pedido >= DATE '2024-01-01'
GROUP BY mes, pr.categoria;Una vista materialitzada desa el resultat precalculat i BigQuery el refresca de manera incremental quan canvia la taula base. A més, i això és el millor, l'optimitzador la fa servir automàticament encara que tu consultis la taula original.
CREATE MATERIALIZED VIEW `alpinashop-datos.alpinashop_analitica.mv_ventas_diarias_sku`
PARTITION BY dia
CLUSTER BY sku
OPTIONS (enable_refresh = TRUE, refresh_interval_minutes = 60)
AS
SELECT
fecha_pedido AS dia,
sku,
SUM(cantidad) AS unidades,
SUM(importe_linea) AS ventas_eur,
COUNT(*) AS num_lineas
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido`
GROUP BY dia, sku;El tauler de control de Looker Studio que construirà la Lucía a 04-07 llegirà uns pocs megabytes d'aquesta vista materialitzada en comptes d'escanejar la taula de línies cada vegada que algú obri l'informe. Limitacions que cal conèixer: només admet un subconjunt de SQL (agregacions sí; JOIN amb restriccions i finestres no), i el refresc incremental consumeix càlcul, així que no en creïs vint.
Quotes de cost: el cinturó de seguretat
Es poden imposar límits durs de bytes processats, per projecte i per usuari:
# Maxim 2 TB al dia a tot el projecte d analitica
gcloud alpha services quota update \
--service=bigquery.googleapis.com \
--consumer=projects/alpinashop-datos \
--metric=bigquery.googleapis.com/quota/query/usage \
--unit=1/d/{project} \
--value=2199023255552I a nivell de sessió o de consulta individual:
# Rebutja la consulta si ha de llegir mes de 10 GB
bq query --use_legacy_sql=false --maximum_bytes_billed=10737418240 \
'SELECT ... 'Per a AlpinaShop la política és: quota de projecte a 2 TB/dia, require_partition_filter a les taules grans, i una alerta de pressupost a Facturació (01-04) a 50 € mensuals sobre l'etiqueta centro-coste:analitica. Tres xarxes de seguretat independents, cap de cara.
- Emmagatzematge lògic davant de físic
L'emmagatzematge té el seu propi model, i des de fa uns anys es pot triar com es factura per conjunt de dades.
| Mode | Què es mesura | Ordre de magnitud | Quan triar-lo |
|---|---|---|---|
| Lògic (per defecte) | Bytes de les dades sense comprimir | ~0,02 $/GB/mes actiu | Dades poc comprimibles |
| Físic | Bytes comprimits realment ocupats, més el time travel | ~0,04 $/GB/mes actiu, però sobre molts menys GB | Dades molt repetitives (l'habitual) |
El preu per GB en mode físic és aproximadament el doble, però els GB solen ser entre 4 i 10 vegades menys perquè BigQuery comprimeix molt bé les columnes repetitives. Per a dades com les d'AlpinaShop —categories, estats, països que es repeteixen milions de vegades— el mode físic sol sortir clarament més barat. Cal mesurar-ho abans de canviar:
-- Comparar el que costaria cada mode, amb dades reals de les teves taules
SELECT
table_name,
ROUND(total_logical_bytes / POW(1024,3), 2) AS gb_logicos,
ROUND(total_physical_bytes / POW(1024,3), 2) AS gb_fisicos,
ROUND(total_logical_bytes / NULLIF(total_physical_bytes, 0), 1) AS ratio_compresion
FROM `alpinashop-datos.alpinashop_analitica`.INFORMATION_SCHEMA.TABLE_STORAGE
ORDER BY total_logical_bytes DESC;A més existeix el preu de llarga durada: una partició que no es modifica durant 90 dies consecutius baixa automàticament de preu, al voltant de la meitat. És automàtic, no cal fer res, i no afecta el rendiment ni la disponibilitat. Com que la major part de l'històric d'AlpinaShop no es toca mai, bona part de l'emmagatzematge acabarà a aquesta tarifa reduïda tot sol.
Compte amb un detall: qualsevol UPDATE sobre una partició reinicia el seu comptador de 90 dies. Un procés mal dissenyat que reescrigui l'històric sencer cada nit manté tota la taula a tarifa alta per sempre. És un altre argument a favor de les càrregues incrementals per partició.
- Control d'accés: conjunt de dades, taula, columna i fila
Aquí s'aplica tot el d'IAM (03-04), més dues capes que només existeixen a BigQuery.
Nivell conjunt de dades i taula
Els rols predefinits rellevants:
| Rol | Què permet | Per a qui a AlpinaShop |
|---|---|---|
roles/bigquery.dataViewer |
Llegir dades i metadades | La Lucía i gcp-datos@, sobre alpinashop_analitica |
roles/bigquery.dataEditor |
A més, crear i modificar taules | Comptes de servei dels pipelines |
roles/bigquery.dataOwner |
A més, esborrar el conjunt de dades i donar permisos | Ningú de manera permanent |
roles/bigquery.jobUser |
Executar consultes (facturar-les al projecte) | Tothom qui consulti |
roles/bigquery.user |
jobUser + crear conjunts de dades + llegir metadades |
Perfil d'analista estàndard |
El matís que confon tothom: dataViewer no n'hi ha prou per consultar. Veure les dades i executar un treball són permisos diferents. Cal també jobUser al projecte on s'executa la consulta. Si la Lucía diu "veig la taula però no la puc consultar", és això el 90 % de les vegades.
# Lectura sobre el dataset per al grup de dades
bq add-iam-policy-binding \
--member='group:[email protected]' \
--role='roles/bigquery.dataViewer' \
alpinashop-datos:alpinashop_analitica
# I permis per llancar consultes al projecte
gcloud projects add-iam-policy-binding alpinashop-datos \
--member='group:[email protected]' \
--role='roles/bigquery.jobUser'Com sempre des de 03-04: permisos a grups, mai a persones. Quan entri un altre analista, n'hi ha prou de posar-lo a gcp-datos@.
Recordem també el rol personalitzat analistaCatalogo que es va crear a 03-04 a alpinashop-datos. Ara té sentit completar-lo amb els permisos analítics mínims:
gcloud iam roles update analistaCatalogo --project=alpinashop-datos \
--add-permissions=bigquery.jobs.create,bigquery.tables.getData,\
bigquery.tables.list,bigquery.datasets.get,bigquery.routines.getNivell columna: amagar l'email del client
Avís de RGPD.
email_clienteés una dada personal. El seu tractament amb finalitats analítiques exigeix base legal, minimització i limitació de l'accés. El que segueix és un control tècnic, no un dictamen jurídic: qualsevol tractament de dades personals reals l'ha de revisar un professional de compliment normatiu o el DPD abans de posar-se en producció. Totes les dades d'aquest curs són fictícies.
El control es fa amb etiquetes de política de Data Catalog. La idea: s'etiqueta la columna, i només qui tingui el rol Lector detallat sobre aquesta etiqueta la pot llegir.
# 1) Taxonomia de sensibilitat per a AlpinaShop
gcloud data-catalog taxonomies create \
--location=europe-west1 \
--display-name="Sensibilitat AlpinaShop" \
--activated-policy-types=FINE_GRAINED_ACCESS_CONTROL
TAXO=$(gcloud data-catalog taxonomies list --location=europe-west1 \
--format="value(name)" --filter="displayName='Sensibilitat AlpinaShop'")
# 2) Etiqueta per a dades personals directes
gcloud data-catalog taxonomies policy-tags create \
--taxonomy="$TAXO" --display-name="pii-directo" \
--description="Identifica directament una persona: email, telefon, adreca"
PT=$(gcloud data-catalog taxonomies policy-tags list --taxonomy="$TAXO" \
--format="value(name)" --filter="displayName='pii-directo'")
# 3) Nomes el grup de seguretat pot llegir columnes amb aquesta etiqueta
gcloud data-catalog taxonomies policy-tags add-iam-policy-binding "$PT" \
--member='group:[email protected]' \
--role='roles/datacatalog.categoryFineGrainedReader'I s'aplica a la columna:
ALTER TABLE `alpinashop-datos.alpinashop_analitica.pedidos`
ALTER COLUMN email_cliente
SET OPTIONS (
policy_tags = ['projects/alpinashop-datos/locations/europe-west1/taxonomies/TAXO_ID/policyTags/PT_ID']
);A partir d'aquell moment, si la Lucía executa SELECT * FROM pedidos, la consulta falla sencera amb un error d'accés a email_cliente. Ha d'escriure SELECT * EXCEPT(email_cliente) o llistar columnes. És un efecte secundari desitjable: reforça el costum de no fer servir SELECT *, que ja sabem que a més és car.
Per a AlpinaShop, la recomanació completa és no portar l'email en cru al magatzem analític. La Lucía necessita saber quants clients diferents van comprar, no qui. Un identificador pseudonimitzat serveix igual:
-- Vista de treball per a analitica: sense dades personals directes
CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_pedidos_analitica` AS
SELECT
* EXCEPT(email_cliente),
TO_HEX(SHA256(CONCAT(email_cliente, 'sal-secreta-desde-secret-manager'))) AS cliente_hash
FROM `alpinashop-datos.alpinashop_analitica.pedidos`;La sal ha de venir de Secret Manager (03-06) i no estar escrita al SQL com aquí; sense sal, un hash d'email és reversible per força bruta i continua sent dada personal. A 04-07 veurem l'eina específica per a això, Sensitive Data Protection.
Nivell fila
Una política d'accés a files filtra quines files veu cada identitat. És útil si demà AlpinaShop obre filial a França i cada equip comercial només ha de veure el seu mercat:
CREATE ROW ACCESS POLICY pol_solo_espana
ON `alpinashop-datos.alpinashop_analitica.pedidos`
GRANT TO ('group:[email protected]')
FILTER USING (envio.pais = 'ES');El filtre s'aplica de manera transparent i ineludible: l'usuari escriu el seu SELECT normal i només obté les files d'Espanya, sense saber que existeix la política.
Compartir sense copiar: vistes autoritzades
Si vols donar accés al resultat però no a la taula base, es fa servir una vista autoritzada: es concedeix permís sobre la vista, la vista té permís sobre la taula, i l'usuari no. És el mecanisme net per exposar dades agregades a màrqueting sense obrir-los el detall de comandes.
bq update --view_udf_resource= --authorized_view \
--source_dataset=alpinashop-datos:alpinashop_analitica \
alpinashop-datos:alpinashop_analitica.v_ventas_mensuales
INFORMATION_SCHEMA: auditar qui gasta i en què
INFORMATION_SCHEMA: auditar qui gasta i en quèBigQuery s'observa a si mateix. INFORMATION_SCHEMA són vistes de només lectura amb metadades i amb l'historial de treballs. Aquesta consulta s'hauria d'executar un cop per setmana en qualsevol equip seriós:
-- Les 20 consultes mes cares dels ultims 7 dies, amb el seu cost estimat
SELECT
user_email,
job_id,
DATE(creation_time) AS dia,
ROUND(total_bytes_billed / POW(1024,4), 3) AS tb_facturados,
ROUND(total_bytes_billed / POW(1024,4) * 6.25, 2) AS coste_eur_aprox,
TIMESTAMP_DIFF(end_time, start_time, SECOND) AS segundos,
total_slot_ms,
cache_hit,
SUBSTR(REGEXP_REPLACE(query, r'\s+', ' '), 1, 160) AS consulta
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND job_type = 'QUERY'
AND state = 'DONE'
AND error_result IS NULL
ORDER BY total_bytes_billed DESC
LIMIT 20;El preu de 6,25 està posat a mà com a ordre de magnitud: substitueix-lo pel vigent a la teva regió i verifica'l a la documentació oficial, perquè canvia.
Altres consultes útils del mateix lloc:
-- Despesa per usuari i dia: per saber qui cal formar
SELECT
user_email,
DATE(creation_time) AS dia,
COUNT(*) AS consultas,
COUNTIF(cache_hit) AS desde_cache,
ROUND(SUM(total_bytes_billed) / POW(1024,4), 3) AS tb_facturados
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
GROUP BY user_email, dia
ORDER BY tb_facturados DESC;
-- Taules que ningu no consulta pero que continuem pagant
SELECT table_name,
ROUND(total_logical_bytes / POW(1024,3), 2) AS gb,
TIMESTAMP_MILLIS(last_modified_time) AS ultima_modificacion
FROM `alpinashop-datos.alpinashop_analitica`.INFORMATION_SCHEMA.TABLE_STORAGE
ORDER BY gb DESC;Aquest últim és l'equivalent analític de la neteja de discos orfes de 02-01: en tot magatzem de dades amb un any de vida hi ha taules _backup_final_v2 que ningú no recorda i que es paguen cada mes.
- BigQuery ML, esmentat i ajornat
BigQuery inclou BigQuery ML, que permet entrenar models amb sintaxi SQL: CREATE MODEL ... OPTIONS(model_type='linear_reg'), i després ML.PREDICT. Es pot fer regressió, classificació, sèries temporals amb ARIMA_PLUS, k-means, factorització matricial per a recomanacions i fins i tot invocar models de Vertex AI des de SQL.
Per a AlpinaShop és una via molt atractiva: previsió de demanda per SKU abans de la campanya de tardor, o segmentació de clients, sense treure la dada de BigQuery ni muntar infraestructura.
Però això és mòdul 5. Allà veurem Vertex AI, AutoML i els models de recomanació amb el rigor que mereixen: com se separen les dades d'entrenament i validació, com s'avalua un model i per què una mètrica bona en proves pot ser inútil en producció. Aquí només queda anotat que la porta existeix i que és a un CREATE MODEL de distància.
Errors habituals i consells
Crear el conjunt de dades a US per descuit. És l'opció per defecte de la consola i és irreversible. A més d'impedir els JOIN amb europe-west1, treu dades personals de clients europeus fora de la UE. Comprova sempre la ubicació abans de carregar el primer byte, i fixa --location=europe-west1 a la configuració de l'equip.
Creure que LIMIT abarateix la consulta. No ho fa: es llegeixen els mateixos bytes. Per mirar dades, bq head o la vista prèvia, que són gratis.
Fer servir SELECT * per costum. En un motor columnar és entre cinc i vint vegades més car. Escriu les columnes, o fes servir SELECT * EXCEPT(...).
Oblidar el filtre de partició. Sense WHERE fecha_pedido ..., una taula particionada es llegeix sencera i la partició no serveix de res. Activa require_partition_filter=TRUE i deixa que el motor et protegeixi.
Fer servir FLOAT64 per a imports. Els errors d'arrodoniment apareixen justament a l'informe que veu direcció. NUMERIC sempre per als diners.
Fer UPDATE i DELETE com a PostgreSQL. BigQuery els admet, però reescriuen particions senceres, són cars i reinicien el comptador de llarga durada. Per a actualitzacions massives, MERGE o reescriptura de la partició completa amb WRITE_TRUNCATE. I per corregir una dada d'una comanda, fes-ho a Cloud SQL i torna a sincronitzar.
Escriure a BigQuery des de l'aplicació web. Acobla la botiga al magatzem analític. Publica a Pub/Sub (04-04) i que sigui un altre procés qui escrigui. La botiga ha de poder vendre encara que l'analítica estigui caiguda.
Donar dataViewer i oblidar jobUser. És el tiquet de suport més repetit del món BigQuery.
Posar LIMIT a una consulta que ordena. ORDER BY sobre milions de files obliga a una ordenació global que pot esgotar els recursos amb l'error Resources exceeded. Agrega primer, ordena després; o fes servir funcions de finestra amb QUALIFY.
Consell: fes servir etiquetes als treballs. bq query --label=proceso:panel-direccion permet després separar a INFORMATION_SCHEMA quant costa cada consumidor de dades. Quan algú pregunti "quant ens costa el tauler de direcció?", tindràs el número.
Consell: desa el DDL a Git. Les sentències CREATE TABLE d'aquesta lliçó són codi. Han d'estar versionades juntament amb la resta del projecte, no només dins de BigQuery. A 06-07 les convertirem en Terraform.
Exercicis
Exercici 1: conjunt de dades, taula i primera càrrega
Crea a alpinashop-datos un conjunt de dades de proves anomenat alpinashop_ejercicios a europe-west1 les taules del qual caduquin als 3 dies. A dins, crea una taula opiniones amb: opinion_id (STRING, obligatori), sku (STRING, obligatori), fecha (DATE, obligatori), puntuacion (INT64), texto (STRING), pais (STRING). Ha d'estar particionada per fecha, agrupada en clústers per sku i rebutjar consultes sense filtre de partició. Després insereix tres ressenyes d'exemple i comprova amb --dry-run quants bytes costa consultar les d'un únic dia.
Exercici 2: la consulta de direcció
Amb les taules d'alpinashop_analitica, escriu una única consulta que retorni, per a cada mes de 2026 i només per a comandes ni cancel·lades ni retornades: el mes, el nombre de comandes, la facturació total, la cistella mitjana, la categoria més venuda d'aquell mes i el percentatge que aquella categoria representa sobre el total del mes. Ordena per mes. Abans d'executar-la, estima'n el cost amb --dry-run.
Exercici 3: diagnòstic d'una factura disparada
La Marta rep l'alerta de pressupost: alpinashop-datos porta gastats 180 € aquest mes quan l'històric eren 12 €. Ningú no ha carregat dades noves. Escriu les consultes d'INFORMATION_SCHEMA que et permetin (a) identificar l'usuari o compte de servei responsable, (b) aïllar la consulta concreta i quantes vegades s'ha executat, i (c) determinar si aprofita la memòria cau. Després proposa tres mesures correctores concretes, ordenades de la més immediata a la més estructural.
Solucions
Solució 1
gcloud config set project alpinashop-datos
# Dataset de proves amb caducitat de 3 dies (259200 segons)
bq --location=europe-west1 mk --dataset \
--description="Dataset d exercicis; les taules caduquen als 3 dies" \
--default_table_expiration=259200 \
--label=entorno:desarrollo --label=equipo:datos \
alpinashop-datos:alpinashop_ejerciciosCREATE TABLE `alpinashop-datos.alpinashop_ejercicios.opiniones`
(
opinion_id STRING NOT NULL,
sku STRING NOT NULL,
fecha DATE NOT NULL,
puntuacion INT64 OPTIONS(description="D 1 a 5"),
texto STRING OPTIONS(description="Text lliure del client"),
pais STRING
)
PARTITION BY fecha
CLUSTER BY sku
OPTIONS(
description="Ressenyes de clients sobre productes (dades ficticies)",
require_partition_filter=TRUE
);
INSERT INTO `alpinashop-datos.alpinashop_ejercicios.opiniones`
(opinion_id, sku, fecha, puntuacion, texto, pais)
VALUES
('OPI-0001','MOCH-40L-AZ', DATE '2026-03-10', 5,
'Molt comoda per a travessies llargues, les corretges aguanten be.','ES'),
('OPI-0002','FRON-300L', DATE '2026-03-10', 3,
'La bateria dura menys del que s anuncia en mode potent.','FR'),
('OPI-0003','CRAM-12P', DATE '2026-03-11', 4,
'Bona adherencia en gel dur, encara que pesen una mica.','ES');bq query --use_legacy_sql=false --dry_run \
'SELECT sku, AVG(puntuacion) AS media
FROM `alpinashop-datos.alpinashop_ejercicios.opiniones`
WHERE fecha = "2026-03-10"
GROUP BY sku'El resultat serà d'uns pocs centenars de bytes: amb tres files, és gairebé tot metadades. L'interessant és l'hàbit, no la xifra. I si treus el WHERE fecha, la consulta no serà cara: fallarà, perquè require_partition_filter=TRUE la rebutja. Aquest error és exactament el comportament desitjat.
Solució 2
WITH pedidos_validos AS (
SELECT
DATE_TRUNC(fecha_pedido, MONTH) AS mes,
pedido_id,
total_pedido
FROM `alpinashop-datos.alpinashop_analitica.pedidos`
WHERE fecha_pedido BETWEEN DATE '2026-01-01' AND DATE '2026-12-31'
AND estado NOT IN ('cancelado', 'devuelto')
),
resumen_mes AS (
SELECT
mes,
COUNT(*) AS num_pedidos,
ROUND(SUM(total_pedido), 2) AS facturacion_eur,
ROUND(AVG(total_pedido), 2) AS ticket_medio
FROM pedidos_validos
GROUP BY mes
),
ventas_categoria AS (
SELECT
DATE_TRUNC(l.fecha_pedido, MONTH) AS mes,
pr.categoria,
SUM(l.importe_linea) AS ventas_cat
FROM `alpinashop-datos.alpinashop_analitica.lineas_pedido` AS l
JOIN `alpinashop-datos.alpinashop_analitica.productos` AS pr USING (sku)
JOIN pedidos_validos AS pv USING (pedido_id)
WHERE l.fecha_pedido BETWEEN DATE '2026-01-01' AND DATE '2026-12-31'
GROUP BY mes, pr.categoria
),
top_categoria AS (
SELECT
mes,
categoria,
ventas_cat,
SUM(ventas_cat) OVER (PARTITION BY mes) AS ventas_mes_total
FROM ventas_categoria
QUALIFY ROW_NUMBER() OVER (PARTITION BY mes ORDER BY ventas_cat DESC) = 1
)
SELECT
FORMAT_DATE('%Y-%m', r.mes) AS mes,
r.num_pedidos,
r.facturacion_eur,
r.ticket_medio,
t.categoria AS categoria_top,
ROUND(100 * t.ventas_cat / NULLIF(t.ventas_mes_total, 0), 1) AS cuota_top_pct
FROM resumen_mes AS r
LEFT JOIN top_categoria AS t USING (mes)
ORDER BY mes;Les claus de la solució:
- Les CTE (
WITH) parteixen el problema en passos llegibles. BigQuery les avalua com a part del pla; no creen taules ni costen a part. QUALIFY ROW_NUMBER() OVER (...) = 1es queda amb la categoria líder de cada mes en una sola passada.ROW_NUMBERi noRANKperquè volem exactament una fila encara que hi hagi empat.- El
SUM(...) OVER (PARTITION BY mes)calcula el total del mes sense col·lapsar les files, cosa que permet dividir per obtenir la quota. - Els filtres de data són a les dues taules particionades. Ometre'ls a
lineas_pedidofaria que elJOINescanegés la taula completa: la consulta donaria el mateix resultat i costaria vint vegades més. NULLIF(..., 0)protegeix de la divisió per zero en un mes sense vendes.
El --dry-run sobre un històric de dos anys hauria de donar uns centenars de megabytes: cèntims. Sense els filtres de partició, desenes de gigabytes.
Solució 3
(a) Identificar el responsable:
SELECT
user_email,
COUNT(*) AS consultas,
ROUND(SUM(total_bytes_billed)/POW(1024,4), 2) AS tb_facturados,
ROUND(SUM(total_bytes_billed)/POW(1024,4)*6.25,2) AS coste_aprox_eur
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH)
AND job_type = 'QUERY'
GROUP BY user_email
ORDER BY tb_facturados DESC;(b) Aïllar la consulta i comptar execucions:
SELECT
SUBSTR(REGEXP_REPLACE(query, r'\s+', ' '), 1, 200) AS consulta,
COUNT(*) AS ejecuciones,
ROUND(AVG(total_bytes_billed)/POW(1024,3), 2) AS gb_por_ejecucion,
ROUND(SUM(total_bytes_billed)/POW(1024,4), 2) AS tb_totales,
MIN(creation_time) AS primera,
MAX(creation_time) AS ultima
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH)
AND job_type = 'QUERY'
GROUP BY consulta
ORDER BY tb_totales DESC
LIMIT 10;(c) Comprovar l'aprofitament de la memòria cau:
SELECT
user_email,
COUNTIF(cache_hit) AS desde_cache,
COUNTIF(NOT cache_hit) AS sin_cache,
ROUND(100 * COUNTIF(cache_hit) / COUNT(*), 1) AS pct_cache
FROM `region-europe-west1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH)
AND job_type = 'QUERY'
GROUP BY user_email;Diagnòstic típic i esperable: el culpable no és una persona, sinó un tauler de control que s'autorefresca cada 5 minuts executant SELECT * FROM lineas_pedido sense filtre de data. Un pct_cache proper a zero ho confirma: si la consulta porta CURRENT_TIMESTAMP() o la taula rep escriptures contínues, la memòria cau no serveix mai. 288 execucions al dia × 2 GB × 30 dies ≈ 17 TB ≈ 100 €.
Tres mesures correctores, del més immediat al més estructural:
- Avui mateix (contenció): aplicar
--maximum_bytes_billeda l'usuari o al compte de servei del tauler i fixar la quota diària del projecte a 2 TB. L'hemorràgia s'atura en minuts encara que ningú no toqui el tauler. - Aquesta setmana (correcció): reescriure la consulta del tauler amb les columnes explícites i filtre de partició, i baixar el refresc de 5 minuts a 1 hora. Sol ser prou per dividir el cost per cent.
- Aquest mes (estructural): crear una vista materialitzada amb l'agregació que el tauler necessita i apuntar-hi el tauler de control; activar
require_partition_filter=TRUEa les taules grans perquè el problema no es pugui repetir; i programar la consulta d'auditoria setmanal amb una alerta si algun usuari supera un llindar de TB.
La mesura 3 és l'única que impedeix la reincidència. Les dues primeres compren temps.
Conclusió
AlpinaShop ja té magatzem de dades. En aquesta lliçó has entès la diferència real —no la d'eslògan— entre OLTP i OLAP: no és qüestió de mida, sinó de forma d'accés, i aquesta forma és la que justifica l'emmagatzematge columnar, la separació d'emmagatzematge i càlcul, i l'execució distribuïda en slots que fa que una consulta sobre vuit-cents milions de files acabi en segons.
Has creat el conjunt de dades alpinashop_analitica a alpinashop-datos, a europe-west1, sabent que aquesta ubicació és una decisió irreversible amb implicacions de compliment normatiu i de capacitat de creuar dades. Has definit les taules pedidos, lineas_pedido, productos i visitas amb tipus correctes —NUMERIC per als diners, sempre—, amb STRUCT per al bloc d'enviament i amb un ARRAY<STRUCT<...>> d'esdeveniments dins de cada sessió, entenent per què la desnormalització, que a PostgreSQL seria un error, aquí és la manera d'evitar el shuffle, que és el que és veritablement car en un motor distribuït.
Has carregat dades per les quatre vies i saps quina fer servir: lots des de Cloud Storage —gratis— per al volum, streaming només quan els segons importen, taules externes i BigLake per al que no compensa duplicar, i EXTERNAL_QUERY contra la rèplica alpinashop-pedidos-replica-informes per al bolcat inicial de l'històric, apuntant deliberadament a la rèplica i amb l'usuari informes_lectura de només lectura.
Has escrit el SQL que respon a les preguntes que la Lucía portava tres mòduls acumulant: vendes per categoria i mes, cistella mitjana amb la seva mediana al costat per no enganyar ningú, productes que no venen, rànquing per categoria amb funcions de finestra i QUALIFY, i l'embut de conversió complet amb UNNEST sobre els esdeveniments imbricats, que per fi posa número al 30 % de cistelles abandonades.
I, sobretot, has après a no arruïnar-te: --dry-run abans d'executar, SELECT * desterrat, LIMIT desemmascarat com a fals estalvi, particionat per data que va convertir 3 GB en 4 MB, agrupació en clústers ordenada per la columna per la qual més filtres, require_partition_filter com a cinturó de seguretat, vistes materialitzades per als taulers, memòria cau de resultats que és gratis si no la trenques, quotes per projecte i per consulta, emmagatzematge físic davant de lògic mesurat amb dades pròpies, i INFORMATION_SCHEMA com l'eina que converteix "la factura ha pujat" en "aquesta consulta, aquest usuari, 288 vegades al dia". Has tancat l'accés amb rols a grups, has recordat que dataViewer sense jobUser no consulta res, i has protegit email_cliente amb una etiqueta de política —amb l'advertiment exprés de RGPD i la recomanació de no portar l'email en cru al magatzem, sinó un identificador pseudonimitzat amb sal de Secret Manager.
Queda un buit evident. Tot el que hem carregat és històric: una foto del que ja va passar, portada de PostgreSQL d'una sola vegada. Però AlpinaShop no para de vendre. Ara mateix, mentre llegeixes això, hi ha clients navegant pel catàleg, afegint motxilles a la cistella i confirmant comandes, i cap d'aquests fets no arriba a alpinashop_analitica. Sincronitzar a mà cada nit és un pedaç que ja es trenca quan algú demana "vull veure les vendes de la campanya avui, no demà".
A 04-02, Cloud Dataflow, construirem la canonada que falta. Veuràs què és un pipeline de dades i per què un script en una VM no n'hi ha prou; aprendràs el model d'Apache Beam, que descriu amb el mateix codi un procés per lots i un en temps real; escriuràs en Python un pipeline que llegeix l'històric del bucket alpinashop-catalogo, el neteja, l'agrega i l'escriu a les taules que acabes de crear; i entraràs al territori realment interessant del streaming —temps de l'esdeveniment davant de temps de procés, marques d'aigua, finestres i dades tardanes— per deixar preparat el pipeline que consumirà el topic pedidos-nuevos així que el creem a 04-04. Quan acabis aquella lliçó, les dades deixaran d'arribar quan algú se'n recorda i començaran a arribar soles.
Curs de Google Cloud Platform (GCP)
Mòdul 1: Introducció a Google Cloud Platform
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
