0% found this document useful (0 votes)
1K views

Resumen Ejercicios Libro Spark

This document summarizes chapters from the book "Learning Spark, Second Edition". It discusses Spark concepts like RDDs, DataFrames, and Datasets. It explains how to work with Spark SQL, including creating and querying SQL tables and views. It also covers common data sources like Parquet, JSON, and CSV and how to read/write them using DataFrames and Spark SQL.

Uploaded by

Paula Iglesias
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1K views

Resumen Ejercicios Libro Spark

This document summarizes chapters from the book "Learning Spark, Second Edition". It discusses Spark concepts like RDDs, DataFrames, and Datasets. It explains how to work with Spark SQL, including creating and querying SQL tables and views. It also covers common data sources like Parquet, JSON, and CSV and how to read/write them using DataFrames and Spark SQL.

Uploaded by

Paula Iglesias
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 86

RESUMEN DEL LIBRO

Learning Spark, Second Edition (databricks.com)

Por:

Paula Iglesias

Junio 2021
Índice
Prólogo .....................................................................................................................................................7
Capitulo 1 .................................................................................................................................................9
Capítulo 2 ...............................................................................................................................................10
Spark’s Directories and Files ..............................................................................................................10
Spark Application Concepts ...............................................................................................................10
SparkSession ......................................................................................................................................10
Spark Jobs ..........................................................................................................................................11
Spark Stages .......................................................................................................................................11
Spark Tasks.........................................................................................................................................12
Transformations and actions .............................................................................................................12
Ejercicios libro ....................................................................................................................................14
1. (Pag 30) ......................................................................................................................................14
2. Contando M&Ms. Pag (39) .......................................................................................................14
Ejercicios extras .................................................................................................................................16
Capitulo 3 - Apache Spark’s Structured APIs .........................................................................................19
Resilient Distributed Datasets (RDD) .................................................................................................19
Intro a DataSet ...................................................................................................................................20
Intro a DataFrame ..............................................................................................................................20
The DataFrame API ............................................................................................................................21
Schemas and Creating DataFrames ...............................................................................................22
Definición de esquemas ............................................................................................................22
Columns and Expressions ..............................................................................................................23
Rows ...............................................................................................................................................24
Common DataFrame Operations ...................................................................................................24
Using DataFrameReader and DataFrameWriter .......................................................................24
Saving a DataFrame as a Parquet file or SQL table....................................................................25
Transformations and actions .....................................................................................................27
Projections and filters. ...............................................................................................................27
Renaming, adding, and dropping columns ................................................................................28
The Dataset API..................................................................................................................................28
Dataset ...........................................................................................................................................29
Typed Objects, Untyped Objects, and Generic Rows ....................................................................29
Creating Datasets...............................................................................................................................30
Dataset Operations ........................................................................................................................30
DataFrame vs Dataset ........................................................................................................................30
The Catalyst Optimizer......................................................................................................................31
Qué es Catalyst ..........................................................................................................................31
Componentes Catalyst ......................................................................................................................32
Trees ..........................................................................................................................................32
Rules...........................................................................................................................................32
Usando Catalyst en Spark SQL ..........................................................................................................32
Conclusiones......................................................................................................................................34
Resumen del tema .............................................................................................................................34
Ejercicios extra ...................................................................................................................................35
Capitulo 4 - Spark SQL and DataFrames: Introduction to Built-in Data Sources ...................................38
Using Spark SQL in Spark Applications ..............................................................................................39
Basic Query Examples ...................................................................................................................39
SQL Tables and Views ...................................................................................................................40
Managed Versus Unmanaged Tables ........................................................................................40
METHOD #1........................................................................................................................................40
METHOD #2........................................................................................................................................40
METHOD #1........................................................................................................................................41
Creating SQL Databases and Tables .............................................................................................41
Creating Views...............................................................................................................................41
Viewing the Metadata ..................................................................................................................42
Almacenamiento en caché de tablas SQL ....................................................................................42
Reading Tables into DataFrames ..................................................................................................43
Data Sources for DataFrames and SQL Tables .............................................................................43
DataFrameReader ......................................................................................................................43
DataFrameWriter .......................................................................................................................45
Parquet ..............................................................................................................................................45
Reading Parquet files into a Spark SQL table.................................................................................46
De DataFrame a Parquet ...............................................................................................................46
JSON ...................................................................................................................................................46
JSON to DataFrame ........................................................................................................................46
JSON to Spark SQL table ................................................................................................................47
DataFrames to JSON ......................................................................................................................47
CSV .....................................................................................................................................................47
CSV to DataFrame ..........................................................................................................................47
CSV to Spark SQL table...................................................................................................................48
DataFrame to CSV ..........................................................................................................................48
Avro ....................................................................................................................................................48
Avro to DataFrame.........................................................................................................................48
Avro to Spark SQL table .................................................................................................................48
DataFrames to Avro .......................................................................................................................49
ORC ....................................................................................................................................................49
Images ................................................................................................................................................49
Binary Files .........................................................................................................................................49
Resumen del tema .............................................................................................................................49
Ejercicios extra ...................................................................................................................................50
Capítulo 5 - Spark SQL and DataFrames: Interacting with External Data Sources ................................50
Spark SQL and Apache Hive ...............................................................................................................50
User-Defined Functions .................................................................................................................50
Spark SQL UDFs ..........................................................................................................................51
Evaluation order and null checking in Spark SQL ......................................................................51
Speeding up and distributing PySpark UDFs with Pandas UDFs ...............................................52
Querying with the Spark SQL Shell, Beeline, and Tableau ................................................................54
Using the Spark SQL Shell ..............................................................................................................54
Crate a table...............................................................................................................................54
Insert data into table .................................................................................................................54
Running a Spark SQL query ........................................................................................................54
Working with Beeline ....................................................................................................................54
Start the Thrift server ................................................................................................................54
Connect to the Thrift server via Beeline ....................................................................................54
Execute a Spark SQL query with Beeline ...................................................................................54
Stop the Thrift server .................................................................................................................54
Working with Tableau ....................................................................................................................54
Start the Thrift server ................................................................................................................54
Start Tableau ..............................................................................................................................54
Stop the Thrift server .................................................................................................................54
External Data Sources ........................................................................................................................54
JDBC and SQL Databases................................................................................................................54
The importance of partitioning .................................................................................................55
Higher-Order Functions in DataFrames and Spark SQL .....................................................................55
Transformando un Array ...................................................................................................................55
Built-in Functions for Complex Data Types .......................................................................................56
High-Order Functions.........................................................................................................................58
Common DataFrames and Spark SQL Operations .............................................................................59
Resumen del tema .............................................................................................................................63
Ejercicios extras .................................................................................................................................63
Capítulo 6 - Spark SQL and Datasets......................................................................................................64
Single API for Java and Scala ..............................................................................................................65
Scala Case Classes and JavaBeans for Datasets .................................................................................65
Working with Datasets ......................................................................................................................66
Creating Sample Data ........................................................................................................................66
Transforming Sample Data ................................................................................................................67
Higher-order functions and functional programming ...............................................................67
Converting DataFrames to Datasets ..........................................................................................70
Memory Management for Datasets and DataFrames .......................................................................70
Dataset Encoders ...............................................................................................................................71
Spark’s Internal Format Versus Java Object Format .................................................................71
Serialization and Deserialization (SerDe)...................................................................................72
Costs of Using Datasets .....................................................................................................................72
Strategies to Mitigate Costs...............................................................................................................73
Resumen ............................................................................................................................................74
Capítulo 7 - Optimizing and Tuning Spark Applications ........................................................................74
Optimización y ajuste de Spark para la eficiencia .............................................................................75
Viewing and Setting Apache Spark Configurations ...........................................................................75
Scaling Spark for Large Workloads ....................................................................................................75
Static versus dynamic resource allocation ................................................................................75
Configuring Spark executors’ memory and the shuffle service.................................................76
Maximizing Spark parallelism ....................................................................................................78
Almacenamiento en caché y persistencia de datos ..........................................................................79
DataFrame.cache().............................................................................................................................79
DataFrame.persist() ...........................................................................................................................80
Using Cache and Persist .....................................................................................................................81
A Family of Spark Joins .....................................................................................................................82
Broadcast Hash Join ...........................................................................................................................82
When to use a broadcast hash join ...........................................................................................83
Shuffle Sort Merge Join .....................................................................................................................84
Optimizing the shuffle sort merge join ......................................................................................86
When to use a shuffle sort merge join ......................................................................................86
Inspecting the Spark UI .....................................................................................................................86
Journey Through the Spark UI Tabs ...................................................................................................86
Prólogo
Soy una chica en prácticas que realizó este curso y fui resumiendo lo más importante. Vas a ver que
hay parrafos que dices pero esto de donde lo ha sacado, y lo que pasa es que en cierto capitulo hacia
un resumen de lo que salia y despues, si en otro capitulo volvia a salir mas informacion del mismo
tema pues volvia al resumen y rellenaba la informacion. Además he completado la información con
foros y videos de los que suelo poner el enlace.

Hay algunos párrafos que igual no estan muy bien escritos porque los traduje con traductor e igual
hay alguna información que falta porque no la vi relevante. Anyways, este resumen no está acabado
y eres libre de rellenar / modificar / borrar información a tu parecer. Que vaya bien :)

Los ejercicios que aparecen en el libro se han probado en las siguientes aplicaciones y en el lenguaje
Scala:

• Databricks Cloud Community: https://community.cloud.databricks.com/

Es una aplicación online con un espacio para escribir los comandos:

Figura 1: Vista de un Notebook 1

Pudiendose también almacenar archivos y trabajar con ellos

• IntelliJ Community: Descargar IntelliJ IDEA: el IDE de Java eficaz y ergonómico de JetBrains

Hay que descargar la versión community (botón negro). Es una aplicación de escritorio donde puedes
tener más organizados por carpetas los distintos ejercicios:

1 Spark usa los Notebooks como zonas donde escribir los distintos comandos
Figura 2: Vista de un proyecto Spark en IntelliJ
Capitulo 1
Introducción a Spark. No lo resumo
Capítulo 2
Spark’s Directories and Files
Estructura del directorio de Spark:

README.md - Este archivo contiene nuevas instrucciones detalladas sobre cómo utilizar los shells de
Spark, construir Spark desde el código fuente, ejecutar ejemplos autónomos de Spark, examinar los
enlaces a la documentación de Spark y las guías de configuración, y contribuir a Spark.

bin - Este directorio, como su nombre indica, contiene la mayoría de los scripts que emplearás para
interactuar con Spark, incluyendo los shells de Spark (spark-sql, pyspark, spark-shell y sparkR).
Utilizaremos estos shells y ejecutables en este directorio más adelante en este capítulo para enviar
una aplicación Spark independiente utilizando spark-submit, y escribir un script que construye y
empuja imágenes Docker cuando se ejecuta Spark con soporte Kubernetes.

sbin - La mayoría de los scripts de este directorio son de carácter administrativo, para iniciar y detener
los componentes de Spark en el clúster en sus distintos modos de despliegue.

kubernetes - Desde el lanzamiento de Spark 2.4, este directorio contiene archivos Docker para crear
imágenes Docker para su distribución de Spark en un clúster Kubernetes. También contiene un archivo
que proporciona instrucciones sobre cómo construir la distribución de Spark antes de construir sus
imágenes Docker.

data - Este directorio está lleno de archivos *.txt que sirven de entrada para los componentes de
Spark: MLlib, Structured Streaming y GraphX.

examples - Para cualquier desarrollador, dos imperativos que facilitan el camino hacia el aprendizaje
de cualquier nueva plataforma son un montón de ejemplos de código "cómo hacer" y una
documentación completa. Spark proporciona ejemplos para Java, Python, R y Scala, y querrás
emplearlos cuando aprendas el framework. Haremos alusión a algunos de estos ejemplos en este
capítulo y en los siguientes.

Spark Application Concepts


SparkSession
El core de toda aplicación Spark, es el driver de Spark, que crea un objeto SparkSession. Cuando estás
trabajando con un Spark-shell, el driver es parte del shell y el objeto SparkSession es creado
automáticamente.

Provee un punto de entrada unificado para programar Spark con las APIs estructuradas.

Es el objeto principal o la base a partir de la cual cuelga toda la funcionalidad de Apache Spark. Solo
hay que importar la clase y crear una instancia en el código.

Es similar al SparkContext de los RDD, pero en este caso, para trabajar con SparkSQL, DataFrame y
DataSet se usa la SparkSession.

//Constructor
val spark = SparkSession.builder()
Spark Jobs
Para entender los siguientes apartados mejor te recomiendo este video:

(11) Apache Spark internals: stages and tasks - YouTube

Por cada grupo de trabajo/ conjunto de tareas el Driver divide el trabajo en grupos mas pequeños de
trabajo llamados Jobs

Spark Stages
Cada Job está dividido en Stages que engloban tareas del mismo estilo

Ejemplo de Stages:
Imagen extraida de: (11) Apache Spark internals: stages and tasks - YouTube

Spark Tasks
Cada Stage está dividido en tareas

En conjunto sería algo así:

Transformations and actions


Transformations
Las operaciones de Spark sobre datos ditribuidos pueden clasificarse en dos tipos: transformaciones
y acciones.

Transformaciones mas comunes: map, filter, flatMap, mapPartitions, mapPartitionsWithIndex,


sample, union, intersection, distinct, groupByKey

Las transformaciones se dividen en 2: Narrow and Wide Transformations


