2008-09-29 11 views
118

Este es un problema que he pasado horas investigando en el pasado. Me parece que es algo que las soluciones modernas RDBMS deberían haber abordado, pero hasta ahora no he encontrado nada que realmente aborde lo que considero una necesidad increíblemente común en cualquier aplicación Web o Windows con un back-end de base de datos.Clasificación dinámica dentro de los procedimientos almacenados de SQL

Hablo de ordenación dinámica. En mi mundo de fantasía, debería ser tan simple como algo así como:

ORDER BY @sortCol1, @sortCol2 

Este es el ejemplo canónico dada por SQL novato y Stored Procedure desarrolladores de todo foros a través de Internet. "¿Por qué no es esto posible?" ellos preguntan. Invariablemente, alguien finalmente llega para darles una conferencia acerca de la naturaleza compilada de los procedimientos almacenados, de los planes de ejecución en general, y todo tipo de otras razones por las cuales no es posible poner un parámetro directamente en una cláusula ORDER BY.


Sé lo que algunos de ustedes ya están pensando: "Deje que el cliente haga la clasificación, entonces". Naturalmente, esto descarga el trabajo de su base de datos. Sin embargo, en nuestro caso, nuestros servidores de bases de datos ni siquiera están empezando a sudar el 99% del tiempo y ni siquiera son multinúcleo o cualquiera de las otras innumerables mejoras a la arquitectura del sistema que ocurren cada 6 meses. Solo por esta razón, hacer que nuestras bases de datos manejen la clasificación no sería un problema. Además, las bases de datos son muy bien en la clasificación. Están optimizados para ello y han tenido años para hacerlo bien, el lenguaje para hacerlo es increíblemente flexible, intuitivo y simple y, sobre todo, cualquier escritor principiante de SQL sabe cómo hacerlo y, lo que es más importante, saben cómo editarlo. hacer cambios, hacer mantenimiento, etc. Cuando sus bases de datos están lejos de ser gravadas y usted simplemente quiere simplificar (y acortar) el tiempo de desarrollo, esto parece una opción obvia.

Luego está el problema de la web. He jugado con JavaScript que hará la ordenación de tablas HTML por el lado del cliente, pero inevitablemente no son lo suficientemente flexibles para mis necesidades y, una vez más, dado que mis bases de datos no son excesivamente exigentes y realmente puedo clasificar realmente , Me cuesta mucho justificar el tiempo que tomaría volver a escribir o rodar mi propio clasificador de JavaScript. Lo mismo ocurre en general con la clasificación del lado del servidor, aunque ya es probablemente preferible a JavaScript. No soy uno que le guste especialmente la sobrecarga de DataSets, así que denígame.

Pero esto trae de vuelta el punto de que no es posible — o más bien, no es fácil. He hecho, con sistemas anteriores, una forma increíble de hackear la clasificación dinámica. No era bonito, ni intuitivo, simple o flexible, y un escritor de SQL principiante se perdería en cuestión de segundos. Ya esto no es tanto una "solución" sino una "complicación".


Los siguientes ejemplos no están destinados para exponer cualquier tipo de mejores prácticas o buen estilo de codificación o nada, ni son indicativos de mis habilidades como programador de T-SQL. Son lo que son y reconozco que son confusos, de mala forma y simplemente piratean.

Pasamos un valor entero como parámetro a un procedimiento almacenado (llamemos simplemente al parámetro "ordenar") y de eso determinamos un grupo de otras variables. Por ejemplo ... digamos que es un género 1 (o por defecto):

DECLARE @sortCol1 AS varchar(20) 
DECLARE @sortCol2 AS varchar(20) 
DECLARE @dir1 AS varchar(20) 
DECLARE @dir2 AS varchar(20) 
DECLARE @col1 AS varchar(20) 
DECLARE @col2 AS varchar(20) 

SET @col1 = 'storagedatetime'; 
SET @col2 = 'vehicleid'; 

IF @sort = 1    -- Default sort. 
BEGIN 
    SET @sortCol1 = @col1; 
    SET @dir1 = 'asc'; 
    SET @sortCol2 = @col2; 
    SET @dir2 = 'asc'; 
