Страница 1 из 1

И опять скорость выполнения запросов...

Добавлено: 06 авг 2007, 09:13
AnryGTR
Здравствуйте уважаемые гуру!!!

Опять у меня проблема со скоростью выполнения запросов :-(
Вначале опишу таблицы:

Отделы (id,название,...)
Услуги (id,id_отдела, наименование,...)
Прайсы (id_услуги, сумма, дата_начала, дата_конца)
Персонал(id,id_отдела,ФИО,...)
Визиты(id,оплата,id_услуги,id_клиента,дата,скидка,возврат_денег,тип_оплаты,выполнение,id_выполнившего,...)

И есть представление - ВИЗИТ_ВСЁ, которое из вышеописанных таблиц уже собирает таблицу вида:
ID_визита,ID_отдела,Наименование_Отдела,ID_услуги,Наименование_услуги,ID_выполнившего_заказ,ФИО_выполнившего заказ, Дата_визита,Цена_соответствующая_дате_визита,скидка,тип_оплаты,возврат_денег,выполнение,оплата,...

Так вот пишу следующий запрос:

Код: Выделить всё

select o.id,min(o.nam_otd) as nam_otd,
 count(a.id),   //кол-во заказов отдела за указанный период
 count(a_1.id), //кол-во оплаченных заказов
 count(a_2.id), //кол-во выполненных заказов
 count(a_3.id), //кол-во заказов со скидкой
 count(a_4.id), //кол-во заказов с возвратом денег (т.е. клиент отказался от заказа - забрал деньги) 
 sum(a_5.price - a_5.price/100*a_5.skidka), //общая поступившая сумма заказов, учитывая скидки
 count(distinct(a_6.kod_pac)), //кол-во клиентов отдела
 sum(a_7.price - a_7.price/100*a_7.skidka), //общая поступившая сумма заказов наличными
 sum(a_8.price - a_8.price/100*a_8.skidka), //общая поступившая сумма заказов перечислениями
 count(distinct(a_9.kod_pac)) //кол-во клиентов оплативших хотя бы один заказ отдела 
 from ОТДЕЛЫ o left join ВИЗИТ_ВСЁ a 
 on(a.код_отела = o.id and a.дата>='+d1+' and a.дата<='+d2+')
 left join ВИЗИТЫ a_1 
 on(a.id = a_1.id and a_1.оплата = 1)
 left join ВИЗИТ_ВСЁ a_5 
 on(a_1.id = a_5.id)
 left join ВИЗИТ_ВСЁ a_7 
 on(a_1.id = a_7.id and a_7.оплата = 1 and a_7.тип_оплаты=0)
 left join ВИЗИТ_ВСЁ a_8 
 on(a_1.id = a_8.id and a_8.оплата = 1 and a_8.тип_оплаты=1)
 left join ВИЗИТЫ a_2 
 on(a_1.id = a_2.id and a_2.выполнение = 1)
 left join ВИЗИТЫ a_3 
 on(a_1.id = a_3.id and a_3.скидка<>0)
 left join ВИЗИТЫ a_4 
 on(a.id = a_4.id and a_4.возврат_денег = 1)
 left join ВИЗИТЫ a_6 
 on(a.id = a_6.id)
 left join ВИЗИТЫ a_9 
 on(a_1.id = a_9.id)
 where o.typ=1 group by o.id


Этот запрос при обработке данных за 1 месяц "думает" порядка 15-18 сек. Не впечатляет...:-(
А представьте отчёт за один год - тут и сервак подвесить не долго!
Я конечно понимаю, что тут большую часть тормозов создаёт представление...но по-другому переписать этот запрос, не используя представления, пока что знаний наверное не хватает, хотя может быть и сам запрос коряво написан.
Самое странное, что тот же самый запрос только не по отделам, а по услугам работает в разы быстрее, тот же случай с расчётом данных за 1 месяц "думает" порядка 4-5 сек! Хотя в том запросе первичный LEFT делается не с представлением,а с таблицей ВИЗИТЫ, отсюда наверное и выигрыш в скорости...

Пожалуйста, люди добрые помогите советом - что тут не так?[/b]

Добавлено: 06 авг 2007, 10:48
WildSery
У тебя вроде FB1.5 был? Воспользуйся оператором CASE :wink:

Добавлено: 06 авг 2007, 10:51
AnryGTR
В смысле? Где и как его применить? :roll:

Добавлено: 06 авг 2007, 10:58
WildSery
У тебя собственно из одной и той же таблицы выбираются данные ограниченные разными условиями.
Ты можешь не накладывать эти условия в соединении, а выбрать всё, и уже потом при подсчёте распределять по условиям.
Например, вот это

Код: Выделить всё

select o.id, min(o.nam_otd) as nam_otd,
       count(a.id),   -- кол-во заказов отдела за указанный период
       count(a_1.id)  -- кол-во оплаченных заказов
  from ОТДЕЛЫ o
       left join ВИЗИТ_ВСЁ a on(a.код_отела = o.id and a.дата>=:d1 and a.дата<=:d2)
       left join ВИЗИТЫ a_1 on(a.id = a_1.id and a_1.оплата = 1)
  where o.typ=1 group by o.id
Заменяем на

Код: Выделить всё

select o.id, min(o.nam_otd) as nam_otd,
       count(a.id),   -- кол-во заказов отдела за указанный период
       count(case when a.оплата=1 then 1 else null end)  -- кол-во оплаченных заказов
  from ОТДЕЛЫ o
       left join ВИЗИТ_ВСЁ a on(a.код_отела = o.id and a.дата>=:d1 and a.дата<=:d2)
  where o.typ=1 group by o.id
Я не вчитывался в твой запрос, но там всё можно одним соединением сделать вроде.

Добавлено: 06 авг 2007, 11:00
AnryGTR
OK, понятно. ща попробую! 8)

Добавлено: 06 авг 2007, 11:39
AnryGTR
Сделал как ты сказал. Проверил скорость выполнения за период в 1,5 месяца:
старый отчёт - 23-24 сек.
отчёт с применением CASE - 15-16 сек.

Уже быстрее, но всё равно по-моему долговато... :?
WildSery спасибо за рекомендацию! :wink:

Добавлено: 06 авг 2007, 12:05
WildSery
Покажи что получилось. Посмотрим чего ещё тебе посоветовать.
И вьюху тоже покажи как строишь.

Добавлено: 06 авг 2007, 13:37
AnryGTR
А получилось вот что:

Код: Выделить всё

  select o.id,min(o.nam_otd) as nam_otd,
  count(a.id),
  count(case when a.оплата=1 then 1 else null end),
  count(case when a.выполнение=1 then 1 else null end),
  count(case when a.скидка<>0 then 1 else null end),
  count(case when a.вазврат_денег=1 then 1 else null end),
  sum(case when a.оплата=1 then a.сумма - a.сумма/100*a.скидка else null end),
  count(distinct(a.id_клиента)),
  sum(case when a.тип_оплаты=0 and a.оплата=1 then a.сумма - a.сумма/100*a.скидка else null end),
  sum(case when a.тип_оплаты=1 and a.оплата=1 then a.сумма - a.сумма/100*a.скидка else null end), 
  count(distinct(case when a.оплата=1 then a.id_клиента else null end))
  from ОТДЕЛЫ o left join ВИЗИТ_ВСЁ a 
  on(a.id_отдела = o.id and a.дата>='+d1+' and a.дата<='+d2+')
  where o.typ=1 group by o.id
Вьюху в БД создал так:

Код: Выделить всё

CREATE VIEW ВИЗИТ_ВСЁ (
  ID,
  Дата,
  Время,
  Дата_время,
  Оплата,
  Сумма,
  Скидка,
  Тип оплаты,
  Возврат_денег,
  Выполнение,
  ID_Клиента,
  ФИО_клиента,
  ID_города,
  Наименование_города,
  ID_района,
  Наименование_района,
  Место_работы,
  ID_услуги,
  Наименование_услуги,
  Пояснение, //Блоб-поле
  ID_отдела,
  Наименование_отдела,
  ID_отдела_принявшего_заказ,
  ID_принявшего_заказ_юзера,
  ФИО_принявшего_заказ,
  ID_исполняющего_заказ,
  ФИО_исполняющего_заказ,
  ID_прибора,
  Наименование_прибора
)
AS
SELECT
       ВИЗИТЫ.ID,
       ВИЗИТЫ.дата,
       ВИЗИТЫ.время,
       ВИЗИТЫ.дата_время,
       ВИЗИТЫ.оплата,
       Прайсы.сумма,
       ВИЗИТЫ.скидка,
       ВИЗИТЫ.тип_оплаты,
       ВИЗИТЫ.возврат_денег,
       ВИЗИТЫ.выполнение,
       ВИЗИТЫ.ID_клиента,
       КЛИЕНТЫ.ФИО,
       КЛИЕНТЫ.id_города,
       ГОРОДА.наименование,
       КЛИЕНЫ.id_района,
       РАЙОНЫ.наименование,
       РАБОТА.Наименование,
       УСЛУГИ.id,
       УСЛУГИ.наименование,
       ВИЗИТЫ.пояснение,//Блоб-поле
       ОТДЕЛЫ.ID,
       ОТДЕЛЫ.наименование,
       ПЕРСОНАЛ.ID_Отдела,
       ВИЗИТЫ.Id_принявшего_заказ,
       ПЕРСОНАЛ.ФИО,
       ВИЗИТЫ.ID_исполняющего_заказ,
       (select ПЕРСОНАЛ.ФИО from ПЕРСОНАЛ where ВИЗИТЫ.id_исполняющего_заказ = ПЕРСОНАЛ.id),
       ПРИБОРЫ.id,
       ПРИБОРЫ.Наименование
FROM ВИЗИТЫ
   INNER JOIN КЛИЕНТЫ ON (ВИЗИТЫ.ID_клиена = КЛИЕНТЫ.ID)
   INNER JOIN ГОРОДА ON (КЛИЕНЫ.iD_города = ГОРОДА.ID)
   INNER JOIN РАЙОНЫ ON (КЛИЕНТЫ.iD_района = РАЙОНЫ.ID)
   INNER JOIN УСЛУГИ ON (ВИЗИТЫ.ID_услуги = УСЛУГИ.ID)
   INNER JOIN РАБОТА ON (КЛИЕНТЫ.iD_работы = РАБОТА.ID)
   INNER JOIN ОТДЕЛЫ ON (УСЛУГИ.ID_отдела = ОТДЕЛЫ.ID)
   INNER JOIN ПРАЙСЫ ON (ПРАЙСЫ.ID_услуги = УСЛУГИ.ID AND (ВИЗИТЫ.дата_время>=ПРАЙСЫ.дата_нач AND ВИЗИТЫ.дата_время<=ПРАЙСЫ.дата_кон))
   INNER JOIN ПЕРСОНАЛ ON (ВИЗИТЫ.ID_принявшего_заказ = ПЕРСОНАЛ.ID)
   INNER JOIN ПРИБОРЫ ON (УСЛУГИ.id_прибора = ПРИБОРЫ.ID)
;

Добавлено: 06 авг 2007, 14:38
WildSery
Ты не запарился ещё на русский поля переводить? Или они у тебя так и называются :shock:
План обоих запросов ещё покажи.

Добавлено: 06 авг 2007, 14:40
AnryGTR
На русский перевёл чтобы понятнее структура была... :lol:

Оба запроса у меня без планов... :?

Добавлено: 06 авг 2007, 15:53
WildSery
AnryGTR писал(а):Оба запроса у меня без планов... :?
:) покажи планы, которые строит сервер на эти запросы.
Посмотреть их можно, отпрепарив например в IBExpert, нажав не F9 (выполнить), а Ctrl+F9 (Prepare), и внизу там будет как раз что надо.

