You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/spark-connect-gotchas.md
+34-26Lines changed: 34 additions & 26 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -28,51 +28,52 @@ For an overview of Spark Connect, see [Spark Connect Overview](spark-connect-ove
28
28
29
29
## Spark Classic
30
30
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.
32
32
33
33
## Spark Connect
34
34
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.
36
36
37
37
## Comparison
38
38
39
39
Both Spark Classic and Spark Connect follow the same lazy execution model for query execution.
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.
53
53
54
54
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.
55
55
56
56
## Spark Connect
57
57
58
-
Spark Connect differs from Classic because the client constructs unresolved proto plans during transformation. When accessing a schemaor 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.
59
59
60
60
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.
61
61
62
62
## Comparison
63
63
64
64
Unlike query execution, Spark Classic and Spark Connect differ in when schema analysis occurs.
| 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**|
72
73
73
74
# Common Gotchas (with Mitigations)
74
75
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.
76
77
77
78
## 1. Reusing temporary view names
78
79
@@ -252,7 +253,7 @@ except Exception as e:
252
253
print(f"Error: {repr(e)}")
253
254
```
254
255
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.
256
257
257
258
### Mitigation
258
259
@@ -286,6 +287,8 @@ try {
286
287
287
288
## 4. Excessive schema access on new DataFrames
288
289
290
+
### 4.1 Creating new DataFrames step by step and accessing their schema on each iteration
291
+
289
292
The following is an anti-pattern:
290
293
291
294
```python
@@ -298,7 +301,7 @@ for i in range(200):
298
301
df.show()
299
302
```
300
303
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.
@@ -311,6 +314,8 @@ Performance can be improved if users avoid large numbers of Analyze requests by
311
314
312
315
### Mitigation
313
316
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
+
314
319
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.
315
320
316
321
```python
@@ -341,6 +346,8 @@ for (i <- 0 until 200) {
341
346
df.show()
342
347
```
343
348
349
+
### 4.2 Creating a large number of intermediate DataFrames and accessing their schema
350
+
344
351
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.
345
352
346
353
```python
@@ -411,12 +418,13 @@ This approach is significantly faster when dealing with a large number of column
0 commit comments