¿Cómo parametrizo una consulta que contiene una IN
cláusula con un número variable de argumentos, como este?
SELECT * FROM Tags
WHERE Name IN ('ruby','rails','scruffy','rubyonrails')
ORDER BY Count DESC
En esta consulta, el número de argumentos puede ser de 1 a 5.
Preferiría no usar un procedimiento almacenado dedicado para esto (o XML), pero si hay alguna forma elegante específica para SQL Server 2008 , estoy abierto a eso.
sql
sql-server
parameters
Jeff Atwood
fuente
fuente
Respuestas:
Aquí hay una técnica rápida y sucia que he usado:
Así que aquí está el código C #:
Dos advertencias:
LIKE "%...%"
las consultas no están indexadas.|
etiqueta en blanco o nula o esto no funcionaráHay otras formas de lograr esto que algunas personas pueden considerar más limpias, así que por favor sigan leyendo.
fuente
Puedes parametrizar cada valor, así que algo como:
Lo que te dará:
No, esto no está abierto a la inyección SQL . El único texto inyectado en CommandText no se basa en la entrada del usuario. Se basa únicamente en el prefijo "@tag" codificado y el índice de una matriz. El índice siempre será un número entero, no es generado por el usuario y es seguro.
Los valores ingresados por el usuario todavía están incluidos en los parámetros, por lo que no hay vulnerabilidad allí.
Editar:
No es que los planes de consulta en caché no sean valiosos, pero IMO esta consulta no es lo suficientemente complicada como para ver muchos beneficios. Si bien los costos de compilación pueden acercarse (o incluso exceder) los costos de ejecución, todavía está hablando milisegundos.
Si tiene suficiente RAM, esperaría que SQL Server probablemente también almacenara en caché un plan para los recuentos comunes de parámetros. Supongo que siempre puede agregar cinco parámetros y dejar que las etiquetas no especificadas sean NULL: el plan de consulta debería ser el mismo, pero me parece bastante feo y no estoy seguro de que valga la pena la microoptimización (aunque, en Desbordamiento de pila: puede valer la pena).
Además, SQL Server 7 y versiones posteriores parametrizarán automáticamente las consultas , por lo que el uso de parámetros no es realmente necesario desde el punto de vista del rendimiento; sin embargo, es crítico desde el punto de vista de la seguridad, especialmente con datos ingresados por el usuario como este.
fuente
Para SQL Server 2008, puede usar un parámetro con valores de tabla . Es un poco de trabajo, pero podría decirse que es más limpio que mi otro método .
Primero, debes crear un tipo
Entonces, su código ADO.NET se ve así:
fuente
SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);
? En teoría, este realmente debería ser el enfoque más rápido. Puede usar índices relevantes (p. Ej., Un índice en el nombre de la etiqueta queINCLUDE
contar sería ideal), y SQL Server debería hacer algunos intentos para obtener todas las etiquetas y sus cuentas. ¿Cómo se ve el plan?La pregunta original era "¿Cómo parametrizo una consulta ..."
Permítanme decir aquí, que esta no es una respuesta a la pregunta original. Ya hay algunas demostraciones de eso en otras buenas respuestas.
Dicho esto, continúe y marque esta respuesta, vote hacia abajo, márquela como no una respuesta ... haga lo que crea que es correcto.
Vea la respuesta de Mark Brackett para la respuesta preferida que yo (y otras 231) voté. El enfoque dado en su respuesta permite 1) el uso efectivo de variables de enlace, y 2) para predicados que son sargable.
Respuesta seleccionada
Lo que quiero abordar aquí es el enfoque dado en la respuesta de Joel Spolsky, la respuesta "seleccionada" como la respuesta correcta.
El enfoque de Joel Spolsky es inteligente. Y funciona razonablemente, exhibirá un comportamiento predecible y un rendimiento predecible, dados los valores "normales", y con los casos límite normativos, como NULL y la cadena vacía. Y puede ser suficiente para una aplicación en particular.
Pero en términos de generalización de este enfoque, consideremos también los casos de esquina más oscuros, como cuando la
Name
columna contiene un carácter comodín (como lo reconoce el predicado LIKE). El carácter comodín que veo más comúnmente usado es%
(un signo de porcentaje). Así que lidiemos con eso aquí ahora, y luego pasemos a otros casos.Algunos problemas con% character
Considere un valor de Nombre de
'pe%ter'
. (Para los ejemplos aquí, utilizo un valor de cadena literal en lugar del nombre de la columna.) Una consulta del formulario devolvería una fila con un valor de Nombre de 'pe% ter':Pero esa misma fila no se devolverá si se invierte el orden de los términos de búsqueda:
El comportamiento que observamos es un poco extraño. Cambiar el orden de los términos de búsqueda en la lista cambia el conjunto de resultados.
Casi no hace falta decir que no queremos
pe%ter
igualar la mantequilla de maní, no importa cuánto le guste.Estuche de esquina oscuro
(Sí, estaré de acuerdo en que este es un caso oscuro. Probablemente uno que probablemente no sea probado. No esperaríamos un comodín en el valor de una columna. Podemos suponer que la aplicación evita que se almacene dicho valor. Pero En mi experiencia, rara vez he visto una restricción de la base de datos que específicamente no permitía caracteres o patrones que se considerarían comodines en el lado derecho de un
LIKE
operador de comparación.Parchar un agujero
Un enfoque para parchar este agujero es escapar del
%
personaje comodín. (Para cualquiera que no esté familiarizado con la cláusula de escape del operador, aquí hay un enlace a la documentación de SQL Server .Ahora podemos hacer coincidir el% literal. Por supuesto, cuando tenemos un nombre de columna, vamos a necesitar escapar dinámicamente del comodín. Podemos usar la
REPLACE
función para encontrar ocurrencias del%
personaje e insertar un carácter de barra diagonal inversa delante de cada uno, de esta manera:Eso resuelve el problema con el comodín%. Casi.
Escapar del escape
Reconocemos que nuestra solución ha introducido otro problema. El personaje de escape. Vemos que también vamos a necesitar escapar de cualquier ocurrencia del personaje de escape en sí. Esta vez, usamos el! como el personaje de escape:
El guión bajo también
Ahora que estamos en un rollo, podemos agregar otro
REPLACE
identificador al comodín de subrayado. Y solo por diversión, esta vez, usaremos $ como el personaje de escape.Prefiero este enfoque a escapar porque funciona en Oracle y MySQL, así como en SQL Server. (Usualmente uso la barra invertida \ como el carácter de escape, ya que ese es el carácter que usamos en las expresiones regulares. ¡Pero por qué estar limitado por la convención!
Esos corchetes molestos
SQL Server también permite que los caracteres comodín se traten como literales encerrándolos entre paréntesis
[]
. Así que todavía no hemos terminado de arreglar, al menos para SQL Server. Dado que los pares de paréntesis tienen un significado especial, también necesitaremos escapar de esos. Si logramos escapar adecuadamente de los corchetes, al menos no tendremos que molestarnos con el guión-
y el quilate^
dentro de los corchetes. Y podemos dejar cualquier%
y_
dentro de los corchetes escapados, ya que básicamente habremos deshabilitado el significado especial de los corchetes.Encontrar pares de paréntesis no debería ser tan difícil. Es un poco más difícil que manejar las ocurrencias de singleton% y _. (Tenga en cuenta que no es suficiente simplemente escapar de todas las apariciones de paréntesis, porque un paréntesis único se considera literal y no necesita escapar. La lógica se está volviendo un poco más borrosa de lo que puedo manejar sin ejecutar más casos de prueba .)
La expresión en línea se vuelve desordenada
Esa expresión en línea en el SQL se está volviendo más larga y fea. Probablemente podamos hacerlo funcionar, pero el cielo ayuda a la pobre alma que viene detrás y tiene que descifrarlo. Como soy un gran admirador de las expresiones en línea, me inclino a no usar una aquí, principalmente porque no quiero tener que dejar un comentario explicando la razón del desastre y disculpándome por ello.
Una función donde?
Bien, entonces, si no manejamos eso como una expresión en línea en el SQL, la alternativa más cercana que tenemos es una función definida por el usuario. Y sabemos que eso no acelerará las cosas (a menos que podamos definir un índice en él, como podríamos hacerlo con Oracle). Si tenemos que crear una función, podríamos hacerlo mejor en el código que llama al SQL declaración.
Y esa función puede tener algunas diferencias de comportamiento, dependiendo del DBMS y la versión. (Un agradecimiento a todos los desarrolladores de Java tan interesados en poder utilizar cualquier motor de base de datos indistintamente).
Conocimiento del dominio
Es posible que tengamos un conocimiento especializado del dominio de la columna, (es decir, el conjunto de valores permitidos para la columna. Podemos saber a priori que los valores almacenados en la columna nunca contendrán un signo de porcentaje, un guión bajo o un corchete pares. En ese caso, solo incluimos un comentario rápido de que esos casos están cubiertos.
Los valores almacenados en la columna pueden permitir% o _ caracteres, pero una restricción puede requerir que esos valores se escapen, tal vez usando un carácter definido, de modo que los valores sean como la comparación "segura". Una vez más, un comentario rápido sobre el conjunto de valores permitidos, y en particular qué personaje se usa como personaje de escape, y sigue el enfoque de Joel Spolsky.
Pero, sin el conocimiento especializado y una garantía, es importante para nosotros al menos considerar manejar esos oscuros casos de esquina, y considerar si el comportamiento es razonable y "según la especificación".
Otros temas recapitulados
Creo que otros ya han señalado suficientemente algunas de las otras áreas de preocupación comúnmente consideradas:
Inyección de SQL (tomar lo que parece ser información suministrada por el usuario e incluirla en el texto de SQL en lugar de proporcionarla a través de variables de enlace. No es necesario usar variables de enlace, es solo un enfoque conveniente para frustrar con la inyección de SQL. Hay otros maneras de lidiar con eso:
plan optimizador que utiliza exploración de índice en lugar de búsquedas de índice, posible necesidad de una expresión o función para escapar de comodines (posible índice de expresión o función)
el uso de valores literales en lugar de variables de enlace afecta la escalabilidad
Conclusión
Me gusta el enfoque de Joel Spolsky. Es inteligente Y funciona.
Pero tan pronto como lo vi, inmediatamente vi un problema potencial con él, y no es mi naturaleza dejarlo pasar. No quiero criticar los esfuerzos de los demás. Sé que muchos desarrolladores toman su trabajo muy personalmente, porque invierten mucho en él y les importa mucho. Entonces, por favor, comprenda, esto no es un ataque personal. Lo que estoy identificando aquí es el tipo de problema que surge en la producción en lugar de las pruebas.
Sí, me he alejado mucho de la pregunta original. Pero, ¿dónde más dejar esta nota sobre lo que considero un problema importante con la respuesta "seleccionada" para una pregunta?
fuente
Puedes pasar el parámetro como una cadena
Entonces tienes la cuerda
Entonces todo lo que tiene que hacer es pasar la cadena como 1 parámetro.
Aquí está la función de división que uso.
fuente
Escuché a Jeff / Joel hablar sobre esto en el podcast de hoy ( episodio 34 , 2008-12-16 (MP3, 31 MB), 1 h 03 min 38 segundos - 1 h 06 min 45 segundos), y pensé que recordé Stack Overflow estaba usando LINQ to SQL , pero tal vez fue abandonado. Aquí está lo mismo en LINQ to SQL.
Eso es. Y sí, LINQ ya mira hacia atrás lo suficiente, pero la
Contains
cláusula me parece extra hacia atrás. Cuando tuve que hacer una consulta similar para un proyecto en el trabajo, naturalmente intenté hacerlo de la manera incorrecta haciendo una unión entre la matriz local y la tabla de SQL Server, pensando que el traductor LINQ to SQL sería lo suficientemente inteligente como para manejar el traducción de alguna manera. No lo hizo, pero proporcionó un mensaje de error que fue descriptivo y me señaló hacia el uso de Contains .De todos modos, si ejecuta esto en el LINQPad altamente recomendado y ejecuta esta consulta, puede ver el SQL real que generó el proveedor de SQL LINQ. Le mostrará cada uno de los valores que se parametrizan en una
IN
cláusula.fuente
Si llama desde .NET, puede usar Dapper dot net :
Aquí Dapper piensa, así que no tienes que hacerlo. Algo similar es posible con LINQ to SQL , por supuesto:
fuente
Esta es posiblemente una forma medio desagradable de hacerlo, lo usé una vez, fue bastante efectivo.
Dependiendo de sus objetivos, puede ser útil.
INSERT
cada valor de búsqueda en esa columna.IN
, puede usar susJOIN
reglas estándar . (Flexibilidad ++)Esto tiene un poco de flexibilidad adicional en lo que puede hacer, pero es más adecuado para situaciones en las que tiene que consultar una tabla grande, con una buena indexación, y desea usar la lista parametrizada más de una vez. Ahorra tener que ejecutarlo dos veces y tener todo el saneamiento hecho manualmente.
Nunca llegué a perfilar exactamente qué tan rápido era, pero en mi situación era necesario.
fuente
En
SQL Server 2016+
podría usar laSTRING_SPLIT
función:o:
Demo en vivo
La respuesta aceptada , por supuesto, funcionará y es una de las formas de hacerlo, pero es antipatrón.
Anexo :
Para mejorar el
STRING_SPLIT
estimación de filas de la función de tabla, es una buena idea materializar los valores divididos como tabla / variable de tabla temporal:SEDE - Demo en vivo
Relacionado: Cómo pasar una lista de valores a un procedimiento almacenado
La pregunta original tiene requerimiento
SQL Server 2008
. Debido a que esta pregunta a menudo se usa como duplicado, he agregado esta respuesta como referencia.fuente
Tenemos una función que crea una variable de tabla a la que puede unirse:
Entonces:
fuente
Esto es asqueroso, pero si tiene garantizado tener al menos uno, puede hacer lo siguiente:
Tener SQL ('etiqueta1', 'etiqueta2', 'etiqueta1', 'etiqueta1', 'etiqueta1') será fácilmente optimizado por SQL Server. Además, obtienes búsquedas directas de índices
fuente
En mi opinión, la mejor fuente para resolver este problema es lo que se ha publicado en este sitio:
Syscomments. Dinakar Nethi
Utilizar:
CRÉDITOS PARA: Dinakar Nethi
fuente
Pasaría un parámetro de tipo de tabla (ya que es SQL Server 2008 ) y haría una
where exists
unión interna. También puede usar XML, usandosp_xml_preparedocument
, y luego incluso indexar esa tabla temporal.fuente
La manera correcta en mi humilde opinión es almacenar la lista en una cadena de caracteres (limitada en longitud por lo que admite el DBMS); El único truco es que (para simplificar el procesamiento) tengo un separador (una coma en mi ejemplo) al principio y al final de la cadena. La idea es "normalizar sobre la marcha", convirtiendo la lista en una tabla de una columna que contenga una fila por valor. Esto te permite girar
en una
o (la solución probablemente preferiría) una unión regular, si solo agrega un "distintivo" para evitar problemas con valores duplicados en la lista.
Desafortunadamente, las técnicas para cortar una cadena son bastante específicas del producto. Aquí está la versión de SQL Server:
La versión de Oracle:
y la versión de MySQL:
(Por supuesto, "pivote" debe devolver tantas filas como la cantidad máxima de elementos que podemos encontrar en la lista)
fuente
Si tiene SQL Server 2008 o posterior, usaría un parámetro de valor de tabla .
Si tiene la mala suerte de estar atascado en SQL Server 2005 , puede agregar una función CLR como esta,
Que podrías usar así,
fuente
Creo que este es un caso cuando una consulta estática no es el camino a seguir. Cree dinámicamente la lista para su cláusula in, escape de sus comillas simples y cree dinámicamente SQL. En este caso, probablemente no verá mucha diferencia con ningún método debido a la pequeña lista, pero el método más eficiente es enviar el SQL exactamente como está escrito en su publicación. Creo que es un buen hábito escribirlo de la manera más eficiente, en lugar de hacer lo que hace el código más bonito, o considerar que es una mala práctica construir dinámicamente SQL.
He visto que las funciones divididas tardan más en ejecutarse que la consulta en sí en muchos casos donde los parámetros se hacen más grandes. Un procedimiento almacenado con parámetros con valores de tabla en SQL 2008 es la única otra opción que consideraría, aunque probablemente sea más lento en su caso. TVP probablemente solo será más rápido para listas grandes si está buscando en la clave principal de TVP, porque SQL creará una tabla temporal para la lista de todos modos (si la lista es grande). No lo sabrá con seguridad a menos que lo pruebe.
También he visto procedimientos almacenados que tenían 500 parámetros con valores predeterminados de nulo y que tenían WHERE Column1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500). Esto hizo que SQL construyera una tabla temporal, hiciera una clasificación / distinción y luego hiciera un escaneo de la tabla en lugar de una búsqueda de índice. Eso es esencialmente lo que estaría haciendo al parametrizar esa consulta, aunque en una escala lo suficientemente pequeña como para que no haga una diferencia notable. Recomiendo encarecidamente que no tenga NULL en sus listas IN, ya que si eso se cambia a NOT IN no actuará como se esperaba. Podría construir dinámicamente la lista de parámetros, pero lo único obvio que obtendría es que los objetos escaparían de las comillas simples por usted. Ese enfoque también es un poco más lento al final de la aplicación, ya que los objetos tienen que analizar la consulta para encontrar los parámetros.
La reutilización de planes de ejecución para procedimientos almacenados o consultas parametrizadas puede proporcionarle una ganancia de rendimiento, pero lo bloqueará en un plan de ejecución determinado por la primera consulta que se ejecute. Eso puede ser menos que ideal para consultas posteriores en muchos casos. En su caso, la reutilización de los planes de ejecución probablemente será una ventaja, pero podría no hacer ninguna diferencia, ya que el ejemplo es una consulta realmente simple.
Notas de los acantilados:
Para su caso, haga lo que haga, ya sea la parametrización con un número fijo de elementos en la lista (nulo si no se usa), construir dinámicamente la consulta con o sin parámetros, o usar procedimientos almacenados con parámetros con valores de tabla no hará mucha diferencia . Sin embargo, mis recomendaciones generales son las siguientes:
Su caso / consultas simples con pocos parámetros:
SQL dinámico, tal vez con parámetros si las pruebas muestran un mejor rendimiento.
Consultas con planes de ejecución reutilizables, llamadas múltiples veces simplemente cambiando los parámetros o si la consulta es complicada:
SQL con parámetros dinámicos.
Consultas con grandes listas:
Procedimiento almacenado con parámetros con valores de tabla. Si la lista puede variar en gran medida, use WITH RECOMPILE en el procedimiento almacenado, o simplemente use SQL dinámico sin parámetros para generar un nuevo plan de ejecución para cada consulta.
fuente
Puede ser que podamos usar XML aquí:
fuente
CTE
y@x
se puede eliminar / incluir en la subselección, si se hace con mucho cuidado, como se muestra en este artículo .Me acercaría a esto de manera predeterminada al pasar una función con valores de tabla (que devuelve una tabla de una cadena) a la condición IN.
Aquí está el código para el UDF (lo obtuve de Stack Overflow en alguna parte, no puedo encontrar la fuente en este momento)
Una vez que tenga esto, su código sería tan simple como esto:
A menos que tenga una cadena ridículamente larga, esto debería funcionar bien con el índice de la tabla.
Si es necesario, puede insertarlo en una tabla temporal, indexarlo y luego ejecutar una unión ...
fuente
Otra posible solución es en lugar de pasar un número variable de argumentos a un procedimiento almacenado, pasar una sola cadena que contenga los nombres que busca, pero hacerlos únicos rodeándolos con '<>'. Luego use PATINDEX para encontrar los nombres:
fuente
Use el siguiente procedimiento almacenado. Utiliza una función de división personalizada, que se puede encontrar aquí .
fuente
Si tenemos cadenas almacenadas dentro de la cláusula IN con la coma (,) delimitada, podemos usar la función charindex para obtener los valores. Si usa .NET, puede mapear con SqlParameters.
Script DDL:
T-SQL:
Puede usar la declaración anterior en su código .NET y asignar el parámetro con SqlParameter.
Demo de Fiddler
EDITAR: cree la tabla llamada SelectedTags con el siguiente script.
Script DDL:
T-SQL:
fuente
Para un número variable de argumentos como este, la única forma que conozco es generar el SQL explícitamente o hacer algo que implique llenar una tabla temporal con los elementos que desea y unirlos contra la tabla temporal.
fuente
En ColdFusion solo hacemos:
fuente
Aquí hay una técnica que recrea una tabla local para ser utilizada en una cadena de consulta. Hacerlo de esta manera elimina todos los problemas de análisis.
La cadena se puede construir en cualquier idioma. En este ejemplo, usé SQL ya que ese era el problema original que estaba tratando de resolver. Necesitaba una forma limpia de pasar los datos de la tabla sobre la marcha en una cadena para ejecutarlos más tarde.
Usar un tipo definido por el usuario es opcional. La creación del tipo solo se crea una vez y se puede hacer con anticipación. De lo contrario, simplemente agregue un tipo de tabla completa a la declaración en la cadena.
El patrón general es fácil de extender y puede usarse para pasar tablas más complejas.
fuente
En SQL Server 2016+, otra posibilidad es usar la
OPENJSON
función.Este enfoque se bloguea en OPENJSON, una de las mejores formas de seleccionar filas por lista de identificadores .
Un ejemplo completo a continuación
fuente
Aquí hay otra alternativa. Simplemente pase una lista delimitada por comas como un parámetro de cadena al procedimiento almacenado y:
Y la función:
fuente
Tengo una respuesta que no requiere un UDF, XML porque IN acepta una instrucción select, por ejemplo, SELECT * FROM Prueba donde Data IN (SELECT Value FROM TABLE)
Realmente solo necesita una forma de convertir la cadena en una tabla.
Esto se puede hacer con un CTE recursivo, o una consulta con una tabla de números (o Master..spt_value)
Aquí está la versión CTE.
fuente
Utilizo una versión más concisa de la respuesta más votada :
Hace un bucle a través de los parámetros de la etiqueta dos veces; pero eso no importa la mayor parte del tiempo (no será su cuello de botella; si es así, desenrolle el bucle).
Si está realmente interesado en el rendimiento y no quiere repetir el ciclo dos veces, aquí hay una versión menos hermosa:
fuente
Aquí hay otra respuesta a este problema.
(nueva versión publicada el 4/6/13).
Salud.
fuente
El único movimiento ganador es no jugar.
No hay variabilidad infinita para ti. Solo variabilidad finita.
En el SQL tienes una cláusula como esta:
En el código C # haces algo como esto:
Básicamente, si el recuento es 0, entonces no hay filtro y todo pasa. Si el recuento es superior a 0, entonces el valor debe estar en la lista, pero la lista se ha completado a cinco con valores imposibles (para que el SQL todavía tenga sentido)
A veces, la solución poco convincente es la única que realmente funciona.
fuente