ปัญหาการเพิ่มประสิทธิภาพด้วยฟังก์ชั่นที่ผู้ใช้กำหนด


26

ฉันมีปัญหาในการทำความเข้าใจว่าทำไมเซิร์ฟเวอร์ SQL ตัดสินใจที่จะเรียกใช้ฟังก์ชันที่ผู้ใช้กำหนดสำหรับทุกค่าในตารางแม้ว่าควรดึงข้อมูลได้เพียงหนึ่งแถว SQL จริงมีความซับซ้อนมากขึ้น แต่ฉันสามารถลดปัญหาลงได้ดังนี้

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

สำหรับการสืบค้นนี้ SQL Server ตัดสินใจที่จะเรียกใช้ฟังก์ชัน GetGroupCode สำหรับทุกค่าเดียวที่มีอยู่ในตารางผลิตภัณฑ์แม้ว่าค่าประมาณการและจำนวนแถวที่แท้จริงที่ส่งคืนจาก ORDERLINE คือ 1 (เป็นคีย์หลัก):

แผนแบบสอบถาม

แผนเดียวกันใน explorer แผนแสดงจำนวนแถว:

สำรวจแผน โต๊ะ:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

ดัชนีที่ใช้สำหรับการสแกนคือ:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

ฟังก์ชั่นนั้นซับซ้อนกว่าเล็กน้อย แต่สิ่งเดียวกันเกิดขึ้นกับฟังก์ชั่นหลายตัวจำลองแบบนี้

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

ฉันสามารถ "แก้ไข" ประสิทธิภาพโดยการบังคับให้เซิร์ฟเวอร์ SQL ดึงผลิตภัณฑ์อันดับ 1 สูงสุดถึงแม้ว่า 1 จะเป็นค่าสูงสุดที่เคยพบ:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

จากนั้นรูปร่างของแผนก็เปลี่ยนเป็นสิ่งที่ฉันคาดว่าจะเป็นในตอนแรก:

แบบสอบถามแผนกับด้านบน

ฉันยังว่าดัชนี PRODUCT_FACTORY ที่เล็กกว่าดัชนีคลัสเตอร์ PRODUCT_PK จะมีผลกระทบ แต่ถึงแม้จะบังคับให้เคียวรีใช้ PRODUCT_PK แผนก็ยังคงเหมือนเดิมโดยมี 6655 การเรียกใช้ฟังก์ชัน

ถ้าฉันปล่อย ORDERHDR ออกโดยสมบูรณ์แผนจะเริ่มด้วยลูปซ้อนระหว่าง ORDERLINE และ PRODUCT ก่อนและฟังก์ชันจะเรียกใช้เพียงครั้งเดียว

ฉันต้องการที่จะเข้าใจว่าอะไรเป็นสาเหตุของเรื่องนี้ได้เนื่องจากการดำเนินการทั้งหมดดำเนินการโดยใช้คีย์หลักและวิธีแก้ไขหากเกิดขึ้นในคิวรีที่ซับซ้อนมากขึ้นซึ่งไม่สามารถแก้ไขได้อย่างง่ายดาย

แก้ไข: สร้างคำสั่งตาราง:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

คำตอบ:


30

