* Join или вложен Select?

Публикувано на 02 април 2011 в раздел Бази от Данни.

Въпросът поставен в заглавието на статията е много често разискван и около него се водят спорове. По принцип има една тенденция програмистите категорично да избягват вложените select заявки, защото още от миналото има един мит, че те се изпълняват бавно. Този мит се дължи главно на грешки в СУБД, които не са използвали правилно индексите при вложените заявки. Днес това отдавна вече (почти) не се среща, т.е. можем да очакваме вложените заявки да вървят достатъчно добре. Така въпросът “join или вложен select” отново стои на дневен ред.

Ще демонстрираме с един пример. Нека имаме следната база от данни:

CREATE TABLE clients(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(255) NOT NULL) ENGINE=InnoDB;

CREATE TABLE products(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(255) NOT NULL) ENGINE=InnoDB;

CREATE TABLE orders(
   client_id INT UNSIGNED NOT NULL,
   product_id INT UNSIGNED NOT NULL,
   quantity INT UNSIGNED NOT NULL,
   PRIMARY KEY(client_id,product_id),
   FOREIGN KEY (client_id) REFERENCES clients(id),
   FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB;

INSERT INTO clients(id, name)
VALUES (NULL, "Ivan"),
       (NULL, "Petar"),
       (NULL, "Maria"),
       (NULL, "Philip");

INSERT INTO products(id, name)
VALUES (NULL, "GSM"),
       (NULL, "Bike"),
       (NULL, "Icecream");

INSERT INTO orders(client_id, product_id, quantity)
VALUES (1, 1, 2),
       (1, 3, 4),
       (2, 1, 1),
       (2, 2, 1),
       (2, 3, 2),
       (3, 2, 2),
       (4, 3, 1);

Поставяме си да решим следната задача: изведете id-тата на хората, които са поръчали продукти 1 или 2. Впечатление прави условието “или”, както също така, че връзката между clients и products е от тип M:M.

Първо ще дадем естественото решение с вложен select:

SELECT clients.name
FROM clients
WHERE clients.id IN(
      SELECT orders.client_id
      FROM orders
      WHERE product_id IN (1,2)
);

Очаквано отговорът на СУБД е:

+-------+
| name  |
+-------+
| Ivan  |
| Petar |
| Maria |
+-------+

При използването на решение с join положението не е толкова просто. Следният пример е грешен:

SELECT clients.name
FROM clients JOIN orders ON clients.id=orders.client_id
WHERE orders.product_id IN (1,2);

Ще видим, че в резултатът от него има повторения:

+-------+
| name  |
+-------+
| Ivan  |
| Petar |
| Petar |
| Maria |
+-------+

За да се избавим от повторенията ще е нужно да използваме ключова дума DISTINCT:

SELECT DISTINCT clients.name
FROM clients JOIN orders ON clients.id=orders.client_id
WHERE orders.product_id IN (1,2);

Така заявката вече ще върне правилен резултат.

Коя от двете заявки беше по-добра? Отговорът еднозначно е, че това е тази, която е с вложен select! При заявката с join първо се генерира таблицата с повторенията, а чак след това се премахнаха дублиращите се. При вложен select дублирания въобще нямаше. Ако направите по-сериозен тест с огромно количество данни с много повторения и създадени индекси по съответните колони, то ще видите, че заявката с вложен select ще се изпълни видимо по-бързо.

Когато използвате SELECT DISTINCT … WHERE… заявки (или GROUP BY), то СУБД първо ще създаде временна свързваща таблица и чак тогава ще ги агрегира. При вложен select това не се случва, съответно не се правят безсмислени операции.

Ето още един начин да направим същата заявка, но вместо IN ще използваме EXISTS:

SELECT clients.name
FROM clients
WHERE EXISTS(
      SELECT * FROM orders
      WHERE orders.product_id IN (1,2) AND orders.client_id=clients.id
);

Дали IN или EXISTS е по-добър подход ще разгледаме по-късно в отделна статия. При всички положения обаче вложения select би трябвало да дава по-добри резултати от използването на join като негова алтернатива.



2 коментара за “Join или вложен Select?”


  1. Божо:

    Благодаря за информацията.
    За мене е много полезно уточнение, защото винаги съм се чудел, как е “по-правилно” да се пише.


  2. Ляля:

    а така мисля, че е по добре:

    SELECT t1.name
    FROM clients t1, (SELECT DISTINCT orders.client_id as id FROM orders WHERE orders.product_id IN (1,2)) t2
    WHERE t1.id = t2.id
    

Trackback URI | RSS за коментарите

Пусни коментар