# SQL

## Що це таке

SQL — це декларативна мова, якою визначають, змінюють і читають дані в
реляційних таблицях: ви описуєте потрібний результат, а база даних сама
вирішує, як його отримати. Абревіатуру зазвичай розшифровують як Structured
Query Language і вимовляють або по літерах, або як «сіквел», а міжнародний
стандарт ISO/IEC 9075, що її визначає, називає мову просто Database Language
SQL.

Ключове слово тут — «декларативна». Інструкція нижче називає таблицю, умову,
групування й порядок, але нічого не каже про індекси, цикли чи про те, які
рядки читати першими:

```sql
SELECT city, count(*) AS deliveries
FROM shipments
WHERE delivered_at >= now() - interval '7 days'
GROUP BY city
ORDER BY deliveries DESC;
```

У PostgreSQL цей текст проходить чотири етапи. Парсер перевіряє синтаксис і
будує дерево, етап переписування розгортає представлення (views), згадані в
інструкції, планувальник порівнює можливі способи виконання — скажімо,
послідовне сканування проти індексу, — і лише тоді виконавець дістає рядки.
Та сама інструкція може тривати мілісекунду або хвилину залежно від того, що
знайде планувальник, тому вміння читати план — частина вміння писати SQL.

Мова складається з частин, які часто називають окремо. Визначення даних
створює та змінює таблиці, схеми й індекси. Маніпулювання даними вставляє,
оновлює, видаляє та зливає рядки. Запити читають їх через SELECT. Привілеї й
транзакції визначають, хто що може робити і які зміни застосовуються разом.
Усе це передається текстом через одне з'єднання, тож SQL — спільна мова для
застосунку, інструмента звітності й людини за терміналом.

Стандарт переглядали багато разів, востаннє — як SQL:2023, і кожна СКБД
реалізує власну його підмножину. Додаток PostgreSQL про відповідність
стандарту зазначає, що система виконує щонайменше 170 зі 177 обов'язкових
можливостей рівня Core і що на момент написання жодна СКБД не заявляла повної
відповідності Core. Понад це кожен рушій має свій діалект: Microsoft SQL Server
говорить мовою Transact-SQL, BigQuery — мовою GoogleSQL, а PostgreSQL додає
PL/pgSQL для процедурного коду, що виконується всередині бази.

## Чому це важливо

Рішення, яке SQL ставить перед командою, — де відбувається робота з даними:
у самій базі, мовою, для виконання якої її створено, чи в коді застосунку,
який вибирає рядки й обробляє їх сам.

Інструкція, що працює з множинами, виконується поруч із даними. Якщо
відфільтрувати мільйон рядків до сорока в базі, мережею піде сорок рядків;
якщо фільтрувати в застосунку, піде мільйон, і більшість із них викинуть.
З'єднання, агрегації та прибирання дублікатів підкоряються тій самій
арифметиці. Перенести таку роботу в SQL — часто найдешевший спосіб пришвидшити
повільну систему, і нової інфраструктури для цього не треба.

Противага — відповідальність за код. Логіка на SQL живе в міграціях і
функціях, які потребують такого самого рев'ю, тестів і контролю версій, як і
решта коду, а довгу функцію на PL/pgSQL вільно читає менше інженерів, ніж код
на Go чи Python. Робочий поділ віддає базі все, що мусить бачити багато рядків,
а правила, які змінюються разом із бізнесом, лишає в застосунку, де їх легше
тестувати окремо.

Переносимість — другий компроміс. Ядро мови — SELECT зі з'єднаннями, GROUP BY та
ORDER BY — переходить між рушіями з мінімальними правками. Краї переходять
погано: арифметика дат, upsert, пагінація, функції JSON і процедурний код
різняться, і перенесення з однієї бази на іншу витрачає на них більшу частину
зусиль. Обираючи рушій, команда обирає й діалект, і що більше логіки живе в
ньому, то дорожче коштуватиме перехід згодом.

Наймання — третій чинник. SQL — одна з небагатьох навичок, спільних для
аналітиків, бекенд-розробників, інженерів даних і адміністраторів баз даних,
тож шар даних на звичайному SQL може прочитати й перевірити будь-хто з них. ORM
ховає SQL, доки сторінка не стане повільною, а тоді комусь однаково доведеться
читати те, що вона згенерувала.

