Оптимизировать SQL, который использует между - PullRequest
10 голосов
/ 17 февраля 2009

Рассмотрим следующие 2 таблицы:

Table A:
id
event_time

Table B
id
start_time
end_time

Каждая запись в таблице A отображается точно на 1 запись в таблице B. Это означает, что таблица B не имеет перекрывающихся периодов. Многие записи из таблицы A могут быть сопоставлены с одной и той же записью в таблице B.

Мне нужен запрос, который возвращает все пары A.id, B.id. Что-то вроде:

SELECT A.id, B.id 
FROM A, B 
WHERE A.event_time BETWEEN B.start_time AND B.end_time

Я использую MySQL и не могу оптимизировать этот запрос. При ~ 980 записях в таблице A и 130 000 в таблице B это занимает вечность. Я понимаю, что это должно выполнить 980 запросов, но более 15 минут работы на мощной машине странно. Есть предложения?

P.S. Я не могу изменить схему базы данных, но я могу добавить индексы. Однако индекс (с 1 или 2 полями) для полей времени не помогает.

Ответы [ 19 ]

4 голосов
/ 17 февраля 2009

Вы можете попробовать что-то вроде этого

Select A.ID,
(SELECT B.ID FROM B
WHERE A.EventTime BETWEEN B.start_time AND B.end_time LIMIT 1) AS B_ID
FROM A

Если у вас есть индекс в полях Start_Time, End_Time для B, то это должно работать довольно хорошо.

3 голосов
/ 17 февраля 2009

Я не уверен, что это можно полностью оптимизировать. Я попробовал это на MySQL 5.1.30. Я также добавил индекс на {B.start_time, B.end_time}, как предложили другие люди. Затем я получил отчет от EXPLAIN, но лучшее, что я мог получить, это Метод доступа к диапазону :

EXPLAIN SELECT A.id, B.id FROM A JOIN B 
ON A.event_time BETWEEN B.start_time AND B.end_time;

+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                                          |
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------------------------+
|  1 | SIMPLE      | A     | ALL  | event_time    | NULL | NULL    | NULL |    8 |                                                | 
|  1 | SIMPLE      | B     | ALL  | start_time    | NULL | NULL    | NULL |   96 | Range checked for each record (index map: 0x4) | 
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------------------------+

См. Примечание справа. Оптимизатор считает, что может иметь возможность использовать индекс на {B.start_time, B.end_time}, но в итоге он решил не использовать этот индекс. Ваши результаты могут отличаться, потому что ваше распределение данных более репрезентативно.

Сравните с использованием индекса, если сравнить A.event_time с постоянным диапазоном:

EXPLAIN SELECT A.id FROM A
WHERE A.event_time BETWEEN '2009-02-17 09:00' and '2009-02-17 10:00';

+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key        | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+
|  1 | SIMPLE      | A     | range | event_time    | event_time | 8       | NULL |    1 | Using where | 
+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+

И сравните с зависимой формой подзапроса, заданной @Luke и @Kibbee, которая, кажется, использует индексы более эффективно:

EXPLAIN SELECT A.id AS id_from_a,
    (
        SELECT B.id
        FROM B
        WHERE A.id BETWEEN B.start_time AND B.end_time
        LIMIT 0, 1
    ) AS id_from_b
FROM A;

+----+--------------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type        | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+--------------------+-------+-------+---------------+---------+---------+------+------+-------------+
|  1 | PRIMARY            | A     | index | NULL          | PRIMARY | 8       | NULL |    8 | Using index | 
|  2 | DEPENDENT SUBQUERY | B     | ALL   | start_time    | NULL    | NULL    | NULL |  384 | Using where | 
+----+--------------------+-------+-------+---------------+---------+---------+------+------+-------------+

Странно, EXPLAIN перечисляет possible_keys как NULL (то есть индексы не могут быть использованы), но затем решает использовать первичный ключ в конце концов. Может быть, идиосинкразия в отчете EXPLAIN в MySQL?

