5
\$\begingroup\$

On my machine it runs in about 60 seconds and about 2-3 minutes live.

The criteria needed are:

  • Current month only (from first of month until current day)
  • I need the rank of the currently logged in person based on total sales.
  • I have two tables across two databases where this data is to be gathered:

    • Database 1: ProPit_User table (this houses the logged in users).
    • Database 2: This houses the sales in the Orders table.
  • Both tables will be linked on the SalesRepID column.

  • There are around around 352 users in the ProPit_User table.
  • There are a lot of orders in the orders table.

This code does what I need it to do; it is just dreadfully slow.

public static int GetNationalRank(int teamId, int CurrentUserId)
{
    List<ProPit_User> lstProPit_User = new List<ProPit_User>();
    List<TeamSales> lstTeamSales = new List<TeamSales>();
    int currentUserRank = 0;

    using (ProsPitEntities ppdb = new ProsPitEntities())
    using (TraegerEntities db = new TraegerEntities())
    {
        lstProPit_User = ppdb.ProPit_User.Where(x => x.SalesRepID != null).ToList();

        foreach (var user in lstProPit_User)
        {

            var strStartDate = DateTime.Now.AddDays(-(DateTime.Now.Day - 1)).ToString("M/d/yyyy");
            var dtStartDate = Convert.ToDateTime(strStartDate);

            var strEndDate = DateTime.Now.AddDays(1).ToString("M/d/yyyy");
            var dtEndDate = Convert.ToDateTime(strEndDate);

            var orders = db.Orders.Where(o => o.DateCompleted >= dtStartDate
                                        && o.DateCompleted < dtEndDate
                                        && (o.Status == 1 || o.Status == 2)
                                        && o.Kiosk != 0).AsEnumerable();

            var total = (from o in orders
                         where o.SalesRepID == user.SalesRepID
                         select o.OrderTotal).Sum();

            TeamSales TeamSales = new TeamSales();

            TeamSales.userId = user.userID;
            TeamSales.TotalSales = total;


            lstTeamSales.Add(TeamSales);

            }

        }

        int rank = lstTeamSales.OrderByDescending(x => x.TotalSales).FindIndex(x => x.userId == CurrentUserId);

        if (rank == -1)
        {
            currentUserRank = 0;
        }
        else
        {
            // Zero based index need to add one to rank
            currentUserRank = rank + 1;
        }

    }

    return currentUserRank;
}

I have .FindIndex added as an extension method to IEnumerable.

All I really need is to get the rank of the person logged in in comparison to everyone else sales.

\$\endgroup\$
1

3 Answers 3

4
\$\begingroup\$

Try what's below on for size. The date calculation is moved out of the loop, the TeamSales list capacity is pre-allocated and finally, the two LINQ statements are combined into one and rolled into AddRange rather than the explicit loop.

