SQL для начинающих: 10 правил построения «точных» запросов

SQL для начинающих: 10 правил построения «точных» запросов

https://t.me/ai_machinelearning_big_data


«Точный» SQL-запрос возвращает «чистые» данные в необходимом и достаточном количестве, при этом потребляет как можно меньше памяти и справляется за минимальное время. Скорость работы с базой влияет на производительность. Потребление памяти может негативно сказаться даже на безопасности. Всё это прямо и косвенно влияет на прибыль компании. В статье разберёмся, как не допускать ошибок.

Для наших целей понадобятся тестовые данные. Будем работать с базой данных Oracle Database. Примеры в статье будут приводиться на языке SQL, PL/SQL. Нам важен подход, который можно адаптировать под другую реляционную систему управления базами данных — РСУБД.

Тестовые данные

⚒ Создадим тестовую таблицу 1:

CREATE SEQUENCE TEST_DATA_1_SEQ
  NOMAXVALUE
  NOMINVALUE
  NOCYCLE
/

CREATE TABLE TEST_DATA_1
(
  TEST_DATA_1_ID NUMBER DEFAULT TEST_DATA_1_SEQ.NEXTVAL NOT NULL
  ,TYPE          VARCHAR2(64) NOT NULL
  ,VALUE         VARCHAR2(128) NOT NULL
  ,PC_USR        VARCHAR2(30) DEFAULT USER NOT NULL
  ,PC_DT         TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL
)
/

ALTER TABLE TEST_DATA_1
  ADD CONSTRAINT TEST_DATA_1_PK PRIMARY KEY (TEST_DATA_1_ID)
  USING INDEX
/

ALTER TABLE TEST_DATA_1
  ADD CONSTRAINT TEST_DATA_1_TYPE_CHK CHECK (TYPE in ('CITY', 'DATE', 'EMPLOYEE', 'STOCK MARKET'))
/

CREATE UNIQUE INDEX TEST_DATA_1_UIDX1 ON TEST_DATA_1 (VALUE)
/

COMMENT ON TABLE TEST_DATA_1 IS 'Тестовые данные 1'
/


⚒ Заполним тестовую таблицу 1 данными:

/* Добавление данных */
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('CITY', 'МОСКВА');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('CITY', 'САНКТ-ПЕТЕРБУРГ');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('EMPLOYEE', 'СОТРУДНИК 1. ПОЛ М. ВОЗРАСТ 18');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('EMPLOYEE', 'СОТРУДНИК 2. ПОЛ Ж. ВОЗРАСТ 19');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('EMPLOYEE', 'СОТРУДНИК 3. ПОЛ Ж. ВОЗРАСТ 20');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('DATE', '01 января 2000');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('DATE', '02 января 2000');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('DATE', '01.01.2001');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('DATE', '02.01.2001');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('DATE', '03.01.2001');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('DATE', '04.01.2001');

/* Извлечение всех  данных */
SELECT t1.*
  FROM TEST_DATA_1 t1;



/* Удаление всех данных без проверок */
TRUNCATE TABLE TEST_DATA_1;


⚒ Создадим тестовую таблицу 2:

CREATE SEQUENCE TEST_DATA_2_SEQ
  NOMAXVALUE
  NOMINVALUE
  NOCYCLE
/

CREATE TABLE TEST_DATA_2
(
  TEST_DATA_2_ID NUMBER DEFAULT TEST_DATA_2_SEQ.NEXTVAL NOT NULL
  ,TEST_DATA_1_ID NUMBER NOT NULL
  ,TYPE           VARCHAR2(64) NOT NULL
  ,VALUE          VARCHAR2(128) NOT NULL
  ,PC_USR         VARCHAR2(30) DEFAULT USER NOT NULL
  ,PC_DT          TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL
)
/

ALTER TABLE TEST_DATA_2
  ADD CONSTRAINT TEST_DATA_2_PK PRIMARY KEY (TEST_DATA_2_ID)
  USING INDEX
/

