Почему мой первичный ключ замедляет простой запрос linq-to-sql? - PullRequest
0 голосов
/ 31 октября 2018

У меня есть очень простая таблица в SQL Server, которая имеет составной первичный ключ. Таблица довольно мала с менее чем 10 МБ данных. Я работаю с таблицей в C #, используя контекст данных, автоматически сгенерированный SqlMetal. Я заполнил таблицу некоторыми начальными данными и был удивлен, когда простой выбор, где запрос занял более 30 секунд, завершился! Я предполагал, что смогу прочитать все 10 МБ данных с диска и передать их по нашей гигабитной сети менее чем за секунду. Я подтвердил, что это правда, выполнив тот же запрос через SSMS Microsoft.

Итак, я начал играть с данными, чтобы выяснить, что случилось. Я обнаружил, что если я удалю первичный ключ из таблицы (и заново создаю свой файл SqlMetal), запрос вернет мои данные менее чем за секунду. Отлично! Кроме нет, потому что нет первичного ключа. Я добавил ключ обратно, заново сгенерировал файл SqlMetal и снова запустил запрос. Снова медленно! Затем я удалил ключ из таблицы, не восстанавливая файл SqlMetal. Все еще медленно! Я думаю, что все это говорит мне, что проблема лежит на стороне клиента, возможно, в реализации linq-to-sql или контекста данных, сгенерированного SqlMetal.

Итак, я начал создавать тестовый код и данные, которые я мог бы поделиться на этом сайте. Я создал тестовую таблицу базы данных с той же схемой, кодом, который заполнил таблицу примерно таким же объемом данных, и, конечно, простым запросом, который занимал так много времени. Я запустил его, чтобы убедиться, что все работает ... И вот, запрос выполнялся довольно быстро. Виноваты ли мои фактические данные?

Поэтому я попытался сделать так, чтобы мои тестовые данные максимально приближались к моим реальным данным. Мои фактические данные имеют естественный ключ, состоящий из bigint и nvarchar(64). Затем я добавляю еще один bigint в качестве третьего элемента к первичному ключу для управления версиями. Когда в набор данных добавляются новые элементы, я присваиваю ему уникальный ID (bigint) и назначаю ему идентификатор версии, равный его идентификатору. Последующие изменения этого элемента генерируют новые идентификаторы версий (bigint). Итак, когда я изначально заполнил таблицу всеми новыми элементами, все идентификаторы версий были равны идентификаторам элементов.

Я повторил это в моих тестовых данных и, конечно же, запрос замедлился до сканирования. Я пытаюсь понять, что происходит с этим запросом. Очевидно, что это не проблема с планом выполнения запросов на сервере, потому что он выполняется быстро при любом сценарии, использующем SSMS. Что делает linq-to-sql с моими данными, прежде чем они возвращают результаты моего запроса? Почему профиль данных (а именно значения первичного ключа) влияет на скорость обработки на стороне клиента?

Вот код, который поможет, если вы хотите проверить это:

Генерация таблицы SQL Server:

