Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot use a SQL Server filtered index on a bit column with value of 0 #24435

Closed
ryanjshaw opened this issue Mar 18, 2021 · 1 comment
Closed

Comments

@ryanjshaw
Copy link

ryanjshaw commented Mar 18, 2021

Background

It is common to design a table that tracks pending work items with a column IsProcessed, IsPublished, etc. One possible index design for a table design like this is to have a filtered index on IsProcessed = (0) or IsPublished = (0). This allows the application to efficiently retrieve pending work items and keeps the index size to a minimum.

Expected Behaviour

I would expect the following LINQ query:
Where(e => e.IsProcessed == false)

To produce this SQL:
[IsProcessed] = CAST(0 AS bit)

Actual Behaviour

Instead we get this:
[IsProcessed] <> CAST(1 AS bit)

Unfortunately (at least on SQL Server 2016), with the latter SQL the optimiser will not make use of the filtered index and instead perform a full clustered index scan i.e. it does not understand a bit only has 2 values and <> CAST(1 AS bit) is equivalent to = CAST(0 AS bit).

This seems to be a regression because in EF6 the expected SQL was produced, see https://stackoverflow.com/a/23399905

Test (EF Core 5.0.4)

    class Event
    {
        public int Id { get; set; }
        public bool IsProcessed { get; set; }
        public byte[] Payload { get; set; }
    }

    class EventDbContext : DbContext
    {
        public DbSet<Event> Events { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Data Source=.");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Event>(entityBuilder =>
            {
                entityBuilder.HasKey(e => e.Id);
                
                entityBuilder.Property(e => e.IsProcessed);
                entityBuilder.HasIndex(b => b.IsProcessed).HasFilter("[IsProcessed] = (0)");

                entityBuilder.Property(e => e.Payload);                
            });
        }
    }

    public class BitFilterBugTest
    {
        private EventDbContext _context = new EventDbContext();

        [Fact]
        public void Not_bool_translates_literally()
        {
            var queryString = _context.Events.Where(e => !e.IsProcessed).ToQueryString();
            // This passes as expected
            Assert.Contains("[IsProcessed] <> CAST(1 AS bit)", queryString);
        }

        [Fact]
        public void Bool_equals_false_translates_literally()
        {
            var queryString = _context.Events.Where(e => e.IsProcessed == false).ToQueryString();
            // This does not pass
            Assert.Contains("[IsProcessed] = CAST(0 AS bit)", queryString);
        }
    }

Workaround

Inverting the field's meaning allows a workaround e.g. using IsUnprocessed with a filtered index of IsUnprocessed = (1) and then Where(e => e.IsUnprocessed) will generate SQL that uses the correct index.

@maumar
Copy link
Contributor

maumar commented Aug 1, 2021

dupe of #23472 which has already been fixed

@maumar maumar closed this as completed Aug 1, 2021
@ajcvickers ajcvickers removed this from the 6.0.0 milestone Aug 12, 2021
@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants