'EntityFramework is very slow to compare strings because create a nvarchar sqlparameter instead of varchar
I have this sample query:
context.BarcodeTipiDoc.AsQueryable().Where(d => d.Barcode.CompareTo(minBarcode) > 0);
That query runs very slow because Entity Framework creates SqlParameter for "minBarcode" as nvarchar
instead of varchar
.
I tried to set column mapping:
[Column("Barcode", TypeName = "varchar(21)")]
public string Barcode { get; set; }
but nothing changed.
There is a way to tell to Entity Framework the right type of the sqlparameter?
This query is almost instantaneous:
DECLARE @__minBarcode_0 AS Varchar(21)
SET @__minBarcode_0 = 'aa'
SELECT TOP(100) [d].[Barcode], [d].[contenttype], [d].[idvolume], [d].[path_documento], [d].[Progressivo], [d].[Stato]
FROM BarcodeTipiDoc AS [d]
WHERE [d].[Barcode] > @__minBarcode_0
Same query generated by Entity Framework, takes several minutes because of nvarchar:
DECLARE @__minBarcode_0 AS nvarchar(21)
SET @__minBarcode_0 = 'aa'
SELECT TOP(100) [d].[Barcode], [d].[contenttype], [d].[idvolume], [d].[path_documento], [d].[Progressivo], [d].[Stato]
FROM BarcodeTipiDoc AS [d]
WHERE [d].[Barcode] > @__minBarcode_0
table schema:
Barcode varchar(21) Unchecked
tipodoc char(4) Unchecked
codutenteinserimento uniqueidentifier Checked
dataacquisizione datetime Checked
firmato bit Checked
tipodocdescrizione varchar(50) Checked
Stato int Unchecked
originedoc tinyint Checked Unchecked
I am not allowed to change anything of the database, I just need to change generated sql code from LINQ by entity framework.
If there are no ways, I'll be forced to write and the execute the select as pure string.
The version is entity framework 2.2, but I can upgrade.
Solution 1:[1]
There is a way to tell to Entity Framework the right type of the sqlparameter?
Currently (EF Core 2.x, 3.0) there is no such way. EF Core tries to infer the parameter type from the usage inside expressions.
So TypeName = "varchar(21)
or .IsUnicode(false).HasMaxLength(21)
column mapping is step in the right direction.
Unfortunately 2.x parameter type inference succeeds for comparison operators like ==
, >
etc. but fails for methods like string.CompareTo
, string.Compare
etc.
This has been fixed in 3.0, but now the translation is far from optimal (CASE WHEN ... > 0
rather than simply >
) and also there are many breaking changes, so upgrading just because of that doesn't make sense and is risky.
What I can offer is a solution based on a custom mapped database scalar methods similar to Entity Framework Core: Guid Greater Than for Paging. It introduces several string
custom methods which are mapped to string
comparison operators:
public static class StringFunctions
{
public static bool IsGreaterThan(this string left, string right) => string.Compare(left, right) > 0;
public static bool IsGreaterThanOrEqual(this string left, string right) => string.Compare(left, right) >= 0;
public static bool IsLessThan(this string left, string right) => string.Compare(left, right) < 0;
public static bool IsLessThanOrEqual(this string left, string right) => string.Compare(left, right) <= 0;
public static ModelBuilder RegisterStringFunctions(this ModelBuilder modelBuilder) => modelBuilder
.RegisterFunction(nameof(IsGreaterThan), ExpressionType.GreaterThan)
.RegisterFunction(nameof(IsGreaterThanOrEqual), ExpressionType.GreaterThanOrEqual)
.RegisterFunction(nameof(IsLessThan), ExpressionType.LessThan)
.RegisterFunction(nameof(IsLessThanOrEqual), ExpressionType.LessThanOrEqual);
static ModelBuilder RegisterFunction(this ModelBuilder modelBuilder, string name, ExpressionType type)
{
var method = typeof(StringFunctions).GetMethod(name, new[] { typeof(string), typeof(string) });
modelBuilder.HasDbFunction(method).HasTranslation(parameters =>
{
var left = parameters.ElementAt(0);
var right = parameters.ElementAt(1);
// EF Core 2.x
return Expression.MakeBinary(type, left, right, false, method);
});
return modelBuilder;
}
}
For EF Core 3.0 replace
return Expression.MakeBinary(type, left, right, false, method);
with (plus respective using
s)
if (right is SqlParameterExpression rightParam)
right = rightParam.ApplyTypeMapping(left.TypeMapping);
else if (left is SqlParameterExpression leftParam)
left = leftParam.ApplyTypeMapping(right.TypeMapping);
return new SqlBinaryExpression(type, left, right, typeof(bool), null);
Now all you need is to call
modelBuilder.RegisterStringFunctions();
inside your OnModelCreating
override.
Then inside your query, instead of
d => d.Barcode.CompareTo(minBarcode) > 0
use
d => d.Barcode.IsGreaterThan(minBarcode)
and it will be translated to
[d].[Barcode] > @__minBarcode_0
with correct db parameter type (same as the db type of BarCode
column).
Solution 2:[2]
You can define the column as non-unicode in the context's OnModelCreating
override:
modelBuilder.Entity<BarcodeTipiDoc>().Property(x => x.Barcode).IsUnicode(false);
Solution 3:[3]
In your column mapping, you originally declared this:
[Column("Barcode", TypeName = "varchar(21)")]
public string Barcode { get; set; }
Can you try this:
[Column(TypeName = "VARCHAR(21)")]
public string Barcode { get; set; }
Or you can specify in the Model Builder:
modelBuilder.Entity<BarCodeTipiDoc>()
.Property(x=> x.BarCode)
.HasColumnType("varchar(21)");
It would also help if you could post the model for your object BarcodeTipiDoc
.
Update: Just saw that you were using EF Core.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Ivan Stoev |
Solution 2 | Gert Arnold |
Solution 3 |