Eric Andres
Eric Andres

Reputation: 3417

SQL Server proc running 5x slower than plain query

I have the following query:

DECLARE @DaysNotUsed int = 14
DECLARE @DaysNotPhoned int = 7

--Total Unique Students
DECLARE @totalStudents TABLE (SchoolID uniqueidentifier, TotalUniqueStudents int)
INSERT INTO @totalStudents
SELECT 
        SSGG.School,
        COUNT(DISTINCT S.StudentID)
    FROM Student S
        INNER JOIN StudentStudents_GroupGroups SSGG ON (SSGG.Students = S.StudentID AND SSGG.School = S.School)
        INNER JOIN [Group] G ON (G.GroupID = SSGG.Groups AND G.School = SSGG.School)
        INNER JOIN SessionHistory SH ON (SH.Student = S.StudentID AND SH.School = S.School AND SH.StartDateTime > GETDATE() - @DaysNotUsed)
    WHERE G.IsBuiltIn = 0
        AND S.HasStartedProduct = 1
    GROUP BY SSGG.School

--Last Used On
DECLARE @lastUsed TABLE (SchoolID uniqueidentifier, LastUsedOn datetime)
INSERT INTO @lastUsed
SELECT
        vi.SchoolID,
        MAX(sh.StartDateTime)
    FROM View_Installation as vi
        INNER JOIN SessionHistory as sh on sh.School = vi.SchoolID
    GROUP BY vi.SchoolID

