Commit 9806401f authored by Marc Gravell's avatar 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)
parent 49e797a4
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
using System.Diagnostics; using System.Diagnostics;
using Xunit; using Xunit;
using System.Data.Common; using System.Data.Common;
using System.Text.RegularExpressions;
#if FIREBIRD #if FIREBIRD
using FirebirdSql.Data.FirebirdClient; using FirebirdSql.Data.FirebirdClient;
#endif #endif
...@@ -159,6 +160,99 @@ public void TestListOfAnsiStrings() ...@@ -159,6 +160,99 @@ public void TestListOfAnsiStrings()
results[1].IsEqualTo("b"); 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] [Fact]
public void TestNullableGuidSupport() public void TestNullableGuidSupport()
{ {
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"System.Data.SqlClient": "4.0.0-*", "System.Data.SqlClient": "4.0.0-*",
"System.Linq": "4.0.1-*", "System.Linq": "4.0.1-*",
"System.Runtime": "4.0.21-*", "System.Runtime": "4.0.21-*",
"System.Text.RegularExpressions": "4.0.11-*",
"System.Threading": "4.0.11-*", "System.Threading": "4.0.11-*",
"xunit": "2.2.0-beta1-build3239" "xunit": "2.2.0-beta1-build3239"
} }
...@@ -162,6 +163,7 @@ ...@@ -162,6 +163,7 @@
}, },
"EntityFramework": "6.1.3", "EntityFramework": "6.1.3",
"FirebirdSql.Data.FirebirdClient": "4.10.0", "FirebirdSql.Data.FirebirdClient": "4.10.0",
"Microsoft.SqlServer.Compact": "4.0.8876.1",
"Microsoft.SqlServer.Types": "11.0.2", "Microsoft.SqlServer.Types": "11.0.2",
"MySql.Data": "6.9.8", "MySql.Data": "6.9.8",
"NHibernate": "4.0.4.4000", "NHibernate": "4.0.4.4000",
...@@ -174,7 +176,7 @@ ...@@ -174,7 +176,7 @@
"Soma": "1.8.0.7", "Soma": "1.8.0.7",
"Susanoo.Core": "1.2.4", "Susanoo.Core": "1.2.4",
"Susanoo.SqlServer": "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": "2.2.0-beta1-build3239",
"xunit.runner.dnx": "2.1.0-rc1-build204" "xunit.runner.dnx": "2.1.0-rc1-build204"
} }
...@@ -184,6 +186,7 @@ ...@@ -184,6 +186,7 @@
"define": [ "COREFX", "ASYNC", "DNX" ] "define": [ "COREFX", "ASYNC", "DNX" ]
}, },
"dependencies": { "dependencies": {
"System.Text.RegularExpressions": "4.0.11-*",
"xunit": "2.2.0-beta1-build3239", "xunit": "2.2.0-beta1-build3239",
"xunit.runner.dnx": "2.1.0-rc1-build204" "xunit.runner.dnx": "2.1.0-rc1-build204"
} }
......
...@@ -30,6 +30,20 @@ public static void SetDefaults() ...@@ -30,6 +30,20 @@ public static void SetDefaults()
/// Indicates whether nulls in data are silently ignored (default) vs actively applied and assigned to members /// Indicates whether nulls in data are silently ignored (default) vs actively applied and assigned to members
/// </summary> /// </summary>
public static bool ApplyNullValues { get; set; } 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 ...@@ -1769,6 +1769,33 @@ public static IDbDataParameter FindOrAddParameter(IDataParameterCollection param
return result; 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> /// <summary>
/// Internal use only /// Internal use only
/// </summary> /// </summary>
...@@ -1800,8 +1827,12 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj ...@@ -1800,8 +1827,12 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
{ {
foreach (var item in list) 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) if (!isDbString)
{ {
ITypeHandler handler; ITypeHandler handler;
...@@ -1809,7 +1840,7 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj ...@@ -1809,7 +1840,7 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
} }
} }
var listParam = command.CreateParameter(); var listParam = command.CreateParameter();
listParam.ParameterName = namePrefix + count; listParam.ParameterName = namePrefix + count.ToString();
if (isString) if (isString)
{ {
listParam.Size = DbString.DefaultLength; listParam.Size = DbString.DefaultLength;
...@@ -1833,6 +1864,20 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj ...@@ -1833,6 +1864,20 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
command.Parameters.Add(listParam); 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))?"; var regexIncludingUnknown = @"([?@:]" + Regex.Escape(namePrefix) + @")(?!\w)(\s+(?i)unknown(?-i))?";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment