Я уверен, что вы до сих пор ошиблись во многих отношениях. И вы, вероятно, не поймете это правильно в ближайшее время. Так зачем тратить свое драгоценное время на подстройку SQL, когда вы могли бы реализовать бизнес-логику?
Позволь мне объяснить…
До недавнего стандарта SQL: 2008 то, что пользователи MySQL знают как LIMIT .. OFFSET было стандартизировано в следующем простом утверждении:
|
1
2
3
4
|
SELECT * FROM BOOK OFFSET 2 ROWSFETCH NEXT 1 ROWS ONLY |
Да. Так много ключевых слов.
SQL действительно очень многословный язык. Лично нам действительно нравится краткость предложения LIMIT .. OFFSET в MySQL / PostgreSQL, поэтому мы выбрали его для jOOQ DSL API .
В SQL:
|
1
|
SELECT * FROM BOOK LIMIT 1 OFFSET 2 |
В jOOQ:
|
1
|
select().from(BOOK).limit(1).offset(2); |
Теперь, когда вы являетесь поставщиком SQL-фреймворка или катите свою собственную внутреннюю абстракцию SQL, вы можете подумать о стандартизации этого аккуратного небольшого предложения. Вот несколько вариантов из баз данных, которые изначально поддерживают нумерацию смещений:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
-- MySQL, H2, HSQLDB, Postgres, and SQLiteSELECT * FROM BOOK LIMIT 1 OFFSET 2-- CUBRID supports a MySQL variant of the -- LIMIT .. OFFSET clauseSELECT * FROM BOOK LIMIT 2, 1-- Derby, SQL Server 2012, Oracle 12, SQL:2008SELECT * FROM BOOK OFFSET 2 ROWS FETCH NEXT 1 ROWS ONLY-- Ingres. Eek, almost the standard. Almost!SELECT * FROM BOOK OFFSET 2 FETCH FIRST 1 ROWS ONLY-- FirebirdSELECT * FROM BOOK ROWS 2 TO 3-- Sybase SQL AnywhereSELECT TOP 1 ROWS START AT 3 * FROM BOOK-- DB2 (without OFFSET)SELECT * FROM BOOK FETCH FIRST 1 ROWS ONLY-- Sybase ASE, SQL Server 2008 (without OFFSET)SELECT TOP 1 * FROM BOOK |
Все идет нормально. Все это может быть обработано. Некоторые базы данных помещают смещения перед пределами, другие ставят ограничения перед смещениями, а семейство T-SQL помещает все предложение TOP перед списком SELECT . Это легко подражать. Теперь насчёт:
- Oracle 11g и меньше
- SQL Server 2008 и меньше
- DB2 с OFFSET
( обратите внимание, что вы можете включить различные альтернативные синтаксисы в DB2 )
Когда вы заглянете в Google, вы найдете миллионы способов эмулировать OFFSET .. FETCH в этих старых базах данных. Оптимальные решения всегда включают в себя:
- Использование дважды вложенных производных таблиц с фильтрацией
ROWNUMв Oracle - Использование производных вкладок с
ROW_NUMBER()вложением с фильтрациейROW_NUMBER()в SQL Server и DB2
Итак, вы имитируете это.
Как вы думаете, вы поймете это правильно?
Давайте рассмотрим пару вопросов, о которых вы, возможно, не задумывались.
Прежде всего, Oracle. Очевидно, что Oracle стремился создать максимальную блокировку от поставщиков, которая превзошла только недавнее появление Apple в Swift . Вот почему решения ROWNUM работают лучше, даже лучше, чем решения на основе стандартных оконных функций SQL: 2003 . Не верь этому? Прочитайте эту очень интересную статью о производительности смещения страниц Oracle .
Итак, оптимальное решение в Oracle это:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
-- PostgreSQL syntax:SELECT ID, TITLE FROM BOOK LIMIT 1 OFFSET 2-- Oracle equivalent:SELECT *FROM ( SELECT b.*, ROWNUM rn FROM ( SELECT ID, TITLE FROM BOOK ) b WHERE ROWNUM <= 3 -- (1 + 2))WHERE rn > 2 |
Так это действительно эквивалент?
Конечно нет. Вы выбираете дополнительный столбец, столбец rn . В большинстве случаев вам может быть все равно, но что, если вы хотите создать ограниченный подзапрос для использования с предикатом IN ?
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
-- PostgreSQL syntax:SELECT *FROM BOOKWHERE AUTHOR_ID IN ( SELECT ID FROM AUTHOR LIMIT 1 OFFSET 2)-- Oracle equivalent:SELECT *FROM BOOKWHERE AUTHOR_ID IN ( SELECT * -- Ouch. These are two columns! FROM ( SELECT b.*, ROWNUM rn FROM ( SELECT ID FROM AUTHOR ) b WHERE ROWNUM <= 3 ) WHERE rn > 2) |
Итак, как вы можете видеть, вам придется выполнить более сложное преобразование SQL. Если вы эмулируете LIMIT .. OFFSET вручную, тогда вы можете просто вставить столбец ID в подзапрос:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
SELECT *FROM BOOKWHERE AUTHOR_ID IN ( SELECT ID -- better FROM ( SELECT b.ID, ROWNUM rn -- better FROM ( SELECT ID FROM AUTHOR ) b WHERE ROWNUM <= 3 ) WHERE rn > 2) |
Итак, это больше похоже на это, верно? Но так как вы не пишете это каждый раз вручную, вы собираетесь начать создавать собственную изящную внутреннюю среду SQL, охватывающую 2-3 варианта использования, с которыми вы уже сталкивались, верно?
Ты можешь это сделать. Таким образом, вы будете автоматически выполнять поиск по столбцам в именах столбцов, чтобы получить вышеуказанное.
Так теперь это правильно?
Конечно нет! Потому что вы можете иметь неоднозначные имена столбцов в SELECT верхнего уровня, но не во вложенных выборках. Что делать, если вы хотите сделать это:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-- PostgreSQL syntax:-- Perfectly valid repetition of two ID columnsSELECT BOOK.ID, AUTHOR.IDFROM BOOKJOIN AUTHORON BOOK.AUTHOR_ID = AUTHOR.IDLIMIT 1 OFFSET 2-- Oracle equivalent:SELECT *FROM ( SELECT b.*, ROWNUM rn FROM ( -- Ouch! ORA-00918: column ambiguously defined SELECT BOOK.ID, AUTHOR.ID FROM BOOK JOIN AUTHOR ON BOOK.AUTHOR_ID = AUTHOR.ID ) b WHERE ROWNUM <= 3)WHERE rn > 2 |
Нет. А хитрость ручного исправления столбцов идентификаторов из предыдущего примера не работает, потому что у вас есть несколько экземпляров ID . А переименование столбцов в случайные значения — это неприятно, потому что пользователь вашей собственной внутренней базы данных хочет получить четко определенные имена столбцов. Т.е. ID и… ID .
Итак, решение состоит в том, чтобы переименовать столбцы дважды. Один раз в каждой производной таблице:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
-- Oracle equivalent:-- Rename synthetic column names back to originalSELECT c1 ID, c2 IDFROM ( SELECT b.c1, b.c2, ROWNUM rn FROM ( -- synthetic column names here SELECT BOOK.ID c1, AUTHOR.ID c2 FROM BOOK JOIN AUTHOR ON BOOK.AUTHOR_ID = AUTHOR.ID ) b WHERE ROWNUM <= 3)WHERE rn > 2 |
Но теперь мы закончили?
Конечно нет! Что если вы вдвойне вложите такой запрос? Будете ли вы думать о переименовании столбцов ID в синтетические имена и обратно? … Давайте оставим это здесь и поговорим о чем-то совершенно ином:
Работает ли то же самое для SQL Server 2008?
Конечно нет! В SQL Server 2008 наиболее популярным подходом является использование оконных функций. А именно, ROW_NUMBER() . Итак, давайте рассмотрим:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
-- PostgreSQL syntax:SELECT ID, TITLE FROM BOOK LIMIT 1 OFFSET 2-- SQL Server equivalent:SELECT b.*FROM ( SELECT ID, TITLE, ROW_NUMBER() OVER (ORDER BY ID) rn FROM BOOK) bWHERE rn > 2 AND rn <= 3 |
Так вот и все, верно?
Конечно нет!
ОК, у нас уже была эта проблема. Мы не должны выбирать * , потому что это сгенерирует слишком много столбцов в случае, если мы используем это как подзапрос для предиката IN . Итак, давайте рассмотрим правильное решение с синтетическими именами столбцов:
|
1
2
3
4
5
6
7
8
|
-- SQL Server equivalent:SELECT b.c1 ID, b.c2 TITLEFROM ( SELECT ID c1, TITLE c2, ROW_NUMBER() OVER (ORDER BY ID) rn FROM BOOK) bWHERE rn > 2 AND rn <= 3 |
Но теперь мы получили это, верно?
Сделайте обоснованное предположение: Нет!
Что произойдет, если вы добавите предложение ORDER BY к исходному запросу?
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
-- PostgreSQL syntax:SELECT ID, TITLE FROM BOOK ORDER BY SOME_COLUMNLIMIT 1 OFFSET 2-- Naive SQL Server equivalent:SELECT b.c1 ID, b.c2 TITLEFROM ( SELECT ID c1, TITLE c2, ROW_NUMBER() OVER (ORDER BY ID) rn FROM BOOK ORDER BY SOME_COLUMN) bWHERE rn > 2 AND rn <= 3 |
Теперь это не работает в SQL Server. Подзапросам не разрешается иметь предложение ORDER BY , если только они не содержат предложение TOP (или предложение OFFSET .. FETCH в SQL Server 2012).
Хорошо, мы, вероятно, можем настроить это, используя TOP 100 PERCENT чтобы сделать SQL Server счастливым.
|
01
02
03
04
05
06
07
08
09
10
|
-- Better SQL Server equivalent:SELECT b.c1 ID, b.c2 TITLEFROM ( SELECT TOP 100 PERCENT ID c1, TITLE c2, ROW_NUMBER() OVER (ORDER BY ID) rn FROM BOOK ORDER BY SOME_COLUMN) bWHERE rn > 2 AND rn <= 3 |
Теперь это правильный SQL согласно SQL Server, хотя у вас нет гарантии, что порядок выполнения производной таблицы сохранится после выполнения запроса. Вполне может быть, что порядок снова меняется под влиянием некоторых факторов.
Если вы хотите упорядочить по SOME_COLUMN во внешнем запросе, вам придется снова преобразовать инструкцию SQL, чтобы добавить еще один синтетический столбец:
|
01
02
03
04
05
06
07
08
09
10
11
|
-- Better SQL Server equivalent:SELECT b.c1 ID, b.c2 TITLEFROM ( SELECT TOP 100 PERCENT ID c1, TITLE c2, SOME_COLUMN c99, ROW_NUMBER() OVER (ORDER BY ID) rn FROM BOOK) bWHERE rn > 2 AND rn <= 3ORDER BY b.c99 |
Это начинает становиться немного противным. И давайте догадаемся,
Это правильное решение!
Конечно нет! Что если в исходном запросе есть DISTINCT ?
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
-- PostgreSQL syntax:SELECT DISTINCT AUTHOR_IDFROM BOOK LIMIT 1 OFFSET 2-- Naive SQL Server equivalent:SELECT b.c1 AUTHOR_IDFROM ( SELECT DISTINCT AUTHOR_ID c1, ROW_NUMBER() OVER (ORDER BY AUTHOR_ID) rn FROM BOOK) bWHERE rn > 2 AND rn <= 3 |
Что произойдет, если автор написал несколько книг? Да, ключевое слово DISTINCT должно удалять такие дубликаты, и эффективно, запрос PostgreSQL сначала корректно удалит дубликаты, а затем применяет LIMIT и OFFSET .
Однако предикат ROW_NUMBER() всегда генерирует различные номера строк, прежде чем DISTINCT сможет удалить их снова. Другими словами, DISTINCT имеет никакого эффекта.
К счастью, мы можем снова настроить этот SQL, используя этот аккуратный трюк :
|
1
2
3
4
5
6
7
8
|
-- Better SQL Server equivalent:SELECT b.c1 AUTHOR_IDFROM ( SELECT DISTINCT AUTHOR_ID c1, DENSE_RANK() OVER (ORDER BY AUTHOR_ID) rn FROM BOOK) bWHERE rn > 2 AND rn <= 3 |
Подробнее об этом трюке читайте здесь:
SQL-трюк: row_number () — это ВЫБРАТЬ, а плотная_ранка () — это ВЫБРАТЬ ОТЛИЧИЕ .
Обратите внимание, что предложение ORDER BY должно содержать все столбцы из списка полей SELECT . Очевидно, это ограничит допустимые столбцы в списке полей SELECT DISTINCT столбцами, которые разрешены в предложении ORDER BY оконной функции (например, никакие другие оконные функции).
Конечно, мы можем попытаться исправить это, используя обычные табличные выражения, или мы рассмотрим
Еще одна проблема ??
Ну конечно; естественно!
Вы даже знаете, какими должны быть столбцы в предложении ORDER BY оконной функции? Вы только что выбрали случайную колонку, случайно? Что если этот столбец не имеет индекса, будет ли по-прежнему работать ваша оконная функция?
Ответ прост, когда ваш исходный SELECT также содержит предложение ORDER BY , тогда вам, вероятно, следует принять его (плюс все столбцы из предложения SELECT DISTINCT , если применимо).
Но что, если у вас нет предложения ORDER BY ?
Еще один трюк! Используйте «постоянную» переменную:
|
1
2
3
4
5
6
7
8
|
-- Better SQL Server equivalent:SELECT b.c1 AUTHOR_IDFROM ( SELECT AUTHOR_ID c1, ROW_NUMBER() OVER (ORDER BY @@version) rn FROM BOOK) bWHERE rn > 2 AND rn <= 3 |
Да, вам нужно использовать переменную, потому что константы не разрешены в этих предложениях ORDER BY в SQL Server. Больно, я знаю.
Узнайте больше об этом трюке @@ version здесь .
Мы уже закончили!?!?
Возможно нет! Но мы, вероятно, охватили около 99% общих и крайних случаев. Теперь мы можем спать спокойно.
Обратите внимание, что все эти преобразования SQL реализованы в jOOQ . jOOQ — это единственная среда абстракции SQL, которая серьезно относится к SQL (со всеми его недостатками и предостережениями), стандартизируя все это безумие.
Как уже упоминалось в начале, с jOOQ вы просто пишете:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Don't worry about general emulationselect().from(BOOK).limit(1).offset(2);// Don't worry about duplicate column names// in subselectsselect(BOOK.ID, AUTHOR.ID).from(BOOK).join(AUTHOR).on(BOOK.AUTHOR_ID.eq(AUTHOR.ID)).limit(1).offset(2);// Don't worry about invalid IN predicatesselect().from(BOOK).where(BOOK.AUTHOR_ID).in( select(AUTHOR.ID) .from(AUTHOR) .limit(1).offset(2));// Don't worry about the ROW_NUMBER() vs.// DENSE_RANK() distinctionselectDistinct(AUTHOR_ID) .from(BOOK).limit(1).offset(2); |
С помощью jOOQ вы можете просто написать свой Oracle SQL или Transact SQL, как если бы он был таким же классным, как PostgreSQL! … Без прыжков с корабля SQL и перехода к JPA.
Пейджинг клавиатуры
Теперь, конечно, если вы читали наш блог или наш партнерский блог, посвященный объяснению производительности SQL , к настоящему времени вы должны знать, что разбиение на страницы OFFSET часто является плохим выбором. Вы должны знать, что нумерация клавиш набора почти всегда превосходит нумерацию смещения.
Прочитайте о том, как jOOQ изначально поддерживает разбиение на страницы набора ключей, используя предложение SEEK, здесь .
| Ссылка: | Прекратите пытаться эмулировать разбиение по страницам SQL OFFSET с помощью собственной базы данных БД! от нашего партнера JCG Лукаса Эдера в блоге JAVA, SQL и JOOQ . |


