Spec-Zone.ru › SQLite

Оператор WITH

Содержание
1. Обзор
2. Обычные общие табличные выражения
3. Рекурсивные общие табличные выражения
3.1. Примеры рекурсивных запросов
3.2. Примеры иерархических запросов
3.3. Запросы к графу
3.4. Управление пошаговым или послойным обходом дерева с помощью ORDER BY
3.5. Примеры необычных рекурсивных запросов
4. Подсказки по материализации
5. Ограничения и замечания

1.Обзор

оператор WITH:

WITH RECURSIVE имя_таблицы_cte КАК НЕ МАТЕРИАЛИЗОВАНО ( запрос_select ) МАТЕРИАЛИЗОВАНО ,

имя_таблицы_cte:

имя_таблицы ( имя_столбца ) ,

запрос_select:

С РЕКУРСИВНАЯ общее_выражение_с_таблицей , ВЫБРАТЬ УНИКАЛЬНЫЕ столбец_результата , ВСЕ ИЗ таблицы_или_подзапроса оператор_соединения , ГДЕ выражение СГРУППИРОВАТЬ ПО выражение ПРИ_УСЛОВИИ выражение ,
ОКОШКО имя-окна КАК определение-окна , ЗНАЧЕНИЯ ( выражение ) , , оператор-сложения ядро-выбора СОРТИРОВКА ПО ОГРАНИЧЕНИЕ выражение элемент-сортировки , СДВИГ выражение , выражение

общее-выражение-с-таблицей:

имя_таблицы ( имя_столбца ) КАК НЕ МАТЕРИАЛИЗОВАННЫЙ ( выражение_select ) ,

сложное_выражение:

ОБЪЕДИНЕНИЕ ОБЪЕДИНЕНИЕ ПЕРЕСЕЧЕНИЕ ИСКЛЮЧЕНИЕ ВСЕ

выражение:

литеральное значение параметр привязки имя схемы . имя таблицы . имя столбца унарный оператор expr expr бинарный оператор expr имя функции ( аргументы функции ) фильтрующее предложение над предложением ( expr ) , CAST ( expr AS имя типа
) expr COLLATE имя_сопоставления expr НЕ ПОДОБНО GLOB REGEXP СОПОСТАВИТЬ expr expr ESCAPE expr expr ISNULL NOTNULL НЕ NULL expr IS НЕ РАЗЛИЧНЫЕ ИЗ expr
expr НЕТ МЕЖДУ expr И expr expr НЕТ В ( select-stmt ) expr , schema-name . table-function ( expr ) table-name , НЕТ СУЩЕСТВУЕТ ( select-stmt )
CASE expr WHEN expr THEN expr ELSE expr END raise-function

filter-clause:

FILTER ( WHERE expr )

function-arguments:

DISTINCT expr , * ORDER BY ordering-term ,

буквальное значение:

CURRENT_TIMESTAMP числовое_литерал строковый_литерал байтовый_литерал NULL ИСТИНА ЛОЖЬ CURRENT_TIME CURRENT_DATE

оператор_over:

СВЕРХУ имя_окна ( основное_имя_окна РАЗДЕЛЁННЫЙ ПО выражение , ПОРЯДОК ПО термин_упорядочения , описание_фрейма )

описание_фрейма:

ГРУППЫ МЕЖДУ НЕОГРАНИЧЕННЫМ ПРЕДШЕСТВУЮЩИМ И НЕОГРАНИЧЕННЫМ СЛЕДУЮЩИМ ДИАПАЗОН СТРОКИ НЕОГРАНИЧЕННЫМ ПРЕДШЕСТВУЮЩИМ expr ПРЕДШЕСТВУЮЩИМ ТЕКУЩАЯ СТРОКА expr ПРЕДШЕСТВУЮЩИМ ТЕКУЩАЯ СТРОКА expr СЛЕДУЮЩИМ expr ПРЕДШЕСТВУЮЩИМ ТЕКУЩАЯ СТРОКА expr СЛЕДУЮЩИМ
EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

