Actualizar 2014-12-18
Dado que la respuesta abrumadora a la pregunta principal es "No", las respuestas más interesantes se han centrado en la parte 2, cómo resolver el acertijo de rendimiento de forma explícita ORDER BY
. Aunque ya marqué una respuesta, no me sorprendería si hubiera una solución aún mejor.
Original
Esta pregunta surgió porque la única solución extremadamente rápida que pude encontrar para un problema en particular solo funciona sin una ORDER BY
cláusula. A continuación se muestra el T-SQL completo necesario para producir el problema, junto con mi solución propuesta (estoy usando SQL Server 2008 R2, si eso es importante).
--Create Orders table
IF OBJECT_ID('tempdb..#Orders') IS NOT NULL DROP TABLE #Orders
CREATE TABLE #Orders
(
OrderID INT NOT NULL IDENTITY(1,1)
, CustID INT NOT NULL
, StoreID INT NOT NULL
, Amount FLOAT NOT NULL
)
CREATE CLUSTERED INDEX IX ON #Orders (StoreID, Amount DESC, CustID)
--Add 1 million rows w/ 100K Customers each of whom had 10 orders
;WITH
Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows
Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows
Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows
Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows
Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows
Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows
FinalCte AS (SELECT ROW_NUMBER() OVER (ORDER BY C) AS Number FROM Cte5)
INSERT INTO #Orders (CustID, StoreID, Amount)
SELECT CustID = Number / 10
, StoreID = Number % 4
, Amount = 1000 * RAND(Number)
FROM FinalCte
WHERE Number <= 1000000
SET STATISTICS IO ON
SET STATISTICS TIME ON
--For StoreID = 1, find the top 500 customers ordered by their most expensive purchase (Amount)
--Solution A: Without ORDER BY
DECLARE @Top INT = 500
SELECT DISTINCT TOP (@Top) CustID
FROM #Orders WITH(FORCESEEK)
WHERE StoreID = 1
OPTION(OPTIMIZE FOR (@Top = 1), FAST 1);
--9 logical reads, CPU Time = 0 ms, elapsed time = 1 ms
GO
--Solution B: With ORDER BY
DECLARE @Top INT = 500
SELECT TOP (@Top) CustID
FROM #Orders
WHERE StoreID = 1
GROUP BY CustID
ORDER BY MAX(Amount) DESC
OPTION(MAXDOP 1)
--745 logical reads, CPU Time = 141 ms, elapsed time = 145 ms
--Uses Sort operator
GO
Estos son los planes de ejecución para la Solución A y B, respectivamente:
La solución A proporciona el rendimiento que necesito, pero no pude lograr que funcione con el mismo rendimiento al agregar cualquier tipo de cláusula ORDER BY (por ejemplo, consulte la Solución B). Y ciertamente parece que la Solución A tendría que entregar sus resultados en orden, ya que 1) la tabla tiene solo un índice, 2) una búsqueda es forzada, eliminando así la posibilidad de usar un escaneo de órdenes de asignación basado en páginas IAM .
Entonces mis preguntas son:
¿Tengo razón en que garantizará el pedido en este caso sin un pedido por cláusula?
Si no, ¿hay otro método para forzar un plan que sea tan rápido como la Solución A, preferiblemente uno que evite los tipos? Tenga en cuenta que tendría que resolver exactamente el mismo problema (para
StoreID = 1
encontrar los 500 principales clientes ordenados por el monto de compra más caro). También tendría que seguir usando la#Orders
tabla, pero diferentes esquemas de indexación estarían bien.
fuente
ORDER BY
.Respuestas:
No. En la actualidad, SQL Server no implementa un distintivo de flujo que conserve el orden (permitiendo
ORDER BY
sin ordenar). En principio, es posible hacerlo, pero muchas cosas son posibles si se nos permite cambiar el código fuente de SQL Server. Si puede presentar un buen caso para este trabajo de desarrollo, puede sugerirlo a Microsoft .Sí. (Las sugerencias de tabla y consulta solo son necesarias cuando se usa el estimador de cardinalidad anterior a 2014):
Solución SQL CLR
El siguiente script muestra el uso de una función SQL CLR con valores de tabla para cumplir con los requisitos establecidos. No soy un experto en C #, por lo que el código puede mejorar:
Tabla de prueba y datos de muestra de la pregunta:
Prueba de funcionamiento:
Plan de ejecución (tenga en cuenta la validación de la
ORDER
garantía):En mi computadora portátil, esto normalmente se ejecuta en 80-100 ms. Esto no es tan rápido como la reescritura de T-SQL anterior, pero debería mostrar una buena estabilidad de rendimiento frente a las diferentes distribuciones de datos.
Código fuente:
fuente
Sin
ORDER BY
muchas cosas pueden salir mal. Ha excluido todos los posibles problemas que se me ocurren, pero eso no significa que no haya ningún problema ni habrá uno en una versión futura.Esto debería funcionar:
Extraiga lotes de 500 filas de la tabla en un bucle y deténgase cuando tenga 500 ID de clientes distintos. La consulta de búsqueda podría verse así:
Esto realizará un escaneo de rango ordenado en el índice. El
Amount <= @lastAmountFetched
predicado está ahí para extraer gradualmente más registros. Cada consulta solo tocará físicamente 500 registros. Eso significa que es O (1). No se vuelve más caro cuanto más te acercas al índice.Debe mantener la variable
@lastAmountFetched
para disminuir al valor más pequeño que obtuvo en esa declaración.De esta manera, escaneará gradualmente el índice de una manera ordenada. Leerá como máximo (500 - 1) filas más de lo que hubiera sido la cantidad óptima.
Esto será mucho más rápido que siempre agregando aproximadamente 100000 pedidos para una tienda en particular. Probablemente, solo se necesitarán unas pocas iteraciones de 500 filas cada una.
Esencialmente, este es un operador diferenciado de flujo codificado manualmente.
Alternativamente, use un cursor para buscar la menor cantidad de filas posible. Esto será mucho más lento porque la ejecución de 500 consultas de una sola fila con mayor frecuencia es más lenta que la ejecución de un lote de 500 filas.
Alternativamente, simplemente consulte todas las filas sin
DISTINCT
ordenarlas y haga que la aplicación cliente finalice la consulta una vez que se hayan devuelto suficientes filas (usandoSqlCommand.Cancel
).fuente
#fetchedOrders
que no contenga clientes que ya hemos visto? Presumiblemente, esto implica una búsqueda de índice en la tabla temporal, que no es exactamente lo mismo que un "flujo distinto" y se vuelve más costoso a medida que se ven más filas (aunque aún superará a la solución B en todos, excepto en el peor de los casos) de tener que escanear todas las filas porque solo hay un cliente, para el cual A y B funcionarán de manera idéntica).IGNORE_DUP_KEY
podría hacer eso.