Sql: Оптимизация между предложением - PullRequest
7 голосов
/ 22 декабря 2009

Я написал заявление, которое занимает почти час, поэтому я прошу помощи, чтобы я мог сделать это быстрее. Итак, поехали:

Я делаю внутреннее соединение двух таблиц:

У меня есть много временных интервалов, представленных интервалами, и я хочу получить данные измерений из мер только в этих интервалах.

intervals: имеет два столбца, один - время начала, другой - время окончания интервала (количество строк = 1295)

measures: имеет два столбца, один с показателем, другой со временем выполнения измерения (количество строк = один миллион)

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

Вот мой код:

select measures.measure as measure, measures.time as time, intervals.entry_time as entry_time, intervals.exit_time as exit_time
    from
    intervals
    inner join  
    measures
    on  intervals.entry_time<=measures.time  and measures.time <=intervals.exit_time  
    order by time asc

Спасибо

Ответы [ 9 ]

19 голосов
/ 22 декабря 2009

Это довольно распространенная проблема.

Обычные B-Tree индексы не подходят для таких запросов:

SELECT  measures.measure as measure,
        measures.time as time,
        intervals.entry_time as entry_time,
        intervals.exit_time as exit_time
FROM    intervals
JOIN    measures
ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
ORDER BY
        time ASC

Индекс подходит для поиска значений в заданных границах, например:

image

, but not for searching the bounds containing the given value, like this:

image

This article in my blog explains the problem in more detail:

(модель вложенных множеств имеет дело с аналогичным типом предиката).

Вы можете сделать индекс на time, таким образом, intervals будет ведущим в соединении, а время в ранге будет использоваться внутри вложенных циклов. Это потребует сортировки по time.

Вы можете создать пространственный индекс в intervals (доступно в MySQL с использованием MyISAM хранилища), который будет включать start и end в одном столбце геометрии. Таким образом, measures может привести к объединению, и сортировка не потребуется.

Пространственные индексы, однако, более медленные, поэтому это будет эффективно, только если у вас мало тактов, но много интервалов.

Поскольку у вас мало интервалов, но много мер, просто убедитесь, что у вас есть индекс для measures.time:

CREATE INDEX ix_measures_time ON measures (time)

Обновление:

Вот пример сценария для проверки:

BEGIN
        DBMS_RANDOM.seed(20091223);
END;
/

CREATE TABLE intervals (
        entry_time NOT NULL,
        exit_time NOT NULL
)
AS
SELECT  TO_DATE('23.12.2009', 'dd.mm.yyyy') - level,
        TO_DATE('23.12.2009', 'dd.mm.yyyy') - level + DBMS_RANDOM.value
FROM    dual
CONNECT BY
        level <= 1500
/

CREATE UNIQUE INDEX ux_intervals_entry ON intervals (entry_time)
/

CREATE TABLE measures (
        time NOT NULL,
        measure NOT NULL
)
AS
SELECT  TO_DATE('23.12.2009', 'dd.mm.yyyy') - level / 720,
        CAST(DBMS_RANDOM.value * 10000 AS NUMBER(18, 2))
FROM    dual
CONNECT BY
        level <= 1080000
/

ALTER TABLE measures ADD CONSTRAINT pk_measures_time PRIMARY KEY (time)
/

CREATE INDEX ix_measures_time_measure ON measures (time, measure)
/

Этот запрос:

SELECT  SUM(measure), AVG(time - TO_DATE('23.12.2009', 'dd.mm.yyyy'))
FROM    (
        SELECT  *
        FROM    (
                SELECT  /*+ ORDERED USE_NL(intervals measures) */
                        *
                FROM    intervals
                JOIN    measures
                ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
                ORDER BY
                        time
                )
        WHERE   rownum <= 500000
        )

использует NESTED LOOPS и возвращается через 1.7 секунд.

Этот запрос:

SELECT  SUM(measure), AVG(time - TO_DATE('23.12.2009', 'dd.mm.yyyy'))
FROM    (
        SELECT  *
        FROM    (
                SELECT  /*+ ORDERED USE_MERGE(intervals measures) */
                        *
                FROM    intervals
                JOIN    measures
                ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
                ORDER BY
                        time
                )
        WHERE   rownum <= 500000
        )

