Приєднатись до підзапиту


836

Я старий шкільний користувач MySQL і завжди віддав перевагу JOINнад підзапитом. Але сьогодні всі використовують підзапит, і я ненавиджу його; Я не знаю чому.

Мені не вистачає теоретичних знань, щоб судити про себе, чи є різниця. Чи підрядний запит настільки ж хороший, як JOINа, отже, нема про що турбуватися?


23
Підзапити іноді чудові. Вони забирають продуктивність в MySQL. Не використовуйте їх.
runrig

8
Я завжди мав враження, що підзапити запити неявно виконуються як об'єднання, де це можливо в певних технологіях БД.
Kezzer

18
Підзапроси не завжди спрацьовують, при з'єднанні з досить великими таблицями кращим способом є здійснення підбору з цієї великої таблиці (обмеження кількості рядків), а потім приєднання.
ovais.tariq

136
"нині всі використовують
підзапит

3
Потенційно пов'язані (хоча і набагато більш конкретні): stackoverflow.com/questions/141278/subqueries-vs-joins / ...
Leigh Brenecki

Відповіді:


190

Взяте з посібника MySQL ( 13.2.10.11 Переписування підзапитів як приєднання ):

ПРИЄДНАЙТЕСЬ ЛІВО [ВАРТУ] може бути швидшим, ніж еквівалентний підзапит, тому що сервер міг би краще оптимізувати його - факт, який не характерний лише для MySQL Server.

Так що підзапити можуть бути повільнішими LEFT [OUTER] JOIN, але, на мою думку, їхня сила трохи вище читабельності.


45
@ user1735921 IMO, це залежить ... Взагалі, дуже важлива читабельність коду, оскільки вона має велике значення для подальшого управління ним ... Згадаймо відоме твердження Дональда Кнута: "Передчасна оптимізація - корінь всього зло (або принаймні більшість його) у програмуванні " . Однак, природно, є області програмування, де продуктивність є найважливішою ... В ідеалі, коли вдасться узгодити одне з іншим :)
simhumileco

30
У складніших запитах я вважаю, що приєднання набагато простіше читати, ніж підзапити. підзапроси перетворюються на миску локшини в моїй голові.
Захра

6
@ user1735921, звичайно, коли запит стає настільки складним, що він робить не так, і ви витрачаєте день на його виправлення ... між ними, як зазвичай, баланс.
fabio.sussetto

6
@ user1735921 Тільки якщо підвищення продуктивності варто збільшити час обслуговування, необхідний у майбутньому
Джошуа Шліхтінг

3
Моя думка Joinі sub queryмає різний синтаксис, тому читабельність ми не можемо порівнювати, обидва мають більш високу читабельність, доки ви хороші в синтаксисі SQL. Продуктивність важливіша.
Thavaprakash Swaminathan

840

Підзапити - це логічно правильний спосіб вирішення задач форми "Отримати факти з А, що обумовлені фактами з В". У таких випадках є більш логічним сенс вставити B у підзапит, ніж робити з'єднання. Це також безпечніше, в практичному розумінні, оскільки вам не потрібно бути обережними щодо отримання дублюваних фактів з "A" через кілька матчів проти B.

Однак на практиці відповідь, як правило, зводиться до ефективності. Деякі оптимізатори висмоктують лимони, коли отримують з'єднання з під-запитом, а деякі висмоктують лимони в інший спосіб, і це оптимізатор, специфічна для СУБД і запит.

Історично явні приєднання зазвичай виграють, отже, усталене уявлення про те, що приєднання є кращим, але оптимізатори стають все кращими, і тому я вважаю за краще писати запити спочатку логічно узгодженим способом, а потім реструктурувати, якщо це обмежують ефективність.


105
Чудова відповідь. Я також додам, що розробники (особливо любителі) не завжди володіють SQL.
Альваро Гонсалес

4
+1 Довго
шукаючи

1
@Marcelo Cantos. Чи можете ви, будь ласка, надати приклад своєї заяви "Це також безпечніше в практичному розумінні, оскільки вам не потрібно бути обережними щодо отримання дублюваних фактів з" A "через кілька матчів проти B."? Я вважав це дуже проникливим, але трохи занадто абстрактним. Дякую.
Jinghui Niu

6
@JinghuiNiu Клієнти , які купили дорогі речі: select custid from cust join bought using (custid) where price > 500. Якщо клієнт купив кілька дорогих предметів, ви отримаєте подвійний збір. Щоб це виправити, select custid from cust where exists (select * from bought where custid = cust.custid and price > 500). Ви можете використовувати select distinct …замість цього, але це часто більше роботи, як для оптимізатора, так і для оцінювача.
Марсело Кантос

1
@MatTheWhale так, я використовував спрощену відповідь, тому що я був ледачий. У реальному сценарії ви б тягнули більше стовпчиків, аніж просто зрізані з cust.
Марсело Кантос

357

У більшості випадків JOINs швидше, ніж підзапити, і дуже рідко такий запит може бути швидшим.

У JOINs RDBMS можна створити план виконання, який краще підходить для вашого запиту і може передбачити, які дані слід завантажувати для обробки та економити час, на відміну від підзапиту, де він буде запускати всі запити та завантажувати всі їх дані для обробки .

Хороша річ у підзапитах - це те, що вони читаються, ніж JOINs: саме тому більшість нових SQL людей віддають перевагу їм; це простий шлях; але коли мова йде про продуктивність, JOINS краще в більшості випадків, навіть якщо їх не важко читати.


14
Так, більшість баз даних, таким чином, включає його як крок оптимізації для перетворення підзапитів у об'єднання під час аналізу вашого запиту.
Cine

16
Ця відповідь трохи спрощена для запитання, яке було задано. Як ви заявляєте: певні підзапити є нормальними, а деякі - ні. Відповідь не дуже допомагає розрізнити два. (також "дуже рідкісний" насправді залежить від ваших даних / програми).
Нерозумно

21
чи можете ви довести будь-який із ваших пунктів за допомогою посилання на документацію чи результати випробувань?
Uğur Gümüşhan

62
Я створив дуже хороший досвід із підзапитами, які містять зворотну посилання на верхній запит, особливо якщо мова йде про кількість рядків вище 100 000. Здається, це використання пам'яті та підключення до файлу swap-файлу. Об'єднання створить дуже великий об'єм даних, який може не вписатись у пам'ять і повинен бути перетворений на сторінку в файл swap. Щоразу це так, час запитів малих підселекторів типу select * from a where a.x = (select b.x form b where b.id = a.id)надзвичайно малий порівняно з об'єднанням. Це дуже специфічна проблема, але в деяких випадках вона приносить вам години і хвилини.
zuloo

13
У мене є досвід роботи з Oracle, і я можу сказати, що під-запити набагато краще на великих таблицях, якщо у вас немає фільтрування чи сортування.
Амір Пашазаде

130

Використовуйте EXPLAIN, щоб побачити, як ваша база даних виконує запит на ваші дані. У цій відповіді величезне "це залежить" ...

PostgreSQL може переписати підзапит на об'єднання або приєднання до підзапросу, коли він вважає, що один швидше, ніж інший. Все залежить від даних, індексів, кореляції, кількості даних, запиту тощо.


6
саме тому postgresql настільки хороший і корисний, що він розуміє, що є ціллю, і виправить запит, виходячи з того, що на його думку є кращим, і postgresql дуже добре знає, як дивитись на його дані
WojonsTech

heww. Я думаю, не потрібно переписувати для мене тонни запитів! postgresql на виграш.
Даніель Шін

77

У 2010 році я приєднався б до автора цього питання і я б голосно проголосував JOIN, але, маючи набагато більше досвіду (особливо в MySQL), я можу констатувати: Так, підзапити можуть бути кращими. Я прочитав тут кілька відповідей; деякі заявлені підзапити швидше, але йому не вистачало хорошого пояснення. Я сподіваюся, що зможу надати один із цією (дуже) пізньою відповіддю:

Перш за все, дозвольте сказати найважливіше: Існують різні форми підзапитів

І друге важливе твердження: розмір має значення

Якщо ви використовуєте підзапити, вам слід знати, як DB-сервер виконує підзапит. Особливо, якщо під-запит оцінюється один раз або для кожного рядка! З іншого боку, сучасний DB-сервер здатний багато оптимізувати. У деяких випадках підзапит допомагає оптимізувати запит, але новіша версія DB-сервера може зробити оптимізацію застарілою.

Підзапити в полях Select-Fields

SELECT moo, (SELECT roger FROM wilco WHERE moo = me) AS bar FROM foo

Майте на увазі, що підзапрос виконується для кожного результуючого рядка foo.
Уникайте цього, якщо можливо; це може різко уповільнити ваш запит на величезних наборах даних. Однак, якщо підзапит не має посилання на fooнього, він може бути оптимізований DB-сервером як статичний вміст і може бути оцінений лише один раз.

Підзапроси в операторі Where

SELECT moo FROM foo WHERE bar = (SELECT roger FROM wilco WHERE moo = me)

Якщо вам пощастило, БД оптимізує це внутрішньо у формат a JOIN. Якщо ні, то ваш запит стане дуже, дуже повільним у величезних наборах даних, оскільки він буде виконувати підзапит для кожної рядки в foo, а не лише результати, як у виділеному типі.

Підзапити в заяві Join

SELECT moo, bar 
  FROM foo 
    LEFT JOIN (
      SELECT MIN(bar), me FROM wilco GROUP BY me
    ) ON moo = me