• Narrow - como filter() and contains() - Pueden operar en una sola partición y producir la
particion de salida sin ningún intercambio de datos.
• Wide - como groupBy() or orderBy() - Se leen datos de otras particiones y se combinan y
escriben en disco.

Imagen que lo explica muy bien:

Todas las transformaciones son evaluadas vagamente. Esto significa que los resultados no son
calculados inmediatamente pero son guardados como lineage.

La evaluación perezosa es la estrategia de Spark para retrasar la ejecución hasta que se invoque una
acción o se "toquen" los datos (se lean o se escriban en el disco)

Mientras que la evaluación perezosa permite que Spark optimice sus consultas al ojear a sus
transformaciones encadenadas, el linaje y la inmutabilidad de los datos proporcionan tolerancia a los
fallos.

Como Spark registra cada transformación en su linaje y los DataFrames son inmutables entre
transformaciones, puede reproducir su estado original simplemente simplemente reproduciendo el
linaje registrado, lo que le da resiliencia en caso de fallos.

Actions
Una vez que los datos se encuentran en el formato que necesitaba para llevar acabo el análisis de
palabras, llegamos al punto de obtener los resultados

Nada en un plan de consulta se ejecuta hasta que se invoca una acción. La acción es lo que
desencadena la ejecución de todas las transformaciones registradas como parte del plan de ejecución
de la consulta.

collect() como select, coge todas las columnas del dataframe


Ejercicios libro
1. (Pag 30)

Código:

// In Scala

import org.apache.spark.sql.functions._

val strings = spark.read.text("../README.md")

val filtered = strings.filter(col("value").contains("Spark"))

filtered.count()

res5: Long = 20

2. Contando M&Ms. Pag (39)

import org.apache.spark.sql.SparkSession

import org.apache.spark.sql.functions._

/**

* Usage: MnMcount <mnm_file_dataset>

*/

object MnMcount {

def main(args: Array[String]) {

val spark = SparkSession

.builder

.appName("MnMCount")

.getOrCreate()

if (args.length < 1) {

print("Usage: MnMcount <mnm_file_dataset>")

sys.exit(1)

}
// Get the M&M data set filename

val mnmFile = args(0)

// Read the file into a Spark DataFrame

val mnmDF = spark.read.format("csv")

.option("header", "true")

.option("inferSchema", "true")

.load(mnmFile)

// Aggregate counts of all colors and groupBy() State and Color

// orderBy() in descending order

val countMnMDF = mnmDF

.select("State", "Color", "Count")

.groupBy("State", "Color")

.agg(count("Count").alias("Total"))

.orderBy(desc("Total"))

// Show the resulting aggregations for all the states and colors

countMnMDF.show(60)

println(s"Total Rows = ${countMnMDF.count()}")

println()

// Find the aggregate counts for California by filtering

val caCountMnMDF = mnmDF

.select("State", "Color", "Count")

.where(col("State") === "CA")

.groupBy("State", "Color")

.agg(count("Count").alias("Total"))

.orderBy(desc("Total"))

// Show the resulting aggregations for California

caCountMnMDF.show(10)

// Stop the SparkSession

spark.stop()

}
MnMcount.main(Array("/FileStore/tables/mnm_dataset_csv-1.txt"))

Ejecutar método:

1a consulta

2a consulta

Ejercicios extras

a). Descargar el Quijote https://gist.github.com/jsdario/6d6c69398cb0c73111e49f1218960f79

Aplicar no solo count (para obtener el número de líneas) y show sino probar distintas sobrecargas del
método show (con/sin truncate, indicando/sin indicar num de filas, etc) así como también los
métodos, head, take, first (diferencias entre estos 3?)

import org.apache.spark.sql.functions._

val quijote = spark.read.text("/FileStore/tables/el_quijote-1.txt")


quijote.count()

b. Del ejercicio de M&M aplicar:

i. Otras operaciones de agregación como el Max con otro tipo de ordenamiento (descendiente).

.orderBy(desc("Total")) => .orderBy("Total")

ii. hacer un ejercicio como el “where” de CA que aparece en el libro pero indicando más opciones de
estados (p.e. NV, TX, CA, CO).