ALTER TABLE TEST_DATA_2
  ADD CONSTRAINT TEST_DATA_2_TYPE_CHK CHECK (TYPE in ('STREET', 'DATE', 'EMPLOYEE', 'STOCK MARKET'))
/

CREATE UNIQUE INDEX TEST_DATA_2_UIDX1 ON TEST_DATA_2 (TEST_DATA_1_ID, VALUE)
/

COMMENT ON TABLE TEST_DATA_2 IS 'Тестовые данные 2'
/


⚒ Заполним тестовую таблицу 2 данными:

/* Добавление данных */
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (1 /*ID МОСКВА*/, 'STREET', 'УЛИЦА КАРЛА МАРКСА');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (1 /*ID МОСКВА*/, 'STREET', 'УЛИЦА КРУПСКОЙ');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (1 /*ID МОСКВА*/, 'STREET', 'МАЛЫЙ ПОЛУЯРОСЛАВСКИЙ ПЕРЕУЛОК');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (2 /*ID САНКТ-ПЕТЕРБУРГ*/, 'STREET', 'УЛИЦА КАРЛА МАРКСА');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (2 /*ID САНКТ-ПЕТЕРБУРГ*/, 'STREET', 'УЛИЦА КРУПСКОЙ');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (3 /*ID СОТРУДНИК 1*/, 'EMPLOYEE', 'ПРОЖИВАЕТ В ГОРОДЕ МОСКВА');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (4 /*ID СОТРУДНИК 2*/,'EMPLOYEE', 'ПРОЖИВАЕТ В ГОРОДЕ МОСКВА');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (5 /*ID СОТРУДНИК 3*/,'EMPLOYEE', 'ПРОЖИВАЕТ В  ГОРОДЕ САНКТ-ПЕТЕРБУРГ');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (6 /*ID 01 января 2000*/, 'DATE', 'Формат день числом, месяц словом, год числом');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (7 /*ID 02 января 2000*/, 'DATE', 'Формат день числом, месяц словом, год числом');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (8 /*ID 01.01.2001*/, 'DATE', 'Формат день числом, месяц числом, год числом');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (9 /*ID 02.01.2001*/, 'DATE', 'Формат день числом, месяц числом, год числом');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (10 /*ID 03.01.2001*/, 'DATE', 'Формат день числом, месяц числом, год числом');
INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (11 /*ID 04.01.2001*/, 'DATE', 'Формат день числом, месяц числом, год числом');

/* Извлечение всех данных */
SELECT t2.*
  FROM TEST_DATA_2 t2;



/* Удаление всех данных без проверок */
TRUNCATE TABLE TEST_DATA_2;


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

Допустим, есть таблица с некоторым количество колонок. К ней можно обратиться двумя разными способами:

⚠️ Опасный подход:

SELECT TYPE
      ,VALUE
  FROM TEST_DATA_1;


✅ Безопасный подход заключается в обращении через псевдоним:

SELECT t1.TYPE AS TYPE
      ,t1.VALUE AS VALUE
  FROM TEST_DATA_1 t1;


Псевдоним (анг. Alias) — это имя, назначенное источнику данных в SQL-запросе при использовании выражения в качестве источника данных или для упрощения ввода и прочтения инструкции SQL. Это полезно, если имя источника слишком длинное или его трудно вводить.

Псевдонимы можно использовать для переименования таблиц и колонок. В отличие от настоящих имён, они могут не соответствовать ограничениям базы данных и содержать до 255 знаков (включая пробелы, цифры и специальные символы).

В случае извлечения данных из одной таблицы без псевдонимов можно обойтись. Рисков нет. Синтаксический анализатор базы данных однозначно знает, данные из какой колонки таблицы запрашиваются. Но рекомендуется всё же использовать их — чтобы выработать привычку.