raise-function:

ВОЗВЫСИТЬ ( ОТКАТ , сообщение_об_ошибке ) ПРОИГНОРИРОВАТЬ ПРЕКРАТИТЬ ПРОВОЛОКА

type-name:

имя ( число со знаком , число со знаком ) ( число со знаком )

число со знаком:

+ числовое значение -

оператор объединения:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

ordering-term:

expr COLLATE имя_сортировки DESC ASC NULLS первым NULLS последним

столбец_результата:

expr КАК псевдоним_столбца * имя_таблицы . *

таблица_или_подзапрос:

имя_схемы . имя_таблицы КАК псевдоним_таблицы ИНДЕКСИРОВАНО ПО имя_индекса НЕ ИНДЕКСИРОВАНО имя_функции_таблицы ( выражение ) , КАК псевдоним_таблицы ( выбираемое_выражение ) ( таблица_или_подзапрос ) ,
join-clause

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

ГРУППЫ МЕЖДУ НЕОГРАНИЧЕННЫХ ПРЕДШЕСТВУЮЩИХ И НЕОГРАНИЧЕННЫХ СЛЕДУЮЩИХ ДИАПАЗОН СТРОКИ НЕОГРАНИЧЕННЫХ ПРЕДШЕСТВУЮЩИХ выражение ПРЕДШЕСТВУЮЩИХ ТЕКУЩАЯ СТРОКА выражение ПРЕДШЕСТВУЮЩИХ ТЕКУЩАЯ СТРОКА выражение СЛЕДУЮЩИХ выражение ПРЕДШЕСТВУЮЩИХ ТЕКУЩАЯ СТРОКА выражение СЛЕДУЮЩИХ
ИСКЛЮЧАТЬ ТЕКУЩИЙ СТРОКА ИСКЛЮЧАТЬ ГРУППА ИСКЛЮЧАТЬ СВЯЗИ ИСКЛЮЧАТЬ НЕТ ДРУГИЕ

Общие табличные выражения или CTE (Common Table Expressions) работают как временные представления, существующие только на время выполнения одного SQL-запроса. Существуют два типа общих табличных выражений: «обычные» и «рекурсивные». Обычные общие табличные выражения полезны для упрощения запросов путём вынесения подзапросов из основного SQL-запроса. Рекурсивные общие табличные выражения позволяют выполнять иерархические или рекурсивные запросы к деревьям и графам, что иначе недоступно в языке SQL.

Все общие табличные выражения (обычные и рекурсивные) создаются с помощью ключевого слова WITH перед оператором SELECT, INSERT, DELETE или UPDATE. Одно ключевое слово WITH может указывать одно или несколько общих табличных выражений, часть из которых могут быть обычными, а часть — рекурсивными.

2. Обычные общие табличные выражения

Обычное общее табличное выражение работает так, как если бы это было представление, существующее на время выполнения одного запроса. Обычные общие табличные выражения полезны для вынесения подзапросов и упрощения чтения и понимания всего SQL-запроса.

Ключевое слово WITH может содержать обычные общие табличные выражения даже если оно включает ключевое слово RECURSIVE. Использование RECURSIVE не вынуждает общие табличные выражения быть рекурсивными.

3. Рекурсивные общие табличные выражения

Рекурсивное общее табличное выражение можно использовать для написания запроса, проходящего по дереву или графу. Рекурсивное общее табличное выражение имеет ту же основную синтаксическую структуру, что и обычное общее табличное выражение, но с дополнительными характеристиками:

  1. "select-stmt" должен быть составным select. То есть тело CTE должно содержать два или более отдельных операторов SELECT, разделенных составными операторами, такими как UNION, UNION ALL, INTERSECT или EXCEPT.
  2. Один или несколько отдельных операторов SELECT, составляющих составной оператор, должны быть «рекурсивными». Оператор SELECT является рекурсивным, если его предложение FROM содержит ровно одну ссылку на таблицу CTE (таблица, указанная слева от ключевого слова AS).
  3. Один или несколько операторов SELECT в составном операторе должны быть нерекурсивными.
  4. Все нерекурсивные операторы SELECT должны предшествовать любым рекурсивным операторам SELECT.
  5. Рекурсивные операторы SELECT должны быть разделены от нерекурсивных операторов SELECT и друг от друга операторами UNION или UNION ALL. Если есть два или более рекурсивных операторов SELECT, они все должны быть разделены тем же оператором, который разделяет первый рекурсивный оператор SELECT от последнего нерекурсивного оператора SELECT.
  6. Рекурсивные операторы SELECT не могут использовать функции агрегации или функции окон.

