กำหนดพารามิเตอร์ SQL IN clause


1041

ฉันจะกำหนดพารามิเตอร์การสืบค้นที่มีส่วนINคำสั่งที่มีอาร์กิวเมนต์จำนวนตัวแปรเช่นนี้ได้อย่างไร

SELECT * FROM Tags 
WHERE Name IN ('ruby','rails','scruffy','rubyonrails')
ORDER BY Count DESC

ในแบบสอบถามนี้จำนวนอาร์กิวเมนต์อาจอยู่ที่ใดก็ได้ตั้งแต่ 1 ถึง 5

ฉันไม่ต้องการใช้ขั้นตอนการจัดเก็บเฉพาะสำหรับนี้ (หรือ XML) แต่ถ้ามีบางวิธีที่หรูหราโดยเฉพาะกับSQL Server 2008ฉันเปิดให้


7
สำหรับ MySQL ดูMySQL เตรียมงบที่มีรายชื่อตัวแปรขนาดตัวแปร
outis

ที่คล้ายกัน: ผ่านพารามิเตอร์อาร์เรย์ขั้นตอนการเก็บ , PreparedStatement ในทางเลือกที่เป็นไปตามข้อ
Vadzim

คำตอบ:


316

นี่เป็นเทคนิคที่รวดเร็วและสกปรกที่ฉันใช้:

SELECT * FROM Tags
WHERE '|ruby|rails|scruffy|rubyonrails|'
LIKE '%|' + Name + '|%'

ดังนั้นนี่คือรหัส C #:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
const string cmdText = "select * from tags where '|' + @tags + '|' like '%|' + Name + '|%'";

using (SqlCommand cmd = new SqlCommand(cmdText)) {
   cmd.Parameters.AddWithValue("@tags", string.Join("|", tags);
}

สองประการ:

  • ประสิทธิภาพนั้นแย่มาก LIKE "%...%"แบบสอบถามไม่ได้จัดทำดัชนี
  • ตรวจสอบให้แน่ใจว่าคุณไม่มี|แท็กว่างเปล่าหรือว่างเปล่าไม่เช่นนั้นจะไม่สามารถใช้งานได้

มีวิธีอื่นในการทำสิ่งนี้ให้สำเร็จซึ่งบางคนอาจคิดว่าสะอาดกว่าดังนั้นโปรดอ่านต่อไป


119
นั่นจะช้ากว่าปกติ
Matt Rogish

13
ใช่นี่คือการสแกนตาราง ยอดเยี่ยมสำหรับ 10 แถวหมัด 100,000
Will Hartung

17
ตรวจสอบให้แน่ใจว่าคุณทดสอบกับแท็กที่มีไพพ์อยู่
Joel Coehoorn

17
สิ่งนี้ไม่แม้แต่จะตอบคำถาม ได้รับแล้วมันเป็นเรื่องง่ายที่จะดูว่าจะเพิ่มพารามิเตอร์ที่ไหน แต่คุณจะยอมรับวิธีการแก้ปัญหานี้ได้อย่างไร ดูเหมือนง่ายกว่า @Mark Brackett เนื่องจากไม่มีการกำหนดค่าพารามิเตอร์
tvanfosson

21
เกิดอะไรขึ้นถ้าแท็กของคุณคือ 'ruby | rails' มันจะจับคู่ซึ่งจะผิด เมื่อคุณเปิดตัวโซลูชันดังกล่าวคุณต้องแน่ใจว่าแท็กไม่มีท่อหรือกรองออกอย่างชัดเจน: เลือก * จากแท็กที่ '| ทับทิม | ราง | Scruffy | rubyonrails |' ชอบ '% |' + ชื่อ + '|%' และชื่อไม่เหมือน '%!%'
AK

729

คุณสามารถกำหนดค่าพารามิเตอร์แต่ละค่าได้ดังนี้:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
string cmdText = "SELECT * FROM Tags WHERE Name IN ({0})";

string[] paramNames = tags.Select(
    (s, i) => "@tag" + i.ToString()
).ToArray();

string inClause = string.Join(", ", paramNames);
using (SqlCommand cmd = new SqlCommand(string.Format(cmdText, inClause))) {
    for(int i = 0; i < paramNames.Length; i++) {
       cmd.Parameters.AddWithValue(paramNames[i], tags[i]);
    }
}

ซึ่งจะให้:

cmd.CommandText = "SELECT * FROM Tags WHERE Name IN (@tag0, @tag1, @tag2, @tag3)"
cmd.Parameters["@tag0"] = "ruby"
cmd.Parameters["@tag1"] = "rails"
cmd.Parameters["@tag2"] = "scruffy"
cmd.Parameters["@tag3"] = "rubyonrails"

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

ค่าที่ผู้ใช้ป้อนยังคงถูกยัดไว้ในพารามิเตอร์ดังนั้นจึงไม่มีช่องโหว่

แก้ไข:

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

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

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

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


2
โดยพื้นฐานแล้วเหมือนกับคำตอบของฉันสำหรับคำถาม "ที่เกี่ยวข้อง" และเห็นได้ชัดว่าเป็นทางออกที่ดีที่สุดเพราะมันสร้างสรรค์และมีประสิทธิภาพมากกว่าการตีความ (ยากกว่า)
tvanfosson

49
นี่คือวิธีที่ LINQ ใช้กับ SQL ทำ BTW
Mark Cidade

3
@Pure: จุดทั้งหมดของสิ่งนี้คือการหลีกเลี่ยง SQL Injection ซึ่งคุณอาจเสี่ยงต่อการใช้ SQL แบบไดนามิก
เรย์

4
@God of Data - ใช่ฉันคิดว่าถ้าคุณต้องการมากกว่า 2100 แท็กคุณจะต้องใช้โซลูชันที่แตกต่าง แต่ Basarb สามารถเข้าถึง 2100 ได้หากความยาวแท็กเฉลี่ยอยู่ที่ <3 ตัวอักษร (เนื่องจากคุณต้องการตัวคั่นด้วย) msdn.microsoft.com/en-us/library/ms143432.aspx
Mark Brackett

2
@bonCodigo - ค่าที่คุณเลือกอยู่ในอาร์เรย์ คุณเพียงแค่วนรอบอาร์เรย์และเพิ่มพารามิเตอร์ (ต่อท้ายด้วยดัชนี) สำหรับแต่ละรายการ
Mark Brackett

249

สำหรับ SQL Server 2008 คุณสามารถใช้พารามิเตอร์ที่มีค่าเป็นตารางได้ มันเป็นบิตของการทำงาน แต่มันเป็น arguably สะอาดกว่าวิธีการอื่น ๆ ของฉัน

ก่อนอื่นคุณต้องสร้างประเภท

CREATE TYPE dbo.TagNamesTableType AS TABLE ( Name nvarchar(50) )

จากนั้นรหัส ADO.NET ของคุณจะเป็นดังนี้:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
cmd.CommandText = "SELECT Tags.* FROM Tags JOIN @tagNames as P ON Tags.Name = P.Name";

// value must be IEnumerable<SqlDataRecord>
cmd.Parameters.AddWithValue("@tagNames", tags.AsSqlDataRecord("Name")).SqlDbType = SqlDbType.Structured;
cmd.Parameters["@tagNames"].TypeName = "dbo.TagNamesTableType";

// Extension method for converting IEnumerable<string> to IEnumerable<SqlDataRecord>
public static IEnumerable<SqlDataRecord> AsSqlDataRecord(this IEnumerable<string> values, string columnName) {
    if (values == null || !values.Any()) return null; // Annoying, but SqlClient wants null instead of 0 rows
    var firstRecord = values.First();
    var metadata = SqlMetaData.InferFromValue(firstRecord, columnName);
    return values.Select(v => 
    {
       var r = new SqlDataRecord(metadata);
       r.SetValues(v);
       return r;
    });
}

41
เราทดสอบสิ่งนี้และพารามิเตอร์ที่มีค่าของตารางคือ DOG ช้า มันเร็วกว่าในการรัน 5 เคียวรีมากกว่าการทำ TVP เดียว
Jeff Atwood

4
@JeffAtwood - คุณได้ลองทำการค้นหาใหม่อีกครั้งSELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);หรือไม่ ในทางทฤษฎีสิ่งนี้ควรเป็นวิธีที่เร็วที่สุด คุณสามารถใช้ดัชนีที่เกี่ยวข้อง (เช่นดัชนีชื่อแท็กที่INCLUDEนับได้ว่าเหมาะสมที่สุด) และ SQL Server ควรพยายามค้นหาแท็กและจำนวนทั้งหมด แผนมีลักษณะอย่างไร
Nick Chammas

