如何在具有非IN条件的已准备好的SQL语句中将[]切片传递给IN条件?

Imagine you have the following SQL query:

SELECT *
  FROM foo
  WHERE type = ?
    AND subtype IN (?)

And you have the following possible data (we imagine that a user interface can set these data):

var Type int
var SubTypes []int

In the case of SubTypes, we are talking about a multiple choice selection.

Now, the following code won't work:

rows, err := sqldb.Query(`SELECT *
  FROM foo
  WHERE type = ?
    AND subtype IN (?)`, Type, SubTypes)

Because the driver (at least the mysql driver used in this example) doesn't recognise a []slice. Typing to explode it (SubTypes...) doesn't work either, because A) you cannot have more than one exploded parameter and B) even if you could, your SQL only supports a single item ((?)).

Prepared statements do not work that way, at least not in major DBMS I know. I mean, in Go, the support for prepared statements implemented by database/sql drivers is supposed to use the corresponding facility provided by the underlying DBMS (a driver might opt to simulate such support if it's not provided by the DB engine it interfaces with).

Now in all the DBMS-s I'm familiar with, the whole idea of prepared statement is that it's processed once by the DB engine and cached; "processed" here means syntax checking, compiling into some DB-specific internal representation and its execution plan figured out. As follows from the term "compiled", the statement's text is processed exactly once, and then each call to the prepared statement just essentially tells the server "here is the ID of that prepared statement I supplied you earlier, and here's the list of actual parameters to use for placeholders it contained". It's like compiling a Go program and then calling it several times in a row with different command-line flags.

So the solution you have come up with is correct: if you want to mess with the statement text between invocation then by all means use client-side text manipulations1 but do not attempt to use the result of it as a prepared statement unless you really intend to execute the resulting text more than once.

And to be may be more clear: your initial attempt to prepare something like

SELECT a, b FROM foo WHERE a IN (?)

supposedly fails at your attempt to supply a set of values for that IN (?) placeholder because commas which would be required there to specify several values are syntax, not parts of the value.

I think it should still be fine to prepare something like

SELECT a, b FROM foo WHERE a IN (?, ?, ?)

because it does not break that rule. Not that it's a solution for you…

See also this and this — studying the latter would allow you to play with prepared statements directly in the MySQL client.


1 Some engines provide for server-side SQL generation with subsequent execution of the generated text.

However, there is a solution. First of all, since we can only have a single exploding parameter and no others, we should first put together our parameters in a single []slice:

var params []interface{}
params = append(params, Type)
for _, subtype := range SubTypes {
  params = append(params, SubTypes)
}

Since the SQL will not expand on its own, let's expand that loop:

var params []interface{}
params = append(params, Type)
inCondition := ""
for _, subtype := range SubTypes {
  params = append(params, SubTypes)
  if inCondition != "" {
    inCondition += ", "
  }
  inCondition += "?"
}

Assuming SubTypes contains []int{1,2,3}, inCondition should now contain ?, ?, ?.

We then combine that to our SQL statement and explode the argument:

sqlstr := fmt.Sprintf(`SELECT *
  FROM foo
  WHERE type = ?
    AND subtype IN (%s)`, inCodition)
rows, err := sqldb.Query(sqlstr, params...)

Of course, it would be pretty cool, if you could simply pass []slices to your prepared statements, and the automatically expanded. But that might give some unexpected results if you are dealing with more 'unknown' data.