Другими словами, рекурсивное общее табличное выражение должно выглядеть примерно так:

recursive-cte:

END_OF_DOCUMENT_MARKER
cte-table-name AS ( initial-select UNION ALL recursive-select ) UNION

cte-table-name:

table-name ( column-name ) ,

На диаграмме выше initial-select означает одну или несколько нерекурсивных инструкций SELECT, а recursive-select — одну или несколько рекурсивных инструкций SELECT. Наиболее распространённый случай — один initial-select и один recursive-select, но допускается наличие более одной инструкции каждого типа.

Назовите таблицу, указанную в имени cte-table-name в рекурсивном общем табличном выражении, «рекурсивной таблицей». На диаграмме recursive-cte выше, рекурсивная таблица должна появляться ровно один раз в операторе FROM каждого верхнего уровня оператора SELECT в recursive-select и не должна появляться нигде ещё в initial-select или recursive-select, включая подзапросы. initial-select может быть сложным запросом, но не может содержать ORDER BY, LIMIT или OFFSET. recursive-select также может быть сложным запросом с ограничением, что все элементы этого сложного запроса должны быть разделены одним оператором UNION или UNION ALL, который разделяет initial-select и recursive-select. В recursive-select допускается использование ORDER BY, LIMIT и/или OFFSET, но нельзя использовать функции агрегации или функции окон.

Возможность для recursive-select быть сложным запросом была добавлена в версии 3.34.0 (2020-12-01). В более ранних версиях SQLite recursive-select мог быть только одной простой инструкцией SELECT.

Базовый алгоритм вычисления содержимого рекурсивной таблицы следующий:

  1. Выполните initial-select и добавьте результаты в очередь.
  2. Пока очередь не пуста:
    1. Извлеките одну строку из очереди.
    2. Вставьте эту строку в рекурсивную таблицу.
    3. Представьте, что только что извлечённая строка — единственная строка в рекурсивной таблице, и выполните рекурсивный запрос, добавив все результаты в очередь.

Базовый процесс выше может быть изменён следующими дополнительными правилами:

  • Если оператор UNION соединяет initial-select с recursive-select, то добавляйте строки в очередь только в том случае, если идентичная строка ранее не добавлялась в очередь. Повторные строки отбрасываются перед добавлением в очередь, даже если эти повторные строки уже были извлечены из очереди в шаге рекурсии. Если оператор — UNION ALL, то все строки, сгенерированные как initial-select, так и recursive-select, всегда добавляются в очередь, даже если они повторяются. При определении, является ли строка повторяющейся, значения NULL сравниваются как равные друг другу, но не равные любому другому значению.

  • Оператор LIMIT, если он присутствует, определяет максимальное количество строк, которое когда-либо будет добавлено в рекурсивную таблицу на шаге 2b. После достижения предела рекурсия прекращается. Предел, равный нулю, означает, что ни одна строка не добавляется в рекурсивную таблицу, а отрицательный предел означает, что может быть добавлено неограниченное количество строк в рекурсивную таблицу.

  • Если оператор OFFSET присутствует и имеет положительное значение N, то первые N строк не будут добавлены в рекурсивную таблицу. Первые N строк всё ещё обрабатываются recursive-select — они просто не добавляются в рекурсивную таблицу. Строки не учитываются при выполнении LIMIT, пока не будут пропущены все строки OFFSET.

  • Если оператор ORDER BY присутствует, он определяет порядок извлечения строк из очереди на шаге 2a. Если оператор ORDER BY отсутствует, то порядок извлечения строк не определён. (В текущей реализации очередь становится FIFO, если оператор ORDER BY отсутствует, но приложения не должны полагаться на этот факт, поскольку он может измениться.)

3.1.Примеры рекурсивных запросов

Следующий запрос возвращает все целые числа от 1 до 1000000:

WITH RECURSIVE
  cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<1000000)
SELECT x FROM cnt;

Рассмотрим, как работает этот запрос. Сначала выполняется initial-select, который возвращает одну строку с одним столбцом «1». Эта строка добавляется в очередь. На шаге 2a эта строка извлекается из очереди и добавляется в «cnt». Затем в соответствии с шагом 2c выполняется recursive-select, генерирующий одну новую строку со значением «2», которую нужно добавить в очередь. В очереди по-прежнему одна строка, поэтому шаг 2 повторяется. Строка «2» извлекается и добавляется в рекурсивную таблицу шагами 2a и 2b. Затем строка, содержащая 2, используется как если бы она содержала всё содержимое рекурсивной таблицы, и recursive-select выполняется снова, что приводит к добавлению в очередь строки со значением «3». Это повторяется 999999 раз, пока, наконец, на шаге 2a единственное значение в очереди — строка, содержащая 1000000. Эта строка извлекается и добавляется в рекурсивную таблицу. Но в этот раз условие WHERE приводит к тому, что recursive-select не возвращает строк, поэтому очередь остаётся пустой, и рекурсия прекращается.

Примечание по оптимизации: В обсуждении выше утверждения, подобные "вставить строку в рекурсивную таблицу", следует понимать концептуально, а не буквально. Похоже, что SQLite накапливает огромную таблицу, содержащую один миллион строк, а затем возвращается и сканирует эту таблицу сверху вниз для генерации результата. На самом деле оптимизатор запросов видит, что значения в рекурсивной таблице «cnt» используются только один раз. Поэтому по мере добавления каждой строки в рекурсивную таблицу эта строка немедленно возвращается в качестве результата основного оператора SELECT и затем отбрасывается. SQLite не накапливает временную таблицу, содержащую миллион строк. Для выполнения приведенного выше примера требуется очень мало памяти. Однако если в примере использовалась операция UNION вместо UNION ALL, то SQLite пришлось бы сохранить все ранее сгенерированные данные, чтобы проверить дубликаты. По этой причине программисты должны стремиться использовать UNION ALL вместо UNION, когда это возможно.

Вот вариация предыдущего примера:

WITH RECURSIVE
  cnt(x) AS (
     SELECT 1
     UNION ALL
     SELECT x+1 FROM cnt
      LIMIT 1000000
  )
SELECT x FROM cnt;

В этой вариации есть две разницы. Initial-select — это «SELECT 1» вместо «VALUES(1)», но это просто разные синтаксические конструкции, выражающие одно и то же. Другое изменение заключается в том, что рекурсия прекращается с помощью оператора LIMIT, а не WHERE. Использование LIMIT означает, что когда миллионная строка добавляется в таблицу «cnt» (и возвращается основным оператором SELECT благодаря оптимизатору запросов), рекурсия останавливается немедленно, независимо от того, сколько строк может остаться в очереди. В более сложных запросах порой бывает сложно гарантировать, что оператор WHERE в конечном итоге приведет к очистке очереди и завершению рекурсии. Но оператор LIMIT всегда остановит рекурсию. Поэтому рекомендуется всегда включать оператор LIMIT в качестве меры предосторожности, если известен верхний предел размера рекурсии.

3.2. Примеры иерархических запросов

Рассмотрим таблицу, описывающую членов организации и цепочку команд в этой организации:

CREATE TABLE org(
  name TEXT PRIMARY KEY,
  boss TEXT REFERENCES org,
  height INT,
  -- other content omitted
);

