https://vladmihalcea.com/sql-server-jdbc-sendstringparametersasunicode/ https://learn.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties?view=sql-server-ver16 If the sendStringParametersAsUnicode property is set to "true", String parameters are sent to the server in Unicode format. If the sendStringParametersAsUnicode property is set to "false", String parameters are sent to the server in non-Unicode format such as ASCII/MBCS instead of Unicode. The default value for the sendStringParametersAsUnicode property is "true". Note: The sendStringParametersAsUnicode property is only checked to send a parameter value with CHAR, VARCHAR, or LONGVARCHAR JDBC types. The new JDBC 4.0 national character methods, such as the setNString, setNCharacterStream, and setNClob methods of SQLServerPreparedStatement and SQLServerCallableStatement classes, always send their parameter values to the server in Unicode whatever the setting of this property. For optimal performance with the CHAR, VARCHAR, and LONGVARCHAR JDBC data types, an application should set the sendStringParametersAsUnicode property to "false" and use the setString, setCharacterStream, and setClob non-national character methods of the SQLServerPreparedStatement and SQLServerCallableStatement classes. When the application sets the sendStringParametersAsUnicode property to "false" and uses a non-national character method to access Unicode data types on the server side (such as nchar, nvarchar and ntext), some data might be lost if the database collation doesn't support the characters in the String parameters passed by the non-national character method. An application should use the setNString, setNCharacterStream, and setNClob national character methods of the SQLServerPreparedStatement and SQLServerCallableStatement classes for the NCHAR, NVARCHAR, and LONGNVARCHAR JDBC data types.
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
In this article, I’m going to explain why you should always disable the sendStringParametersAsUnicode default JDBC Driver setting when using SQL Server.
Let’s assume we have the following database table:
The PostID
column is the Primary Key, and the Title
column is of the VARCHAR
type and has a secondary index as well:
1
|
CREATE INDEX IDX_Post_Title ON Post (Title) |
The Post
table contains the following records:
| PostID | Title | |--------|---------------------------------------------| | 1 | High-Performance Java Persistence, part 1 | | 2 | High-Performance Java Persistence, part 2 | | 3 | High-Performance Java Persistence, part 3 | | 4 | High-Performance Java Persistence, part 4 | | .. | .. | | 249 | High-Performance Java Persistence, part 249 | | 250 | High-Performance Java Persistence, part 250 | |
As you can see, the Title
column is highly selective since every record has a different title value.
When finding a Post
row by its associated Title
column value, we expect an Index Seek operation against the IDX_Post_Title
index, but this is not what we get when using the default SQL Server JDBC settings.
For instance, if we enable the runtime query statistics to retrieve the associated execution plan of the SQL query that filters by the Title
column:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
executeStatement(entityManager, "SET STATISTICS IO, TIME, PROFILE ON" ); try (PreparedStatement statement = connection.prepareStatement( "" " SELECT PostId, Title FROM Post WHERE Title = ? "" " )) { statement.setString( 1 , title); if (statement.execute() && statement.getMoreResults()) { LOGGER.info( "Execution plan: {}{}" , System.lineSeparator(), resultSetToString(statement.getResultSet()) ); } } |
We get the following SQL execution plan:
1
2
3
4
5
|
|StmtText | | ----------------------------------------------------------------------------------------------------| | SELECT PostId, Title FROM Post WHERE Title = @P0 | | | --Clustered Index Scan(OBJECT:([high_performance_sql].[dbo].[Post].[PK__Post__AA12603828AEBF55]),| | WHERE :(CONVERT_IMPLICIT(nvarchar(255),[high_performance_sql].[dbo].[Post].[Title],0)=[@P0])) | |
The Clustered Index Scan
operation tells us that SQL Server has used the PostId
Clustered Index to scan the leaf pages in search of the Title
value we provided.
The reason why the IDX_Post_Title
index was not used is because of the implicit conversion that was done between the provided NVARCHAR
value and the VARCHAR
value of the Title
column.
Even if we provided the Title
bind parameter value as a VARCHAR
using the setString
method:
1
|
statement.setString( 1 , title); |
The SQL Server JDBC Driver behaved as if we used setNString
method instead.
By default, SQL Server sends all String
parameter values as NVARCHAR
since the sendStringParametersAsUnicode
configuration is set to true
.
So, if we set the sendStringParametersAsUnicode
configuration value to false
1
|
jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=high_performance_sql;sendStringParametersAsUnicode= false ; |
And, rerun the previous SQL query, we will get the following execution plan:
1
2
3
4
5
|
|StmtText | | --------------------------------------------------------------------------------| | SELECT PostId, Title FROM Post WHERE Title = @P0 | | | --Index Seek(OBJECT:([high_performance_sql].[dbo].[Post].[IDX_Post_Title]), | | SEEK:([high_performance_sql].[dbo].[Post].[Title]=[@P0]) ORDERED FORWARD )| |
That’s exactly what we were expecting from the start. There’s an Index Seek on the IDX_Post_Title
index, and there’s no implicit conversion happening anymore.
Now, even if you disable the sendStringParametersAsUnicode
setting, you can still persist Unicode data in NHAR
, NVARCHAR
or NLONGVARCHAR
column.
So, if the Title
column is of the NVARCHAR
type:
1
2
3
4
5
|
CREATE TABLE Post ( PostID BIGINT NOT NULL , Title NVARCHAR(255), PRIMARY KEY (PostID) ) |
We can set the Title
column using the setNString
PreparedStatement
method:
1
2
3
4
5
6
7
8
9
10
11
|
try (PreparedStatement statement = connection.prepareStatement( "" " INSERT INTO Post (Title, PostID) VALUES (?, ?) "" " )) { statement.setNString( 1 , "România" ); statement.setLong( 2 , 1L); assertEquals( 1 , statement.executeUpdate()); } |
And, we can read the Title
column using the getNString
ResultSet
method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
try (PreparedStatement statement = connection.prepareStatement( "" " SELECT Title, PostId FROM Post WHERE Title = ? "" " )) { statement.setNString( 1 , "România" ); try (ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { assertEquals( "România" , resultSet.getNString( 1 )); assertEquals(1L, resultSet.getLong( 2 )); } } } |
If you’re using JPA and Hibernate, the NVARCHAR
column needs to be annotated with the @Nationalized
Hibernate annotation to instruct Hibernate that the underlying String
attribute needs to be handled by the StringNVarcharType
, as opposed to the default StringType
:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Entity (name = "Post" ) public class Post { @Id @Column (name = "PostID" ) private Long id; @Column (name = "Title" ) @Nationalized private String title; } |
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
March 3, 2023
Thank you for inspiring article, but are you sure about your results? I tried the scenario you described and SQL server have chosen index seek operation even in case of sendStringParametersAsUnicode=true.
May be it is dependent of SQL server version. I used MSSQL 2017.
Regards
Pavel Rund
March 3, 2023
You’re welcome.
This test provides the proof.
Here are the results:
Test with sendStringParametersAsUnicode=true
| Rows | Executes | StmtText | StmtId | NodeId | Parent | PhysicalOp | LogicalOp | Argument | DefinedValues | EstimateRows | EstimateIO | EstimateCPU | AvgRowSize | TotalSubtreeCost | OutputList | Warnings | Type | Parallel | EstimateExecutions |
| ---- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | -------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ------------ | ------------ | ----------- | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | -------- | -------- | ------------------ |
| 1 | 1 | SELECT PostId, Title FROM Post WHERE Title = @P0 | 1 | 1 | 0 | | | | | 2.0 | | | | 0.0050384817 | | | SELECT | 0 | |
| 1 | 1 | |--Clustered Index Scan(OBJECT:([high_performance_java_persistence].[dbo].[Post].[PK__Post__AA12603836E8D7BA]), WHERE:(CONVERT_IMPLICIT(nvarchar(255),[high_performance_java_persistence].[dbo].[Post].[Title],0)=[@P0])) | 1 | 2 | 1 | Clustered Index Scan | Clustered Index Scan | OBJECT:([high_performance_java_persistence].[dbo].[Post].[PK__Post__AA12603836E8D7BA]), WHERE:(CONVERT_IMPLICIT(nvarchar(255),[high_performance_java_persistence].[dbo].[Post].[Title],0)=[@P0]) | [high_performance_java_persistence].[dbo].[Post].[PostID], [high_performance_java_persistence].[dbo].[Post].[Title] | 2.0 | 0.0046064816 | 4.32E-4 | 61 | 0.0050384817 | [high_performance_java_persistence].[dbo].[Post].[PostID], [high_performance_java_persistence].[dbo].[Post].[Title] | | PLAN_ROW | 0 | 1.0 |
Test with sendStringParametersAsUnicode=false
| Rows | Executes | StmtText | StmtId | NodeId | Parent | PhysicalOp | LogicalOp | Argument | DefinedValues | EstimateRows | EstimateIO | EstimateCPU | AvgRowSize | TotalSubtreeCost | OutputList | Warnings | Type | Parallel | EstimateExecutions |
| ---- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------ | ---------- | ----------- | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | -------- | -------- | ------------------ |
| 1 | 1 | SELECT PostId, Title FROM Post WHERE Title = @P0 | 1 | 1 | 0 | | | | | 1.0 | | | | 0.0032831 | | | SELECT | 0 | |
| 1 | 1 | |--Index Seek(OBJECT:([high_performance_java_persistence].[dbo].[Post].[IDX_Post_Title]), SEEK:([high_performance_java_persistence].[dbo].[Post].[Title]=[@P0]) ORDERED FORWARD) | 1 | 2 | 1 | Index Seek | Index Seek | OBJECT:([high_performance_java_persistence].[dbo].[Post].[IDX_Post_Title]), SEEK:([high_performance_java_persistence].[dbo].[Post].[Title]=[@P0]) ORDERED FORWARD | [high_performance_java_persistence].[dbo].[Post].[PostID], [high_performance_java_persistence].[dbo].[Post].[Title] | 1.0 | 0.003125 | 1.581E-4 | 61 | 0.0032831 | [high_performance_java_persistence].[dbo].[Post].[PostID], [high_performance_java_persistence].[dbo].[Post].[Title] | | PLAN_ROW | 0 | 1.0 |
So, you can run the test for yourself and see that it works as explained in the article.