(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Первичный ключ - составной или суррогатный?

Источник: ln

Этот выпуск посвящен "вечной" теме выбора столбцов для первичного ключа. По мотивам случайно обнаруженного замечательного ответа Тома Кайта на вопросы, заданные в 2001-2003 годах

Первичный ключ - составной или суррогатный?

Том,

У меня есть таблица из 3 полей, комбинация значений которых уникальна для каждой записи. Вот эти поля:

Object_ID      CHAR(4)
Ticket_Number  NUMBER
Start_DateTime DATE

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

Поэтому мне кажется, что, вместо добавления нового поля, единственным назначением которого будет уникально идентифицировать строку в таблице, я могу использовать комбинацию этих трех полей. Но в руководстве PL/SQL Developer's Guide рекомендуется не использовать составные первичные ключи.

Что ты думаешь по этому поводу?

Надо учитывать также следующее:

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

С другой стороны, если лучше добавить новое числовое поле для идентификации записей, как проще всего увеличивать значение этого поля при каждой вставке? Есть ли в Oracle подобие типа данных autonumber MS Access?

Ответ Тома Кайта

Если требуется, чтобы "эти три поля уникально идентифицировали запись в любом случае", придется задавать по ним ограничение уникальности (UNIQUE CONSTRAINT) в любом случае. Если дублирование object_id,ticket_number,start_datetime - ошибка, ограничение уникальности НЕОБХОДИМО.

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

Чтобы получить тип "auto increment" в Oracle, необходимо выполнить:

create sequence my_seq;
create trigger my_trigger before insert on T for each row
begin
   select my_seq.nextval into :new.ID from dual;
end;
/

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

Да, использование последовательности не гарантирует отсутствие пропусков при нумерации - в любой не однопользовательской системе они точно будут. Единственный способ нумеровать без пропусков - обрабатывать транзакции по одной. Последовательности предназначались не для этого, и любая система с таким требованием не будет масштабироваться (для компьютерных систем оно вообще смешное - при обработке документов вручную еще куда ни шло, но в компьтерной системе - оно просто бессмысленно).

Последовательности - хорошо масштабируемый способ генерации суррогатных ключей.

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

  • Последовательность меньше, чем 3 столбца. Если ключ необходимо использовать как внешний в множестве других таблиц, экономия места может оказаться огромной.
  • Люди часто пытаются изменять первичный ключ, чтобы исправить данные - использование неизменного суррогатного ключа решает раз и навсегда проблему каскадных изменений , поскольку первичный ключ (значение последовательности) изменять не придется никогда - только 3 столбца данных, которые больше нигде не хранятся.
  • Проще написать:
    select * from p, c where p.primary_key = c.foreign_key
    
    чем
    select * from p, c where p.id1 = c.fk1 and p.id2 = c.fk2 and p.id3 = c.fk3
    
    да и работает первый запрос быстрее.

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

Составной ключ в одном столбце

Мне интересно твое мнение о том, чтро делать, если клиенты настаивают на использовании "магических кодов" - составных ключей, впихнутых в один столбец.

Простой пример - следующий идентификатор события:

03-40123    Формат:  FY-XNNNN

где:

FY    = 2 цифры финансового года
X     = 1 цифра, задающая тип события
NNNN  = 4 цифры, задающие порядковый номер события данного типа в этом финансовом году

Лично я пыаюсь избегать такого рода идентификаторов (как бы они не генерировались) как чумы, по следующим, как мне кажется, очевидным, причинам:

  • Если при вводе данных произошла ошибка, и ключ оказался неверным (например, событие было типа 3, а не 4) - все очень печално, ведь это событие уже всем известно как 03-40123
  • Делаются конкретные предположения о максимальных значениях:
    1) В году никогда не будет более 10000 событий типа 1
    2) Нельзя добавить одиннадцатый тип события
    3) Две цифры года - это мы уже проходили...
  • Такие ключи неудобно реализовывать
  • Зачем вообще эта возможность узнать финансовый год и тип события без дополнительного запроса?