Каждый член организации имеет имя, а большинство членов имеют одного начальника. (У главы всей организации поле «начальник» имеет значение NULL.) Строки таблицы «org» образуют дерево.

Вот запрос, вычисляющий среднюю высоту всех членов организации Алисы, включая саму Алису:

WITH RECURSIVE
  works_for_alice(n) AS (
    VALUES('Alice')
    UNION
    SELECT name FROM org, works_for_alice
     WHERE org.boss=works_for_alice.n
  )
SELECT avg(height) FROM org
 WHERE org.name IN works_for_alice;

Следующий пример использует два общих выражения с подзапросами в одном операторе WITH. Следующая таблица записывает генеалогическое древо:

CREATE TABLE family(
  name TEXT PRIMARY KEY,
  mom TEXT REFERENCES family,
  dad TEXT REFERENCES family,
  born DATETIME,
  died DATETIME -- NULL if still alive
  -- other content
);

Таблица «family» похожа на предыдущую таблицу «org», за исключением того, что теперь у каждого члена есть два родителя. Мы хотим узнать всех живых предков Алисы, от старших к младшим. Сначала определяется обычное общее выражение «parent_of». Это обычное общее выражение — это представление, которое можно использовать для поиска всех родителей любого человека. Затем это обычное общее выражение используется в рекурсивном общем выражении «ancestor_of_alice». Затем рекурсивное общее выражение используется в окончательном запросе:

WITH RECURSIVE
  parent_of(name, parent) AS
    (SELECT name, mom FROM family UNION SELECT name, dad FROM family),
  ancestor_of_alice(name) AS
    (SELECT parent FROM parent_of WHERE name='Alice'
     UNION ALL
     SELECT parent FROM parent_of JOIN ancestor_of_alice USING(name))
SELECT family.name FROM ancestor_of_alice, family
 WHERE ancestor_of_alice.name=family.name
   AND died IS NULL
 ORDER BY born;

3.3. Запросы к графу

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

CREATE TABLE edge(aa INT, bb INT);
CREATE INDEX edge_aa ON edge(aa);
CREATE INDEX edge_bb ON edge(bb);

Индексы не требуются, но они повышают производительность для больших графов. Для поиска всех узлов графа, которые связаны с узлом 59, используйте запрос, похожий на следующий:

WITH RECURSIVE nodes(x) AS (
   SELECT 59
   UNION
   SELECT aa FROM edge JOIN nodes ON bb=x
   UNION
   SELECT bb FROM edge JOIN nodes ON aa=x
)
SELECT x FROM nodes;

В этом случае initial-select — это простой запрос «SELECT 59». Это устанавливает базовый случай. recursive-select состоит из двух других операторов SELECT. Первый рекурсивный SELECT следует ребрам в направлении bb-to-aa, а второй рекурсивный SELECT следует ребрам в направлении aa-to-bb. UNION используется вместо UNION ALL, чтобы предотвратить вхождение рекурсии в бесконечный цикл, если граф содержит циклы.

Вот реальный пример использования запроса к графу для направленного графа: система управления версиями (VCS) обычно хранит эволюционирующие версии проекта в виде ориентированного ациклического графа (DAG). Назовем каждую версию проекта «коммитом». Один коммит может иметь ноль или более предков. Большинство коммитов (кроме первого) имеют одного предка, но в случае слияния коммит может иметь двух или более предков. Схема для отслеживания коммитов и порядка их появления может выглядеть примерно так:

CREATE TABLE checkin(
  id INTEGER PRIMARY KEY,
  mtime INTEGER -- timestamp when this checkin occurred
);
CREATE TABLE derivedfrom(
  xfrom INTEGER NOT NULL REFERENCES checkin, -- parent checkin
  xto INTEGER NOT NULL REFERENCES checkin,   -- derived checkin
  PRIMARY KEY(xfrom,xto)
);
CREATE INDEX derivedfrom_back ON derivedfrom(xto,xfrom);

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

