7

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.

College Code
  • 907
  • 6
  • 12
Stefano Balzarotti
  • 1,760
  • 1
  • 18
  • 35
  • Not related to your question, but why the AsQueryable() call? – Ricardo Peres Nov 28 '19 at 14:22
  • 1
    It's unlikely that it's slow due to it being `nvarchar`, more likely it's because the column type is `varchar(max)` instead of being a fixed length. – DavidG Nov 28 '19 at 14:23
  • It is slow because during execution sql server does an index seek with CONVERT_IMPLICIT, from sql server execution plan – Stefano Balzarotti Nov 28 '19 at 14:25
  • 1
    The column type is wrong though, if you do it properly, it would be much better. In fact, you should really make the column nvarchar too. – DavidG Nov 28 '19 at 14:26
  • Sorry I can't change the column type in sql, the database is already in production and contains 75M of records. – Stefano Balzarotti Nov 28 '19 at 14:28
  • If you run the same query directly in sql server declaring varchar as type is almost instantaneous. – Stefano Balzarotti Nov 28 '19 at 14:29
  • What version of sql server are you using ? I've seen the same behaviour in the past (with sql server 2000 or 2005), where my query was slow because the parameter type didn't match the column type (nvarchar vs varchar in my case as well). Fortunately, I didn't use EF, so it was a fairly easy fix for me then. – Frederik Gheysels Nov 28 '19 at 14:50
  • 2
    Which Entity Framework-core version is this? – Gert Arnold Nov 28 '19 at 14:53
  • >>> I can't change the column type in sql, the database is already in production and contains 75M of records.<<< you can add the nvarchar column and update it in small batches, then drop old column and rename new column – sepupic Nov 28 '19 at 14:54
  • @StefanoBalzarotti do you really intend to store 2GB of data in a single field? If not, fix the type. `max` is stored differently. If you use `varchar(21)` for the *parameter* why do you use `varchar(max)` for the value? Apart from that you didn't post the table schema, database version or EF Core version. EF Core should have generated a `varchar(21)` parameter, not `nvarchar` – Panagiotis Kanavos Nov 28 '19 at 14:54
  • @StefanoBalzarotti ` contains 75M of records` that's **exactly** why you have to change it. That's a lot of inefficiency repeated 75M times – Panagiotis Kanavos Nov 28 '19 at 14:55
  • Where in the question do you see varchar(max)? – sepupic Nov 28 '19 at 14:58
  • @sepupic I show this in the comments. In the question I see nothing. No schema, no execution plan, no database or EF Core version. All of these matter and should appear in the question itself. Nothing I could use to reproduce the issue or find a fix – Panagiotis Kanavos Nov 28 '19 at 14:59
  • Sorry, I think you are right to change to nvarchar, bit I don't have the permission to to do it, I am not the owner of the database. – Stefano Balzarotti Nov 28 '19 at 15:09
  • @Gert Arnold The version is entityframewok 2.2, but I try to upgrade to 3.0 – Stefano Balzarotti Nov 28 '19 at 15:31
  • 1
    Keep in mind that 3.0 has a lot of breaking changes, so it will fix this issue, but might introduce others. – Ivan Stoev Nov 28 '19 at 19:23

3 Answers3

10

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 usings)

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).

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
3

You can define the column as non-unicode in the context's OnModelCreating override:

modelBuilder.Entity<BarcodeTipiDoc>().Property(x => x.Barcode).IsUnicode(false);
Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • 1
    Good answer, as I see your solution works if I use == instead of CompareTo. Also [Column(TypeName = "VARCHAR(21)")] works. As I see the problem is generated by CompareTo. – Stefano Balzarotti Nov 28 '19 at 15:20
  • In ef-core 3 I see it work both with `CompareTo` and `==`, while the attribute has no effect. – Gert Arnold Nov 28 '19 at 15:25
  • I use ef_core 2, I try to upgrade to ef-core 3, thank you. – Stefano Balzarotti Nov 28 '19 at 15:26
  • In EF-core 3, using `.HasColumnType("varchar(21)")` also has the desired effect. I'd prefer more vendor-agnostic configuration options though. – Gert Arnold Nov 28 '19 at 15:27
  • 1
    @Gert Attribute works in 3.0 as well. I agree that this is better (db agnostic) way of specifying `varchar`, but it doesn't solve the original issue, which is the inability of 2.x to infer the correct parameter db type. – Ivan Stoev Nov 28 '19 at 19:31
  • 1
    @Ivan I tested this in 3.0, not yet knowing OP's version. I'm not too surprised it doesn't work in EF2, and glad that this is yet another area where 3 has made progress. But somehow in my tests, setting the attribute only (with `varchar`, obviously) always generates `nvarchar` parameters... – Gert Arnold Nov 28 '19 at 19:40
  • Hmm, probably a Linqpad artifact (cached model or something). The attribute does work! – Gert Arnold Nov 28 '19 at 19:45
  • @Gert That's another EF (Core) flaw - silently ignoring invalid attributes :( Side note - after initial excitement, I'm quite disappointed by 3.0. Too many breaking changes, many of them IMHO unnecessary. Now waiting for 3.1 :) – Ivan Stoev Nov 28 '19 at 20:15
  • 1
    @Ivan Sigh... yeah, longing for the day that this endless beta testing stage is over. It started with EF core 1 FCOL, along with all these tiring SO questions about issues that really aren't exciting al all. Anyway, I stopped looking at EF2 altogether because that's a dead end now. – Gert Arnold Nov 28 '19 at 20:20
  • I tried with entity framework core 3. but I had no performance improvements, the generated query is changed completely with a CASE WHEN. Tomorrow I try to run execution plan to check it, but I guess it still does an implicit conversion. – Stefano Balzarotti Nov 28 '19 at 20:23
  • 2
    @StefanoBalzarotti What Ivan said. QED. By the way, your question didn't fall into this "tiring" category because it taught me a couple of things about EF3 that I *think* will last. – Gert Arnold Nov 28 '19 at 20:27
0

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.

Angelo
  • 1,578
  • 3
  • 20
  • 39