## Як це виглядає на практиці

На одній платформі функції PostgreSQL збирають кожну відповідь API як готовий
JSON-документ, а обробник на Go пересилає її як `json.RawMessage`, не
розбираючи й не кодуючи заново. В
[архітектурі наскрізної передачі JSON](/uk/portfolio/designed-a-json-passthrough-architecture-where-postgresql-functions-58/)
формування відповіді стало завданням SQL і виконується там, де дані вже
лежать. Невеликий помічник QueryJSON зробив цей шаблон найпростішим шляхом, і
DTO, мапери та структури відповідей, які зазвичай накопичує багатошаровий API,
зникли. Код обробників суттєво скоротився, а зміна того, що повертає функція,
доходить до клієнта без жодної правки в обробнику.

У конвеєрі даних та сама мова виглядає інакше. У
[шести конвеєрах ETL/ELT](/uk/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/)
на BigQuery, MSSQL і PostgreSQL сирі файли завантажували в проміжні (staging)
схеми за допомогою bcp, psql і SSIS, а очищення, нормалізацію та збагачення
даних написали як процедури на PL/pgSQL і T-SQL. Три рушії, кожен зі своїм
діалектом, в одній роботі: описане вище питання переносимості, побачене
зсередини. Сторінка вимірює результат так: замість понад трьох годин ручної
праці весь цикл тепер триває менш ніж 20 хвилин, дані оновлюються щодня замість
щотижня, а Python API, які їх читають, стали на 30% швидшими.

## Де виникають проблеми

**Інструкції, склеєні зі шматків тексту.** Клієнт на прізвище O'Brien ламає
сторінку пошуку, або підступно складене введення читає чужі дані. Причина —
введення користувача, вклеєне в текст SQL, де база не може відрізнити дані від
команди. Передавайте значення як параметри, окремо від інструкції, — це
дозволяє кожен поширений драйвер. У клієнтській бібліотеці самого PostgreSQL
параметризований виклик до того ж приймає лише одну команду в тексті, що дає
додатковий захист від SQL-ін'єкцій.

**Порядок, який був випадковістю.** Список, що місяцями приходив
відсортованим, раптом перемішується після завантаження даних або зміни плану.
Без ORDER BY PostgreSQL повертає рядки в тому порядку, який найшвидше
отримати, а LIMIT без ORDER BY може навіть щоразу повертати іншу підмножину.
Кожен результат, для якого важливий порядок, мусить задавати його в самому
запиті.

**NULL як звичайне значення.** Фільтр `WHERE discount = NULL` повертає нуль
рядків, і `NOT IN` щодо підзапиту з NULL поводиться так само. Будь-яке
порівняння з NULL дає «невідомо», а не істину чи хибність, а WHERE залишає
лише рядки, для яких умова істинна. Пишіть `IS NULL` і віддавайте перевагу
`NOT EXISTS` перед `NOT IN`, коли внутрішній стовпець може містити NULL.

**Діалект, що просочується в тести.** Тести проходять на SQLite, а той самий
код падає на PostgreSQL у продакшні; або переїзд на SQL Server спотикається об
кожен `LIMIT`, який цей рушій записує як `TOP` чи `OFFSET ... FETCH`. Кожен
рушій приймає те, що інші відкидають. Тестуйте на тому рушії, який справді
розгортаєте, і тримайте специфічний для рушія синтаксис у якомога меншій
кількості місць.

## Що почитати далі

- [Part II. The SQL Language](https://www.postgresql.org/docs/current/sql.html)
  — повний посібник і довідник PostgreSQL з мови, від синтаксису до порад
  щодо продуктивності.
- [Appendix D. SQL Conformance](https://www.postgresql.org/docs/current/features.html)
  — частини ISO/IEC 9075 і те, які можливості з них реалізує PostgreSQL.
- [Transact-SQL Reference](https://learn.microsoft.com/en-us/sql/t-sql/language-reference)
  — діалект SQL Server, описаний самою Microsoft.
- [Introduction to SQL in BigQuery](https://cloud.google.com/bigquery/docs/introduction-sql)
  — GoogleSQL і застарілий діалект, який він замінив.
- [Query Language Understood by SQLite](https://www.sqlite.org/lang.html)
  — синтаксис вбудованого рушія, інструкція за інструкцією.

<https://engineer.company/uk/glossary/sql/>