Мы хотим узнать двадцать самых последних предков по времени (из тысяч и тысяч предков во всем DAG) для коммита «@BASELINE». (Запрос, похожий на этот, используется системой управления версиями Fossil для отображения N последних предков коммита. Например: https://www.sqlite.org/src/timeline?p=trunk&n=30.)

WITH RECURSIVE
  ancestor(id,mtime) AS (
    SELECT id, mtime FROM checkin WHERE id=@BASELINE
    UNION
    SELECT derivedfrom.xfrom, checkin.mtime
      FROM ancestor, derivedfrom, checkin
     WHERE ancestor.id=derivedfrom.xto
       AND checkin.id=derivedfrom.xfrom
     ORDER BY checkin.mtime DESC
     LIMIT 20
  )
SELECT * FROM checkin JOIN ancestor USING(id);

Оператор «ORDER BY checkin.mtime DESC» в рекурсивном операторе SELECT делает запрос намного быстрее, предотвращая следование ветвям, которые сливают коммиты из далекого прошлого. ORDER BY заставляет рекурсивный оператор SELECT сосредоточиться на самых последних коммитах, которые нам нужны. Без ORDER BY в рекурсивном операторе SELECT пришлось бы вычислять весь набор тысяч предков, сортировать их по времени изменения и затем брать двадцать лучших. ORDER BY по существу создает очередь с приоритетами, которая заставляет рекурсивный запрос сначала обращаться к самым последним предкам, что позволяет использовать оператор LIMIT для ограничения области запроса только интересующими нас коммитами.

3.4. Управление поиском в ширину или в глубину дерева с использованием ORDER BY

Оператор ORDER BY в рекурсивном операторе SELECT может использоваться для управления тем, является ли поиск дерева поиском в ширину или в глубину. В качестве иллюстрации мы воспользуемся вариацией таблицы «org» из приведенного выше примера, без столбца «height» и с вставленными реальными данными:

CREATE TABLE org(
  name TEXT PRIMARY KEY,
  boss TEXT REFERENCES org
) WITHOUT ROWID;
INSERT INTO org VALUES('Alice',NULL);
INSERT INTO org VALUES('Bob','Alice');
INSERT INTO org VALUES('Cindy','Alice');
INSERT INTO org VALUES('Dave','Bob');
INSERT INTO org VALUES('Emma','Bob');
INSERT INTO org VALUES('Fred','Cindy');
INSERT INTO org VALUES('Gail','Cindy');

Вот запрос, отображающий структуру дерева в ширину:

WITH RECURSIVE
  under_alice(name,level) AS (
    VALUES('Alice',0)
    UNION ALL
    SELECT org.name, under_alice.level+1
      FROM org JOIN under_alice ON org.boss=under_alice.name
     ORDER BY 2
  )
SELECT substr('..........',1,level*3) || name FROM under_alice;

Оператор «ORDER BY 2» (что равносильно «ORDER BY under_alice.level+1») заставляет обработку более высоких уровней в организационной структуре (с меньшими значениями «level») происходить первой, что приводит к поиску в ширину. Результат:

Alice
...Bob
...Cindy
......Dave
......Emma
......Fred
......Gail

Но если мы изменим оператор ORDER BY, добавив модификатор «DESC», это заставит рекурсивный оператор SELECT обрабатывать более низкие уровни организации (с большими значениями «level») первой, что приведет к поиску в глубину:

WITH RECURSIVE
  under_alice(name,level) AS (
    VALUES('Alice',0)
    UNION ALL
    SELECT org.name, under_alice.level+1
      FROM org JOIN under_alice ON org.boss=under_alice.name
     ORDER BY 2 DESC
  )
SELECT substr('..........',1,level*3) || name FROM under_alice;

Вывод этого переработанного запроса:

Alice
...Bob
......Dave
......Emma
...Cindy
......Fred
......Gail

Если оператор ORDER BY отсутствует в рекурсивном операторе SELECT, очередь ведет себя как FIFO, что приводит к поиску в ширину.

3.5. Примеры необычных рекурсивных запросов

Следующий запрос вычисляет приближение множества Мандельброта и выводит результат в виде ASCII-изображения:

WITH RECURSIVE
  xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
  yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),
  m(iter, cx, cy, x, y) AS (
    SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis
    UNION ALL
    SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m 
     WHERE (x*x + y*y) < 4.0 AND iter<28
  ),
  m2(iter, cx, cy) AS (
    SELECT max(iter), cx, cy FROM m GROUP BY cx, cy
  ),
  a(t) AS (
    SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') 
    FROM m2 GROUP BY cy
  )
