Самый эффективный способ ВЫБРАТЬ одну строку в одной: множество пар таблиц в MySQL - PullRequest
2 голосов
/ 06 февраля 2012

Допустим, у меня есть следующие данные в таблицах "один ко многим", соответственно, город и человек:

SELECT city.*, person.* FROM city, person WHERE city.city_id = person.person_city_id;
+---------+-------------+-----------+-------------+----------------+
| city_id | city_name   | person_id | person_name | person_city_id |
+---------+-------------+-----------+-------------+----------------+
|       1 | chicago     |         1 | charles     |              1 |
|       1 | chicago     |         2 | celia       |              1 |
|       1 | chicago     |         3 | curtis      |              1 |
|       1 | chicago     |         4 | chauncey    |              1 |
|       2 | new york    |         5 | nathan      |              2 |
|       3 | los angeles |         6 | luke        |              3 |
|       3 | los angeles |         7 | louise      |              3 |
|       3 | los angeles |         8 | lucy        |              3 |
|       3 | los angeles |         9 | larry       |              3 |
+---------+-------------+-----------+-------------+----------------+
9 rows in set (0.00 sec)

И я хочу выбрать одну запись от человека для каждого уникального города, используя определенную логику. Например:

SELECT city.*, person.* FROM city, person WHERE city.city_id = person.person_city_id
GROUP BY city_id ORDER BY person_name DESC
;

Подразумевается, что в каждом городе я хочу получить наибольшее значение для лексикографии, например:

+---------+-------------+-----------+-------------+----------------+
| city_id | city_name   | person_id | person_name | person_city_id |
+---------+-------------+-----------+-------------+----------------+
|       2 | new york    |         5 | nathan      |              2 |
|       3 | los angeles |         6 | luke        |              3 |
|       1 | chicago     |         1 | curtis      |              1 |
+---------+-------------+-----------+-------------+----------------+

Фактический вывод, который я получаю, однако:

+---------+-------------+-----------+-------------+----------------+
| city_id | city_name   | person_id | person_name | person_city_id |
+---------+-------------+-----------+-------------+----------------+
|       2 | new york    |         5 | nathan      |              2 |
|       3 | los angeles |         6 | luke        |              3 |
|       1 | chicago     |         1 | charles     |              1 |
+---------+-------------+-----------+-------------+----------------+

Я понимаю, что причина этого несоответствия в том, что MySQL сначала выполняет GROUP BY, а затем ORDER BY. Это прискорбно для меня, так как я хочу, чтобы GROUP BY имела логику выбора, для которой она выбирает запись.

Я могу обойти это, используя несколько вложенных операторов SELECT:

SELECT c.*, p.* FROM city c,
    ( SELECT p_inner.* FROM
        ( SELECT * FROM person ORDER BY person_city_id, person_name DESC ) p_inner
        GROUP BY person_city_id ) p
    WHERE c.city_id = p.person_city_id;
+---------+-------------+-----------+-------------+----------------+
| city_id | city_name   | person_id | person_name | person_city_id |
+---------+-------------+-----------+-------------+----------------+
|       1 | chicago     |         3 | curtis      |              1 |
|       2 | new york    |         5 | nathan      |              2 |
|       3 | los angeles |         6 | luke        |              3 |
+---------+-------------+-----------+-------------+----------------+

Кажется, что это было бы ужасно неэффективно, когда таблица person становится сколь угодно большой. Я предполагаю, что внутренние операторы SELECT не знают о внешних фильтрах WHERE. Это правда?

Каков наилучший подход для эффективного выполнения заказа на до GROUP BY?

Ответы [ 2 ]

1 голос
/ 06 февраля 2012

Обычный способ сделать это (в MySQL) - соединить вашу таблицу с самим собой.

Сначала получить наибольшее person_name за city (то есть за person_city_id в таблице person):

SELECT p.*
FROM person p
LEFT JOIN person p2
 ON p.person_city_id = p2.person_city_id
 AND p.person_name < p2.person_name
