简体   繁体   English

C#/ ODP.NET:大型IN子句解决方法

[英]C#/ODP.NET: large IN clause workaround

We have a C# component that handles attaching arbitrary-sized element lists into IN clauses for semi-arbitrary SQL SELECT queries. 我们有一个C#组件,它处理将任意大小的元素列表附加到用于半任意SQL SELECT查询的IN子句中。 Essentially this boils down to receiving something like: 从本质上讲,这归结为接收以下内容:

SELECT COUNT(*) FROM a WHERE b IN (...)

...where the "..." is the only portion of the query the component is allowed to modify. ...其中“...”是允许组件修改的查询的唯一部分。

Currently the component will insert a comma-separated set of named bind parameters, then attach the corresponding IDbDataParameter objects to the command and execute; 目前该组件将插入一组以逗号分隔的命名绑定参数,然后将相应的IDbDataParameter对象附加到命令并执行; the component is made aware of the types for the parameters it has to bind. 组件知道它必须绑定的参数的类型。 This works well, until the calling code supplies a parameter set larger than the database is willing to accept. 这很有效,直到调用代码提供的参数集大于数据库愿意接受的参数集。 The objective here is to get such large sets working with queries against Oracle 11gR2 via ODP.NET. 这里的目标是通过ODP.NET使这些大型集合处理针对Oracle 11gR2的查询。

This task is complicated somewhat by the following approaches being deemed unacceptable by those setting the requirements: 这项任务有些复杂,以下方法被设定要求的人认为是不可接受的:

  • Global Temporary Tables 全球临时表
  • Stored procedures 存储过程
  • Anything requiring CREATE TYPE to have been executed 任何需要CREATE TYPE东西都已被执行

The solution to this is not required to execute only one query. 解决方案不需要只执行一个查询。

I'm trying to make this work by binding the clause as an array, using code sourced from elsewhere: 我试图通过使用从其他地方获取的代码将子句绑定为数组来完成此工作:

IList<string> values;

//...

OracleParameter parameter = new OracleParameter();
parameter.ParameterName = "parm";
parameter.DbType = DbType.String;
parameter.Value = values.ToArray();
int[] sizes = new int[values.Count];
for (int index = 0; index < values.Count; index++)
{
    sizes[index] = values[index].Length;
}
parameter.ArrayBindSize = sizes;

//...

The command subsequently executes without throwing an exception, but the value returned for COUNT is zero (compared to the expected value, from running the query in SQLDeveloper with a nested SELECT returning the same parameter set). 该命令随后执行而不抛出异常,但为COUNT返回的值为零(与预期值相比,在SQLDeveloper中运行查询时,嵌套的SELECT返回相同的参数集)。 Going through the ODP.NET docs hasn't brought any joy thus far. 到目前为止,通过ODP.NET文档并未带来任何乐趣。

The questions for this are: 对此的问题是:

  • Is there a way to make the above parameter attachment work as expected? 有没有办法使上述参数附件按预期工作?
  • Is there another viable way to achieve this without using one of the vetoed approaches? 是否有另一种可行的方法来实现这一目标而不使用其中一种否决方法?

