views:

95

answers:

2

I have the below LINQ to SQL method that takes an inordinate amount of time to execute yet its SQL counterpart is quite simple and fast. Am I doing something wrong in the LINQ part? I am just trying to return some data to display, read-only, in a Data Grid.

I understand that if the tool doesn't fit don't use it and as such I could just do a SQL call here but I'd like to understand why there is such a difference.

Below is the LINQ and then the SQL that it dumps.

    public static DataTable GetEnrolledMembers(Guid workerID)
    {
        using (var DB = CmoDataContext.Create())
        {                
            var AllEnrollees = from enrollment in DB.tblCMOEnrollments
                               where enrollment.CMOSocialWorkerID == workerID || enrollment.CMONurseID == workerID
                               join supportWorker in DB.tblSupportWorkers on enrollment.EconomicSupportWorkerID
                                   equals supportWorker.SupportWorkerID into workerGroup
                               from worker in workerGroup.DefaultIfEmpty()
                               select
                                   new
                                       {
                                           enrollment.ClientID,
                                           enrollment.CMONurseID,
                                           enrollment.CMOSocialWorkerID,
                                           enrollment.EnrollmentDate,
                                           enrollment.DisenrollmentDate,
                                           ESFirstName = worker.FirstName,
                                           ESLastName = worker.LastName,
                                           ESPhone = worker.Phone
                                       };

            var result = from enrollee in AllEnrollees.AsEnumerable()
                         where (enrollee.DisenrollmentDate == null || enrollee.DisenrollmentDate > DateTime.Now)
                         let lastName = BLLConnect.MemberLastName(enrollee.ClientID)
                         let firstName = BLLConnect.MemberFirstName(enrollee.ClientID)
                         orderby enrollee.DisenrollmentDate ascending , lastName ascending
                         select new
                             {
                                 enrollee.ClientID,
                                 LastName = lastName,
                                 FirstName = firstName,
                                 NurseName = BLLAspnetdb.NurseName(enrollee.CMONurseID),
                                 SocialWorkerName = BLLAspnetdb.SocialWorkerName(enrollee.CMOSocialWorkerID),
                                 enrollee.EnrollmentDate,
                                 enrollee.DisenrollmentDate,
                                 ESWorkerName = enrollee.ESFirstName + " " + enrollee.ESLastName,
                                 enrollee.ESPhone
                             };

            DB.Log = Console.Out;
            return result.CopyLinqToDataTable();
        }
    }

And the SQL:

SELECT [t0].[ClientID], [t0].[CMONurseID], [t0].[CMOSocialWorkerID], [t0].[EnrollmentDate], [t0].[DisenrollmentDate], [t1].[FirstName] AS [ESFirstName], [t1].[LastName] AS [ESLastName], [t1].[Phone] AS [ESPhone]
FROM [dbo].[tblCMOEnrollment] AS [t0]
LEFT OUTER JOIN [dbo].[tblSupportWorker] AS [t1] ON [t0].[EconomicSupportWorkerID] = ([t1].[SupportWorkerID])
WHERE ([t0].[CMOSocialWorkerID] = @p0) OR ([t0].[CMONurseID] = @p1)
-- @p0: Input UniqueIdentifier (Size = 0; Prec = 0; Scale = 0) [060632ee-be09-4057-b17b-2d0190d0ff74]
-- @p1: Input UniqueIdentifier (Size = 0; Prec = 0; Scale = 0) [060632ee-be09-4057-b17b-2d0190d0ff74]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.4926
+4  A: 

By adding AsEnumerable():

var result = from enrollee in AllEnrollees.AsEnumerable()
            ...

You're forcing the first query to be evaluated completely, and every record (AllEnrollees) to be fetched from the DB.

With your SQL statement, you're doing all of the filtering on the server, which will be much faster.

Reed Copsey
I see, Should I post a second question on how to rework my LINQ statement the right way?
Refracted Paladin
@Refracted: Potentially. You can try just removing "AsEnumerable" and seeing if your provider can make sense out of the query - it may be all that's necessary here.
Reed Copsey
@Reed: I removed the `AsEnumerable()` and, consequently, the two `let`'s in the second part. Sadly, it now runs even SLOWER then before!
Refracted Paladin
+1  A: 

First, I don't think you are comparing apples to apples, you have plenty of BllConnect.Something calls around the select of the second linq query. Additionally you have to take out the AsEnumerable as mentioned in the other answer.

Consider the below (given you add the corresponding relations in the db and/or linq2sql designer):

public static DataTable GetEnrolledMembers(Guid workerID)
{
    using (var DB = CmoDataContext.Create())
    {                
        var AllEnrollees = from enrollment in DB.tblCMOEnrollments
                           where enrollment.CMOSocialWorkerID == workerID 
                                 || enrollment.CMONurseID == workerID
                           let w = enrollment.EconomicSupporterWorker
                           select new
                                   {
                                       enrollment.ClientID,
                                       enrollment.CMONurseID,
                                       enrollment.CMOSocialWorkerID,
                                       enrollment.EnrollmentDate,
                                       enrollment.DisenrollmentDate,
                                       ESFirstName = w != null ? w.FirstName : null,
                                       ESLastName = w != null ? w.LastName : null,
                                       ESPhone = w != null ? w.Phone : null
                                   };
        var filteredEnrollees = AllEnrollees
            .Where(e=> e.DisenrollmentDate == null || e.DisenrollmentDate > DateTime.Now);
        //benchmark how much it delays if you do a .ToList until here
        // ... when comparing the sql, run it on the same remote computer you are running this, 
        // so you take into account the time to transfer the data.
        filteredEnrollees = filteredEnrollees 
            .OrderBy(e=> e.DisenrollmentData) // benchmark here again
            .ThenBy(e=> BLLConnect.MemberLastName(enrollee.ClientID)); // prob. causing issues
        var result = // do what you already had, but against filteredEnrollees and benchmark
        // prob. issues with BllConnect.* and BllAspnetdb.* being called for each record / 
        // ... doesn't happen in sql side

        DB.Log = Console.Out;
        return result.CopyLinqToDataTable();
    }
}
eglasius
Thank you for the help. You are correct, `BLLConnect.MemberLastName()` is causing problems in your post saying there isn't a SQL counterpart. Also, I wasn't sure what you were trying to show me with `from worker in workerGroup.DefaultIfEmpty()`
Refracted Paladin
Just edited out the `from worker in workerGroup.DefaultIfEmpty()` / was there by mystake. Re BllConnect.MemberLastName(), that's what you should be getting in your version when you take out the AsEnumerable. That's precisely the issue, by using AsEnumerable or anything equivalent you might be doing, you are causing part of the linq query to be executed on the server.
eglasius
I suggest to replace the BLLConnect.MemberLastName by its inline equivalent so linq2sql does it in the sql side. Additionally make sure none of the calls you make later hit any external resources / like the database again, since those will be called for each row.
eglasius
Thanks, that make's sense. Last thing, could you elaborate, slightly, on what you mean by "inline equivalent"? Thanks.
Refracted Paladin
To move the code that you have inside the method, to the linq query / if that's possible, not sure what you are doing in there.
eglasius
Calling a separate DB...There is data in 3 seperate DB's that go into this.
Refracted Paladin
there is your answer, you are hitting other DB's several times for each row, which is not what the sql query you showed does. I'd search around / post a different question on how to go about it. My recommendation is that you need to get the lists of IDs you need to retrieve, and then use that with a single query for each DB. Then you recompose it in your code / depending on the size of the data, you still can do the last piece of order by, after you join the results.
eglasius