В случае извлечения данных из нескольких таблиц отказ от использования псевдонимов увеличивает риск получения некорректного результата. Допустим, что у таблиц есть колонки с одинаковым именем. Когда данные извлекаются и SQL-запрос звучит как: «Получаю записи из таблиц колонку А», то о какой колонке «А» идёт речь: из первой или второй таблицы? Если для таблицы назначен псевдоним, то SQL-запрос может звучать уже так: «Получаю записи из таблицы Т1 колонку А».

К SQL-запросу, возможно, придётся вернуться через какое-то время, чтобы внести в него изменения. В таких случаях подсказки в виде псевдонима (alias) помогут определить нужную колонку. Практически со стопроцентной уверенностью будет понятно, из какой таблицы что извлекали.

⚠️ Опасный подход:

SELECT TEST_DATA_1.TYPE
      ,TEST_DATA_1.VALUE
      ,TEST_DATA_2.TYPE
      ,TEST_DATA_2.VALUE
  FROM TEST_DATA_1
      ,TEST_DATA_2
WHERE TEST_DATA_1.TEST_DATA_1_ID = TEST_DATA_2.TEST_DATA_1_ID;



✅ Безопасный подход заключается в обращении через псевдоним:

SELECT t1.TYPE AS TYPE_1 /* Колонка TYPE из таблицы TEST_DATA_1 */
      ,t1.VALUE AS VALUE_1 /* Колонка VALUE из таблицы TEST_DATA_1 */
      ,t2.TYPE AS TYPE_2 /* Колонка TYPE из таблицы TEST_DATA_2 */
      ,t2.VALUE AS VALUE_2 /* Колонка VALUE из таблицы TEST_DATA_2 */
 FROM TEST_DATA_1 t1
     ,TEST_DATA_2 t2
WHERE t1.TEST_DATA_1_ID = t2.TEST_DATA_1_ID;



2. Извлекай только те данные, которые планируешь использовать

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

Рассмотрим пример «Карточка сотрудника». У нас есть таблица «Сотрудник» с колонками ФИО, пол, возраст. Данные из них извлекаются и выводятся на форму «Карточка сотрудника». SQL-запрос можно написать следующим образом: «Извлекаю все колонки из таблицы по указанному сотруднику». В таком случае извлекаются все колонки.

⚠️ Опасный подход заключается в извлечении всех данных:

SELECT t1.*
  FROM TEST_DATA_1 t1
 WHERE t1.TEST_DATA_1_ID = 3 /* ID EMPLOYEE = СОТРУДНИК 1 */;



В будущем могут появиться дополнительные колонки в базе данных — например, описание должностных обязанностей или адрес проживания — в рамках нового информационного потока использования базы данных. То есть вне «Карточки сотрудника».

/* Добавление новой колонки в таблицу */
ALTER TABLE TEST_DATA_1
  ADD DESCRIPTION VARCHAR2(4000)
/

/* Обновление данных в таблице */
UPDATE TEST_DATA_1 t1
   SET T1.DESCRIPTION = 'ДОПОЛНИТЕЛЬНЫЕ ДАННЫЕ ПО СОТРУДНИКУ 1'
 WHERE t1.TEST_DATA_1_ID = 3 /* ID EMPLOYEE = СОТРУДНИК 1 */;

/* Извлечение данных из таблицы */
SELECT t1.*
  FROM TEST_DATA_1 t1
 WHERE t1.TEST_DATA_1_ID = 3 /* ID EMPLOYEE = СОТРУДНИК 1 */;



В результате данные по новым полям заполняются уже не только формой «Карточки сотрудника». И SQL-запрос получения информации для формы начинает работать медленнее. Причина в том, что приходится извлекать данные из большего количества колонок.

Деградация скорости получения данных может происходить постепенно или резко — но в самый неподходящий момент. Зачастую это связано с тем, что поля свободного ввода данных могут быть большими. То есть база данных должна больше информации подгрузить в память и потом отдать клиенту, приложение которого не готово к такому потоку данных.