END 
ELSE IF @sort = 2   -- Reversed order default sort. 
BEGIN 
    SET @sortCol1 = @col1; 
    SET @dir1 = 'desc'; 
    SET @sortCol2 = @col2; 
    SET @dir2 = 'desc'; 
END 

Ya se puede ver como si declaré más variables @colX para definir otras columnas que podía realmente ser creativo con las columnas para ordenar basado en el valor de "ordenar" ...para usarlo, por lo general termina pareciéndose a la siguiente cláusula increíblemente desordenado:

ORDER BY 
    CASE @dir1 
     WHEN 'desc' THEN 
      CASE @sortCol1 
       WHEN @col1 THEN [storagedatetime] 
       WHEN @col2 THEN [vehicleid] 
      END 
    END DESC, 
    CASE @dir1 
     WHEN 'asc' THEN 
      CASE @sortCol1 
       WHEN @col1 THEN [storagedatetime] 
       WHEN @col2 THEN [vehicleid] 
      END 
    END, 
    CASE @dir2 
     WHEN 'desc' THEN 
      CASE @sortCol2 
       WHEN @col1 THEN [storagedatetime] 
       WHEN @col2 THEN [vehicleid] 
      END 
    END DESC, 
    CASE @dir2 
     WHEN 'asc' THEN 
      CASE @sortCol2 
       WHEN @col1 THEN [storagedatetime] 
       WHEN @col2 THEN [vehicleid] 
      END 
    END 

Obviamente, esto es un ejemplo muy simplificada. Las cosas reales, ya que usualmente tenemos cuatro o cinco columnas para apoyar la clasificación, cada una con una posible segunda columna secundaria o incluso una tercera para ordenar además de eso (por ejemplo, fecha descendente, luego ordenadas de forma secundaria por nombre ascendente) y cada una de clasificación direccional que efectivamente duplica el número de casos. Sí ... se pone peludo muy rápido.

La idea es que uno podría "fácilmente" cambiar los casos de clasificación de modo que vehicleid se clasifique antes de la fecha de almacenamiento ... pero la pseudo-flexibilidad, al menos en este simple ejemplo, realmente termina ahí. Esencialmente, cada caso que falla una prueba (porque nuestro método de clasificación no se aplica a esta vez) representa un valor NULO. Y así terminas con una cláusula que funciona de la siguiente manera:

ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah 

Ya entendiste la idea. Funciona porque SQL Server efectivamente ignora valores nulos en orden por cláusulas. Esto es increíblemente difícil de mantener, ya que cualquiera con conocimientos básicos de SQL puede ver. Si he perdido a alguno de ustedes, no se sientan mal. Nos llevó mucho tiempo hacerlo funcionar y aún nos confundimos al tratar de editarlo o crear otros nuevos como este. Afortunadamente, no es necesario cambiar a menudo, de lo contrario, rápidamente "no valdría la pena".

Sin embargo, hizo trabajo.


Mi pregunta es entonces: hay una manera mejor?

Estoy de acuerdo con las soluciones que no sean procedimientos almacenados, ya que me doy cuenta de que puede no ser el camino a seguir. Preferiblemente, me gustaría saber si alguien puede hacerlo mejor dentro del Procedimiento almacenado, pero si no, ¿cómo se las arregla para que el usuario pueda ordenar dinámicamente las tablas de datos (bidireccionalmente también) con ASP.NET?

¡Y gracias por leer (o al menos rozar) una pregunta tan larga!

PS: Alégrate de que no me muestro mi ejemplo de un procedimiento almacenado que apoya la clasificación dinámica, filtrado dinámico/texto-búsqueda de columnas, la paginación a través de ROWNUMBER() OVER, Y try ... catch con la transacción rollbacking en errores ... "del tamaño de un gigante" ni siquiera comienza a describirlos.


