Skip to content

Commit 97bfa31

Browse files
committed
Update according to feedback
1 parent 0c910f8 commit 97bfa31

1 file changed

Lines changed: 34 additions & 26 deletions

File tree

docs/spark-connect-gotchas.md

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,51 +28,52 @@ For an overview of Spark Connect, see [Spark Connect Overview](spark-connect-ove
2828

2929
## Spark Classic
3030

31-
In traditional Spark, DataFrame transformations (e.g., `filter`, `limit`) are lazy. This means they are not executed immediately but are recorded in a logical plan. The actual computation is triggered only when an action (e.g., `show()`, `collect()`) is invoked.
31+
In traditional Spark, DataFrame transformations (e.g., `filter`, `limit`) are lazy. This means they are not executed immediately but are encoded in a logical plan. The actual computation is triggered only when an action (e.g., `show()`, `collect()`) is triggered.
3232

3333
## Spark Connect
3434

35-
Spark Connect follows a similar lazy evaluation model. Transformations are constructed on the client side and sent as unresolved proto plans to the server. The server then performs the necessary analysis and execution when an action is called.
35+
Spark Connect follows a similar lazy evaluation model. Transformations are constructed on the client side and sent as unresolved plans to the server. The server then performs the necessary analysis and execution when an action is called.
3636

3737
## Comparison
3838

3939
Both Spark Classic and Spark Connect follow the same lazy execution model for query execution.
4040

41-
| Aspect | Spark Classic | Spark Connect |
42-
|:--------------------------------------------------------------------------------------|:----------------|:----------------|
43-
| Transformations: `df.filter(...)`, `df.select(...)`, `df.limit(...)`, etc | Lazy execution | Lazy execution |
44-
| SQL queries: <br/> `spark.sql("select …")` | Lazy execution | Lazy execution |
45-
| Actions: `df.collect()`, `df.show()`, etc | Eager execution | Eager execution |
46-
| SQL commands: <br/> `spark.sql("insert …")`, <br/> `spark.sql("create …")`, <br/> etc | Eager execution | Eager execution |
41+
| Aspect | Spark Classic & Spark Connect |
42+
|:--------------------------------------------------------------------------------------|:------------------------------|
43+
| Transformations: `df.filter(...)`, `df.select(...)`, `df.limit(...)`, etc | Lazy execution |
44+
| SQL queries: <br/> `spark.sql("select …")` | Lazy execution |
45+
| Actions: `df.collect()`, `df.show()`, etc | Eager execution |
46+
| SQL commands: <br/> `spark.sql("insert …")`, <br/> `spark.sql("create …")`, <br/> etc | Eager execution |
4747

4848
# Schema Analysis: Eager vs. Lazy
4949

5050
## Spark Classic
5151

52-
Traditionally, Spark Classic performs schema analysis eagerly during the logical plan construction phase. This means that when you define transformations, Spark immediately analyzes the DataFrame's schema to ensure all referenced columns and data types are valid.
52+
Traditionally, Spark Classic performs analysis eagerly during logical plan construction. This analysis phase converts the unresolved plan into a fully resolved logical plan and verifies that the operation can be executed by Spark. One of the key benefits of performing this work eagerly is that users receive immediate feedback when a mistake is made.
5353

5454
For example, executing `spark.sql("select 1 as a, 2 as b").filter("c > 1")` will throw an error eagerly, indicating the column `c` cannot be found.
5555

5656
## Spark Connect
5757

58-
Spark Connect differs from Classic because the client constructs unresolved proto plans during transformation. When accessing a schema or executing an action, the client sends the unresolved plans to the server via RPC (remote procedure call). The server then performs the analysis and execution. This design defers schema analysis.
58+
Spark Connect differs from Classic because the client constructs unresolved plans during transformation and defers their analysis. Any operation that requires a resolved plan—such as accessing a schema, explaining the plan, persisting a DataFrame, or executing an action—causes the client to send the unresolved plans to the server over RPC. The server then performs full analysis to get its resolved logical plan and do the operation.
5959

6060
For example, `spark.sql("select 1 as a, 2 as b").filter("c > 1")` will not throw any error because the unresolved plan is client-side only, but on `df.columns` or `df.show()` an error will be thrown because the unresolved plan is sent to the server for analysis.
6161

6262
## Comparison
6363

6464
Unlike query execution, Spark Classic and Spark Connect differ in when schema analysis occurs.
6565

66-
| Aspect | Spark Classic | Spark Connect |
67-
|:--------------------------------------------------------------------------|:--------------|:---------------------------------------------------------------------------|
68-
| Transformations: `df.filter(...)`, `df.select(...)`, `df.limit(...)`, etc | Eager | **Lazy** |
69-
| Schema access: `df.columns`, `df.schema`, `df.isStreaming`, etc | Eager | **Eager** <br/> **Triggers an analysis RPC request, unlike Spark Classic** |
70-
| Actions: `df.collect()`, `df.show()`, etc | Eager | Eager |
71-
| Dependent session state: UDFs, temporary views, configs, etc | Eager | **Lazy** <br/> **Evaluated during the execution** |
66+
| Aspect | Spark Classic | Spark Connect |
67+
|:--------------------------------------------------------------------------------|:--------------|:---------------------------------------------------------------------------------------|
68+
| Transformations: `df.filter(...)`, `df.select(...)`, `df.limit(...)`, etc | Eager | **Lazy** |
69+
| Schema access: `df.columns`, `df.schema`, `df.isStreaming`, etc | Eager | **Eager** <br/> **Triggers an analysis RPC request, unlike Spark Classic** |
70+
| Actions: `df.collect()`, `df.show()`, etc | Eager | Eager |
71+
| Dependent session state of DataFrames: UDFs, temporary views, configs, etc | Eager | **Lazy** <br/> **Evaluated during the plan execution of the DataFrame** |
72+
| Dependent session state of temporary views: UDFs, temporary views, configs, etc | Eager | **Eager** <br/> **The analysis is triggered eagerly when creating the temporary view** |
7273

7374
# Common Gotchas (with Mitigations)
7475

75-
If not careful about the difference between lazy vs. eager analysis, there are four key gotchas to be aware of: 1) overwriting temporary view names, 2) capturing external variables in UDFs, 3) delayed error detection, and 4) excessive schema access on new DataFrames.
76+
If you are not careful about the difference between lazy vs. eager analysis, there are four key gotchas to be aware of: 1) overwriting temporary view names, 2) capturing external variables in UDFs, 3) delayed error detection, and 4) excessive schema access on new DataFrames.
7677