Це цікаво. Ми поєднуємо JOINз підзапитом. І тут ми отримуємо реальну силу підзапитів. Уявіть набір даних з мільйонами рядків, wilcoале лише декількома різними me. Замість того, щоб приєднатися до величезної таблиці, у нас тепер є менша тимчасова таблиця, до якої можна приєднатися. Це може призвести до набагато швидших запитів залежно від розміру бази даних. Ви можете мати такий самий ефект із CREATE TEMPORARY TABLE ...і INSERT INTO ... SELECT ..., що може забезпечити кращу читабельність для дуже складних запитів (але можна заблокувати набори даних на рівні ізоляції, що повторюється).

Вкладені підзапити

SELECT moo, bar
  FROM (
    SELECT moo, CONCAT(roger, wilco) AS bar
      FROM foo
      GROUP BY moo
      HAVING bar LIKE 'SpaceQ%'
  ) AS temp_foo
  ORDER BY bar

Ви можете вкладати підзапити на кілька рівнів. Це може допомогти у величезних наборах даних, якщо вам доведеться групувати або сортувати результати. Зазвичай DB-сервер створює для цього тимчасову таблицю, але іноді вам не потрібно сортування по всій таблиці, лише на наборі результатів. Це може забезпечити набагато кращі показники роботи залежно від розміру столу.

Висновок

Підзапити не замінюють a, JOINі ви не повинні використовувати їх так (хоча це можливо). На мою скромну думку, правильне використання підзапиту - це використання як швидка заміна CREATE TEMPORARY TABLE .... Хороший підзапит запиту зменшує набір даних таким чином, який ви не можете виконати в ONоператорі a JOIN. Якщо підзапит має одне з ключових слів GROUP BYабо DISTINCT, переважно, він не розташований у полях вибору або операторі where, то це може значно підвищити ефективність.


3
Для Sub-queries in the Join-statement: (1) створення похідної таблиці з самого підзапиту може зайняти дуже багато часу. (2) отримана отримана таблиця не індексується. ці два тільки можуть значно уповільнити SQL.
jxc

@jxc Я можу говорити лише за MySQL (1) Там є тимчасова таблиця, схожа на приєднання. Час залежить від кількості даних. Якщо ви не можете зменшити дані за допомогою запиту, використовуйте з'єднання. (2) Це правильно, це залежить від фактору, який ви можете зменшити в тимчасовій таблиці. У мене були випадки реального світу, коли я міг зменшити розмір приєднання з декількох мільйонів до кількох сотень і скоротити час запиту з декількох секунд (з повним використанням індексу) до чверті секунди за допомогою запиту.
Trendfischer

IMO: (1) така тимчасова таблиця (похідна таблиця) не матеріалізується, тому кожен раз, коли ви запускаєте SQL, тимчасова таблиця повинна бути відтворена, що може бути дуже затратним і справжньою шийкою (тобто за допомогою групи на мільйони записів) (2), навіть якщо ви можете зменшити розмір темп-таблиці до 10записів, оскільки немає індексу, це все ще означає потенційно запитувати в 9 разів більше записів даних, ніж без таблиці темп, приєднавшись до інших таблиць. BTW У мене виникла ця проблема раніше з моїм db (MySQL), в моєму випадку використання підзапиту в SELECT listмогло б бути набагато швидшим.
jxc

@jxc Я не сумніваюся, що є маса прикладів, коли використання підзапиту менш оптимальне. Як хорошу практику слід використовувати EXPLAINзапит перед оптимізацією. Зі старими set profiling=1ви могли легко побачити, якщо тимчасовий стіл - це вузьке місце. І навіть індексу потрібен час на обробку, B-Trees оптимізують запити для записів, але 10 записова таблиця може бути набагато швидшою, ніж індекс для мільйонів записів. Але це залежить від кількох факторів, таких як розміри та типи полів.
Trendfischer

1
Мені дуже сподобалося ваше пояснення. Дякую.
unpairestgood

43

Перш за все, для порівняння двох перших слід розрізнити запити з підзапитами на:

  1. клас підзапитів, у яких завжди є відповідні еквівалентні запити, написані з приєднанням
  2. клас підзапитів, який неможливо переписати за допомогою приєднання

Для першого класу запитів хороша RDBMS побачить приєднання та підзапити як еквівалентні та створить ті самі плани запитів.

У наші дні навіть mysql це робить.

І все-таки іноді це не відбувається, але це не означає, що приєднання завжди виграють - у мене були випадки, коли підзапроси в mysql покращували продуктивність. (Наприклад, якщо що-небудь заважає планувальнику mysql правильно оцінити вартість, і якщо планувальник не бачить варіант з'єднання і варіант запиту як той самий, підзапити можуть перевершити об'єднання шляхом вимушування певного шляху).

Висновок полягає в тому, що ви повинні перевірити свої запити як для варіантів приєднання, так і для підзапиту, якщо ви хочете бути впевнені, який з них буде краще.

Для другого класу порівняння не має сенсу, оскільки ці запити неможливо переписати, використовуючи приєднання, і в цих випадках підзапити - це природний спосіб виконувати необхідні завдання, і ви не повинні їх дискримінувати.


1
чи можете ви навести приклад запиту, написаного за допомогою підзапитів, який не можна перетворити на приєднання (другий клас, як ви це називаєте)?
Захра

24

Я думаю, що у цитованих відповідях було недостатньо підкреслено питання про дублікати та проблемні результати, які можуть виникнути внаслідок конкретних випадків (використання).

(хоча Марсело Кантос це згадує)

Наведу приклад із курсів Lagunita Стенфорда по SQL.

Студентський стіл

+------+--------+------+--------+
| sID  | sName  | GPA  | sizeHS |
+------+--------+------+--------+
|  123 | Amy    |  3.9 |   1000 |
|  234 | Bob    |  3.6 |   1500 |
|  345 | Craig  |  3.5 |    500 |
|  456 | Doris  |  3.9 |   1000 |
|  567 | Edward |  2.9 |   2000 |
|  678 | Fay    |  3.8 |    200 |
|  789 | Gary   |  3.4 |    800 |
|  987 | Helen  |  3.7 |    800 |
|  876 | Irene  |  3.9 |    400 |
|  765 | Jay    |  2.9 |   1500 |
|  654 | Amy    |  3.9 |   1000 |
|  543 | Craig  |  3.4 |   2000 |
+------+--------+------+--------+

Застосувати таблицю

(заявки до конкретних університетів та спеціальностей)

+------+----------+----------------+----------+
| sID  | cName    | major          | decision |
+------+----------+----------------+----------+
|  123 | Stanford | CS             | Y        |
|  123 | Stanford | EE             | N        |
|  123 | Berkeley | CS             | Y        |
|  123 | Cornell  | EE             | Y        |
|  234 | Berkeley | biology        | N        |
|  345 | MIT      | bioengineering | Y        |
|  345 | Cornell  | bioengineering | N        |
|  345 | Cornell  | CS             | Y        |
|  345 | Cornell  | EE             | N        |
|  678 | Stanford | history        | Y        |
|  987 | Stanford | CS             | Y        |
|  987 | Berkeley | CS             | Y        |
|  876 | Stanford | CS             | N        |
|  876 | MIT      | biology        | Y        |
|  876 | MIT      | marine biology | N        |
|  765 | Stanford | history        | Y        |
|  765 | Cornell  | history        | N        |
|  765 | Cornell  | psychology     | Y        |
|  543 | MIT      | CS             | N        |
+------+----------+----------------+----------+

Спробуємо знайти бали середнього балу для студентів, які подали заявку на CSмажор (незалежно від університету)

Використання підзапиту:

select GPA from Student where sID in (select sID from Apply where major = 'CS');

+------+
| GPA  |
+------+
|  3.9 |
|  3.5 |
|  3.7 |
|  3.9 |
|  3.4 |
+------+

Середнє значення для цього набору результатів:

select avg(GPA) from Student where sID in (select sID from Apply where major = 'CS');

+--------------------+
| avg(GPA)           |
+--------------------+
| 3.6800000000000006 |
+--------------------+

Використання з'єднання:

select GPA from Student, Apply where Student.sID = Apply.sID and Apply.major = 'CS';

+------+
| GPA  |
+------+
|  3.9 |
|  3.9 |
|  3.5 |
|  3.7 |
|  3.7 |
|  3.9 |
|  3.4 |
+------+

середнє значення для цього набору результатів:

select avg(GPA) from Student, Apply where Student.sID = Apply.sID and Apply.major = 'CS';

+-------------------+
| avg(GPA)          |
+-------------------+
| 3.714285714285714 |
+-------------------+

Очевидно, що друга спроба дає помилкові результати в нашому випадку використання, враховуючи, що вона підраховує дублікати для обчислення середнього значення. Очевидно також, що використання distinctз приєднаним висловлюванням не усуне проблему, враховуючи, що вона помилково убереже одне з трьох випадків 3.9оцінки. Правильний випадок - це врахування ДВА (2) вступу в 3.9бал, враховуючи, що насправді є ДВА (2) студенти з такою оцінкою, які відповідають нашим критеріям запитів.

Здається, що в деяких випадках підзапит - це найбезпечніший спосіб, окрім будь-яких проблем із ефективністю.


Я думаю, ви не можете тут використовувати підзапит. Це не той випадок, коли ви логічно можете використовувати будь-який, але один дає неправильну відповідь через технічну реалізацію цього. Це випадок, коли ви НЕ МОЖЕТЕ використовувати підзапит, оскільки студент, який не належить до CS, може набрати 3,9, що є у списку балів IN. Контекст CS втрачається, коли виконується підзапит, що не є логічним. Тож це не гарний приклад, коли можна використовувати будь-яке. Використання підзапиту концептуально / логічно неправильно для цього випадку використання, навіть якщо на щастя це дає правильний результат для іншого набору даних.
Саурабх Патіль

22

Документація MSDN для SQL Server говорить

Багато операторів Transact-SQL, які включають підзапити, можна альтернативно сформулювати як об'єднання. Інші питання можуть задаватися лише підзапросами. У Transact-SQL зазвичай не існує різниці в продуктивності між оператором, що включає підзапит, і семантично еквівалентною версією, яка цього не робить. Однак у деяких випадках, коли існування необхідно перевірити, з'єднання дає кращі показники. В іншому випадку вкладений запит повинен бути оброблений для кожного результату зовнішнього запиту, щоб забезпечити усунення дублікатів. У таких випадках підхід приєднання дасть кращі результати.

тому якщо вам потрібно щось подібне

select * from t1 where exists select * from t2 where t2.parent=t1.id

спробуйте використовувати замість приєднання. В інших випадках це не має значення.

Я кажу: Створення функцій для підзапитів усуває проблему захаращеності і дозволяє реалізувати додаткову логіку для підзапитів. Тому я рекомендую створювати функції для підзапитів, коли це можливо.

Скупчення коду є великою проблемою, і галузь працює над тим, щоб уникати цього десятиліттями.


9
Заміна підзапитів на функції є дуже поганою ідеєю в деяких RDBMS (наприклад, Oracle), тому я рекомендую навпаки - використовувати підзапити / приєднання замість функцій, де це можливо.
Френк Шмітт

3
@FrankSchmitt, будь ласка, підтримайте ваш аргумент посиланнями.
Uğur Gümüşhan

2
Також є випадки, коли вам слід використовувати підзапрос замість з'єднання, навіть якщо ви перевіряєте наявність: якщо ви перевіряєте NOT EXISTS. А NOT EXISTSперемоги над LEFT OUTER JOIN з різних причин: Намети, відмовостійкість (в разі nulable стовпців) і читаність. sqlperformance.com/2012/12/t-sql-queries/left-anti-semi-join
Tim Schmelter

16

Запуск на дуже великій базі даних зі старої CMS Mambo:

SELECT id, alias
FROM
  mos_categories
WHERE
  id IN (
    SELECT
      DISTINCT catid
    FROM mos_content
  );

0 секунд

SELECT
  DISTINCT mos_content.catid,
  mos_categories.alias
FROM
  mos_content, mos_categories
WHERE
  mos_content.catid = mos_categories.id;

~ 3 секунди

ПОЯСНЕННЯ показує, що вони вивчають точно таку ж кількість рядків, але один займає 3 секунди, а один - майже миттєвий. Мораль розповіді? Якщо продуктивність важлива (коли її немає?), Спробуйте кілька способів і подивіться, який із них найшвидший.

І ...

SELECT
  DISTINCT mos_categories.id,
  mos_categories.alias
FROM
  mos_content, mos_categories
WHERE
  mos_content.catid = mos_categories.id;

0 секунд

Знову ж результати, однакова кількість досліджених рядків. Я думаю, що DISTINCT mos_content.catid розбирається набагато більше часу, ніж DISTINCT mos_categories.id.


1
Я хотів би дізнатися більше про те, що ви намагаєтеся вказати в останньому рядку "Я здогадуюсь, що DISTINCT mos_content.catid розбирається набагато більше часу, ніж DISTINCT mos_categories.id." . Ви хочете сказати, що ідентифікатор слід називати тільки, idа не називати щось подібне catid? Спроба оптимізувати мої db-доступу, і ваші знання можуть допомогти.
bool.dev

2
використання SQL IN у цьому випадку є поганою практикою, і це нічого не підтверджує.
Uğur Gümüşhan

15

Як на моє спостереження, як у двох випадках, якщо таблиця містить менше 100 000 записів, то з'єднання буде працювати швидко.

Але у випадку, якщо таблиця містить понад 100 000 записів, найкращим результатом є запит.

У мене є одна таблиця, яка містить 500 000 записів, що я створив під запитом, і час її результату виглядає так

SELECT * 
FROM crv.workorder_details wd 
inner join  crv.workorder wr on wr.workorder_id = wd.workorder_id;

Результат: 13,3 секунди

select * 
from crv.workorder_details 
where workorder_id in (select workorder_id from crv.workorder)

Результат: 1,65 секунди