Я периодически сталкиваюсь с этой проблемой, обычно - при обновлении старых систем и/или систем "бумажного" документооборота, пользователи которых не хотят изменять систему нумерации. Часто мне удается уговорить клиента перейти на простые последовательности, но не всегда.

Я знаю, что ты не сторонник простых правил, но оправдывает ли природа данных или необходимость "запоминающегося" ключа создание такого типа ключей? Когда ты считаешь обоснованным использование такого составного ключевого столбца для идентфикации данных? Добавишь ли ты собственный суррогатный ключ и позволишь пользователям хранить свои магические коды где угодно, или будешь настаивать на использовании простой последовательности?

Ответ Тома Кайта

Такого рода поля могут (и должны) быть ПРОИЗВОДНЫМИ от других данных. Клиенту нет необходимости знать, как в физической схеме фактически реализован первичный ключ - это деталь реализации.

Так что, я бы сделал так:

create table t ( id int number primary key, /* заполняется значениями последовательности */
                 fy date,
                 incident_type varchar2(1),
                 goofy_number  number,
                 ...
create view v as
select
t.*, 
to_char(fy,'yy')//'-'//incident_type//to_char(goofy_number,'fm0000') their_field
from t;

Можно даже создать индекс по функции (function-based index) по полю their_field, если они собираются искать по его значениям.

Комментарий читателя от 23 ноября 2002 года

Как обычно, твое решение предельно ясно. Мне особенно понравилась идея про индекс по функции!

Однако это не рашает мою проблему "изменения адреса".

Как я попытался объяснить в первом пункте, как только значения fy, incident_type, и goofy_number определены и строка вставлена, значение their_number тоже неявно определено. С этого момента значение their_number может выдаваться в отчетах, сообщаться заинтересованным сторонам и т.д.

Если оказывается, что, например, значение incident_type перовначально оказалось ошибочным, и оно изменяется, значения their_number в базе данных и в отчетах, у заинтересованных сторон и т.д. перестают совпадать.

Можно строить their_number по столбцам, которые не меняются, но это не всегда соответствует требованиям клиента.

Можно строить значение their_number при вставке, помещать в отдельный столбец (с ограничением уникальности, а не первичного ключа), и больше никогда его не изменять при изменении базовых значений его компонентов.

В конечном итоге, меня интересует следующее:

Как "эксперт", нанятый для создания солидных моделей данных, не выхожу ли я за пределы моих полномочий (и не трачу ли зря время), часами пытаясь убедить клиентов не использовать their_number, а заменить его простым значением последовательности?

Ответ Тома Кайта

Если вы представили им все факты, как в вопросе, продемонстрировав, что это может привести к ошибкам в интерпретации данных, и они все равно настаивают на своем  - вы сделали все, что могли. Можете включить СВОЙ первичный ключ в отчеты, чтобы при возникновении проблемы можно было получить соотвествующее значение. Вы не выходите за пределы своих полномочий. Я неоднократно повторял, что наша работа как раз и состоит в том, чтобы обращать на подобные вещи мнимание тех, кто не является профессиональным программистом. Последний раз пободная проблема возникла, когда меня спросили на сайте, как выбрать N случайных строк из таблицы. Я написал, как это сделать, но проблема все усложнялась, пока не выяснилось, что нужна случайная выборка 4 строк из сложного запроса со множеством соединений и т.п. Причем, выборка эта должна была делаться сотни/тысячи раз в день. Для этого требовалось множество ресурсов.

А зачем все это понадобилось? Чтобы на портале "вывесить" фотографии 4 случайно выбранных сотрудников. Я ответил: "Сообщите клиентам, что 90% ресурсов машины теперь будет уходить на выдачу этих 4 фотографий, - захотят ли они за это платить". Мнения разделились - надо ли "знать свое место" и тупо, как бараны, делать то, что требуют, или доказывать, что практически бесполезная возможность дается дорогой ценой, и не нужна.

Я бы продолжал настаивать на своем - ваши аргументы на 100% верны. Если они решат не прислушиваться к советам, попытайтесь, по возможности, защитить их от проблем (с помощью суррогатного ключа).

Не хотел бы я работать там, где за год происходит только 9999 событий... Маловато перспектив для роста... А первого января придется этот смешной счетчик снова в 0 сбрасывать...

Изменение составного первичного ключа

У нас есть две таблицы следующего вида:

create table t1(c1 number, c2 varchar2(50),
  constraint t1p primary key(c1, c2));

create table t2(c1 number, c2 varchar2(50),  c3 number,
  constraint t2p primary key(c1, c2, c3),
  constraint t2f foreign key (c1, c2) references t1(c1, c2));

insert into t1(c1, c2) values(1, 'c2');
insert into t2(c1, c2, c3) values(1, 'c2', 3);

Необходимо изменить значение c2 в таблице t1. Нет ли способа изменить этот первичный ключ, не создавая суррогатного?

Ответ Тома Кайта

Раз так, c1,c2 не является первичным ключом - первичный ключ не должен меняться.

Если хотите, используйте ограничения с отложенной проверкой (deferrable constraints).

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

Изменение первичного ключа. Комментарий от 15 января 2003 года

Пакет работает прекрасно.

Вопросы:

  1. После изменения первичного ключа, как "отвязать" пакет от таблицы (он необходим для других таблиц, так что просто удалить его я не могу).
  2. Этот пакет кажестя лучшим решением в нашем случае, поскольку остальной код менять не придется. Помимо снижения производительности, какие еще недостатки имеет данное решение?
  3. "Ограничения с отложенной проверкой", о которых вы пишете, это когда добавляют ключевое слово deferrable для внешних ключей, так что можно сначала изменить первичный ключ, а потом - внешний. Вы это имеете в виду? Например:
    create table t1(c1 number, c2 varchar2(50),
      constraint t1p primary key(c1, c2));
    
    create table t2(c1 number, c2 varchar2(50),  c3 number,
      constraint t2p primary key(c1, c2, c3),
      constraint t2f foreign key (c1, c2) 
        references t1(c1, c2) deferrable initially deferred);
    

Ответ Тома Кайта

Не знаю, как относиться к утверждения, что "Пакет работает прекрасно". Сомнительный комплимент, как по мне...

  1. Пакет не связывается с таблицей. Он создает ряд триггеров и хранимых процедур, для указанной таблицы - он генерирует специфический код. Сам по себе он ни с одним объектом не связан.
  2. Вы имеете ввиду помимо того факта, что изменение первичного ключа вообще недачная идея, связанная с ошибкой проектирования?
  3. Да, проверку внешних ключей можно отложить и делать так:
    set constraints all deferred;
    update parent primary key;
    update child1 fkey;
    update child2 fkey;
    set constraints all immediate <<=== если это не сработает, не было зафиксировано изменение в подчиненной таблице
    

Изменение первичного ключа. Комментарий от 16 января 2003 года

Сразу после установки пакета и создания таблиц t1, t2, t3, для изменения первичного ключа мне пришлось сначала выполнить команду:

exec update_cascade.on_table('t2')

Но если открыть новый сеанс после этого, изменить первичный ключ мне удалось и без команды 'exec update_cascade.on_table('t2')'. Но я не хочу, чтобы пользователи постоянно меняли этот первичный ключ.

Вот пример:

SQL> connect UCDEMO/UCDEMO@e2rs Connected. SQL> update t2 2 set b=900 3 where b = 6; -- первичный ключ изменен без выполнения 'exec update_cascade.on_table('t2')' 1 row updated.

Ответ Тома Кайта

Команда exec update_cascade.on_table('t2') создала (как и описано на указанной странице) триггеры и пакеты для поддержки каскадного изменения. Если его больше не нужно поддерживать, УДАЛИТЕ их.

Как насчет использования sys_guid() вместо последовательности для генерации значений первичного ключа?

Какие преимущества и недостатки связаны с использованием sys_guid() (подозреваю, что используется больше места на диске?) sys_guid() можно указать в качестве стандартного значения (и не понадобиться триггер). Понятно, что "пропуски" значений - не проблема :)