Actualización:

  • me gustaría evitar SQL dinámico. Analizar una cadena y ejecutar un EXEC en ella derrota el propósito de tener un procedimiento almacenado en primer lugar. A veces me pregunto si los inconvenientes de hacer tal cosa no valdrían la pena, al menos en estos casos especiales de clasificación dinámica. Aún así, siempre me siento sucio cada vez que hago cadenas dinámicas de SQL como esa — como si todavía estuviera viviendo en el mundo ASP clásico.
  • Una gran parte de la razón por la que queremos que los procedimientos almacenados en primer lugar sea para security. No llego a hacer un llamado por cuestiones de seguridad, solo sugiero soluciones. Con SQL Server 2005 podemos establecer permisos (por usuario, si es necesario) a nivel de esquema en procedimientos almacenados individuales y luego negar cualquier consulta directamente a las tablas.Criticar los pros y los contras de este enfoque es tal vez para otra pregunta, pero nuevamente no es mi decisión. Solo soy el mono del código principal. :)
+0

Consulte http://stackoverflow.com/questions/3659981/sql-server-dynamic-order-by-with-mixed-datatypes-is-this-a-good-idea también - SQL Server dinámico ORDER BY con tipos de datos mixtos – Lijo

+0

SQL dinámico es la forma LEJANA ... SI [y esta es una gran IF] ... tu capa de acceso a datos es estricta y tu SQL dinámico es generado por un sistema rígidamente programado con reglas RDBMS expresadas en forma perfecta . Una arquitectura de base de ingeniería algorítmica es una cosa de belleza ... – hajikelist

Respuesta

0

¿Qué hay de manejo de ordenación en el material que muestra los resultados - rejillas, informes, etc. en lugar de en SQL?

EDIT:

Para aclarar las cosas desde esta respuesta se bajó votó-antes, voy a elaborar un poco ...

Usted ha declarado que sabía acerca de la clasificación del lado del cliente, pero quería dirigir claro de eso Esa es su llamada, por supuesto.

Lo que quiero señalar, sin embargo, es que al hacerlo desde el lado del cliente, puede extraer datos UNA VEZ y luego trabajar con él como desee, en lugar de hacer múltiples viajes de ida y vuelta a el servidor cada vez que se cambia el tipo.

Su SQL Server no se está gravando en este momento y eso es increíble. No debería ser. Pero el hecho de que no esté sobrecargado no significa que se mantenga así para siempre.

Si se utiliza cualquiera de las cosas ASP.NET más reciente para la visualización en la web, una gran cantidad de cosas que ya se cuece en pleno.

¿Vale la pena añadir tanto código para cada procedimiento almacenado solo para manejar la clasificación? De nuevo, tu llamada.

No soy el que finalmente estará a cargo de apoyarlo. Pero reflexione sobre lo que se verá involucrado a medida que se agregan/eliminan columnas dentro de los diversos conjuntos de datos utilizados por los procedimientos almacenados (que requieren modificaciones a las declaraciones CASE) o cuando de repente, en lugar de clasificar por dos columnas, el usuario decide que necesitan tres: requiriendo que ahora actualice cada uno de sus procedimientos almacenados que usan este método.

Para mí, vale la pena obtener una solución funcional del lado del cliente y aplicarla a las pocas pantallas de datos que se visualizan para el usuario y que se haga con ella. Si se agrega una nueva columna, ya está procesada. Si el usuario desea ordenar por varias columnas, puede ordenar por dos o veinte de ellas.

+0

Esa sería la forma correcta, pero no se considera "una mejor manera" –

+0

Porque entonces todavía estoy escribiendo mi propia clasificación en C# o JavaScript y parece que debería ser mucho más fácil y rápido en SQL. Por lo tanto, mi pregunta. ¿Me estaba perdiendo algo obvio o estamos atascados escribiendo nuestra propia clasificación personalizada (en C# o JavaScript) en cada maldita aplicación en la que trabajamos? –

+3

Espera, ¿qué pasa con los conjuntos de resultados con decenas de miles de filas? No puede devolver todos esos datos al cliente. Tienes que hacer paginación y ordenar en la base de datos. –

2

En algún momento, ¿no vale la pena alejarse de los procedimientos almacenados y simplemente utilizar consultas parametrizadas para evitar este tipo de hackers?

+1

En algunos casos, tal vez son mazos en un clavo, pero a menudo queremos establecer permisos (EJECUTAR en particular) directamente en los procedimientos almacenados y no permitir consultas SQL directamente en las tablas, incluso SELECT. Tampoco me gustan mucho los hackers, pero la seguridad no es algo que deba hacer. –