Рассмотрим пример «Телефон». На телефоне пользователя установлено приложение. Сам телефон старый. Пользователь не выполнял обновления программного обеспечения (ПО), но замечает, что с какого-то момента времени приложение начало работать медленнее. У другого пользователя на новом телефоне то же приложение работает быстро. Ошибка «плавающая», но для разработчика неприятная.

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

Чтобы таких неожиданностей не возникало, нужно извлекать строго те данные, которые требуется использовать и показывать на форме. В данном случае нужно было написать: «Извлекаю колонки ФИО, возраст, пол из таблички сотрудника, с фильтрацией по сотруднику».

✅ Безопасный подход заключается в получении нужных данных:

SELECT t1.TYPE AS TYPE_1 /* Колонка TYPE из таблицы TEST_DATA_1 */
      ,t1.VALUE AS VALUE_1 /* Колонка VALUE из таблицы TEST_DATA_1 */
  FROM TEST_DATA_1 t1
WHERE t1.TEST_DATA_1_ID = 3 /* ID EMPLOYEE = СОТРУДНИК 1 */;



3. По максимуму используй данные, которые извлёк из таблицы

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

После обращения к таблице Table1, нужно постараться написать SQL-запрос так, чтобы не пришлось извлекать данные из неё несколько раз. Это не всегда возможно, но попытаться стоит.

⚠️ Опасный подход:

SELECT t1.TYPE AS TYPE_1
      ,t1.VALUE AS VALUE_1
      ,t2.VALUE AS VALUE_2
  FROM TEST_DATA_1 t1
      ,TEST_DATA_2 T2
WHERE T1.TEST_DATA_1_ID = T2.TEST_DATA_1_ID
  AND t2.VALUE = 'ПРОЖИВАЕТ В ГОРОДЕ МОСКВА'
UNION ALL
SELECT t1.TYPE AS TYPE_1
      ,t1.VALUE AS VALUE_1
      ,t2.VALUE AS VALUE_2
  FROM TEST_DATA_1 t1
      ,TEST_DATA_2 T2
WHERE T1.TEST_DATA_1_ID = T2.TEST_DATA_1_ID
  AND t2.VALUE = 'ПРОЖИВАЕТ В ГОРОДЕ САНКТ-ПЕТЕРБУРГ'
ORDER BY VALUE_1;



/* План запроса */



✅ Безопасный подход заключается в использовании полученных данных максимально продуктивно:

SELECT t1.TYPE AS TYPE_1
      ,t1.VALUE AS VALUE_1
      ,t2.VALUE AS VALUE_2
  FROM TEST_DATA_1 t1
      ,TEST_DATA_2 T2
WHERE T1.TEST_DATA_1_ID = T2.TEST_DATA_1_ID
  AND t2.VALUE IN ('ПРОЖИВАЕТ В ГОРОДЕ МОСКВА', 'ПРОЖИВАЕТ В ГОРОДЕ САНКТ-ПЕТЕРБУРГ')
ORDER BY VALUE_1;



/* План запроса */



Неоптимальный SQL-запрос может выполняться дольше, уронить инфраструктуру и даже повлиять на безопасность системы.

⚒ Рассмотрим тестовый пример:

/*
 * Тестовый пример
 * Каждый случай запроса выполняется 1 000 000 раз в “холостую”
 */

declare
  start_time pls_integer;
  end_time   pls_integer;