มีเหตุผลทางเทคนิคหลักสามประการที่คุณจะได้รับตามแผน:

  1. กรอบการคิดต้นทุนของเครื่องมือเพิ่มประสิทธิภาพไม่มีการสนับสนุนจริงสำหรับฟังก์ชั่นที่ไม่ใช่แบบอินไลน์ มันไม่ได้พยายามที่จะมองเข้าไปในนิยามของฟังก์ชั่นเพื่อดูว่ามันอาจมีราคาแพงแค่เพียงกำหนดค่าใช้จ่ายคงที่น้อยมากและประเมินว่าฟังก์ชั่นจะสร้างเอาต์พุต 1 แถวในแต่ละครั้งที่เรียก สมมติฐานการสร้างแบบจำลองทั้งสองนี้มักไม่ปลอดภัยอย่างสมบูรณ์ สถานการณ์ได้รับการปรับปรุงเล็กน้อยในปี 2014 โดยเปิดใช้งานตัวประมาณเชิงการนับใหม่เนื่องจากการเดาแบบ 1 แถวคงที่จะถูกแทนที่ด้วยการเดาแบบ 100 แถวแบบคงที่ อย่างไรก็ตามยังไม่มีการสนับสนุนสำหรับการคิดต้นทุนเนื้อหาของฟังก์ชั่นที่ไม่ใช่แบบอินไลน์
  2. SQL Server เริ่มยุบรวมและนำไปใช้กับการรวมตรรกะภายใน n-ary เดียว สิ่งนี้ช่วยให้เครื่องมือเพิ่มประสิทธิภาพเหตุผลเกี่ยวกับการเข้าร่วมคำสั่งซื้อในภายหลัง การเพิ่มคำสั่งการเข้าร่วมแบบสมัครสมาชิกเข้ามาในภายหลังและขึ้นอยู่กับการวิเคราะห์พฤติกรรมเป็นหลัก ตัวอย่างเช่นการรวมภายในมาก่อนการรวมภายนอกตารางเล็กและการรวมการเลือกก่อนตารางขนาดใหญ่และการรวมการเลือกน้อยและอื่น ๆ
  3. เมื่อ SQL Server ทำการปรับให้เหมาะสมตามต้นทุนมันจะแยกความพยายามออกเป็นขั้นตอนที่เป็นทางเลือกเพื่อลดโอกาสในการใช้แบบสอบถามที่มีต้นทุนต่ำนานเกินไป มีสามขั้นตอนหลักค้นหา 0 ค้นหา 1 และค้นหา 2 แต่ละเฟสมีเงื่อนไขการเข้าและเฟสต่อมาเปิดใช้งานการสำรวจเครื่องมือเพิ่มประสิทธิภาพมากกว่าคนก่อนหน้า ข้อความค้นหาของคุณมีคุณสมบัติเหมาะสมสำหรับขั้นตอนการค้นหาที่มีความสามารถน้อยที่สุดระยะที่ 0 พบว่ามีแผนต้นทุนต่ำพอที่จะไม่เข้าสู่ระยะต่อมา

เนื่องจากมีการประมาณค่าความสำคัญน้อยที่กำหนดให้กับ UDF จึงทำให้การวิเคราะห์การขยายแบบรวมเข้าร่วมนั้นน่าเสียดายที่เปลี่ยนตำแหน่งก่อนหน้านี้ในต้นไม้มากกว่าที่คุณต้องการ

แบบสอบถามยังมีคุณสมบัติสำหรับการค้นหาการเพิ่มประสิทธิภาพ 0 โดยอาศัยการมีอย่างน้อยสามตัว (รวมถึงใช้) แผนทางกายภาพสุดท้ายที่คุณจะได้รับพร้อมกับการสแกนแบบคี่ มีราคาต่ำพอที่เครื่องมือเพิ่มประสิทธิภาพจะพิจารณาแผน "ดีพอ" การประมาณราคาที่ต่ำและความสำคัญสำหรับ UDF นั้นมีส่วนช่วยให้เสร็จเร็วขึ้น

ค้นหา 0 (หรือเรียกอีกอย่างว่าขั้นตอนการประมวลผลธุรกรรม) กำหนดเป้าหมายข้อความค้นหาประเภท low-cardinality OLTP โดยมีแผนขั้นสุดท้ายที่มักจะมีลูปซ้อนกัน ที่สำคัญกว่านั้นการค้นหา 0 ทำงานเพียงส่วนย่อยที่ค่อนข้างเล็กของความสามารถในการสำรวจของเครื่องมือเพิ่มประสิทธิภาพ เซ็ตย่อยนี้ไม่รวมการดึงทรีคิวรีของคิวรีแบบใช้ร่วมในการเข้าร่วม (กฎPullApplyOverJoin) ตรงนี้เป็นสิ่งที่จำเป็นในกรณีทดสอบเพื่อเปลี่ยนตำแหน่ง UDF ที่ใช้เหนือการรวมเพื่อให้ปรากฏครั้งสุดท้ายในลำดับของการดำเนินการ (เหมือนเดิม)