.where(col("State") === "CA" => .where(col("State") === "CA" || col("State") === "NV")

iii. Hacer un ejercicio donde se calculen en una misma operación el Max, Min, Avg, Count. Revisar el
API (documentación) donde encontrarán este ejemplo: ds.agg(max($"age"), avg($"salary"))
ds.groupBy().agg(max($"age"), avg($"salary")) NOTA: $ es un alias de col()

val caMax = caCountMnMDF.agg(min("Count"),max("Count"),avg("Count"),sum("Count"))

caMax.show
iv. Hacer también ejercicios en SQL creando tmpView

caCountMnMDF.agg(min("Count"),max("Count"),avg("Count"),sum("Count")).createOrReplaceTemp
View("people")

val sqlDF = spark.sql("SELECT * FROM people")

sqlDF.show()
Capitulo 3 - Apache Spark’s Structured APIs
Resilient Distributed Datasets (RDD)
Es una colección de elementos tolerante a fallos que es capaz de operar en paralelo

RDD es la abstracción más básica en Spark. Es lenguaje de bajo nivel. están particionados en los nodos
del cluster. Se suelen crear a partir de un fichero del HDFS, Usan la evaluación perezosa (todas las
transformaciones que hagamos a los RDD se van a ir almacenando en un grafo aciclico dirijido (DAG)
y no se van a resolver hasta que no quede mas remedio)

Tipo de objetos basicos a la hora de comenzar a crear los RDD

• SparkContext - Especifica como se va a acceder a nuestro cluster


• SparkConf - Contiene informacion sobre nuestra aplicación: número de hilos, memoria que
queremos que consuma, ...

Hay tres características vitales asociadas a un RDD:

• Dependencias

En primer lugar, se requiere una lista de dependencias que indique a Spark cómo se construye un RDD
con sus entradas. Cuando sea necesario para reproducir resultados, Spark puede recrear un RDD a
partir de estas dependencias y replicar operaciones en él. Esta característica da a los RDDs resiliencia

• Particiones (with some locality information)

En segundo lugar, las particiones proporcionan a Spark la capacidad de dividir el trabajo para
paralelizar el cálculo en las particiones entre los ejecutores. En algunos casos -por ejemplo, la lectura
de HDFS, Spark utilizará la información de localización para enviar el trabajo a los ejecutores cercanos
a los datos. datos. De este modo, se transmiten menos datos por la red.

• Compute function: Partition => Iterator[T]

Y finalmente, un RDD tiene una función de cálculo que produce un Iterator[T] para los datos que se
almacenarán en el RDD

//Ejemplo:

// sc=DpstkContext()

val cadenas = Array("openwebinars", "big data", "openwebinars")

val cadenasRDD = sc.parallelize(cadenas)

// Para ver el contenido

cadenasRDD.collect()

Mejor usar operaciones DSL de alto nivel y la API DataFrame

DSL (Domain Specific Languaje) - En la API de Spark se puede usar Java, Python, Spark, R, and SQL
Cuando usar RDD:

• Estás utilizando un paquete de terceros que está escrito utilizando RDDs


• Pueden renunciar a la optimización del código, a la utilización eficiente del espacio y al
rendimiento disponibles con DataFrames y Datasets
• Desea indicar a Spark con precisión cómo realizar una consulta

Para cambiar entre DataFrames o Datasets y RDDs se utiliza una simple llamada al método de la API,
df.rdd.

Intro a DataSet
Un DataSet es una colección de datos distribuidos que tienen ya una estructura, a diferencia de los
RDD, que son conjuntos de datos desestructurados.
Crear un Dataset:

val people = spark.read.parquet("...").as[Person] // Scala

Intro a DataFrame
Un DataFrame es un DataSet que a la vez está organizado en columnas.

Ejemplos:

val dataDF = spark.createDataFrame(Seq(("Brooke", 20), ("Brooke", 25),


("Denny", 31), ("Jules", 30), ("TD", 35))).toDF("name", "age")

val avgDF = dataDF.groupBy("name").agg(avg("age"))


Para seleccionar una columna del DataFrame:

val ageCol = people("age") // in Scala

Ej1. (Pag 46) Agregar todas las edades de cada nombre, agruparlas por nombre, y luego promediar las
edades

Resultado de la ejecución en Databricks:


The DataFrame API
Ejemplo de DataFrame:

Los DataFrames son inmutables y Spark mantiene un linaje de todas las transformaciones. Puede
añadir o cambiar los nombres y tipos de datos de las columnas, creando nuevos DataFrames mientras
se conservan las versiones anteriores. Una columna con nombre en un DataFrame y su tipo de datos
asociado a Spark asociado pueden ser declarados en el esquema.

Basic Scala data types in Spark

Scala structured data types in Spark


Schemas and Creating DataFrames
Un esquema en Spark define los nombres de las columnas y los tipos de datos asociados a un
DataFrame. A menudo, los esquemas entran en juego cuando se leen datos estructurados de una
fuente de datos externa. Definir un esquema por adelantado, en lugar de adoptar un enfoque de
esquema sobre la lectura, ofrece tres ventajas:

• Libera a Spark de la carga de inferir los tipos de datos


• Se evita que Spark cree un trabajo separado sólo para leer una gran parte de de su archivo
para determinar el esquema, lo que para un archivo de datos grande puede ser costoso y
llevar tiempo.
• Puede detectar errores con antelación si los datos no coinciden con el esquema.

Definición de esquemas
Spark te permite definir un esquema de dos formas:

• Definiendolo programandolo
• Usando un Lenguaje de Definicion de Datos (DDL) (Scala, Python, ...), que es mucho más simple
y fácil de leer.

Ejemplo: Definir un esquema programando para un DataFrame con tres columnas nombradas: autor
titulo y pages.

Definiendo el mismo esquema usando DDL es mucho más simple:

Leyendo de un JSON:
Columns and Expressions

expr is the same as col


Rows
Una fila en Spark es un objeto genérico Row, que contiene una o más columnas. Cada columna puede
ser del mismo tipo de datos (por ejemplo, entero o cadena), o pueden tener diferentes tipos (entero,
cadena, mapa, matriz, etc.). Dado que Row es un objeto en Spark y una colección ordenada de campos,
se puede instanciar un Row en cada uno de los lenguajes soportados por Spark y acceder a sus campos
mediante una de Spark y acceder a sus campos mediante un índice que comienza en 0

Row objects can be used to create DataFrames:

Common DataFrame Operations


Spark provides an inter‐ face, DataFrameReader, that enables you to read data into a DataFrame from
myriad data sources in formats such as JSON, CSV, Parquet, Text, Avro, ORC, etc.

Using DataFrameReader and DataFrameWriter

The spark.read.csv() function reads in the CSV file and returns a DataFrame of rows and named
columns with the types dictated in the schema

If you don’t want to specify the schema, Spark can infer schema from a sample at a lesser cost.
El metodo write tiene muchas ociones para escribir en ficheros , pero el principal es el save()

census_df.write.save("hdfs://…/some_parent_dir/another_directory")
Writing DataFrames to HDFS como ficheros Parquet

census_df.write.parquet("hdfs://…/spark_output/parquet_dir")
La otra forma de escribir un DataFrame como ficheros Parquet es usando el Save() y el format()

Parquet is a columnar format that is supported by many other data processing systems:

Mas opciones al escribir:

.option("compression", "gzip")
.mode("overwrite")
Partición
En Spark, los datos de un DataFrame se mantienen en la memoria y si el tamaño de los
datos excede la cantidad de memoria disponible, entonces los datos se derraman en la
unidad de disco local de los nodos trabajadores del clúster. Sin embargo, este espacio de
disco local no es el mismo que el de HDFS. Dado que Spark está diseñado para ejecutarse
en un sistema distribuido de máquinas, los datos de un DataFrame se dividen en piezas
más pequeñas y se distribuyen en la memoria y/o el almacenamiento local de los nodos
trabajadores. En este sentido, un DataFrame de Spark se divide lógicamente, es decir, se
particiona. Así, una partición es una colección atómica de subconjuntos de datos de un
DataFrame que reside en la memoria y/o en el disco local de un nodo trabajador. El
propósito de las particiones del DataFrame es que los datos divididos en trozos más
pequeños puedan tener cálculos ejecutados en paralelo. Esto acelera enormemente los
cálculos.
coalesce utiliza las particiones existentes para minimizar la cantidad de datos que se
barajan.
repartition crea nuevas particiones y hace un barajado completo.
coalesce da como resultado particiones con diferentes cantidades de datos (a veces
particiones que tienen tamaños muy diferentes) y repartition da como resultado
particiones de tamaño más o menos igual.

Saving a DataFrame as a Parquet file or SQL table

Introducción:
// In Scala it would be similar

val fireSchema = StructType(

Array(

StructField("CallNumber", IntegerType, true),

StructField("UnitID", StringType, true),

StructField("IncidentNumber", IntegerType, true),

StructField("CallType", StringType, true),

StructField("Location", StringType, true),

... ...

StructField("Delay", FloatType, true)


)

// Read the file using the CSV DataFrameReader

val sfFireFile="/databricks-datasets/learning-spark-v2/sf-fire/sf-fire-calls.csv"

val fireDF = spark.read.schema(fireSchema) .option("header", "true") .csv(sfFireFile)

Fuente: (15) The Parquet Format and Performance Optimization Opportunities Boudewijn Braams
(Databricks) - YouTube

Transformations and actions


Projections and filters.
SHOW

By default show() method displays only 20 rows from DataFrame.

show(2) // para mostrar solo 2 filas

Values in columns are truncated at 20 characters

show(false) // Muestra todo el contenido de la columna

show(2, false) // Las dos cosas juntas

show(2,25) // muestra 25 caracteres

show(2,25,true) // muestra las filas verticalmente

show(self, n=20, truncate=True, vertical=False):

return number of distinct types of calls using countDistinct():


filter for only distinct non-null CallTypes from all the rows:

Renaming, adding, and dropping columns

spark.sql.functions has a set of to/from date/time‐ stamp functions such as to_timestamp() and
to_date() to convert to more usble format:

Este codigo ha:

1.Convert the existing column’s data type from string to a Spark-supported timestamp.

2. Use the new format specified in the format string "MM/dd/yyyy" or "MM/dd/yyyy hh:mm:ss a"
where appropriate.

3. After converting to the new data type, drop() the old column and append the new one specified in
the first argument to the withColumn() method.

4. Assign the new modified DataFrame to fire_ts_df.

The Dataset API


Spark 2.0 unificó las APIs DataFrame y Dataset como API estructuradas con interfaces similares para
que los desarrolladores sólo tengan que aprender un único conjunto de APIs.
Dataset
Es una colección de datos distribuidos que tienen una estructura a diferencia de los RDD que son
computos de datos desestructurados. API disponible en Java y Scala

Dataframe

Es un DataSet organizado en columnas con su nombre de columna

Conceptualmente, puedes pensar en un DataFrame en Scala como un alias para una colección de
objetos genéricos, Dataset[Row], donde un Row es un objeto JVM genérico no tipado que puede
contener diferentes tipos de campos. Un Dataset, por el contrario, es una colección de objetos JVM
fuertemente tipados en Scala o una clase en Java. O, como dice la documentación de Dataset, un

Dataset es: una colección fuertemente tipada de objetos específicos del dominio que pueden ser
transformados en paralelo usando operaciones funcionales o relacionales. Cada Dataset [en Scala]
también tiene una vista unitaria llamada DataFrame, que es un Dataset de Row.

Typed Objects, Untyped Objects, and Generic Rows


En los lenguajes soportados por Spark, los Datasets sólo tienen sentido en Java y Scala, mientras que
en Python y R sólo tienen sentido los DataFrames. Esto se debe a que Python y R no son seguros en
tiempo de compilación; los tipos se infieren o asignan dinámicamente durante la ejecución, no
durante el tiempo de compilación. Lo contrario ocurre en Scala y Java: los tipos se vinculan a las
variables y objetos en tiempo de compilación. En Scala, sin embargo, un DataFrame es sólo un alias
para el Dataset[Row] no tipado.

Row is a generic object type in Spark, holding a collection of mixed types that can be accessed using
an index.

For example, an Int as one of your fields in a Row will be mapped or converted to IntegerType or
IntegerType() respectively for Scala or Java and Python:
Creating Datasets
As with creating DataFrames from data sources, when creating a Dataset you have to know the
schema. In other words, you need to know the data types. Although with JSON and CSV data it’s
possible to infer the schema, for large data sets this is resource-intensive (expensive). When creating
a Dataset in Scala, the easiest way to specify the schema for the resulting Dataset is to use a case class.

Scala: Case classes

Cuando quieras instanciar tu propio objeto específico del dominio como un Dataset, puedes hacerlo
definiendo una clase case en Scala. Como ejemplo, veamos una colección de lecturas de dispositivos
del Internet de las Cosas (IoT) en un archivo JSON (utilizamos este archivo en el ejemplo de extremo a
extremo más adelante en esta sección).

Dataset Operations
Just as you can perform transformations and actions on DataFrames, so you can with Datasets.
Depending on the kind of operation, the results will vary:

In this query, we used a function as an argument to the Dataset method filter(). This is an overloaded
method with many signatures.

DataFrame vs Dataset
A estas alturas te estarás preguntando por qué y cuándo debes usar DataFrames o Data sets. En
muchos casos, cualquiera de los dos sirve, dependiendo de los lenguajes con los que se trabaje, pero
hay algunas situaciones en las que es preferible uno u otro. He aquí algunos ejemplos:

DataFrame Dataset
Decirle a Spark qué hacer, no cómo hacerlo
Semántica rica, abstracciones de alto nivel y operadores DSL
Si su procesamiento exige expresiones de alto nivel, filtros, mapas,
agregaciones, cálculo de medias o sumas, consultas SQL, acceso a
columnas o uso de operadores relacionales en datos semiestructurados
Si su procesamiento requiere transformaciones relacionales similares a
las consultas SQL
Desea unificar, optimizar el código (Catalyst optimizer) y simplificar las
API en todos los componentes de Spark
Si eres un usuario de R
Si eres un usuario de Python (baja a RDDs si necesitas más control)
Si quieres eficiencia de espacio y velocidad
Si quieres una seguridad de tipos estricta en tiempo de compilación y no
te importa crear múltiples case classes para un Dataset[T] específico
Si desea aprovechar y beneficiarse de la eficiente serialización de
Tungsten (mejor eficiente en memoria) con codificadores

The Catalyst Optimizer


Resumen del blog: Spark SQL: Optimizador Catalyst - BI Geek Blog (bi-geek.com)

Spark SQL es un módulo de Apache Spark para el procesamiento de datos estructurados. Una de las
grandes diferencias respecto a la Spark API RDD es que sus interfaces proporcionan información
adicional para realizar procesos más eficientes. Esta información además es útil para que Spark SQL
se beneficie internamente del uso de su optimizador Catalyst y mejorar el rendimiento en el
procesamiento de datos.

La mayor abstracción en la API de Spark SQL es el DataFrame. En Spark, un DataFrame es una colección
distribuida de datos organizada en filas con el mismo esquema. Conceptualmente es equivalente a
una tabla en una base de datos relacional. Los DataFrames en Spark tienen las mismas capacidades
que los RDDs, como por ejemplo su inmutabilidad, en memoria, resilientes, computación distribuida.
Además, le aplica una estructura llamada esquema a los datos. La mayor diferencia entre los
DataFrames y los RDDs es que los DataFrames, al tener más información sobre la estructura de los
datos, permite un mayor nivel de abstracción y técnicas de optimización que no serían posibles con
los RDDs. A partir de la versión de Spark 2.0, se fusionaron en una sola API los DataFrames y Datasets,
unificando así sus capacidades en el procesamiento de datos a través de sus librerías.

Qué es Catalyst
Spark SQL fue diseñado con un optimizador llamado Catalyst basado en la programación funcional de
Scala. Sus dos propósitos principales son: primero, añadir nuevas técnicas de optimización para
resolver algunos problemas con “big data” y segundo, permitir a los desarrolladores ampliar y
personalizarnm las funciones del optimizador.
Componentes Catalyst
Los componentes principales del optimizador de Catalyst son los siguientes:

Trees
El tipo de datos principal en Catalyst son los tree. Cada tree está compuesto por nodes, y cada node
tiene un nodetype y cero o más hijos. Estos objetos son inmutables y pueden ser manipulados con
lenguaje funcional.

Rules
Los trees pueden ser manipulados usando reglas, que son funciones de un tree a otro tree. El
método de transformación aplica la función pattern matching recursivamente en todos los nodos del
tree transformando cada patrón al resultado. A continuación se muestra un ejemplo de regla
aplicada a un tree.

Usando Catalyst en Spark SQL


El optimizador de Catalyst en Spark ofrece una optimización basada en reglas y basada en costes. La
optimización basada en reglas indica cómo ejecutar la consulta a partir de un conjunto de reglas
definidas. Mientras tanto, la optimización basada en costes genera múltiples planes de ejecución y los
compara para elegir el de menor coste.

DF -> Spark SQL Engine -> RDD

El optimizador Catalyst toma una consulta computacional y la convierte en un plan de ejecución. Pasa
por cuatro fases de transformación:

1. Análisis

2. Optimización lógica

3. Planificación física
4. Generación de código

Por ejemplo, considere una de las consultas de nuestro ejemplo de M&Ms en el capítulo 2.

Los dos bloques de código de ejemplo siguientes pasarán por el mismo proceso, terminando
eventualmente con un plan de consulta similar y un bytecode idéntico para su ejecución. Es decir,
independientemente del lenguaje que utilices, tu computación pasa por el mismo camino y el
bytecode resultante es probablemente el mismo:

Plan de Optimizacion/Optimization

Para ver las diferentes etapas por las que pasa el código Python, puedes utilizar el método
count_mnm_df.explain(True) en el DataFrame. O, para ver los diferentes planes diferentes planes
lógicos y físicos, en Scala puedes llamar a df.queryExecution.logical o
df.queryExecution.optimizedPlan. (En el Capítulo 7, hablaremos más sobre ajuste y depuración de
Spark y cómo leer los planes de consulta).

Repasemos cada una de las cuatro fases de optimización de las consultas.


Fase 1: Análisis

El motor Spark SQL comienza generando un árbol de sintaxis abstracta (AST) para la consulta SQL o
DataFrame. En esta fase inicial, cualquier columna o nombre de tabla se resolverá consultando un
Catálogo interno, una interfaz programática de Spark SQL que contiene una lista de nombres de
columnas, tipos de datos, funciones, tablas, bases de datos, etc. Una vez que se han resueltos con
éxito, la consulta pasa a la siguiente fase.

Fase 2: Optimización lógica

Como muestra la Figura primera, esta fase comprende dos etapas internas. Aplicando un enfoque de
optimización basado en reglas estándar, el optimizador de Catalyst construirá primero un conjunto de
múltiples planes y, a continuación, utilizando su optimizador basado en costes (CBO), asignará costes
a cada plan. Estos planes se presentan como árboles de operadores (como en la Figura 3-5); pueden
incluir, por ejemplo, el proceso de plegado de constantes, pushdown de predicados, poda de
proyecciones, simplificación de expresiones booleanas, etc. Este plan lógico es la entrada en el plan
físico.

Fase 3: Planificación física

En esta fase, Spark SQL genera un plan físico óptimo para el plan lógico seleccionado, utilizando
operadores físicos que coinciden con los disponibles en el motor de ejecución de Spark. El plan a
ejecutarse se selecciona usando el modelo basado en costes (comparación entre costes de los
modelos).

Fase 4: Generación de código

La fase final de la optimización de la consulta consiste en generar un bytecode Java eficiente para
ejecutar en cada máquina. Dado que Spark SQL puede operar sobre conjuntos de datos cargados en
memoria,

Spark puede utilizar tecnología de compilación de última generación para la generación de código para
acelerar la ejecución. En otras palabras, actúa como un compilador. El proyecto Tungsten, que facilita
la la generación de código en toda la etapa, desempeña un papel importante.

¿Qué es la generación de código en toda la fase? Es una fase de optimización de la consulta física que
reduce toda la consulta a una sola función, eliminando las llamadas a funciones virtuales y empleando
los registros de la CPU para los datos intermedios. La segunda generación del motor Tungsten,
introducido en Spark 2.0, utiliza este enfoque para generar un código RDD compacto para la ejecución
final. Esta estrategia racionalizada mejora significativamente la eficiencia y el rendimiento de la CPU.

Conclusiones
El optimizador Catalyst de Spark SQL mejora la productividad de los desarrolladores y el rendimiento
de las consultas que escriben. Catalyst transforma automáticamente las consultas relacionales para
ejecutarlas más eficientemente usando técnicas como filtrados, índices y asegurando que los joins
de los orígenes de datos se realizan en el orden más eficiente. Además, su diseño permite a la
comunidad de Spark implementar y extender el optimizador con nuevas funcionalidades.

Resumen del tema


En este capítulo, nos sumergimos en las APIs estructuradas de Spark, comenzando con una historia y
los méritos de la estructura en Spark.

A través de operaciones de datos comunes ilustrativas y ejemplos de código, demostramos que las
APIs de alto nivel DataFrame y Dataset son mucho más expresivas e intuitivas que la API RDD de bajo
nivel. Diseñadas para facilitar el procesamiento de grandes conjuntos de datos. Las APIs
estructuradas proporcionan operadores específicos del dominio para operaciones de datos
comunes, aumentando la claridad y la expresividad de su código.

Exploramos cuándo utilizar los RDD, los DataFrames y los Datasets, en función de los escenarios de
su caso de uso. de los casos de uso.

Y finalmente, echamos un vistazo bajo el capó para ver cómo los principales componentes del motor
Spark SQL -el optimizador Catalyst y el Proyecto Tungsten- soportan las APIs estructuradas de alto
nivel y los operadores DSL. Como has visto, no importa cuál de los lenguajes soportados por Spark se
utilice, una consulta de Spark se somete al mismo viaje de optimización, desde la construcción del
plan lógico y físico hasta el plan final.

Desde la construcción del plan lógico y físico hasta la generación final de código compacto.

Los conceptos y ejemplos de código de este capítulo han sentado las bases para los los dos próximos
capítulos, en los que ilustraremos la interoperabilidad sin fisuras entre DataFrames, Datasets y Spark
SQL.

Ejercicios extra
b. Leer el CSV del ejemplo del cap2 y obtener la estructura del schema dado por defecto.

Resultado:

c. Cuando se define un schema al definir un campo por ejemplo StructField('Delay', FloatType(), True)
¿qué significa el último parámetro Boolean?

Si el parámetro puede ser null o no


d. Dataset vs DataFrame (Scala). ¿En qué se diferencian a nivel de código?

dataframe=dataset[Row]

Un DataSet es una colección de datos distribuidos que tienen ya una


estructura, a diferencia de los RDD, que son conjuntos de datos
desestructurados.
Un DataFrame es un DataSet que a la vez está organizado en columnas.
A DataFrame is equivalent to a relational table in Spark SQL

e. Utilizando el mismo ejemplo utilizado en el capítulo para guardar en parquet y guardar los datos en
los formatos:

Parquet
// In Scala to save as a table

val parquetTable = ...

// name of the table

fireDF.write.format("parquet").saveAsTable(parquetTable)

otra forma
import org.apache.spark.sql.SparkSession

val spark = SparkSession

.builder

.appName("MnMCount")

.getOrCreate()

val data = Seq(("James ","","Smith","36636","M",3000),

("Michael ","Rose","","40288","M",4000),

("Robert ","","Williams","42114","M",4000),

("Maria ","Anne","Jones","39192","F",4000),

("Jen","Mary","Brown","","F",-1))

val columns = Seq("firstname","middlename","lastname","dob","gender","salary")

import spark.sqlContext.implicits._

val df = data.toDF(columns:_*)
df.write.format("parquet").save("/tmp/data/example_cap3.parquet")

i. JSON

ii. CSV (dándole otro nombre para evitar sobrescribir el fichero origen)

iii. AVRO

f. Revisar al guardar los ficheros (p.e. json, csv, etc) el número de ficheros creados, revisar su contenido
para comprender (constatar) como se guardan.

JSON

Dividido en 5 particiones (0-4)

Part0:
i. ¿A qué se debe que hayan más de un fichero?

cada particion saca un fichero

ii. ¿Cómo obtener el número de particiones de un DataFrame?

df.rdd.getNumPartitions()
numberDF.rdd.partitions.size

iii. ¿Qué formas existen para modificar el número de particiones de un DataFrame?

coalesce uses existing partitions to minimize the amount of data that's shuffled.

repartition creates new partitions and does a full shuffle.

coalesce results in partitions with different amounts of data (sometimes partitions that have
much different sizes) and repartition results in roughly equal sized partitions.

.coalesce(1)

.repartition(1)
iv. Llevar a cabo el ejemplo modificando el número de particiones a 1 y revisar de nuevo el/los ficheros
guardados

df.coalesce(1).write.format("csv").save("/Filestore/tables/example_cap3_coalesce.csv")

Capitulo 4 - Spark SQL and DataFrames:


Introduction to Built-in Data Sources
Using Spark SQL in Spark Applications
consultas SQL - Método sql() sobre la instancia SparkSession, spark, como spark.sql("SELECT * FROM
myTableName")

Estas consultas sql ejecutadas de esta manera devuelven un DataFrame donde puedes realizar más
consultas Spark

Basic Query Examples


Estos ejemplos le ofrecerán una muestra de cómo utilizar SQL en sus aplicaciones Spark via

spark.sql programmatic interface

De froma similar a la API DataFrame la interfaz te permite consultar datos estructurados en tus
aplicaciones Spark

Normalmente, en una aplicación Spark independiente, crearás una instancia SparkSession


manualmente, como se muestra en el siguiente ejemplo. Sin embargo, en un shell de Spark (o en un
cuaderno de Databricks), la SparkSession se crea por ti y se puede acceder a ella a través de la variable
llamada spark.

Reading the data set into a temporary view:


If you want to specify a schema, you can use a DDL-formatted string:

SQL Tables and Views


Managed Versus Unmanaged Tables
Información ampliada con esta web: 3 Ways To Create Tables With Apache Spark | by AnBento |
Apr, 2021 | Towards Data Science

Para una tabla gestionada (managed or internal), Spark gestiona la estructura de la tabla y tanto los
metadatos como los datos en el almacén de archivos, que puede ser un sistema de archivos local,
HDFS, o un almacén de objetos como Amazon S3 o Azure Blob. Si intenta escribir SQL en esta tabla,
obtendrá un error de que la tabla o la columna no existen.

Para una tabla no gestionada (unmanaged or external), tu defines la tabla y la estructura y se la


comunicas a Spark sólo gestiona los metadatos, mientras que tú mismo gestionas los datos en una
fuente de datos externa como Cassandra. Si desea ejecutar SQL en esta tabla, puede hacerlo.

Si importas un archivo csv para construir una tabla de referencia, automáticamente se construirá
una tabla gestionada...

Con una managed table - un comando sql como DROP TABLE borra los metadatos y los datos

Creando managed table:

METHOD #1
df_final.write.mode("overwrite").saveAsTable("salesTable_manag1")

METHOD #2
%sql,

CREATE TABLE IF NOT EXISTS salesTable_manag2 AS

SELECT * FROM df_final_View

o
Con una unmanaged - el mismo comando borraria solo metadatos

Creando unmanaged table:

METHOD #1
To create an unmanaged (external) table you can simply specify a path before the saveAsTable()
method:

METHOD #2

As similar result can be obtained by specifying the location as part of a SQL query.

%sql,

CREATE EXTERNAL TABLE IF NOT EXISTS salesTable_unmanag2

ROW FORMAT DELIMITED FIELDS TERMINATED BY ','

LOCATION '/FileStore/tables/salesTable_unmanag2' AS

SELECT * FROM df_final_View

Creating SQL Databases and Tables


Por defecto, Spark crea tablas debajo de la base de datos default. Para crear tu propia base de datos
puede ejecutar un comando SQL desde su aplicación o cuaderno Spark.

Creating Views
Además de crear tablas, Spark puede crear vistas sobre tablas existentes.
Las vistas pueden ser:

• Globales (visibles para todas las Spark Sessions o para un cluster dado)
• Orientadas a sesiones (visibles para una única SparkSession), y son temporales: desaparecen
después de que termine la aplicación de Spark.

Crear vistas tiene una sintaxis similar a crear tablas en una base de datos . Cuando creas una vista, la
puedes consultar como si fuera una tabla. La diferencia entre una vista y una tabla es que las vistas no
mantienen los datos, las tablas persisten despues de que la aplicación de Spark termine, pero las vistas
desaparecen

Para acceder a una vista global: global_temp.<view_name>

Para acceder a una vista normal: sin el global_temp

Viewing the Metadata


Spark maneja los metadatos asociados con cada tabla manejada o no manejada.

En una aplicación Spark, después de crear la SparkSession variable, puedes acceder a todos los
metadatos guardados con metodos como estos:

Almacenamiento en caché de tablas SQL


Aunque hablaremos de las estrategias de almacenamiento en caché de las tablas en el próximo
capítulo, vale la pena mencionar aquí que, al igual que los DataFrames, se pueden almacenar en caché
y desalmacenar tablas y vistas SQL.

En Spark 3.0, además de otras opciones, puedes especificar una tabla como LAZY, es decir que sólo
debe ser almacenada en caché cuando se utiliza por primera vez en lugar de inmediatamente

Va mas rapido
Reading Tables into DataFrames
A menudo, los ingenieros de datos construyen pipelines de datos como parte de sus procesos
regulares de ingestión de datos y ETL. Llenan las bases de datos y las tablas de Spark SQL con datos
limpios para para su consumo por parte de las aplicaciones.

Supongamos que tiene una base de datos existente, learn_spark_db, y una tabla, us_delay_flights_tbl,
lista para su uso. En lugar de leer desde un archivo JSON externo, puedes utilizar simplemente SQL
para consultar la tabla y asignar el resultado devuelto a un DataFrame:

Now you have a cleansed DataFrame read from an existing Spark SQL table. You can also read data in
other formats using Spark’s built-in data sources, giving you the flex‐ ibility to interact with various
common file formats.

Data Sources for DataFrames and SQL Tables


DataFrameReader
DataFrameReader es el centro de la construcción para leer datos de una fuente de datos a un
DataFrame. Tiene un formato definido y un patrón de uso:

DataFrameReader.format(args).option("key", "value").schema(args).load()

Este patrón de encadenamiento de métodos es común en Spark, y fácil de leer. Lo vimos en el Capítulo
3 cuando exploramos patrones comunes de análisis de datos.

Ten en cuenta que sólo puedes acceder a un DataFrameReader a través de una instancia de
SparkSession.

Es decir, no puedes crear una instancia de DataFrameReader. Para obtener una instancia del mismo,
utiliza:

SparkSession.read // or SparkSession.readStream

Mientras que read devuelve un handle a DataFrameReader para leer en un DataFrame desde una
fuente de datos estática, readStream devuelve una instancia para leer desde una fuente de streaming.

(Cubriremos el Streaming Estructurado más adelante en el libro).

Los argumentos de cada uno de los métodos públicos de DataFrameReader toman diferentes valores:
Importante, aprendete esas y las siguientes opciones de lectura de datos

.option() puede ser:

.option("inferSchema", "true") // Para que infiera el esquema de datos automatico

.option("header", "true") // La fuente de datos tiene un header

.option("delimiter", "\t")

.option("mode", "PERMISSIVE")

.option("compression", "snappy")

.option("mode", "FAILFAST") // Exit if any errors

.option("nullValue", "") // Replace any null data with quotes

.option("emptyValue", 0)

.option("path", file) //igual que load()

.option("pathGlobFilter", "*.jpg")

.option("recursiveFileLookup", "true")

Conectar con BD:

.option("url", "jdbc:postgresql:[DBSERVER]")

.option("dbtable", "[SCHEMA].[TABLENAME]")

.option("driver", "com.mysql.jdbc.Driver")

.option("user", "[USERNAME]")

.option("password", "[PASSWORD]")

Las opciones tambien se pueden poner juntas:

.options(header="true", inferSchema="true", sep="\t")

Ejemplo de combinación:
DataFrameWriter
DataFrameWriter hace lo contrario de su homólogo: guarda o escribe datos en una fuente de datos
incorporada especificada. A diferencia del DataFrameReader, no se accede a su instancia desde una
SparkSession, sino desde el DataFrame que se desea guardar.

Patrones de uso recomendados:

DataFrameWriter.format(args).option(args).bucketBy(args).partitionBy(args)
.save(path) // bucketBy = orderby

DataFrameWriter.format(args).option(args).sortBy(args).saveAsTable(table)

df.write.mode("overwrite").saveAsTable("tabla") //Crea una tabla managed llamada


tabla

Parquet
Parquet es un formato de archivo columnar de código abierto que ofrece muchas optimizaciones de
E/S (como la compresión, que ahorra espacio de almacenamiento y permite un rápido acceso a las
columnas de datos).
Debido a su eficacia y a estas optimizaciones, le recomendamos que después de haber transformado
y limpiado sus datos, guarde sus DataFrames en el formato Parquet para su consumo posterior.

Parquet files are stored in a directory structure that contains the data files, metadata, a number of
compressed files, and some status files.

To read Parquet files into a DataFrame, you simply specify the format and path:

//""" para varias lineas (reduce tabuladores a espacios)

tip: leer -> spark.read.format.load

Reading Parquet files into a Spark SQL table

De DataFrame a Parquet

Recall that Parquet is the default file format. If you don’t include the format() method, the DataFrame
will still be saved as a Parquet file.

JSON
La notación de objetos de JavaScript (JSON) también es un formato de datos muy popular. Saltó a la
fama por ser un formato fácil de leer y de descifrar en comparación con XML. Tiene dos formatos de
representación: el modo de una línea y el modo de varias líneas. Ambos modos son compatibles con
Spark.

En el modo de línea única, cada línea denota un único objeto JSON, mientras que en el modo multilínea
todo el objeto constituye un único objeto JSON. Para leer en este modo, establezca multiLine como
verdadero en el método option()

JSON to DataFrame
JSON to Spark SQL table

DataFrames to JSON

CSV
Tan utilizado como los archivos de texto plano, este formato de archivo de texto común captura cada
dato o campo delimitado por una coma; cada línea con campos separados por comas representa un
registro. Aunque la coma es el separador por defecto, puede utilizar otros delimitadores

para separar los campos en los casos en que las comas formen parte de sus datos. Las hojas de cálculo
más populares pueden generar archivos CSV, por lo que es un formato muy popular entre los analistas
de datos y de negocios.

CSV to DataFrame
CSV to Spark SQL table

DataFrame to CSV

Avro
Se introdujo en Spark 2.4 con una fuente de datos incorporada. Es usado por ejemplo por Apache
Kafka para serialización y deserialización de mensajes. Ofrece muchos beneficios, incluyendo mapeo
directo a JSON, velocidad y eficacia y fijaciones disponibles para muchos lenguajes de programación

Avro to DataFrame
Leer un fichero Avro a un DataFrame usando DataFrameReader es consistente en uso con las otras
fuentes de datos

Avro to Spark SQL table


DataFrames to Avro

ORC
Como formato de archivo columnar optimizado adicional, Spark 2.x admite un lector vectorizado de
ORC vectorizado. Dos configuraciones de Spark dictan qué implementación de ORC utilizar.

Cuando spark.sql.orc.impl se establece como nativo y spark.sql.orc.enableVectorize dReader se


establece en true, Spark utiliza el lector ORC vectorizado. Un lector vectorizado

lee bloques de filas (a menudo 1.024 por bloque) en lugar de una fila cada vez, lo que agiliza las
operaciones y reduce el uso de la CPU para operaciones intensivas como escaneos, filtros,
agregaciones y uniones. y uniones.

Para las tablas Hive ORC SerDe (serialización y deserialización) creadas con el comando SQL SQL USING
HIVE OPTIONS (fileFormat 'ORC'), se utiliza el lector vectorizado cuando el parámetro de configuración
de Spark spark.sql.hive.convertMetastoreOrc está se establece en true.

ORC a DataFrame

ORC a Spark SQL table

DataFrames to ORC

Images
Binary Files
Resumen del tema
Para recapitular, este capítulo exploró la interoperabilidad entre la API de DataFrame y Spark SQL.
En particular, has visto cómo utilizar Spark SQL para:

• Crear tablas gestionadas y no gestionadas utilizando Spark SQL y la API del DataFrame API.
• Leer y escribir en varias fuentes de datos y formatos de archivo incorporados.
• Emplear la interfaz programática spark.sql para emitir consultas SQL sobre datos
estructurados almacenados como Spark SQL.
• Examinar el catálogo de Spark para inspeccionar los metadatos asociados a las tablas y
vistas.

A través de los fragmentos de código en el capítulo y los cuadernos disponibles en el GitHub del
libro, tienes una idea de cómo utilizar DataFrames y Spark SQL. Siguiendo con el siguiente capítulo
explora más a fondo cómo Spark interactúa con las fuentes de datos externas. Verá algunos
ejemplos más detallados de transformaciones y la interoperabilidad entre la API de DataFrame y
Spark SQL

Ejercicios extra
a). Realizar todos los ejercicios propuestos de libro

b) GlobalTempView vs TempView

La global se puede acceder desde todas las SparkSession creadas, sin el global solo con la que la hayas
creado

c) Leer los AVRO, Parquet, JSON y CSV escritos en el cap3