begin
  /* 1 Случай */
  start_time := dbms_utility.get_time;

  for indx in 1 .. 1000000 loop
     for cur in (select t1.TYPE as TYPE_1
                    ,t1.VALUE as VALUE_1
                    ,t2.VALUE as VALUE_2
                from TEST_DATA_1 t1
                    ,TEST_DATA_2 T2
               where T1.TEST_DATA_1_ID = T2.TEST_DATA_1_ID
                 and t2.VALUE = 'ПРОЖИВАЕТ В ГОРОДЕ МОСКВА'
               union all
               select t1.TYPE as TYPE_1
                    ,t1.VALUE as VALUE_1
                    ,t2.VALUE as VALUE_2
                from TEST_DATA_1 t1
                    ,TEST_DATA_2 T2
               where T1.TEST_DATA_1_ID = T2.TEST_DATA_1_ID
                 and t2.VALUE = 'ПРОЖИВАЕТ В ГОРОДЕ САНКТ-ПЕТЕРБУРГ'
                order by VALUE_1) loop
      null;
    end loop;
  end loop;

  end_time := dbms_utility.get_time;
  dbms_output.put_line('execution time 1 --> ' || (end_time - start_time) / 100 || ' sec');

  /* 2 Случай */
  start_time := dbms_utility.get_time;

  for indx in 1 .. 1000000 loop
    for cur in (select t1.TYPE as TYPE_1
                   ,t1.VALUE as VALUE_1
                   ,t2.VALUE as VALUE_2
               from TEST_DATA_1 t1
                   ,TEST_DATA_2 T2
              where T1.TEST_DATA_1_ID = T2.TEST_DATA_1_ID
                and t2.VALUE in ('ПРОЖИВАЕТ В ГОРОДЕ МОСКВА', 'ПРОЖИВАЕТ В ГОРОДЕ САНКТ-ПЕТЕРБУРГ')
               order by VALUE_1) loop
      null;
    end loop;
  end loop;

  end_time := dbms_utility.get_time;
  dbms_output.put_line('execution time 2 --> ' || (end_time - start_time) / 100 || ' sec');
end;
/

/*
 * Результат выполнения
 * Важно не время, которое зависит от ресурсов на ПК, а разница выполнения
 */
/*
Done in 64,516 seconds
 */
execution time 1 --> 46.83 sec
execution time 2 --> 17.67 sec


Рассмотрим пример «Работа ЦОД». Есть Центр Обработки Данных (ЦОД). В нём, на одном из ресурсов внутри приложения, выполняется некий SQL-запрос, который постепенно использует всю доступную память без ограничений. И приложениям, которые стоят на том же ресурсе, со временем перестаёт хватать памяти на стабильную работу. Это может привести к их падению.

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


4. Проверяй запросы SQL на индексы

SQL-запросы бывают простые и сложные. Иногда извлекается мало данных, иногда — много. Если таблица большая, и в ней очень разнообразные данные, то в зависимости от того, как обращаться к этим данным, использовать индекс или нет, можно потерять время.

Рассмотрим пример «Брокерская биржа». В рамках отдельного процесса извлекаются данные для покупки-продажи акций. Используя оптимизированный SQL-запрос, можно быстро получать информацию, по какой цене торгуется каждая акция. И делать прогноз — покупать или продавать.

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

Индексы — это инструмент оптимизации извлечения данных. Конечно, это не панацея, и если таблица маленькая, по ней проще пройти прямым перебором и получить данные.

Добавим в тестовую таблицу 1 новые данные:

/* Добавление новых данных */
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('STOCK MARKET', 'АКЦИЯ 1. СТОИМОСТЬ 101 РУБ');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('STOCK MARKET', 'АКЦИЯ 2. СТОИМОСТЬ 102 РУБ');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('STOCK MARKET', 'АКЦИЯ 3. СТОИМОСТЬ 103 РУБ');
INSERT INTO TEST_DATA_1 (TYPE, VALUE) VALUES ('STOCK MARKET', 'АКЦИЯ 4. СТОИМОСТЬ 104 РУБ');


⚠️ Опасный подход заключается в игнорировании использования индексов:

/* Извлечение всех данных TYPE = STOCK MARKET */
SELECT t1.TYPE AS TYPE_1
      ,t1.VALUE AS VALUE_1
  FROM TEST_DATA_1 t1
WHERE t1.TYPE = 'STOCK MARKET';

/* План запроса */



Добавим в тестовую таблицу 1 новый индекс

/* Добавление нового индекса */
CREATE INDEX TEST_DATA_1_IDX1 ON TEST_DATA_1 (TYPE)
/


✅ Безопасный подход заключается в использовании индексов:

