提交 9806401f 编写于 作者: M Marc Gravell

New feature: PadListExpansions - reduces query plan saturation by padding "in"...

New feature: PadListExpansions - reduces query plan saturation by padding "in" lists and populating with nulls; opt-in (see remarks on setting)
上级 49e797a4
......@@ -26,6 +26,7 @@
using System.Diagnostics;
using Xunit;
using System.Data.Common;
using System.Text.RegularExpressions;
#if FIREBIRD
using FirebirdSql.Data.FirebirdClient;
#endif
......@@ -159,6 +160,99 @@ public void TestListOfAnsiStrings()
results[1].IsEqualTo("b");
}
[Fact]
public void TestListExpansionPadding_Enabled()
{
TestListExpansionPadding(true);
}
[Fact]
public void TestListExpansionPadding_Disabled()
{
TestListExpansionPadding(false);
}
private void TestListExpansionPadding(bool enabled)
{
bool oldVal = SqlMapper.Settings.PadListExpansions;
try
{
SqlMapper.Settings.PadListExpansions = enabled;
connection.ExecuteScalar<int>(@"
create table #ListExpansion(id int not null identity(1,1), value int null);
insert #ListExpansion (value) values (null);
declare @loop int = 0;
while (@loop < 12)
begin -- double it
insert #ListExpansion (value) select value from #ListExpansion;
set @loop = @loop + 1;
end
select count(1) as [Count] from #ListExpansion").IsEqualTo(4096);
var list = new List<int>();
int nextId = 1, batchCount;
var rand = new Random(12345);
const int SQL_SERVER_MAX_PARAMS = 2095;
TestListForExpansion(list, enabled); // test while empty
while (list.Count < SQL_SERVER_MAX_PARAMS)
{
try
{
if (list.Count <= 20) batchCount = 1;
else if (list.Count <= 200) batchCount = rand.Next(1, 40);
else batchCount = rand.Next(1, 100);
for (int j = 0; j < batchCount && list.Count < SQL_SERVER_MAX_PARAMS; j++)
list.Add(nextId++);
TestListForExpansion(list, enabled);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failure with {list.Count} items: {ex.Message}", ex);
}
}
}
finally
{
SqlMapper.Settings.PadListExpansions = oldVal;
}
}
private void TestListForExpansion(List<int> list, bool enabled)
{
var row = connection.QuerySingle(@"
declare @hits int;
select @hits = count(1) from #ListExpansion where id in @ids ;
declare @query nvarchar(max) = N' in @ids '; -- ok, I confess to being pleased with this hack ;p
select @hits as [Hits], @query as [Query];
", new { ids = list });
int hits = row.Hits;
string query = row.Query;
int argCount = Regex.Matches(query, "@ids[0-9]").Count;
int expectedCount = GetExpectedListExpansionCount(list.Count, enabled);
hits.IsEqualTo(list.Count);
argCount.IsEqualTo(expectedCount);
}
static int GetExpectedListExpansionCount(int count, bool enabled)
{
if (!enabled) return count;
if (count <= 5 || count > 2070) return count;
int padFactor;
if (count <= 150) padFactor = 10;
else if (count <= 750) padFactor = 50;
else if (count <= 2000) padFactor = 100;
else if (count <= 2070) padFactor = 10;
else padFactor = 200;
int blocks = count / padFactor, delta = count % padFactor;
if (delta != 0) blocks++;
return blocks * padFactor;
}
[Fact]
public void TestNullableGuidSupport()
{
......
......@@ -126,6 +126,7 @@
"System.Data.SqlClient": "4.0.0-*",
"System.Linq": "4.0.1-*",
"System.Runtime": "4.0.21-*",
"System.Text.RegularExpressions": "4.0.11-*",
"System.Threading": "4.0.11-*",
"xunit": "2.2.0-beta1-build3239"
}
......@@ -162,6 +163,7 @@
},
"EntityFramework": "6.1.3",
"FirebirdSql.Data.FirebirdClient": "4.10.0",
"Microsoft.SqlServer.Compact": "4.0.8876.1",
"Microsoft.SqlServer.Types": "11.0.2",
"MySql.Data": "6.9.8",
"NHibernate": "4.0.4.4000",
......@@ -174,7 +176,7 @@
"Soma": "1.8.0.7",
"Susanoo.Core": "1.2.4",
"Susanoo.SqlServer": "1.2.4",
"Microsoft.SqlServer.Compact": "4.0.8876.1",
"System.Text.RegularExpressions": "4.0.11-*",
"xunit": "2.2.0-beta1-build3239",
"xunit.runner.dnx": "2.1.0-rc1-build204"
}
......@@ -184,6 +186,7 @@
"define": [ "COREFX", "ASYNC", "DNX" ]
},
"dependencies": {
"System.Text.RegularExpressions": "4.0.11-*",
"xunit": "2.2.0-beta1-build3239",
"xunit.runner.dnx": "2.1.0-rc1-build204"
}
......
......@@ -30,6 +30,20 @@ public static void SetDefaults()
/// Indicates whether nulls in data are silently ignored (default) vs actively applied and assigned to members
/// </summary>
public static bool ApplyNullValues { get; set; }
/// <summary>
/// Should list expansions be padded with null-valued parrameters, to prevent qurey-plan saturation? For example,
/// an 'in @foo' expansion with 7, 8 or 9 values will be sent as a list of 10 values, with 3, 2 or 1 of them null.
/// The padding size is relative to the size of the list; "next 10" under 150, "next 50" under 500,
/// "next 100" under 1500, etc.
/// </summary>
/// <remarks>
/// Caution: this should be treated with care if your DB provider (or the specific configuration) allows for null
/// equality (aka "ansi nulls off"), as this may change the intent of your query; as such, this is disabled by
/// default and must be enabled.
/// </remarks>
public static bool PadListExpansions { get; set; }
}
}
}
......@@ -1769,6 +1769,33 @@ public static IDbDataParameter FindOrAddParameter(IDataParameterCollection param
return result;
}
internal static int GetListPaddingExtraCount(int count)
{
switch(count)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
return 0; // no padding
}
if (count < 0) return 0;
int padFactor;
if (count <= 150) padFactor = 10;
else if (count <= 750) padFactor = 50;
else if (count <= 2000) padFactor = 100; // note: max param count for SQL Server
else if (count <= 2070) padFactor = 10; // try not to over-pad as we approach that limit
else if (count <= 2100) return 0; // just don't pad between 2070 and 2100, to minimize the crazy
else padFactor = 200; // above that, all bets are off!
// if we have 17, factor = 10; 17 % 10 = 7, we need 3 more
int intoBlock = count % padFactor;
return intoBlock == 0 ? 0 : (padFactor - intoBlock);
}
/// <summary>
/// Internal use only
/// </summary>
......@@ -1800,8 +1827,12 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
{
foreach (var item in list)
{
if (count++ == 0)
if (++count == 1) // first item: fetch some type info
{
if(item == null)
{
throw new NotSupportedException("The first item in a list-expansion cannot be null");
}
if (!isDbString)
{
ITypeHandler handler;
......@@ -1809,7 +1840,7 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
}
}
var listParam = command.CreateParameter();
listParam.ParameterName = namePrefix + count;
listParam.ParameterName = namePrefix + count.ToString();
if (isString)
{
listParam.Size = DbString.DefaultLength;
......@@ -1833,6 +1864,20 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
command.Parameters.Add(listParam);
}
}
if (Settings.PadListExpansions && !isDbString)
{
int padCount = GetListPaddingExtraCount(count);
for(int i = 0; i < padCount; i++)
{
count++;
var padParam = command.CreateParameter();
padParam.ParameterName = namePrefix + count.ToString();
if(isString) padParam.Size = DbString.DefaultLength;
padParam.DbType = dbType;
padParam.Value = DBNull.Value;
command.Parameters.Add(padParam);
}
}
}
var regexIncludingUnknown = @"([?@:]" + Regex.Escape(namePrefix) + @")(?!\w)(\s+(?i)unknown(?-i))?";
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册