Парсер
В приложении можно выделить 2 основных функциональных блока: парсер и оптимизатор. Данная глава посвящена первому из них.
Задача парсера — разобрать исходный SQL-скрипт и уложить каждый его шаг в объект специально разработанного класса, список таких объектов будет представлять собой направленный граф.
Разрабатываемая программа рассчитана на то, чтобы оптимизировать скрипты, на основе которых далее будут строиться ETL-процессы, это ограничивает утверждения, которые надо разбирать до «create table as select». Остальные операции, присутствующие в скрипте, как правило, носят разовый характер и не должны учитываться.
Ранее, в разделе «Цели и задачи», говорилось о том, что программа должна работать в трех режимах.
Первый — самый простой, анализ скрипта только лишь на основе имеющегося кода. Используется в случае, если из программы нельзя подключиться к базе, а у пользователя нет возможности предоставить нужные исходные данные — DDL исходных таблиц, размер и степень неравномерности распределения промежуточных.
Parser & SQL Data
При данном режиме накладываются ограничения и на разбор SQL-скриптов. Так, при выборе из исходных таблиц (не промежуточных) не допускается конструкция «select *».
Второй — анализ скрипта на основе имеющегося кода и дополнительных исходных данных. Если у программы нет возможности подключиться к базе самостоятельно, для парсера дополнительным преимуществом будет «знание» DDL объектов — определение исходных таблиц со списком их полей и индексами. При наличии такой информации можно разобрать конструкцию «select *».
Третий режим — программа может подсоединиться к базе сама. В данном случае пользователю не надо будет дополнительно собирать данные, что является куда более удобным решением.
В программе есть 3 основных типа объектов, представляющих собой таблицы — TableNode (таблица-узел), TableStep (таблица-шаг) и TableStepUnion (шаг-объединение).
Все объекты этих типов хранятся в списке, по которому можно построить направленный граф (пример на рис. 5).
Рис. 5 Способ представления SQL-скрипта в виде графа
Класс TableStep наследуется от класса TableNode и расширяет его. Класс TableStepUnion также наследует класс TableNode, но в качестве исходных таблиц он использует только TableStep-объекты.
Рассмотрим атрибуты, необходимые в объектах этих классов для полного разбора и дальнейшей оптимизации.
TableNode — класс, представляющий таблицу без учета того, из каких источников и как она собирается.
- — Название схемы БД, в которой лежит таблица
- — Имя таблицы
- — Список колонок
- — PI
Все атрибуты представляют собой строки (имя схемы или таблицы) или списки строк (список колонок, PI)
Класс TableStep наследует атрибуты класса TableNode и добавляет новые, чтобы сохранить то, как именно создается данная таблица.
До описания атрибутов объектов этого класса рассмотрим структуру SQL-запросов на выборку данных в СУБД Teradata.
Live] 실패!!! 파서 콤비네이터를 이용한 SQL Parser만들다가 피곤해서 그만 ㅠ_ㅠ
- — SELECT — список выражений
- — FROM — список исходных таблиц и условий их соединения
- — WHERE — дополнительные условия на отдельные строки итогового результата
- — GROUP BY — список полей/выражений, по которым надо группировать
- — HAVING — условия на группы
- — ORDER BY — упорядочивание вывода строк (только в простом select)
- — QUALIFY — условия на аналитические функции
QUALIFY — особенность диалекта SQL, используемого в Teradata. Выражения, использующиеся в этой части — аналог части having, но для аналитических функций. Такой вид функций позволяет работать с группой данных, при этом не прибегая к группировке, что в некоторых ситуациях значительно упрощает скрипты, уменьшая количество необходимых для достижения результата шагов. Конструкция qualify же позволяет отфильтровать неподходящие записи при том же чтении из таблицы, тогда как в oracle, например, для этого понадобится дополнительная выборка с условием where на результат аналитической функции.
Еще одна особенность диалекта SQL Teradata — возможность использования в GROUP BY номеров колонок из части SELECT. Это позволяет сократить длину запроса, но иногда делает его менее удобным для чтения, особенно если SELECT-список достаточно длинный. Так вместо GROUP BY FIELD_1, FIELD_2, FIELD_3 можно написать GROUP BY 1,2,3. Цифры, названия колонок и выражения можно использовать в произвольном сочетании.
Из представленной выше структуры в исходных SQL-скриптах могут встречаться все конструкции, кроме ORDER BY. Из остальных обязательно должны присутствовать только SELECT и FROM, а HAVING может быть только при наличии GROUP BY.
Класс TableStepUnion наследует класс TableNode. Кроме атрибутов основного класса он содержит только список TableStep-шагов (приведенных к типу TableNode) и то, какой именно операцией эти исходные таблицы объединяются: union (all) — объединение (all — не убирая дубли из финального результата), except (all) — исключение результата второй выборки из первой, intersect (all) — данные, совпадающие в витринах, minus аналог Teradata для except. На практике очень часто intersect, except и minus реализовываются с помощью inner или left объединений, тогда основным случаем использования типа таблиц TableStepUnion становится именно объединение данных.
Считается, что скрипт, который подается на оптимизацию, синтаксически корректен. Парсер не должен заниматься проверкой синтаксиса, однако, если что-то не получится разобрать, он выдаст ошибку с указанием примерного места и части запроса/скрипта, в которой произошла ошибка.
В таблице 5 приведено описание основных классов, используемых для реализации хранения в программе SQL-кода (часть TableStep), и их назначения:
Таблица 5 Краткое описание основных классов в TableStep
«Обертка» для класса TableNode, к таблице приписывается псевдоним.
Название колонки и таблица (TableSelect), из которой она выбирается
Выражение — текст и список колонок, которые в этом выражении используются. В самом тексте колонки заменены на маркеры вида «[0]».
Колонки в select-списке. Представляет собой выражение (Expression) и псевдоним (при наличии).
Условие. Объекты данного класса могут использоваться в QUALIFY, WHERE, HAVING и внутри FROM в соединениях таблиц
Условие соединения таблиц. Тип Join’а и список условий (Condition) при их наличии.
После выделения из шага скрипта select-части она представляется в следующем виде:
- — select expression_list:
- — expression alias
- — …
- — from join_list:
- — left table join right table on condition_list
- — left_expression condition right_expression
- — where condition_list
- — left_expression condition right_expression
- — group by expression_list (ссылки на выражения из select)
- — having condition_list
- — qualify condition_list
Для разбора SQL-скриптов были разработаны 2 класса: SQLСleanUp и SQLParser. Первый принимает на вход текст скрипта целиком, очищает его от мусора (в т.ч. не интересующие нас для оптимизации шаги).
Такая структура близка к способу хранения информации о ETL-процессе в инструменте для их построения — SAS Data Integration Studio, поэтому данный парсер может быть в дальнейшем использован и для других проектов, в частности — автоматической сборки простейших ETL по SQL-коду.
Источник: vuzlit.com
sqlparse 0.4.3
sqlparse is a non-validating SQL parser for Python. It provides support for parsing, splitting and formatting SQL statements.
The module is compatible with Python 3.5+ and released under the terms of the New BSD license.
Visit the project page at https://github.com/andialbrecht/sqlparse for further information about this project.
Quick Start
$ pip install sqlparse
Links
Project page
sqlparse is licensed under the BSD license.
Источник: pypi.org
Alexeyyy/SQL-Parser
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch branches/tags
Branches Tags
Could not load branches
Nothing to show
Could not load tags
Nothing to show
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
- Local
- Codespaces
HTTPS GitHub CLI
Use Git or checkout with SVN using the web URL.
Work fast with our official CLI. Learn more.
Sign In Required
Please sign in to use Codespaces.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
Git stats
Files
Failed to load latest commit information.
Latest commit message
Commit time
README.md
Парсер, который позволяет «разобрать» SQL запрос вида SELECT по «полочкам».
Например, вот такие запросы (несовсем логичные примеры с точки зрения смысла, но синтаксически верные —> значит имеют место быть):
select f.*, (select sum ( a.flight_id)from flights as a group by a.flight_id) as s from flights as f join aircrafts as air on ((((air.aircraft_code) = f.aircraft_code)) and air.
aircraft_code = f.aircraft_code) or (1=1 and 1=1 or (((2 > 3 and 4 2 or 3!=3 and 2 1)))) full outer join aircrafts_data as ad on ad.aircraft_code = air.
aircraft_code left join airports_data as apd on apd.airport_code = f.aircraft_code and apd.airport_code = (select ap.airport_code from airports as ap where (select 1 as column) = 1 limit 1 ) where f.
flight_no = ‘a’ and ((f.aircraft_code = ‘asd’ or f.aircraft_code = ‘5454’ ) and f.
aircraft_code = ‘2332’ or (select count(*) + (select count(*) from flights) from tickets) > 10) order by f.aircraft_code, f.flight_no, (select max(airports.
airport_code) from airports limit 10) asc limit ( select count(*) from airports )
SeLeCt «f1».flight_id fRoM flights as «f1» inner join flights as f_2 on «f1».aircraft_code = f_2.
aircraft_code where «f1».flight_id between 10 and 100 and f_2.flight_id not in (1,2,3,4,5,6,7, (select 1), (select (select 2))) group by «f1».flight_id order by «f1».flight_id DESC limit ((select count(*) alias_1 from flights limit all) + (select count(*) + 1 alias_2 from flights limit null)) offset (select 8)
select (select (select (select (select (select (select (select (select (select 1 a)b)c)d)e)f)g)h)i)k
P.S. схема и данные PG-базы взяты с сайта PostgresPro, за что им отдельное Спасибо!
Как работает парсер?
Если не вдаваться в подробности, то парсер работает по следующему алгоритму:
- На вход получает набор запросов, которые необходимо «распарсить». Вот пример тестового файла.
- «Нормирует» строку запроса с точки зрения удаления лишних пробелов
- Находит все подзапросы и составляет «дерево вложенности». Для i-го запроса все видимые подзапросы маркируются guid-ами
- Раскладывает запрос и все его подзапросы по ключевым словам — clauses, ими являются select, from, join, where, order by, group by, having, limit, offset
- Парсит каждый из запросов в отдельности и сохраняет данные в виде списков
- Печатает информацию в виде таблиц в файл. Пример выходных данных программы-парсера
Дополнительно был реализован функционал восстановления запроса – обратная операция, чтобы была возможность протестировать правильность раскладки частей запроса парсером.
В парсер не встроена проверка на синтаксическую правильность SQL (компилятора тут нет =) ), поэтому пожалуйста подавайте запрос, который компилируется утилитой psql или какой-нибудь средой, например, DataGrip.
Недостатком парсера является то, что он не поддерживает алиасы в виде ключевых слов SQL (могут быть интересные последствия), например:
SELECT 10 AS JOIN
Источник: github.com
Парсинг и оптимизация SQL и PL/SQL в базе данных Oracle
Вы можете ознакомиться с тем, как работает кэш данных в СУБД Oracle в этой и этой статьях, мы можем вернуться к кэшу кода, точнее – к библиотечному кэшу (library cache) и к кэшу словаря (dictionary cache). Оба кэша являются частью разделяемого пула (shared pool, или кучи SGA), поэтому мы неизбежно будем знакомиться с некоторыми универсальными механизмами, воздействующими на весь разделяемый пул.
Так как библиотечный кэш является тем местом, куда попадают инструкции SQL или PL/SQL (в форме, пригодной для выполнения), мы начнем наше путешествие по библиотечному кэшу с момента, когда пользовательская программа посылает Oracle фрагмент текста и ожидает выполнения некоторых действий.
После того, как мы узнаем, что Oracle делает с прежде не встречавшимся фрагментом текста, мы займемся исследованием разных способов повторного выполнения заданного фрагмента и посмотрим, как меняется нагрузка на Oracle, в зависимости от выбранного способа.
Понимание SQL
Давайте рассмотрим процесс парсинга и оптимизации SQL в Oracle. Возьмем за основу следующую инструкцию:
update t1 set small_no = small_no + 0.1 where small_no = 1 ;
Насколько сложно будет базе данных Oracle понять, что означает эта инструкция, и выбрать наиболее оптимальный способ ее выполнения? Строго говоря, первая часть вопроса относится к парсингу (parsing), а вторая – к оптимизации. К сожалению многие склонны объединять эти два понятия под одним названием «парсинг».
Парсинг команд SQL в базе данных Oracle
Первым делом, разумеется, база данных Oracle проверяет – является ли этот текст допустимой инструкцией SQL – этот шаг известен как синтаксический анализ. Если будет сделан вывод, что это допустимая инструкция, Oracle попробует ответить себе на множество вопросов об этой инструкции, таких как:
- Каков тип объекта t1? Таблица, представление, синоним или что-то иное.
- Являются ли id и small_no столбцами объекта t1 (может быть id – это функция, возвращающая целое число)?
- Имеет ли столбец small_no какие-либо ограничения, которые необходимо проверить перед его изменением?
- Имеются ли в объекте какие-либо триггеры, которые должны срабатывать при обновлении?
- Имеются ли какие-нибудь индексы, которые должны обновляться вместе с объектом?
- Имеет ли данный пользователь право изменять объект?
Эта стадия исследования называется семантическим анализом – комбинация синтаксического и семантического анализа как раз и называется парсингом.
Оптимизация инструкций SQL в базе Oracle
Как только будет выяснено, что инструкция является допустимой и пользователь обладает всеми необходимыми правами для ее выполнения, Oracle начинает оптимизировать SQL и в ходе оптимизации пытается ответить еще на ряд вопросов, таких как:
- Имеются ли какие-либо индексы, включающие столбцы id и/или small_no , которые могут помочь в выполнении?
- Какие статистики доступны для столбцов, таблиц и индексов?
После этого оптимизатор собирает дополнительную информацию о системе (значения различных параметров – динамических переменных и статистик) и запускает сложную процедуру, определяющую путь выполнения инструкции. Но об этой стадии мы побеседуем потом. А сейчас я хочу поговорить об основах парсинга.
Чтобы показать, какой объем работы проделывает версия 11g (11.2.0.2, в данном случае) для парсинга даже простой инструкции, я очистил разделяемый пул (причины я объясню чуть ниже) вызовом alter system flush shared_pool; enabled sql_trace, выполнил запрос, приведенный выше, и затем запустил утилиту tkprof для анализа полученного файла трассировки. Заключительный раздел в файле, произведенном утилитой tkprof, содержал следующую сводную информацию:
1 session in tracefile. 2 user SQL statements in trace file. 15 internal SQL statements in trace file. 17 SQL statements in trace file. 17 unique SQL statements in trace file. 737 lines in trace file. 0 elapsed seconds in trace file.
Итак, в трассировочном файле хранятся сведения о выполнении двух «пользовательских» ( user ) инструкциях SQL: первая – это инструкция alter session set sql_trace true ;, которую я выполнил, чтобы включить sql_trace , и вторая – моя инструкция update . Однако, Oracle выполнил еще 15 внутренних ( internal ) инструкций SQL – вспомогательных инструкций, с помощью которых Oracle собрал всю информацию, необходимую для парсинга и оптимизации моей тестовой инструкции. Инструкции этого типа обычно называют системно-рекурсивными (sys-recursive) инструкциями. Если вы думаете, что 15 дополнительны инструкций – это много, значит вы еще не видели всю картину целиком. Ниже приводится статистики, отражающие работу этих 15 инструкций:
OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS call count cpu elapsed disk query current rows ——- —— ——- ———- ———- ——— ——— ——— Parse 15 0.01 0.00 0 0 0 0 Execute 99 0.06 0.05 0 0 0 0 Fetch 189 0.03 0.01 0 388 0 633 ——- —— ——- ———- ———- ——— ——— ——— total 303 0.10 0.07 0 388 0 633 Misses in library cache during parse: 15 Misses in library cache during execute: 15
В строке Parse можно видеть число вызовов parse для передачи 15 внутренних инструкций механизму Oracle. Но взгляните на цифры ниже – общее число вызовов execute для выполнения всех этих инструкций составило 99, и 189 вызовами fetch было извлечено 633 строки информации. Объем выполняемой работы поражает.
В действительности этот демонстрационный пример не совсем точно отражает нагрузку, и я могу показать расхождения, если воспользуюсь еще одной таблицей в той же схеме. Вторая таблица называется t1a и является точной копией таблицы t1 – вплоть до определения первичного ключа. Если сейчас запустить ту же инструкцию update , подставив имя t1a вместо t1 , утилита tkprof сгенерирует следующие результаты.
OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS call count cpu elapsed disk query current rows ——- —— ——— ——— ———- ——— ——— ——— Parse 15 0.00 0.00 0 0 0 0 Execute 43 0.00 0.00 0 0 0 0 Fetch 51 0.00 0.00 0 135 0 526 ——- —— ——— ——— ———- ——— ——— ——— total 109 0.00 0.00 0 135 0 526 Misses in library cache during parse: 0
Даже при том, что число вызовов parse осталось тем же, появились любопытные расхождения в числе вызовов execute и fetch . Существует еще одно важное отличие в значении Misses in library cache . Эта последняя деталь многое говорит нам о том, как Oracle пытается добиться максимальной эффективности при обработке инструкций SQL, и дает некоторые подсказки, касающиеся кэширования выполняемого кода, если знать, как правильно интерпретировать результаты.
Интерпретация результатов tkprof
Прежде чем выполнить первую инструкцию, я очистил разделяемый пул. Сделано это было для того, чтобы удалить любые, выполнявшиеся прежде, инструкции из библиотечного кэша, вместе с информацией о привилегиях и зависимостях, а также все определения объектов из кэша словаря.
Когда я попытался выполнить первую тестовую инструкцию, Oracle выполнил запрос к базе данных (или, если говорить точнее, к множеству таблиц, известному как словарь данных (data dictionary)), чтобы найти определения всех объектов, на которые ссылается инструкция. Это видно по инструкции, сохраненной в трассировочном файле:
select obj#, type#, ctime, mtime, stime, status, dataobj#, flags, oid$, spare1, spare2 from obj$ where owner#=:1 and name=:2 and namespace=:3 and remoteowner is null and linkname is null and subname is null ;
Данная конкретная инструкция проверяет наличие объекта в указанном пространстве имен и принадлежащего указанной схеме в локальной базе данных. Другие запросы обращаются к таким таблицам, как tab$ – за информацией о таблицах, col$ – за информацией о столбцах, ind$ – за информацией об индексах, icol$ – за информацией о столбцах, для которых определены индексы, и так далее. Однако в качестве примера я выбрал данный запрос потому, что он был выполнен пять раз для первой тестовой инструкции и только два раза – для второй.
В обоих тестах было выполнено два запроса для поиска некоторой информации о таблице ( t1 / t1a ) и ее первичном ключе ( t1_pk /t1a_pk ) – но затем в первом тесте было выполнено еще три запроса для поиска информации о паре индексов с именами i_objauth2 и i_objauth1 , и таблицы trigger$. Oracle потребовались дополнительные сведения о внутренних таблицах и индексах, чтобы решить, как выполнить рекурсивные инструкции SQL для поиска дополнительной информации о моих таблицах и индексах. Это объясняет, почему для первого тестового запроса потребовалось выполнить больше работы, чем для второго. В ходе выполнения первого теста была выполнена масса «подготовительных» операций, тогда как в ходе выполнения второго теста использовалась кэшированная информация,
оставшаяся после первого теста.
Когда я говорю о кэшированной информации, я не имею в виду кэшированные данные. В данном случае подразумеваются кэшированные метаданные (информация о данных) и код – именно для этой цели и существуют кэш словаря и библиотечный кэш, соответственно. Кэш словаря (также известный как кэш строк (row cache)) хранит информацию об объектах, таких как таблицы, индексы, столбцы, гистограммы, триггеры и так далее. Библиотечный кэш хранит информацию об инструкциях (SQL и PL/SQL), их курсорах, а также массу другой информации о зависимостях объектов и привилегиях.
Изменение числа выполненных запросов во втором тесте наглядно показало, что Oracle использует преимущества, которые дает кэш словаря. Например, чтобы найти информацию об индексе i_objauth1 , не потребовалось выполнять какие-либо запросы, потому что эта информация уже была прочитана из словаря данных и скопирована в кэш словаря в процессе выполнения первого теста.
Аналогично, изменение числа промахов в библиотечном кэше (library cache misses) во втором тесте наглядно показало, что Oracle использует преимущества, которые дает библиотечного кэша. Взгляните на следующие строки, которые я извлек из результатов работы утилиты tkprof. После первого теста она сообщила:
Misses in library cache during parse: 15 Misses in library cache during execute: 15
Misses in library cache during parse: 0
Промах (miss) библиотечного кэша говорит, что попытка найти в библиотечном кэше (допустимый и действительный) дочерний курсор провалилась, и потому пришлось оптимизировать инструкцию перед ее использованием. В первом тесте пришлось оптимизировать все 15 рекурсивных инструкций SQL.
Во втором тесте – ни одной, потому что соответствующие курсоры уже имелись в библиотечном кэше. Первый тест показывает наличие промахов обоих видов – при парсинге и выполнении – каждой инструкции. Отсутствие промахов при выполнении возможно, но в данном случае, я думаю, проявилась ошибка в 11g (см. примечание ниже) – как показала трассировка события 10 053, инструкции оптимизировались один раз в ходе тестирования.
Примечание. В выводе tkprof каждая инструкция должна сопровождаться двумя строками, в которых указывается число промахов библиотечного кэша в ходе выполнения parse и execute . С помощью этой информации можно определить, сколько раз инструкция оптимизировалась в процессе трассировки. К сожалению, похоже, что 11g сообщает о промахах execute сразу после промаха parse , и я полагаю (но не могу этого доказать), что это ошибка. В ранних версиях Oracle статистика Misses in library cache during execute давала удобную возможность определить потери оптимизированных инструкций из-за нехватки памяти – я думаю, что в 11g следует вычитать промахи parse из промахов execute , чтобы получить точное представление о промахах в ходе выполнения.
Далее мы продолжим использовать этот пример в процессе исследования библиотечного кэша и кэша словаря. И начнем с кэша словаря, но уже в новой статье.
Источник: oracle-patches.com