Question

If I execute my (simple) query directly in SQL Server Management Studio...

SELECT auftrag_prod_soll.ID 
  FROM auftrag_prod_soll 
 WHERE auftrag_prod_soll.auftrag_produktion = 51621 
   AND auftrag_prod_soll.prod_soll_über = 539363
 ORDER BY auftrag_prod_soll.reihenfolge

...everything is fine and fast...

Table 'auftrag_prod_soll'. Scan count 2, logical reads 6, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:
  CPU time = 0 ms,  elapsed time = 102 ms.

...because SQL Server chooses a sensible execution plan based on the two filtering criteria:

good


On the other hand, if my application executes the same query with a cursor...

declare @p1 int
declare @p3 int
set @p3=4
declare @p4 int
set @p4=1
declare @p5 int
set @p5=-1
exec sp_cursoropen @p1 output,N' SELECT auftrag_prod_soll.ID  FROM auftrag_prod_soll  WHERE auftrag_prod_soll.auftrag_produktion =  51621   AND auftrag_prod_soll.prod_soll_über =  539363 ORDER BY auftrag_prod_soll.reihenfolge',@p3 output,@p4 output,@p5 output

exec sp_cursorfetch @p1,2,0,1

exec sp_cursorclose @p1

...the performance is terrible...

Table 'auftrag_prod_soll'. Scan count 1, logical reads 1118354, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:
  CPU time = 1094 ms,  elapsed time = 1231 ms.

...because SQL Server chooses a terrible execution plan:

bad


I know that I can work around this by using an index hint. However, I want to understand why this happens.

I have tried:

  • DBCC FREEPROCCACHE
  • UPDATE STATISTICS auftrag_prod_soll

but it didn't make a difference.

I also looked at the histograms of the two indices on prod_soll_über and auftrag_produktion: They are well distributed, so SQL Server should be able to deduce that the query will return a few rows at most and, thus, the key lookup and the sorting operation will be way faster than the index scan.

I also tried to create a non-clustered index containing both auftrag_produktion and prod_soll_über, but it did not change the execution plan of the cursor (it did make the direct query even faster, though).


Here is the complete table definition, in case it's relevant:

CREATE TABLE [auftrag_prod_soll](
    [auftrag_produktion] [int] NULL,
    [losgrößenunabh] [smallint] NOT NULL,
    [stückliste_vorh] [smallint] NOT NULL,
    [erledigt] [smallint] NOT NULL,
    [ext_wert_ueberst] [smallint] NOT NULL,
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [prod_soll_über] [int] NULL,
    [artikel] [int] NULL,
    [gesamtmenge_soll] [float] NULL,
    [produktionstext] [nvarchar](max) NULL,
    [reihenfolge] [int] NULL,
    [reihenfolge_druck] [int] NULL,
    [infkst_unter] [int] NULL,
    [ebene] [smallint] NULL,
    [bezeichnung] [varchar](50) NULL,
    [extern_text] [nvarchar](max) NULL,
    [intern_preis] [float] NULL,
    [intern_wert] [float] NULL,
    [extern_preis] [float] NULL,
    [extern_wert] [float] NULL,
    [extern_proz] [float] NULL,
    [dummyfeld] [varchar](50) NULL,
    [mengeneinheit] [varchar](50) NULL,
    [artikel_art] [smallint] NULL,
    [s_insert] [float] NULL,
    [s_update] [float] NULL,
    [s_user] [varchar](255) NULL,
    [preiseinheit] [float] NULL,
    [memo] [nvarchar](max) NULL,
    [lager_nummer] [int] NULL,
    [zweitmenge] [float] NULL,
    [zweit_einheit] [float] NULL,
    [zweit_mengeneinh] [varchar](50) NULL,
    [kst_preis1] [float] NULL,
    [kst_preis2] [float] NULL,
    [kst_preis3] [float] NULL,
    [kst_preis4] [float] NULL,
    [p_position] [int] NULL,
    [zeilen_status] [int] NULL,
    [fs_adresse_lief] [uniqueidentifier] NULL,
    [t_artikel_stückliste] [int] NULL,
    [div_text1] [varchar](255) NULL,
    [div_text2] [varchar](255) NULL,
    [menge_urspr] [float] NULL,
    [fs_artikel_index] [uniqueidentifier] NULL,
    [s_guid] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
    [gemein_kosten] [float] NULL,
    [fs_leistung] [uniqueidentifier] NULL,
    [sonderlogik_ok_rech] [smallint] NOT NULL,
    [sonderlogik_ok_manuell] [int] NULL,
    [menge_inkl_frei] [float] NULL,
    [art_einheit] [int] NULL,
    [drittmenge] [float] NULL,
 CONSTRAINT [PK__auftrag_prod_sol__50E5F592] PRIMARY KEY CLUSTERED ([ID] ASC)
) 