(I'm aware this is similar to this (unanswered) question , but that scenario does not mention having the same restrictions on approaches.) (我知道这与这个(未答复的)问题类似,但是这种情况没有提到对方法有相同的限制。)

Well, since you are not allowed to use Global Temporary Tables, are you at least allowed to create normal tables? 那么,既然不允许使用全局临时表,那么您是否至少可以创建普通表? If so, here is a way: 如果是这样,这是一种方式:

Create an OracleCommand object with the following command text: 使用以下命令文本创建OracleCommand对象:

@"BEGIN
CREATE TABLE {inListTableName}
(
  inValue   {dbDataType}
)

INSERT INTO {inListTableName}(inValue) VALUES(:inValue);
END"

Set the ArrayBindCount on the command object to the number of items you need in your in list. 将命令对象上的ArrayBindCount设置为列表中所需的项目数。

Replace {inListTableName} with the Guid.NewGuid().ToString() . {inListTableName}替换为Guid.NewGuid().ToString()

Replace the {dbDataType} with the correct oracle data type for the list of values that you want to use in your in clause. {dbDataType}替换为要在in子句中使用的值列表的正确oracle数据类型。

Add an OracleParameter to the OracleCommand named "inValue" and set the value of the parameter to an array containing the values that you want in your in clause. 将OracleParameter添加到名为“inValue”的OracleCommand中,并将参数的值设置为包含in子句中所需值的数组。 If you have a Hashset (which I recommend using to avoid sending unnecessary duplicates), use the .ToArray() on it to get an array. 如果你有一个Hashset(我建议使用它来避免发送不必要的重复项),使用它.ToArray()来获取一个数组。

Execute this command. 执行此命令。 This is your prep command. 这是你的准备命令。

Then use the following sql snippet as the value portion of the in clause in your select sql statement: (SELECT {inListTableName}.inValue FROM {inListTableName}) 然后使用以下sql片段作为select sql语句中in子句的值部分: (SELECT {inListTableName}.inValue FROM {inListTableName})

For example: 例如:

SELECT FirstName, LastName FROM Users WHERE UserId IN (SELECT {inListTableName}.inValue FROM {inListTableName});

Execute this command to get a reader. 执行此命令以获取读者。

Lastly, one more command with the following command text: 最后,再使用以下命令文本执行以下命令:

DROP TABLE {inListTableName};

This is your cleanup command. 这是你的清理命令。 Execute this command. 执行此命令。

You might want to create an alternate schema/user to create the inListTable so that you can grant appropriate permissions to your user to only create tables in that schema. 您可能希望创建备用架构/用户来创建inListTable以便您可以向用户授予适当的权限,以便仅在该架构中创建表。

All of this can be encapsulated in a reusable class with the following interface: 所有这些都可以使用以下接口封装在可重用的类中:

public interface IInListOperation
{
    void    TransmitValueList(OracleConnection connection);
    string  GetInListSQLSnippet();
    void    RemoveValueList();
}

TransmitValueList would create your prep command, add the parameter and execute the prep command. TransmitValueList将创建您的prep命令,添加参数并执行prep命令。

GetInListSQLSnippet would simply return (SELECT {inListTableName}.inValue FROM {inListTableName}); GetInListSQLSnippet只会返回(SELECT {inListTableName}.inValue FROM {inListTableName});

RemoveValueList cleans up. RemoveValueList清理。

The constructor for this class would take the value list and oracle db data type, and generate the inListTableName . 此类的构造函数将获取值列表和oracle db数据类型,并生成inListTableName

If you can use a Global Temporary Table, I would recommend that over creating and dropping tables. 如果您可以使用全局临时表,我建议您创建和删除表。

Edit: I'd like to add that this approach works well if you have clauses involving NOT IN lists or other inequality operators. 编辑:我想补充一点,如果您有包含NOT IN列表或其他不等运算符的子句,这种方法很有效。 Take the following for example: 以下面的例子为例:

SELECT FirstName, LastName FROM Users WHERE Status == 'ACTIVE' OR UserID NOT IN (1,2,3,4,5,6,7,8,9,10);

If you use the approach of splitting the NOT IN part up, you will end up getting invalid results. 如果您使用将NOT IN部分拆分的方法,最终会得到无效结果。 The following example of dividing the previous example will return all users instead of all but those with UserIds 1-10. 以下分割上一个示例的示例将返回所有用户,而不是除UserIds 1-10之外的所有用户。

SELECT FirstName, LastName FROM Users WHERE UserID NOT IN (1,2,3,4,5)
UNION
SELECT FirstName, LastName FROM Users WHERE UserID NOT IN (6,7,8,9,10);

Maybe this is too simplistic for the kind of query you're doing, but is there any reason why you couldn't split this into several queries and combine the results together in code? 也许这对于你正在进行的查询来说太简单了,但是有没有理由为什么你不能将它分成几个查询并将结果组合在一起?

ie Let's imagine 5 elements are too many for the query... 即让我们想象查询的5个元素太多了...

select COUNT(*) from A where B in (1,2,3,4,5)  

you'd separately perform 你要单独表演

select COUNT(*) from A where B in (1,2,3)
select COUNT(*) from A where B in (4,5)

and then add those results together. 然后将这些结果一起添加。 Of course, you'd have to make sure that in-clause list is distinct so you don't double up on your counts. 当然,你必须确保条款清单是不同的,所以你不要加倍计算。

If you can do it this way, there is an added opportunity for parallelism if you're allowed more than one connection. 如果您可以这样做,如果允许多个连接,则可以增加并行机会。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM