Введення.Співбесіди на позиції, пов’язані з даними (аналітики, інженери, вчені даних) все частіше включають нестандартні та просунуті питання щодо SQL. Великі технологічні компанії (Google, Amazon та ін.) висувають високі вимоги: важлива не тільки правильність запиту, але й уміння оптимізувати його та розбиратися у реальних бізнес-даних[1][2]. У цьому гайді ми розберемо категорії найпоширеніших складних SQL-задач із реальних співбесід – від платформ на кшталт DataLemur, LeetCode, StrataScratch – і докладно пояснимо рішення. Кожне завдання супроводжується аналізом: умова, оптимальний підхід, використовувані SQL-конструкції, можливі помилки та фінальне рішення (для PostgreSQL та MySQL, із зазначенням відмінностей, де необхідно). Наприкінці додано окремий розділ про сучасні бази даних, включаючи векторні БД (Pinecone, Weaviate, Milvus та ін.), з прикладами того, що можуть запитати про них на співбесіді та як виглядають SQL-подібні запити для роботи з векторами.
Агрегатні функції та угруповання даних
Агрегація– Основа багатьох аналітичних запитів. Інтерв’юери перевіряють вміння використовувати COUNT, SUM, AVG та інші агрегати з GROUP BY та фільтрацією результатів. Завдання цього типу вимагають обчислити метрики за групами (наприклад, сумарний продаж за категоріями, середній чек за клієнтами тощо) і правильно застосувати умови відбору результатів через HAVING. Важлива навичка – розуміти різницю між фільтрацією до агрегування (WHERE) та після агрегування (HAVING).
- Приклад 1 (угруповання з умовою):“Типи заявок”. Дано таблицю заявок loans з полем type (тип заявки). Потрібно вивести user_id тих користувачів, які зробили щонайменше одну заявку типу “Refinance” та мінімум одну типу “InSchool”[3][4]. Оптимальне рішення – відфільтрувати записи потрібних типів та згрупувати за користувачем, вимагаючи наявності обох типів:
SELECT user_idFROM loansWHERE type IN ('Refinance', 'InSchool')GROUP BY user_idHAVING COUNT(DISTINCT type) = 2;
Тут ми спочатку обмежуємо рядки двома потрібними значеннями type (WHERE), потім групуємо по user_id і за умови HAVING перевіряємо, що у кожній групі є обидва типу (через COUNT(DISTINCT type) = 2). Можлива помилка: використовувати WHERE замість HAVING для перевірки кількості – це не спрацює, тому що умову на агрегат можна встановити лише після угруповання. Також важливо використовувати COUNT(DISTINCT …), інакше, якщо в таблиці є дублі, простий COUNT(*) міг би дати хибний результат. У нашому запиті WHERE фільтрує потрібні типи до агрегування, а HAVING гарантує наявність обох типів у групі[5]. Такий запит коректно виконується і в PostgreSQL, і MySQL.
- Приклад 2 (агрегат та підзапит):«Міста з найдорожчими будинками». Це завдання (реальне питання від Zillow) вимагає знайти назви міст, де середня ціна будинку вища за середньоринкову по країні[6][7]. Ми маємо таблицю продажів zillow_transactions(city, mkt_price) і потрібно порівняти середню ціну по місту із середньою ціною по всій таблиці. Рішення – використовувати підзапит у секції HAVING:
SELECT cityFROM zillow_transactions AS zGROUP BY cityHAVING AVG(z.mkt_price) > ( SELECT AVG(mkt_price) FROM zillow_transactions)ORDER BY city;
У внутрішньому субзапиті обчислюємо загальнонаціональну середню ціну (SELECT AVG(mkt_price) FROM …), а у зовнішньому запиті вибираємо ті city, для яких середня ціна більша за цю величину[8]. Тут підзапит виконується один раз для всього запиту (не корельований), що є ефективним. Можливі помилки: забути згрупувати по city або спробувати застосувати WHERE для порівняння із середнім (знову ж таки потрібно HAVING, тому що фільтр по агрегату). Цей запит є універсальним для SQL-92, підтримується і PostgreSQL, і MySQL без змін.
- Приклад 3 (умови на агрегати):«Сума за парними та непарними вимірами». Питання з інтерв’ю Google: є таблиця показань датчика, де кожен вимір має унікальний measurement_id та значення. Потрібно вивести для кожного дня суму значень непарних і парних ID вимірювань окремо. Рішення – використовувати агрегування з умовою через CASE (або фільтр Postgres):
SELECT DATE(measurement_time) AS measurement_day, SUM(CASE WHEN measurement_id % 2 = 1 THEN measurement_value ELSE 0 END) AS odd_sum, SUM(CASE WHEN measurement_id % 2 = 0 THEN measurement_val measurementsGROUP BY DATE(measurement_time);
Тут ми групуємо по дню (обрізаючи час) і обчислюємо дві суми: одна підсумовує значення лише там, де measurement_id непарний, інша – де парний. У PostgreSQL ту ж логіку можна записати коротше за допомогою модифікатора FILTER: SUM(measurement_value) FILTER (WHERE measurement_id % 2 = 1) (але в MySQL синтаксису FILTER немає, тому застосовуємо CASE як вище). Примітка: переконайтеся, що ви використовуєте правильну функцію для вилучення дати – наприклад, DATE() або CAST … AS DATE – щоб угруповання сприйняло значення як однакові для всіх записів цього дня.
Чому це важливо?Вміння комбінувати агрегати з фільтрацією та підзапитами показує, що кандидат може обчислювати нетривіальні метрики. У Big Tech-інтерв’ю можуть запитати щось на кшталт «Знайдіть медіанний показник X за групами Y» або «Скільки користувачів виконали певні типи дій», перевіряючи знання HAVING, DISTINCT та вкладених запитів. Наприклад, Google у реальному інтерв’ю питав знаходження медіани числа пошукових запитів на користувача, знаючи розподіл частот[9][10]. Такі завдання вимагають творчо застосовувати агрегати: медіану можна отримати, обчисливши кумулятивний розподіл чи використовуючи спеціальні функції. У PostgreSQL є агрегатні функції для перцентилів (наприклад, percentile_cont(0.5)), але MySQL довелося б вручну вибирати середнє двох центральних значень або писати підзапит з порядком. Головне – розуміти принцип та описати його співрозмовнику.
Віконні функції (Window Functions)
Віконні функції – потужний інструмент для аналізу даних. Вони дозволяють обчислювати ковзні агрегати, ранги та показники по вікну рядків, зберігаючи при цьому всі вихідні рядки. Питання на співбесіді часто перевіряють знання ROW_NUMBER(), RANK(), DENSE_RANK(), а також функцій зі зсувом (LAG()/LEAD()) та рамок (PARTITION BY та рамки ROWS BETWEEN). У Big Tech завданнях віконні функції застосовуються, наприклад, для знаходження рекордів, рейтингів, top-N результатів за групами або в усьому наборі даних, без втрати подробиць рядків.
Порівняння функцій ранжирування (ROW_NUMBER, RANK, DENSE_RANK) у SQL.
Перш ніж перейти до прикладів, коротко згадаємо різницю між основними функціями ранжирування[11]. Нехай ми маємо відсортований список значень (наприклад, оцінки користувачів).
- ROW_NUMBER() надає унікальний порядковий номер кожному рядку в межах партиції (без перепусток).
- RANK() присвоює однаковий ранг рівним значенням, пропускаючи наступне значення рангу після групи рівних (тобто якщо два елементи поділили 1-е місце, наступний отримає ранг 3).
- DENSE_RANK() теж дає рівним значенням один ранг, але не пропускає наступний номер (у тому випадку наступний після двох перших також отримає ранг 2).
Наприклад, для значень [100, 90, 90, 80] ROW_NUMBER() дасть [1, 2, 3, 4]; RANK() – [1, 2, 2, 4]; DENSE_RANK() – [1, 2, 2, 3].
Розглянемо тепер завдання, де без віконних функцій не обійтися:
- Приклад 4 (ранжування та вибір топ-N):«Найдальніші поїздки (топ-10 користувачів)». У реальному інтерв’ю Lyft кандидату дали дві таблиці: lyft_rides_log(user_id, distance) – лог поїздок із довжинами, та lyft_users(id, name) – дані користувачів. Попросили знайти 10 користувачів, які сумарно проїхали найбільшу відстань, та вивести їх id, ім’я та загальну дистанцію[12][13]. Цей запит поєднує JOIN, агрегування та віконну функцію ранжування:
SELECT user_id, name, traveled_distanceFROM ( SELECT lu.id AS user_id, lu.name, SUM(lr.distance) AS traveled_distance, RANK() OVER (ORDER BY SUM(lr.distance) DESC) AS rnk FROM lyft_user ON lu.id = lr.user_id
GROUP BY lu.id, lu.name) AS subWHERE rnk <= 10;
Тут усередині підзапиту ми з’єднали таблиці, згрупували по користувачеві та порахували SUM(distance) для кожного. Потім за допомогою RANK() OVER (ORDER BY SUM(distance) DESC) надали ранг – 1 для найбільшого пробігу, 2 для наступного і т.д.[14]. У зовнішньому запиті вибрали лише рядки з rnk <= 10 (перші 10 рангів). Зверніть увагу: ми не вказали PARTITION BY, отже, ранжування йде по всьому набору записів (в даному випадку по всіх користувачах). Чому не ROW_NUMBER()? У цьому завдання, якщо кілька користувачів проїхали однакову відстань, вони отримають однаковий ранг і разом увійдуть до топ-10, потенційно повернувши трохи більше ніж 10 результатів. Якщо ж вимагалося строго 10 записів, незалежно від рівності, можна було взяти ROW_NUMBER() – тоді при сумах, що збігаються, хтось би випав з десятки. Співрозмовник зазвичай уточнює, як зробити з рівними значеннями. Відмінності СУБД: PostgreSQL і MySQL (починаючи з 8.0) підтримують віконні функції ідентично. У MySQL необхідно переконатися, що версія >= 8.0, інакше такий запит не виконається, т.к. старіші версії віконні функції не підтримують.
- Приклад 5 (ранги з умовою сортування):“Рейтинг активності користувачів пошти”. Питання з інтерв’ю Google: дано таблицю google_gmail_emails(from_user, to_user) з повідомленнями. Потрібно вивести для кожного відправника загальну кількість надісланих листів та його ранг активності (1 – у найактивнішого)[15][16]. Причому, якщо кілька користувачів відправили однакову кількість листів, сортувати їх на ім’я (алфавіту), але присвоїти їм різні ранги (тобто не мати “нічиїх” за рангом). Цей випадок – класичний патерн: потрібно використовувати ROW_NUMBER(), але другою умовою сортування врахувати ім’я, щоб розривати можливі нічиї за кількістю. Запит:
SELECT from_user, COUNT(*) AS total_emails, ROW_NUMBER() OVER ( ORDER BY COUNT(*) DESC, from_user ASC ) AS activity_rankFROM google_gmail_emailsGROUP BY from_user;
Тут у віконній функції ROW_NUMBER() ми вказали одразу два критерії впорядкування вікна: спершу за зменшенням кількості листів, а потім на ім’я користувача зростання[16]. Це гарантує унікальний ранг навіть за рівного COUNT(*). Зверніть увагу: у секції ORDER BY віконної функції можна використовувати агрегат COUNT(*) безпосередньо, хоча в самому SELECT поза віконною функцією агрегати повинні бути або всередині агрегатної функції, або в GROUP BY. SQL оптимізатор розуміє, що вікно обчислюється після GROUP BY. Альтернативи: можна було визначити ранги через підзапит з DENSE_RANK() і потім для однакових значень вручну поправити, але використання ROW_NUMBER з додатковим сортуванням простіше.
- Приклад 6 (пошук максимуму з використанням віконної функції):“Максимальне число перемог”. Питання від Netflix: дана таблиця oscar_nominees(nominee, winner) з інформацією про нагороди, де winner = TRUE для виграних номінацій. Потрібно знайти, хто отримав найбільше премій Оскар (тобто максимальне число winner=TRUE), та вивести ім’я та кількість[17][18]. Завдання можна вирішити підзапит з агрегатами або навіть одним запитом з ORDER BY LIMIT 1, але інтерв’юер міг очікувати рішення з віконною функцією RANK() для демонстрації навичок:
WITH wins AS ( SELECT nominee, COUNT(CASE WHEN winner = TRUE THEN 1 END) AS wins_count, RANK() OVER (ORDER BY COUNT(CASE WHEN winner = TRUE THEN 1 END) DESC) AS rnk FROM oscar_nom wins_countFROM winsWHERE rnk = 1;
Тут ми використовуємо COUNT з умовою (через CASE) або можна було для підрахунку перемог. Застосовуючи RANK () OVER (ORDER BY … DESC), привласнюємо ранг 1 володарям найбільшого числа перемог. Потім у зовнішньому запиті фільтруємо WHERE rnk = 1 – це може повернути кілька імен, якщо кілька людей лідирують за кількістю нагород. Обговорення: інтерв’юер може запитати, чим відрізняється використання ROW_NUMBER() – в даному випадку ROW_NUMBER() повернув би лише одну людину (при однаковій кількості перемог вибрав би когось за неявним порядком), що неправильно за змістом. DENSE_RANK() теж підійшов би – дав би 1 для лідерів, 2 для наступних тощо – але якщо нам потрібні лише лідери, достатньо RANK(). У рішенні від Netflix було зазначено, що ROW_NUMBER() не підходить, а RANK() чи DENSE_RANK() – коректні[19][20].
Віконні функції та продуктивність.У великих компаніях можуть звернути увагу, що з читанні великих таблиць використання віконних функцій без партиціонування може бути важким. Тому, якщо доречно, варто застосовувати PARTITION BY для розбивки на групи (наприклад, ранжування в кожній категорії). Також важливо знати, що MySQL 8+ і PostgreSQL віконні функції реалізовані ефективно, але все ж індекси на поля сортування можуть допомогти. Однак індекси не завжди використовуються для вікон (особливо якщо є PARTITION BY), тому іноді запит з віконною функцією важко оптимізувати. На співбесіді достатньо показати розуміння, що відбувається – що СУБД має відсортувати дані у вказаному порядку для обчислення вікна, і це може бути дорогим.
Підзапити та CTE (вкладені запити)
Підзапитиперевіряють вашу здатність розбити складне завдання на частини. Вони бувають некорельовані (виконуються незалежно) та кореловані (залежать від поточного рядка зовнішнього запиту). Також сюди відносяться CTE (Common Table Expressions, “пов’язані таблиці” після WITH), які часто покращують читання рішення.
Запитання з цієї області можуть вимагати: – використовувати підзапит для обчислення проміжного результату (наприклад, загальна середня величина, як ми робили вище у прикладі з містами), – або корелированное підзапит для фільтрації за умовою на кожен рядок (наприклад, «знайдіть записи, які є останніми за часом для HE… (SELECT …)).
Також інтерв’юери люблять запитати «що таке корельований підзапит» теоретично.
Корельований підзапит– це підзапит, який виконується для кожного рядка зовнішнього запиту, використовуючи його значення[21]. Наприклад, застосування: знайти працівників, чия зарплата вища за середню за їхнім відділом – можна реалізувати корелованим підзапитом, що порівнює зарплату співробітника з SELECT AVG(salary) FROM employees WHERE dept_id = outer.dept_id. Таке підзапит перераховуватиметься для кожного рядка зовнішньої таблиці. Це менш ефективно, ніж об’єднання через JOIN або використання агрегатів із угрупованням, але іноді зручніше.
- Приклад 7 (корельований підзапит):«Співробітники із зарплатою вищі, ніж у їхнього менеджера». Класичне завдання (є на LeetCode): у таблиці Employee(emp_id, name, salary, manager_id) потрібно вибрати імена тих співробітників, чия salary більше, ніж salary їх менеджера. Одне з рішень – корельований підзапит:
SELECT e.nameFROM Employee AS eWHERE e.salary > (SELECT m.salary FROM Employee AS m WHERE m.emp_id = e.manager_id);
Тут внутрішній запит обирає зарплату менеджера співробітника e. Він виконується для кожного рядка в зовнішньому запиті. Зауваження: якщо співробітник не має менеджера (manager_id = NULL), підзапит поверне NULL і порівняння > NULL дасть FALSE, такий рядок просто відкидається, що логічно. В інтерв’ю очікують розуміння, що це завдання можна вирішити самооб’єднанням (self-JOIN): з’єднати таблицю Employee сама з собою по e.manager_id = m.emp_id і порівняти e.salary > m.salary. Такий JOIN-підхід часто краще за ефективністю, тому що замість багатьох підзапитів виконується одне з’єднання.
- Приклад 8 (кілька рівнів вкладеності):Іноді трапляються завдання, де потрібно використовувати кілька підзапитів. Наприклад, на платформі StrataScratch було питання: “Знайдіть користувача, який зробив другу за кількістю покупок на сайті”. Рішення: спочатку отримати агреговану таблицю (користувач -> число покупок), потім взяти ранг за кількістю і вибрати ранг 2. Це можна зробити через два підзапит: внутрішній для агрегації і зовнішній для фільтрації по рангу, або через віконну функцію. Підзапити можуть вкладатись і глибше, але важливо не переускладнювати – у реальному інтерв’ю запитують до 2-3 рівнів вкладеності максимум. Якщо бачите, що вкладеність стає важко читати, краще запропонувати альтернативу з CTE.
- Коли краще CTE:Common Table Expression, яка оголошується через WITH, корисна, коли потрібно використовувати результат підзапиту кілька разів або просто зробити запит читання. Наприклад, те саме завдання з Оскарами (Приклад 6) ми вирішили через CTE wins для зрозумілості. У PostgreSQL CTE за умовчанням матеріалізується як тимчасовий результат (починаючи з версії 12, оптимізатор може “розгортати” прості CTE), а ось MySQL 8.0 обробляє WITH як субзапит, що оптимізується. На співбесіді від вас не чекають глибокого знання внутрішніх відмінностей реалізації CTE, але важливо правильно використовувати синтаксис та доречно пояснити, що CTE покращує структуру запиту.
Порада:якщо ставлять відкрите теоретичне питання типу «Що таке корельований підзапит і коли його застосовувати?», варто коротко відповісти (як у тексті вище) і навести невеликий приклад сценарію[21]. Зазвичай цього достатньо.
Об’єднання таблиць (JOINS) та складні джерела даних
Практично будь-яка реальна аналітична задача стосується даних з декількох таблиць. Тому на інтерв’ю часто дають завдання JOIN різних типів: внутрішнє з’єднання (INNER JOIN), зовнішні (LEFT/RIGHT JOIN), повне (FULL JOIN), а також самоз’єднання. Кандидат повинен розуміти, як працюють ці сполуки та у яких випадках їх застосовувати. Також можуть перевірити знання ключових моментів: що таке Cartesian product (декартовий твір), чим відрізняється WHERE vs ON при з’єднанні, і як поєднувати дані, щоб не втратити записи.
- Приклад 9 (множинний JOIN та фільтрація):“Унікально закріплені консультанти”. Завдання (DataLemur, інтерв’ю Accenture): є дві таблиці – staffing(employee_id, engagement_id) (який співробітник який проект задіяний) і engagement(engagement_id, client_name). Потрібно вивести для кожного клієнта: загальна кількість консультантів, у нього зайнятих, і скільки з них працюють тільки на цього клієнта (ексклюзивно)[22][23]. Рішення передбачає два з’єднання та угруповання. Алгоритм: з’єднати таблиці по engagement_id щоб проставити кожному engagement його client_name, потім для кожного клієнта порахувати (a) число унікальних співробітників та (b) число унікальних співробітників, які мають лише один запис і він належить цьому клієнту.
Один із підходів – використовувати підзапит для (b): спочатку визначити всіх співробітників, зайнятих більш ніж на одному проекті, та виключити їх. Однак можна і одним запитом з агрегатами та умовою:
SELECT e.client_name, COUNT(DISTINCT s.employee_id) AS total_staffed, COUNT(DISTINCT CASE WHEN emp.engagement_count = 1 THEN s.employee_id END) AS exclusive_staffedFROM staffing AS sJOIN engagement AS. e.engagement_idJOIN ( SELECT employee_id, COUNT(DISTINCT engagement_id) AS engagement_count FROM staffing GROUP BY employee_id) AS emp ON s.employee_id = emp.employee_id
GROUP BY e.client_nameORDER BY e.client_name;
Тут через вкладений підзапит emp ми обчислюємо скільки проектів у кожного співробітника. При з’єднанні та фінальному угрупованню по клієнту використовуємо умовний COUNT з CASE: вважаємо співробітника в exclusive_staffed тільки якщо engagement_count = 1 (тобто він вважається рівно в одному проекті)[24][25]. Такий запит працює і в PostgreSQL, і MySQL. Примітка: в MySQL можна було замість COUNT(DISTINCT CASE…) використовувати SUM(CASE…THEN 1 ELSE 0 END), але з DISTINCT також підтримується.
Можливі помилки:забути DISTINCT – тоді якщо співробітник задіяний на проекті у клієнта двічі, його вважатимуть двічі. Або неправильно з’єднати таблиці: тут потрібно staffing→engagement по engagement_id, а також підключити підзапит по employee_id. Важливо зрозуміти, що послідовність JOIN-ів впливає на результат тільки через те, які рядки відсіються або дублюються, але логічно це один вираз. Наприклад, якби в підзапиті emp ми не використовували DISTINCT, то зайві дублі могли спотворити результат.
- Приклад 10 (LEFT JOIN vs INNER JOIN):“Користувачі без активних підписок”. Типове питання (варіації є на LeetCode): є таблиця Users та таблиця Subscriptions, потрібно вибрати всіх користувачів, які не мають активної підписки. Рішення: лівим з’єднанням отримати користувачів, які не мають рядків у підписках з умовою активності. Наприклад:
SELECT u.user_id, u.nameFROM Users AS uLEFT JOIN Subscriptions AS ON u.user_id = s.user_id AND s.status = 'active'WHERE s.user_id IS NULL;
Тут умова s.status = ‘active’ поставлена в секції ON так, що якщо для користувача немає активних підписок, у результуючому JOIN-рядку поля з s будуть NULL, і ми відфільтруємо таких WHERE s.user_id IS NULL. Це патерн anti-join (лівий джойн + перевірка на NULL). Помилка: іноді кандидати ставлять умову status=’active’ у WHERE – тоді навіть у LEFT JOIN це перетвориться фактично на INNER (бо WHERE відсіє все NULL). Правильне місце умови – у ON, якщо хочемо зберегти користувачів без активних передплат. Цей приклад ілюструє розуміння, як фільтри при з’єднанні впливають результат.
- Приклад 11 (CROSS JOIN підступ):Іноді дають каверзне питання розуміння сполук. Класичний приклад: «Є таблиця Product з 8 записами та таблиця Manufacturer з 4 записами. Що поверне запит SELECT * FROM Product, Manufacturer;?» Багато хто по неуважності не згадує, що без явної умови це зробить декартове твір 8×4 = 32 рядків[26][27]. Правильна відповідь: 32 рядки, тому що за відсутності JOIN … ON або WHERE SQL виконає CROSS JOIN усіх записів[27]. На співбесіді таке питання перевіряє, чи знає кандидат, що забутий ON чи умова з’єднання – поширена помилка, яка веде до множення рядків. Це теоретичне питання, але доречно згадати.
Об’єднання та СУБД:PostgreSQL та MySQL мають однаковий синтаксис JOIN. Варто пам’ятати, що MySQL historically допускає вказівку з’єднань через кому у FROM (як у прикладі вище), але це рекомендовано – краще писати явні JOIN. Також MySQL дозволяє використовувати USING(col) скорочення, а PostgreSQL – ні (тільки у стандартному SQL-стилі). Ці нюанси на інтерв’ю зазвичай не фокусуються, важливо коректно з’єднати та вибрати потрібні дані.
Робота з датами, часом та послідовностями подій
Завдання, пов’язані з часом – найчастіший гість на інтерв’ю середнього та просунутого рівня. Вас можуть запитати обчислити різницю дат, знайти записи в межах тимчасового вікна, визначити послідовність подій (наприклад, сесії початку/закінчення). Тут стануть у нагоді функції роботи з датами (DATEDIFF, DATE_TRUNC, INTERVAL тощо), а також віконні функції LAG/LEAD для аналізу послідовностей. Розглянемо пару прикладів із реальних завдань:
- Приклад 12 (інтервал між подіями):«Плоти, що повторюються». Питання (SQL-інтерв’ю в Stripe): дана таблиця транзакцій з полями transaction_id, merchant_id, credit_card_id, amount, transaction_timestamp. Потрібно знайти випадки повторної оплати – тобто коли в одному і тому ж магазині (merchant_id) з тією ж карткою (credit_card_id) і на ту ж суму відбувається два платежі протягом 10 хвилин, і вивести загальну кількість таких повторних платежів[28][29]. Звучить складно, але рішення зводиться до порівняння кожної транзакції з іншими за цими ключами та часом. Підходів два:
- Самоз’єднання (self-join):з’єднати таблицю транзакцій сама з собою за умовою збігу merchant_id, credit_card_id, amount і перевірити, що час t2.timestamp лежить у межах 10 хвилин після t1.timestamp. Тобто умова t2.transaction_timestamp BETWEEN t1.transaction_timestamp AND t1.transaction_timestamp + INTERVAL ’10 minute’ (у PostgreSQL) або t1.transaction_timestamp >= t2.transaction_timestamp AND TIMESTAMPDIFF(MINU t2.transaction_timestamp) <= 10 (у MySQL). Потрібно також виключити збіг одного й того самого запису (наприклад, t1.id <> t2.id). Потім згрупувати такі пари за ідентифікатором, порахувати кількість. Це громіздко, але працює.
- Віконна функція LAG:більш витончено можна відсортувати записи за часом у рамках тих самих merchant, card, amount і обчислити LAG(transaction_timestamp) OVER (PARTITION BY merchant_id, credit_card_id, amount ORDER BY transaction_timestamp) – час попередньої транзакції з тими. Потім SELECT порівняти поточну мітку часу з лагом: якщо різниця <= 600 секунд (10 хвилин), то вважати це повтором. І нарешті агрегувати сумарно кількість таких випадків.
У контексті інтерв’ю, якщо кандидат пропонує рішення через LAG, це демонструє володіння сучасними можливостями. Кути: обов’язково врахувати, що якщо дві транзакції відбулися в 8 хвилин різниці, а наступна ще через 8, то перша і третя вже не в 10-хвилинному вікні один з одним, але обидві є повтором з проміжною. Як вважати такі серії – зазвичай вважають кожну наступну оплату після першої як повторну, тому результат у прикладі Stripe був 1 (як кількість випадків повторної оплати)[29][30]. Реалізація може скластися з умов, але важливо проговорити логіку правильно.
PostgreSQL vs MySQL:у Postgres можна легко додавати інтервал (t1_time + INTERVAL ’10 minute’), а MySQL використовуються функції типу TIMESTAMPDIFF або оператор DATE_ADD(t1_time, INTERVAL 10 MINUTE). Віконна функція LAG доступна і там, і там (з MySQL 8+). Однак, MySQL не підтримує тип INTERVAL в LAG безпосередньо – потрібно віднімати значення або використовувати TIMEDIFF. Ці деталі не критичні, головне – загальне рішення.
- Приклад 13 (обчислення часу роботи):“Сумарний час роботи серверів (в днях)”. Питання з інтерв’ю Amazon: є лог включення/вимкнення серверів – таблиця server_log(server_id, status_time, session_status)[31][32]. Кожен запис – або start, або stop для певного сервера. Потрібно порахувати сумарний час (в днях), який весь парк серверів був включений, враховуючи, що серверів багато і кожен може вмикатися та вимикатися кілька разів[33][34]. По суті, потрібно підсумовувати періоди роботи кожного сервера та результат висловити у днях (ціло повних днів).
Рішення: спочатку розрахувати час роботи кожного окремого інтервалу start-stop кожного сервера, а потім підсумовувати. Найпростіше – скористатися віконною функцією LEAD (або LAG):
WITH server_sessions AS (SELECT server_id, status_time AS start_time, LEAD(status_time) OVER (PARTITION BY server_id ORDER BY status_time) AS end_time FROM server_log WHERE session_status = 'start' ORDER BY server_id, status FLOOR (SUM (EXTRACT (EPOCH FROM (end_time - start_time))) / 86400) AS total_uptime_daysFROM server_sessions;
Тут CTE server_sessions бере для кожного запису start наступний запис часу як end_time (який повинен бути відповідним stop). Передбачається, що дані коректні і кожен start матиме свій stop. Потім у зовнішньому запиті підсумовуємо різниці end_time – start_time по всіх серверах (у PostgreSQL EXTRACT (EPOCH FROM interval) видає кількість секунд) і ділимо на 86400 (число секунд на дні), беремо FLOOR для повного числа днів. Отримуємо шуканий сумарний аптайм днями.
У MySQL еквівалент: можна використовувати TIMESTAMPDIFF(SECOND, start_time, end_time) і потім ділити на 86400. Віконна функція LEAD підтримується, тому запит майже не змінюється, за винятком синтаксису приведення типів.
Перевірка на прикладі:Якщо сервер 1 працював з 2 серпня по 4 серпня (2 дні), а сервер 2 – з 17 по 24 серпня (7 днів), сумарно отримаємо 9. (У прикладі DataLemur було отримано 21, що викликає питання – можливо, там були інші дані або враховувалися часткові дні округленням вгору. На інтерв’ю стоїть уточніть.
Примітка:Такі завдання перевіряють навички роботи з датами. Інтерв’юер може поцікавитися, що якщо проміжки накладаються (наприклад, якщо кілька серверів працюють одночасно – чи треба їх часи підсумовувати просто арифметично чи рахувати загальне покриття за часом?). У нашому формулюванні явно сказано підсумовувати uptime кожного сервера, тому накладки не зменшують лічильник – просто складаємо час всіх серверів[35]. Цей момент слід зрозуміти з вимоги.
Питання на дати можуть також включати: «Знайдіть відсоток збільшення чого-небудь по місяцях», «Порівняйте показники за сусідні дні» (тоді застосовується LAG/LEAD для попереднього дня), «Знайдіть максимальну послідовність щоденних дій користувача» (потрібні дати і різниця в один день – тут корисний трюк DATEDIFF змінних/вікон). При відповіді важливо показати впевненість у функціях конкретної СУБД до роботи з часом. Наприклад, згадати, що MySQL є DATEDIFF(date1, date2) (різниця днями), а Postgres можна просто віднімати дати чи використовувати AGE().
Аналітичні та сценарні завдання (Open-Ended)
Окрема категорія – відкриті питання, де немає чітко заданої метрики, яку необхідно обчислити. Кандидату дають деяку бізнес-ситуацію та очікують, що він сам придумає, які показники вирахувати та як, щоб відповісти на запитання. Часто це трапляється для досвідчених ролей (Senior Data Analyst, Data Scientist). Тут важливі як SQL-навыки, а й розуміння даних. Тим не менш, написати SQL все одно доведеться.
Відмінність від звичайних завдань:вам не говорять прямо “Порахуй X”, а формулюють проблему. Наприклад: «Впали доходи, як дізнатися, який відсоток виручки втрачено через незавершені замовлення?» або «Як виміряти ефективність кампанії запуску нового продукту?». Потрібно вигадати метрику, а потім висловити її на SQL.
- Приклад 14 (open-ended, визначення метрик):“Відсоток втраченої виручки”. Питання (реальне інтерв’ю Uber): дана таблиця замовлень uber_orders з колонками: service_name (тип сервісу), number_of_orders, monetary_value, status_of_order (завершено/не завершено/і т.п.). Потрібно для кожного сервісу порахувати: (a) відсоток незавершених замовлень та (b) відсоток втраченої виручки через незавершені замовлення[36][37]. Виходить, спершу треба зрозуміти, як визначити “незавершений” та “втрачений виторг”. Рішення (запропоноване на StrataScratch) використовувало відразу кілька сучасних фішок SQL – фільтрацію в агрегатах (Postgres) або CASE – та оформлення через підзапит:
SELECT service_name, (lost_orders_number * 100.0 / total_orders) AS orders_loss_percent, (lost_profit * 100.0 / possible_profit) AS profit_loss_percentFROM ( SELECT service_name, SUM(number_of_orders) AS total (WHERE status_of_order != 'Completed') AS lost_orders_number, SUM(monetary_value) AS possible_profit, SUM(monetary_value) FILTER (WHERE status_of_order != 'Completed') AS lost_profit FROM;
У внутрішньому запиті ми групуємо за service_name і обчислюємо: загальну кількість замовлень, кількість незавершених (все, де status_of_order != ‘Completed’), повна можлива виручка (сума всіх monetary_value) та втрачена виручка (сума за незавершеними)[38][39]. У PostgreSQL при цьому елегантно використовуються SUM(…) FILTER (WHERE …). У MySQL еквівалент буде SUM(CASE WHEN status_of_order != ‘Completed’ THEN monetary_value ELSE 0 END) та аналогічно для підрахунку замовлень. У зовнішньому запиті просто рахуємо відсотки.
Обговорення метрик:Відсоток незавершених = (не завершені / все) * 100. Відсоток втрати виручки = (втрачений виторг / весь можливий виторг)*100. У рішенні зазначено, що треба явно обумовити, що вважаємо замовлення незавершеним, якщо статус не ‘Completed’ (може бути ‘Cancelled’, ‘Other’ і т.п.)[40]. Такі нюанси – саме те, що перевіряється у open-ended питаннях. Співрозмовник хоче побачити, що ви мислите критично: “Статус ‘Other’ – чи вважати його завершеним? Може, ні? Треба уточнити.” У нашому випадку розробник рішення припустив, що всі, відмінні від ‘Completed’ – це провалені замовлення[40], але визнає, що це припущення, і на інтерв’ю кандидат має це проговорити.
Можливі помилки:Порахувати відсоток немає від загального, а, скажімо, від завершених – неуважність до визначення метрики. Або забути помножити на 100 (просять же відсоток). Ще – неправильно округлити, але зазвичай формат виводу не критичний, головне значення.
Open-ended питання можуть включати багато аспектів. Їхнє рішення часто передбачає багатокроковий SQL або навіть кілька запитів. Тому інтерв’юер може дозволити вам не писати відразу весь код, а спершу описати підхід: які таблиці з’єднати, які поля агрегувати, які фільтри застосувати. Відмінна відповідь включає обговорення різних інтерпретацій та чітке обґрунтування обраної.
Якщо раптом трапляється open-ended питання рівня «Як ви визначили метрику успіху X і отримали її з бази?», структура відповіді має бути така: 1. Визначаєте поняття успіху/метрики своїми словами. 2. Пропонуєте набір даних/полів, необхідних розрахунку. 3. Описуєте, які SQL-конструкції застосувати (JOIN, агрегати, вікна та ін.). 4. За потреби наводьте спрощений синтаксис запиту або фрагменти.
Іноді замість конкретного SQL-коду інтерв’юеру достатньо вашої здатності логічно розбити завдання та знання функцій/синтаксису для реалізації.
Оптимізація запитів та просунуті концепції
На вищих позиціях (і все частіше навіть на середніх) роботодавці хочуть впевненості, що кандидат не тільки напише працюючий SQL, а й оптимальний SQL. Питання оптимізації можуть бути теоретичними: «Як працює індекс?», «Коли краще використовувати денормалізацію?», «Чому цей запит виконується повільно і як прискорити?» – або практичними: вам можуть показати запит та запитати, як його покращити.
Індекси та ефективність.Варто повторити основи: індекс – структура даних, що прискорює пошук по певному стовпцю (аналогічно покажчику або зміст книги). Користь: швидше вибірка по полю, що індексується, особливо на великих таблицях[41]. Ціна: повільніше операції вставки/оновлення та додаткове місце на диску[42]. Інтерв’юер може запитати, які типи індексів ви знаєте (B-Tree – найпоширеніший, хеш-індекси, GiST/GIN для Postgres тощо). Але частіше досить загальних фраз: «Створюємо індекс, якщо часто шукаємо полем, і він значно прискорює SELECT, але треба бути обережним – багато індексів уповільнюють INSERT/UPDATE»[41].
Приклад питання:“Поясніть, як працює індекс і які плюси/мінуси дає” – можна відповісти приблизно так: “Індекс – це додаткова структура (наприклад, збалансоване дерево), що зберігає значення з вибраних стовпців і вказівники на відповідні рядки. При запиті за умовою на ці стовпці СУБД може шукати в індексі[41]. Мінуси: при оновленні даних індекс також треба оновлювати, що уповільнює запис; плюс індекс посідає місце. У деяких випадках (маленька таблиця, або потрібно прочитати більшу частину рядків), індекс не дає виграшу.”Цього рівня відповіді достатньо. Можна додати, що в MySQL двигун InnoDB використовує B-Tree індекси для більшості випадків, і що кластерний індекс за первинним ключем, але це деталі для тих, хто особливо цікавиться.
Денормалізація та нормалізація.Такі концептуальні питання перевіряють розуміння дизайну БД. Нормалізація – приведення структури даних до усунення надмірності (1НФ, 2НФ, 3НФ і т.д.), а денормалізація – свідомий відступ від нормалізації для підвищення швидкості читання (зберігаємо дублі або передраховані агрегати, щоб не робити складні JOINи на льоту)[43][44]. Google, наприклад, може запитати: «Що таке денормалізація і коли ви її використовували?». Відповідь: “Денормалізація – це додавання надлишкових даних у схему, відхилення від правил нормальних форм. Робиться для прискорення читання – наприклад, зберігаємо сумарні щомісячні продажі прямо в таблиці, замість обчислення через JOINи щоразу. Застосовно в системах аналітичної звітності, сховища даних виправдана. Мінус – ризик неузгодженості даних та складність оновлень.”[45].
Зберігаються процедури vs функції (UDF).Таке питання теж прозвучало в DataLemur: «У чому різниця між stored procedure та user-defined function?». Коротко: процедура, що зберігається – виконує набір SQL-операцій, може повертати набори результатів, підтримує транзакції та зміни даних, але не може викликатися в SELECT як вираз. UDF (скалярна або таблична функція) – повертає значення (або таблицю), може використовуватися в запитах як частина виразу, але зазвичай не повинна змінювати дані. Процедури використовуються для операцій, дій, UDF для обчислень. Крім того, деякі СУБД (наприклад, SQL Server) мають відмінності в плані, що UDF можна в SELECT, а процедури немає. Якщо не впевнені, можна відповісти на рівні: “Обидва механізми дозволяють зберегти на боці БД шматок логіки. Процедури часто для цілих бізнес-операцій (можуть змінювати дані), а функції – для обчислення і можуть використовуватися як частина запиту. Вибір залежить від завдання: складний процес транзакції – процедура, просте обчислення значення – функцій”.[46].
DELETE vs TRUNCATE.Здавалося б, базове питання, але на інтерв’ю люблять його ставити, щоб побачити, чи знає кандидат про деталі роботи БД. DELETE – мовний оператор DML, що видаляє рядки, можна за умови WHERE; кожне видалення пишеться в журнал транзакцій (лог), тригери ON DELETE спрацьовують[47]. TRUNCATE – команда DDL, що миттєво очищає всю таблицю, зазвичай шляхом скидання даних сегмента; не можна вибрати окремі рядки – завжди все; журналує лише факт видалення сторінки/таблиці, а не кожен рядок, тому працює набагато швидше на більших обсягах[47][48]. Але TRUNCATE має обмеження: не можна, якщо є зовнішній ключ на таблицю; не викликає звичайних тригерів (у деяких СУБД), і відкотити його складніше (у транзакції не завжди можна, наприклад, MySQL TRUNCATE не транзакціонний).
На інтерв’ю досить сказати: “DELETE – більш гнучкий, можна видаляти вибірково, але повільніше для великих таблиць; TRUNCATE – швидке скидання всіх даних, але без умов, фактично перетворює порожню таблицю.”[47]. Цим ви покажете увагу до деталей.
Інші просунуті теми:Можуть запитати про VIEW (уявлення), тригери, транзакції (властивості ACID). Наприклад, Що таке ACID? – просте питання, відповідь: Atomicity, Consistency, Isolation, Durability – 4 властивості транзакцій[49]. Atomicity – або всі зміни всередині транзакції застосовуються, або жодного (усі чи нічого)[50]. Consistency – транзакція переводить базу з одного узгодженого стану до іншого (цілісність даних не порушується правилами)[51]. Isolation – паралельні транзакції не заважають один одному (ефект ніби виконувались послідовно, за певного рівня ізоляції)[51]. Durability – після підтвердження транзакції (COMMIT) зміни зберігаються назавжди, навіть якщо збій системи станеться[52]. Це питання теоретичне, SQL-код не виявляється, але важливо знати.
Підсумовуючи: секція оптимізації – ваш шанс блиснути широтою знань. Бажано згадати особистий досвід оптимізації (якщо є), наприклад: “Зіткнувся з довгими запитами, використовували EXPLAIN щоб зрозуміти план, додавали потрібні індекси, переписували підзапит на JOIN і досягли прискорення.” – Така розповідь справляє гарне враження. Але не вигадуйте, якщо не питають – відповідайте насправді заданого питання.
Сучасні бази даних та векторні БД
Сфера даних постійно розвивається, і в останні роки з’явилися нові типи СУБД та розширення класичних БД. На співбесідах (особливо у компаніях, пов’язаних з ML/AI) можуть запитати про векторні бази даних або інші сучасні сховища. Векторні БД (наприклад, Pinecone, Weaviate, Milvus, Qdrant, Vespa та ін.) призначені для зберігання та пошуку за векторними уявленнями даних – зазвичай ембеддинги з нейромереж. Наприклад, ми можемо зберігати ембединг зображення чи тексту як масив чисел розмірністю 100–1000, і хотіти швидко знаходити “найближчі” вектори (тобто схожі об’єкти).
Що можуть запитати:– Основи: “Що таке векторна база і навіщо вона потрібна?”. Відповідь: це база, оптимізована під пошук найближчих сусідів у зарозумілому просторі. Вона дозволяє по вхідному вектору швидко знайти до найбільш схожих з мільйонів (наприклад, пошук схожих продуктів по ембеддингу опису). Класичні СУБД не справляються з таким пошуком ефективно, тож з’явилися спеціалізовані. – Принцип роботи: можуть поцікавитись методами, як досягається швидкість. Ключові слова: використання Approximate Nearest Neighbor (ANN) алгоритмів замість точного пошуку, щоб прискорити рахунок наближення[53]. Також спеціальні індекси: графові (HNSW), дерева та інвертовані файли (IVF), локально чутливі хешування (LSH) тощо.[54]. Вони зменшують обсяг порівнянь – не доводиться порівнювати з кожним вектором у базі, що дозволило б шукати по мільярдним наборам швидко[54][55]. – Метрики подібності: часто згадують cosine similarity (косинусна подібність) та евклідова відстань. Cosine similarity популярна для текстових ембеддингів, тому що вона дивиться на кут між векторами (суть – наскільки вони схожі, ігноруючи масштаб).[56]. Евклідова відстань – пряма відстань у просторі, що теж використовується. Можуть запитати: “Які метрики підтримуються?” – можна перерахувати: cosine, euclidean, іноді dot-product (скалярний твір) – залежно від движка. – Приклад завдання: «Ось у нас є таблиця з ембеддингами товарів, як знайти 5 найбільш схожих на заданий товар?». Від вас очікують хоча б міркування: “Я б використав vector similarity search: обчислюємо ембеддинг заданого товару, потім шукаємо 5 найближчих на відстані у просторі ембеддингів.” Якщо запитують, як це виглядає в SQL-подібному синтаксисі, можна навести приклад із pgvector (розширення PostgreSQL для векторів). pgvector визначено оператор <-> (або <=>) для обчислення відстані між векторами. Наприклад, запит на топ-3 схожих документів може виглядати так:
SELECT content FROM embedding ORDER BY embedding <-> '[0.12, 0.25, ..., -0.07]'::vector LIMIT 3;
Тут embedding – стовпчик типу VECTOR, а ‘<…>’::vector – літерал шуканого вектора. Оператор <-> обчислює відстань (за замовчуванням, евклідову або косинусну, залежно від налаштувань) та ORDER BY сортує від найближчого (найменшу відстань) до далі. Примітка: у прикладі pgvector використовується оператор <=> для косинусної міри[57][58]але суть аналогічна. Такий синтаксис – SQL-подібний запит до векторної бази усередині PostgreSQL.
Спеціалізовані векторні бази даних (Pinecone, Weaviate) частіше не використовують SQL. Наприклад, Pinecone – це хмарний сервіс із власним API: запит надсилається через REST або gRPC, де ви передаєте вектор та отримуєте список найближчих. Weaviate надає GraphQL-інтерфейс для семантичного пошуку. Однак, тренд йде до уніфікації: є проекти, що дозволяють писати на SQL поверх векторного пошуку (наприклад, Singlestore додають функції для цього, або спільне використання PostgreSQL + pgvector, як показано вище).
- Що ще можуть згадати:гібридний пошук – коли поєднується умова по вектору та звичайне фільтр-поле. Наприклад: Знайдіть схожі документи, але тільки в категорії ‘спорт’. У SQL з pgvector це було б поєднання WHERE category = ‘sport’ та ORDER BY embedding <-> :vec. У деяких двигунах (Weaviate) є прямий параметр фільтра у запиті. Шардинг векторів – як масштабується (через розбиття за ID чи clusters). Це вже глибокі деталі, малоймовірно для інтерв’ю, але якщо запитали – можна згадати, що при розподілі потрібно підтримувати баланс, а пошук може вимагати звернутися до багатьох вузлів, тому часто використовують наближений пошук плюс розподіл.
- Питання на досвід:«Чи використовували ви векторні бази? Навіщо?» – тут добре б відповісти, якщо знаєте, реальні приклади: пошук схожих зображень, семантичний пошук FAQ (коли у користувача питання, а ви векторизуєте питання і шукаєте векторно схожі відповіді), рекомендаційні системи (за схожістю переваг, теж через ембеддінги). Навіть якщо не використовували, можна сказати: “Я знайомий з концепцією, читав про них. Типове застосування – функції пошуку в додатках з AI, наприклад, для чату з контекстом (Retrieval-Augmented Generation), коли зберігають ембеддинги документів і з питання користувача шукають релевантні документи, щоб підтягнути інформацію.”[59][60].
- Про інтеграцію:можливо, запитають, як інтегрувати векторне сховище у існуючу архітектуру. Відповідь: або використовувати гібридну СУБД (наприклад, PostgreSQL з розширенням – тоді можна в одному запиті поєднувати звичайні дані та векторний пошук), або використовувати окремий сервіс (наприклад, Pinecone) та в додатку спочатку шукати там top-K ідентифікаторів, потім йти в основну БД за повними даними. Тут немає правильної/неправильної відповіді, важливим є розуміння, що векторний пошук – додатковий крок.
Порада:якщо вакансія передбачає роботи з ML, то ймовірність глибоких питань про вектори невисока. Але базово знати, що це – корисно. Також можуть запитати про бази NoSQL, NewSQL, Hadoop і т.п., але це виходить за рамки суто SQL-інтерв’ю. У контексті нашого питання обмежимося векторними, як найболючіших зараз.
Приклад питання на SQL-подібний синтаксис для векторів:Інтерв’юер: «Припустимо, у нас PostgreSQL із pgvector. Покажіть запит, який знаходить найближчих сусідів даного вектора у таблиці.» – Ви можете написати як вище і пояснити, що створивши індекс HNSW по колонці embedding, ми забезпечимо швидкий ANN-пошук[61][62]. Варто згадати, що для точного пошуку можна без індексу, але це повільно; зазвичай використовують ANN і отримують майже точні результати значно швидше[53].
Векторні бази даних – це для similarity search. Тому ключові слова: найближчий сусід, cosine similarity, висока розмірність, approximate search.
Підготувавшись за цими пунктами, ви успішно відповісте на сучасні питання щодо БД, покажете свою актуальність у 2025 році та справите враження на інтерв’юерів!
Висновок.Цей гайд охопив широкий спектр тем: від угруповань та віконних функцій до індексів та vector search. Звичайно, список питань не вичерпний, але принципи розбору застосовуються до багатьох інших завдань. Практикуйтеся на згаданих платформах (DataLemur, LeetCode, StrataScratch) – там зібрані сотні реальних питань від великих компаній. Уважно читайте умови, розбивайте проблему на кроки, спочатку переконуйтесь у коректності підходу, а потім пишіть фінальний SQL. І не забувайте пояснювати ваші рішення “словами” – на співбесіді важливо показати хід думок. Успіхів у підкоренні SQL-інтерв’ю!
Джерело використаних прикладів і матеріалів: реальні інтерв’ю-питання з DataLemur[28][9], StrataScratch[6][8], статті та обговорення на Medium та профільних ресурсах[54][63].
[1] [2] [3] [4] [5] [6] [7] [8] [12] [13] [14] [15] [16] [17] [18] [19] [20] [26] [27] [36] [37] [38] [39] [40] [47] [48] SQL Interview Questions You Must Prepare: The Ultimate Guide – StrataScratch
[9] [10] [21] [22] [23] [24] [25] [28] [29] [30] [31] [32] [33] [34] [35] [41] [42] [46] [49] [50] [51] [52] 20 Advanced SQL Interview Questions (With Answers!)
[11] What I’ve Learned Solving All 44 SQL Leetcode Hard Questions in 7 Days | by chinhau | Medium
[43] [44] [45] 14 Google SQL Interview Questions (Updated 2025)
[53] [54] [55] [56] [63] Top 25 Vector Database Interview Questions and Answers | by Sanjay Kumar PhD | Medium
[57] [58] [59] [60] [61] [62] PostgreSQL as a Vector Database: A Pgvector Tutorial | TigerData
Source: https://uproger.com/polnyj-gajd-po-realnym-sql-voprosam-s-sobesedovanij/