Добавлено: 06 авг 2007, 15:54
AnryGTR
А у меня его нет, а в блэйзтопе есть такая фишка?

Добавлено: 06 авг 2007, 17:23
AnryGTR
Набираю в IBExpert тот запрос, который я привёл выше (через CASE) и вот что вижу:

Код: Выделить всё

Plan:
PLAN SORT (JOIN (O NATURAL,JOIN (A ВИЗИТЫ NATURAL,A ПЕРСОНАЛ INDEX (PK_ID_USERS),A КЛИЕНТЫ INDEX (PK_KOD),
A РАБОТА INDEX (PK_ID_WRK),A ГОРОДА INDEX (PK_ID_CITY),A РАЙОНЫ INDEX (PK_ID_RAJ),A УСЛУГИ INDEX (PK_ID_USLUGA),A ПРИБОРЫ INDEX (PK_ID_PRIB),A ОТДЕЛЫ INDEX (PK_ID_OTDEL),A ПРАЙСЫ INDEX (FK_ID_USL))))

Adapted plan:
PLAN SORT (JOIN (O NATURAL,JOIN (A ВИЗИТЫ NATURAL,A ПЕРСОНАЛ INDEX (PK_ID_USERS),A КЛИЕНЫ INDEX (PK_KOD),
A РАБОТА INDEX (PK_ID_WRK),A ГОРОДА INDEX (PK_ID_CITY),A РАЙОНЫ INDEX (PK_ID_RAJ),A УСЛУГИ INDEX (PK_ID_USLUGA),A ПРИБОРЫ INDEX (PK_ID_PRIB),A ОТДЕЛЫ INDEX (PK_ID_OTDEL),A ПРАЙСЫ INDEX (FK_ID_USL))))
З.Ы. Имена всех героев естесно вымышлены! :lol:

