Обновление 2014-12-18
С подавляющим ответом на главный вопрос «Нет», более интересные ответы были сосредоточены на части 2, как решить проблему производительности с помощью явного ORDER BY
. Хотя я уже отметил ответ, я не удивлюсь, если бы было еще более эффективное решение.
оригинал
Этот вопрос возник, потому что единственное чрезвычайно быстрое решение, которое я смог найти для конкретной проблемы, работает только без ORDER BY
предложения. Ниже приведен полный T-SQL, необходимый для решения проблемы, а также предложенное мной решение (если это имеет значение, я использую SQL Server 2008 R2).
--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
Вот планы выполнения для решений A и B соответственно:
Решение A дает нужную мне производительность, но я не смог заставить ее работать с той же производительностью при добавлении какого-либо предложения ORDER BY (например, см. Решение B). И, конечно, кажется, что решение A должно было бы доставлять результаты по порядку, так как 1) таблица имеет только один индекс, 2) принудительное выполнение поиска, что исключает возможность использования сканирования порядка размещения на основе страниц IAM. ,
Итак, мои вопросы:
Прав ли я, что это гарантирует порядок в этом случае без заказа по пункту?
Если нет, есть ли другой способ форсировать план, который так же быстр, как решение А, предпочтительно тот, который избегает сортировки? Обратите внимание, что это должно было бы решить точно такую же проблему (например
StoreID = 1
, найти 500 лучших клиентов, заказанных по их самой дорогой сумме покупки). Также необходимо будет использовать#Orders
таблицу, но с другими схемами индексации все будет в порядке.
источник
ORDER BY
.Ответы:
Нет . Потоковое отличие , сохраняющее порядок (разрешающий
ORDER BY
без сортировки), сегодня не реализовано в SQL Server. В принципе это можно сделать, но тогда многое возможно, если нам разрешат изменить исходный код SQL Server. Если у вас есть веские основания для этой разработки, вы можете предложить это Microsoft .Да. (Советы по таблицам и запросам требуются только при использовании показателя кардинальности до 2014 года):
SQL CLR решение
Следующий скрипт демонстрирует использование табличной функции SQL CLR для удовлетворения заявленных требований. Я не эксперт по C #, поэтому код может быть улучшен:
Тестовая таблица и пример данных из вопроса:
Функциональный тест:
План выполнения (обратите внимание на валидацию
ORDER
гарантии):На моем ноутбуке это обычно выполняется за 80-100 мс. Это далеко не так быстро, как переписывание T-SQL выше, но оно должно показывать хорошую стабильность производительности при различных распределениях данных.
Исходный код:
источник
Без
ORDER BY
много чего может пойти не так. Вы исключили все возможные проблемы, о которых я могу подумать, но это не означает, что проблем нет и не будет в будущем выпуске.Это должно работать:
Вытяните партии из 500 строк из таблицы в цикле и остановитесь, когда вы получите 500 различных идентификаторов клиентов. Запрос на выборку может выглядеть так:
Это выполнит упорядоченное сканирование диапазона индекса.
Amount <= @lastAmountFetched
Предикат есть пошагово тянуть больше записей. Каждый запрос только физически касается 500 записей. Это означает, что это O (1). Он не становится дороже, чем дальше вы попадаете в индекс.Вы должны поддерживать переменную так,
@lastAmountFetched
чтобы она уменьшалась до наименьшего значения, которое вы извлекли в этом выражении.Таким образом, вы будете постепенно сканировать индекс упорядоченным способом. Вы будете читать не более (500 - 1) строк больше, чем было бы оптимальное количество.
Это будет намного быстрее, чем всегда, собирая около 100000 заказов для определенного магазина. Возможно, потребуется всего несколько итераций по 500 строк в каждой.
По сути, это отдельный оператор потока, закодированный вручную.
Или используйте курсор, чтобы получить как можно меньше строк. Это будет намного медленнее, потому что выполнение 500 однострочных запросов чаще всего медленнее, чем выполнение пакета из 500 строк.
Альтернативно, просто запросите все строки без
DISTINCT
упорядоченного способа и заставьте клиентское приложение завершить запрос, как только будет возвращено достаточное количество строк (используяSqlCommand.Cancel
).источник
#fetchedOrders
отсутствие клиентов, которых мы уже видели? Предположительно это связано с индексом искать на временную таблицу, которая не совсем то же самое , как «поток отличается» и делает получить более дорогой, чем больше строк , которые мы видели (хотя он все равно будет бить решение B во всех , но в худшем случае необходимости сканировать все строки, потому что есть только один клиент, для которого A и B будут работать одинаково).IGNORE_DUP_KEY
может сделать это.