SELECT group_concat(rtrim(t),x'0a') FROM a;

В этом запросе общие выражения «xaxis» и «yaxis» определяют сетку точек, для которых будет приближено множество Мандельброта. Каждая строка в общем выражении «m(iter,cx,cy,x,y)» означает, что после «iter» итераций итерация множества Мандельброта, начатая в cx,cy, достигла точки x,y. Количество итераций в этом примере ограничено 28 (что существенно ограничивает разрешение вычислений, но достаточно для вывода ASCII-изображения с низким разрешением). Общее выражение «m2(iter,cx,cy)» содержит максимальное количество итераций, достигнутых при старте в точке cx,cy. Наконец, каждая строка в общем выражении «a(t)» содержит строку, которая представляет собой одну строку выходного ASCII-изображения. Оператор SELECT в конце просто запрашивает общее выражение «a» для получения всех строк ASCII-изображения по одной.

Запуск приведенного выше запроса в командной оболочке SQLite приводит к следующему выводу:

                                    ....#
                                   ..#*..
                                 ..+####+.
                            .......+####....   +
                           ..##+*##########+.++++
                          .+.##################+.
              .............+###################+.+
              ..++..#.....*#####################+.
             ...+#######++#######################.
          ....+*################################.
 #############################################...
          ....+*################################.
             ...+#######++#######################.
              ..++..#.....*#####################+.
              .............+###################+.+
                          .+.##################+.
                           ..##+*##########+.++++
                            .......+####....   +
                                 ..+####+.
                                   ..#*..
                                    ....#
                                    +.

Следующий запрос решает головоломку Судоку. Состояние головоломки определяется строкой из 81 символа, сформированной путем чтения записей из сетки по строкам слева направо, а затем сверху вниз. Пустые клетки в головоломке обозначаются символом «.» Таким образом, входная строка:

53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79

Соответствует головоломке, подобной этой:

5 3 7
6 1 9 5
9 8 6
8 6 3
4 8 3 1
7 2 6
6 2 8
4 1 9 5
8 7 9

Вот запрос, решающий головоломку:

WITH RECURSIVE
  input(sud) AS (
    VALUES('53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79')
  ),
  digits(z, lp) AS (
    VALUES('1', 1)
    UNION ALL SELECT
    CAST(lp+1 AS TEXT), lp+1 FROM digits WHERE lp<9
  ),
  x(s, ind) AS (
    SELECT sud, instr(sud, '.') FROM input
    UNION ALL
    SELECT
      substr(s, 1, ind-1) || z || substr(s, ind+1),
      instr( substr(s, 1, ind-1) || z || substr(s, ind+1), '.' )
     FROM x, digits AS z
    WHERE ind>0
      AND NOT EXISTS (
            SELECT 1
              FROM digits AS lp
             WHERE z.z = substr(s, ((ind-1)/9)*9 + lp, 1)
                OR z.z = substr(s, ((ind-1)%9) + (lp-1)*9 + 1, 1)
                OR z.z = substr(s, (((ind-1)/3) % 3) * 3
                        + ((ind-1)/27) * 27 + lp
                        + ((lp-1) / 3) * 6, 1)
         )
  )
SELECT s FROM x WHERE ind=0;

Общее выражение «input» определяет входную головоломку. Общее выражение «digits» определяет таблицу, содержащую все цифры от 1 до 9. Работа по решению головоломки выполняется общим выражением «x». Запись в x(s,ind) означает, что строка из 81 символа «s» является корректной головоломкой Судоку (в ней нет противоречий), и что первая неизвестная буква находится в позиции «ind», или ind==0, если все позиции символов заполнены. Таким образом, цель — вычислить записи для «x» с «ind» равным 0.