Я погоджуюсь, що іноді порушення запиту також працює, коли у вас мільйон записів, ви не хочете використовувати приєднання, оскільки вони тривають назавжди. Швидше обробляйте це в коді, а карта в коді краще.
користувач1735921

1
Зв’яжіть ваші приєднання не працюють досить швидко, можливо, вам не вистачає індексу. Аналізатор запитів може бути дуже корисним у порівнянні фактичної продуктивності.
digital.aaron

Я згоден з Аджай Гаджера, я це бачив сам.
користувач1735921

14
Як має сенс порівнювати ефективність двох запитів, які дають різні результати?
Пол Шпігель

Так, це різні запити, але повертається той самий результат
король neo

12

Підзапроси, як правило, використовуються для повернення одного рядка як атомного значення, хоча вони можуть використовуватися для порівняння значень проти кількох рядків із ключовим словом IN. Вони дозволені майже в будь-якому значущому пункті оператора SQL, включаючи цільовий список, пункт WHERE тощо. Простий підзапит може бути використаний як умова пошуку. Наприклад, між парою таблиць:

   SELECT title FROM books WHERE author_id = (SELECT id FROM authors WHERE last_name = 'Bar' AND first_name = 'Foo');

Зауважте, що використання оператора нормального значення для результатів підзапиту вимагає повернення лише одного поля. Якщо вам цікаво перевірити наявність одного значення в наборі інших значень, використовуйте IN:

   SELECT title FROM books WHERE author_id IN (SELECT id FROM authors WHERE last_name ~ '^[A-E]');

Це, очевидно, відрізняється від слова LEFT-JOIN, де ви просто хочете приєднати матеріали з таблиць A і B, навіть якщо умова приєднання не знайде запису відповідності в таблиці B тощо.

Якщо ви просто переживаєте за швидкість, вам доведеться перевірити свою базу даних і написати хороший запит і побачити, чи є якась значна різниця в продуктивності.


11

Версія MySQL: 5.5.28-0ubuntu0.12.04.2-log

Я також мав враження, що JOIN - це завжди краще, ніж підзапит у MySQL, але EXPLAIN - це кращий спосіб зробити судження. Ось приклад, коли підзапити працюють краще, ніж JOIN.

Ось мій запит із 3 підзапитами:

EXPLAIN SELECT vrl.list_id,vrl.ontology_id,vrl.position,l.name AS list_name, vrlih.position AS previous_position, vrl.moved_date 
FROM `vote-ranked-listory` vrl 
INNER JOIN lists l ON l.list_id = vrl.list_id 
INNER JOIN `vote-ranked-list-item-history` vrlih ON vrl.list_id = vrlih.list_id AND vrl.ontology_id=vrlih.ontology_id AND vrlih.type='PREVIOUS_POSITION' 
INNER JOIN list_burial_state lbs ON lbs.list_id = vrl.list_id AND lbs.burial_score < 0.5 
WHERE vrl.position <= 15 AND l.status='ACTIVE' AND l.is_public=1 AND vrl.ontology_id < 1000000000 
 AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=43) IS NULL 
 AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=55) IS NULL 
 AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=246403) IS NOT NULL 
ORDER BY vrl.moved_date DESC LIMIT 200;

ПОЯСНЕНО показує:

+----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
| id | select_type        | table    | type   | possible_keys                                       | key          | key_len | ref                                             | rows | Extra                    |
+----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+
|  1 | PRIMARY            | vrl      | index  | PRIMARY                                             | moved_date   | 8       | NULL                                            |  200 | Using where              |
|  1 | PRIMARY            | l        | eq_ref | PRIMARY,status,ispublic,idx_lookup,is_public_status | PRIMARY      | 4       | ranker.vrl.list_id                              |    1 | Using where              |
|  1 | PRIMARY            | vrlih    | eq_ref | PRIMARY                                             | PRIMARY      | 9       | ranker.vrl.list_id,ranker.vrl.ontology_id,const |    1 | Using where              |
|  1 | PRIMARY            | lbs      | eq_ref | PRIMARY,idx_list_burial_state,burial_score          | PRIMARY      | 4       | ranker.vrl.list_id                              |    1 | Using where              |
|  4 | DEPENDENT SUBQUERY | list_tag | ref    | list_tag_key,list_id,tag_id                         | list_tag_key | 9       | ranker.l.list_id,const                          |    1 | Using where; Using index |
|  3 | DEPENDENT SUBQUERY | list_tag | ref    | list_tag_key,list_id,tag_id                         | list_tag_key | 9       | ranker.l.list_id,const                          |    1 | Using where; Using index |
|  2 | DEPENDENT SUBQUERY | list_tag | ref    | list_tag_key,list_id,tag_id                         | list_tag_key | 9       | ranker.l.list_id,const                          |    1 | Using where; Using index |
+----+--------------------+----------+--------+-----------------------------------------------------+--------------+---------+-------------------------------------------------+------+--------------------------+