CSV -> sf-fire-calls.csv

JSON -> blogs.json

Capítulo 5 - Spark SQL and DataFrames:


Interacting with External Data Sources
Spark SQL and Apache Hive
Spark SQL es un componente fundamental de Apache Spark que integra el procesamiento relacional
con la API de programación funcional de Spark. Su génesis se encuentra en el trabajo en Shark. Shark
se construyó originalmente sobre la base de código Hive en la parte superior de Apache Spark y se
convirtió en uno de los primeros motores de consulta SQL interactivos en sistemas Hadoop. Demostró
que era posible tener lo mejor de ambos mundos; tan rápido como un como un data warehouse, y
que escale tan bien como Hive/MapReduce

Spark SQL permite a los programadores de Spark aprovechar las ventajas de un rendimiento más
rápido y programación relacional (por ejemplo, consultas declarativas y almacenamiento optimizado),
así como llamar a bibliotecas de análisis complejas (por ejemplo, aprendizaje automático). Como se
ha comentado en el capítulo anterior, a partir de Apache Spark 2.x, la SparkSession proporciona un
único punto de entrada unificado para manipular datos en Spark

User-Defined Functions
Aunque Apache Spark tiene un sinfín de funciones incorporadas, la flexibilidad de Spark permite a
los ingenieros y científicos de datos definir también sus propias funciones. Estas se conocen como
funciones definidas por el usuario (UDF).
Spark SQL UDFs
El beneficio de crear tu propio PySpark o Scala UDFs es que tu (y otros) podreis hacer uso de ellas
dentro del propio Spark SQL. Por ejemplo, un cientifico de datos puede envolver un modelo ML
dentro de un UDF para que un analista de datos pueda consultar sus predicciones en Spark SQL sin
tener que entender necesariamente el funcionamiento interno del modelo.

