08.12.2010, 10:24 | #21 |
Злыдни
|
Цитата:
Настоятельно рекомендую перед дефрагментацией почистить некоторые таблицы: выполненные пакеты, записи об официальных лицах и т.п.
__________________
люди...считают, что если техника не ломается, то ее не нужно ремонтировать. Инженеры считают, что если она не ломается, то нуждается в совершенствовании. |
|
08.12.2010, 10:30 | #22 |
Moderator
|
Цитата:
Для этих манипуляций у меня имеется джоб оббегания по таблицам Аксапты и сохранения данных в Аксесе: X++: static void Job350_AllRecIds(Args _args) { str strFileName = @'C:\AxForumTests\Gustav\RecId.mdb'; str strFolderName; Filename filename; FilenameType filenameType; FilePath filePath; container conPath; COM dbe, db; COM cnn, rst; COM flds, fld; int i, nLines; int timeFullStart, timeFullFinish; Dictionary dictionary = new Dictionary(); TableId tableId; DictTable dictTable; Common common; int row, timeStart; int recordCount; #CCADO #define.dbLangGeneral(';LANGID=0x0409;CP=1252;COUNTRY=0') void recreateAccessTable(str _table, str _fields) { try { nLines = infolog.line(); db.Execute('DROP TABLE [' + _table + ']'); } catch (Exception::Error) { infolog.clear(nLines); } db.Execute( 'CREATE TABLE [' + _table + '] (' + _fields + ')' ); } ; timeFullStart = timenow(); // parse file name ----------------------------------------------------------------------------- [filePath, filename, filenameType] = fileNameSplit(strFileName); conPath = str2con_RU(filePath, '\\'); // you can use str2con if you do not have Russian DIS-layer if (subStr(filePath,2,1) != ':') { info(@'For drive use only one character syntax: C:\...! Do not use: \\server\folder\...!'); return; } // create folders if they not exist ------------------------------------------------------------ if (conlen(conPath)>1) { strFolderName = conpeek(conPath,1); // C: for (i=2; i<=conlen(conPath); i++) { strFolderName += strFmt(@'\%1', conpeek(conPath,i)); // C:\AxForumTests... if (!WinAPI::folderExists(strFolderName)) WinApi::createDirectory(strFolderName); } } // create MDB-file if not exists --------------------------------------------------------------- dbe = new COM('DAO.DBEngine.36'); if (!WinAPI::fileExists( strFileName )) { try { nLines = infolog.line(); db = dbe.CreateDatabase(strFileName, #dbLangGeneral); } catch (Exception::Error) { infolog.clear(nLines); info( 'Probably mdb-file already exists and is open at the moment!' ); } } else { db = dbe.OpenDatabase( strFileName ); } // (re)create tables in MDB-file ---------------------------------------------------------------- recreateAccessTable( 'UsedRecId', 'TblId LONG, ' + 'DataAreaId TEXT(3),' + 'RecId LONG ' ); recreateAccessTable( 'RecIdHoles', 'FromRecId LONG, ' + 'ToRecId LONG ' ); db.Close(); db = null; dbe = null; // export EmplTable (a few fields) from Axapta to similar table in Access ---------------------- cnn = new COM('ADODB.Connection'); cnn.connectionString('Provider=Microsoft.Jet.OLEDB.4.0;' + 'Data Source=' + strFileName); cnn.Open(); rst = new COM('ADODB.Recordset'); rst.LockType(#adLockOptimistic); rst.Open('UsedRecId', cnn); flds = rst.Fields(); row = 0; for (i=1; i<= dictionary.tableCnt(); i++) { tableId = dictionary.tableCnt2Id(i); dictTable = new DictTable(tableId); print strFmt('%1 -- %2 -- %3', tableId, dictTable.name(), row); // если в очередной таблице нет записей // то переходим к следующей try { nLines = infolog.line(); recordCount = new SysDictTable(tableId).recordCount(); } catch //может случиться, если таблица есть в репозитарии, но нет в базе { infolog.clear(nLines); recordCount = 0; } if (! recordCount) continue; common = dictTable.makeRecord(); // цикл по записям таблицы (МОЖНО ОГРАНИЧИТЬ ДИАПАЗОН ПОИСКА RecId ПО ВСЕМ ТАБЛИЦАМ) while select common where common.RecId >= 800000001 && common.RecId <= 900000000 { row++; rst.AddNew(); fld = flds.Item('TblId' ); fld.Value(tableId); fld = flds.Item('DataAreaId'); fld.Value(common.dataAreaId); fld = flds.Item('RecId' ); fld.Value(common.RecId); rst.Update(); } } rst.Close(); rst = null; cnn.Close(); cnn = null; timeFullFinish = timenow(); box::info(strfmt('Total running time: %1 sec -- Records: %2', timeFullFinish - timeFullStart, row)); } Далее уже в Аксесе создаем VBA-модуль со следующей начинкой: Код: Option Compare Database Option Explicit Sub packHolesInIntSequenceToRanges() '=================================================================== ' Сворачивание "дырок" в последовательности целых чисел в диапазоны '=================================================================== Dim cnn As ADODB.Connection Dim rstSource As ADODB.Recordset Dim rstRanges As ADODB.Recordset Dim rangeNum As Integer 'счетчик непрерывных диапазонов Dim curr As Long 'текущее (очередное) значение из последовательности Dim prev As Long 'предыдущее значение (конец непрерывного диапазона) Const minDelta As Long = 25 'минимальный размер непрерывного диапазона '(диапазоны меньшей длины игнорируются) Const startFrom As Long = 15082620 '15000001 'стартовое значение процессса - либо "дырка", либо нет 'первый prev все равно на 1 меньше и считается не дыркой Const stopOn As Long = 19997895 'следующий NextVal - 25*кол-во пользователей - последний curr - считается дыркой Set cnn = Application.CurrentProject.AccessConnection Set rstSource = New ADODB.Recordset With rstSource Set .ActiveConnection = cnn .source = "SELECT * FROM UsedRecId WHERE DataAreaId=""ppp""" & _ " AND RecId >= " & CStr(startFrom) & _ " AND RecId <= " & CStr(stopOn) & " ORDER BY RecId" .Open End With Set rstRanges = New ADODB.Recordset With rstRanges Set .ActiveConnection = cnn .source = "RecIdHoles" .LockType = adLockOptimistic .Open End With rangeNum = 0 prev = startFrom - 1 Do While Not rstSource.EOF curr = rstSource("RecId").Value If curr - prev > 1 Then 'записываем диапазон, закончившийся на предыдущем If curr - prev - 1 >= minDelta Then 'здесь именно минус 1 rangeNum = rangeNum + 1 Debug.Print rangeNum rstRanges.AddNew rstRanges("FromRecId").Value = prev + 1 'From rstRanges("ToRecId").Value = curr - 1 'To rstRanges.Update End If End If prev = curr rstSource.MoveNext Loop 'для последнего диапазона curr = stopOn + 1 'не дырка If curr - prev > 1 Then 'записываем диапазон, закончившийся на предыдущем If curr - prev - 1 >= minDelta Then 'здесь именно минус 1 rangeNum = rangeNum + 1 Debug.Print rangeNum rstRanges.AddNew rstRanges("FromRecId").Value = prev + 1 'From rstRanges("ToRecId").Value = curr - 1 'To rstRanges.Update End If End If Set rstRanges = Nothing Set rstSource = Nothing Set cnn = Nothing End Sub Последующий перенос данных из аксессной RecIdHoles в RECIDHOLES в схеме Аксапты - любым желаемым способом, вплоть до приаттачивания RECIDHOLES к файлу MDB как таблицы ODBC и элементарного ручного копипаста из одной таблицы в другую. Или через Excel - вставляем данные RecIdHoles в колонки A и B, в ячейке C1 пишем формулу ="INSERT INTO RECIDHOLES VALUES ("&A1&","&B1&");" и копируем ее на следующие строки; далее копируем содержимое колонки С в QA (для MS SQL Server) или TOAD (для Oracle) и исполняем этот набор операторов INSERT. Последний раз редактировалось Gustav; 08.12.2010 в 11:28. |
|
|
За это сообщение автора поблагодарили: mazzy (2), Logger (10), lev (5), vml (1), S.Kuskov (8). |
08.12.2010, 11:37 | #23 |
Модератор
|
Цитата:
Сообщение от Gustav
Код: CREATE TABLE RECIDHOLES ( FROMRECID NUMBER(10), TORECID NUMBER(10) ) Код: CREATE OR REPLACE TRIGGER SystemSequences_TBU BEFORE UPDATE ON SYSTEMSEQUENCES REFERENCING NEW AS New OLD AS Old FOR EACH ROW WHEN ( SUBSTR(NLS_LOWER(Old.DataAreaId),1,3) = 'ppp' AND Old.Id = -1 AND Old.TabId = 0 ) Код: while select common where common.RecId >= 800000001 && common.RecId <= 900000000 { row++; rst.AddNew(); fld = flds.Item('TblId' ); fld.Value(tableId); fld = flds.Item('DataAreaId'); fld.Value(common.dataAreaId); fld = flds.Item('RecId' ); fld.Value(common.RecId); rst.Update(); } P.S. Особенно если в простых случаях (нет виртуальных компаний и правильное наследование типов для ссылок по RecId) все и так само собой работает
__________________
-ТСЯ или -ТЬСЯ ? |
|
|
За это сообщение автора поблагодарили: Gustav (3). |
08.12.2010, 11:44 | #24 |
Модератор
|
Я взял на себя смелость предположить что будь в свое время проплачена поддержка, топикстартер в 2010 году на версии 3.0 не сидел бы и сейчас просто взять и получить лицензии на 2009 задешево не получится
__________________
-ТСЯ или -ТЬСЯ ? |
|
08.12.2010, 12:45 | #25 |
Moderator
|
Ну, несколько компаний можно прописать либо через условие триггера:
Код: WHEN ( (SUBSTR(NLS_LOWER(Old.DataAreaId),1,3) = 'ppp' AND Old.Id = -1 AND Old.TabId = 0) OR (SUBSTR(NLS_LOWER(Old.DataAreaId),1,3) = 'rrr' AND ...) OR (SUBSTR(NLS_LOWER(Old.DataAreaId),1,3) = 'sss' AND ...) ) Цитата:
И совсем же необязательно сразу искать дыры во всем диапазоне от 0 до текущего значения NextVal. Допустим, текущий NextVal в районе 1.5 миллиардов, тогда можно, не торопясь, составить таблицу дыр для RecId = 0..500 млн, настроить триггер, запустить его в эксплуатацию. А через годик обработать уже диапазон 500 млн..1 млрд и т.д. Это ж всё разовые задачки - обработал, запустил, забыл ("украл, выпил, в тюрьму" (с)) Цитата:
Я ж только предлагаю, не навязываю же... |
|
08.12.2010, 12:59 | #26 |
Участник
|
Цитата:
Для начала, код очевидно "сырой". В том смысле, что писался под конкретную задачу и как решение "в общем случае" не годится. В этом решении важен не конкретный код, а сама идея хранения "дыр" и своевременное переключение нумератора на очередную "дыру". Ну, а "допилить" код под свои нужды - уже дело техники. Важна сама идея. Теперь по вопросам: 1. Насчет компаний, сам же и ответил. Не вижу особых проблем добавить фильтр по компаниям 2. Время выполнения определения "дыр" Очевидно, по крайней мере, будет не больше, чем время дефрагментации. Кроме того, очевидно, есть предмет для оптимизации алгоритма. Чего не скажешь про алгоритм дефрагментации. Ведь поиск дыр - это чистые запросы, а дефрагментация - это запросы+модификация. Если запросы еще можно оптимизировать (да и сам алгоритм изменить), то с модификацией особых вариантов нет. Надо модифицировать ВСЕ записи. Быстро это выполнено быть не может Кроме того, как справедливо заметил Gustav, можно разбить процесс поиска дыр на этапы, чего в принципе невозможно сделать для дефрагментации. 3. Преимущества, по сравнению с дефрагментацией (явно не прозвучало, но вопрос очевиден) Дефрагментация, как бы ей мозги ни вправляли, всегда оставляет вероятность того, что где-то чего-то не учли и ссылки будут нарушены. При использовании "дыр" такого быть не может, поскольку ссылки не меняются. Как мне кажется, это из области фантастики. Большинство российских внедрений кастомизировано по самое "не балуйся". Причем не один раз и самыми разным людьми с очень разным опытом. Поэтому простых случаев не может быть "по определению" |
|
|
За это сообщение автора поблагодарили: Gustav (12). |
08.12.2010, 13:14 | #27 |
Member
|
Цитата:
Сообщение от Vadik
...
Не проще вправить мозг стандартному "дефрагментатору" и успокоиться еще на пару лет? ...
__________________
С уважением, glibs® |
|
08.12.2010, 13:23 | #28 |
Модератор
|
И правда, если
X++: EDT , RecId Цитата:
Судя по не утихающим до сих пор дискуссиям, в решении данной проблемы простота и надежность не являются приоритетными критериями
__________________
-ТСЯ или -ТЬСЯ ? |
|
08.12.2010, 13:56 | #29 |
Moderator
|
Лично однажды забыл поставить RefRecId для одной ссылки. (Хотя я не самый безграмотный и ленивый специалист и про проблему эту знал когда писал. Просто делал в жуткой запаре). Потом мы этим "Надежным и простым" дефрагментатором продефрагментировали. Потом дня через три заметили утрату связи (функциональность не слишком регулярно использовалась). Потом около 2 недель - восстанавливали связь по всяким эвристикам.
Так что вероятность ошибок со ссылками всегда присутствует. А поскольку на работающей системе остановить бизнес на денек-другой и попросить сотрудников потестить - не потрелялось ли что - не реально, предложеный Gustav'ом вариант имеет право на жизнь. Хотя мне тоже кажется что его надо еще доводить и оптимизировать изрядно. Но сама по себе идея интересна и неплоха... Кстати - тут помниться пару недель назад была дискуссия насчет навешивания ярлыков... Это я к употреблению термина "Кулибин"... |
|
08.12.2010, 16:04 | #30 |
Участник
|
Очень понравилась идея от Gustav, т.к. дырок действительно очень много. При "использованных" свыше 3,5 млрд номеров RecId у нас в базе всего около 400 млн записей. И ее еще можно почистить...
Переход на новую версию - это что-то из области фантастики для нашей компании. Так что вполне вероятно никакой дефрагментации не потребуется. |
|
08.12.2010, 21:34 | #31 |
Moderator
|
Цитата:
Я набросал еще один джоб, помогающий понять степень заполненности базы по диапазонам-этапам (stages) размером в 100 млн. номеров RecId. X++: static void Job351_CountRecIdsPerStage(Args _args) { // расчет количеств RecId по этапам int i, nLines; int timeFullStart, timeFullFinish; Dictionary dictionary = new Dictionary(); TableId tableId; DictTable dictTable; Common common; int row, timeStart; int recordCount; int stage; int stageCnt = 22; int recIdPerStage = 100000000; int recIdStart = 1; int recIdEnd = recIdPerStage; int recIdCount; ; timeFullStart = timenow(); for (stage=1; stage<= stageCnt; stage++) { recIdCount = 0; for (i=1; i<= dictionary.tableCnt(); i++) { tableId = dictionary.tableCnt2Id(i); dictTable = new DictTable(tableId); print strFmt('%1 -- %2 -- %3', stage, tableId, dictTable.name()); // если в очередной таблице нет записей // то переходим к следующей try { nLines = infolog.line(); recordCount = new SysDictTable(tableId).recordCount(); } catch //может случиться, если таблица есть в репозитарии, но нет в базе { infolog.clear(nLines); recordCount = 0; } if (! recordCount) continue; common = dictTable.makeRecord(); select count(RecId) from common where common.RecId >= recIdStart && common.RecId <= recIdEnd; recIdCount += common.RecId; } info(strFmt('%1 -- %2 -- %3 -- %4', stage, recIdStart, recIdEnd, recIdCount)); if (! recIdCount) break; recIdStart = recIdEnd + 1; recIdEnd = recIdEnd + recIdPerStage; } timeFullFinish = timenow(); box::info(strfmt('Total running time: %1 sec', timeFullFinish - timeFullStart)); } Код: stage recIdStart recIdEnd recIdCount % к 100 млн. --------------------------------------------------------------- 1 1 100 000 000 8 050 727 8% 2 100 000 001 200 000 000 878 353 1% 3 200 000 001 300 000 000 1 478 347 1% 4 300 000 001 400 000 000 1 131 490 1% 5 400 000 001 500 000 000 2 195 859 2% 6 500 000 001 600 000 000 1 427 424 1% 7 600 000 001 700 000 000 1 259 705 1% 8 700 000 001 800 000 000 2 905 657 3% 9 800 000 001 900 000 000 16 803 406 17% 10 900 000 001 1 000 000 000 12 565 324 13% 11 1 000 000 001 1 100 000 000 16 741 287 17% 12 1 100 000 001 1 200 000 000 21 317 892 21% 13 1 200 000 001 1 300 000 000 11 187 373 11% 14 1 300 000 001 1 400 000 000 0 0% --------------------------------------------------------------- 97 942 844 Кстати, нашёл у себя аксесный mdb-файл на 28 млн. записей таблицы UsedRecId (см. мой пред. пост). Так вот этот файл имеет размер 1 Gb. Можно использовать это соотношение как оценочное при планировании "завоевания" этапов. Alenka, а у вас поставщик-внедренец Аксапты - не GMCS ли тоже? У них в приложении масса полезных запросов-отчетов, которые, тем не менее, RecId расходуют нещадно - за счет заполнения вспомогательных таблиц временными данными (не путать с временными таблицами). Т.е. на "совесть" этих запросов-отчетов в нашем приложении половину дыр точно можно списывать... |
|
|
За это сообщение автора поблагодарили: aidsua (1), G.Menshikh (1). |
08.12.2010, 22:13 | #32 |
Участник
|
легко. экспорт/импорт с удалением.
|
|
09.12.2010, 10:00 | #33 |
Участник
|
2 Gustav: внедряли Аксапту нам КОРУС Консалтинг.
Такое кол-во пустот в нумерации действительно возникает из-за таблиц с временными данными, жизнь которых составляет от1 дня до 2 недель. Пустота вначале тоже есть, но она составляет всего около 2 млн. |
|
|
За это сообщение автора поблагодарили: Gustav (2). |
14.01.2011, 10:03 | #34 |
Участник
|
Наконец-то нашлось время на испытание, проверку и тестирование идеи с триггером. Выяснилось, что, к сожалению, сама идея заполнения дыр, оставляя в запасе 25 номеров, "не дружит" с insert_recordset. При использовании insert_redordset значение nextVal в SystemSequence передвигается сразу на количество вставляемых записей. Поэтому необходимо держать в запасе в текущей дыре с неиспользованными RecId не 25 номеров, а неограниченное количество, что естественно невозможно.
2 Gustav: Этот случай был просто не учтен Вами или есть какое-то решение? Последний раз редактировалось Alenka; 14.01.2011 в 10:58. |
|
|
За это сообщение автора поблагодарили: Vadik (1), Gustav (3), S.Kuskov (1). |
14.01.2011, 12:33 | #35 |
Участник
|
|
|
|
За это сообщение автора поблагодарили: Vadik (1), Alenka (1). |
14.01.2011, 13:50 | #36 |
Moderator
|
Цитата:
Сообщение от Alenka
Наконец-то нашлось время на испытание, проверку и тестирование идеи с триггером. Выяснилось, что, к сожалению, сама идея заполнения дыр, оставляя в запасе 25 номеров, "не дружит" с insert_recordset. При использовании insert_redordset значение nextVal в SystemSequence передвигается сразу на количество вставляемых записей. Поэтому необходимо держать в запасе в текущей дыре с неиспользованными RecId не 25 номеров, а неограниченное количество, что естественно невозможно.
2 Gustav: Этот случай был просто не учтен Вами или есть какое-то решение? Цитата:
Сообщение от Gustav
2. У таблицы SystemSequence в Ax 3.0 имеется метод setCacheSize, позволяющий установить размер кэша иным, нежели 25. Перед использованием триггера рекомендуется проверить код приложения Аксапты на присутствие вызовов этого метода (у меня не было ни одного). При необходимости можно увеличить minDelta в триггере до значения максимального параметра этих вызовов, либо (более муторно) в триггере предусмотреть генерирование ошибки (исключения) при попытке Аксапты сделать шаг больше, чем 25.
Логично? Или я выдаю желаемое за действительное? Еще раз отмечу, что в данный момент могу рассуждать только теоретически. |
|
14.01.2011, 14:10 | #37 |
Участник
|
Нет, такой анализ не поможет, т.к. проанализировать-то можно, а исправить ничего уже будет нельзя, т.к. вставляемым записям будут присваиваться RecId, начиная со старого значения. Т.е. надо было при предыдущем смещении nextVal "предусмотреть" возможность вставки с помощью insert_recordset.
|
|
14.01.2011, 14:14 | #38 |
Участник
|
Беда в том, что (:Old.NextVal) и является первым выделенным RecId для текущей вставки, а (:New.NextVal) очередной RecId уже для следующей вставки и изменить можно только его.
|
|
14.01.2011, 14:23 | #39 |
Участник
|
Боюсь это придется делать в АХ, т.е. "прошерстить" код и делать уже там перенаправление на самую большую дырку
Можно попробовать забить на эту проблему, ну не пройдет с первого раза транзакция, так пройдет со второго Потом в АХ найдется немного мест, которые требуют уникальности RecId в разрезе нескольких таблиц, т.ч. шансы на удачный исход дополнительно повышаются |
|
|
За это сообщение автора поблагодарили: Gustav (2). |
14.01.2011, 14:25 | #40 |
Moderator
|
Цитата:
Сообщение от Alenka
Нет, такой анализ не поможет, т.к. проанализировать-то можно, а исправить ничего уже будет нельзя, т.к. вставляемым записям будут присваиваться RecId, начиная со старого значения. Т.е. надо было при предыдущем смещении nextVal "предусмотреть" возможность вставки с помощью insert_recordset.
Цитата:
insert_recordset, желающий вставить 100 записей (вместо 25), вероятно, возьмет старый nextVal и прибавит к нему 100 и попытается сохранить в SystemSequence для последующих запросов. Что можем мы в этой ситуации? Можем увидеть, что собирается быть выполненным шаг больше 25, а также проверить существует ли свободное пространство в текущей дыре для выполнения этого шага. Если оно есть, то всё в порядке. Если его нет, то выбирать следующую подходящую дыру бесполезно, так как наш insert_recordset уже запомнил для себя 100 последовательных номеров - надо генерить ошибку (исключение). При этом можно, конечно, уже иметь на примете следующую подходящую дыру. В общем, дальнейший успех зависит от того, сможем ли мы хорошо обработать это исключение. Тогда текущая попытка увеличения nextVal будет как бы холостым шагом, а следующая после обработки исключения попытка выделения 100 номеров будет успешной. Блин, должна быть! |
|