2

We have a C# component that handles attaching arbitrary-sized element lists into IN clauses for semi-arbitrary SQL SELECT queries. 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; 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.

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

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). Going through the ODP.NET docs hasn't brought any joy thus far.

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.)

Community
  • 1
  • 1
T2PS
  • 372
  • 1
  • 2
  • 12
  • Have you tried using an anonymous PL/SQL block (eg in between a BEGIN and END)? It's basically stored procedure code except it is on the client side, not stored in the server so you control it as a developer. – Christian Shay Aug 20 '14 at 20:06
  • Are you saying that you are hitting a limit on number of parameters? Or is it size of the SQL? Could you share the ORA error you get? – Christian Shay Aug 20 '14 at 20:07
  • Without the attempted changes, it gets: ORA-01795: maximum number of expressions in a list is 1000 – T2PS Aug 21 '14 at 00:18
  • There's some suggested workarounds in this link. Do any of them apply to you? http://stackoverflow.com/questions/17842453/is-there-a-workaround-for-ora-01795-maximum-number-of-expressions-in-a-list-is – Christian Shay Aug 24 '14 at 22:44
  • @ChristianShay 2 of those answers require another table that can be used to resolve the IN clause, which I don't believe is the case for the OP. The most up-voted answer would work, however it has significant additional complexity for the C# component - it goes from setting a parameter that's used in an IN clause to needing to know about and duplicate the entire WHERE clause. –  Aug 25 '14 at 00:17
  • 1
    There's dozens of questions involving ORA-01795 asked across the net and they all involve reformmatting the SQL in ways that violate the requirements given in this question.. so my best guess is that there is no answer, and T2PS is going to need to push back on the people supplying the SQL to him or constraining him from modifying it to add multiple "or where" clauses. If you get truly desperate, try asking this question over on the OTN SQL forums, in the same forum as this question: https://community.oracle.com/thread/235143 – Christian Shay Aug 25 '14 at 05:07
  • @ChristianShay We'll see how far pushing back gets us. The requirement to circumvent the ORA-01795 restriction is unlikely to be dropped, but we may be able to get concession that a suitable user-defined type be pre-existing, hopefully opening up one of the other approaches described elsewhere. – T2PS Aug 25 '14 at 08:50
  • if you have all the parameter values before hand, can you select multiple blocks/ranges of parameters and loop thru the query? I know that sounds bad ("loop query") but if there is an upper limit to the number of parameters, it wouldn't be so terrible as your options seem limited... – solidau Aug 27 '14 at 22:22
  • I'm advised that some of the queries we're being fed contain eg. CTEs so the callers can implement paging; I didn't think splitting the parameters like that is compatible with such a query. – T2PS Aug 29 '14 at 08:35

2 Answers2

1

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:

@"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.

Replace {inListTableName} with the 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.

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. If you have a Hashset (which I recommend using to avoid sending unnecessary duplicates), use the .ToArray() on it to get an array.

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})

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.

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.

GetInListSQLSnippet would simply return (SELECT {inListTableName}.inValue FROM {inListTableName});

RemoveValueList cleans up.

The constructor for this class would take the value list and oracle db data type, and generate the 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. 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. The following example of dividing the previous example will return all users instead of all but those with 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);
Tyree Jackson
  • 2,588
  • 16
  • 22
  • The restriction against temporary tables applies equally to regular tables, I'm afraid -- no permission. – T2PS Aug 29 '14 at 02:09
  • That is unfortunate. Please keep in mind the part that I added at the end applies if you need to use NOT IN at any point. Those queries cannot be broken up. Will they allow you to have one table permanently added to your data model? If so, you could create a permanent InList table and add an ID to partition the values in the table for specific query executions. Then you just need to alter the GetInListSQLSnippet to filter the values in the permanent InList table by that ID. You could use any query specific value for the ID to filter the list. – Tyree Jackson Aug 29 '14 at 02:16
  • The data models the queries run against don't belong to us either, which is where the restriction against GTTs, `CREATE TYPE` etc comes from. I'm told the odds of the external DBAs approving such a change are smaller than those of our team winning the lottery without buying any tickets... – T2PS Aug 29 '14 at 08:26
  • Are you able to get your own schema on the same oracle database instance outside of their data model? If so, then you could do it with fully qualified table names crossing the schemas. I investigated how to do this back in 2008 for SQL Server and this year for Oracle. In my experience, there is no other reliable way to get a large list of values to the database for these kinds of operations. The use of ArrayBindCount for example is restricted by Oracle for use in ExecuteNonQuery (Update/Insert/Delete) statements only. – Tyree Jackson Aug 29 '14 at 14:35
  • Don't forget that when doing UNION there's an implicit DISTINCT for the selects. This may, or may not, be problematic. – Jay Otterbein Aug 29 '14 at 20:51
0

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?

i.e. Let's imagine 5 elements are too many for the query...

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.

  • Unfortunately the queries we're talking about are arbitrary and generally have more complexity than your examples, including use of CTEs to perform paging, `INNER JOIN`s, etc. I'm told the calling code, in places where it knows that its query result can be merged this way, has already been modified to do so; this question is aimed at the remaining cases. Paging in particular has been described as incompatible with this approach. – T2PS Aug 29 '14 at 02:13