9
ฉันยังได้ทดสอบสิ่งนี้และมันก็เร็วอย่างที่คิด (เทียบกับการสร้างสตริง IN ขนาดใหญ่) ฉันมีปัญหาบางอย่างในการตั้งค่าพารามิเตอร์ แต่เนื่องจากฉันได้รับ "ไม่สามารถแปลงค่าพารามิเตอร์จาก Int32 [] เป็น IEnumerable`1" ได้ อย่างไรก็ตามแก้ไขได้และนี่คือตัวอย่างที่ฉันทำpastebin.com/qHP05CXc
Fredrik Johansson

6
@FredrikJohansson - จากทั้งหมด 130 คะแนนคุณอาจเป็นคนเดียวที่พยายามใช้งานจริง! ฉันทำผิดพลาดในการอ่านเอกสารและจริง ๆ แล้วคุณจำเป็นต้องมี <sqlDataRecord> IEnumerable ไม่ใช่แค่ IEnumerable ใด ๆ อัปเดตรหัสแล้ว
Mark Brackett

3
@ MarkBrackett ยอดเยี่ยมพร้อมการอัพเดท! รหัสนี้จริง ๆ แล้วบันทึกวันสำหรับฉันเนื่องจากฉันกำลังค้นหาดัชนี Lucene และบางครั้งมันกลับมากกว่าฮิต 50,000 หรือดังนั้นฮิตที่ต้องถูก doublechecked กับเซิร์ฟเวอร์ SQL - ดังนั้นฉันสร้างอาร์เรย์ของ int [] (เอกสาร / คีย์ SQL) แล้วโค้ดข้างต้นมาใน OP ทั้งตอนนี้ใช้เวลาน้อยกว่า 200ms :).
Fredrik Johansson

188

คำถามเดิมคือ"ฉันจะตั้งค่าแบบสอบถามอย่างไร ... "

ขอผมพูดตรงนี้นะว่านี่ไม่ใช่คำตอบของคำถามเดิม มีการสาธิตในคำตอบที่ดีอื่น ๆ อยู่แล้ว

เมื่อทำตามที่กล่าวมาให้ทำเครื่องหมายคำตอบนี้แล้วลงคะแนนทำเครื่องหมายว่าไม่ใช่คำตอบ ... ทำทุกอย่างที่คุณเชื่อว่าถูกต้อง

ดูคำตอบจาก Mark Brackett สำหรับคำตอบที่ต้องการซึ่ง upvoted I (และอีก 231 คน) วิธีการที่ให้ไว้ในคำตอบของเขาช่วยให้ 1) สำหรับการใช้งานตัวแปรที่มีผลผูกพันและ 2) สำหรับเพรดิเคตที่สามารถระบุได้

คำตอบที่เลือก

สิ่งที่ฉันต้องการพูดถึงคือคำตอบของโจเอลสโปลกี้คำตอบ "เลือก" เป็นคำตอบที่ถูกต้อง

วิธีการของ Joel Spolsky นั้นฉลาด และมันก็ใช้งานได้อย่างสมเหตุสมผลมันจะแสดงพฤติกรรมที่สามารถคาดการณ์ได้และประสิทธิภาพที่คาดการณ์ได้ซึ่งให้ค่า "ปกติ" และกรณีขอบเชิงบรรทัดฐานเช่น NULL และสตริงว่าง และอาจเพียงพอสำหรับแอปพลิเคชันเฉพาะ

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

ปัญหาบางอย่างกับตัวอักษร%

'pe%ter'พิจารณาค่าชื่อของ (สำหรับตัวอย่างที่นี่ฉันใช้ค่าสตริงตัวอักษรแทนชื่อคอลัมน์) แถวที่มีค่าชื่อ '`pe% ter' จะถูกส่งกลับโดยแบบสอบถามของฟอร์ม:

select ...
 where '|peanut|butter|' like '%|' + 'pe%ter' + '|%'

แต่แถวเดียวกันนั้นจะไม่ถูกส่งคืนหากลำดับของคำค้นหาถูกย้อนกลับ:

select ...
 where '|butter|peanut|' like '%|' + 'pe%ter' + '|%'

พฤติกรรมที่เราสังเกตเห็นนั้นแปลกมาก การเปลี่ยนลำดับของคำค้นหาในรายการจะเปลี่ยนชุดผลลัพธ์

เกือบจะเป็นไปโดยไม่บอกว่าเราอาจไม่ต้องการpe%terจับคู่เนยถั่วไม่ว่าเขาจะชอบมากแค่ไหนก็ตาม

กรณีมุมปิดบัง

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

การปะรู