Ответ Тома Кайта

Да, RAW(16) - больше по размеру, чем большинство числовых полей. Его не так удобно записывать, как число. Во многих случаях данные типа raw не обрабатываются - они неявно преобразуются в 32-байтовую строку типа varchar2. Не уверен, что использовал бы этот подход без веских причин.

Первичный ключ: sys_guid или последовательность

Мы используем sys_guid вместо последовательности (есть требование глобальной уникальности первичных ключей) - в любом случае, имеет смысл сделать тест и поделиться результатами.

Результаты показали следующее:

  1. По времени работы последовательности - быстрее (я использовал опцию cache с кэшем размером 5000) - разброс значений был велик, но на разных прогонах последовательности оказались от 52% до 90% быстрее, чем вызовы sys_guid.

    Думаю, причина в кэшировании значений последовательностей в области sga, правильно? Однако странно, почему sys_guid работает медленнее - как внутренне реализована функция sys_guid?

  2. Понятно, что значения последовательности занимают меньше места (примерно в 2 раза)
  3. Однако sys_guid использует меньше защелок, в частности, следующих:
    STAT...session logical reads   319,026    317,195    -1,831
    STAT...db block gets           318,025    315,680    -2,345
    LATCH.cache buffers chains   1,398,729  1,366,857   -31,872
    LATCH.library cache            407,043    207,830  -199,213
    LATCH.sequence cache           300,040          0  -300,040
    

    Не мог бы ты объяснить, что это за защелки - я думаю, последние две связаны с получением и установкой следующего значения для сеанса.

    С чем связано большее количество db block gets и cache buffer chains при использовании последовательностей? Судя по руководству " Oracle Reference " cache buffer chains связаны с конфликтами при доступе к блоку, т.е. последовательности многократно обращаются к одному и тому же блоку (вероятно, во внутренней таблицеЮ используемой для хранения последовательностей - таблица seq$)?

    test@apps> @schema
    test@apps> drop sequence my_seq;
    
    Sequence dropped.
    
    Elapsed: 00:00:00.09
    
    test@apps> create sequence my_seq cache 5000;
    
    Sequence created.
    
    Elapsed: 00:00:00.02
    
    test@apps> drop table t_guid ;
    
    Table dropped.
    
    Elapsed: 00:00:04.51
    
    test@apps> create table t_guid
      2  (
      3    id RAW(16) default sys_guid() not null primary key,
      4    value number
      5  );
    
    Table created.
    
    Elapsed: 00:00:00.09
    
    test@apps> drop table t_seq ;
    
    Table dropped.
    
    Elapsed: 00:00:01.44
    
    test@apps> create table t_seq
      2  (
      3    id int not null primary key,
      4    value number
      5  );
    
    Table created.
    
    Elapsed: 00:00:00.08
    
    test@apps> @data
    test@apps> create or replace package run_benchmark_pkg as
      2    g_number_of_records constant integer := 100000 ;
      3  
      4    procedure insert_with_seq;
      5    procedure insert_with_guid;
      6  end;
      7  /
    
    Package created.
    
    Elapsed: 00:00:00.09
    
    test@apps> create or replace package body run_benchmark_pkg as
      2    procedure insert_with_seq
      3    is
      4    begin
      5       for i in 1 .. g_number_of_records
      6       loop
      7         insert into t_seq values( my_seq.nextval, i);
      8       end loop;
      9    end;
     10  
     11    procedure insert_with_guid
     12    is
     13    begin
     14       for i in 1 .. g_number_of_records
     15       loop
     16         insert into t_guid (value) values( i);
     17       end loop;
     18    end;
     19  
     20  end;
     21  /
    
    Package body created.
    
    Elapsed: 00:00:00.16
    
    test@apps> begin
      2    runstats_pkg.rs_start;
      3    run_benchmark_pkg.insert_with_seq;
      4    runstats_pkg.rs_middle;
      5    run_benchmark_pkg.insert_with_guid;
      6    runstats_pkg.rs_stop(200);
      7  end;
      8  /
    
    Run1 ran in 5728 hsecs
    Run2 ran in 7337 hsecs
    run 1 ran in 78.07% of the time
        
    Name                                        Run1              Run2       Diff
    STAT...calls to get snapshot s           100,582           100,847        265
    STAT...commit cleanouts                      423               708        285
    STAT...commit cleanouts succes               423               708        285
    LATCH.list of block allocation               567               858        291
    STAT...immediate (CURRENT) blo               372               674        302
    STAT...calls to kcmgcs                       396               718        322
    LATCH.transaction allocation                 864             1,266        402
    STAT...consistent gets - exami               670             1,130        460
    STAT...consistent gets                     1,001             1,515        514
    STAT...free buffer requested               2,916             3,775        859
    STAT...change write time                   2,455             3,775      1,320
    STAT...session logical reads             319,026           317,195     -1,831
    STAT...dirty buffers inspected             1,996             3,991      1,995
    STAT...free buffer inspected               1,996             3,991      1,995
    LATCH.redo allocation                    205,074           207,309      2,235
    STAT...redo entries                      203,898           206,150      2,252
    STAT...db block gets                     318,025           315,680     -2,345
    LATCH.cache buffers lru chain              4,826             7,585      2,759
    STAT...db block changes                  408,338           411,971      3,633
    LATCH.checkpoint queue latch               8,652            12,829      4,177
    LATCH.cache buffers chains             1,398,729         1,366,857    -31,872
    LATCH.library cache                      407,043           207,830   -199,213
    LATCH.sequence cache                     300,040                 0   -300,040
    STAT...redo size                      49,988,292        56,368,192  6,379,900
        
    Run1 latches total versus runs -- difference and pct
    Run1              Run2                   Diff     Pct
    3,946,936         3,426,263          -520,673 115.20%
    
    PL/SQL procedure successfully completed.
    
    Elapsed: 00:02:11.30
    
    test@apps> exec show_space ( 'T_GUID')
    Unformatted Blocks .....................              30
    FS1 Blocks (0-25)  .....................               0
    FS2 Blocks (25-50) .....................               0
    FS3 Blocks (50-75) .....................               1
    FS4 Blocks (75-100).....................              31
    Full Blocks        .....................             370
    Total Blocks............................             512
    Total Bytes.............................       4,194,304
    Total MBytes............................               4
    Unused Blocks...........................              64
    Unused Bytes............................         524,288
    Last Used Ext FileId....................               9
    Last Used Ext BlockId...................         177,800
    Last Used Block.........................              64
    
    PL/SQL procedure successfully completed.
    
    Elapsed: 00:00:00.27
    
    test@apps> exec show_space ( 'T_SEQ')
    Unformatted Blocks .....................               0
    FS1 Blocks (0-25)  .....................               0
    FS2 Blocks (25-50) .....................               1
    FS3 Blocks (50-75) .....................               0
    FS4 Blocks (75-100).....................              40
    Full Blocks        .....................             203
    Total Blocks............................             256
    Total Bytes.............................       2,097,152
    Total MBytes............................               2
    Unused Blocks...........................               0
    Unused Bytes............................               0
    Last Used Ext FileId....................               9
    Last Used Ext BlockId...................         176,776
    Last Used Block.........................             128
    
    PL/SQL procedure successfully completed.
    
    Elapsed: 00:00:00.22
    
    test@apps> spool off 
    