+1

Esta es la razón por la cual muchas personas se están moviendo al mapeo relacional de objetos. Innecesarios viajes de ida y vuelta para clasificar, enormes bloques de CASE para las mismas actualizaciones sin sentido a toneladas de columnas cuando realmente solo se necesita actualizar una, etc. El único argumento ganador para los procedimientos almacenados que aún permanece es la seguridad. –

+0

Pasaré de un ORM (EF) a un procedimiento almacenado porque el ORM no admite la búsqueda de texto completo. –

84

Sí, es un dolor, y la forma en que está haciendo tiene una apariencia similar a lo que hago:

order by 
case when @SortExpr = 'CustomerName' and @SortDir = 'ASC' 
    then CustomerName end asc, 
case when @SortExpr = 'CustomerName' and @SortDir = 'DESC' 
    then CustomerName end desc, 
... 

Esto, para mí, sigue siendo mucho mejor que la construcción de SQL dinámico de código, que gira en una pesadilla de escalabilidad y mantenimiento para los DBA.

Lo que hago desde el código es refactorizar la paginación y la ordenación, así que al menos no tengo mucha repetición allí con los valores poblados para @SortExpr y @SortDir.

Por lo que respecta al SQL, mantenga el diseño y el formato de la misma manera entre los diferentes procedimientos almacenados, de modo que al menos sea claro y reconocible cuando ingrese para realizar cambios.

+1

Exactamente. Mi objetivo era evitar hacer un comando EXEC en una gran cadena 5000 varchar. Todo lo que hacemos debe hacerse a través de procedimientos almacenados, aunque solo sea para la seguridad adicional, ya que podemos establecer permisos sobre ellos en el nivel de esquema. La escalabilidad y el rendimiento son solo una ventaja en nuestro caso. –

+1

Agregue capacidad de mantenimiento a {seguridad, escalabilidad, rendimiento}. Una vez que tienes 3 o 4 aplicaciones con SQL dinámico ejecutándose contra tu base de datos, estás jodido, no puedes cambiar nada, especialmente a medida que las aplicaciones envejecen y los desarrolladores avanzan. Exec y sql dinámico son malvados. –

+0

Eso es todo --- ya lo hacemos, mucho antes de llegar aquí, para todas las aplicaciones web ASP ASP todavía en funcionamiento y muchas, muchas aplicaciones de Access VB que aún circulan. Me muevo y tengo que contener la urgencia de corregir errores flagrantes cada vez que tengo que realizar tareas de mantenimiento en cualquiera de ellos. –

4

Puede haber una tercera opción, ya que su servidor tiene muchos ciclos de repuesto: use un procedimiento de ayuda para hacer la clasificación a través de una tabla temporal. Algo así como

create procedure uspCallAndSort 
(
    @sql varchar(2048),  --exec dbo.uspSomeProcedure arg1,'arg2',etc. 
    @sortClause varchar(512) --comma-delimited field list 
) 
AS 
insert into #tmp EXEC(@sql) 
declare @msql varchar(3000) 
set @msql = 'select * from #tmp order by ' + @sortClause 
EXEC(@msql) 
drop table #tmp 
GO 

Advertencia: No he probado esto, pero "debería" trabajo en SQL Server 2005

2
(lo que creará una tabla temporal a partir de un conjunto de resultados sin especificar las columnas de antemano.)

Estoy de acuerdo, uso el lado del cliente. Pero parece que esa no es la respuesta que quieres escuchar.

Por lo tanto, es perfecto como es. No sé por qué querrías cambiarlo, o incluso preguntar "¿Hay una mejor manera?" Realmente, debería llamarse "El Camino". Además, parece funcionar y se adapta perfectamente a las necesidades del proyecto y probablemente sea extensible en los próximos años. Dado que sus bases de datos no están sujetas a impuestos y la clasificación es realmente muy fácil debería seguir así en los años venideros.

No lo sudaría.

+0

