У меня есть таблица «Клиенты» с семью столбцами: «Идентификатор», «Имя клиента», «Адрес1», «Адрес2», «Город», «Штат», «Почтовый индекс». Я пытаюсь выполнить разбиение курсора на страницы через API по набору результатов, но пользователь может определить столбец сортировки. Ни для одного поля, кроме поля ID (которое автоматически увеличивается), нет ограничений уникальности. Когда я выполняю сортировку по полю идентификатора, я могу без проблем просмотреть весь набор данных. Например, мой первый вызов к столу:
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
ORDER BY
c.Id desc
Я беру 10 результатов, устанавливаю 11-й в качестве курсора, а затем мой следующий запрос будет
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
where ID <= 999990
ORDER BY
c.Id desc
И так далее и тому подобное.
Если я просматриваю столбец, который не так прост, я сталкиваюсь с запутанной проблемой:
Вот набор результатов, упорядоченный по имени клиента.
Итак, в этом случае я бы установил курсор на «Клиент 99999», и мой SQL-запрос был бы
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
WHERE CustomerName <= 'Customer 99999'
ORDER BY
C.CustomerName DESC
и я получу следующие результаты:
Это отлично работает, когда результаты не имеют дубликатов в столбцах. Но когда я сортирую по городу, есть много строк с одним и тем же городом, поэтому я подумал, что мне всегда следует сортировать по двум ключам: введенный столбец, а затем уникальный ключ (ID). Итак, мой SQL будет выглядеть так:
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
ORDER BY
C.City DESC, C.ID DESC
Итак, теперь моя логика заключалась бы в том, чтобы отслеживать значение отсортированного столбца 11-й строки и идентификатор. В этом случае я бы ввел City-99,998999 в качестве курсора. Итак, мой следующий запрос будет:
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
where c.city <= 'City-99' and ID <= 998999
ORDER BY
C.City DESC, C.ID DESC
И это работает. Но если я применю ту же логику к набору данных, не имеющему повторяющихся значений, логика перестанет работать.
Если бы я взял 11-е значения из этого набора данных, отсортированного по имени клиента:
И возьмем 11-ю строку в качестве курсора, мой запрос будет следующим:
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
where c.CustomerName <= 'Customer 99999' and ID <= 99999
ORDER BY
C.CustomerName DESC, C.ID DESC
Есть ли метод, о котором я не думаю, чтобы создать простое правило для эффективного перемещения курсора? Кажется, что иногда использование двух ключей полезно, когда в выбранном столбце есть повторяющиеся значения, но бесполезно, когда повторяющихся значений нет.
Если вы хотите получить повторяемые результаты, вы должны использовать порядок, который дает уникальный порядок.
Я не думаю, что ваша логика работает. Вероятно, вам следует сделать ИЛИ stackoverflow.com/a/19609938/13061224 потому что нет гарантии, что идентификатор всегда убывает
@Dalek Ваш первый комментарий - это не нумерация страниц курсором. Я пытаюсь избежать смещения/выборки производительности.
Но не уверен, что разбиение на страницы набора ключей сильно поможет, вы все равно, вероятно, получите плохую производительность, если поля не проиндексированы. Возможно, ваш API не должен позволять случайный просмотр всей вашей базы данных клиентов без фильтров.
Да, динамический поиск + нумерация страниц практически невозможно обеспечить хорошую производительность.
По сути, вам понадобится индекс для каждого столбца сортировки. Если индекса нет, вы не сможете найти строки 501–600, не отсортировав всю таблицу. Теперь вам не обязательно охватывать все индексы; например, используя индекс всего лишь CustomerName
, вы легко можете сделать WITH rws AS (SELECT PK, CustomerName FROM table ORDER BY ... OFFSET FETCH ...) SELECT <table.cols> FROM table JOIN rws ON table.PK = rows.PK ORDER BY rws.CustomerName;
Индекс в столбце сортировки позволяет легко находить строки Nth
->Nth+PageSize
, а затем использовать идентификаторы (которые включены в индексы NC по умолчанию), чтобы найти только эти PageSize
строки и получить остальные данные. Покрывающий индекс может помочь в этом случае, если вам не нужны все столбцы, но в противном случае, в зависимости от количества строк на странице, он все равно может выполнить сканирование, чтобы найти все эти строки. Но одно сканирование обычно лучше, чем два. Некоторые подходы и их относительную стоимость вы можете увидеть здесь.
И всегда, чтобы получить предсказуемые результаты, вам не нужно добавлять столбцы в предложение WHERE
, вам нужно добавить их к тому, что вычисляет номер строки. По сути, вам нужно добавить достаточное количество столбцов для такой сортировки, чтобы каждая строка была уникальной; это единственный способ получить предсказуемую сортировку.
(И в примере вы можете увидеть, как различные подходы масштабируются по мере увеличения количества страниц. Например, измените 500 на 50 000 в каждом из них и обратите внимание, что первый пример увеличивается в 8 раз, второй остается постоянным. , а в третьем примере увеличивается лишь на небольшую величину. Наведите указатель мыши на узел SELECT
в каждом графическом плане выполнения и посмотрите на значение рядом с Estimated Subtree Cost
.)
На самом деле, в некотором смысле, неиндексированный поиск может быть быстрым, потому что после первого сканирования таблицы строки будут находиться в памяти, что обеспечит быстрый доступ во второй раз :)
@siggemannen Но сортировка по-прежнему может быть довольно трудоемкой операцией, независимо от того, находятся ли данные уже в памяти или нет, особенно в системах, уже привязанных к процессору, и особенно в системах, где размер таблицы> ОЗУ (да, таких существует много).
Всегда сортируйте, используя «уникальный ключ» при реализации внутренней нумерации страниц.
Тривиальный случай: вы сортируете по ID
, это ключ.
При сортировке по другому критерию, который не является уникальным, сделайте его уникальным. Например, при сортировке по City
добавьте ID
в конце условия сортировки, например:
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
ORDER BY
C.City DESC, C.ID DESC -- added ID here
Добавляя ID
в конце предложения ORDER BY
, вы делаете столбцы сортировки уникальными. Таким образом, внутренняя нумерация страниц будет работать должным образом при получении следующей страницы и т. д., например:
SELECT TOP(11)
c.*
FROM [Customers] AS [c]
where c.city < 'City-99' or c.city = 'City-99' and ID <= 998999
ORDER BY
C.City DESC, C.ID DESC
Поскольку SQL Server не реализует «кортежное неравенство», предикат в предложении WHERE
выглядит неуклюжим. Однако это будет работать хорошо.
Примечание. Кортежное неравенство может перефразировать предикат поиска как (c.city, c.id) <= ('City-99', 998999)
. Они реализованы в DB2, PostgreSQL, MySQL, MariaDB. К сожалению, они не реализованы в Oracle или SQL Server. Они позволяют гораздо лучше оптимизировать запрос, если производительность является проблемой.
Я попробовал ваш последний SQL, и из-за начального условия он всегда возвращается к началу списка, независимо от идентификатора, который я помещаю в поле идентификатора, когда существует несколько городов-99.
@Josh В предикате WHERE была небольшая опечатка. Теперь это исправлено. См. пример бега.
ваш ответ, а также ссылка на веб-сайт use-the-index-luke.com помогли получить необходимый sql. Я подробно опишу это в последующем ответе, но отмечу ваш ответ как ответ. Спасибо за помощь
Разбивка на страницы в SQL Server может выполняться двумя разными способами в стандартном ISO SQL:
1) только с ORDER BY:
SELECT *
FROM Customers
ORDER BY City DESC, ID DESC OFFSET 0 ROWS FETCH NEXT 11 ROWS ONLY;
Чтобы разбить на страницы страницу из 10 строк и страницу 7, вы можете выполнить этот запрос:
SELECT *
FROM Customers
ORDER BY City DESC, ID DESC OFFSET 1 + 10 * 7 ROWS FETCH NEXT 10 ROWS ONLY;
В более общем смысле вы можете использовать переменные SQL:
DECLARE @PAGE INT = 7, @ROWS INT = 10;
SELECT *
FROM Customers
ORDER BY City DESC, ID DESC OFFSET 1 + @ROWS * @PAGE ROWS FETCH NEXT @ROWS ROWS ONLY;
Если OFFSET/FETCH недоступен в вашей версии SQL Server...
2) Пагинация с помощью оконной функции ROW_NUMBER():
WITH
QUERY AS
(
SELECT *, ROW_NUMBER() OVER(ORDER BY City DESC, ID DESC) AS ROWNUM
FROM Customers
)
SELECT *
FROM QUERY
WHERE ROWNUM <= 11
ORDER BY 1 City DESC, ID DESC;
Чтобы разбить на страницы страницу из 10 строк и страницу 7, вы можете выполнить этот запрос:
WITH
QUERY AS
(
SELECT *, ROW_NUMBER() OVER(ORDER BY City DESC, ID DESC) AS ROWNUM
FROM Customers
)
SELECT *
FROM QUERY
WHERE ROWNUM BETWEEN 1 + 10 * 7 AND (7 + 1) * 10
ORDER BY 1 City DESC, ID DESC;
И вообще:
DECLARE @PAGE INT = 7, @ROWS INT = 10;
WITH
QUERY AS
(
SELECT *, ROW_NUMBER() OVER(ORDER BY City DESC, ID DESC) AS ROWNUM
FROM Customers
)
SELECT *
FROM QUERY
WHERE ROWNUM BETWEEN 1 + @ROWS * @PAGE AND (@PAGE + 1) * @ROWS
ORDER BY 1 City DESC, ID DESC;
Кстати, TOP — это нестандартный оператор, специфичный для MS SQL Server, и его не следует использовать, если какой-то стандарт SQL ISO может сделать то же самое или лучше...
Мне удалось найти решение, которое представляет собой комбинацию ответа @The Impaler и информации с веб-сайта use-the-index-luke.com, в частности этого лакомого кусочка:
SELECT *
FROM ( SELECT *
FROM sales
WHERE sale_date <= ?
AND NOT (sale_date = ? AND sale_id >= ?)
ORDER BY sale_date DESC, sale_id DESC
)
WHERE rownum <= 10
Я выбрал два варианта: один с использованием смещения/предела, а другой с использованием подхода на основе курсора, который я искал.
Плохо работающий SQL был:
select c.* FROM customers c order by city asc, id asc offset 999989 rows FETCH NEXT 11 ROWS ONLY;
Объединив предыдущие ответы и предложения веб-сайта, я получил следующий SQL:
SELECT top(10) * FROM ( SELECT * FROM customers WHERE City <= 'City-99'
AND NOT (City = 'City-99' AND ID >= 999999 )) t ORDER BY City DESC, ID DESC
Представление профилировщика SQL Server было еще более черно-белым.
CPU / Reads / Duration
Offset / Limit 2049 22163 553
Cursor 1626 22163 274
Таким образом, хотя я и не уменьшил количество операций чтения между операторами, процессор и длительность были значительно увеличены.
Мне удалось выполнить сортировку по любому столбцу, использовать комбинацию ключей столбца сортировки и столбца идентификатора и получить довольно хорошую производительность, не индексируя ни один из столбцов.
Говоря об индексировании столбцов, я вернулся и поместил индекс в столбец «Город», чтобы посмотреть, будет ли это иметь значение:
CPU / Reads / Duration
Offset / Limit 2156 22163 586
Cursor 0 48 48
При использовании того же SQL, описанного выше, улучшения становятся еще более радикальными.
И я понимаю, что правильная гигиена базы данных заключается в том, чтобы заранее знать, какие столбцы индексировать, но я выпускаю системы, которые не знают, как клиенты будут их использовать изначально, поэтому я настаиваю на чем-то лучше, чем Limit. / компенсировать. Я надеюсь, что мы уловим высокую частоту событий пользовательской сортировки, а затем порекомендуем индекс, если он не существует, для столбцов, по которым сортирует клиент в первую очередь.
Также, пожалуйста, не используйте изображения для данных, используйте уценку таблицы.