Ответ Тома Кайта

  1. Хотя это и сказано в документации, sys_guid обращается к ОС и, несомненно, требует больше ресурсов процессора
  2. LATCH.cache buffers chains   1,398,729  1,366,857  -31,872
    
    Защелки для получения блоков из буферного кэша... Последовательности хранятся в блоках... Эти защелки связаны с получением буфера для изменения последовательности и/или получения ее текущего значения.
    LATCH.library cache          407,043    207,830    -199,213
    
    Защелки в библиотечном кэше. Определение последовательности находится именно там. Обращение к последовательности вызывает установку этих защелок.
    LATCH.sequence cache         300,040          0    -300,040
    
    Очевидно ;) Это действительно защелки, обеспечивающие поддержку последовательности.

Комментарий от 4 августа 2003 года

В документации Oracle 9.0.1.1.1 сказано, что: "Составной первичный ключ может содержать не более 32 столбцов". Но я попробовал задать 33 столбца, и все получилось. Почему?

Вот как я это делал.

create table Test(x1 number(2) ,x2 number(2) ,x3 number(2) ,x4 number(2) ,x5 
number(2) ,x6 number(2) ,x7 number(2) ,x8 number(2) ,x9 number(2) ,x10 
number(2),x11 number(2),x12 number(2),x13 number(2),x14 number(2),x15 
number(2),x16 number(2),x17 number(2),x18 number(2),x19 number(2),x20 
number(2),x21 number(2),x22 number(2),x23 number(2),x24 number(2),x25 
number(2),x26 number(2),x27 number(2),x28 number(2),x29 number(2),x30 
number(2),x31 number(2),x32 number(2),x33 number(2),x34 number(2),x35 
number(2),x36 number(2),x37 number(2),x38 number(2),x39 number(2),x40 number(2),
primary key( 
x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x2
3,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33))
/ 

