Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›PySpark›Writing Output — Parquet, Delta, JDBC

DataFrames and SparkSQL

Writing Output — Parquet, Delta, JDBC

This lesson explores the mechanisms for persisting processed DataFrames into storage formats and relational databases. Mastery of output modes and partitioning is critical for ensuring data integrity and optimizing downstream retrieval speeds. These techniques are essential when transitioning from transient in-memory computation to final analytical or operational production tables.

Understanding Write Modes

Before writing data, you must define how Spark handles existing files at the destination. The SaveMode parameter governs this behavior, acting as a safeguard for your data pipelines. 'Append' adds the current DataFrame to existing data, which is ideal for time-series logs or incremental daily ingestion. 'Overwrite' replaces existing data, useful for full batch refreshes where the state must be reset entirely. 'ErrorIfExists' is the default safety setting that prevents accidental data loss by halting execution if the target path is already populated. Finally, 'Ignore' silently skips the write operation if data exists, preventing redundant processing. Understanding these modes is fundamental because it dictates the idempotent nature of your jobs. When a pipeline fails mid-execution, choosing the correct mode ensures that re-running the job does not result in duplicate records or corrupted datasets, which is vital for production stability in distributed environments.

# Writing using specific SaveMode options
df.write.mode("overwrite").parquet("/path/to/output/data")
df.write.mode("append").parquet("/path/to/output/data")

Parquet and Columnar Storage

Parquet is the standard format for big data storage due to its columnar layout and schema enforcement. Unlike row-based formats, Parquet stores columns together, allowing Spark to perform projection pushdown—reading only the specific columns required by your query. This significantly reduces I/O throughput, saving both time and network bandwidth. Furthermore, Parquet supports predicate pushdown, where metadata stored within the file footer allows Spark to skip entire blocks of data that do not meet your filter criteria before even reading the file into memory. Because Parquet is self-describing, the schema is preserved across writes, ensuring that downstream processes encounter consistent data types. This efficiency makes it the backbone of efficient data lakes. When you write to Parquet, always ensure that your partitioning strategy aligns with how your data will be queried to maximize the performance of these inherent optimization features.

# Writing to Parquet with partition columns for optimization
df.write.mode("overwrite") \
  .partitionBy("event_date") \
  .parquet("/data/analytics/events")

The Delta Lake Advantage

Delta Lake introduces a transaction log to Parquet, enabling ACID compliance for your data lake. By tracking every write operation in a JSON-based log, Delta guarantees that readers never see partially written data if a job fails mid-process. This is a massive improvement over standard Parquet, where a failed write could leave orphaned files and inconsistent states. Delta also supports time travel, allowing you to query previous versions of the data by timestamp or version ID, which is invaluable for debugging or recovering from accidental data corruption. Additionally, Delta provides schema evolution and constraint enforcement, preventing malformed data from polluting your downstream tables. By abstracting the complexities of file management, Delta allows you to perform updates, deletes, and merges on your data, effectively bringing relational database capabilities to your object storage. It is the preferred choice for enterprise-grade data engineering workloads that require reliability and auditability.

# Writing to Delta format to enable ACID transactions
df.write.mode("append") \
  .format("delta") \
  .save("/data/production/users")

Writing to JDBC Databases

When outputting data to a relational database, you are effectively shifting the computation results into a structured system designed for point-in-time lookups and transactional integrity. Unlike file-based sinks, JDBC connections involve establishing a network socket with a foreign database engine. You must provide a valid connection string, table name, and authentication credentials. Spark optimizes this write by batching records before pushing them through the network, which minimizes the overhead of individual insert statements. One crucial consideration is the performance impact on the destination database; dumping massive datasets directly into an OLTP database can lock tables or degrade performance for existing applications. Always consider writing to a staging table first, then performing an atomic rename or merge within the database itself. Additionally, tuning the batch size is vital to balancing the throughput of the write against the transaction log size of the target database system.

# Writing data to an external database via JDBC
df.write.format("jdbc") \
  .option("url", "jdbc:postgresql://host:port/db") \
  .option("dbtable", "target_table") \
  .option("user", "db_user") \
  .option("password", "secure_password") \
  .mode("append") \
  .save()

Partitioning and File Sizing