/* Извлечение всех данных TYPE = STOCK MARKET */
SELECT t1.TYPE AS TYPE_1
      ,t1.VALUE AS VALUE_1
  FROM TEST_DATA_1 t1
WHERE t1.TYPE = 'STOCK MARKET';



/* План запроса */



Рассмотрим пример «Доставка почты». Показательный пример работы индексов — доставка почты из точки А в одном городе, в точку Б в другом. Зная, куда конкретно нужно доставить посылку, мы можем идти по индексам и определить, где и когда повернуть, чтобы довезти посылку за максимально короткое время. Если везти посылку на машине, то это сокращает расход топлива — а значит, и материальные издержки на доставку.

В противном случае можно сворачивать не там, спрашивать дорогу у прохожих, которые знают её плохо. И, вместо того чтобы доставить посылку за Время Т1, опоздать на Время Т2. В итоге покупатель ждёт, а продавец теряет деньги.

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


5. Начинай запрос SQL с таблицы с меньшим набором записей

Допустим, нам нужно соединить две таблицы: с маленьким количеством записей и с большим. Стоит сделать следующее:

  • начинать извлечение данных из таблицы с меньшим набором данных;
  • продолжать извлечение данных из таблицы с большим набором данных.

⚒ Добавим в тестовую таблицу 2 новые данные:

/* Добавление новых данных */
declare
  l_type       test_data_2.type%type := 'STOCK MARKET';
  l_value_2 test_data_2.value%type := '';
  l_sql          varchar2(128) := '';
begin
  /* Извлечение данных из тестовой таблицы 1 */
  for cur_t1 in (select t1.test_data_1_id as test_data_1_id
                       ,t1.type as type_1
                       ,t1.value as value_1
                   from TEST_DATA_1 t1
                  where type = l_type) loop
    /* Цикл до 1 000 000  на каждую полученную запись из тестовой таблицы 1 */
    for indx in 1 .. 1000000 loop
      l_value_2 := cur_t1.value_1 || '. ' || 'ЗАПИСЬ ' || indx;
      l_sql     := 'INSERT INTO TEST_DATA_2 (TEST_DATA_1_ID, TYPE, VALUE) VALUES (' || --
                cur_t1.TEST_DATA_1_ID || ', ' || --
                '''' || l_type || ''', ' || --
                '''' || l_value_2 || ''')';
      /* Выполнение динамического запроса */
      execute immediate l_sql;
    end loop;
  end loop;
end;
/

/* Общее число записей в таблице TEST_DATA_1 */
SELECT COUNT(1) AS CNT
  FROM  TEST_DATA_1
/



/* Общее число записей в таблице TEST_DATA_2 */
SELECT COUNT(1) AS CNT
  FROM TEST_DATA_2
/



/* Добавление нового индекса для таблицы TEST_DATA_1 */
CREATE INDEX TEST_DATA_1_IDX2 ON TEST_DATA_1 (TEST_DATA_1_ID, TYPE)
/

/* Добавление нового индекса для таблицы TEST_DATA_2 */
CREATE INDEX TEST_DATA_2_IDX1 ON TEST_DATA_2 (TEST_DATA_1_ID, TYPE)
/
CREATE INDEX TEST_DATA_2_IDX2 ON TEST_DATA_2 (TEST_DATA_1_ID)
/
CREATE INDEX TEST_DATA_2_IDX3 ON TEST_DATA_2 (TYPE)
/

/* Сбор статистики после добавления данных */
declare
  l_user varchar2(30 char) := user;
begin
  /* Для таблицы TEST_DATA_1 */
  DBMS_STATS.GATHER_TABLE_STATS(ownname => l_user --
                               ,tabname => 'TEST_DATA_1'
                               ,cascade => true);

  /* Для таблицы TEST_DATA_2 */
  DBMS_STATS.GATHER_TABLE_STATS(ownname => l_user --
                               ,tabname => 'TEST_DATA_2'
                               ,cascade => true);
end;
/