CREATE NONCLUSTERED INDEX [artikel] ON [auftrag_prod_soll] ([artikel] ASC)
CREATE NONCLUSTERED INDEX [auftrag_produktion] ON [auftrag_prod_soll] ([auftrag_produktion] ASC)
CREATE NONCLUSTERED INDEX [dummyfeld] ON [auftrag_prod_soll] ([dummyfeld] ASC)
CREATE NONCLUSTERED INDEX [fs_adresse_lief] ON [auftrag_prod_soll] ([fs_adresse_lief] ASC)
CREATE NONCLUSTERED INDEX [fs_artikel_index] ON [auftrag_prod_soll] ([fs_artikel_index] ASC)
CREATE NONCLUSTERED INDEX [fs_leistung] ON [auftrag_prod_soll] ([fs_leistung] ASC)
CREATE NONCLUSTERED INDEX [lager_nummer] ON [auftrag_prod_soll] ([lager_nummer] ASC)
CREATE NONCLUSTERED INDEX [prod_soll_über] ON [auftrag_prod_soll] ([prod_soll_über] ASC)
CREATE NONCLUSTERED INDEX [reihenfolge] ON [auftrag_prod_soll] ([reihenfolge] ASC)
CREATE UNIQUE NONCLUSTERED INDEX [s_guid] ON [auftrag_prod_soll] ([s_guid] ASC)
CREATE NONCLUSTERED INDEX [s_insert] ON [auftrag_prod_soll] ([s_insert] ASC)
CREATE NONCLUSTERED INDEX [u_test] ON [auftrag_prod_soll] ([auftrag_produktion] ASC,
    [prod_soll_über] ASC)
CREATE NONCLUSTERED INDEX [zeilen_status] ON [auftrag_prod_soll] ([zeilen_status] ASC)
ALTER TABLE [auftrag_prod_soll] ADD  DEFAULT ((0)) FOR [losgrößenunabh]
ALTER TABLE [auftrag_prod_soll] ADD  DEFAULT ((0)) FOR [stückliste_vorh]
ALTER TABLE [auftrag_prod_soll] ADD  DEFAULT ((0)) FOR [erledigt]
ALTER TABLE [auftrag_prod_soll] ADD  DEFAULT ((0)) FOR [ext_wert_ueberst]
ALTER TABLE [auftrag_prod_soll] ADD  CONSTRAINT [DF__auftrag_p__s_gui__28A2FA0E]  DEFAULT (newid()) FOR [s_guid]
ALTER TABLE [auftrag_prod_soll] ADD  DEFAULT ((0)) FOR [sonderlogik_ok_rech]

How can I help SQL Server find the good query plan even if cursors are used?

I have temporarily "fixed" this problem by disabling the "reihenfolge" index, but I still want to understand why this happens, so that avoid such problems in the future.

The values of @p3, @p4, and @p5 remain at their initial values (4, 1, -1) after the call to sp_cursoropen, but as soon as I "fix" the problem by removing the reihenfolge index, they switch to (1, 1, 0).

Was it helpful?

Solution

How can I help SQL Server find the good query plan even if cursors are used?

Literally: use a plan guide or hints. But it would be much better to provide SQL Server with an optimal index, whether a cursor is used or not:

CREATE INDEX [IX dbo.auftrag_prod_soll auftrag_produktion prod_soll_über reihenfolge] 
ON dbo.auftrag_prod_soll (auftrag_produktion, prod_soll_über, reihenfolge);

This is better than the index-intersection plus sort plan, and much better than the scan-in-order and lookup plan. This index allows an equality seek on both auftrag_produktion and prod_soll_über, while also ensuring the matching rows can come back in reihenfolge order:

Optimal plan

Cursors

The parameters supplied to sp_cursoropen determine the type of cursor requested, and optionally which options are acceptable. The server may change these options (hence being output parameters) if the requested type and options is not valid or available (for a range of possible reasons).

The provided code requests a forward-only, read-only cursor, which the server delivers as a dynamic-type cursor. See Understanding SQL Server Fast_Forward Server Cursors for the details on the choice between static and dynamic style plans.

When you "fix" the problem, a keyset cursor is delivered, because a dynamic plan is no longer possible (a dynamic cursor plan cannot sort).

You need to specify the cursor options needed by the application (e.g. for concurrency) as well as any type that happens to be best for performance, given the intended usage. If you intend to fetch all rows, or the plan for fetching one row quickly is in fact not optimal, you may need to specify a different type e.g. static with @P3 = 8. Add 0x80000 (static acceptable) if you want to be sure of being delivered a static cursor.

Based on the execution plan image it seems SQL Server chooses a dynamic plan underestimating the number of rows that will need to be passed to the Key Lookup before a predicate there (I assume) matches the first row:

Scan plus lookup

Notice the large number of rows being read from the scan. The best a dynamic plan can do is scan the reihenfolge index in order. Although SQL Server knows about the distribution of values from statistics, it does not know where in a particular scan order those values are. So it guesses at the costs involved in the dynamic plan, and happens to cost it cheaper than a plan with a blocking sort operator.

OTHER TIPS

I want to understand why this happens.

It seems to me that the reason this happens is the difference between a query with literal values vs a query using parameters. Although you said that the indexes "are well distributed," there still may be some edge values that aren't and the optimizer isn't willing to make that leap of faith without actual values.

Have you attempted the cursor with literal values to see how it behaves? Have you tried using parameters in Management Studio to see how it behaves there?

Licensed under: CC-BY-SA with attribution
Not affiliated with dba.stackexchange
scroll top