использует MERGE JOIN, и мне пришлось остановить его через 5 минут.

Обновление 2:

Скорее всего, вам потребуется заставить движок использовать правильный порядок таблиц в объединении, используя следующую подсказку:

SELECT  /*+ LEADING (intervals) USE_NL(intervals, measures) */
        measures.measure as measure,
        measures.time as time,
        intervals.entry_time as entry_time,
        intervals.exit_time as exit_time
FROM    intervals
JOIN    measures
ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
ORDER BY
        time ASC

Оптимизатор Oracle недостаточно умен, чтобы видеть, что интервалы не пересекаются. Вот почему он, скорее всего, будет использовать measures в качестве ведущей таблицы (что было бы мудрым решением, если интервалы пересекаются).

Обновление 3:

WITH    splits AS
        (
        SELECT  /*+ MATERIALIZE */
                entry_range, exit_range,
                exit_range - entry_range + 1 AS range_span,
                entry_time, exit_time
        FROM    (
                SELECT  TRUNC((entry_time - TO_DATE(1, 'J')) * 2) AS entry_range,
                        TRUNC((exit_time - TO_DATE(1, 'J')) * 2) AS exit_range,
                        entry_time,
                        exit_time
                FROM    intervals
                )
        ),
        upper AS
        (
        SELECT  /*+ MATERIALIZE */
                MAX(range_span) AS max_range
        FROM    splits
        ),
        ranges AS
        (
        SELECT  /*+ MATERIALIZE */
                level AS chunk
        FROM    upper
        CONNECT BY
                level <= max_range
        ),
        tiles AS
        (
        SELECT  /*+ MATERIALIZE USE_MERGE (r s) */
                entry_range + chunk - 1 AS tile,
                entry_time,
                exit_time
        FROM    ranges r
        JOIN    splits s
        ON      chunk <= range_span
        )
SELECT  /*+ LEADING(t) USE_HASH(m t) */
        SUM(LENGTH(stuffing))
FROM    tiles t
JOIN    measures m
ON      TRUNC((m.time - TO_DATE(1, 'J')) * 2) = tile
        AND m.time BETWEEN t.entry_time AND t.exit_time

Этот запрос разбивает временную ось на диапазоны и использует HASH JOIN для объединения мер и временных меток в значениях диапазона с тонкой фильтрацией позже.

См. Эту статью в моем блоге для более подробного объяснения того, как это работает:

3 голосов
/ 23 декабря 2009

Подводя итог: ваш запрос выполняется с полным набором ИЗМЕРЕНИЙ. Он сопоставляет время каждой записи MEASURES с записью INTERVALS. Если окно времен, охватываемое INTERVALS, примерно аналогично окну, охватываемому MEASURES, тогда ваш запрос также выполняется с полным набором INTERVALS, в противном случае он работает с подмножеством.

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

Возможные стратегии:

  • нет индексов вообще
  • индекс по ИЗМЕРЕНИЯМ (ВРЕМЯ, ИЗМЕРЕНИЕ)
  • индекс по ИЗМЕРЕНИЯМ (ВРЕМЯ)
  • нет указателей по ИЗМЕРЕНИЯМ
  • индекс по INTERVALS (ENTRY_TIME, EXIT_TIME)
  • индекс ИНТЕРВАЛОВ (ENTRY_TIME)
  • без индекса на ИНТЕРВАЛАХ
  • параллельный запрос

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

Вот данные теста. Как видите, я использую несколько большие наборы данных. Окно INTERVALS больше, чем окна MEASURES, но не намного. Интервалы шириной 10000 секунд, а измерения проводятся каждые 15 секунд.

SQL> select min(entry_time), max(exit_time), count(*) from intervals;