No tengo ningún problema con el lado del cliente, ya que voy por esa ruta con las aplicaciones de Windows. Pero, ¿qué hay de las aplicaciones web? No encuentro mucha solución de JavaScript lo suficientemente flexible. Y sí, funciona como dije de la manera en que lo tenemos, pero es una pesadilla de SQL. Por supuesto, me gustaría saber si hay mejores formas. –

+0

Está integrado en los controles .NET más nuevos (2.0 y superiores). O puede crear uno propio y aplicarlo a una vista de datos. http://msdn.microsoft.com/en-us/library/hwf94875(VS.80).aspx –

+2

Mi problema es uno de escalabilidad y rendimiento. Hacer la ordenación del lado del cliente o del lado del servidor web requiere cargar todos los datos en lugar de solo los 10 o 15 que se mostrarán en una página a la vez. Esto es extremadamente costoso, a largo plazo, mientras que la ordenación de bases de datos no tiene eso. –

5

Mis aplicaciones hacen esto mucho pero todas están construyendo dinámicamente el SQL. Sin embargo, cuando trato con procedimientos almacenados hago esto:

  1. Haga que el procedimiento almacenado sea una función que devuelva una tabla de sus valores - no ordena.
  2. Luego, en el código de su aplicación, haga un select * from dbo.fn_myData() where ... order by ... para que pueda especificar dinámicamente el orden de clasificación allí.

Entonces, al menos, la parte dinámica está en su aplicación, pero la base de datos todavía está haciendo el trabajo pesado. .

+1

Ese es probablemente el mejor compromiso que he visto hasta ahora entre el uso de SQL dinámico y procedimientos almacenados juntos. Me gusta. Puede que alguna vez experimente con un enfoque similar, pero tal cambio sería prohibitivo en cualquiera de nuestros proyectos en curso. –

+1

Puede lograr lo mismo utilizando una variable de tabla local en lugar de devolver datos de la función tabular. Encuentro que las tablas locales son más flexibles que las funciones, ya que puede generar información de depuración. –

4

Hay un par de maneras diferentes que usted puede cortar esto en

Requisitos:

  1. Sólo una instrucción SELECT en el sp
  2. Dejar de lado cualquier clasificación (o tienen un defecto)

Luego inserte en una tabla temporal:

create table #temp (your columns) 

insert #temp 
exec foobar 

select * from #temp order by whatever 

Método # 2: configurar un servidor vinculado de nuevo a sí mismo, a continuación, seleccione De esta usando openquery: http://www.sommarskog.se/share_data.html#OPENQUERY

-1

Esta solución sólo podría funcionar en .NET, no sé.

Recojo los datos en C# con el orden de clasificación inicial en la cláusula de orden SQL, coloco esos datos en un DataView, los guardo en una variable Session y los utilizo para construir una página.

Cuando el usuario hace clic en un encabezado de columna para ordenar (o página, o filtro), no vuelvo a la base de datos. En su lugar, vuelvo a mi DataView almacenado en caché y configuro su propiedad "Ordenar" a una expresión que construyo dinámicamente, tal como lo haría con el SQL dinámico. (Hago el filtrado de la misma manera, usando la propiedad "RowFilter").

Lo puede ver/sentir trabajando en una demostración de mi aplicación, BugTracker.NET, en http://ifdefined.com/btnet/bugs.aspx

+0

¡DULCE! ¡Bug tracker.NET rock! – digiguru

-6

Debe evitar la clasificación de SQL Server, a menos que sea necesario. ¿Por qué no ordenar en el servidor de la aplicación o el lado del cliente? También .NET Genéricos hace SORTIN excepcional

+6

Debido a la escalabilidad.Está bien para unos pocos miles de filas, pero no quiero bajar diez mil y ordenar eso. O más. Además, ¿qué pasa con la búsqueda? A menudo solo quiero sacar lo que necesito mostrar. Ordenar las filas 21-30 de 24056 después del hecho sería incorrecto. –

22

Este enfoque mantiene las columnas que se pueden ordenar puedan ser duplicados en dos ocasiones en el orden por, y es un poco más legible IMO:

SELECT 
    s.* 
