|
07.12.2010, 14:29 | #1 |
Участник
|
Миф об уникальности RecId?
Axapta 3.0 SP3
Добрый день. Хочу опять поднять вопрос о RecId. Дело в том, что мы столкнулись с проблемой заканчивающихся номеров RecId, т.е. мы уже давно перешли на отрицательные значения и в течение ближайших месяцев снова должны достигнуть 0. На форуме уже обсуждалась эта проблема и высказывались предположения о том, что же произойдет после того, как будет достигнто нулевой значение. При этом высказывалась мысль о том, что при создании новой записи в таблице будет искаться неиспользованное значение RecId, т.е. будут заполняться пустоты в нумерации. Было также предположение, что никакого поиска не будет, а при вставки новой записи с существующим RecId будет возникать ошибка. Я решила промоделировать эту ситуацию на тестовой базе. Сдвинула NextVal в SystemSequences на значение, близкое к нулю. Как и ожидалось, RecId снова стало принимать положительные значения. Но! стали появляться записи с уже существующими RecId. Хотелось бы услышать ваше мнение по данному вопросу. Получается, что уникальность RecId - это миф? |
|
07.12.2010, 14:37 | #2 |
Модератор
|
Миф - это идея о том, что в системе есть функциональность, которая будет искать свободные "дырки" в диапазонах выделенных значений RecId
А уникальность RecId в таблице при наличии уникального индекса по этому полю - не миф см. Как выполнять дефрагментирование RecID
__________________
-ТСЯ или -ТЬСЯ ? |
|
|
За это сообщение автора поблагодарили: Alenka (1). |
07.12.2010, 14:41 | #3 |
Участник
|
Дефрагментация стандартными способами на нашей базе невозможна из-за ее размера (около 500Гб) и потере целостности (стандартный метод не правит те ссылки на RecId, которые хранятся в таблицах).
Как же уникальность не миф, когда я получила по крайней мере 2 записи с одинаковыми RecId? |
|
07.12.2010, 14:44 | #4 |
Модератор
|
Цитата:
Стандартный метод корректно правит ссылки в пределах одной компании там, где они оформлены корректно (с использованием правильных EDT) Делает он это неторопливо, но даже в Вашем случае за сутки отработать
__________________
-ТСЯ или -ТЬСЯ ? |
|
07.12.2010, 14:57 | #5 |
Участник
|
2 Vadik : Просто я ее уже читала раньше.
2 oip: Все правильно, я неверно понимала понятие уникальности. Ошибка при вставке и сообщение о существующей записи появляются при создании записи с существующим RecId в этой же таблице. При этом запись все-таки создается со следующим RecId. Спасибо за помощь и наставления . |
|
07.12.2010, 15:05 | #6 |
Модератор
|
Значит невнимательно читали, потому что Ваши выводы и аргументация ошибочны
Цитата:
Дефрагментация стандартными способами на нашей базе невозможна из-за ее размера (около 500Гб) и потере целостности (стандартный метод не правит те ссылки на RecId, которые хранятся в таблицах)
__________________
-ТСЯ или -ТЬСЯ ? |
|
07.12.2010, 15:35 | #7 |
Участник
|
Надо просто найти поля где хранятся ссылки на RecID и исправить EDT поля на RefRecId - тогда обновляться все будет корректно.
|
|
07.12.2010, 14:41 | #8 |
Axapta
|
Вы все неправильно понимаете. Смотрите, для 3.0 ситуация такая. Генерация Аксаптой нового RecId проста как три копейки. Аксапта грубо говоря (опустим сейчас не значащие для нас нюансы) берет соответствующее последнее значение из номерной серии и прибавляет к нему единицу. Все, данное значение и записывается в таблицу. Разумеется, Аксапта не проверяет никакую уникальность, и не пытается найти неиспользуемый номер. Представляете, с какой бы скоростью она работала, если бы каждый раз это делала?!
Поэтому никакой гарантированной уникальности RecId в пределах всей базы нет. Проблема возникает, если в той же самой таблице, в которой мы создаем запись, уже есть запись с таким RecId Соответственно, как только у вас RecId пойдут по второму кругу, вы будете в случайные моменты времени все чаще и чаще сталкиваться с такой ошибкой. А вообще, возникающие проблемы и возможные способы решения уже обсуждались и не один раз. |
|
|
За это сообщение автора поблагодарили: Alenka (1). |
07.12.2010, 16:46 | #9 |
Злыдни
|
Скорость дефрагментации RecId очень сильно зависит от производительноти сервера и скорости работы дисковой подсистемы. Лет 5 назад проводил дефрагментацию на базе размером в 25 Гигов. Сервер с двумя старенькими Xeon-ами, 4Gb оперативки и SCSI подсстемой (RAID-10) пережевал дефрагментацию за час с небольшим. При этом не должны работать пользователи и пакетники. Я думаю, что в Вашем случае все должно уложиться в один выходной день. Можете, во избежании случайных подключений, запустить дефрагментацию в двухуровневой конфигурации.
__________________
люди...считают, что если техника не ломается, то ее не нужно ремонтировать. Инженеры считают, что если она не ломается, то нуждается в совершенствовании. |
|
07.12.2010, 19:47 | #10 |
Участник
|
Не помню, был ли где уже подобный совет, поэтому, возможно, повторюсь.
Для начала, неплохо бы было посмотреть "глазами" на значение RecId тех таблиц, где приблизились к ранее использовавшимся значениям на предмет того, не было ли массовых удалений или ручной правки значения счетчика RecId. Т.е. "глобальных" разрывов в последовательности RecId. Все таблицы смотреть не обязательно, достаточно посмотреть таблицу, в которой наиболее часто создают новые записи (например, InventTrans). Причем создаются не раз в месяц большими партиями (закрытие склада), а достаточно регулярно. Тут важен именно момент создания, а не модификации. Ну, простейший запрос вида Код: select dataAreaId, round(RecId/1000000,0), count(*) from InventTrans with (nolock) group by dataAreaId, round(RecId/1000000,0) order by 1,2 Правда, такой совет подойдет, скорее, для виртуальных компаний, где не так уж и много таблиц и относительно легко найти "дыры" в последовательности значений. Для "обычных" компаний придется писать скрипт по перебору всех таблиц (не сложно, но "муторно"), чтобы убедиться, что найденная "дыра" действительно "дыра" RecId, а не просто большая группа удаленных записей в одной..двух таблицах. PS: Ага, была озвучена эта идея http://axforum.info/forums/showthrea...992#post110992 Цитата:
По результатам телефонного митинга с John McBride (менеджер команды разработки) и Mathieu Kemenovic (глобальная служба поддержки) мне подтвердили что они ничего менять в 3.0 не будут (и даже не будут делать private hot-fix) и предоставлили набор SQL-скриптов, которые ищут большие "дырки" последовательности идентификаторов записей и используют их. Данные скрипты неавтоматические и необходимо выполнять ряд шаманских танцев с бубнами...
|
|
07.12.2010, 20:22 | #11 |
Участник
|
Возможно, у автора появился повод для перехода на новую версию?
__________________
Ivanhoe as is.. |
|
07.12.2010, 21:10 | #12 |
Moderator
|
В порядке эксперимента - автоматическое использование дыр RecId
Эх, готовил материал для сообщения в блоге (где-то уже с года полтора назад), все собирался нормально оформить и выложить, да, чувствую, если сейчас не сообщу, то для "трёшки" уже совсем никому не надо будет. Поэтому несколько сумбурно, схематично, но выкладываю. Ветка, вроде, подходящая
Честно скажу, в промышленной эксплуатации не пользовал (не припёрло), но на тестовой всё отрабатывало довольно прилично. Axapta - 3.0 SP4, СУБД - Oracle 10. Общий смысл такой - заставить Аксапту автоматически использовать имеющиеся дыры RecId. Для этого нужно провести подготовительную работу: при помощи самописного скрипта пробежаться по всем таблицам системы и собрать информацию о неиспользованных интервалах RecId. Для Ax 3.0 (двухзвенка, без АОС) имеет смысл отбирать только непрерывные диапазоны размером не менее 25, т.е. не меньше размера кэша. Далее грузим полученные диапазоны в таблицу дыр (не аксаптовскую, просто созданную на уровне БД в той же схеме): Код: CREATE TABLE RECIDHOLES ( FROMRECID NUMBER(10), TORECID NUMBER(10) ) Код: ALTER TABLE RECIDHOLES ADD ( CHECK (TORECID-FROMRECID+1>=25)) Код: 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 ) DECLARE currNextVal NUMBER(10); cntBetween NUMBER(10); cntAbove NUMBER(10); minDelta NUMBER(10); b4switching NUMBER(10); holesRange RecIdHoles%ROWTYPE; BEGIN b4switching := 1118091751; -- NextVal перед переключением (передвинуть с запасом на 100-200, чтобы не схватили) minDelta := 25; -- Axapta 3.0 Cache Size currNextVal := :New.NextVal; -- значение, которое собирается вставить Аксапта -- если здесь 1, то более ничего не делаем - :New.NextVal проходит в таблицу SELECT COUNT(*) INTO cntBetween FROM RecIdHoles h WHERE currNextVal BETWEEN h.FromRecId AND h.ToRecId AND h.ToRecId - currNextVal + 1 >= minDelta; IF cntBetween = 0 THEN -- иначе попадаем сюда и устанавливаем новое значение, равное FromId следующего диапазона SELECT COUNT(*) INTO cntAbove FROM RecIdHoles h WHERE h.FromRecId > currNextVal AND h.ToRecId - h.FromRecId + 1 >= minDelta; -- на всякий случай IF cntAbove > 0 THEN -- если мы в диапазонах, охватываемых таблицей дыр RecIdHoles SELECT * INTO holesRange FROM (SELECT * FROM RecIdHoles h WHERE h.FromRecId > currNextVal AND h.ToRecId - h.FromRecId + 1 >= minDelta -- на всякий случай ORDER BY h.FromRecId) WHERE ROWNUM = 1; currNextVal := holesRange.FromRecId; ELSE -- если уже нет IF currNextVal <= b4switching THEN -- значит кончились записи в таблице дыр RecIdHoles currNextVal := b4switching; END IF; -- если currNextVal > b4switching, то уже всё поехало нормально END IF; :New.NextVal := currNextVal; END IF; EXCEPTION WHEN OTHERS THEN -- Consider logging the error and then re-raise RAISE; END SystemSequences_TBU; P.S. ОГРАНИЧЕНИЯ: 1. В текущей редакции триггер можно использовать на монотонно возрастающем участке генерации RecId, достаточно далеком от крайних значений, т.е. перевалка через максимальное значение (2G) и продолжение с минимального (-2G) текущим алгоритмом не предусмотрена. 2. У таблицы SystemSequence в Ax 3.0 имеется метод setCacheSize, позволяющий установить размер кэша иным, нежели 25. Перед использованием триггера рекомендуется проверить код приложения Аксапты на присутствие вызовов этого метода (у меня не было ни одного). При необходимости можно увеличить minDelta в триггере до значения максимального параметра этих вызовов, либо (более муторно) в триггере предусмотреть генерирование ошибки (исключения) при попытке Аксапты сделать шаг больше, чем 25. По иронии судьбы в Ax 4.0 метод setCacheSize отсутствует, но в "четверке" уже и подобный триггер не нужен Последний раз редактировалось Gustav; 08.12.2010 в 11:13. |
|
|
За это сообщение автора поблагодарили: fed (3), Vadik (1), Владимир Максимов (10), gl00mie (10), Alenka (1), Mileyko (1). |
08.12.2010, 00:17 | #13 |
Модератор
|
А код, который отыщет свободные диапазоны RecId в компании на объеме данных, сопоставимом с постановкой задачи (500Гб) - будет?
__________________
-ТСЯ или -ТЬСЯ ? |
|
08.12.2010, 10:30 | #14 |
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 | #15 |
Модератор
|
Цитата:
Сообщение от 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, 12:45 | #16 |
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 | #17 |
Участник
|
Цитата:
Для начала, код очевидно "сырой". В том смысле, что писался под конкретную задачу и как решение "в общем случае" не годится. В этом решении важен не конкретный код, а сама идея хранения "дыр" и своевременное переключение нумератора на очередную "дыру". Ну, а "допилить" код под свои нужды - уже дело техники. Важна сама идея. Теперь по вопросам: 1. Насчет компаний, сам же и ответил. Не вижу особых проблем добавить фильтр по компаниям 2. Время выполнения определения "дыр" Очевидно, по крайней мере, будет не больше, чем время дефрагментации. Кроме того, очевидно, есть предмет для оптимизации алгоритма. Чего не скажешь про алгоритм дефрагментации. Ведь поиск дыр - это чистые запросы, а дефрагментация - это запросы+модификация. Если запросы еще можно оптимизировать (да и сам алгоритм изменить), то с модификацией особых вариантов нет. Надо модифицировать ВСЕ записи. Быстро это выполнено быть не может Кроме того, как справедливо заметил Gustav, можно разбить процесс поиска дыр на этапы, чего в принципе невозможно сделать для дефрагментации. 3. Преимущества, по сравнению с дефрагментацией (явно не прозвучало, но вопрос очевиден) Дефрагментация, как бы ей мозги ни вправляли, всегда оставляет вероятность того, что где-то чего-то не учли и ссылки будут нарушены. При использовании "дыр" такого быть не может, поскольку ссылки не меняются. Как мне кажется, это из области фантастики. Большинство российских внедрений кастомизировано по самое "не балуйся". Причем не один раз и самыми разным людьми с очень разным опытом. Поэтому простых случаев не может быть "по определению" |
|
|
За это сообщение автора поблагодарили: Gustav (12). |
08.12.2010, 13:23 | #18 |
Модератор
|
И правда, если
X++: EDT , RecId Цитата:
Судя по не утихающим до сих пор дискуссиям, в решении данной проблемы простота и надежность не являются приоритетными критериями
__________________
-ТСЯ или -ТЬСЯ ? |
|
08.12.2010, 13:14 | #19 |
Member
|
Цитата:
Сообщение от Vadik
...
Не проще вправить мозг стандартному "дефрагментатору" и успокоиться еще на пару лет? ...
__________________
С уважением, glibs® |
|
08.12.2010, 00:22 | #20 |
Модератор
|
- неспортивно
- неуплаченная "дань за двенадцать лет" (поддержка) зело велика будет - перевод тройки на 2009 - челлендж покруче любой дефрагментации RecId
__________________
-ТСЯ или -ТЬСЯ ? |
|
|
За это сообщение автора поблагодарили: CDR (1). |
Теги |
ax3.0, recid, дефрагментирование recid, законченный пример, полезное |
|
Похожие темы | ||||
Тема | Ответов | |||
if (record) vs if (record.RecId) | 18 | |||
поля, содержащие RecId | 15 | |||
Что лучше select RecId или select TableId | 9 | |||
aEremenko: Дефрагментация RecID | 2 | |||
Два RecId у одной записи таблицы | 33 |
Опции темы | Поиск в этой теме |
Опции просмотра | |
|