The physical layout of your output files significantly influences subsequent read performance. If you create too many small files, the driver will spend excessive time managing metadata and listing files, creating a 'small file problem' that slows down downstream analytics. Conversely, creating too few files prevents Spark from effectively parallelizing the work, as you have fewer tasks to distribute across the executor nodes. You can control this by using coalesce() or repartition() before writing your data. Partitioning by high-cardinality columns, such as user IDs, often results in an explosion of small files that can degrade performance, while partitioning by low-cardinality columns, like dates or regions, usually provides the best balance. The goal is to produce files that are large enough to take advantage of sequential disk I/O while keeping the total task count aligned with the number of available cores in your cluster for future processing stages.

# Controlling file sizing before writing
df.coalesce(10) # Merge partitions to create larger, fewer files
  .write.mode("overwrite") \
  .parquet("/data/final_output")

Key points

  • Save modes define the behavior of Spark when writing to a path that already contains data.
  • Parquet utilizes columnar storage to allow for predicate and projection pushdown optimizations.
  • Delta Lake adds an ACID-compliant transaction log to Parquet, preventing corruption during failed writes.
  • Time travel is a feature of Delta Lake that allows developers to access historical snapshots of data.
  • JDBC writes require careful batch sizing to prevent overwhelming the target relational database's transaction logs.
  • Partitioning should be based on columns frequently used in filter predicates to maximize query performance.
  • Excessive small files lead to high metadata overhead and should be managed via repartitioning or coalescing.
  • The target storage format should be chosen based on whether you need ACID guarantees or simple analytical throughput.

Common mistakes

  • Mistake: Using .parquet() on an existing folder without specifying a save mode. Why it's wrong: By default, PySpark errors out if the target path exists. Fix: Explicitly define the save mode using .mode('overwrite') or .mode('append').
  • Mistake: Writing to JDBC without handling partition counts. Why it's wrong: A massive Spark partition count can overload a database with concurrent connections. Fix: Use .repartition() before the .jdbc() write to balance throughput and connection overhead.
  • Mistake: Forgetting to specify the table schema when writing to a new Delta table. Why it's wrong: While Delta can infer schema, relying on implicit inference can lead to unexpected type casting issues in downstream jobs. Fix: Explicitly define the schema or ensure the Dataframe schema is strictly cast before writing.
  • Mistake: Using coalesce() to reduce file sizes before a write operation. Why it's wrong: coalesce() can lead to data skew if the partitions are unevenly sized. Fix: Use repartition() if you need to redistribute data evenly to ensure uniform file sizes in Parquet/Delta.
  • Mistake: Overwriting a Delta table by deleting the underlying files manually. Why it's wrong: This corrupts the Delta log, causing subsequent reads to fail or show inconsistent data. Fix: Always use the Delta Table API or Spark's overwrite mode, which properly updates the transaction log.

Interview questions

How do you write a PySpark DataFrame to a Parquet file, and why is Parquet preferred in big data workflows?

To write a DataFrame to Parquet in PySpark, you use the command 'df.write.parquet('path/to/directory')'. Parquet is highly preferred because it is a columnar storage format, which allows for efficient data compression and enables 'predicate pushdown,' where the engine reads only the necessary columns rather than the entire dataset. This significantly reduces I/O overhead and speeds up analytical queries, making it the industry standard for scalable data lakes.

What is the role of the 'mode' parameter when saving PySpark DataFrames, and what are the common options?

The 'mode' parameter defines how PySpark handles the situation where data already exists at the specified output path. Common options include 'overwrite', which replaces existing data; 'append', which adds new data to the existing directory; 'error' (the default), which throws an exception if data exists; and 'ignore', which silently does nothing. Understanding this is critical to preventing data loss or unintentional duplication during automated pipeline executions in production environments.

How do you write a DataFrame to a Delta table, and what core advantage does Delta provide over standard Parquet?

You write to Delta format by using 'df.write.format('delta').save('path')'. The core advantage of Delta Lake over standard Parquet is the addition of an ACID-compliant transaction log. This log tracks every change to the dataset, enabling features like time travel, schema enforcement, and concurrent reads and writes without corruption. While Parquet is just a file format, Delta provides the reliability and structural integrity required for high-stakes production data lakes.

How can you write data from PySpark to an external database using JDBC, and what performance considerations should you keep in mind?

To write via JDBC, use 'df.write.format('jdbc').option('url', jdbc_url).option('dbtable', table_name).option('user', user).option('password', password).save()'. When writing large datasets to an RDBMS via JDBC, performance can be a bottleneck because it creates a single connection by default. To improve this, you should manage batch sizes using the 'batchsize' option and consider using the 'numPartitions' and 'partitionColumn' options to parallelize the write operation across multiple Spark tasks to the target database.