SELECT 
        VI.SchoolID, 
        INS.DateAdded,
        INS.Removed,
        INS.DateRemoved,
        INS.DateToInclude,
        VI.SchoolName AS [School Name], 
        VI.UsersLicensed AS [Licenses],
        ISNULL(TS.TotalUniqueStudents, 0) as [Total Unique Students],
        ISNULL(TS.TotalUniqueStudents, 0) * 100 / VI.UsersLicensed as [% of Students Using],
        S.State,
        LU.LastUsedOn,
        DATEDIFF(DAY, LU.LastUsedOn, GETDATE()) AS [Days Not Used],
        SI.AreaSalesManager AS [Sales Rep],
        SI.CaseNumber AS [Case #],
        SI.RenewalDate AS [Renewal Date],
        SI.AssignedTo AS [Assigned To],
        SI.Notes AS [Notes]
    FROM View_Installation VI
        INNER JOIN School S ON S.SchoolID = VI.SchoolID
        LEFT OUTER JOIN @totalStudents TS on TS.SchoolID = VI.SchoolID
        INNER JOIN @lastUsed LU on LU.SchoolID = VI.SchoolID
        LEFT OUTER JOIN InactiveReports..SchoolInfo SI ON S.SchoolID = SI.SchoolID
        LEFT OUTER JOIN InactiveReports..InactiveSchools INS ON S.SchoolID = INS.SchoolID
    WHERE VI.UsersLicensed > 0
        AND VI.LastPhoneHome > GETDATE() - @DaysNotPhoned
        AND
        (
            (
                SELECT COUNT(DISTINCT S.StudentID)
                    FROM Student S
                        INNER JOIN StudentStudents_GroupGroups SSGG ON (SSGG.Students = S.StudentID AND SSGG.School = S.School)
                        INNER JOIN [Group] G ON (G.GroupID = SSGG.Groups AND G.School = SSGG.School)
                    WHERE G.IsBuiltIn = 0
                        AND S.School = VI.SchoolID
            ) * 100 / VI.UsersLicensed < 50
            OR
            VI.SchoolID NOT IN 
            (
                SELECT DISTINCT SH1.School
                FROM SessionHistory SH1
                WHERE SH1.StartDateTime > GETDATE() - @DaysNotUsed
            ) 
        )
    ORDER BY [Days Not Used] DESC

Running just plain sql like this in SSMS take about 10 seconds to run. When I created a stored procedure with exactly the same code, the query takes 50 seconds instead. The only difference in the actual code of the proc is a SET NOCOUNT ON that the IDE put in by default, but adding that line to the query doesn't have any impact. Any idea what would cause such a dramatic slow down like this?

EDIT I neglected the declare statements at the beginning. These are not in the proc, but are parameters to it. Could this be the difference?

Upvotes: 2

Views: 2693

Answers (4)

Eric Andres
Eric Andres

Reputation: 3417

OK, thank you all for your help. Turns out it was a terribly stupid rookie mistake. The first time I created the proc, it created it under my user's schema instead of the dbo schema. When I called the proc I was simply doing 'exec proc_name', which I'm realizing now was using the version of the proc under my user's schema. Running 'exec dbo.proc_name' ran as expected.

Upvotes: 0

Adam G
Adam G

Reputation: 35

I would recommend recompiling the execution plan for the stored procedure.

usage: sp_recompile '[target]'

example: sp_recompile 'dbo.GetObject'

When you execute a query from SSMS the query plan is automatically redone every time its executed. However with stored procs, sql server caches execution plans for stored procedures, and its this execution plan that gets used everytime the stored proc is called.

Link for sp_recompile.

You can also change the proc to use with WITH RECOMPILE clause within the stored proc.

Example:

CREATE PROCEDURE dbo.GetObject
(
    @parm1 VARCHAR(20)
)
WITH RECOMPILE
AS
BEGIN 
  -- Queries/work here.
END

However this will force the execution plan to be recompiled every time the stored proc is called. This is good for dev/testing where the proc and/or data changes quite frequently. Make sure you remove it when you deploy it to production, as this can have a performance hit.

sp_recompile only recompiles the execution plan once. If you need to do it again at a later date, you will need to make the call again.

Good luck!

Upvotes: 0

Cade Roux
Cade Roux

Reputation: 89711

Also, in addition to everything else mentioned, if you are on SQL Server 2008 and up, have a look at OPTIMIZE FOR UNKNOWN http://blogs.msdn.com/b/sqlprogrammability/archive/2008/11/26/optimize-for-unknown-a-little-known-sql-server-2008-feature.aspx

Upvotes: 1

Aaron Bertrand
Aaron Bertrand

Reputation: 280431

I agree about the potential parameter sniffing issue, but I would also check these settings.

For the procedure:

SELECT uses_ansi_nulls, uses_quoted_identifier
  FROM sys.sql_modules
  WHERE [object_id] = OBJECT_ID('dbo.procedure_name');

For the SSMS query window where the query is running fast:

SELECT [ansi_nulls], [quoted_identifier]
  FROM sys.dm_exec_sessions
  WHERE session_id = @@SPID;

If either of these don't match, you might consider dropping the stored procedure and re-creating it with those two settings matching. For example, if the procedure has uses_quoted_identifier = 0 and the session has quoted_identifier = 1, you could try:

DROP PROCEDURE dbo.procedure_name;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE PROCEDURE dbo.procedure_name
AS
BEGIN
    SET NOCOUNT ON;
    ...
END
GO

Ideally all of your modules will be created with the exact same QUOTED_IDENTIFIER and ANSI_NULLS settings. It's possible the procedure was created when the settings were off (the default is on for both), or it's possible that where you are executing the query, one or both options are off (you can change this behavior in SSMS under Tools/Options/Query Execution/SQL Server/ANSI).

I'm not going to make any disclaimers about the behavior of the stored procedure with the different settings (for example you may have wanted ANSI_NULLS off so you could compare NULL = NULL), that you'll have to test, but at least you'll be comparing queries that are being run with the same options, and it will help narrow down potential parameter sniffing issues. If you're intentionally using SET ANSI_NULLS OFF, however, I caution you to find other approaches as that behavior will eventually be unsupported.

Other ways around parameter sniffing:

  • make sure you don't inadvertently compile the procedure with atypical parameters
  • use the recompile option either on the procedure or on the statement that seems to be the victim (I'm not sure if all of these are valid, because I can only tell that you are using SQL Server 2005 or greater, and some of these were introduced in 2008)
  • declare local variables similar to your input parameters, and pass the input parameter values to them, using the local variables later in the prodedure and ignoring the input parameters

The last option is my least favorite, but it's the quickest / easiest fix in the midst of troubleshooting and when users are complaining.

Upvotes: 2

Related Questions