Добавлено: 06 авг 2007, 17:29
WildSery
AnryGTR писал(а):А у меня его нет, а в блэйзтопе есть такая фишка?
Обязательно должна.
Это ж самый важный инструмент профилирования запросов (после головы).
К сожалению, блэйзтоп даже не видел ни разу, работаю либо в IBExpert, либо в isql, так что не знаю, где там.

Добавлено: 06 авг 2007, 17:33
AnryGTR
так что насчёт ПЛАНОВ?

Добавлено: 06 авг 2007, 17:37
WildSery
Тормоза у тебя собственно в том, что для выполнения LEFT JOIN у тебя вьюха запрашивается столько раз, сколько строк выбрано из "ОТДЕЛЫ" (см. статью "Методы доступа").
Если бы это было соединение прямо тут же, то запрос выполнялся по индексу, а для вьюхи индекс уже не подберёшь, и он выполняется весь.
Выхода 2:
1. отказаться от LEFT JOIN
2. переписать запрос, вместо вьюхи присоединив собственно таблицы (причём только нужные можно сразу), тогда соединение будет выполняться по индексам.

Добавлено: 06 авг 2007, 17:45
AnryGTR
мда...понятно...насчёт первого варианта не совсем понял, если отказаться от лефтджойна, то как по-другому?
А насчёт второго - небольшой эскизик запроса не мог бы набросать?
Ну если не можешь, я не обижусь, итак ты уже со мной провозился :lol:
Кстати, на CyberGuru читал про планы, и там говорилось что оптимизатору можно подсовывать свои планы...может как-нить так решается?

