Data sources and formats (CSV, JSON, Parquet, ORC, Avro), schema inference vs explicit schemas, partitioned reads/writes, and save modes.
⏱ 16 min readTopics chapter readerLevel · Structured APIs
01 · Orientation
Start here
⏱ 6 min · Topic 1 of 14
Every Spark job reads data first and writes data last. How you read and write decides the speed, the cost and the safety of the job.
You know the storage ideas from "Storage Layers & File Formats": row and columnar files, Parquet, compression and folders named by value. This chapter shows the Spark code and what Spark does with each choice.
A DataFrameReader loads data into a DataFrame. You get one from spark.read.
A DataFrameWriter saves a DataFrame. You get one from df.write.
The data is the shop orders 101 to 106. Some files in this chapter are made up, and the text says so. New made-up orders have ids from 210.
The chapter has two halves.
Reading: the chain of calls, formats, read tasks, schemas, options, bad rows, databases and changing schemas.
Writing: partition folders, the number of files, and save modes.
02 · The API
The read and write chain
⏱ 6 min · Topic 2 of 14
Every read is one chain of calls. You pick a format, add options, give a schema, and load a path.
An option is a named setting for one read or write, such as header. A path is a file or a folder. A schema is the list of columns and their types.
Here is the chain. It reads the orders file and writes it back as Parquet.
Read a CSV file, write Parquetworked example
SQL
Input data
orders.csv (the first line holds the column names)6 rows
An empty marker file. It means the write finished.
part-00000-....snappy.parquet
All 6 orders, compressed. One task wrote it.
The empty amount of order 105 is read as null. Spark writes a folder, not a single file.
The write is the last line. It starts the job.
The schema line gives each column a type. A later section explains it.
The mode line says what to do if the folder already exists. The save modes section explains that.
Each format also has a shortcut. spark.read.parquet(path) means format("parquet").load(path). The writer has the same shortcuts: csv, json, parquet and orc.
03 · Formats
Pick a format, skip data
⏱ 8 min · Topic 3 of 14
Spark reads many formats with the same chain. This table lists the ones you meet most.
Format
Read it with
What to know
csv
spark.read.csv(path)
Text lines. No types are stored.
json
spark.read.json(path)
One JSON object per line by default.
parquet
spark.read.parquet(path)
Columnar. Types are stored. The best choice for analytics.
orc
spark.read.orc(path)
Columnar like Parquet. Common with Hive, an older SQL engine.
text
spark.read.text(path)
One string column named value. One row per line.
jdbc
spark.read.format("jdbc")
Rows from a database table. It has its own section.
Parquet and ORC store each column together, with the smallest and largest value of each block of rows. "Storage Layers & File Formats" explains why that helps. Now watch Spark use it.
Read the Parquet folder from the last section. Ask for orders above 103, and only two columns.
Orders 101, 102 and 103 are filtered out. Order 105 stays because 105 is above 103. Its amount is null.
The plan is simplified. Look at ReadSchema and PushedFilters.
ReadSchema lists the two columns Spark reads. The columns customer_id, status and order_date stay on disk. This is column pruning.
PushedFilters is the filter that Spark hands to the Parquet reader. The reader can skip a block when its largest order_id is 103 or less. This is predicate pushdown.
With 6 rows nothing is skipped, but on big files whole blocks are.
Spark still shows its own Filter step and checks the rows it gets. A CSV file has no blocks and no statistics, so Spark must read and parse every line.
Watch out
Spark only pushes filters the reader understands. A filter such as upper(status) = "PAID" is not pushed. Compare the column with a plain value.
04 · Read size
How many read tasks
⏱ 6 min · Topic 4 of 14
A task is one unit of work. One read task reads one piece of the input. "Partitioning & Parallelism" covers tasks in depth.
Spark cuts a file into pieces of at most spark.sql.files.maxPartitionBytes. The default is 128 MB.
Say you have one Parquet file of 1,024 MB. This is a made-up size.
1,024 MB divided by 128 MB is 8. You get 8 tasks when the cluster has 8 cores or fewer. With more cores, Spark cuts smaller pieces so every core gets work.
Each piece goes to one task. getNumPartitions counts the pieces, which Spark calls partitions in memory.
Not every file can be cut. This table uses the same 1,024 MB size.
Input
Can Spark cut it?
Read tasks
One Parquet file
Yes
8
One plain CSV file
Yes
8
One CSV file compressed with gzip
No
1
One JSON file read with multiLine true
No
1
A gzip file cannot be cut, because you cannot start reading in the middle. One task reads all of it while the other cores wait. A multiLine JSON file also needs one task for the whole file.
Spark can also pack many small files into one piece. So the number of tasks is not always the number of files.
05 · Schema
Give Spark a schema
⏱ 6 min · Topic 5 of 14
CSV and JSON files do not store types. Spark must learn them. You can let Spark guess, or you can give a schema.
To guess for CSV, you set inferSchema to true. Spark reads the data once to guess the types, then reads it again to load it. A big file costs two passes, and you see an extra job in the Spark UI.
Take a small made-up file with two columns of the orders, amounts included. Read it both ways.
Spark guessed double for amount. A double is a binary fraction. For money you want decimal(10,2): up to 10 digits, 2 after the point.
The given schema is a DDL (Data Definition Language) string. It lists column names and types.
You can also build a StructType object. The DDL string is shorter, so most code uses it.
For JSON, Spark guesses by default, and it reads the data to do so.
The option samplingRatio, for example 0.1, makes Spark guess from that share of the objects. It is faster, but a type that only appears in the rest is missed.
Watch out
When Spark guesses the types of a CSV file, it reads a code such as 007 as the number 7. The zeros are lost. Give such a column the type STRING.
06 · Options
CSV and JSON options
⏱ 5 min · Topic 6 of 14
These options change how Spark reads text files. You set each one with .option("name", "value").
Option
Default
What it does
header
false
CSV only. The first line holds the column names. Without it, columns are named _c0, _c1 and so on.
sep
comma
CSV only. The character that splits a line into columns.
inferSchema
false
CSV only. Read the data once more to guess the types.
multiLine
false
JSON and CSV. One record may span many lines.
By default, Spark reads JSON as one object per line. This format is called JSON Lines. A file that spreads one object over many lines needs multiLine.
Here is a made-up file in that second style. It holds orders 101 and 102.
Without multiLine, Spark would read each line as its own object. The lines would not parse.
One array of two objects gives two rows.
A multiLine file cannot be cut into pieces. One task reads the whole file, as the read tasks section showed.
07 · Bad rows
Bad rows and read modes
⏱ 7 min · Topic 7 of 14
A bad row is a line that Spark cannot parse with your schema. An example is text in a number column. The read mode says what Spark does with such a line.
The mode is an option named mode. There are three values.
PERMISSIVE is the default. Spark keeps the row and can store the raw line in a column.
DROPMALFORMED drops the bad row without a message.
FAILFAST stops the job at the first bad row.
For PERMISSIVE, you add a string column named _corrupt_record to your schema. Spark puts the raw bad line there. Without it, the row stays but the raw line is lost.
Here is a made-up file. The id of the second line is the text abc.
The bad line is kept. Its order_id is null, and the raw text is in _corrupt_record.
The other columns of a bad row can also be null. Do not rely on them.
Now read the same file with the other two modes.
Mode
Rows returned
What happens to the line abc,paid,30.00
PERMISSIVE
3
Kept, with the raw text in _corrupt_record.
DROPMALFORMED
2 (orders 210 and 212)
Gone. You get no message.
FAILFAST
None
The job stops with an error about a malformed (bad) record.
Watch out
Spark refuses a query that uses only _corrupt_record on a raw CSV or JSON file. Select another column too, or cache the DataFrame first.
08 · Databases
Read from a database
⏱ 9 min · Topic 8 of 14
JDBC is the standard way for Java programs to talk to a database. Spark uses it to read a database table.
By default Spark opens one connection and one task reads the whole table. To read in parallel, you give four options together. They are partitionColumn, lowerBound, upperBound and numPartitions.
Spark cuts the range of ids into steps of equal size. The size of one step is the stride.
Say the shop database holds the orders table, with order ids 101 to 106. Read it in 2 parts.
The stride is (107 - 101) / 2 = 3. Part 0 takes the ids below 104. Part 1 takes the rest.
Each part is one task with its own connection to the database.
The column must be a number, a date or a timestamp. Pick one with evenly spread values.
The bounds do not filter rows. They only set the stride. Change them to 101 and 103 and look at what happens.
Part
WHERE clause
Orders it reads
0
order_id < 102 OR order_id IS NULL
101
1
order_id >= 102
102, 103, 104, 105, 106
All 6 orders still come back. But the parts are uneven: 1 row against 5. The first part and the last part also take every row outside the bounds.
The option fetchsize is the number of rows each task gets from the database in one trip. A JDBC driver is the small library for one kind of database, and some fetch few rows by default. The Oracle JDBC driver fetches 10.
A larger fetchsize can speed up the read.
Watch out
Each part opens its own connection. A large numPartitions can overload the database. Ask the database owner for a safe limit.
09 · Layout
Write partition folders
⏱ 6 min · Topic 9 of 14
partitionBy writes one folder for each value of a column. You know this layout from "Storage Layers & File Formats". Here you write it with Spark.
Write the 6 orders from orders.csv with partitionBy on status. One task holds them all. Then read one status back.
The status column is not stored inside the files. The folder name holds it, and Spark reads it back from there.
So each folder has one file.
PartitionFilters shows partition pruning. Spark reads only the files in the folder status=paid. It never opens the files in the other two folders.
The query returns orders 101, 102, 104 and 106.
Pick a column with few values that you often filter on, such as the date. A column such as order_id makes one folder for each order.
Folders or buckets
partitionBy makes folders, and bucketBy groups rows into a fixed number of buckets by the value of a column. bucketBy works only with saveAsTable, which saves a named table. See "Broadcast Joins, Shared Variables & Bucketing".
10 · File count
How many files you write
⏱ 6 min · Topic 10 of 14
Each task that holds rows writes its own file. With partitionBy, a task writes one file for each value it holds.
So the file count is tasks times values, at most. Take a made-up orders table read in 200 tasks, where every task holds all 3 statuses.
The paid folder has 4 rows, so it needs 2 files. The other two folders fit in one file each. The total is 4 files.
Use a cap to limit file size. Do not use it to fix too many tiny files.
coalesce(n) lowers the number of tasks without a shuffle. repartition(n) shuffles the data into n even tasks. Both change the file count, because tasks write the files.
11 · Write behavior
Save modes and overwrite
⏱ 7 min · Topic 11 of 14
A save mode says what Spark does when the target already exists. You set it with .mode("..."). There are four.
Mode
If the target exists
errorifexists (default)
The write fails with an error.
append
Spark adds the new files next to the old ones.
overwrite
Spark replaces the old data. How much it replaces depends on a setting.
ignore
Spark does nothing. It does not write and gives no error.
The setting for overwrite is spark.sql.sources.partitionOverwriteMode. The default is static. Static overwrite deletes everything under the path, then writes.
The other value, dynamic, replaces only the partition folders that get new rows.
Take a table orders_by_month, partitioned by month. It holds January (orders 101 to 106) and a made-up February (orders 210 and 211). A new February batch arrives with orders 210, 211 and 212.
Merging reads the footer of every file. That costs time when a folder has many files. Merging fails if one column has two different types in two files.
A plain folder of Parquet files has no record of its schema history. "Delta Lake & the Lakehouse" shows how a table format tracks it.
13 · Recap
Check yourself
⏱ 4 min · Topic 13 of 14
Five points to keep.
Read with the chain: format, options, schema, path. Parquet lets Spark skip columns and blocks.
Give a schema. Guessing reads the data twice, and it can pick a wrong type.
PERMISSIVE keeps a bad row. DROPMALFORMED hides it. FAILFAST stops the job.
Files written equal tasks times partition values, at most. Control them with repartition and maxRecordsPerFile.
Overwrite on a partitioned table needs partitionOverwriteMode dynamic, or it deletes the other partition folders.
Answer these without looking back.
A JDBC read has lowerBound 101, upperBound 103 and numPartitions 2. Which part reads orders 102 to 106?
A gzip CSV file is 1,024 MB. How many read tasks does it get, and why?
Why can Spark skip blocks of a Parquet file, but not of a CSV file?
A job writes with partitionBy and 50 tasks. Each task holds 4 values. How many files can it write at most?
14 · Next Chapter
Next Chapter
You can now move data in and out of Spark with the right format, schema, mode and folders.
⏱ 3 min · Topic 14 of 14
Next chapter
Core DataFrame Operations
You can now move data in and out of Spark with the right format, schema, mode and folders.
Next is the everyday DataFrame API: select, filter, withColumn, and the column expressions you use in every job.