วิธีการหนึ่งในการปะแก้หลุมนี้คือการหลบหนี%อักขระตัวแทน (สำหรับคนที่ไม่คุ้นเคยกับประโยคหนีกับผู้ประกอบการที่นี่เป็นเชื่อมโยงไปยังเอกสาร SQL Server

select ...
 where '|peanut|butter|'
  like '%|' + 'pe\%ter' + '|%' escape '\'

ตอนนี้เราสามารถจับคู่% ตัวอักษร แน่นอนว่าเมื่อเรามีชื่อคอลัมน์เราจะต้องหลีกเลี่ยงสัญลักษณ์แทน เราสามารถใช้REPLACEฟังก์ชันเพื่อค้นหา%อักขระและแทรกอักขระเครื่องหมายทับขวาหน้าอักขระแต่ละตัวดังนี้

select ...
 where '|pe%ter|'
  like '%|' + REPLACE( 'pe%ter' ,'%','\%') + '|%' escape '\'

เพื่อแก้ปัญหาด้วย% wildcard เกือบจะ

หลบหนีการหลบหนี

เราตระหนักดีว่าการแก้ปัญหาของเราได้แนะนำปัญหาอื่น ตัวละครหนี เราเห็นว่าเราจะต้องหลบหนีจากการหลบหนีของตัวละครด้วย คราวนี้เราใช้! เป็นตัวละครหนี:

select ...
 where '|pe%t!r|'
  like '%|' + REPLACE(REPLACE( 'pe%t!r' ,'!','!!'),'%','!%') + '|%' escape '!'

ขีดล่างด้วย

ตอนนี้เรากำลังอยู่ในระหว่างการหมุนเราสามารถเพิ่มREPLACEหมายเลขอ้างอิงอื่นที่เป็นเครื่องหมายขีดล่าง และเพื่อความสนุกในครั้งนี้เราจะใช้ $ เป็นตัวละครในการหลบหนี

select ...
 where '|p_%t!r|'
  like '%|' + REPLACE(REPLACE(REPLACE( 'p_%t!r' ,'$','$$'),'%','$%'),'_','$_') + '|%' escape '$'

ฉันชอบวิธีนี้ในการหลบหนีเพราะมันทำงานใน Oracle และ MySQL รวมถึง SQL Server (ฉันมักจะใช้ \ backslash เป็นตัวละครหนีเนื่องจากเป็นตัวละครที่เราใช้ในการแสดงออกปกติ แต่ทำไมต้องถูก จำกัด ด้วยการประชุม!

วงเล็บที่น่ารำคาญ

SQL Server []ยังช่วยให้อักขระตัวแทนที่จะถือว่าเป็นตัวอักษรโดยแนบไว้ในวงเล็บ ดังนั้นเรายังไม่ได้ทำการแก้ไขอย่างน้อยสำหรับ SQL Server เนื่องจากคู่ของวงเล็บมีความหมายพิเศษเราจะต้องหลบหนีเช่นกัน หากเราจัดการเพื่อหลบหนีวงเล็บอย่างถูกต้องอย่างน้อยที่สุดเราก็ไม่ต้องกังวลกับเครื่องหมายยัติภังค์-และกะรัต^ภายในวงเล็บ และเราสามารถปล่อยให้สิ่งใด%และ_ตัวละครในวงเล็บหนีเพราะเราจะปิดการใช้งานความหมายพิเศษของวงเล็บ

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

การแสดงออกแบบอินไลน์ยุ่งเหยิง

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

ฟังก์ชั่นอยู่ที่ไหน

เอาล่ะดังนั้นหากเราไม่จัดการกับสิ่งนั้นในรูปแบบอินไลน์ใน SQL ทางเลือกที่ใกล้เคียงที่สุดที่เรามีคือฟังก์ชั่นที่ผู้ใช้กำหนด และเรารู้ว่าจะไม่เร่งความเร็วสิ่งใด ๆ (เว้นแต่ว่าเราจะสามารถกำหนดดัชนีบนมันอย่างที่เราสามารถทำได้กับ Oracle) หากเราต้องสร้างฟังก์ชั่นเราควรทำอย่างนั้นในรหัสที่เรียก SQL คำให้การ.

และฟังก์ชั่นนั้นอาจมีความแตกต่างในพฤติกรรมขึ้นอยู่กับ DBMS และเวอร์ชั่น (ตะโกนออกมาให้กับนักพัฒนา Java ทั้งหมดที่คุณมีความสามารถในการใช้โปรแกรมฐานข้อมูลใด ๆ แทนกันได้)

ความรู้เกี่ยวกับโดเมน

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

ค่าที่เก็บไว้ในคอลัมน์อาจอนุญาตให้ใช้สำหรับ% หรือ _ ตัวอักษร แต่ข้อ จำกัด อาจต้องการค่าเหล่านั้นที่จะหลบหนีบางทีอาจใช้ตัวละครที่กำหนดไว้เช่นค่าที่เปรียบเทียบ LIKE "ปลอดภัย" อีกครั้งความคิดเห็นอย่างรวดเร็วเกี่ยวกับชุดของค่าที่อนุญาตและโดยเฉพาะอย่างยิ่งตัวละครที่จะใช้เป็นตัวละครหนีและไปกับวิธีการ Joel Spolsky

แต่หากขาดความรู้เฉพาะทางและการรับประกันเป็นสิ่งสำคัญที่เราจะต้องพิจารณาการจัดการกรณีมุมที่คลุมเครือและพิจารณาว่าพฤติกรรมนั้นมีเหตุผลและ "ตามข้อกำหนด"


ปัญหาอื่น ๆ ที่ recapitulated

ฉันเชื่อว่าคนอื่น ๆ ได้ชี้ให้เห็นอย่างเพียงพอแล้วในเรื่องอื่น ๆ ที่เป็นข้อกังวล:

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

  • แผนเครื่องมือเพิ่มประสิทธิภาพโดยใช้การสแกนดัชนีแทนที่จะค้นหาดัชนีความต้องการที่เป็นไปได้สำหรับการแสดงออกหรือฟังก์ชั่นสำหรับการหลบหนีสัญลักษณ์ (ดัชนีที่เป็นไปได้ในการแสดงออกหรือฟังก์ชั่น)

  • การใช้ค่าตามตัวอักษรแทนตัวแปรการเชื่อมโยงส่งผลกระทบต่อความยืดหยุ่น


ข้อสรุป

ฉันชอบแนวทางของ Joel Spolsky มันฉลาด และมันใช้งานได้

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

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


คุณช่วยบอกให้เราทราบได้ไหมถ้าคุณใช้หรือชอบแบบสอบถามที่มีพารามิเตอร์ ในกรณีนี้มันถูกต้องหรือไม่ที่จะข้ามไปตามกฎของ 'ใช้การสืบค้นแบบกำหนดพารามิเตอร์' และฆ่าเชื้อด้วยภาษาดั้งเดิม? ขอบคุณมาก
Luis Siquot

2
@ Luis: ใช่ฉันชอบการใช้ตัวแปรผูกในงบ SQL และจะหลีกเลี่ยงตัวแปรผูกเมื่อใช้พวกเขาทำให้เกิดปัญหาประสิทธิภาพ รูปแบบเชิงบรรทัดฐานของฉันสำหรับปัญหาดั้งเดิมคือการสร้างคำสั่ง SQL แบบไดนามิกด้วยจำนวนตัวยึดตำแหน่งที่ต้องการในรายการ IN จากนั้นผูกค่าแต่ละค่ากับหนึ่งในตัวยึดตำแหน่ง ดูคำตอบจาก Mark Brackett ซึ่งเป็นคำตอบที่ฉันยกขึ้น (และอีก 231 คน)
spencer7593

133

คุณสามารถส่งพารามิเตอร์เป็นสตริงได้

ดังนั้นคุณมีสตริง

DECLARE @tags

SET @tags = ruby|rails|scruffy|rubyonrails

select * from Tags 
where Name in (SELECT item from fnSplit(@tags, ‘|’))
order by Count desc

จากนั้นสิ่งที่คุณต้องทำคือส่งสตริงเป็นพารามิเตอร์ 1 ตัว

นี่คือฟังก์ชั่นแยกที่ฉันใช้

CREATE FUNCTION [dbo].[fnSplit](
    @sInputList VARCHAR(8000) -- List of delimited items
  , @sDelimiter VARCHAR(8000) = ',' -- delimiter that separates items
) RETURNS @List TABLE (item VARCHAR(8000))

BEGIN
DECLARE @sItem VARCHAR(8000)
WHILE CHARINDEX(@sDelimiter,@sInputList,0) <> 0
 BEGIN
 SELECT
  @sItem=RTRIM(LTRIM(SUBSTRING(@sInputList,1,CHARINDEX(@sDelimiter,@sInputList,0)-1))),
  @sInputList=RTRIM(LTRIM(SUBSTRING(@sInputList,CHARINDEX(@sDelimiter,@sInputList,0)+LEN(@sDelimiter),LEN(@sInputList))))

 IF LEN(@sItem) > 0
  INSERT INTO @List SELECT @sItem
 END

IF LEN(@sInputList) > 0
 INSERT INTO @List SELECT @sInputList -- Put the last item in
RETURN
END

2
คุณยังสามารถเข้าร่วมฟังก์ชั่นตารางได้ด้วยวิธีการนี้
Michael Haren

ฉันใช้โซลูชันที่คล้ายกับสิ่งนี้ใน Oracle ไม่จำเป็นต้องวิเคราะห์คำซ้ำตามวิธีการแก้ปัญหาอื่น ๆ
Leigh Riffel

9
นี่คือวิธีการฐานข้อมูลที่บริสุทธิ์ที่คนอื่นต้องการทำงานในรหัสที่อยู่นอกฐานข้อมูล
David Basarab

การสแกนแบบตารางนี้หรือใช้ประโยชน์จากดัชนีเป็นต้นหรือไม่
Pure.Krome

ดีกว่าจะใช้ CROSS ใช้กับฟังก์ชั่นตาราง SQL (อย่างน้อยในปี 2005 เป็นต้นไป) ซึ่งเป็นหลักรวมกับตารางที่ส่งคืน
adolf กระเทียม

66

ฉันได้ยินเจฟฟ์ / โจเอลคุยเรื่องนี้ในพอดคาสต์วันนี้ ( ตอนที่ 34 , 2008-12-16 (MP3, 31 MB), 1 ชั่วโมง 03 นาที 38 วินาที - 1 ชั่วโมง 06 นาที 45 วินาที) และฉันคิดว่าฉันจำได้ว่า Stack Overflow ใช้LINQ กับ SQLแต่อาจถูกทิ้งไว้ นี่คือสิ่งเดียวกันใน LINQ กับ SQL

var inValues = new [] { "ruby","rails","scruffy","rubyonrails" };

var results = from tag in Tags
              where inValues.Contains(tag.Name)
              select tag;

แค่นั้นแหละ. และใช่ LINQ มองย้อนกลับไปแล้วพอสมควร แต่Containsประโยคดูเหมือนย้อนกลับเป็นพิเศษสำหรับฉัน เมื่อฉันต้องทำแบบสอบถามที่คล้ายกันสำหรับโครงการในที่ทำงานฉันพยายามทำสิ่งที่ผิดโดยการเข้าร่วมระหว่างอาร์เรย์ในเครื่องและตาราง SQL Server การหา LINQ ไปยัง SQL นักแปลจะฉลาดพอที่จะจัดการ แปลอย่างใด มันไม่ได้ แต่มันไม่ให้เกิดข้อผิดพลาดที่เป็นพรรณนาและชี้ให้ฉันที่มีต่อการใช้ประกอบด้วย

อย่างไรก็ตามถ้าคุณเรียกใช้สิ่งนี้ในLINQPad ที่แนะนำอย่างยิ่งและเรียกใช้แบบสอบถามนี้คุณสามารถดู SQL จริงที่ผู้ให้บริการ SQL LINQ สร้างขึ้น มันจะแสดงให้คุณเห็นแต่ละค่าที่ได้รับพารามิเตอร์ในINประโยค


50

หากคุณโทรจาก. NET คุณสามารถใช้Dapper dot net :

string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"};
var tags = dataContext.Query<Tags>(@"
select * from Tags 
where Name in @names
order by Count desc", new {names});

ที่นี่ Dapper ใช้ความคิดดังนั้นคุณไม่จำเป็นต้องทำ สิ่งที่คล้ายกันเป็นไปได้กับLINQ กับ SQLแน่นอน:

string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"};
var tags = from tag in dataContext.Tags
           where names.Contains(tag.Name)
           orderby tag.Count descending
           select tag;

11
ซึ่งเกิดขึ้นกับสิ่งที่เราใช้ในหน้านี้สำหรับคำถามจริงที่ถาม ( dapper
Sam Saffron


สิ่งนี้จะล้มเหลวหากชื่อยาว
cs0815

29

นี่อาจเป็นวิธีที่น่ารังเกียจครึ่งหนึ่งในการทำฉันใช้ครั้งเดียวก็ค่อนข้างมีประสิทธิภาพ

อาจมีการใช้งานทั้งนี้ขึ้นอยู่กับเป้าหมายของคุณ

  1. สร้างตารางชั่วคราวด้วยหนึ่งคอลัมน์
  2. INSERT แต่ละค่าการค้นหาในคอลัมน์นั้น
  3. แทนที่จะใช้กฎINคุณสามารถใช้JOINกฎมาตรฐานของคุณได้ (ความยืดหยุ่น ++)

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

ฉันไม่เคยมีโอกาสทำโปรไฟล์อย่างรวดเร็วแต่ในสถานการณ์ของฉันมันต้องการ


มันไม่น่ารังเกียจเลย! ยิ่งไปกว่านั้น IMHO เป็นวิธีที่สะอาดมาก และถ้าคุณดูแผนการดำเนินการคุณจะเห็นว่ามันเป็นเหมือนคำสั่ง IN แทนที่จะเป็นตารางชั่วคราวคุณสามารถสร้างตารางคงที่พร้อมดัชนีซึ่งคุณเก็บพารามิเตอร์ไว้พร้อมกับ SESSIONID
ตำรวจ SQL

27

ในSQL Server 2016+คุณสามารถใช้STRING_SPLITฟังก์ชั่น:

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails';

SELECT * 
FROM Tags
WHERE Name IN (SELECT [value] FROM STRING_SPLIT(@names, ','))
ORDER BY [Count] DESC;

หรือ:

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails';

SELECT t.*
FROM Tags t
JOIN STRING_SPLIT(@names,',')
  ON t.Name = [value]
ORDER BY [Count] DESC;

LiveDemo

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

E. ค้นหาแถวตามรายการค่า

สิ่งนี้จะแทนที่รูปแบบการต่อต้านทั่วไปเช่นการสร้างสตริง SQL แบบไดนามิกในชั้นแอปพลิเคชันหรือ Transact-SQL หรือโดยใช้ตัวดำเนินการ LIKE:

SELECT ProductId, Name, Tags
FROM Product
WHERE ',1,2,3,' LIKE '%,' + CAST(ProductId AS VARCHAR(20)) + ',%';

ภาคผนวก :

เพื่อปรับปรุงการSTRING_SPLITประมาณแถวฟังก์ชั่นของตารางมันเป็นความคิดที่ดีที่จะทำให้ค่าที่แยกเป็นจริงเป็นตัวแปรตาราง / ตารางชั่วคราว:

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails,sql';

CREATE TABLE #t(val NVARCHAR(120));
INSERT INTO #t(val) SELECT s.[value] FROM STRING_SPLIT(@names, ',') s;

SELECT *
FROM Tags tg
JOIN #t t
  ON t.val = tg.TagName
ORDER BY [Count] DESC;

SEDE - การสาธิตสด

ที่เกี่ยวข้อง: วิธีการส่งรายการค่าในขั้นตอนการจัดเก็บ


SQL Server 2008คำถามเดิมมีความต้องการ เนื่องจากคำถามนี้มักถูกใช้ซ้ำซ้อนฉันจึงได้เพิ่มคำตอบนี้ไว้เป็นข้อมูลอ้างอิง


1
ฉันไม่ได้ทำการทดสอบอย่างสมบูรณ์แบบ แต่ฉันรู้สึกว่านี่เป็นวิธีที่สะอาดที่สุดในปี 2016+ ฉันยังคงชอบที่จะสามารถเพียงแค่ผ่านอาร์เรย์ของ int แต่จนแล้ว ...
แดเนียล

24

เรามีฟังก์ชั่นที่สร้างตัวแปรตารางที่คุณสามารถเข้าร่วม:

ALTER FUNCTION [dbo].[Fn_sqllist_to_table](@list  AS VARCHAR(8000),
                                           @delim AS VARCHAR(10))
RETURNS @listTable TABLE(
  Position INT,
  Value    VARCHAR(8000))
AS
  BEGIN
      DECLARE @myPos INT

      SET @myPos = 1

      WHILE Charindex(@delim, @list) > 0
        BEGIN
            INSERT INTO @listTable
                        (Position,Value)
            VALUES     (@myPos,LEFT(@list, Charindex(@delim, @list) - 1))

            SET @myPos = @myPos + 1

            IF Charindex(@delim, @list) = Len(@list)
              INSERT INTO @listTable
                          (Position,Value)
              VALUES     (@myPos,'')

            SET @list = RIGHT(@list, Len(@list) - Charindex(@delim, @list))
        END

      IF Len(@list) > 0
        INSERT INTO @listTable
                    (Position,Value)
        VALUES     (@myPos,@list)

      RETURN
  END 

ดังนั้น:

@Name varchar(8000) = null // parameter for search values    

select * from Tags 
where Name in (SELECT value From fn_sqllist_to_table(@Name,',')))
order by Count desc

20

นี่คือขั้นต้น แต่ถ้าคุณรับประกันว่าจะมีอย่างน้อยหนึ่งคุณสามารถทำ:

SELECT ...
       ...
 WHERE tag IN( @tag1, ISNULL( @tag2, @tag1 ), ISNULL( @tag3, @tag1 ), etc. )

การมี IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') จะถูกปรับแต่งโดย SQL Server นอกจากนี้คุณจะได้รับการค้นหาดัชนีโดยตรง


1
พารามิเตอร์ทางเลือกที่มี Null ตรวจสอบประสิทธิภาพการทำงานเสียเนื่องจากเครื่องมือเพิ่มประสิทธิภาพต้องใช้จำนวนพารามิเตอร์ที่ใช้ในการสร้างแบบสอบถามที่มีประสิทธิภาพ แบบสอบถามสำหรับ 5 พารามิเตอร์อาจต้องการแผนแบบสอบถามแตกต่างจากหนึ่งสำหรับ 500 พารามิเตอร์
Erik Hart

18

ในความคิดของฉันแหล่งที่ดีที่สุดในการแก้ปัญหานี้คือสิ่งที่โพสต์บนเว็บไซต์นี้:

Syscomments Dinakar Nethi

CREATE FUNCTION dbo.fnParseArray (@Array VARCHAR(1000),@separator CHAR(1))
RETURNS @T Table (col1 varchar(50))
AS 
BEGIN
 --DECLARE @T Table (col1 varchar(50))  
 -- @Array is the array we wish to parse
 -- @Separator is the separator charactor such as a comma
 DECLARE @separator_position INT -- This is used to locate each separator character
 DECLARE @array_value VARCHAR(1000) -- this holds each array value as it is returned
 -- For my loop to work I need an extra separator at the end. I always look to the
 -- left of the separator character for each array value

 SET @array = @array + @separator

 -- Loop through the string searching for separtor characters
 WHILE PATINDEX('%' + @separator + '%', @array) <> 0 
 BEGIN
    -- patindex matches the a pattern against a string
    SELECT @separator_position = PATINDEX('%' + @separator + '%',@array)
    SELECT @array_value = LEFT(@array, @separator_position - 1)
    -- This is where you process the values passed.
    INSERT into @T VALUES (@array_value)    
    -- Replace this select statement with your processing
    -- @array_value holds the value of this element of the array
    -- This replaces what we just processed with and empty string
    SELECT @array = STUFF(@array, 1, @separator_position, '')
 END
 RETURN 
END

ใช้:

SELECT * FROM dbo.fnParseArray('a,b,c,d,e,f', ',')

เครดิตสำหรับ: Dinakar Nethi


คำตอบที่ยอดเยี่ยมสะอาดและแยกส่วนการดำเนินการที่รวดเร็วเป็นพิเศษยกเว้นการแยกวิเคราะห์ CSV เริ่มต้นลงในตาราง (องค์ประกอบจำนวนน้อยครั้งเดียว) แม้ว่าจะสามารถใช้ charindex ที่ง่ายกว่า / เร็วกว่าแทน Patindex ได้ไหม? Charindex () อนุญาตให้ใช้อาร์กิวเมนต์ 'start_location' ซึ่งอาจสามารถหลีกเลี่ยงการตัดสตริงอินพุตแต่ละ iter ได้? เพื่อตอบคำถามเดิมสามารถเข้าร่วมกับผลการทำงานได้
crokusek

18

ฉันจะส่งพารามิเตอร์ประเภทตาราง (เนื่องจากเป็นSQL Server 2008 ) และwhere existsเข้าร่วมหรือเข้าร่วม คุณอาจใช้ XML การใช้sp_xml_preparedocumentแล้วแม้แต่ดัชนีดัชนีตารางชั่วคราวนั้น


คำตอบของ Ph.E มีตัวอย่างการสร้างตาราง temp (จาก csv)
crokusek

12

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

ใน (ct1, ct2, ct3 ... ctn)

เป็น

ใน (เลือก ... )

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

น่าเสียดายที่เทคนิคการแบ่งสตริงนั้นค่อนข้างเฉพาะผลิตภัณฑ์ นี่คือรุ่นของ SQL Server:

 with qry(n, names) as
       (select len(list.names) - len(replace(list.names, ',', '')) - 1 as n,
               substring(list.names, 2, len(list.names)) as names
        from (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' names) as list
        union all
        select (n - 1) as n,
               substring(names, 1 + charindex(',', names), len(names)) as names
        from qry
        where n > 1)
 select n, substring(names, 1, charindex(',', names) - 1) dwarf
 from qry;

รุ่น Oracle:

 select n, substr(name, 1, instr(name, ',') - 1) dwarf
 from (select n,
             substr(val, 1 + instr(val, ',', 1, n)) name
      from (select rownum as n,
                   list.val
            from  (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' val
                   from dual) list
            connect by level < length(list.val) -
                               length(replace(list.val, ',', ''))));

และรุ่น MySQL:

select pivot.n,
      substring_index(substring_index(list.val, ',', 1 + pivot.n), ',', -1) from (select 1 as n
     union all
     select 2 as n
     union all
     select 3 as n
     union all
     select 4 as n
     union all
     select 5 as n
     union all
     select 6 as n
     union all
     select 7 as n
     union all
     select 8 as n
     union all
     select 9 as n
     union all
     select 10 as n) pivot,    (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' val) as list where pivot.n <  length(list.val) -
                   length(replace(list.val, ',', ''));

(แน่นอน "pivot" จะต้องส่งคืนแถวได้มากเท่าจำนวนสูงสุดของรายการที่เราสามารถหาได้ในรายการ)


11

หากคุณมีSQL Server 2008หรือหลังจากนั้นผมจะใช้ตารางมูลค่าพารามิเตอร์

หากคุณโชคร้ายพอที่จะติดอยู่บนSQL Server 2005คุณสามารถเพิ่มฟังก์ชันCLRเช่นนี้

[SqlFunction(
    DataAccessKind.None,
    IsDeterministic = true,
    SystemDataAccess = SystemDataAccessKind.None,
    IsPrecise = true,
    FillRowMethodName = "SplitFillRow",
    TableDefinintion = "s NVARCHAR(MAX)"]
public static IEnumerable Split(SqlChars seperator, SqlString s)
{
    if (s.IsNull)
        return new string[0];

    return s.ToString().Split(seperator.Buffer);
}

public static void SplitFillRow(object row, out SqlString s)
{
    s = new SqlString(row.ToString());
}

ซึ่งคุณสามารถใช้แบบนี้

declare @desiredTags nvarchar(MAX);
set @desiredTags = 'ruby,rails,scruffy,rubyonrails';

select * from Tags
where Name in [dbo].[Split] (',', @desiredTags)
order by Count desc

10

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

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

ฉันได้เห็นขั้นตอนการจัดเก็บที่มีพารามิเตอร์ 500 ด้วยค่าเริ่มต้นเป็นโมฆะและมี WHERE Column1 IN (@ Param1, @ Param2, @ Param3, ... , @ Param500) สิ่งนี้ทำให้ SQL สร้างตารางชั่วคราวทำการเรียงลำดับ / แตกต่างจากนั้นทำการสแกนตารางแทนการค้นหาดัชนี นั่นคือสิ่งที่คุณต้องทำโดยการกำหนดพารามิเตอร์การสืบค้นแม้ว่าจะมีขนาดเล็กพอที่จะไม่ทำให้เกิดความแตกต่างที่เห็นได้ชัดเจน ฉันขอแนะนำไม่ให้มีค่า NULL ในรายการ IN ของคุณราวกับว่าสิ่งนั้นได้รับการเปลี่ยนเป็นแบบไม่อยู่ในนั้น คุณสามารถสร้างรายการพารามิเตอร์แบบไดนามิก แต่สิ่งเดียวที่ชัดเจนที่คุณจะได้รับคือวัตถุจะหนีจากคำพูดเดียวสำหรับคุณ วิธีการนั้นก็ช้าลงเล็กน้อยในส่วนท้ายของแอปพลิเคชันเนื่องจากวัตถุจะต้องแยกวิเคราะห์แบบสอบถามเพื่อค้นหาพารามิเตอร์

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

หน้าผาบันทึก:

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

คำสั่งเคส / คำสั่งง่าย ๆ ของคุณด้วยพารามิเตอร์บางอย่าง:

Dynamic SQL อาจมีพารามิเตอร์หากการทดสอบแสดงประสิทธิภาพที่ดีขึ้น

แบบสอบถามที่มีแผนการดำเนินการที่นำมาใช้ซ้ำได้ซึ่งเรียกหลาย ๆ ครั้งโดยเพียงแค่เปลี่ยนพารามิเตอร์หรือหากแบบสอบถามมีความซับซ้อน:

SQL พร้อมพารามิเตอร์แบบไดนามิก

ข้อความค้นหาที่มีรายการขนาดใหญ่:

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


คุณหมายถึงอะไรโดย "ขั้นตอนการจัดเก็บ" ที่นี่? คุณช่วยโพสต์ตัวอย่างได้ไหม?
struhtanov

9

อาจเป็นเราสามารถใช้ XML ที่นี่:

    declare @x xml
    set @x='<items>
    <item myvalue="29790" />
    <item myvalue="31250" />
    </items>
    ';
    With CTE AS (
         SELECT 
            x.item.value('@myvalue[1]', 'decimal') AS myvalue
        FROM @x.nodes('//items/item') AS x(item) )

    select * from YourTable where tableColumnName in (select myvalue from cte)

1
CTEและ@xสามารถยกเลิก / inlined เข้า subselect ถ้าทำอย่างระมัดระวังตามที่แสดงในบทความนี้
robert4

9

ฉันจะเข้าหาสิ่งนี้โดยค่าเริ่มต้นด้วยการส่งผ่านฟังก์ชั่นที่มีค่าของตาราง (ที่ส่งคืนตารางจากสตริง) ไปยังเงื่อนไข IN

นี่คือรหัสสำหรับ UDF (ฉันได้มาจาก Stack Overflow ที่ไหนสักแห่งฉันไม่สามารถหาแหล่งที่มาได้ในขณะนี้)

CREATE FUNCTION [dbo].[Split] (@sep char(1), @s varchar(8000))
RETURNS table
AS
RETURN (
    WITH Pieces(pn, start, stop) AS (
      SELECT 1, 1, CHARINDEX(@sep, @s)
      UNION ALL
      SELECT pn + 1, stop + 1, CHARINDEX(@sep, @s, stop + 1)
      FROM Pieces
      WHERE stop > 0
    )
    SELECT 
      SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s
    FROM Pieces
  )

เมื่อคุณได้รับรหัสของคุณแล้วก็จะง่ายเหมือนนี้

select * from Tags 
where Name in (select s from dbo.split(';','ruby;rails;scruffy;rubyonrails'))
order by Count desc

ถ้าคุณไม่มีสายยาวที่น่าขันนี่น่าจะทำงานได้ดีกับดัชนีตาราง

หากจำเป็นคุณสามารถแทรกลงในตารางชั่วคราวสร้างดัชนีจากนั้นเรียกใช้การเข้าร่วม ...


8

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

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0

8

ใช้ขั้นตอนการจัดเก็บต่อไปนี้ จะใช้ฟังก์ชั่นแยกที่กำหนดเองซึ่งสามารถพบได้ที่นี่

 create stored procedure GetSearchMachingTagNames 
    @PipeDelimitedTagNames varchar(max), 
    @delimiter char(1) 
    as  
    begin
         select * from Tags 
         where Name in (select data from [dbo].[Split](@PipeDelimitedTagNames,@delimiter) 
    end

8

หากเรามีสตริงที่เก็บไว้ในส่วนคำสั่ง IN ด้วยเครื่องหมายจุลภาค (,) ที่คั่นด้วยเราสามารถใช้ฟังก์ชัน charindex เพื่อรับค่า ถ้าคุณใช้. NET คุณสามารถแมปด้วย SqlParameters

สคริปต์ DDL:

CREATE TABLE Tags
    ([ID] int, [Name] varchar(20))
;

INSERT INTO Tags
    ([ID], [Name])
VALUES
    (1, 'ruby'),
    (2, 'rails'),
    (3, 'scruffy'),
    (4, 'rubyonrails')
;

T-SQL:

DECLARE @Param nvarchar(max)

SET @Param = 'ruby,rails,scruffy,rubyonrails'

SELECT * FROM Tags
WHERE CharIndex(Name,@Param)>0

คุณสามารถใช้คำสั่งด้านบนในรหัส. NET ของคุณและแมปพารามิเตอร์กับ SqlParameter

พู้ทำเล่นสาธิต

แก้ไข: สร้างตารางที่ชื่อว่า SelectedTags โดยใช้สคริปต์ต่อไปนี้

สคริปต์ DDL:

Create table SelectedTags
(Name nvarchar(20));

INSERT INTO SelectedTags values ('ruby'),('rails')

T-SQL:

DECLARE @list nvarchar(max)
SELECT @list=coalesce(@list+',','')+st.Name FROM SelectedTags st

SELECT * FROM Tags
WHERE CharIndex(Name,@Param)>0

คุณสามารถแสดงตัวอย่างของการทำงานนี้โดยที่ไม่มีรายการค่าตายตัวที่เป็นไปได้หรือไม่
John Saunders

@ JohnSaunders ฉันได้แก้ไขสคริปต์โดยไม่ต้องใช้รายการ hardcoded ใด ๆ กรุณายืนยัน.
Gowdhaman008

3
ข้อ จำกัด หนึ่งอย่างสำหรับตัวเลือกนี้ CharIndex ส่งคืน 1 ถ้าพบสตริง IN ส่งคืนการจับคู่สำหรับคำศัพท์ที่แน่นอน CharIndex สำหรับ "Stack" จะส่งคืน 1 สำหรับคำว่า "StackOverflow" IN จะไม่ มีคำตอบเล็กน้อยสำหรับคำตอบนี้โดยใช้ PatIndex ด้านบนที่ล้อมรอบชื่อด้วย '<'% name% '>' ที่เอาชนะข้อ จำกัด นี้ วิธีแก้ปัญหาที่สร้างสรรค์สำหรับปัญหานี้
Richard Vivian

7

สำหรับตัวแปรจำนวนอาร์กิวเมนต์เช่นนี้วิธีเดียวที่ฉันรู้คือสร้าง SQL อย่างชัดเจนหรือทำบางสิ่งที่เกี่ยวข้องกับการสร้างตารางชั่วคราวด้วยรายการที่คุณต้องการและเข้าร่วมกับตารางชั่วคราว


7

ในColdFusionเราเพิ่งทำ:

<cfset myvalues = "ruby|rails|scruffy|rubyonrails">
    <cfquery name="q">
        select * from sometable where values in <cfqueryparam value="#myvalues#" list="true">
    </cfquery>

7

นี่เป็นเทคนิคที่สร้างตารางท้องถิ่นที่จะใช้ในสตริงการสืบค้น การทำเช่นนี้จะช่วยขจัดปัญหาการแยกวิเคราะห์ทั้งหมด

สตริงสามารถสร้างได้ในภาษาใด ๆ ในตัวอย่างนี้ฉันใช้ SQL เนื่องจากเป็นปัญหาเดิมที่ฉันพยายามแก้ไข ฉันต้องการวิธีการที่สะอาดในการส่งผ่านข้อมูลตารางในการบินในสตริงเพื่อดำเนินการในภายหลัง

การใช้ประเภทที่ผู้ใช้กำหนดเป็นตัวเลือก การสร้างประเภทถูกสร้างขึ้นเพียงครั้งเดียวและสามารถทำได้ล่วงหน้า มิฉะนั้นเพียงแค่เพิ่มประเภทตารางแบบเต็มให้กับการประกาศในสตริง

รูปแบบทั่วไปง่ายต่อการขยายและสามารถใช้สำหรับการส่งผ่านตารางที่ซับซ้อนมากขึ้น

-- Create a user defined type for the list.
CREATE TYPE [dbo].[StringList] AS TABLE(
    [StringValue] [nvarchar](max) NOT NULL
)

-- Create a sample list using the list table type.
DECLARE @list [dbo].[StringList]; 
INSERT INTO @list VALUES ('one'), ('two'), ('three'), ('four')

-- Build a string in which we recreate the list so we can pass it to exec
-- This can be done in any language since we're just building a string.
DECLARE @str nvarchar(max);
SET @str = 'DECLARE @list [dbo].[StringList]; INSERT INTO @list VALUES '

-- Add all the values we want to the string. This would be a loop in C++.
SELECT @str = @str + '(''' + StringValue + '''),' FROM @list

-- Remove the trailing comma so the query is valid sql.
SET @str = substring(@str, 1, len(@str)-1)

-- Add a select to test the string.
SET @str = @str + '; SELECT * FROM @list;'

-- Execute the string and see we've pass the table correctly.
EXEC(@str)

7

ใน SQL Server 2016+ ความเป็นไปได้อีกอย่างก็คือการใช้OPENJSONฟังก์ชั่น

วิธีนี้เป็นวิธี blogged เกี่ยวกับในOPENJSON - หนึ่งในวิธีที่ดีที่สุดในการเลือกแถวด้วยรายการของรหัส

ตัวอย่างการทำงานเต็มรูปแบบด้านล่าง

CREATE TABLE dbo.Tags
  (
     Name  VARCHAR(50),
     Count INT
  )

INSERT INTO dbo.Tags
VALUES      ('VB',982), ('ruby',1306), ('rails',1478), ('scruffy',1), ('C#',1784)

GO

CREATE PROC dbo.SomeProc
@Tags VARCHAR(MAX)
AS
SELECT T.*
FROM   dbo.Tags T
WHERE  T.Name IN (SELECT J.Value COLLATE Latin1_General_CI_AS
                  FROM   OPENJSON(CONCAT('[', @Tags, ']')) J)
ORDER  BY T.Count DESC

GO

EXEC dbo.SomeProc @Tags = '"ruby","rails","scruffy","rubyonrails"'

DROP TABLE dbo.Tags 

7

นี่คือทางเลือกอื่น เพียงผ่านรายการที่คั่นด้วยจุลภาคเป็นพารามิเตอร์สตริงไปยังขั้นตอนการจัดเก็บและ:

CREATE PROCEDURE [dbo].[sp_myproc]
    @UnitList varchar(MAX) = '1,2,3'
AS
select column from table
where ph.UnitID in (select * from CsvToInt(@UnitList))

และฟังก์ชั่น:

CREATE Function [dbo].[CsvToInt] ( @Array varchar(MAX))
returns @IntTable table
(IntValue int)
AS
begin
    declare @separator char(1)
    set @separator = ','
    declare @separator_position int
    declare @array_value varchar(MAX)

    set @array = @array + ','

    while patindex('%,%' , @array) <> 0
    begin

        select @separator_position = patindex('%,%' , @array)
        select @array_value = left(@array, @separator_position - 1)

        Insert @IntTable
        Values (Cast(@array_value as int))
        select @array = stuff(@array, 1, @separator_position, '')
    end
    return
end

6

ฉันมีคำตอบที่ไม่ต้องใช้ UDF, XML เพราะ IN ยอมรับคำสั่ง select เช่น SELECT * จากการทดสอบโดยที่ Data IN (ค่า SELECT จากตาราง)

คุณต้องการวิธีแปลงสตริงเป็นตาราง

สิ่งนี้สามารถทำได้ด้วย CTE แบบเรียกซ้ำหรือแบบสอบถามที่มีตารางตัวเลข (หรือ Master..spt_value)

นี่คือรุ่น CTE

DECLARE @InputString varchar(8000) = 'ruby,rails,scruffy,rubyonrails'

SELECT @InputString = @InputString + ','

;WITH RecursiveCSV(x,y) 
AS 
(
    SELECT 
        x = SUBSTRING(@InputString,0,CHARINDEX(',',@InputString,0)),
        y = SUBSTRING(@InputString,CHARINDEX(',',@InputString,0)+1,LEN(@InputString))
    UNION ALL
    SELECT 
        x = SUBSTRING(y,0,CHARINDEX(',',y,0)),
        y = SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y))
    FROM 
        RecursiveCSV 
    WHERE
        SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y)) <> '' OR 
        SUBSTRING(y,0,CHARINDEX(',',y,0)) <> ''
)
SELECT
    * 
FROM 
    Tags
WHERE 
    Name IN (select x FROM RecursiveCSV)
OPTION (MAXRECURSION 32767);

6

ฉันใช้คำตอบที่ได้รับการโหวตมากที่สุดฉบับย่อ:

List<SqlParameter> parameters = tags.Select((s, i) => new SqlParameter("@tag" + i.ToString(), SqlDbType.NVarChar(50)) { Value = s}).ToList();

var whereCondition = string.Format("tags in ({0})", String.Join(",",parameters.Select(s => s.ParameterName)));

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

หากคุณสนใจในประสิทธิภาพและไม่ต้องการวนซ้ำสองครั้งนี่เป็นเวอร์ชั่นที่สวยงามน้อยกว่า:

var parameters = new List<SqlParameter>();
var paramNames = new List<string>();
for (var i = 0; i < tags.Length; i++)  
{
    var paramName = "@tag" + i;

    //Include size and set value explicitly (not AddWithValue)
    //Because SQL Server may use an implicit conversion if it doesn't know
    //the actual size.
    var p = new SqlParameter(paramName, SqlDbType.NVarChar(50) { Value = tags[i]; } 
    paramNames.Add(paramName);
    parameters.Add(p);
}

var inClause = string.Join(",", paramNames);

5

นี่คือคำตอบสำหรับปัญหานี้อีก

(เวอร์ชันใหม่โพสต์เมื่อ 6/4/13)

    private static DataSet GetDataSet(SqlConnectionStringBuilder scsb, string strSql, params object[] pars)
    {
        var ds = new DataSet();
        using (var sqlConn = new SqlConnection(scsb.ConnectionString))
        {
            var sqlParameters = new List<SqlParameter>();
            var replacementStrings = new Dictionary<string, string>();
            if (pars != null)
            {
                for (int i = 0; i < pars.Length; i++)
                {
                    if (pars[i] is IEnumerable<object>)
                    {
                        List<object> enumerable = (pars[i] as IEnumerable<object>).ToList();
                        replacementStrings.Add("@" + i, String.Join(",", enumerable.Select((value, pos) => String.Format("@_{0}_{1}", i, pos))));
                        sqlParameters.AddRange(enumerable.Select((value, pos) => new SqlParameter(String.Format("@_{0}_{1}", i, pos), value ?? DBNull.Value)).ToArray());
                    }
                    else
                    {
                        sqlParameters.Add(new SqlParameter(String.Format("@{0}", i), pars[i] ?? DBNull.Value));
                    }
                }
            }
            strSql = replacementStrings.Aggregate(strSql, (current, replacementString) => current.Replace(replacementString.Key, replacementString.Value));
            using (var sqlCommand = new SqlCommand(strSql, sqlConn))
            {
                if (pars != null)
                {
                    sqlCommand.Parameters.AddRange(sqlParameters.ToArray());
                }
                else
                {
                    //Fail-safe, just in case a user intends to pass a single null parameter
                    sqlCommand.Parameters.Add(new SqlParameter("@0", DBNull.Value));
                }
                using (var sqlDataAdapter = new SqlDataAdapter(sqlCommand))
                {
                    sqlDataAdapter.Fill(ds);
                }
            }
        }
        return ds;
    }

ไชโย


4

การย้ายที่ชนะเท่านั้นไม่สามารถเล่นได้

ไม่มีความแปรปรวนไม่สิ้นสุดสำหรับคุณ ความแปรปรวน จำกัด เท่านั้น

ใน SQL คุณมีประโยคเช่นนี้:

and ( {1}==0 or b.CompanyId in ({2},{3},{4},{5},{6}) )

ในรหัส C # คุณทำสิ่งนี้:

  int origCount = idList.Count;
  if (origCount > 5) {
    throw new Exception("You may only specify up to five originators to filter on.");
  }
  while (idList.Count < 5) { idList.Add(-1); }  // -1 is an impossible value
  return ExecuteQuery<PublishDate>(getValuesInListSQL, 
               origCount,   
               idList[0], idList[1], idList[2], idList[3], idList[4]);

ดังนั้นโดยทั่วไปหากการนับเป็น 0 จะไม่มีตัวกรองและทุกอย่างจะผ่านไป หากการนับสูงกว่า 0 ค่านั้นจะต้องอยู่ในรายการ แต่รายการนั้นถูกเติมออกเป็นห้าด้วยค่าที่เป็นไปไม่ได้ (เพื่อให้ SQL ยังคงสมเหตุสมผล)

บางครั้งวิธีการแก้ปัญหาขาเป็นวิธีเดียวที่ได้ผล

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.