Ответ Тома Кайта

Похоже, предел - 33 столбца. Меня это не беспокоит, поскольку "1" - наиболее типичное количество, а 5 или 6 - разумный максимум... 32 будет "слегка перебор", а 33 - еще хуже.

Первичный ключ: sys_guid или последовательность - комментарий от 18 августа 2003 года

Мы используем sys_guid, поскольку хотим избежать конфликтов при переносе данных из одной базы в другую. Точная причина мне не известна, но при экспортировании данных из базы db1 и импортировании в другую базу данных, db2, при использовании последовательностей возможны конфликты (поскольку одинаковые последовательности создавались в обеих схемах).

Сталкивались ли вы с такой ситуацией? Нет ли более элегантного решения проблемы, кроме использования sys_guid вместо последовательностей. Мы не знаем точного количества баз и не можем просто начинать последовательности с разных значений.

Ответ Тома Кайта

Пусть имеется N баз, которые потенциально придется поддерживать. Если не уверены, увеличьте количество в 100 раз.

Потом в перовй базе выполняем:

create sequence S start with 1 increment by n*100;

Во второй:

create sequence S start with 2 increase by n*100;

Пусть n = 10, тогда в первой базе будут генерироваться числа:

1, 1001, 2001, 3001, ...

Во второй:

2, 1002, 2002, 3002, ...

Получили не перекрывающиеся последовательности для 1000 баз данных.

Не перекрывающиеся последовательности!

"не перекрывающиеся последовательности для 1000 баз данных."

Я находил это решение на сайте. Оно очень элегантно, но надо заранее знать количество баз. В нашем случае требовалось, чтобы можно было наполнять данными любую локальную схему, а затем путем экспорта/импорта добавлять накопленные данные в центральную базу.

В идеале один и тот же сценарий установки должен работать всегда, не создавая конфликтов в любой базе. Да, кроме использования централизованной таблицы с начальными значениями для последовательностей, к которой будут обращаться последовательно, - другой достойной альтернативы не видно...

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 16.03.2010 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Oracle Database Standard Edition 2 Named User Plus License
Oracle Database Personal Edition Named User Plus Software Update License & Support
Oracle Database Standard Edition 2 Processor License
Oracle Database Personal Edition Named User Plus License
Enterprise Connectors (1 Year term)
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
СУБД Oracle "с нуля"
Delphi - проблемы и решения
Adobe Photoshop: алхимия дизайна
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100