2016-05-24 11 views
0

Ich benutze SQL Server 2014. Ich habe eine Multi-Anweisung Tabelle Wert Funktion zum Aufteilen einer Zeichenfolge in eine Tabelle über ein Trennzeichen.SQL Server: Korrekte Schätzung von Zeilen von String Split-Funktion

ich Aufspalten bin nicht lange Strings aber ich benutze diese Funktion in gespeicherten Prozeduren wie folgt aus:

ALTER PROCEDURE dbo.example 
    @parameters 
AS 
Begin 
SELECT * 
FROM TableA 
LEFT JOIN Table B on B.ID = A.FID 
WHERE B.ID IN (SELECT Data FROM dbo.fn_Split(@parameters, ',') 
END 

Die eigentliche sproc hat Multi-Joins und viele Parameter. Wenn ich den Ausführungsplan betrachtete, gab der Operator Table Scan (fn_split) Cost 0% immer eine ungenaue Schätzung der Zeilen zurück. Für 11 Parameter werden 100 Zeilen geschätzt.

Ich hörte Tabellenwert-Funktion mit mehreren Anweisungen ist langsam, aber mit inline, XML oder Jeff Moden der Splitter sind langsamer, dass mein orginial ein, wenn in der WHERE-Klausel verwendet. Sie haben schreckliche Ausführungspläne und schlechteste Schätzungen der Zeilen

Gibt es eine Möglichkeit, eine korrekte Schätzung von Zeilen zu erhalten, wenn eine Zeichenfolge in eine Tabelle aufgeteilt wird?

Meine Funktion:

ALTER FUNCTION dbo.fn_Split( 
    @RowData NVARCHAR(MAX), 
    @Delimeter NVARCHAR(MAX) 
) 
RETURNS @RtnValue TABLE (
    ID INT IDENTITY(1,1), 
    Data NVARCHAR(MAX) 
) 
AS 
BEGIN 
DECLARE @Iterator INT 
SET @Iterator = 1 
DECLARE @FoundIndex INT 
SET @FoundIndex = CHARINDEX(@Delimeter,@RowData) 
WHILE (@FoundIndex>0) 
BEGIN 
    INSERT INTO @RtnValue (data) 
    SELECT 
     Data = LTRIM(RTRIM(SUBSTRING(@RowData, 1, @FoundIndex - 1))) 
    SET @RowData = SUBSTRING(@RowData, 
      @FoundIndex + DATALENGTH(@Delimeter)/2, 
      LEN(@RowData)) 
    SET @Iterator = @Iterator + 1 
    SET @FoundIndex = CHARINDEX(@Delimeter, @RowData) 
END 
INSERT INTO @RtnValue (Data) 
SELECT Data = LTRIM(RTRIM(@RowData)) 
RETURN 
END 
+0

Es ist dem Optimierer nicht möglich, die Anzahl der Zeilen zu kennen, da das Verhalten der Funktion und der Wert der Parameter beim Erstellen des Plans nicht analysiert werden können. Ich würde annehmen, dass Ihre beste Wette ist, eine temporäre Tabelle zu verwenden, da es Statistiken hat –

+1

3 Wörter - Tabelle bewerteter Parameter – Hogan

Antwort

0

Wie wäre eine temporäre Tabelle mit?

Begin 
    create table #split (data varchar(255) primary key); 

    insert into #split(data) 
     select data 
     from dbo.fn_Split(@parameters, ','); 

    SELECT * 
    FROM TableA LEFT JOIN 
     Table B 
     on B.ID = A.FID 
    WHERE B.ID IN (SELECT Data FROM #split); 

END; 
+0

Glauben Sie, dass dies auch als CTE funktionieren würde? – Hogan

+1

@Hogan könnte es die gleiche grundlegende Funktionalität bieten, aber es könnte möglicherweise oder nicht so effizient sein. Tabellenvariablen, temporäre Tabellen und CTEs können alle dazu verwendet werden, Daten vorübergehend in der Abfrage zu speichern. Die Art und Weise, in der SQL Server Zeilenschätzungen festlegt, kann jedoch davon abhängen, welche verwendet wird und wie sie verwendet wird. –

0

Apologies necroing eine 6 Monate alte Post, aber ich dachte, die OP noch hören könnte, und es kann andere in der Zukunft helfen.

Ich habe das meiste von dem, was ich in den Kommentaren im Code sagen wollte, dokumentiert. Scheint so, als ob es mehr "insitu" macht.

Bevor wir anfangen, sollten jedoch alle Leistungsansprüche von mir oder irgendjemand sorgfältig geprüft werden, insbesondere ohne codierten Beweis. In diesem Sinne erstellen wir zuerst ein paar Millionen Row-Test-Tabellen, damit wir die Leistung überprüfen und überprüfen können.

Hier ist der Code für die Testtabellen ...

--================================================================================================= 
--  Create and populate the two test tables. 
--  Nothing in this section is a part of the solution. We're just setting up a test. 
--================================================================================================= 
--===== Drop the two test tables to make reruns in SSMS easier 
    IF OBJECT_ID('dbo.TableA','U') IS NOT NULL DROP TABLE dbo.TableA; 
    IF OBJECT_ID('dbo.TableB','U') IS NOT NULL DROP TABLE dbo.TableB; 
GO 
--===== Create the two test tables. I'm assuming some form of index on the 2 columns in question. 
CREATE TABLE dbo.TableA (FID BIGINT NOT NULL PRIMARY KEY); 
CREATE TABLE dbo.TableB (ID BIGINT NOT NULL PRIMARY KEY); 

--===== Populate TableA with a sequence from 1 to a million. 
INSERT INTO dbo.TableA WITH(TABLOCK) 
     (FID) 
SELECT TOP 1000000 
     FID = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
    FROM  sys.all_columns ac1 --CROSS JOIN acts like a looped rowsource (Pseudo-Cursor) 
    CROSS JOIN sys.all_columns ac2 --and uses nothing from these tables except the presence of rows. 
; 
--===== Populate TableB with a sequence from 1 to 10,000 with a couple of pieces missing that 
    -- will appear in @Parameters for testing. 
INSERT INTO dbo.TableB WITH(TABLOCK) 
     (ID) 
SELECT ID = FID 
    FROM dbo.TableA 
    WHERE FID NOT IN (7363,805,34) --We'll include these in @Parameters for testing. 
; 
GO 

Jetzt wollen wir untersuchen, warum der ursprüngliche Code so langsam sein könnte, wenn die DelimitedSplit8K Funktion verwendet wird. Aktivieren Sie den tatsächlichen Ausführungsplan, führen Sie die folgenden Schritte aus und überprüfen Sie die Kommentare im Code, um zu verstehen, warum es selbst bei solchen einfachen und gut indizierten Tabellen fast 3 Sekunden dauert, bis sie ausgeführt werden.

--================================================================================================= 
--  Let's see why the original query using the dbo.DelimitedSplit8K function might be so slow. 
--  After making minor corrections to the original code, here's what we end up with. Run it 
--  with the actual execution plan and discover two things. 
--  1. The query is not SARGable and causes clustered index scans on both tables. 
--  2. Because of the way the query was designed, the TABLE SPOOL is actually the result of a 
--   CROSS JOIN between the 8 elements in the parameters and result of the JOIN between 
--   TableA and TableB. In other words, it has to generate nearly 8 million internal rows in 
--   this case. Imagine if you had 50 elements in your @Parameters. 
-- 
--  Bottom line is that it's not the DelimitedSplit8K function that's slow. It's the way the 
--  optimizer used an iTVF (Inline Table Valued Function) in this case and WHERE IN can 
--  sometimes create a case of "HIDDEN RBAR". 
-- 
--  P.S. The LEFT OUTER JOIN acts as if it where an inner join here because of the limit you've 
--  placed on the RIGHT table with the function. 
--================================================================================================= 
--===== Start measuring stuff. NEVER do this with a SCALAR or mTVF (Multi-statment Table Valued 
    -- function because it will make them seem hundreds of times slower. The proof is too long to 
    -- show here so here's a reference for you. If you don't want to join to read the article 
    -- (not sure you need to anymore), then you'll have to test that on your own. 
    -- http://www.sqlservercentral.com/articles/T-SQL/91724/ "How to make Scalar UDFs Run Faster" 
    SET STATISTICS IO,TIME ON 
; 
--===== Simulate the stored procedure having parameters passed to it. 
DECLARE @Parameters VARCHAR(8000); 
SELECT @Parameters = '8097,345,7363,805,34,8745,13,987654' 
; 
    -- Corrected code from the original post using DelimitedSplit8K instead of fnSplit. 
SELECT * 
    FROM  TableA a 
    LEFT JOIN TableB b ON b.ID = a.FID 
    WHERE b.ID IN (SELECT Item FROM dbo.DelimitedSplit8K(@parameters, ',')) 
; 
--===== Stop measuring stuff. 
    SET STATISTICS IO,TIME OFF 
; 
GO 

Sie können den versehentlichen CROSS JOIN in der "Worktable" in der Statistikausgabe unten sehen. 2.000.054 Lesevorgänge sind 16,38 GB intern erstellte Daten. DAS dauert lange.

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

(5 row(s) affected) 
Table 'TableA'. Scan count 5, logical reads 2126, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'TableB'. Scan count 5, logical reads 2309, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Worktable'. Scan count 4, logical reads 2000054, 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 = 11670 ms, elapsed time = 3163 ms. 

Wie Sie sehen können, iTVFs (Inline-Tabellenwertfunktionen) kann ein wenig pingelig sein. Mal sehen, was passiert, wenn wir es in einem Join verwenden. Und überprüfen Sie die tatsächliche Anzahl der Zeilen im tatsächlichen Ausführungsplan im Vergleich zum ursprünglichen Code oben.

--================================================================================================= 
--  If we use the iTVF (DelimitedSplit8K) in a JOIN instead of in a WHERE, there is no 
--  accidental CROSS JOIN because the results of the function are materialized just once. 
--  The results are returned more than 3100 times faster. 
--================================================================================================= 
--===== Start measuring stuff. 
    SET STATISTICS IO,TIME ON 
; 
--===== Simulate the stored procedure having parameters passed to it. 
DECLARE @Parameters VARCHAR(8000); 
SELECT @Parameters = '8097,345,7363,805,34,8745,13,987654' 
; 
SELECT a.*,b.* 
    FROM dbo.TableA a 
    LEFT JOIN dbo.TableB b 
    ON a.FID = b.ID --I make my ON statements look like the outer join for ease of reading 
    JOIN (SELECT Item FROM dbo.DelimitedSplit8K(@parameters, ',')) s 
    ON b.ID = s.Item 
; 
--===== Stop measuring stuff. 
    SET STATISTICS IO,TIME OFF 
; 
GO 

Hier sind die Ergebnisse für diesen Code. Mehr als 3100-mal schneller und mehr als 42.000-mal weniger E/A in Form von logischen Lesevorgängen (sogar Speicherleistungsvorteile mit dieser Verbesserung um 5 Größenordnungen).

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

(5 row(s) affected) 
Table 'TableA'. Scan count 0, logical reads 19, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'TableB'. Scan count 0, logical reads 28, 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 = 1 ms. 

p.s. Die Schätzungen für die Anzahl der Zeilen, die von jeder der Tabellen zurückgegeben werden, sind immer noch "1", wahrscheinlich weil die Funktion ihre eigene Tally-ähnliche Struktur während des Betriebs erstellt. Nicht sicher, ob es in diesem Fall wichtig ist.