7778
## 1. Reusing temporary view names
7879

@@ -252,7 +253,7 @@ except Exception as e:
252253
print(f"Error: {repr(e)}")
253254
```
254255

255-
The above error handling is useful in Spark Classic because it performs eager analysis, which allows exceptions to be thrown promptly. However, in Spark Connect, this code does not pose any issue, as it only constructs a local unresolved proto plan without triggering any analysis.
256+
The above error handling is useful in Spark Classic because it performs eager analysis, which allows exceptions to be thrown promptly. However, in Spark Connect, this code does not pose any issue, as it only constructs a local unresolved plan without triggering any analysis.
256257

257258
### Mitigation
258259

@@ -286,6 +287,8 @@ try {
286287

287288
## 4. Excessive schema access on new DataFrames
288289

290+
### 4.1 Creating new DataFrames step by step and accessing their schema on each iteration
291+
289292
The following is an anti-pattern:
290293

291294
```python
@@ -298,7 +301,7 @@ for i in range(200):
298301
df.show()
299302
```
300303

301-
While building the DataFrame step by step, each time a new DataFrame is generated with an empty schema, which is lazily computed on access. However, if a user's code frequently accesses the schema of these new DataFrames using methods such as df.columns, it will result in a large number of analysis requests to the server.
304+
While building the DataFrame step by step, each time a new DataFrame is generated with an empty schema, which is lazily computed and cached on access. However, if a user's code accesses the schema of a large number of **new** DataFrames using methods such as `df.columns`, it will result in a large number of analysis requests to the server.
302305

303306
<p style="text-align: center;">
304307
<img src="img/spark-connect-gotchas-antipattern.png"
@@ -311,6 +314,8 @@ Performance can be improved if users avoid large numbers of Analyze requests by
311314

312315
### Mitigation
313316

317+
In the above specific example, the recommended mitigation is to create all the column expressions in a loop, and create a single project with all columns (`df.select(*col_exprs)`).
318+
314319
If your code cannot avoid the above anti-pattern and must frequently check columns of new DataFrames, maintain a set to track column names to avoid analysis requests thereby improving performance.
315320

316321
```python
@@ -341,6 +346,8 @@ for (i <- 0 until 200) {
341346
df.show()
342347
```
343348

349+
### 4.2 Creating a large number of intermediate DataFrames and accessing their schema
350+
344351
Another similar case is creating a large number of unnecessary intermediate DataFrames and analyzing them. In the following case, the goal is to extract the field names from each column of a struct type.
345352

346353
```python
@@ -411,12 +418,13 @@ This approach is significantly faster when dealing with a large number of column
411418

412419
# Summary
413420

414-
| Aspect | Spark Classic | Spark Connect |
415-
|:----------------------|:--------------|:--------------|
416-
| **Query execution** | Lazy | Lazy |
417-
| **Schema analysis** | Eager | Lazy |
418-
| **Schema access** | Local | Triggers RPC |
419-
| **Temporary views** | Plan embedded | Name lookup |
420-
| **UDF serialization** | At creation | At execution |
421+
| Aspect | Spark Classic | Spark Connect |
422+
|:----------------------|:--------------|:----------------------------------------------------|
423+
| **Query execution** | Lazy | Lazy |
424+
| **Command execution** | Eager | Eager |
425+
| **Schema analysis** | Eager | Lazy |
426+
| **Schema access** | Local | Triggers RPC, and caches the schema on first access |
427+
| **Temporary views** | Plan embedded | Name lookup |
428+
| **UDF serialization** | At creation | At execution |
421429

422430
The key difference is that Spark Connect defers analysis and name resolution to execution time.

0 commit comments

Comments
 (0)