FROM 
    (SELECT 
    CASE @SortCol1 
     WHEN 'Foo' THEN t.Foo 
     WHEN 'Bar' THEN t.Bar 
     ELSE null 
    END as SortCol1, 
    CASE @SortCol2 
     WHEN 'Foo' THEN t.Foo 
     WHEN 'Bar' THEN t.Bar 
     ELSE null 
    END as SortCol2, 
    t.* 
    FROM 
    MyTable t) as s 
ORDER BY 
    CASE WHEN @dir1 = 'ASC' THEN SortCol1 END ASC, 
    CASE WHEN @dir1 = 'DESC' THEN SortCol1 END DESC, 
    CASE WHEN @dir2 = 'ASC' THEN SortCol2 END ASC, 
    CASE WHEN @dir2 = 'DESC' THEN SortCol2 END DESC 
2

Cuando se paginación resultados ordenados, SQL dinámico es una buena opción Si está paranoico con la inyección de SQL, puede usar los números de columna en lugar del nombre de la columna. He hecho esto antes de usar valores negativos para descender. Algo como esto ...

declare @o int; 
set @o = -1; 

declare @sql nvarchar(2000); 
set @sql = N'select * from table order by ' + 
    cast(abs(@o) as varchar) + case when @o < 0 then ' desc' else ' asc' end + ';' 

exec sp_executesql @sql 

Luego solo necesita asegurarse de que el número esté dentro de 1 a # de columnas. Incluso podría expandir esto a una lista de números de columnas y analizar eso en una tabla de entradas usando una función como this. De allí tendría que construir la cláusula ORDER BY al igual que ...

declare @cols varchar(100); 
set @cols = '1 -2 3 6'; 

declare @order_by varchar(200) 

select @order_by = isnull(@order_by + ', ', '') + 
     cast(abs(number) as varchar) + 
     case when number < 0 then ' desc' else '' end 
from dbo.iter_intlist_to_tbl(@cols) order by listpos 

print @order_by 

Un inconveniente es que hay que recordar el orden de las columnas en el lado del cliente. Especialmente, cuando no muestra todas las columnas o las visualiza en un orden diferente. Cuando el cliente desea ordenar, asigna los nombres de las columnas al orden de las columnas y genera la lista de entradas.

+0

Utilizamos sp_executesql para generar consultas dinámicas de informes. Muy efectivo. El SQL no se puede construir desde la aplicación, pero los parámetros solo se insertan donde es necesario y se ejecuta de forma normal. –

3

Una técnica de procedimiento almacenado (¿hackeo?) He utilizado para evitar SQL dinámico para ciertos trabajos es tener una columna de clasificación única. Es decir,

SELECT 
    name_last, 
    name_first, 
    CASE @sortCol WHEN 'name_last' THEN [name_last] ELSE 0 END as mySort 
FROM 
    table 
ORDER BY 
    mySort 

Ésta es fácil de vencer en la sumisión - se puede concat campos en su columna mysort, invertir el orden con funciones matemáticas o de fecha, etc.

Preferiblemente sin embargo, yo uso mi asp .NET gridviews u otros objetos con ordenamiento incorporado para hacer la clasificación para mí DESPUÉS de recuperar los datos del servidor Sql. O incluso si no está incorporado, por ejemplo, tablas de datos, etc. en asp.net.

1

Un argumento en contra de hacer la clasificación en el lado del cliente es información de gran volumen y paginación. Una vez que el recuento de filas va más allá de lo que puede visualizar fácilmente, a menudo se clasifica como parte de un salto/toma, que probablemente desee ejecutar en SQL.

Para Entity Framework, puede usar un procedimiento almacenado para manejar su búsqueda de texto. Si encuentra el mismo problema de ordenación, la solución que he visto es utilizar un proceso almacenado para la búsqueda, devolviendo solo una clave de identificación establecida para la coincidencia. A continuación, vuelva a consultar (con el género) en contra de la base de datos usando los identificadores en una lista (contiene). EF maneja esto bastante bien, incluso cuando el conjunto de ID es bastante grande. Sí, se trata de dos viajes redondos, pero le permite mantener siempre su clasificación en la base de datos, lo que puede ser importante en algunas situaciones y le impide escribir una gran cantidad de lógica en el procedimiento almacenado.

Cuestiones relacionadas