นอกจากนี้ยังมีปัญหาที่เครื่องมือเพิ่มประสิทธิภาพสามารถตัดสินใจระหว่างการเข้าร่วมลูปที่ซ้อนกันแบบไร้เดียงสา (เข้าร่วมเพรดิเคตที่การรวมตัวเอง) และการเข้าร่วมการจัดทำดัชนีที่สัมพันธ์กัน (นำไปใช้) หลังมักจะเป็นรูปร่างแผนที่ต้องการ แต่เครื่องมือเพิ่มประสิทธิภาพมีความสามารถในการสำรวจทั้งสอง ด้วยการคิดต้นทุนและการคาดการณ์ที่ไม่ถูกต้องจึงสามารถเลือกการเข้าร่วม NL ที่ไม่ใช้เช่นเดียวกับในแผนการส่ง (อธิบายการสแกน)

ดังนั้นมีเหตุผลหลายอย่างที่เกี่ยวข้องกับคุณสมบัติเครื่องมือเพิ่มประสิทธิภาพทั่วไปหลายประการที่ทำงานได้ดีในการค้นหาแผนดีในช่วงเวลาสั้น ๆ โดยไม่ต้องใช้ทรัพยากรมากเกินไป การหลีกเลี่ยงสาเหตุใดสาเหตุหนึ่งก็เพียงพอที่จะสร้างรูปร่างแผน 'ที่คาดหวัง' สำหรับแบบสอบถามตัวอย่างแม้จะมีตารางว่างเปล่า:

วางแผนบนตารางว่างโดยปิดใช้งานการค้นหา 0

ไม่มีวิธีที่ได้รับการสนับสนุนเพื่อหลีกเลี่ยงการค้นหาการเลือกแผน 0 การเลิกใช้เครื่องมือเพิ่มประสิทธิภาพก่อนกำหนดหรือเพื่อปรับปรุงการคิดต้นทุนของ UDF (นอกเหนือจากการปรับปรุงแบบ จำกัด ในรูปแบบ SQL Server 2014 CE สำหรับสิ่งนี้) สิ่งนี้ทำให้สิ่งต่าง ๆ เช่นคู่มือวางแผนการเขียนแบบสอบถามด้วยตนเอง (รวมถึงTOP (1)แนวคิดหรือใช้ตารางชั่วคราวกลาง) และหลีกเลี่ยง 'กล่องดำ' ที่มีต้นทุนต่ำ (จากมุมมอง QO) เช่นฟังก์ชั่นที่ไม่ใช่แบบอินไลน์

การเขียนใหม่CROSS APPLYที่OUTER APPLYสามารถทำงานได้เนื่องจากจะป้องกันไม่ให้บางส่วนของการทำงานแบบยุบตัวเร็ว แต่คุณต้องระมัดระวังในการรักษาความหมายของข้อความค้นหาต้นฉบับ (เช่นการปฏิเสธNULLแถวที่เพิ่มขึ้นที่อาจนำมาใช้โดยไม่มีเครื่องมือเพิ่มประสิทธิภาพยุบกลับไป ใช้ข้าม) คุณต้องระวังแม้ว่าพฤติกรรมนี้จะไม่รับประกันว่าจะยังคงมีเสถียรภาพดังนั้นคุณจะต้องจำไว้ว่าให้ทดสอบพฤติกรรมที่สังเกตเห็นดังกล่าวทุกครั้งที่คุณแก้ไขหรืออัพเกรด SQL Server

โดยรวมแล้วทางออกที่เหมาะสมสำหรับคุณขึ้นอยู่กับปัจจัยหลายอย่างที่เราไม่สามารถตัดสินได้สำหรับคุณ อย่างไรก็ตามฉันขอแนะนำให้คุณพิจารณาโซลูชันที่รับประกันว่าจะทำงานได้ในอนาคตเสมอและทำงานร่วมกับเครื่องมือเพิ่มประสิทธิภาพ (แทนที่จะต่อต้าน) ทุกที่ที่ทำได้


24

ดูเหมือนว่านี่เป็นการตัดสินใจตามต้นทุนโดยเครื่องมือเพิ่มประสิทธิภาพ แต่ค่อนข้างดี