Той самий запит із JOINs:

EXPLAIN SELECT vrl.list_id,vrl.ontology_id,vrl.position,l.name AS list_name, vrlih.position AS previous_position, vrl.moved_date 
FROM `vote-ranked-listory` vrl 
INNER JOIN lists l ON l.list_id = vrl.list_id 
INNER JOIN `vote-ranked-list-item-history` vrlih ON vrl.list_id = vrlih.list_id AND vrl.ontology_id=vrlih.ontology_id AND vrlih.type='PREVIOUS_POSITION' 
INNER JOIN list_burial_state lbs ON lbs.list_id = vrl.list_id AND lbs.burial_score < 0.5 
LEFT JOIN list_tag lt1 ON lt1.list_id = vrl.list_id AND lt1.tag_id = 43 
LEFT JOIN list_tag lt2 ON lt2.list_id = vrl.list_id AND lt2.tag_id = 55 
INNER JOIN list_tag lt3 ON lt3.list_id = vrl.list_id AND lt3.tag_id = 246403 
WHERE vrl.position <= 15 AND l.status='ACTIVE' AND l.is_public=1 AND vrl.ontology_id < 1000000000 
AND lt1.list_id IS NULL AND lt2.tag_id IS NULL 
ORDER BY vrl.moved_date DESC LIMIT 200;

а вихід:

+----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type   | possible_keys                                       | key          | key_len | ref                                         | rows | Extra                                        |
+----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | lt3   | ref    | list_tag_key,list_id,tag_id                         | tag_id       | 5       | const                                       | 2386 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | l     | eq_ref | PRIMARY,status,ispublic,idx_lookup,is_public_status | PRIMARY      | 4       | ranker.lt3.list_id                          |    1 | Using where                                  |
|  1 | SIMPLE      | vrlih | ref    | PRIMARY                                             | PRIMARY      | 4       | ranker.lt3.list_id                          |  103 | Using where                                  |
|  1 | SIMPLE      | vrl   | ref    | PRIMARY                                             | PRIMARY      | 8       | ranker.lt3.list_id,ranker.vrlih.ontology_id |   65 | Using where                                  |
|  1 | SIMPLE      | lt1   | ref    | list_tag_key,list_id,tag_id                         | list_tag_key | 9       | ranker.lt3.list_id,const                    |    1 | Using where; Using index; Not exists         |
|  1 | SIMPLE      | lbs   | eq_ref | PRIMARY,idx_list_burial_state,burial_score          | PRIMARY      | 4       | ranker.vrl.list_id                          |    1 | Using where                                  |
|  1 | SIMPLE      | lt2   | ref    | list_tag_key,list_id,tag_id                         | list_tag_key | 9       | ranker.lt3.list_id,const                    |    1 | Using where; Using index                     |
+----+-------------+-------+--------+-----------------------------------------------------+--------------+---------+---------------------------------------------+------+----------------------------------------------+

Порівняння rowsстовпця вказує на різницю та використовується запит із JOIN Using temporary; Using filesort.

Звичайно, коли я запускаю обидва запити, перший виконується за 0,02 секунди, другий не виконується навіть через 1 хв, тому EXPLAIN пояснив ці запити належним чином.

Якщо у мене немає внутрішнього приєднання на list_tagстолі, тобто якщо я його видаляю

AND (SELECT list_id FROM list_tag WHERE list_id=l.list_id AND tag_id=246403) IS NOT NULL  

з першого запиту і відповідно:

INNER JOIN list_tag lt3 ON lt3.list_id = vrl.list_id AND lt3.tag_id = 246403

з другого запиту, EXPLAIN повертає однакову кількість рядків для обох запитів, і обидва ці запити працюють однаково швидко.


У мене схожа ситуація, але з більшою кількістю приєднань, ніж ваша, спробую один раз пояснити
pahnin

В Oracle або PostgreSQL я б спробував: А НЕ ІСНУЄ (ВИБІР 1 ВІД list_tag WHERE list_id = l.list_id AND tag_id in (43, 55, 246403))
Девід Олдрідж

11

Підзапити мають можливість обчислювати функції агрегації на льоту. Наприклад, знайдіть мінімальну ціну книги та отримайте всі книги, які продаються за цією ціною. 1) Використання підзапитів:

SELECT titles, price
FROM Books, Orders
WHERE price = 
(SELECT MIN(price)
 FROM Orders) AND (Books.ID=Orders.ID);

2) за допомогою JOIN

SELECT MIN(price)
     FROM Orders;
-----------------
2.99

SELECT titles, price
FROM Books b
INNER JOIN  Orders o
ON b.ID = o.ID
WHERE o.price = 2.99;