Este es un ejemplo simplificado de la creación de una UDF de Spark SQL. Tenga en cuenta que las
UDFs operan por sesión y no se persiguen en el metastore subyacente:

// Se crea la funcion cubo, se registra en udf, luego se crea una vista que tenga id del 1,9 y luego se
junta con cubo

spark.sql("SELECT id, cubed(id) AS id_cubed FROM udf_test").show()

Reutilizadas en otros lenguajes

Evaluation order and null checking in Spark SQL


Spark SQL (que incluye SQL, the DataFrame API, y el Dataset API) no garantiza el orden de evaluación
de subexpresiones. Por ejemplo, la siguiente query no garantiza que "s is not null" se ejecute antes
que "strlen(s) >1)

spark.sql("SELECT s FROM test1 WHERE s IS NOT NULL AND strlen(s) > 1")

Por lo tanto, para realizar una correcta comprobación de nulos, se recomienda hacer lo siguiente lo
siguiente:

1. Hacer que la propia UDF sea null-aware y hacer la comprobación de null dentro de la UDF.

2. Utilizar expresiones IF o CASE WHEN para realizar la comprobación de nulos e invocar la UDF en
una rama condicional.
Speeding up and distributing PySpark UDFs with Pandas UDFs

Uno de los problemas vigentes usando PySpark UDFs fue que tenian menor rendimiento que los
UDFs de Scala. Esto fue porque los UDFs de PySpark requerían movimiento de datos entre la JVM y
Python, el cual era muy caro. Para resolver este problema, PandasUDFs (vectorized UDFs) fueron
introducidos como parte de Apache Spark 2.3. Un Panda UDF usa Apache Arrow para transferir
datos y Pandas para trabajar con los datos. Se define una UDF de Pandas utilizando la palabra clave
pandas_udf como el decorador, o para envolver la propia función.

Una vez que los datos están en el formato de Apache Arrow, ya no hay necesidad de
serializar/pickear los datos ya que están en un formato consumible por el proceso de Python. En
lugar de operar sobre entradas individuales fila por fila, se opera sobre una Serie o DataFrame de
Pandas (es decir, ejecución vectorizada).

Desde Apache Spark 3.0 con Python 3.6 y más, los UDFs de Panda fueron divididos en dos categorías
API: Pandas UDFs and Pandas Function APIs

Pandas UDFs

Solo para Python

Con Apache Spark 3.0, las UDFs de Pandas infieren el tipo de UDF de Pandas a partir de las pistas de
tipo de Python en las UDFs de Pandas como pandas.Series, pandas.DataFrame, Tuple e Iterator.
Anteriormente era necesario definir y especificar manualmente cada tipo de UDF de Pandas.
Actualmente, los casos soportados de sugerencias de tipos de Python en UDFs de Pandas son Serie a
Serie, Iterador de Serie a Iterador de Serie, Iterador de Multi de series, Iterador de series a Iterador
de series, Iterador de series múltiples a Iterador de series, y Serie a Escalar (un solo valor)

Pandas Function APIs

Las APIs de funciones de Pandas permiten aplicar directamente una función local de Python a un
PySpark DataFrame donde tanto la entrada como la salida son instancias de Pandas. Para Spark 3.0,
las APIs de funciones de Pandas soportadas son grouped map, map, cogrouped map.

Ejemplo de UDF Panda Scalar para Spark 3.0


Usando la funcion cubo:

Con DataFrames:

Plan de ejecución:

A diferencia de una función local, el uso de una UDF vectorizada dará lugar a la ejecución de trabajos
de Spark; la función local anterior es una función de Pandas ejecutada sólo en el controlador de
Spark. Esto se hace más evidente al ver la UI de Spark para una de las etapas de esta función
pandas_udf
Como muchos trabajos de Spark, el trabajo comienza con parallelize() para enviar datos locales
(lotes binarios de Arrow) a los ejecutores y llama a mapPartitions() para convertir los lotes binarios
de Arrow al formato de datos interno de Spark, que puede ser distribuido a los trabajadores de
Spark. Hay una serie de pasos de WholeStageCodegen, que representan un paso fundamental paso
en el rendimiento (gracias a la generación de código WholeStageCodegen del Proyecto Tungsten que
mejora significativamente la eficiencia y el rendimiento de la CPU). Pero es el paso ArrowE valPython
que identifica que (en este caso) se está ejecutando una UDF de Pandas.

Querying with the Spark SQL Shell, Beeline, and Tableau


Hay varios mecanismos para consultar Apache Spark, incluyendo el shell Spark SQL la utilidad
Beeline CLI, y las herramientas de informes como Tableau y Power BI.

Using the Spark SQL Shell


Crate a table
Insert data into table
Running a Spark SQL query
Working with Beeline
Es sólo un cliente JDBC que se conecta al Thriftserver de Spark (servidor JDBC). Beeline proporciona
una interfaz SQL para que puedas interactuar con Spark SQL

Start the Thrift server


Connect to the Thrift server via Beeline
Execute a Spark SQL query with Beeline
Stop the Thrift server
Working with Tableau
Para conectar esta herramineta BI a Spark SQL

Start the Thrift server


Start Tableau
Stop the Thrift server
External Data Sources
JDBC and SQL Databases
Spark SQK incluye una API de la fuente de datos para poder leer datos de otras bases de datos
usando JDBC. Esto simplifica el hacer consultas a esas fuentes de datos ya que revuelve los
resultados como un DataFrame, proveyendo todos los beneficios de Spark SQL (incluyendo el
rendimiento y la habilidad de unirlo con otras fuentes de datos)

Para empezar necesitarás especificar el JDBC driver para tu JDBC fuente de datos y será necesario
qeu esté en el classpath de Spark. Desde el fichero de $SPARK_HOME, empitirá un comando como el
siguiente:

./bin/spark-shell --driver-class-path $database.jar --jars $database.jar


Utilizando la API de la fuente de datos, las tablas de la base de datos remota pueden cargarse como
un DataFrame o vista temporal de Spark SQL. Los usuarios pueden especificar las propiedades de la
conexión JDBC

en las opciones de la fuente de datos. La Tabla 5-1 contiene algunas de las propiedades de conexión
propiedades de conexión (insensible a mayúsculas y minúsculas) que soporta Spark.

The importance of partitioning


Cuando transferimos grandes cantidades de datos entre el Spark SQL y una fuente externa JDBC, es
importante particionar tu fuente de datos. Todos tus datos van a través de una conexión con el
driver, que puede saturar y disminuir significantemente el rendimiento de tu expraccion, tanto como
los recursos de tu fuente del sistema potencialmente los saturen. Mientras esas propiedades JDBC
son opcionales, para unas operaciones a larga-escala es altamente recomendado usar las siguientes
propiedades:

Higher-Order Functions in DataFrames and Spark SQL


2 soluciones para manipular tipos de datos complejos (struct, array, map):

• Explorando la estructura anidada en filas individuales, aplicando funciones y re creando


estructuras anidadas
• Construyendo una UDF

Transformando un Array
(23) An Introduction to Higher Order Functions in Spark SQL with Herman van Hovell (Databricks) -
YouTube

Option 1. Explode and collect

Explode: Crea una tupla por cada elemento del array


Id Vals =>> Id Vals

1 [1,2,3] 1 1

2 [4,5,6] 1 2

1 3 (...)

Collect: Agrupa los elementos con un id dado

Id Val =>> Id Vals

1 1+1 1 [2,3,4]

1 2+1

1 3+1

Option 2 :

spark.sql("SELECT id, plusOneInt(values) AS values FROM table").show()

Built-in Functions for Complex Data Types


High-Order Functions
transform(values, value -> lambda expression) // coge un array (valores) y una función anonima (
expresion lambda) como entrada. La función transparentemente crea un nuevo array aplicando la
función anónima a cada elemento, y después asignando el resultado a la salida del array (similar al
aprovechamiento UDF pero mas eficientemente)

Ejemplo simple:

• Creamos DataFrame
• Aplicar high-order queries

transform()
Produce un array aplicando una función a cada elemento del array de entrada

filter(celsius, t -> t > 38)

exists(celsius, t -> t = 38)

reduce(array, B, function, function)

reduce( )
Reduce los elementos de una rray a un solo valor uniendo los elementos en un buffer B usando la
function <B,T,B> y aplicando una funcion final function<B,R>

Common DataFrames and Spark SQL Operations


• Aggregate functions

• Collection functions

• Datetime

• Math functions

• Miscellaneous functions

• Non-aggregate functions

• Sorting functions
• String functions

• UDF functions

• Window functions

• Unions and joins

val bar = delays.union(foo) -> Unir dos DataFrames con el mismo esquema juntos

options with join -> inner, cross, outer, full, full_outer, left, left_outer, right, right_outer, left_semi,
and left_anti

default of an inner join between the air portsna and foo DataFrames:

• Windowing

Una función de ventana utiliza los valores de las filas de una ventana (un rango de filas de entrada)
para devolver un conjunto de valores, normalmente en forma de otra fila. Con las funciones de
ventana, es posible operar en un grupo de filas mientras se devuelve un único valor para cada fila de
entrada. En esta sección, mostraremos cómo utilizar la función de ventana dense_rank() pero hay
muchas otras funciones:

dense_rank()
Para entenderlo mejor: (23) Part 1 How to find nth highest salary in sql - YouTube

Da un ranking, en que posicion está cada numero

=
• Modifications

Aunque los propios DataFrames son inmutables, puede modificarlos mediante operaciones para
crear nuevos DataFrames diferentes, con diferentes columnas, por ejemplo. (Recordemos que los
RDDs subyacentes son inmutables, es decir, no pueden ser cambiados - para asegurar que hay
lineaje da datos para las operaciones de Spark)

Adding new columns

Dropping columns

Renaming columns

Pivoting
Original:

Sentencia:

Resultado:

Resumen del tema


Este capítulo ha explorado cómo Spark SQL interactúa con componentes externos. Hablamos de la
creación de funciones definidas por el usuario, incluyendo los UDFs de Pandas, y presentamos
algunas opciones para ejecutar consultas de Spark SQL (incluyendo el shell de Spark SQL, Beeline y
Tableau). A continuación, proporcionamos ejemplos de cómo utilizar Spark SQL para conectarse con
una variedad de fuentes de datos externas, tales como bases de datos SQL, PostgreSQL, MySQL,
Tableau, Azure Cosmos DB, MS SQL Server, y otros.