Решатель работает путем добавления новых записей в рекурсивную таблицу «x». Учитывая предыдущие записи, рекурсивный оператор SELECT пытается заполнить одну новую позицию всеми значениями от 1 до 9, которые действительно подходят в эту позицию. Сложный подзапрос «NOT EXISTS» — это магия, которая определяет, является ли каждая строка-кандидат «s» корректной головоломкой Судоку или нет.

Окончательный ответ находится путем поиска строки с ind==0. Если у исходной головоломки Судоку не было единственного решения, запрос вернёт все возможные решения. Если исходная головоломка была неразрешимой, то строк не будет возвращено. В этом случае единственный ответ:

534678912672195348198342567859761423426853791713924856961537284287419635345286179

Решение было вычислено менее чем за 300 миллисекунд на современном рабочем станцции.

4. Подсказки по материализации

Формы «AS MATERIALIZED» и «AS NOT MATERIALIZED» общего выражения с подзапросом являются нестандартным синтаксисом SQL, скопированным из PostgreSQL. Использование MATERIALIZED или NOT MATERIALIZED после ключевого слова AS предоставляет неявные подсказки планировщику запросов о том, как должно быть реализовано общее выражение с подзапросом.

Если используется фраза MATERIALIZED, тогда select-stmt будет материализован в эпифемерную таблицу, которая хранится в памяти или в временном файле на диске. Эта временная таблица будет использоваться вместо имени таблицы CTE, когда имя таблицы CTE появляется в последующем SQL-запросе. Поскольку select-stmt вычисляется немедленно, теряется возможность применения оптимизаций, таких как выравнивание запроса или оптимизация сдвига. Эта потеря оптимизации — это особенность, а не ошибка. Разработчики могут использовать ключевое слово MATERIALIZED в качестве «барьера оптимизации», чтобы более точно контролировать поведение планировщика запросов SQLite. SQLite позаимствовало идею использования MATERIALIZED в качестве барьера оптимизации у PostgreSQL.

Если используется фраза NOT MATERIALIZED, тогда select-stmt подставляется как подзапрос вместо каждого вхождения имени таблицы CTE. Оптимизации, такие как выравнивание и сдвиг, затем применяются к подзапросу так, как будто подзапрос был использован напрямую. Несмотря на своё название, фраза NOT MATERIALIZED не запрещает использование материализации. Планировщик запросов по-прежнему свободен реализовать подзапрос с помощью материализации, если посчитает, что это наилучшее решение. Настоящий смысл NOT MATERIALIZED ближе к «ОБРАЩАТЬСЯ КАК К ОБЫЧНОЙ ВИДЕ или ПОДЗАПРОСУ».

Если ни один из указанных намеков не присутствует, тогда SQLite свободно выбирает стратегию реализации, которую, по его мнению, лучше всего использовать. Это рекомендуемый подход. Не используйте ключевые слова MATERIALIZED или NOT MATERIALIZED для выражения общего набора данных, если у вас нет веской причины.

Подсказки MATERIALIZED и NOT MATERIALIZED доступны только в версии SQLite 3.35.0 (2021-03-12) и более поздних.

5.Ограничения и замечания

  • Оператор WITH нельзя использовать в CREATE TRIGGER.

  • Оператор WITH должен находиться в начале уровня верхнего SELECT-запроса или в начале подзапроса. Оператор WITH нельзя добавить к второму или последующему SELECT-оператору составного SELECT.

  • Спецификация SQL:1999 требует, чтобы ключевое слово RECURSIVE следовали за WITH в любом операторе WITH, который включает рекурсивное общее выражение таблицы. Однако для совместимости с SqlServer и Oracle, SQLite не проверяет это правило.

Эта страница была изменена в последний раз 2024-01-29 11:00:27 UTC

SQLite is in the Public Domain.
https://sqlite.org/lang_with.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API