public static int GetNationalRank(int teamId, int CurrentUserId)
{
    int currentUserRank;
    var startDate = Convert.ToDateTime(DateTime.Now.AddDays(-(DateTime.Now.Day - 1)).ToString("M/d/yyyy"));
    var endDate = Convert.ToDateTime(DateTime.Now.AddDays(1).ToString("M/d/yyyy"));
    List<ProPit_User> proPitUsers;

    using (var ppdb = new ProsPitEntities())
    {
        proPitUsers = ppdb.ProPit_User.Where(x => x.SalesRepID != null).ToList();
    }

    var teamSales = new List<TeamSales>(proPitUsers.Count());

    using (var db = new TraegerEntities())
    {
        teamSales.AddRange(proPitUsers
            .Select(user => new
            {
                user,
                total = db.Orders.Where(o => o.DateCompleted >= startDate
                    && o.DateCompleted < endDate
                    && (o.Status == 1 || o.Status == 2)
                    && o.Kiosk != 0
                    && o.SalesRepID == user.SalesRepID).Select(o => (decimal?)o.OrderTotal).Sum() ?? 00.0M
            })
            .Select(userTotal => new TeamSales { userId = userTotal.user.userID, TotalSales = userTotal.total }));

        var rank = teamSales
            .OrderByDescending(x => x.TotalSales)
            .FindIndex(x => x.userId == CurrentUserId);

        currentUserRank = rank == -1 ? 0 : rank + 1;
    }

    return currentUserRank;
}
\$\endgroup\$
9
  • \$\begingroup\$ This provided the following error on the AddRange line. The cast to value type 'Decimal' failed because the materialized value is null. Either the result type's generic parameter or the query must use a nullable type. \$\endgroup\$ Commented May 22, 2014 at 21:53
  • \$\begingroup\$ From poking around it is erroring out on an individual who exists in the ProPit_User database but has no sales in the sales database. When running a query on the Orders table it comes up with 0 rows. \$\endgroup\$ Commented May 22, 2014 at 21:58
  • \$\begingroup\$ Made an edit to coalesce nulls to 0.00 in the Select. \$\endgroup\$ Commented May 22, 2014 at 21:58
  • \$\begingroup\$ I tried that edit. But it gives an error saying cannot ?? cannot be applied to operands of decimal and decimal. I then tried to cast it as (decimal?) which would allow it to build but it then errors out on the page saying The cast to value type 'Decimal' failed because the materialized value is null. Either the result type's generic parameter or the query must use a nullable type. pesky errors. \$\endgroup\$ Commented May 22, 2014 at 22:05
  • \$\begingroup\$ I tried to update the class for TotalSales from decimal to decimal? but same isues. \$\endgroup\$ Commented May 22, 2014 at 22:06
4
\$\begingroup\$

I think your main problem is that for each user in the list you are performing two LINQ queries, at least one of which hits the database.

What you are doing is:

  • for each user
    • get all orders (for everyone) and load them into memory
    • select out only the orders you are looking for
    • do stuff with data
    • delete all orders you retrieved so you can just go and get them all again next time

I think you would have more luck if you pulled all of this out of the loop:

        var strEndDate = DateTime.Now.AddDays(1).ToString("M/d/yyyy");
        var dtEndDate = Convert.ToDateTime(strEndDate);

        var orders = db.Orders.Where(o => o.DateCompleted >= dtStartDate
                                    && o.DateCompleted < dtEndDate
                                    && (o.Status == 1 || o.Status == 2)
                                    && o.Kiosk != 0).AsEnumerable();

This way you:

  • Query all of the orders for everyone (only once)
  • For each user
    • select out only the orders you are looking for
    • do stuff with data

This way you are only getting all of the data only once. Remember that SQL queries are the slowest part of almost any method. You don't want to query more information than you need to (select Col1, Col2 is preferable to select * unless you absolutely must have all information) and to not query more rows than you need to. You also normally wont want to use more than one query where one would suffice.

Also it might be of interest to note that you can capture the SQL generated by LINQ (see: http://msdn.microsoft.com/en-us/library/bb386961%28v=vs.110%29.aspx). Your query here looks really simple but you might be surprised at the stupid SQL that is sometimes generated by a LINQ to SQL statement. Updating the structure of the LINQ statement will affect the SQL that is generated which could affect the queries performance in the database. As I said though I don't think this is your problem but it's worth noting none the less.

Edit: Apparently I can't comment on Jesse C. Slicer's answer. I do like his solution because the query there is restricting the query results for each user to only the orders made by that user. So instead of getting all of the orders each time you only get the ones you want. This is an improvement but my solution above might be a bit better as it only hits the database once ... assuming of course that all of the results are able to fit into memory.

\$\endgroup\$
2
\$\begingroup\$

To make things short as I am on my mobile right now:

Calculate start- and enddate outside of the foreach loop. This is the most obvious step to performance here.

On a related note.. drop the hungarian Notation. Prefixing str and dt respectively adds no meaning to the name of the variable, especially when you aren't even using the String representation for anything but creating the datetime representation, which is beating around the bush from bottom to top...

\$\endgroup\$
1
  • \$\begingroup\$ I moved the date time stuff out of the foreach block. If it did change the time it was minuscule. I also cleaned up the datetime down to two lines instead of four. I think the problem lies within the linq and the calls to the database. \$\endgroup\$ Commented May 22, 2014 at 21:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.