Compare writing to Parquet files versus writing to Delta tables in PySpark. When would you choose one over the other?

Writing to Parquet is ideal for immutable, archived datasets where storage cost and read-efficiency for massive scans are the only priorities. However, you would choose Delta Lake when your workflow requires frequent updates, deletes, or upserts—operations that are not natively supported by Parquet files. Delta is also superior for production pipelines that need schema evolution, version control, and data consistency during concurrent writes, which standard Parquet fails to provide on its own.

Explain the importance of partitioning when writing PySpark DataFrames to disk. How does it impact both the write process and subsequent reads?

Partitioning is the process of splitting data into subdirectories based on column values using 'df.write.partitionBy('col_name').parquet('path')'. During writing, this creates physical separation, which allows for 'partition pruning' during future reads. For example, if you filter by date, Spark ignores all directories not matching that date. While it speeds up reads significantly, you must be careful: excessive partitioning creates 'small file' problems, which increase metadata overhead on the file system and can degrade overall cluster performance.

All PySpark interview questions →

Check yourself

1. When writing a DataFrame to a Parquet file, why is 'overwrite' mode generally safer for production pipelines than simply deleting the output directory?

  • A.It prevents partial data reads if the job fails mid-write.
  • B.It forces the Parquet file to be stored in the local executor memory.
  • C.It automatically compresses the data using GZIP compression.
  • D.It skips the Spark driver initialization process.
Show answer

A. It prevents partial data reads if the job fails mid-write.
Option 1 is correct because Spark writes to a temporary directory first and performs an atomic rename/commit, ensuring partial data is not visible. Options 2, 3, and 4 are incorrect because they relate to memory management, compression algorithms, and job lifecycle, none of which are the primary benefit of the atomic overwrite process.

2. What is the primary architectural difference when writing to Delta Lake compared to standard Parquet files in PySpark?

  • A.Delta Lake converts the data to a row-based format for faster JDBC integration.
  • B.Delta Lake maintains a transaction log that tracks file modifications.
  • C.Delta Lake requires an external SQL server to manage the metadata.
  • D.Delta Lake forces all data types to be converted to Strings.
Show answer

B. Delta Lake maintains a transaction log that tracks file modifications.
Option 2 is correct because the transaction log provides ACID guarantees. Option 1 is false because Delta remains columnar; Option 3 is false as it is file-based; Option 4 is false as Delta preserves schema and data types.

3. You are writing to a JDBC database using Spark. If the job fails, why might you find 'orphaned' rows in the destination table?

  • A.Because JDBC writes do not support transactional rollbacks by default.
  • B.Because Spark automatically truncates the table upon restart.
  • C.Because the JDBC driver is not configured for multi-threaded access.
  • D.Because Spark overwrites the database metadata at the start of the task.
Show answer

A. Because JDBC writes do not support transactional rollbacks by default.
Option 0 is correct because JDBC writes typically commit per partition or per batch, and if the job crashes, those committed batches are not rolled back automatically. Options 1, 2, and 3 describe incorrect behavior or unrelated configuration issues.

4. Why is it recommended to perform a 'repartition' operation before saving a very large DataFrame to multiple Parquet files?

  • A.To ensure each file contains exactly one row.
  • B.To consolidate data into a single file to speed up file system metadata access.
  • C.To manage the number of output files and control file size for better query performance.
  • D.To force the Spark driver to collect all data before writing to disk.
Show answer

C. To manage the number of output files and control file size for better query performance.
Option 2 is correct because controlling the number of partitions directly dictates the number of files written, preventing the 'small file problem.' Option 0 is inefficient, Option 1 causes bottlenecks, and Option 3 defeats the purpose of distributed processing.

5. When performing an 'upsert' (merge) operation in Delta Lake, what must exist to ensure the operation can successfully identify matches?

  • A.A column that contains only null values to signal a new record.
  • B.A common key or set of keys between the source and target DataFrames.
  • C.A secondary index created on the JDBC source database.
  • D.A hardcoded file path to the target delta table.
Show answer

B. A common key or set of keys between the source and target DataFrames.
Option 1 is correct because the merge command requires a join condition (the 'on' clause) to map rows. Options 0, 2, and 3 are irrelevant to the logic of comparing source and target data sets for updates or inserts.

Take the full PySpark quiz →

← PreviousSparkSQL — Running SQL on DataFramesNext →Joins in Spark — Broadcast, Shuffle

PySpark

26 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app