Exploramos las funciones incorporadas de Spark para tipos de datos complejos, y dimos algunos
ejemplos de trabajo con funciones de orden superior. Por último, discutimos algunos operadores
relacionales comunes y mostramos cómo realizar una selección de operaciones de DataFrame.

En el próximo capítulo, exploramos cómo trabajar con Datasets, los beneficios de las operaciones
fuertemente tipadas, y cuándo y por qué usarlas.

Ejercicios extras
a. Realizar todos los ejercicios propuestos de libro (excepto los de hive en caso de utilizar spark
instalado en local y en el caso de RDBMS hacer únicamente ejemplo especificado más adelante)

b. Pros y Cons utilizar UDFs


Pros:

• Es más rápido que Explore & Collect


• No sufre de trampas de corrección

Cons:

• Es relativamente lento, necesitamos mucha serialización


• Necesitas registrar UDFs por tipo
• No funciona para SQL

c. Instalar MySQL, descargar driver y cargar datos de BBDD de empleados


https://dev.mysql.com/doc/employee/en/

i. Cargar con spark datos de empleados y departamentos

ii. Mediante Joins mostrar toda la información de los empleados además de su título y
salario.

iii. Diferencia entre Rank y dense_rank (operaciones de ventana)

Rank: Tiene en cuenta anteriores repetidos, por ejemplo si dos salarios tienen el mismo
puesto, el numero 1 , el siguiente salario va a obtener el rankin 3 porque ya hay 2 antes de él

Capítulo 6 - Spark SQL and Datasets


En los capítulos 4 y 5 cubrimos Spark SQL y el DataFrame API. Miramos como conectar fuentes de
datos tanto built-in como external , echamos un vistazo al motor de Spark SQL y exploramos temas
como la interoperabilidad entre SQL y los DataFrames, creando y manejado vistas y tablas, y
avanzadas transformaciones DataFrame y SQL .

Aunque hemos nos hemos introducido brevemente en la API Dataset, nos hemos saltado aspectos
de como los Datasets - colecciones distribuidas fuertemente tipadas - son creadas, guardadas y
serializadas y deserializadas en Spark.

En este capítulo, vamos bajo la cubierta para entender los Dataset: exploraremos el trabajar con
Datasets en Java y Scala, como Spark maneja la memoria para acomodar las construcciones de los
Datasets como parte de la API de alto nivel y el coste asociado usando Datasets.
Single API for Java and Scala
Los Datasets ofrecen una unificada y singular API para objetos fuertemente tipados. A lo largo de los
lenuajes soportados por Spark, solo Scala y Java son fuertemente tipados, por lo que solo Python y R
soportan solo la untipada DataFrame API.

Los Datasets son objetos tipados de dominios especificos que pueden ser operados en paralelo
usando programación funcional o los operadores DSLJava con los que estás familiarizados de la API
DataFrame. Gracias a esta singular API, los desarrolladores ya no se arriesgan a quedarse atrás. Por
ejemplo cualquier interfaz futura o cambios de comportamiento para Scala's groupBy(), flatMap(),
map(), o filter() API será la misma para Java también, porque es una interfaz singular que es común a
ambas implementacioes.

Scala Case Classes and JavaBeans for Datasets


Si recuerdas del capítulo 3, Spark tiene multiples tipos de datos internos, como String Type,
BinaryType, IntegerType, BooleanType y MapType, que utiliza para mapear de datos específicos del
lenguaje en Scala y Java durante las operaciones de Spark. Esta mapeo es realizado mediante
codificadores, que discutiremos más tarde en este capítulo.

Para crear un Dataset[T], donde T es tu objeto tipado en Scala, necesitas una case class que defina el
objeto. Usando nuestro ejemplo de datos dell Capitulo 3, deciamos q teniamos un fichero JSON con
millones de entradas de blog sobre Apache Spark con el siguiente formato:

Para crear un Dataset[Bloggers] distribuimos, debemos primero definir una Scala case class que
defina cada campo individual que comprende un Scala object. Este case class sirve un plano o
esquema para el objeto tipado Bloggers:

y ya leemos el fichero de la fuente:

Similarmente puedes crear una clase JAvaBean de tipo Bloggers en Java y después usar los
codificadores para crear un Dataset<Bloggers>
Como puedes ver, el crear Datasets en Scala y Java requiere un poco de previsión, como puedes ver
en todas las individuales nombres de columnas y tipos para las filas q estas leyendo. No como los
DataFrames, donde opcionalmente dejas a Spark que infiera el esquema, los Dataset requieren
que definas los tipos de datos antes de tiempo y que tu case class o JavaBean class encaje en tu
esquema.

Los nombres de los campos en la definición de clase de Scala o Java deben encajar en orden en la
fuente de datos. Los nombres de columnas de cada fila en los datos están automáticamente
mapeadas a los nombres correspondientes en la class y los tipos son automaticamente presetvados

Puedes usar una Scala class o JavaBean class existente en los nombres de campos para encajar con
tu input data. Trabajar con la API Dataset es tan facil, conciso y declarativo como trabajar con
DataFrames. Para la mayoría de transformaciones con Dataset's puedes usar los mismos operadores
relacionales que has aprendido en los capítulos previos. Vamos a examinar algunos aspectos con una
muestra de datos.

Ejemplo Case Classes: Un producto tiene un combre, categoría y descripción opcional. Una categoría
tiene un nombre y puede tener una categoría padre. Todos los atributos son read-only

class Category(val name: String), class Product(val name: String)

Working with Datasets


Una forma sencilla y dinámica de crear un conjunto de datos de muestra es utilizando una instancia
de SparkSession. En este escenario, con fines ilustrativos, creamos dinámicamente un objeto Scala
con tres campos: uid (ID único para un usuario), uname (cadena de nombre de usuario generada
aleatoriamente) y usage (minutos de uso del servidor o servicio).

Creating Sample Data


Primero vamos a generar algunas muestras de datos:
En Java la idea es similar, pero tenemos que usar Encoders explicitos (en Scala, Spark lo maneja
implicitamente)

Ahora, que renemos nuestro Dataset generado, vamos a desarrollar algunas de las más comunes
transformaciones que hemos hecho en capítulos anteriores

Transforming Sample Data


Recordemos que los conjuntos de datos son colecciones fuertemente tipadas de objetos específicos
del dominio. Estos objetos pueden transformarse en paralelo utilizando operaciones funcionales o
relacionales.

Algunos ejemplos de estas transformaciones son map(), reduce(), filter(), select() y aggregate().
Como ejemplos de funciones de orden superior, estos métodos pueden tomar lambdas, cierres o
funciones como argumentos y devolver los resultados. Como tales, se prestan bien a la
programación funcional.

Scala es un lenguaje de programación funcional, y más recientemente las lambdas, los argumentos
funcionales y los cierres han sido añadidos a Java también. Probemos un par de funciones de orden
superior en Spark y utilicemos construcciones de programación funcional con el ata de ejemplo que
creamos antes.

Higher-order functions and functional programming


Para un ejemplo sencillo, utilicemos filter() para devolver todos los usuarios de nuestro conjunto de
datos dsUsage cuyo uso supere los 900 minutos. Una forma de hacerlo es utilizar una expresión
funcional como argumento del método filter():
Otra forma de definir una función y proveer esa función como un argumento a filter():

En el primer caso utilizamos una expresión lambda, {d.usage > 900}, como argumento del método
filter(), mientras que en el segundo caso definimos una función Scala, def filterWithUsage(u: Usage)
= u.usage > 900. En ambos casos, el método filter() itera sobre cada fila del objeto Usage en el
Dataset distribuido y aplica la expresión o ejecuta la función, devolviendo un nuevo Dataset de tipo
Usage para las filas donde el valor de la expresión o función es verdadero. (Ver la documentación de
Scala para los detalles de la firma del método).

En Java, el argumento de filter() es de tipo FilterFunction<T>. Este puede ser definido bien en línea
de forma anónima o con una función con nombre. Para este ejemplo, definiremos nuestra función
con nombre y la asignaremos a la variable f. Al aplicar esta función en filter() devolverá un nuevo
Dataset con todas las filas para las que nuestra condición de filtro es verdadera.

Not all lambdas or functional arguments must evaluate to Boolean values; they can return computed
values too. Consider this example using the higher-order function map(), where our aim is to find out
the usage cost for each user whose usage value is over a certain threshold so we can offer those
users a special price per minute.
Para utilizar map() en Java, hay que definir una MapFunction<T>. Esta puede ser una clase anónima
o una clase definida que extienda MapFunction<T>. Para este ejemplo la utilizamos en línea, es
decir, en la propia llamada al método:

Aunque hemos calculado valores para el coste de uso, no sabemos a qué usuarios están asociados
los valores calculados. ¿Cómo podemos obtener esta información?

Los pasos son sencillos:

1. Crear una clase Scala case o JavaBean, UsageCost, con un campo adicional columna llamada coste.

2. Definir una función para calcular el coste y utilizarla en el método map().

Ahora tenemos un conjunto de datos transformado con una nueva columna, coste, calculada por la
función de nuestra transformación map(), junto con todas las demás columnas.

There are a few things to observe about using higher-order functions and Datasets:

• We are using typed JVM objects as arguments to functions.


• We are using dot notation (from object-oriented programming) to access individual fields
within the typed JVM object, making it easier to read.
• Some of our functions and lambda signatures can be type-safe, ensuring compiletime error
detection and instructing Spark what data types to work on, what operations to perform,
etc.
• Our code is readable, expressive, and concise, using Java or Scala language features in
lambda expressions.
• Spark provides the equivalent of map() and filter() without higher-order functional
constructs in both Java and Scala, so you are not forced to use functional programming with
Datasets or DataFrames. Instead, you can simply use conditional DSL operators or SQL
expressions: for example, dsUsage.filter("usage> 900") or dsUsage($"usage" > 900). (For
more on this, see “Costs of Using Datasets” on page 170.)
• For Datasets we use encoders, a mechanism to efficiently convert data between JVM and
Spark’s internal binary format for its data types (more on that in “Dataset Encoders” on page
168).

Las funciones de orden superior y la programación funcional no son exclusivas de los conjuntos de
datos de Spark; también puedes utilizarlas con los DataFrames.

Recordemos que un DataFrame es un Dataset[Row], donde Row es un objeto JVM genérico no


tipificado que puede contener diferentes tipos de campos. La firma del método toma expresiones o
funciones que operan sobre Row, lo que significa que el tipo de datos de cada Row puede ser un
valor de entrada para la expresión o función.

Converting DataFrames to Datasets


Para una fuerte comprobación de tipo de las consultas y construcciones, puede convertir los
DataFrames en conjuntos de datos. Para convertir un DataFrame df en un Dataset de tipo
SomeCaseClass, simplemente utilice la notación df.as[SomeCaseClass]. Ya hemos visto un ejemplo
de esto:

spark.read.format("json") devuelve un DataFrame<Row>, que en Scala es un alias de tipo para


Dataset[Row]. El uso de .as[Bloggers] instruye a Spark para que utilice codificadores, discutidos más
adelante en este capítulo, para serializar/deserializar objetos de la memoria interna de Spark rep de
memoria interna de Spark a objetos Bloggers de la JVM.

Memory Management for Datasets and DataFrames


Spark es un motor intensivo de big data distribuido en memoria, por lo que su uso eficiente de la
memoria es crucial para su velocidad de ejecución. A lo largo de su historia, el uso de la memoria de
Spark ha evolucionado significativamente:

• Spark 1.0 utilizaba objetos Java basados en RDD para el almacenamiento en memoria, la
serialización y la deserialización, lo que resultaba caro en términos de recursos y lento.
Además, el almacenamiento se asignaba en el heap de Java, por lo que se estaba a merced
de la recolección de basura (GC) de la JVM para grandes conjuntos de datos.
• Spark 1.x introdujo el proyecto Tungsten. Una de sus características más destacadas era un
nuevo formato interno basado en filas para disponer de Datasets y DataFrames en la
memoria fuera de la pila, utilizando offsets y de memoria fuera de la pila, utilizando
desplazamientos y punteros. Spark utiliza un mecanismo eficiente llamado codificador para
serializar y deserializar entre la JVM y su formato interno Tungsten. La asignación de
memoria fuera de la pila significa que Spark se ve menos afectado por la GC.
• Spark 2.x introdujo el motor Tungsten de segunda generación, con generación de código en
etapas completas y diseño de memoria basado en columnas vectoriales. Basado en ideas y
técnicas de los compiladores modernos, esta nueva versión también aprovecha de las
modernas arquitecturas de CPU y caché para un rápido acceso paralelo a los datos con la
acceso paralelo a los datos con el enfoque "instrucción única, datos múltiples" (SIMD).

Dataset Encoders
Los codificadores convierten los datos en la memoria fuera de la pila desde el formato interno
Tungsten de Spark a objetos Java de la JVM. En otras palabras, serializan y deserializan los objetos
Dataset desde formato interno de Spark a objetos JVM, incluyendo tipos de datos primitivos. Por
ejemplo un Encoder[T] convertirá el formato interno de Spark en Dataset[T].

Spark tiene soporte incorporado para generar automáticamente codificadores para tipos primitivos
(por ejemplo, string, integer, long), clases de caso Scala y JavaBeans. En comparación con Java y
Kryo, los codificadores de Spark son mucho más rápidos.

En nuestro anterior ejemplo de Java, creamos explícitamente un codificador

Encoder<UsageCost> usageCostEncoder = Encoders.bean(UsageCost.class);

Sin embargo, para Scala, Spark genera automáticamente el bytecode para estos eficientes
convertidores eficientes. Echemos un vistazo al formato interno de Spark basado en filas de
Tungsteno.