Інший випадок: кілька GROUP BYs з різними таблицями: stackoverflow.com/questions/11415284/… Підзапроси, здається, суворо загальніші. Дивіться також людину MySQL: dev.mysql.com/doc/refman/5.7/uk/optimizing-subqueries.html | dev.mysql.com/doc/refman/5.7/uk/rewriting-subqueries.html
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

6
-1 Це вводить в оману, коли ви використовуєте підзапит і приєднуєтесь до обох прикладів. Те, що ви витягнули підзапит на другий запит, щоб визначити найнижчу ціну замовлення, не впливає, оскільки база даних буде робити те саме. Крім того, ви не переписуєте об'єднання за допомогою підзапиту; обидва запити використовують з'єднання. Ви перебуваєте правильно , що підзапити дозволяють агрегатні функції, але цей приклад не демонструє той факт.
Девід Харкнесс

Я погоджуюся з Девідом, і ви можете використовувати групу, щоб отримати мінімальну ціну.
користувач1735921

9
  • Загальне правило полягає в тому, що приєднання в більшості випадків швидше (99%).
  • Чим більше таблиць даних, тим запити повільніше.
  • Чим менше таблиць даних, тим підзапити мають еквівалентну швидкість, як приєднання .
  • Ці підзапити простіше, легше зрозуміти і легше читати.
  • Більшість рамок Інтернету та додатків, а також їх "ORM" та "Активний запис" генерують запити з підзапитами , оскільки за допомогою підзапитів простіше розділити відповідальність, підтримувати код тощо.
  • Для менших веб-сайтів чи програм підзапити в порядку, але для великих веб-сайтів і додатків вам часто доведеться переписувати згенеровані запити, щоб приєднатися до запитів, особливо якщо запит використовує багато підзапитів у запиті.

Деякі люди кажуть, що "деякі RDBMS можуть переписати підзапит на з'єднання або приєднання до підзапиту, коли він вважає, що один швидше, ніж інший", але це твердження стосується простих випадків, безумовно, не для складних запитів із підзапитами, які насправді викликають проблеми у виконанні.


> але це твердження стосується простих випадків. Я розумію, що це або простий випадок, який можна переписати на RDBMS на "JOIN", або це такий складний випадок, що підзапити тут доречні. :-) Приємна точка щодо ОРМ. Я думаю, що це має найбільший вплив.
пілат

4

Різниця спостерігається лише тоді, коли друга таблиця приєднання має значно більше даних, ніж первинна. У мене був досвід, як показано нижче ...

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

Але тим часом запити приєднання працюють з іншими таблицями, які мають менше записів, ніж основна таблиця.

Тому я вважаю, що заяви про з'єднання та підзапроси працюють нормально, і це залежить від даних та ситуації.


3

У наші дні багато dbs можуть оптимізувати підзапити та приєднання. Таким чином, ви просто повинні вивчити свій запит, використовуючи пояснення і побачити, який з них швидший. Якщо різниця у продуктивності не велика, я вважаю за краще використовувати підзапроси, оскільки вони прості та легкі для розуміння.


1

Я просто замислювався над тією ж проблемою, але я використовую підзапит у частині ВІД. Мені потрібні підключення та запити з великих таблиць, "невільницька" таблиця має 28 мільйонів записів, але результат лише 128, тому малі великі результати результату! Я використовую функцію MAX ().

По-перше, я використовую LEFT JOIN, тому що я вважаю, що це правильний спосіб, mysql можна оптимізувати і т. Д. Другий раз лише для тестування, я переписав, щоб передібрати вибір на JOIN.

Ліворуч ПРИЄДНАЙТЕСЬ час виконання: 1.12s Час виконання SUB-SELECT: 0,06 с

У 18 разів швидший підбір, ніж приєднання! Якраз у рекламі чокіто. Підселекція виглядає жахливо, але результат ...


-1

Якщо ви хочете пришвидшити запит за допомогою приєднання:

Для "внутрішнього приєднання / приєднання" не використовуйте там, де умова замість цього використовується в режимі "ВКЛ". Наприклад:

     select id,name from table1 a  
   join table2 b on a.name=b.name
   where id='123'

 Try,

    select id,name from table1 a  
   join table2 b on a.name=b.name and a.id='123'

Для "Приєднання ліворуч / праворуч" не використовуйте в режимі "УВІМКНЕНО", оскільки якщо ви використовуєте приєднання ліворуч / праворуч, воно отримає всі рядки для будь-якої однієї таблиці. Отже, не використовуйте його в "Увімкнено". Отже, спробуйте скористатися умовою "Де"


Це залежить від SQL-сервера та складності запиту. Багато реалізацій SQL оптимізували б такі прості запити для найкращої продуктивності. Можливо, наведіть приклад ім’я та версії сервера, де така поведінка відбувається для покращення відповіді?
Trendfischer
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.