Если поступить наоборот, то мы потеряем время, потому что перебирать данные из большей таблицы дольше.

⚒ Рассмотрим тестовый пример:

/*
 * Тестовый пример
 * Каждый случай запроса выполняется 100 раз в “холостую”
 * Запросы усложнены и их можно упростить, добиваясь большей производительности и схожего результата
 * Попробуйте поэкспериментировать
 */

declare
  start_time pls_integer;
  end_time   pls_integer;
begin
  /* 1 Случай. От большего к меньшему */
  start_time := dbms_utility.get_time;

  for indx in 1 .. 100 loop
    for cur in (select t1.type as type_1
                      ,t1.value as value_1
                      ,t2_.type_2 as type_2
                      ,t2_.value_2_min as value_2_min
                      ,t2_.value_2_max as value_2_max
                      ,t2_.value_2_cnt as value_2_cnt
               from (select t2.TEST_DATA_1_ID as TEST_DATA_1_ID
                           ,t2.TYPE as TYPE_2
                           ,min(t2.VALUE) as VALUE_2_MIN
                           ,max(t2.VALUE) as VALUE_2_MAX
                           ,count(t2.VALUE) as VALUE_2_CNT
                      from TEST_DATA_2 t2
                     where t2.type = 'STOCK MARKET'
                     group by t2.TEST_DATA_1_ID
                             ,t2.TYPE
                     order by t2.TEST_DATA_1_ID) t2_
                join TEST_DATA_1 t1
                  on t1.TEST_DATA_1_ID = t2_.TEST_DATA_1_ID
                 and t1.type = 'STOCK MARKET'
               order by t1.value) loop
      null;
    end loop;
  end loop;end_time := dbms_utility.get_time;
  dbms_output.put_line('execution time 1 --> ' || (end_time - start_time) / 100 || ' sec');

  /* 2 Случай. От меньшего к большему */
  start_time := dbms_utility.get_time;

  for indx in 1 .. 100 loop
    for cur in (select t1_.type_1 as type_1
                     ,t1_.value_1 as value_1
                     ,t2.type as type_2
                     ,min(t2.value) as value_2_min
                     ,max(t2.value) as value_2_max
                     ,count(t2.value) as value_2_cnt
                from (select t1.TEST_DATA_1_ID as TEST_DATA_1_ID
                            ,t1.TYPE as TYPE_1
                            ,t1.VALUE as VALUE_1
                        from TEST_DATA_1 t1
                       where t1.TYPE = 'STOCK MARKET'
                       order by t1.value) t1_
                 join TEST_DATA_2 t2
                   on t2.TEST_DATA_1_ID = t1_.TEST_DATA_1_ID
                  and t2.TYPE = 'STOCK MARKET'
                group by t1_.type_1
                        ,t1_.value_1
                        ,t2.type) loop
      null;
    end loop;
  end loop;

  end_time := dbms_utility.get_time;
  dbms_output.put_line('execution time 2 --> ' || (end_time - start_time) / 100 || ' sec');
end;
/

/*
 * Результат выполнения
 * Важно не время, которое зависит от ресурсов на ПК, а разница выполнения
 * Каждый вариант возвращает одинаковые данные
 */



/* Executed in 269,203 seconds */

execution time 1 --> 149.49 sec
execution time 2 --> 119.68 sec


Рассмотрим пример «Очередь клиентов». Есть поток клиентов, каждого из которых нужно обслужить. Операторы, заполняя форму «Анкета» задают серию вопросов. Один из них, влияет на дальнейший ход общения: «Вам исполнилось 18 лет?». Если клиент отвечает нет, то оператор прекращает общение, иначе продолжает задавать вопросы.

Если оператор задаст вопрос про возраст в конце общения, то любой потенциальный клиент должен будет заполнить всю анкету, даже если в этом нет смысла. Рациональный подход в общении с клиентами помогает операторам за одно и то же время обслужить большее число клиентов. С базами данных всё так же.

Часть 2

Источник


Report Page