Как я могу выполнить разбиение курсора на страницы со строковыми столбцами и целочисленными значениями?

У меня есть таблица «Клиенты» с семью столбцами: «Идентификатор», «Имя клиента», «Адрес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

Есть ли метод, о котором я не думаю, чтобы создать простое правило для эффективного перемещения курсора? Кажется, что иногда использование двух ключей полезно, когда в выбранном столбце есть повторяющиеся значения, но бесполезно, когда повторяющихся значений нет.

Также, пожалуйста, не используйте изображения для данных, используйте уценку таблицы.

Dale K 26.08.2024 22:59

Если вы хотите получить повторяемые результаты, вы должны использовать порядок, который дает уникальный порядок.

Dale K 26.08.2024 23:01

Я не думаю, что ваша логика работает. Вероятно, вам следует сделать ИЛИ stackoverflow.com/a/19609938/13061224 потому что нет гарантии, что идентификатор всегда убывает

siggemannen 26.08.2024 23:02

@Dalek Ваш первый комментарий - это не нумерация страниц курсором. Я пытаюсь избежать смещения/выборки производительности.

Josh 26.08.2024 23:03

Но не уверен, что разбиение на страницы набора ключей сильно поможет, вы все равно, вероятно, получите плохую производительность, если поля не проиндексированы. Возможно, ваш API не должен позволять случайный просмотр всей вашей базы данных клиентов без фильтров.

siggemannen 26.08.2024 23:08

Да, динамический поиск + нумерация страниц практически невозможно обеспечить хорошую производительность.

Dale K 26.08.2024 23:11

По сути, вам понадобится индекс для каждого столбца сортировки. Если индекса нет, вы не сможете найти строки 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;

testing-for-ya 26.08.2024 23:32

Индекс в столбце сортировки позволяет легко находить строки Nth->Nth+PageSize, а затем использовать идентификаторы (которые включены в индексы NC по умолчанию), чтобы найти только эти PageSize строки и получить остальные данные. Покрывающий индекс может помочь в этом случае, если вам не нужны все столбцы, но в противном случае, в зависимости от количества строк на странице, он все равно может выполнить сканирование, чтобы найти все эти строки. Но одно сканирование обычно лучше, чем два. Некоторые подходы и их относительную стоимость вы можете увидеть здесь.

testing-for-ya 26.08.2024 23:46

И всегда, чтобы получить предсказуемые результаты, вам не нужно добавлять столбцы в предложение WHERE, вам нужно добавить их к тому, что вычисляет номер строки. По сути, вам нужно добавить достаточное количество столбцов для такой сортировки, чтобы каждая строка была уникальной; это единственный способ получить предсказуемую сортировку.

testing-for-ya 26.08.2024 23:49

(И в примере вы можете увидеть, как различные подходы масштабируются по мере увеличения количества страниц. Например, измените 500 на 50 000 в каждом из них и обратите внимание, что первый пример увеличивается в 8 раз, второй остается постоянным. , а в третьем примере увеличивается лишь на небольшую величину. Наведите указатель мыши на узел SELECT в каждом графическом плане выполнения и посмотрите на значение рядом с Estimated Subtree Cost.)

testing-for-ya 27.08.2024 00:02

На самом деле, в некотором смысле, неиндексированный поиск может быть быстрым, потому что после первого сканирования таблицы строки будут находиться в памяти, что обеспечит быстрый доступ во второй раз :)

siggemannen 27.08.2024 11:12

@siggemannen Но сортировка по-прежнему может быть довольно трудоемкой операцией, независимо от того, находятся ли данные уже в памяти или нет, особенно в системах, уже привязанных к процессору, и особенно в системах, где размер таблицы> ОЗУ (да, таких существует много).

testing-for-ya 27.08.2024 19:17
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
13
73
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Всегда сортируйте, используя «уникальный ключ» при реализации внутренней нумерации страниц.

Тривиальный случай: вы сортируете по 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 27.08.2024 14:24

@Josh В предикате WHERE была небольшая опечатка. Теперь это исправлено. См. пример бега.

The Impaler 27.08.2024 19:55

ваш ответ, а также ссылка на веб-сайт use-the-index-luke.com помогли получить необходимый sql. Я подробно опишу это в последующем ответе, но отмечу ваш ответ как ответ. Спасибо за помощь

Josh 27.08.2024 20:06

Разбивка на страницы в 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. / компенсировать. Я надеюсь, что мы уловим высокую частоту событий пользовательской сортировки, а затем порекомендуем индекс, если он не существует, для столбцов, по которым сортирует клиент в первую очередь.

Другие вопросы по теме