СПАСИБО тебе огромное!!! 8)

Добавлено: 06 авг 2007, 20:16
WildSery
Отказаться от лефтджойна - т.е. присоединить отделы, у которых всё будет по NULLям можно позже, или вообще не присоединять.

Если вью отбросить и составлять сразу запрос, должно получиться что-то вроде

Код: Выделить всё

select o.id, min(o.nam_otd) as nam_otd, 
	count(v.id),
	count(case when v.оплата=1 then 1 else null end),	
	count(case when v.выполнение=1 then 1 else null end),
	count(case when v.скидка!=0 then 1 else null end),
	count(case when v.вазврат_денег=1 then 1 else null end),
	count(distinct(v.id_клиента)),
	count(distinct(case when v.оплата=1 then v.id_клиента else null end)),
	sum(case when v.оплата=1 then p.сумма - p.сумма/100*v.скидка else null end),
	sum(case when v.тип_оплаты=0 and v.оплата=1 then p.сумма - p.сумма/100*v.скидка else null end),
	sum(case when v.тип_оплаты=1 and v.оплата=1 then p.сумма - p.сумма/100*v.скидка else null end)
  from ОТДЕЛЫ o
	   left join УСЛУГИ u ON (u.ID_отдела = o.ID) 
	   left join ВИЗИТЫ v ON (v.ID_услуги = u.ID and v.дата between '+d1+' and '+d2+')
	   left join ПРАЙСЫ p ON (p.ID_услуги = u.ID and v.дата_время between p.дата_нач and p.дата_кон)
  where o.typ=1
  group by o.id