CREATE TABLE [dbo].[TestClusteredIndex]
(
    [VID] [BIGINT] NOT NULL,
    [EID] [BIGINT] NOT NULL,
    [Filter] [NVARCHAR](64) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    [MID] [NVARCHAR](64) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    [SD] [DATE] NULL,
    [ED] [DATE] NULL,
    [B] [BIT] NOT NULL,

    CONSTRAINT [PK_TestClusteredIndex] 
        PRIMARY KEY CLUSTERED ([EID] ASC, [Filter] ASC, [VID] ASC)
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[TestHeap](
    [VID] [bigint] NOT NULL,
    [EID] [bigint] NOT NULL,
    [Filter] [nvarchar](64) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    [MID] [nvarchar](64) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    [SD] [date] NULL,
    [ED] [date] NULL,
    [B] [bit] NOT NULL
) ON [PRIMARY]

GO

Создайте свой собственный DataContext, используя SqlMetal.

C # Тестовый код:

    public static void RunTests()
    {
        FillDB(false);
        TestSpeedWithPK(true);
        TestSpeedWithoutPK(true);

        FillDB(true);
        TestSpeedWithPK(true);
        TestSpeedWithoutPK(true);
    }

    public static void FillDB(bool problematic)
    {
        long l1 = 5000;
        long l2 = 750000;
        bool dummy = true;
        var db = new YourDataContext(@"YourConnectionString");
        Console.WriteLine("\nClearing database tables...");
        db.ExecuteCommand("DELETE FROM [TestClusteredIndex];");
        db.ExecuteCommand("DELETE FROM [TestHeap];");

        var rows = new List<TestClusteredIndex>();
        Console.WriteLine("Generating " + (problematic ? string.Empty : "non-") + "problematic data...");

        for (int i = 0; i < 120000; i++)
        {
            var row = new TestClusteredIndex
            {
                EID = problematic ? i : l1++,
                VID = problematic ? i : l2++,
                Filter = "Filter" + (char)(i % 3 + 65),
                MID = (i / 0.5).GetHashCode().ToString(),
                SD = null,
                ED = null,
                B = dummy = !dummy
            };

            rows.Add(row);
        }
        Console.WriteLine("Saving " + rows.Count + " rows to table with primary key...");
        db.TestClusteredIndex.InsertAllOnSubmit(rows);
        db.SubmitChanges();
        Console.WriteLine("Copying data to table with no primary key...");
        db.ExecuteCommand("INSERT INTO [TestHeap]([VID],[EID],[Filter],[MID],[SD],[ED],[B]) SELECT [VID],[EID],[Filter],[MID],[SD],[ED],[B] FROM [TestClusteredIndex];");
        Console.WriteLine("Done creating test data.\n");
    }

    public static void TestSpeedWithPK(bool displayQuery)
    {
        Console.Write("Running speed test (primary key: yes)... ");
        var db = new YourDataContext(@"YourConnectionString");
        var sw = new Stopwatch();
        sw.Start();

        var items = db.TestClusteredIndex
            .Where(x => x.Filter == "FilterA")
            .ToList();

        sw.Stop();
        Console.WriteLine("Queried " + items.Count + " records in " + Math.Round(sw.ElapsedMilliseconds / 1000.0, 2) + " seconds.");
    }

    public static void TestSpeedWithoutPK(bool displayQuery)
    {
        Console.Write("Running speed test (primary key:  no)... ");
        var db = new YourDataContext(@"YourConnectionString");
        var sw = new Stopwatch();
        sw.Start();

        var items = db.TestHeap
            .Where(x => x.Filter == "FilterA")
            .ToList();

        sw.Stop();
        Console.WriteLine("Queried " + items.Count + " records in " + Math.Round(sw.ElapsedMilliseconds / 1000.0, 2) + " seconds.");
    }

Вот вывод, когда я запускаю тестовые сценарии:

/*
Clearing database tables...
Generating non-problematic data...
Saving 120000 rows to table with primary key...
Copying data to table with no primary key...
Done creating test data.

Running speed test (primary key: yes)... Queried 40000 records in 0.29 seconds.

@p0:FilterA
SELECT [t0].[VID], [t0].[EID], [t0].[Filter], [t0].[MID], [t0].[SD], [t0].[ED], [t0].[B]
FROM [dbo].[TestClusteredIndex] AS [t0]
WHERE [t0].[Filter] = @p0

Running speed test (primary key:  no)... Queried 40000 records in 0.09 seconds.

@p0:FilterA
SELECT [t0].[VID], [t0].[EID], [t0].[Filter], [t0].[MID], [t0].[SD], [t0].[ED], [t0].[B]
FROM [dbo].[TestHeap] AS [t0]
WHERE [t0].[Filter] = @p0

Clearing database tables...
Generating problematic data...
Saving 120000 rows to table with primary key...
Copying data to table with no primary key...
Done creating test data.

Running speed test (primary key: yes)... Queried 40000 records in 30.46 seconds.

@p0:FilterA
SELECT [t0].[VID], [t0].[EID], [t0].[Filter], [t0].[MID], [t0].[SD], [t0].[ED], [t0].[B]
FROM [dbo].[TestClusteredIndex] AS [t0]
WHERE [t0].[Filter] = @p0

Running speed test (primary key:  no)... Queried 40000 records in 0.07 seconds.

@p0:FilterA
SELECT [t0].[VID], [t0].[EID], [t0].[Filter], [t0].[MID], [t0].[SD], [t0].[ED], [t0].[B]
FROM [dbo].[TestHeap] AS [t0]
WHERE [t0].[Filter] = @p0
*/
...