2 голосов
/ 27 октября 2010

Я провел несколько тестов для аналогичной проблемы - вычисления страны на основе IP-адреса (заданного в виде числа). Вот мои данные и результаты:

  • Таблица A (содержит пользователей и IP-адреса) содержит около 20 записей.
  • Таблица B (содержит диапазоны IP-адресов для каждой страны) содержит около 100000 записей.

Запрос JOIN с использованием «между» занимает около 10 секунд; SELECT внутри запроса SELECT с использованием «между» занимает около 5,5 секунд; SELECT внутри запроса SELECT с использованием пространственного индекса занимает около 6,3 секунды. Запрос JOIN с использованием пространственного индекса занимает 0 секунд!

2 голосов
/ 17 февраля 2009

Обычно я бы не рекомендовал такой запрос, но ...

Поскольку вы указали, что таблица A имеет только около 980 строк и каждая строка отображается точно на одну строку в таблице B, вы можете сделать следующее, и это, скорее всего, будет намного быстрее, чем декартово соединение:

SELECT A.id AS id_from_a,
    (
        SELECT B.id
        FROM B
        WHERE A.event_time BETWEEN B.start_time AND B.end_time
        LIMIT 0, 1
    ) AS id_from_b
FROM A
1 голос
/ 17 февраля 2009

Обратите внимание, что при выполнении этого запроса вы на самом деле создаете 980x130000 записей в памяти перед применением условия. Такой JOIN не очень рекомендуется, и я понимаю, почему он вызовет проблемы с производительностью.

1 голос
/ 17 февраля 2009

Не изменяя схему, вы не можете добавить индекс? Попробуйте многостолбцовый индекс для start_time и end_time.

1 голос
/ 17 февраля 2009

Если вы не можете изменить схему - в частности, если вы не можете добавить индекс в a.event_time, я не вижу большой возможности для улучшения на уровне SQL.

Я бы более склонен сделать это в коде.

  • прочитать все кортежи B start / end / id в список, отсортированный по времени старта
  • читать все события A
  • за каждое событие A
    • найти наибольшее время начала <= время события (двоичный поиск подойдет) </li>
    • если время события <= время окончания, добавьте A в список событий этого B </li>
    • иначе этот B не имеет дома
0 голосов
/ 17 февраля 2009

Попробуйте, используя стандартный оператор сравнения (<и>).

0 голосов
/ 19 февраля 2009

В моем решении есть две оговорки:

1) Вы сказали, что можете добавлять индексы, но не изменять схему, поэтому я не уверен, сработает ли это для вас или нет, поскольку у вас не может быть индексов на основе функций в MySQL, и вам нужно будет создать дополнительный столбец в таблице B. 2) Другим предостережением для этого решения является то, что вы должны использовать механизм MyISAM для таблицы B. Если вы не можете использовать MyISAM, то это решение не будет работать, поскольку для пространственных индексов поддерживается только MyISAM.

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

В этом решении используется поддержка MySQL для пространственных данных (см. документацию здесь ). Хотя пространственные типы данных могут быть добавлены к различным механизмам хранения, только MyISAM поддерживается для пространственных индексов R-дерева (см. документация здесь ), которые необходимы для получения необходимой производительности. Еще одно ограничение заключается в том, что пространственные типы данных работают только с числовыми данными, поэтому вы не можете использовать эту технику при запросах диапазона на основе строк.

Я не буду вдаваться в детали теории о том, как работают пространственные типы и как полезен пространственный индекс, но вы должны взглянуть на объяснение Джереми Коула здесь относительно того, как использовать пространственные типы данных и индексы для поиска GeoIP. Также посмотрите на комментарии, так как они поднимают некоторые полезные моменты и альтернативу, если вам нужна грубая производительность и вы можете отказаться от некоторой точности.

