Как оптимизировать запрос на удаление SQL с подзапросом для Firebird?

Следующий запрос очень медленный. Кажется, подзапрос выполняется для каждой строки в таблице?!

delete from HISTORY
where ID in (
  select ID from (
    select ID, ROW_NUMBER() over(partition by SOURCE order by ID desc) as NUM from HISTORY
  ) where NUM > 100
);

Это запрос на очистку. Он должен удалить все, кроме 100 самых последних записей за SOURCE.

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

Конечно, есть PK на ID и FK и индекс на SOURCE (оба столбца Integer).


person maf-soft    schedule 04.11.2020    source источник
comment
> the subselect is executed for each row in the table?! Точно. Возможно, получите некоторые идеи от stackoverflow.com/questions/11906720/   -  person Arioch 'The    schedule 05.11.2020
comment
вы можете использовать IDE, такую ​​​​как IBExpert, для проверки статистики выполнения запросов, например, сколько было выборок из кэша и выборки с диска, чтобы вы могли проверить свою идею о линейном масштабировании по количеству строк :-)   -  person Arioch 'The    schedule 05.11.2020
comment
как правило, когда вы видите in (и особенно not in) в interbase/firebird, попробуйте проверить, можете ли вы легко переформулировать запрос во что-то другое. Это не всегда плохо, но достаточно часто, чтобы выработать эту привычку.   -  person Arioch 'The    schedule 07.11.2020
comment
Правильно ли я помню, что многие другие системы баз данных делают это намного лучше и не имеют с этим проблем?   -  person maf-soft    schedule 07.11.2020
comment
Может быть. Но Interbase/Firebird с самого начала был ориентирован на недорогой движок, как с точки зрения обслуживания (отсутствие выделенного администратора баз данных, мгновенный перезапуск БД после серьезных сбоев), так и с точки зрения размера кода и занимаемой площади, добавляемой к работающим приложениям. С одной стороны, у него небольшая реструктуризация запросов, с другой — более высокая предсказуемость и, возможно, меньше ошибок в оптимизаторе SQL. Я думаю, что из основных систем RBMS только SQLite может сравниться с FB / IB с небольшими показателями. Конечно, он не может соответствовать системам, которые в 100 раз больше по коду и по командам.   -  person Arioch 'The    schedule 07.11.2020
comment
По поводу повторного выполнения подзапросов тоже есть такое мнение, что его нельзя утверждать инвариантным, так как результаты запроса volatile variables в терминах C++, их значение можно менять внутри цикла. Например, в read-commit транзакциях — зафиксировав еще какой-нибудь tx. А до FB3 даже из того же запроса (insert into TableName select * from TableName запускается бесконечно, пока не встретится ошибка (нарушение PK, нехватка тома диска, нехватка int32 для ID и т. д.), смотрите стабильность курсора на firebirdsql.org/file/documentation/release_notes/html/en/3_0/   -  person Arioch 'The    schedule 08.11.2020


Ответы (1)


Firebird 3 добавил опцию DELETE в предложение MERGE. Впервые он упоминается в примечаниях к выпуску. Теперь он правильно задокументирован в Справочник по SQL для Firebird 3.

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

merge into HISTORY HDel
using ( select ID, SOURCE, ROW_NUMBER() over
                (partition by SOURCE order by ID desc) as NUM 
        from HISTORY ) HVal
   on (HVal.NUM > 100) and (HVal.ID = HDel.ID) and (HVal.Source = HDel.Source)
WHEN MATCHED THEN DELETE

В вашей конкретной базе данных (HVal.Source = HDel.Source) фильтрация кажется излишней, но я все же решил добавить ее, чтобы сделать запрос как можно более общим для будущих читателей. Береженого Бог бережет :-)


В Firebird 2.x такой возможности не было, а при отсутствии в FB3 функций MERGE/DELETE и оконных функций можно вернуться к явному императивному программированию и писать старые добрые циклы. Потребуется написать и выполнить небольшую программу PSQL (либо постоянную именованную хранимую процедуру, либо специальный оператор EXECUTE BLOCK) с явным циклом по SOURCE значениям.

Что-то вроде (я не проверял синтаксис, просто царапал по памяти):

execute block as
declare variable SRC_VAL integer;
declare variable ID_VAL integer;
begin
  for select distinct SOURCE from HISTORY into :SRC_VAL do begin
     :ID_VAL = NULL;
     select first(1) skip(100) ID from HISTORY
       where SOURCE = :SRC_VAL
       order by ID desc
       into :ID_VAL;
     if (:ID_VAL IS NOT NULL) then
       delete from HISTORY 
         where SOURCE = :SRC_VAL 
           and ID <= :ID_VAL;
  end
end
person Arioch 'The    schedule 05.11.2020
comment
Вот немного документации на английском языке: firebirdsql.org/ файл/документация/release_notes/html/en/3_0/ - person maf-soft; 06.11.2020
comment
Отлично, я протестировал запрос на слияние, и он работает так, как есть, и быстро! (Я просто удалил SOURCE из столбцов, возвращаемых подзапросом, и из условия включения, потому что в моем случае идентификатор уже уникален) - person maf-soft; 08.11.2020
comment
Английскую MERGE документацию по Firebird 3 можно найти по адресу firebirdsql.org/file/documentation/html/en/refdocs/fblangref30/ - person Mark Rotteveel; 26.02.2021
comment
Спасибо @MarkRotteveel - person Arioch 'The; 26.02.2021