MIN(ENTRY MAX(EXIT_   COUNT(*)
--------- --------- ----------
01-JAN-09 20-AUG-09       2001

SQL> select min(ts), max(ts), count(*) from measures;

MIN(TS)   MAX(TS)     COUNT(*)
--------- --------- ----------
02-JAN-09 17-JUN-09    1200001

SQL>

NB В моих тестовых данных я предположил, что записи INTERVAL не перекрываются. Это имеет важное следствие: запись MEASURES объединяется только с одним INTERVAL.

Benchmark

Вот эталонный тест без индексов.

SQL> exec dbms_stats.gather_table_stats(user, 'MEASURES', cascade=>true)

PL/SQL procedure successfully completed.

SQL> exec dbms_stats.gather_table_stats(user, 'INTERVALS', cascade=>true)

PL/SQL procedure successfully completed.

SQL> set timing on
SQL> 
SQL> select m.measure
  2         , m.ts as "TIME"
  3         , i.entry_time
  4         , i.exit_time
  5  from
  6      intervals i
  7  inner join
  8      measures m
  9      on ( m.ts between  i.entry_time and i.exit_time )
 10  order by m.ts asc
 11  /

1200001 rows selected.

Elapsed: 00:05:37.03

SQL>

МЕРЫ тесты

Теперь давайте создадим уникальный индекс для INTERVALS (ENTRY_TIME, EXIT_TIME) и опробуем различные стратегии индексации для MEASURES. Во-первых, только индекс MEASURES TIME.

SQL> create index meas_idx on measures (ts)
  2  /

Index created.

SQL> exec dbms_stats.gather_table_stats(user, 'MEASURES', cascade=>true)

PL/SQL procedure successfully completed.

SQL> 
SQL> set autotrace traceonly exp
SQL> 
SQL> set timing on
SQL> 
SQL> select m.measure
  2         , m.ts as "TIME"
  3         , i.entry_time
  4         , i.exit_time
  5  from
  6      intervals i
  7  inner join
  8      measures m
  9      on ( m.ts between  i.entry_time and i.exit_time )
 10  order by m.ts asc
 11  /

1200001 rows selected.

Elapsed: 00:05:20.21

SQL>

Теперь давайте индексируем столбцы MEASURES.TIME и MEASURE

SQL> drop  index meas_idx
  2  /

Index dropped.

SQL> create index meas_idx on measures (ts, measure)
  2  /

Index created.

SQL> exec dbms_stats.gather_table_stats(user, 'MEASURES', cascade=>true)

PL/SQL procedure successfully completed.


SQL> select m.measure
  2         , m.ts as "TIME"
  3         , i.entry_time
  4         , i.exit_time
  5  from
  6      intervals i
  7  inner join
  8      measures m
  9      on ( m.ts between  i.entry_time and i.exit_time )
 10  order by m.ts asc
 11  /

1200001 rows selected.

Elapsed: 00:05:28.54

SQL>

Теперь без индекса на MEASURES (но все еще индекс на INTERVALS)

SQL> drop  index meas_idx
  2  /

Index dropped.

SQL> exec dbms_stats.gather_table_stats(user, 'MEASURES', cascade=>true)

PL/SQL procedure successfully completed.

SQL> select m.measure
  2         , m.ts as "TIME"
  3         , i.entry_time
  4         , i.exit_time
  5  from
  6      intervals i
  7  inner join
  8      measures m
  9      on ( m.ts between  i.entry_time and i.exit_time )
 10  order by m.ts asc
 11  /

1200001 rows selected.

Elapsed: 00:05:24.81

SQL> 

Так что же делает параллельный запрос?

SQL> select /*+ parallel (4) */
  2         m.measure
  3         , m.ts as "TIME"
  4         , i.entry_time
  5         , i.exit_time
  6  from
  7      intervals i
  8  inner join
  9      measures m
 10      on ( m.ts between  i.entry_time and i.exit_time )
 11  order by m.ts asc
 12  /

1200001 rows selected.

Elapsed: 00:02:33.82


SQL>

МЕРЫ Заключение

Не большая разница во времени для разных индексов. Я был немного удивлен, что построение индекса по MEASURES (TS, MEASURE) привело к полному сканированию таблицы и несколько более медленному времени выполнения. С другой стороны, неудивительно, что параллельный запрос выполняется намного быстрее. Таким образом, если у вас есть Enterprise Edition и у вас есть запасные процессоры, использование PQ определенно сократит затраченное время, хотя это не изменит затрат на ресурсы (и на самом деле lot больше сортировки).

ИНТЕРВАЛЫ тесты

Так, что могут изменить различные индексы на ИНТЕРВАЛАХ? В следующих тестах мы сохраним индекс на MEASURES (TS). Прежде всего мы удалим первичный ключ в обоих столбцах INTERVALS и заменим его ограничением только на INTERVALS (ENTRY_TIME).

SQL> alter table intervals drop  constraint ivl_pk drop  index
  2  /

Table altered.

SQL> alter table intervals add  constraint ivl_pk primary key (entry_time) using  index
  2  /

Table altered.

SQL> exec dbms_stats.gather_table_stats(user, 'INTERVALS', cascade=>true)

PL/SQL procedure successfully completed.


SQL> select m.measure
  2         , m.ts as "TIME"
  3         , i.entry_time
  4         , i.exit_time
  5  from
  6      intervals i
  7  inner join
  8      measures m
  9      on ( m.ts between  i.entry_time and i.exit_time )
 10  order by m.ts asc
 11  /

1200001 rows selected.

Elapsed: 00:05:38.39

SQL> 

Наконец, без индекса на ИНТЕРВАЛАХ вообще

SQL> alter table intervals drop  constraint ivl_pk drop  index
  2  /

Table altered.

SQL> exec dbms_stats.gather_table_stats(user, 'INTERVALS', cascade=>true)

PL/SQL procedure successfully completed.

SQL> select m.measure
  2         , m.ts as "TIME"
  3         , i.entry_time
  4         , i.exit_time
  5  from
  6      intervals i
  7  inner join
  8      measures m
  9      on ( m.ts between  i.entry_time and i.exit_time )
 10  order by m.ts asc
 11  /

1200001 rows selected.

Elapsed: 00:05:29.15

SQL> 

ИНТЕРВАЛ заключение

Индекс ИНТЕРВАЛОВ имеет небольшую разницу. То есть индексирование (ENTRY_TIME, EXIT_TIME) приводит к более быстрому выполнению. Это связано с тем, что оно обеспечивает быстрое полное сканирование индекса, а не полное сканирование таблицы. Это было бы более значительным, если бы временной интервал, очерченный ИНТЕРВАЛОМ, был значительно шире, чем у МЕР.

Общие выводы

Поскольку мы выполняем запросы к полной таблице, ни один из индексов существенно не изменил время выполнения. Так что если у вас есть Enterprise Edition и несколько процессоров, Parallel Query даст вам лучшие результаты. В противном случае самыми лучшими индексами будут INTERVALS (ENTRY_TIME, EXIT_TIME) и MEASURES (TS). Решение Nested Loops определенно быстрее, чем Parallel Query - см. Edit 4 ниже.

Если вы работали с подмножеством ИЗМЕРЕНИЙ (скажем, за неделю), то наличие индексов имело бы большее влияние. Вероятно, что два, которые я рекомендовал в предыдущем абзаце, останутся наиболее эффективными,

Последнее наблюдение: я запустил его на стандартном двухъядерном ноутбуке с 5 Гбайт SGA. Все же все мои запросы заняли меньше шести минут. Если ваш запрос действительно занимает час, то у вашей базы данных есть серьезные проблемы. Хотя это длительное время работы может быть артефактом наложения ИНТЕРВАЛОВ, что может привести к декартовому произведению.

** Редактировать **

Изначально я включил вывод из

SQL> set autotrace traceonly stat exp

Но увы ТАК строго усек мой пост. Таким образом, я переписал это, но без выполнения или статистики. Те, кто хочет подтвердить мои выводы, должны будут выполнить запросы к ним.

Редактировать 4 (предыдущее редактирование удалено из-за нехватки места)

С третьей попытки мне удалось воспроизвести улучшение производительности для решения Quassnoi.

SQL> set autotrace traceonly stat exp
SQL>
SQL> set timing on
SQL>
SQL> select
  2          /*+ LEADING (i) USE_NL(i, m) */
  3              m.measure
  4             , m.ts as "TIME"
  5             , i.entry_time
  6             , i.exit_time
  7  from
  8      intervals i
  9  inner join
 10      measures m
 11      on ( m.ts between  i.entry_time and i.exit_time )
 12  order by m.ts asc
 13  /

1200001 rows selected.

Elapsed: 00:00:18.39

Execution Plan
----------------------------------------------------------
Plan hash value: 974071908

---------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |           |  6003K|   257M|       |   973K  (1)| 03:14:46 |
|   1 |  SORT ORDER BY                |           |  6003K|   257M|   646M|   973K  (1)| 03:14:46 |
|   2 |   NESTED LOOPS                |           |       |       |       |            |          |
|   3 |    NESTED LOOPS               |           |  6003K|   257M|       |   905K  (1)| 03:01:06 |
|   4 |     TABLE ACCESS FULL         | INTERVALS |  2001 | 32016 |       |  2739   (1)| 00:00:33 |
|*  5 |     INDEX RANGE SCAN          | MEAS_IDX  | 60000 |       |       |   161   (1)| 00:00:02 |
|   6 |    TABLE ACCESS BY INDEX ROWID| MEASURES  |  3000 | 87000 |       |   451   (1)| 00:00:06 |
---------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - access("M"."TS">="I"."ENTRY_TIME" AND "M"."TS"<="I"."EXIT_TIME")


Statistics
----------------------------------------------------------
         66  recursive calls
          2  db block gets
      21743  consistent gets
      18175  physical reads
          0  redo size
   52171689  bytes sent via SQL*Net to client
     880416  bytes received via SQL*Net from client
      80002  SQL*Net roundtrips to/from client
          0  sorts (memory)
          1  sorts (disk)
    1200001  rows processed

SQL> 

Так что Nested Loops - определенно лучший способ.

Полезные уроки из упражнения

  1. Выполнение диагностических тестов - это гораздо больше ценнее, чем гадать и теоретизирования
  2. Понимание данных имеет решающее значение
  3. Даже с 11g мы все еще время от времени нужно использовать подсказки, чтобы подтолкнуть оптимизатор в некоторых случаях
2 голосов
/ 22 декабря 2009

попробуйте параллельный запрос

  alter session enable parallel query;

  select /*+ parallel */ ... same as before;

Вы также можете создать материализованное представление, возможно, с параллельной подсказкой выше. Создание MV может занять много времени, но после его создания к нему можно обращаться неоднократно.

2 голосов
/ 22 декабря 2009

Вы не можете по-настоящему оптимизировать ваше утверждение - оно довольно простое.

Что вы могли бы сделать, так это выяснить, не помогут ли вам какие-то индексы.

Вы выбираете на intervals.entry_time, intervals.exit_time, measures.time - индексируются ли эти столбцы?

2 голосов
/ 22 декабря 2009

Первое, что я делаю, это чтобы ваш инструмент базы данных сгенерировал план выполнения, который вы можете просмотреть (это «Control-L» в MSSQL, но я не уверен, как это сделать в Oracle), - который попытается укажите медленные части и, в зависимости от вашего сервера / редактора, он может даже порекомендовать некоторые базовые индексы. После того, как у вас есть план выполнения, вы можете искать любые сканы таблиц соединений внутренних циклов, которые очень медленны - индексы могут помочь при просмотре таблиц, и вы можете добавить дополнительные предикаты соединения, чтобы облегчить соединения циклов.

Полагаю, MEASURES нужен индекс для столбца TIME, и вы также можете включить столбец MEASURE для ускорения поиска. Попробуйте это:

CREATE INDEX idxMeasures_Time ON Measures ([Time]) INCLUDES (Measure)

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

ON measures.time BETWEEN intervals.entry_time AND intervals.exit_time

Это просто объединяет ваши два <= и> = в один оператор.

1 голос
/ 23 декабря 2009

Может быть очень эффективный способ написания этого запроса, если интервалы являются детерминированными, потому что запрос может быть преобразован в равное соединение, которое поддается более эффективному объединению хэша.

Например, если интервалы ежечасно:

ENTRY_TIME          EXIT_TIME
2000-01-15 09:00:00 2000-01-15 09:59:59
2000-01-15 10:00:00 2000-01-15 10:59:59
2000-01-15 11:00:00 2000-01-15 11:59:59
2000-01-15 12:00:00 2000-01-15 12:59:59
....

Тогда объединение можно записать как:

intervals.entry_time=trunc(measures.time,'HH')

Это уменьшит стоимость всего, вплоть до объединения, в значительной степени для полного сканирования каждой из таблиц.

Однако, поскольку у вас там есть операция ORDER BY, я думаю, что сортировка слиянием может все же превзойти ее, поскольку запрос написан прямо сейчас, потому что оптимизатор будет сортировать меньший набор данных для сортировки слиянием, чем он для хеш-соединения (потому что в последнем случае придется сортировать больше столбцов данных). Вы можете обойти это, структурировав запрос следующим образом:

select
  measures.measure     as measure,
  measures.time        as time,
  intervals.entry_time as entry_time,
  intervals.exit_time  as exit_time
from
  intervals inner join  
  (select time, measure from measures order by time) measures
  on  intervals.entry_time=trunc(measures.time,'HH')  
/

Это дает более низкую оценку стоимости, чем сортировка слиянием в моем тестовом экземпляре 10.2.0.4, но я бы посчитал это немного рискованным.

Итак, я бы искал сортировку-слияние или переписал ее, чтобы разрешить использование хеш-соединения, если это возможно.

1 голос
/ 22 декабря 2009

Ваш SQL эквивалентен:

select m.measure. m.time, 
     i.entry_time, i.exit_time
from intervals i
    join measures m
        on m.time Between i.entry_time And i.exit_time  
order by time asc

Единственное, что я могу предложить, это убедиться, что есть индекс m.Time. Затем, если это недостаточно улучшает производительность, попробуйте добавить индексы для i.Start_Time и i.End_Time, а также

0 голосов
/ 23 декабря 2009

В этом случае вы получите большую часть строк из обеих таблиц, плюс у вас есть сортировка.

Вопрос в том, действительно ли вызывающему процессу нужны все строки или только первые несколько? Это изменит способ оптимизации запроса.

Я предполагаю, что ваш вызывающий процесс хочет ВСЕ строки. Так как предикат объединения не равен равенству, я бы сказал, что MERGE JOIN может быть лучшим подходом для достижения цели. Объединение слиянием требует, чтобы его источники данных были отсортированы, поэтому, если мы можем избежать сортировки, запрос должен выполняться настолько быстро, насколько это возможно (за исключением более интересных подходов, таких как специализированные индексы или материализованные представления).

Чтобы избежать операций SORT для intervals и measures, можно добавить индексы для (measures.time, measures.measure) и (intervals.entry_time, intervals.exit_time). База данных может использовать индекс, чтобы избежать сортировки, и это будет быстрее, потому что ей не нужно посещать какие-либо блоки таблиц.

В качестве альтернативы, если у вас есть только индекс на measures.time, запрос все равно может работать нормально, без добавления другого большого индекса - он будет работать медленнее, хотя, поскольку ему, вероятно, придется читать много блоков таблицы, чтобы получить measures.measure для предложения SELECT.

0 голосов
/ 22 декабря 2009

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

Для каждой записи в таблице мер можно иметь несколько записей в таблице интервалов (intervals.entry_time<=measures.time), а для каждой записи в таблице интервалов можно иметь несколько записей в мере (measures.time <=intervals.exit_time). результирующие отношения «один ко многим» и «многие к одному», вызванные объединением, означают многократное сканирование таблицы для каждой записи. Я сомневаюсь, что декартово произведение является правильным термином, но оно довольно близко.

Индексирование определенно помогло бы, но помогло бы еще больше, если бы вы могли найти лучший ключ для объединения двух таблиц. наличие отношений «один ко многим» в одном направлении определенно ускорит обработку, поскольку не нужно будет сканировать каждую таблицу / индекс дважды для каждой записи.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...