Основная предпосылка заключается в том, что мы можем взять начало / конец и использовать две из них для создания четырех различных точек, по одной для каждого угла прямоугольника с центром в районе 0,0 на сетке xy, а затем выполнить быстрый поиск в пространственный индекс, чтобы определить, находится ли конкретный момент времени, о котором мы заботимся, внутри прямоугольника или нет. Как упоминалось ранее, см. Объяснение Джереми Коула для более подробного обзора того, как это работает.

В вашем конкретном случае нам нужно будет сделать следующее:

1) Измените таблицу на таблицу MyISAM (обратите внимание, что вам не следует делать это, если вы не в полной мере осведомлены о последствиях такого изменения, таких как отсутствие транзакций и поведение блокировки таблиц, связанных с MyISAM).

alter table B engine = MyISAM;

2) Затем мы добавляем новый столбец, который будет содержать пространственные данные. Мы будем использовать тип данных многоугольника, так как нам нужно уметь удерживать полный прямоугольник.

alter table B add column time_poly polygon NOT NULL;

3) Затем мы заполняем новый столбец данными (имейте в виду, что любые процессы, которые обновляют или вставляют в таблицу B, необходимо будет изменить, чтобы убедиться, что они также заполняют новый столбец). Поскольку начальный и конечный диапазоны являются временами, нам необходимо преобразовать их в числа с помощью функции unix_timestamp (см. Документацию здесь , чтобы узнать, как она работает).

update B set time_poly := LINESTRINGFROMWKB(LINESTRING(
    POINT(unix_timestamp(start_time), -1),
    POINT(unix_timestamp(end_time), -1),
    POINT(unix_timestamp(end_time), 1),
    POINT(unix_timestamp(start_time), 1),
    POINT(unix_timestamp(start_time), -1)
  ));

4) Затем мы добавляем пространственный индекс в таблицу (как упоминалось ранее, это будет работать только для таблицы MyISAM и приведет к ошибке «ОШИБКА 1464 (HY000): используемый тип таблицы не поддерживает ПРОСТРАНСТВЕННЫЕ индексы») ).

alter table B add SPATIAL KEY `IXs_time_poly` (`time_poly`);

5) Затем вам нужно будет использовать следующий выбор, чтобы использовать пространственный индекс при запросе данных.

SELECT A.id, B.id 
FROM A inner join B force index (IXs_time_poly)
ON MBRCONTAINS(B.time_poly, POINTFROMWKB(POINT(unix_timestamp(A.event_time), 0)));

Индекс силы предназначен для 100% уверенности, что MySQL будет использовать этот индекс для поиска. Если все прошло хорошо, объяснение вышеупомянутого выбора должно показать что-то похожее на следующее:

mysql> explain SELECT A.id, B.id
    -> FROM A inner join B force index (IXs_time_poly)
    -> on MBRCONTAINS(B.time_poly, POINTFROMWKB(POINT(unix_timestamp(A.event_time), 0)));
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra                                           |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------------------------------------------+
|  1 | SIMPLE      | A     | ALL  | NULL          | NULL | NULL    | NULL |    1065 |                                                 | 
|  1 | SIMPLE      | B     | ALL  | IXs_time_poly | NULL | NULL    | NULL | 7969897 | Range checked for each record (index map: 0x10) | 
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------------------------------------------+
2 rows in set (0.00 sec)

Пожалуйста, обратитесь к анализу Джереми Коула для деталей о преимуществах производительности этого метода по сравнению с предложением между ними.

Дайте мне знать, если у вас есть какие-либо вопросы.

Спасибо

-Dipin

0 голосов
/ 18 февраля 2009

Лично, если у вас есть отношение один ко многим, и каждая запись в таблице a относится только к одной записи в таблице b, я бы сохранил идентификатор таблицы b в таблице a, а затем сделал бы регулярное соединение для получения данных. В настоящее время у вас плохой дизайн, который никогда не будет по-настоящему эффективным.

...