Возможно, при соединении ещё можно заменить LEFT'ы кроме первого на INNER, выстроив лесенкой, типа

Код: Выделить всё

  from ОТДЕЛЫ o
	   left join УСЛУГИ u 
				 join ВИЗИТЫ v ON (v.ID_услуги = u.ID and v.дата between '+d1+' and '+d2+')
			    join ПРАЙСЫ p ON (p.ID_услуги = u.ID and v.дата_время between p.дата_нач and p.дата_кон)
	   ON (u.ID_отдела = o.ID) 
но я чего-то сомневаюсь, что так будет быстрее. Нужно смотреть.

PS. Свои планы рисовать не стоит. Сервер как правило нарисует лучше, изредка только нужна подсказка. Гораздо важнее, как именно запрос напишешь.

Добавлено: 07 авг 2007, 09:08
AnryGTR
ЙЕС,ЙЕС,ЙЕС, ну наконец-то...а то мне шеф с этим отчётом все мозги выпарил...WildSery СПС Огромнейшее тебе, ну просто респект и уважуха!!! :P
Переписал запрос примерно под твой первый вариант(всё через LEFT JOIN) - время: 1-2 сек! :shock:
Мда, казалось бы одно представление - а столько тормозов даёт... :?
вот вчера скачал с Нэта себе книгу Хелен Борри про FireBird буду глубже изучать специфику запросов! :lol:
Да, ещё один маленький вопросик(я уже тебя запарил наверное? :lol: ):
вот у меня второй такой же отчёт по услугам, т.е.идут наименования услуг, а рядом всё те же данные, соответсвенно по услугам, так вот имеется ли такая возможность:
первая строка в сетке "Наименование отдела", а потом те данные по услугам, когда услуги отдела заканчиваются следующая строка - Второй отдел, а за ним опять же его услуги, и т.д....
Ну типа по отделам разбить услуги, так нагляднее и понятнее... :roll:

Добавлено: 07 авг 2007, 10:06
WildSery
AnryGTR писал(а):Мда, казалось бы одно представление - а столько тормозов даёт... :?
Как уже сказал, это потому, что при соединении с вьёй зачастую не используется никакой индекс, потому она собирается на каждую строку.
AnryGTR писал(а):первая строка в сетке "Наименование отдела", а потом те данные по услугам, когда услуги отдела заканчиваются следующая строка - Второй отдел, а за ним опять же его услуги, и т.д....
Что-то вроде вот этого, наверное:

Код: Выделить всё

select o.id || ' ' || u.id, u.Usluga_Name, u.skidka, sum(p.сумма)
  from ОТДЕЛЫ o
       join УСЛУГИ u ON (u.ID_отдела = o.ID)
	   join ПРАЙСЫ p ON (p.ID_услуги = u.ID and v.дата_время between p.дата_нач and p.дата_кон)
  where o.typ=1
union all
select o.id || ' ', o.nam_otd, null, null
  from ОТДЕЛЫ o
  where o.typ=1
order by 1