Spark’s Internal Format Versus Java Object Format


Los objetos Java tienen una gran sobrecarga: información de cabecera, hashcode, información
Unicode, etc. Incluso una simple cadena de Java como "abcd" requiere 48 bytes de almacenamiento,
en lugar de los 4 bytes que cabría esperar.

Imagine la sobrecarga para crear, por ejemplo, un objeto MyClass(Int, String, String).

En lugar de crear objetos basados en la JVM para Datasets o DataFrames, Spark asigna memoria
Java fuera de la pila para disponer sus datos y emplea codificadores para convertir los datos de la
representación en memoria a un objeto JVM. Por ejemplo, la Figura 6-1 muestra cómo el objeto
JVM MyClass(Int, String, String) sería almacenado internamente.
Cuando los datos se almacenan de esta manera contigua y son accesibles a través de punteros arith
metic y offets, los codificadores pueden serializar o deserializar rápidamente esos datos. ¿Qué
significa esto?

Serialization and Deserialization (SerDe)


Un concepto que no es nuevo en la computación distribuida, donde los datos viajan frecuentemente
a través de la red entre los nodos de un cluster, la serialización y deserialización es el proceso por el
cual un objeto tipificado es codificado (serializado) en una presentación o formato binario por el
emisor y decodificado (deserializado) desde el formato binario a su respectivo objeto tipificado
por el receptor.

Por ejemplo, si el objeto JVM MyClass de la Figura 6-1 tuviera que ser compartido entre nodos en un
cluster de Spark, el emisor lo serializaría en una matriz de bytes, y el receptor lo deserializaría en un
objeto JVM de tipo MyClass.

La JVM tiene su propio serializador y deserializador Java incorporado, pero es ineficiente porque
(como vimos en la sección anterior) los objetos Java creados por la JVM en la memoria heap están
hinchados. Por lo tanto, el proceso es lento.

Aquí es donde los codificadores de conjuntos de datos vienen al rescate, por algunas razones:

• El formato binario interno de Tungsteno de Spark (ver Figuras 6-1 y 6-2) almacena los
objetos fuera de la memoria del heap de Java, y es compacto, por lo que esos objetos
ocupan menos espacio.
• Los codificadores pueden serializar rápidamente recorriendo la memoria utilizando una
simple aritmética de punteros con direcciones de memoria y desplazamientos (Figura 6-2).
• En el extremo receptor, los codificadores pueden deserializar rápidamente la representación
binaria en la representación interna de Spark. Los codificadores no se ven obstaculizados por
las pausas de recolección de basura de la JVM.

Costs of Using Datasets


En “DataFrames Versus Datasets”, en la página 74 del capítulo 3, describimos algunas de las ventajas
de utilizar Datasets, pero estas ventajas tienen un coste. Como se indicó en la sección anterior,
cuando los Datasets se pasan a funciones de orden superior como fil(), map(), o flatMap() que
toman lambdas y argumentos funcionales, hay un costo asociado con la deserialización del formato
interno de Spark en el objeto JVM.
En comparación con otros serializadores utilizados antes de la introducción de los codificadores en
Spark, este coste es menor y tolerable. Sin embargo, sobre conjuntos de datos más grandes y
muchas consultas, este coste se acumula y puede afectar al rendimiento.

Strategies to Mitigate Costs


1. Una estrategia para mitigar la excesiva serialización y deserialización es utilizar expresiones DSL
en las consultas y evitar el uso excesivo de lambdas como funciones anónimas como argumentos
de funciones de orden superior. Dado que las lambdas son anónimas y opacas para el optimizador
de Catalyst hasta el momento de la ejecución, cuando las utilizas no puede discernir eficientemente
lo que estás haciendo (no le está diciendo a Spark lo que tiene que hacer) y por tanto no puede
optimizar tus consultas (ver "El optimizador de Catalyst" en la página 77 del Capítulo 3).

2. La segunda estrategia es encadenar las consultas de tal manera que se minimice la serialización y
deserialización. Encadenar consultas es una práctica común en Spark.

Ilustrémoslo con un ejemplo sencillo. Supongamos que tenemos un Dataset de tipo Persona, donde
Persona se define como una clase de caso Scala:

Queremos emitir un conjunto de consultas a este Dataset, utilizando la programación funcional.

Examinemos un caso en el que componemos una consulta de forma ineficiente, de tal manera que
que incurrimos involuntariamente en el coste de la serialización y deserialización repetidas:

Como se puede observar en la Figura 6-3, cada vez que pasamos de lambda a DSL (filter($"salario" >
8000)) incurrimos en el coste de serializar y deserializar el objeto Persona objeto JVM
En cambio, la siguiente consulta sólo utiliza DSL y ninguna lambda. Como resultado, es mucho más
eficiente: no se requiere serialización/deserialización para toda la consulta compuesta y
encadenada:

Resumen
En este capítulo, hemos explicado cómo trabajar con Datasets en Java y Scala. Exploramos cómo
Spark gestiona la memoria para acomodar las construcciones de Dataset como parte de parte de su
API unificada y de alto nivel, y hemos considerado algunos de los costes utilizar Datasets y cómo
mitigar esos costes. También mostramos cómo utilizar las construcciones de programación funcional
de Java y las construcciones de programación funcional de Scala en Spark.

Por último, echamos un vistazo a cómo los codificadores serializan y deserializan desde formato
binario Tungsten interno de Spark a objetos JVM.

En el próximo capítulo, veremos cómo optimizar Spark examinando estrategias eficientes de E/S,
optimizando y ajustando las configuraciones de Spark, y qué atributos y señales que hay que buscar
mientras se depuran las aplicaciones de Spark

Capítulo 7 - Optimizing and Tuning Spark


Applications
En el capítulo anterior, explicamos cómo trabajar con Datasets en Java y Scala. Exploramos cómo
Spark gestiona la memoria para acomodar las construcciones de Dataset como parte de su API
unificada y de alto nivel, y consideramos los costes asociados a utilizar Datasets y cómo mitigar esos
costes.
Además de mitigar los costes, también queremos considerar cómo optimizar y afinar Spark. En este
capítulo, hablaremos de un conjunto de configuraciones de Spark que permiten optimizarlo,
veremos la familia de estrategias de unión de Spark, e inspeccionaremos la interfaz de usuario de
Spark, buscando pistas de mal comportamiento.

Optimización y ajuste de Spark para la eficiencia


Aunque Spark tiene muchas configuraciones para ajustar (tunning), este libro sólo cubrirá un puñado
de las configuraciones más importantes y comúnmente ajustadas. Para una lista completa agrupada
por temas funcionales, puede consultar la documentación.

Viewing and Setting Apache Spark Configurations


Scaling Spark for Large Workloads
Las grandes cargas de trabajo de Spark suelen ser trabajos por lotes: algunos se ejecutan cada
noche, mientras que otros se programan a intervalos regulares durante el día. En cualquier caso,
estos trabajos pueden procesar decenas de terabytes de datos o más.

Para evitar fallos en los trabajos debido a la falta de recursos o la degradación gradual del
rendimiento, hay un puñado de configuraciones de Spark que puede activar o modificar. Estas
configuraciones afectan a tres componentes de Spark: el Spark, el ejecutor y el servicio de barajado
que se ejecuta en el ejecutor (the Spark driver, the executor, and the shuffle service running on the
executor).

La responsabilidad del controlador de Spark es coordinar con el administrador del clúster para lanzar
ejecutores en un clúster y programar las tareas de Spark en ellos. Con grandes cargas de trabajo
puede tener cientos de tareas. En esta sección se explican algunas configuraciones que puedes
ajustar o habilitar para optimizar la utilización de recursos, paralelizar tareas y evitar cuellos de
botella para un gran número de tareas.

Algunas de las ideas de optimización y conocimientos han sido extraídas de empresas de big data
como Facebook que utilizan Spark a escala de terabytes, como elos dijeron a la comunidad Spark en
la Cumbre Spark + AI.1 (e Spark + AI Summit.)

Static versus dynamic resource allocation


Cuando se especifican recursos de computación como argumentos de línea de comandos a spark-
submit como hemos hecho antes, se limita el límite. Esto significa que si más tarde se necesitan más
recursos cuando las tareas se pongan en cola en el controlador debido a una carga de trabajo mayor
de la prevista, Spark no puede acomodar o asignar recursos adicionales.

Si en cambio se utiliza la configuración de asignación dinámica de recursos de Spark, el controlador


de Spark puede solicitar más o menos recursos de computación a medida que la demanda de
grandes cargas de trabajo fluye y disminuye. En los escenarios en los que sus cargas de trabajo son
dinámicas, es decir, varían en su demanda de capacidad de cálculo, el uso de la asignación dinámica
ayuda a acomodar los picos

Un caso de uso en el que esto puede ser útil es el streaming, donde el volumen de flujo de datos
puede ser irregular. Otro es el análisis de datos bajo demanda, en el que puede haber un alto
volumen de consultas SQL durante los picos de trabajo. Activar la asignación dinámica de recursos
permite a Spark lograr una mejor utilización de los recursos, liberando ejecutores cuando no se
utilizan y adquiriendo otros nuevos cuando se necesitan.
Además de cuando se trabaja con cargas de trabajo grandes o variables, la asignación dinámica es
también útil en un entorno multi-inquilino, donde Spark puede desplegarse junto a otras
aplicaciones o servicios en YARN Mesos o Kubernetes. Sin embargo, tenga en cuenta que las
demandas cambiantes de recursos de Spark de recursos de Spark puede afectar a otras aplicaciones
que recursos al mismo tiempo

Para habilitar y configurar la asignación dinámica, puede utilizar ajustes como los siguientes.

Tenga en cuenta que los números aquí son arbitrarios; los ajustes apropiados dependerán de la
naturaleza de su carga de trabajo y deben ajustarse en consecuencia. Algunas de estas
configuraciones no pueden establecerse dentro de un REPL de Spark, por lo que tendrás que
establecerlas programáticamente

spark.dynamicAllocation.enabled true // El controlador de Spark solicitará al administrador del


clúster que cree dos ejecutores para empezar, como mínimo

spark.dynamicAllocation.minExecutors 2

spark.dynamicAllocation.schedulerBacklogTimeout 1m // A medida que la cola de tareas aumenta,


se solicitarán nuevos ejecutores cada vez que el tiempo de espera de la cola de tareas se pase. En
este caso cuando haya tareas pendientes que no han sido scheduled for over 1 minute, el driver
pedirá que se lanze un nuevo ejecutor para schedule backlogged tasks, hasta un macimo de 20

spark.dynamicAllocation.maxExecutors 20

spark.dynamicAllocation.executorIdleTimeout 2min // Por contraste, si un ejecutro termina una


tarea y es inutil por 2 minutos, el driver de Spark lo finalizará

Configuring Spark executors’ memory and the shuffle service


No basta con activar la asignación dinámica de recursos. También hay que entender de la memoria
del ejecutor para que los ejecutores no se vean privados de memoria o no se queden sin memoria o
se vean afectados por la recolección de basura de la JVM.

La cantidad de memoria disponible para cada ejecutor está controlada por spark.executor.memory.
Ésta se divide en tres secciones, como se muestra en la Figura 7-2: memoria de ejecución, memoria
de almacenamiento y memoria reservada.
La división por defecto es del 60% para la memoria de ejecución y el 40% para el almacenamiento,
después de permitir 300 MB para la memoria reservada, con el fin de evitar errores OOM. La
documentación de Spark aconseja que esto funcionará para la mayoría de los casos, pero puede
ajustar qué fracción de spark.executor.memory que quieres que utilice cualquiera de las dos
secciones como línea de base. Cuando el almacenamiento

Cuando la memoria de almacenamiento no está siendo utilizada, Spark puede adquirirla para usarla
en la memoria de ejecución para propósitos de ejecución, y viceversa.

La memoria de ejecución se utiliza para los shuffles , uniones, ordenaciones y agregaciones de Spark.
Dado que diferentes consultas pueden requerir diferentes cantidades de memoria, la fracción
(spark.memory.fraction es 0.6 por defecto) de la memoria disponible para dedicar a esto puede ser
difícil de sintonizar, pero es fácil de ajustar. Por el contrario, la memoria de almacenamiento se
utiliza principalmente para almacenar en caché estructuras de datos de usuario y particiones
derivadas de DataFrames.

Durante las operaciones de mapeo y barajado (map and shuffle) , Spark escribe y lee de los archivos
de barajado del disco local, por lo que hay una gran actividad de E/S. Esto puede dar lugar a un
cuello de botella, porque las configuraciones por defecto no son óptimas para trabajos de Spark a
gran escala. Saber qué configuraciones ajustar puede mitigar este riesgo durante esta fase de un
trabajo de Spark.

En la Tabla 7-1, capturamos algunas configuraciones recomendadas para ajustar de manera que los
procesos map, spill, y merge durante estas operaciones no se vean entorpecidos por una ineficiente
E/S y para permitir que estas operaciones empleen la memoria intermedia antes de escribir las
particiones finales aleatorias en el disco. El ajuste del servicio de barajado que se ejecuta en cada
ejecutor puede también puede ayudar a aumentar el rendimiento general para grandes cargas de
trabajo de Spark.
Maximizing Spark parallelism
Gran parte de la eficiencia de Spark se debe a su capacidad para ejecutar múltiples tareas en
paralelo a escala.

Para entender cómo se puede maximizar el paralelismo -es decir, leer y procesar tantos datos en
paralelo como sea posible, tienes que ver cómo Spark lee los datos en la memoria

