7

Got a c#.net app which I need to modify. The query at the moment effectively does this:

select * from contract where contractnum = :ContractNum

(very simplified, just to show we're using an = and one parameter)

That parameter is read in from the Settings.Settings file on the C# app and has one string in it. I need to modify it to include multiple contracts, so I figure I can change the SQL to:

select * from contract where contractnum in (:ContractNum)

but that returns no results, no matter how I format the string in the parameter.

Is there a way I can get oracle to do an IN with a parameter?

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Gareth
  • 624
  • 3
  • 10
  • 26
  • When you use odp.net or devart as data provider you could use an Oracle collection (a nested table) as parameter. This is the fastest way but it isn't possible when you use system.data.oracleclient. So what kind of dataprovider do you use? – tuinstoel Oct 26 '09 at 19:11

7 Answers7

7

You can use an Oracle collection of numbers as a parameter (bind variable) when you use ODP.NET as dataprovider. This works with Oracle server 9, 10 or 11 and ODP.net release >= 11.1.0.6.20 .

A similar solution is possible when you use Devart's .NET dataprovider for Oracle.

Let's select the contracts with contractnum's 3 and 4.

We have to use an Oracle type to transfer an array of contract numbers to our query.

MDSYS.SDO_ELEM_INFO_ARRAY is used because if we use this already predefined Oracle type we don't have to define our own Oracle type. You can fill MDSYS.SDO_ELEM_INFO_ARRAY with max 1048576 numbers.

using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;

[OracleCustomTypeMappingAttribute("MDSYS.SDO_ELEM_INFO_ARRAY")]
public class NumberArrayFactory : IOracleArrayTypeFactory
{
  public Array CreateArray(int numElems)
  {
    return new Decimal[numElems];
  }

  public Array CreateStatusArray(int numElems)
  {
    return null;
  }
}

private void Test()
{
  OracleConnectionStringBuilder b = new OracleConnectionStringBuilder();
  b.UserID = "sna";
  b.Password = "sna";
  b.DataSource = "ora11";
  using (OracleConnection conn = new OracleConnection(b.ToString()))
  {
    conn.Open();
    using (OracleCommand comm = conn.CreateCommand())
    {
      comm.CommandText =
      @" select  /*+ cardinality(tab 10) */ c.*  " +
      @" from contract c, table(:1) tab " +
      @" where c.contractnum = tab.column_value";

      OracleParameter p = new OracleParameter();
      p.OracleDbType = OracleDbType.Array;
      p.Direction = ParameterDirection.Input;
      p.UdtTypeName = "MDSYS.SDO_ELEM_INFO_ARRAY";
      //select contract 3 and 4
      p.Value = new Decimal[] { 3, 4 };
      comm.Parameters.Add(p);

      int numContracts = 0;
      using (OracleDataReader reader = comm.ExecuteReader())
      {
        while (reader.Read())
        {
           numContracts++;
        }
      }
      conn.Close();
    }
  }
}

The index on contract.contractnum isn't used when one omits hint /*+ cardinality(tab 10) */. I assumed contractnum is the primary key so this column will be indexed.

See also here: http://forums.oracle.com/forums/thread.jspa?messageID=3869879#3869879

tuinstoel
  • 7,248
  • 27
  • 27
  • Read also: http://blog.tanelpoder.com/2012/08/02/the-limitations-of-cursor_sharing-force-and-force_matching_signature-for-sql-plan-stability/ – TTT Mar 27 '13 at 12:43
  • This worked for our scenario. The gotcha for me was including the NumberArrayFactory, even though it is not explicitly used in the code. – Carl Nov 12 '14 at 16:59
6

you could use a pipelined function to transform a string into a table which could be used with the IN operator. For example (tested with 10gR2):

SQL> select * from table(demo_pkg.string_to_tab('i,j,k'));

COLUMN_VALUE
-----------------
i
j
k

with the following package:

SQL> CREATE OR REPLACE PACKAGE demo_pkg IS
  2     TYPE varchar_tab IS TABLE OF VARCHAR2(4000);
  3     FUNCTION string_to_tab(p_string VARCHAR2,
  4                            p_delimiter VARCHAR2 DEFAULT ',')
  5        RETURN varchar_tab PIPELINED;
  6  END demo_pkg;
  7  /

Package created
SQL> CREATE OR REPLACE PACKAGE BODY demo_pkg IS
  2     FUNCTION string_to_tab(p_string VARCHAR2,
  3                            p_delimiter VARCHAR2 DEFAULT ',')
  4        RETURN varchar_tab PIPELINED IS
  5        l_string          VARCHAR2(4000) := p_string;
  6        l_first_delimiter NUMBER := instr(p_string, p_delimiter);
  7     BEGIN
  8        LOOP
  9           IF nvl(l_first_delimiter,0) = 0 THEN
 10              PIPE ROW(l_string);
 11              RETURN;
 12           END IF;
 13           PIPE ROW(substr(l_string, 1, l_first_delimiter - 1));
 14           l_string          := substr(l_string, l_first_delimiter + 1);
 15           l_first_delimiter := instr(l_string, p_delimiter);
 16        END LOOP;
 17     END;
 18  END demo_pkg;
 19  /

Package body created

Your query would look like this:

select * 
  from contract 
 where contractnum in (select column_value
                         from table(demo_pkg.string_to_tab(:ContractNum)))
Vincent Malgrat
  • 66,725
  • 9
  • 119
  • 171
  • 1
    +1 - AFAIK, this is the only way to use all of: bind variable, unknown number of elements, and the "IN" clause. If you have a known upper bound on the number of elements you can always code the statement to use that number of elements and programatically substitute nulls when there are leftover placeholders – dpbradley Oct 26 '09 at 16:45
  • No this is not the only way to use bind variables. You can also bind a Oracle collection of numbers and join with table(:numbers). You will no longer need a pipelined function. However your data provider has to support it. – tuinstoel Oct 26 '09 at 19:34
  • This is the approach that we have been using for almost 10 years. After (finally) moving over to use ODP.NET, we are being forced to use the method highlighted in the answer posted by tuinstoel. That seems to be a much nicer solution anyway, rather than string splitting. – Carl Nov 12 '14 at 17:02
  • @Carl I agree: a collection would be a better choice in general. It might take a bit more effort to setup, but a collection doesn't suffer from the limitations of varchars (max length, no type check on each member of the list, hard to use the separator character in a member, etc...) – Vincent Malgrat Nov 12 '14 at 17:08
2

Have yet to find a db that supports evaluating a single string variable containing commas to separate as the sole IN clause.

Your options are to substring the variable so the comma delimited variable contents are turned into rows, so you can then join onto this. Or to use dynamic SQL, which is a SQL statement constructed as a string in a sproc before the statement is executed.

OMG Ponies
  • 325,700
  • 82
  • 523
  • 502
  • I found a reference to using & instead of : for parameter identification and that works: Parameter value: '1182411', '1182423' SQL: select * from contract where contractnum in (&ContractNum) I have no idea why this works, or whether it is "officially" supported by Orace or just TOAD. Have you used & for Oracle before? Any idea what the difference is? – Gareth Oct 26 '09 at 16:20
  • :variable is a bind variable; the only time I recall using &variable was to define/use a bind variable within PLSQL Developer. – OMG Ponies Oct 26 '09 at 16:25
  • 2
    The ampersand is the default character indicating a substitution variable in SQL*Plus. TOAD (and other IDEs) support some SQL*Plus syntax. – APC Oct 26 '09 at 16:27
  • 2
    The ampersand will likely not work with C#. Even if it does work it won't be able to bind (performance problem) and will be open to SQL injection since it will build a query on the fly (security problem). – Vincent Malgrat Oct 26 '09 at 16:48
  • Your options are certainly options but they are not the only one. – tuinstoel Oct 26 '09 at 19:12
1

I know this is an old question but it is one of several in which the selected answer did not solve my problem and I don't want to start yet another thread on this topic so I'll just put down what I found in my travels in the hope that it might help someone.

I don't work with Oracle much but, like in SQL Server, it seems that to pass a table-valued parameter you need to have a corresponding UDT (user defined table) to which you have EXECUTE permissions (I could be wrong). This means that other answers suggesting the use of a built-in SYS UDT come with some freight and I couldn't figure out whether it really is possible to pass a table to something that is not a PL/SQL stored procedure in the current version of ODP.net.

Second, the string-parse solution is a kludge for all the obvious reasons (can't cache the execution plan or whatever Oracle calls it, doesn't scale well, etc).

So I spent rather a lot of time trying do the IN-clause using a table-valued parameter on a datamart to which I have only READ permission before I was hit by a blinding flash of the obvious (At an ASP.net forum no less). Turns out Oracle supports Xml queries 'natively' so instead of passing an array of values you can pass an xml list (if that is all you need). Again, I may be wrong, but it gets handled as a legitimate bind parameter and this is an example of how simple it is to use (vb.net, ADO.net, ODP.net using NuGet package):

    Dim xe As New XElement("l", New XElement("i", "ITEM-A"), New XElement("i", "ITEM-B"))
    Using conn As New OracleConnection(myConnectionString)
        conn.Open()
        Using cmd As OracleCommand = conn.CreateCommand()
            cmd.CommandType = CommandType.Text
            Dim query As String
            query = "  SELECT s.FOO, q.BAR " & vbCrLf
            query &= " FROM TABLE1 s LEFT OUTER JOIN " & vbCrLf
            query &= "      TABLE2 q ON q.ID = s.ID " & vbCrLf
            query &= " WHERE (COALESCE(q.ID, 'NULL') NOT LIKE '%OPTIONAL%') AND "
            query &= "       (s.ID IN ("
            query &= "                      SELECT stid "
            query &= "                      FROM XMLTable('/l/i' PASSING XMLTYPE(:stid) COLUMNS stid VARCHAR(32) PATH '.')"
            query &= "                 )"
            query &= "        )"
            cmd.CommandText = query
            Dim parameter As OracleParameter = cmd.Parameters.Add("stid", OracleDbType.NVarchar2, 4000)
            parameter.Value = xe.ToString
            Using r As OracleDataReader = cmd.ExecuteReader
                While r.Read()
                    //Do something
                End While
            End Using
        End Using
        conn.Close()

This is more of an observation than a carefully researched solution so please comment if there is anything inappropriate about doing it this way.


There is apparently a 4000 character limit using this method (2000 if NVARCHAR) so I had to watch my paging. The informative error message you get if you go over is

ORA-01460: unimplemented or unreasonable conversion requested

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
saminpa
  • 603
  • 1
  • 7
  • 13
0

For using parameter with IN statement you can use this construction:

select * from contract where contractnum
in (select column_value from table (:ContractNum))

where ContractNum is the custom array type.

Sergey
  • 306
  • 1
  • 8
0

Maybe someone is still looking for an answer, here's an example with rexexp. https://blogs.oracle.com/aramamoo/passing-comma-separated-string-as-bind-variable-for-vo-querys-in-operator-v2

In this case, each emp no is coma separated

WHERE Emp.ENAME in 
  (select regexp_substr(:Bind_Ename_Comma_Sep_List,'[^,]+', 1, level) 
   from dual 
    connect by 
        regexp_substr(:Bind_Ename_Comma_Sep_List, '[^,]+', 1, level) 
            is not null)
J. M. Arnold
  • 6,261
  • 3
  • 20
  • 38
0

Another way is to use the INSTR function: For example if the comma delimited data is in :xyz then you can determine if a value in a named item fldOne is in that list as follows:

INSTR(',' || :xyz || ',', ',' || fldOne ||',') > 0

will return true if the value fldOne references is in the list and false otherwise.

Of course, if you want to match against an explicit string, then you can replace fldOne with 'value'.

BTW this will also work if you created a comma separated variable using ListAgg or any other mechanism. If the comma separated list was named MyList then just replace :xyz above with MyList.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
  • You really should use code fencing and some other formatting in your post, it is hard to read as a single continuous line like that. – Chris Schaller Feb 15 '22 at 22:57