หากคุณเพิ่ม 50000 แถวใน PRODUCT เครื่องมือเพิ่มประสิทธิภาพจะคิดว่าการสแกนใช้งานได้มากเกินไปและให้แผนกับการค้นหาสามครั้งและการเรียก UDF หนึ่งครั้ง

แผนการที่ฉันได้รับ 6655 แถวใน PRODUCT

ป้อนคำอธิบายรูปภาพที่นี่

ด้วย 50,000 แถวใน PRODUCT ฉันได้รับแผนนี้แทน

ป้อนคำอธิบายรูปภาพที่นี่

ฉันเดาว่าค่าใช้จ่ายในการโทรหา UDF นั้นต่ำเกินไป

วิธีแก้ปัญหาหนึ่งที่ทำงานได้ดีในกรณีนี้คือเปลี่ยนเคียวรีเพื่อใช้ outer นำไปใช้กับ UDF ฉันได้รับแผนการที่ดีไม่ว่าจะมีกี่แถวในตาราง PRODUCT

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

ป้อนคำอธิบายรูปภาพที่นี่

วิธีแก้ปัญหาที่ดีที่สุดในกรณีของคุณน่าจะได้รับค่าที่คุณต้องการลงในตารางชั่วคราวแล้วทำการค้นหาตารางชั่วคราวด้วยการข้ามที่ใช้กับ UDF ด้วยวิธีนี้คุณมั่นใจว่า UDF จะไม่ถูกดำเนินการเกินความจำเป็น

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

แทนที่จะยังคงอยู่ในตาราง temp คุณสามารถใช้top()ในตารางที่ได้รับมาเพื่อบังคับให้ SQL Server ประเมินผลลัพธ์จากการรวมก่อนที่จะเรียก UDF เพียงใช้จำนวนที่สูงมากในการทำให้ SQL Server ต้องนับแถวของคุณสำหรับส่วนของแบบสอบถามก่อนที่มันจะสามารถใช้งาน UDF ได้

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

ป้อนคำอธิบายรูปภาพที่นี่

ฉันต้องการที่จะเข้าใจว่าอะไรเป็นสาเหตุของเรื่องนี้ได้เนื่องจากการดำเนินการทั้งหมดดำเนินการโดยใช้คีย์หลักและวิธีแก้ไขหากเกิดขึ้นในคิวรีที่ซับซ้อนมากขึ้นซึ่งไม่สามารถแก้ไขได้อย่างง่ายดาย

ฉันไม่สามารถตอบได้ แต่คิดว่าฉันควรแบ่งปันสิ่งที่ฉันรู้อยู่แล้ว ฉันไม่รู้ว่าทำไมการสแกนตาราง PRODUCT ถึงได้รับการพิจารณาเลย อาจมีบางกรณีที่สิ่งที่ดีที่สุดที่ต้องทำและมีบางอย่างเกี่ยวกับวิธีที่เครื่องมือเพิ่มประสิทธิภาพปฏิบัติกับ UDF ที่ฉันไม่รู้

สิ่งที่สังเกตได้อย่างหนึ่งคือแบบสอบถามของคุณได้รับการวางแผนที่ดีใน SQL Server 2014 ด้วยตัวประมาณค่าแบบใหม่ นั่นเป็นเพราะจำนวนแถวโดยประมาณสำหรับการเรียกใช้ UDF แต่ละครั้งคือ 100 แทนที่จะเป็น 1 เนื่องจากอยู่ใน SQL Server 2012 และก่อนหน้า แต่มันจะยังคงทำการตัดสินใจต้นทุนตามเดิมระหว่างเวอร์ชันสแกนและเวอร์ชันค้นหาของแผน ด้วยแถวน้อยกว่า 500 (497 ในกรณีของฉัน) ใน PRODUCT คุณจะได้รับเวอร์ชันการสแกนของแผนแม้ใน SQL Server 2014


2
อย่างใดทำให้ฉันนึกถึงเซสชันของ Adam Machanic ที่ SQL Bits: sqlbits.com/Sessions/Event14/ …
James Z
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.