WHERE p2.person_name IS NULL

Это объединяет person внутри себя в каждомperson_city_id (ваша переменная GROUP BY), а также объединяет таблицы так, что p2 s person_name больше p s person_name.

, так как это левое соединениеесли есть p.person_name, для которого не больше p2.person_name (в том же городе), то p2.person_name будет NULL.Это как раз "величайшие" person_name с на город.

Итак, чтобы присоединить к нему свою другую информацию (от city), просто сделайте еще одно соединение:

SELECT c.*,p.*
FROM person p
LEFT JOIN person p2
 ON p.person_city_id = p2.person_city_id
 AND p.person_name < p2.person_name
LEFT JOIN city c                           -- add in city table
 ON p.person_city_id = c.city_id           -- add in city table
WHERE p2.person_name IS NULL               -- ORDER BY c.city_id if you like
0 голосов
/ 06 февраля 2012

Ваше «решение» не является допустимым SQL, но оно работает в MySQL.Однако вы не можете быть уверены, что это произойдет с будущим изменением кода оптимизатора запросов.Можно было бы немного улучшить, чтобы иметь только 1 уровень вложенности (все еще не допустимый SQL):

--- Option 1 ---
SELECT 
       c.*
     , p.* 
FROM 
      city AS c
  JOIN
      ( SELECT * 
        FROM person 
        ORDER BY person_city_id
               , person_name DESC 
      ) AS p
    ON  c.city_id = p.person_city_id
GROUP BY p.person_city_id

Другой способ (допустимый синтаксис SQL, работает и в других СУБД) - создать подзапросвыбрать фамилию для каждого города и затем присоединиться:

--- Option 2 ---
SELECT 
       c.*
     , p.* 
FROM 
      city AS c
  JOIN
      ( SELECT person_city_id
             , MAX(person_name) AS person_name 
        FROM person 
        GROUP BY person_city_id
      ) AS pmax
    ON  c.city_id = pmax.person_city_id
  JOIN 
      person AS p
    ON  p.person_city_id = pmax.person_city_id
    AND p.person_name = pmax.person_name

Другим способом является самостоятельное объединение (таблицы person) с трюком <, который описывает @matumatic_coffee.

--- Option 3 ---
  see @mathematical-coffee's answer

Еще один способ - использовать LIMIT 1 подзапрос для объединения city с person:

--- Option 4 ---
SELECT 
       c.*
     , p.* 
FROM 
      city AS c
  JOIN
      person AS p
    ON
      p.person_id =
      ( SELECT person_id
        FROM person AS pm 
        WHERE pm.person_city_id = c.city_id
        ORDER BY person_name DESC
        LIMIT 1
      ) 

. Это запустит подзапрос (в таблице person) для каждого города, и будет эффективно, если у вас есть индекс (person_city_id, person_name) для движка InnoDB или (person_city_id, person_name, person_id) для движка MyISAM.


Существует одно существенное различие между этими опциями:

Оприоны 2 и 3 будут возвращать все связанные результаты (если у вас есть два или более человека в городе с одинаковым именем в алфавитном порядкепоследний, тогда будут показаны оба или все).

Опции 1 и 4 будут возвращать один результат на город, даже если есть связи.Вы можете выбрать, какой из них, изменив предложение ORDER BY.


Какой вариант более эффективен, зависит также от распределения ваших данных, поэтому лучший способ - попробовать их все, проверить их планы выполнения.и найти лучшие индексы, которые работают для каждого из них.Индекс на (person_city_id, person_name), скорее всего, будет хорошим для любого из этих запросов.

С распределением я имею в виду:

  • У вас мало городов с большим количеством людей на город?(Я думаю, что варианты 2 и 4 будут вести себя лучше в этом случае)

  • Или много городов с небольшим количеством людей в городе?(вариант 3 может быть лучше с такими данными).

...