desde el almacenamiento y qué significan las particiones para Spark.

En el lenguaje de la gestión de datos, una partición es una forma de organizar los datos en un
subconjunto configurable y legible de trozos o bloques de datos contiguos en el disco. Estos
subconjuntos de datos pueden ser leídos o procesados independientemente y en paralelo, si es
necesario, por más más de un hilo en un proceso. Esta independencia es importante porque permite
el paralelismo masivo del procesamiento de datos.

Spark es vergonzosamente eficiente en el procesamiento de sus tareas en paralelo. Como aprendiste


en el Capítulo 2, para cargas de trabajo a gran escala un trabajo de Spark tendrá muchas etapas, y
dentro de cada etapa habrá muchas tareas. Spark programará, en el mejor de los casos, un hilo por
tarea por núcleo, y cada tarea procesará una partición distinta. Para optimizar la utilización de los
recursos y maximizar el paralelismo, lo ideal es que haya al menos tantas particiones como núcleos
haya en el ejecutor, como se muestra en la Figura 7-3. Si hay más particiones que núcleos en cada
ejecutor, todos los núcleos se mantienen ocupados. Puedes pensar que las particiones son unidades
atómicas de paralelismo: un único hilo corriendo en un único core pueden trabajar sobre una unica
particion
Almacenamiento en caché y persistencia de datos
¿Cuál es la diferencia entre el almacenamiento en caché y la persistencia? En Spark son sinónimos.
Dos llamadas a la API, cache() y persist(), ofrecen estas capacidades. Esta última proporciona

La última proporciona más control sobre cómo y dónde se almacenan los datos, en memoria y en
disco, serializados y no serializados. Ambos contribuyen a mejorar el rendimiento del acceso
frecuente a los DataFrames o a las tablas.

Más info en: (24) Cache vs Persist | Spark Tutorial | Deep Dive - YouTube

DataFrame.cache()
cache() almacenará tantas particiones leídas en memoria en los ejecutores de Spark como la
memoria lo permita (ver Figura 7-2). Mientras que un DataFrame puede ser cacheado
fraccionadamente, las particiones no pueden ser cacheadas fraccionadamente (por ejemplo, si
tienes 8 particiones pero sólo 4,5 particiones pueden caber en la memoria, sólo 4 se almacenarán en
la caché). Sin embargo, si no se almacenan en la caché todas las particiones, cuando quiera acceder
a los datos de nuevo, las particiones que no se han almacenado en la caché tendrán que volver a
calcularse, lo que ralentizará el trabajo de Spark.

Usando cache() el nivel de almacenaje por defecto es MEMORY_ONLY mientras que con persist()
puede haber mas opciones

Cache() = Persist(StorageLevel.MEMORY_ONLY)

Veamos un ejemplo de cómo el almacenamiento en caché de un DataFrame grande mejora el


rendimiento al acceder a un DataFrame:
Figura: Creando un dataframe a partir de un id y una columna calculada usando toDF

La primera count() materializa la caché, mientras que la segunda accede a la caché, lo que resulta en
un tiempo de acceso cerca de 12 veces más rápido para este conjunto de datos.

Cuando se utiliza cache() o persist(), el DataFrame no está completamente cacheado hasta que se
invoca una acción que recorre cada registro (por ejemplo, count()). Si se utiliza una acción como
take(1), sólo se almacenará en la caché porque Catalyst se da cuenta de que no es necesario calcular
todas las particiones sólo para recuperar un registro.

Observando cómo se almacena un DataFrame a través de un ejecutor en un host local, como se


muestra en la Figura 7-4, podemos ver que todos ellos caben en la memoria (recordemos que a bajo
nivel los DataFrame están respaldados por RDDs).

DataFrame.persist()
persist(StorageLevel.LEVEL) es matizado, proporcionando el control sobre cómo sus datos son datos
a través de StorageLevel. La Tabla 7-2 resume los diferentes niveles de almacenamiento. Los datos
en disco siempre se serializan utilizando la serialización Java o Kryo

StorageLevel:

MEMORY_ONLY Los datos se almacenan directamente como objetos y se guardan sólo en la


memoria. (objetos Java en la JVM)

MEMORY_ONLY_SER Los datos se serializan como representación de matriz de bytes compacta y se


almacenan sólo en la memoria. Para utilizarlos tienen que ser deserializados con un coste.

MEMORY_AND_DISK Los datos se almacenan directamente como objetos en memoria


(deserializados) , pero si no hay suficiente memoria el resto se serializan y almacenan en el disco.

DISK_ONLY Los datos se serializan y se almacenan en el disco.

OFF_HEAP Los datos se almacenan fuera de la memoria. La memoria fuera de la pila se utiliza en
Spark para el almacenamiento y la ejecución de consultas;

véase "Configuración de la memoria de los ejecutores de Spark y el servicio de barajado" en la


página 178.

MEMORY_AND_DISK_SER Como MEMORY_ONLY_SER (los datos se serializan cuando se almacenan


en la memoria) y como MEMORY_AND_DISK (si no hay suficiente memoria el resto se serializan y
almacenan en el disco).

Cada StorageLevel (excepto OFF_HEAP) tiene un equivalente LEVEL_NAME_2, lo que significa


replicar dos veces en dos ejecutores diferentes de Spark: MEMORY_ONLY_2,
MEMORY_AND_DISK_SER_2, etc. Aunque esta opción es cara, permite la localización de datos en
dos lugares, proporcionando tolerancia a fallos y dando a Spark la opción de programar una tarea
local a una copia de los datos.When to Cache and Persist

Veamos el mismo ejemplo que en la sección anterior, pero utilizando el método persist():

Using Cache and Persist


Los casos de uso más comunes para el almacenamiento en caché son los escenarios en los que se
desea acceder a un gran conjunto de datos repetidamente para realizar consultas o
transformaciones. Algunos ejemplos son:

• DataFrames comúnmente utilizados durante el entrenamiento iterativo de aprendizaje


automático
• DataFrames a los que se accede habitualmente para realizar transformaciones frecuentes
durante el ETL o la construcción de pipelines de datos

No todos los casos de uso exigen la necesidad de almacenar en caché. Algunos escenarios que no
pueden justificar el almacenamiento en caché de sus DataFrames son

• DataFrames que son demasiado grandes para caber en la memoria


• Una transformación barata en un DataFrame que no requiera un uso frecuente,
independientemente del tamaño

Como regla general, debe utilizar la memoria caché con criterio, ya que puede incurrir en costes de
recursos al serializar y deserializar dependiendo del StorageLevel utilizado.

A continuación, nos centraremos en un par de operaciones comunes de unión de Spark que


desencadenan un costoso movimiento de datos, exigiendo recursos informáticos y de red del
clúster, y cómo podemos mitigar este movimiento organizando los datos.

A Family of Spark Joins


Las operaciones de unión son un tipo de transformación común en el análisis de big data en el que
dos conjuntos de datos, en forma de tablas o DataFrames, se fusionan sobre una clave común. Al
igual que las bases de datos relacionales, las API de Spark DataFrame y Dataset y Spark SQL ofrecen
una serie de transformaciones de unión: inner joins, outer joins, left joins, right joins, etc. Todas
estas operaciones desencadenan una gran cantidad de movimiento de datos entre los ejecutores de
Spark.

En el centro de estas transformaciones se encuentra la forma en que Spark calcula qué datos
producir, qué claves y datos asociados escribir en el disco, y cómo transferir esas claves y datos a los
nodos como parte de operaciones como groupBy(), join(), agg(), sortBy() y reduceByKey(). Este
movimiento se denomina comúnmente "shuffle".

Spark tiene cinco estrategias de unión distintas por las que intercambia, mueve, ordena, agrupa y
fusiona (exchanges, moves, sorts, groups, and merges) datos entre ejecutores: el broadcast hash
join (BHJ), el shuffle hash join (SHJ) shuffle sort merge join (SMJ), broadcast nested loop join
(BNLJ), y shuffle-andreplicated nested loop join (también conocido como Cartesian product join).
Aquí nos centraremos sólo en dos de (BHJ y SMJ), porque son los más comunes.

Broadcast Hash Join


También conocido como map-side-only join, el broadcast hash join se emplea cuando dos conjuntos
de datos, uno pequeño (que cabe en la memoria del controlador y del ejecutor) y otro lo
suficientemente grande como para no tener que moverlo, deben unirse en determinadas
condiciones o columnas. Utilizando una variable de difusión de Spark, el conjunto de datos más
pequeño es difundido por el controlador a todos los ejecutores de Spark, como se muestra en la
Figura 7-6, y posteriormente de datos más grande en cada ejecutor. Esta estrategia evita el gran
intercambio
Por defecto, Spark utilizará un broadcast join si el conjunto de datos más pequeño es inferior a 10
MB. Esta configuración se establece en spark.sql.autoBroadcastJoinThreshold; puedes aumentar el
tamaño dependiendo de la cantidad de memoria que tenga en cada ejecutor y en el controlador. Si
estás seguro de que tienes suficiente memoria puedes puede utilizar un broadcast join con
DataFrames mayores de 10 MB (incluso hasta 100 MB).

Un caso de uso común es cuando se tiene un conjunto común de claves entre dos DataFrames de
datos, uno de los cuales contiene menos información que el otro, y se necesita una vista combinada
de ambos. Por ejemplo, considere un caso sencillo en el que tiene un gran conjunto de datos de
jugadores de fútbol de todo el mundo, playersDF, y un conjunto de datos más pequeño de los clubes
de fútbol en los que juegan clubesDF, y desea unirlos con una clave común

En este código estamos forzando a Spark a hacer un broadcast join, pero se recurrirá a este tipo de
unión por defecto si el tamaño del conjunto de datos más pequeño está por debajo del
spark.sql.autoBroadcastJoinThreshold.

La BHJ es la unión más fácil y rápida que ofrece Spark, ya que no implica ningún shuffle del conjunto
de datos; todos los datos están disponibles localmente para el ejecutor después de una emisión.
Usted sólo tienes que asegurarte de que tienes suficiente memoria tanto en el controlador de Spark
como en el lado del ejecutor para mantener el conjunto de datos más pequeño en la memoria.

En cualquier momento después de la operación, puedes ver en el plan físico qué operación de join se
realizó ejecutando joinedDF.explain(mode)

En Spark 3.0, se puede utilizar joinedDF.explain('mode') para mostrar una salida legible y digerible.
Los modos incluyen 'simple', 'extendido', 'codegen', 'coste' y 'formateado'.

When to use a broadcast hash join


Utilice este tipo de unión bajo las siguientes condiciones para obtener el máximo beneficio:

• Cuando cada clave dentro de los conjuntos de datos más pequeños y más grandes se hashee
a la misma partición de Spark
• Cuando un conjunto de datos es mucho más pequeño que el otro (y dentro de la
configuración por defecto de 10 MB, o más si tiene suficiente memoria)
• Cuando sólo se desea realizar un equi-join, para combinar dos conjuntos de datos basados
en las claves coincidentes sin clasificar
• Cuando no le preocupa el uso excesivo del ancho de banda de la red o los errores OOM
porque el conjunto de datos más pequeño se transmitirá a todos los ejecutores de Spark

Especificar un valor de -1 en spark.sql.autoBroadcastJoinThreshold hará que Spark recurra siempre a


una fusión de tipo shuffle, de la que hablaremos en la siguiente sección

Shuffle Sort Merge Join


El algoritmo sort-merge es una forma eficiente de fusionar dos grandes conjuntos de datos sobre
una clave común que sea ordenable, única y que pueda ser asignada o almacenada en la misma
partición,es decir, dos conjuntos de datos con una clave hashable común que acaban estando en la
misma partición. Desde la perspectiva de Spark, esto significa que todas las filas de cada conjunto de
datos con la misma clave se hash en la misma partición en el mismo ejecutor. Evidentemente, esto
significa que los datos tienen que ser colocados o intercambiados entre los ejecutores.

Como su nombre indica, este esquema de unión tiene dos fases: una fase de ordenación seguida de
una fase de fusión. La fase de ordenación ordena cada conjunto de datos por su clave de unión
deseada; la fase de fusión itera sobre cada clave en la fila de cada conjunto de datos y fusiona las
filas si las dos claves coinciden.

Por defecto, el SortMergeJoin está habilitado mediante spark.sql.join.preferSortMergeJoin. Aquí hay


un fragmento de código de un cuaderno de aplicaciones autónomas disponible para este capítulo en
el repositorio GitHub del libro. La idea principal es tomar dos grandes DataFrames, con un millón de
registros, y unirlos en dos claves comunes, uid ==users_id.

Estos datos son sintéticos pero ilustran el punto:


Examinando nuestro plan de ejecución final, observamos que Spark empleó un SortMergeJoin como
era de esperar, para unir los dos DataFrames. La operación de intercambio es el barajado de los
resultados de la operación map en cada ejecutor:

Además, la interfaz de usuario de Spark (de la que hablaremos en la siguiente sección) muestra tres
etapas para todo el trabajo: las operaciones de Intercambio y Ordenación ocurren en la etapa final
seguido de la fusión de los resultados, como se muestra en las Figuras 7-7 y 7-8. El intercambio es
costoso y requiere que las particiones sean barajadas a través de la red entre ejecutores.
Optimizing the shuffle sort merge join
When to use a shuffle sort merge join
Inspecting the Spark UI
Journey Through the Spark UI Tabs

Poreguntas del examen


Gestores de recursos de un cluster
Standalone

You might also like