Реалізація підтипу підтипу в конструкції типу / підтипу з взаємовиключними підкласами


20

Вступ

Для того, щоб це питання було корисним для майбутніх читачів, я буду використовувати загальну модель даних для ілюстрації проблеми, з якою я стикаюся.

Наша модель даних складається з 3 об'єктів, які повинні бути позначені як A, Bі C. Для того, щоб все було просто, всі їх атрибути матимуть intтип.

Entity Aмає наступні атрибути: D, Eі X;

Entity Bмає наступні атрибути: D, Eі Y;

Суб'єкт господарювання Cмає такі атрибути: Dі Z;

Оскільки всі об'єднання мають спільний атрибут D, я вирішив застосувати дизайн / тип підтипу .

Важливо: Суб'єкти взаємовиключні! Це означає, що сутність є або A, або B, або C.

Проблема:

Суті Aі Bє ще одна спільна ознака E, але цей атрибут не присутній в об'єкті C.

Питання:

Я хотів би використати описану вище характеристику для подальшої оптимізації дизайну, якщо це можливо.

Якщо чесно, я поняття не маю, як це зробити, ні з чого почати намагатися, звідси і цей пост.

Відповіді:


6

Наскільки це запитання є продовженням Чи правильна моя реалізація схеми дизайну типу / підтипу (для взаємовиключних підкласів)? , що саме по собі є продовженням Не знаю, як перетворити змінну сутність у реляційну таблицю , я б запитав: що саме ви намагаєтесь оптимізувати? Зберігання? Об'єктна модель? Складність запиту? Виконання запитів? Існують компроміси при оптимізації одного аспекту проти іншого, оскільки ви не можете оптимізувати всі аспекти одночасно.

Я повністю згоден з пунктами Ремуса щодо:

  • У кожного підходу є плюси і мінуси (тобто постійно присутній фактор "це залежить"), і
  • Перший пріоритет - ефективність моделі даних (неефективну модель даних неможливо виправити чистим та / або ефективним кодом програми)

Зважаючи на це, вибір, який стоїть перед вами, полягає в наступному, розташованому в порядку найменшої нормалізації до більшості нормалізації:

  • просування власності Eдо таблиці базового типу
  • зберігаючи його в декількох таблицях підтипу
  • повністю нормалізуючись Eдо нової, посередницької таблиці підкласів на тому ж рівні C, що, Aта Bбезпосередньо будуть підкласами ( @ відповідь MDCCL )

Давайте розглянемо кожен варіант:

Перемістити властивість Eдо таблиці базового типу

PRO

  • Знижена складність запиту для запитів , які необхідно , Eале не X, Y, або Z.
  • Потенційно більш ефективно для запитів , які потребують , Eале не X, Yабо Z(особливо агрегатні запити) за приводу не JOIN.
  • Потенціал для створення індексу (D, E)(і якщо так, потенційно відфільтрованого індексу, (D, E)де EntityType <> C, якщо така умова дозволена)

КОНС

  • Неможливо позначити EякNOT NULL
  • Потрібна додаткова CHECK CONSTRAINTтаблиця базового типу, щоб переконатися, що E IS NULLколи EntityType = C(хоча це не велика проблема)
  • Потрібно навчити користувачів моделі даних щодо того, чому це Eмає бути NULLі навіть слід повністю ігнорувати, коли EntityType = C.
  • Трохи менш ефективні, коли Eце тип фіксованої довжини, і велика частина рядків призначена для EntityType C(тобто не використовуючи, Eотже, це є NULL), і не використовує ні SPARSEпараметр у стовпці, ні стиснення даних на кластерному індексі
  • Потенційно менш ефективні для запитів, які не потребують, Eоскільки наявність Eу таблиці базового типу збільшуватиме розмір кожного рядка, що, у свою чергу, зменшує кількість рядків, які можуть вміститися на сторінці даних. Але це сильно залежить від точного типу даних E, FILLFACTOR, скільки рядків у таблиці базового типу тощо.

Зберігайте властивості Eв кожній таблиці підтипів

PRO

  • Чистіша модель даних (тобто не потрібно турбуватися про навчання інших щодо того, чому стовпець Eу таблиці базового типу не повинен використовуватися, оскільки "його насправді немає")
  • Напевно, більше нагадує об'єкт-модель
  • Може позначати стовпець так, NOT NULLніби це обов'язкова властивість суб'єкта
  • Немає необхідності в додатковій CHECK CONSTRAINTтаблиці базового типу, щоб переконатися, що E IS NULLколи EntityType = C(хоча це не величезний приріст)

КОНС

  • Щоб отримати цю властивість, потрібно приєднатись до підтипу таблиці (ив)
  • Потенційно дещо менш ефективно при потребі E, завдяки ПРИЄДНАННІ, залежно від того, скільки рядків A+ у Bвас є, на відміну від кількості рядків C.
  • Трохи складніші / складніші для операцій, які мають справу виключно з сутностями Aта Bне C ) такими ж "типами". Звичайно, ви можете абстрагувати це за допомогою перегляду, який робить UNION ALLміж SELECTтаблицями JOINed для Aта іншими SELECTтаблицями JOINed для B. Це дозволить знизити складність запитів SELECT , але не так корисно для INSERTі UPDATEзапитів.
  • Залежно від конкретних запитів та того, як часто вони виконуються, це може бути потенційною неефективністю у тих випадках, коли наявність індексу на (D, E)справді допоможе одному чи більше часто використовуваним запитам, оскільки їх не можна індексувати разом.

Нормалізувати Eпосередницьку таблицю між базовим класом та A&B

(Зауважте, що мені подобається відповідь @ MDCCL як життєздатна альтернатива, залежно від обставин. Далі не розуміється як сувора критика цього підходу, а як засіб додати певну перспективу - мою, звичайно - оцінюючи це в тому ж контексті, що і два варіанти, які я вже запропонував. Це полегшить уточнення того, що я вважаю відносною різницею між повною нормалізацією та сучасним підходом часткової нормалізації.)

PRO

  • модель даних повністю нормалізована (не може бути нічого суттєво неправильного в цьому, враховуючи те, що призначені для роботи RDBMS)
  • зменшена складність запитів для запитів, які потребують Aі B, але ні C(тобто немає необхідності в двох запитах, об'єднаних через UNION ALL)

КОНС

  • трохи більше місця, зайнятого ( Barтаблиця дублює ідентифікатор, і є новий стовпець, BarTypeCode) [мізерно, але щось слід пам’ятати]
  • незначне збільшення складності запиту як додаткового JOINпотрібно, щоб дістатися до будь-якого AабоB
  • збільшена площа поверхні для блокування, здебільшого на INSERT( DELETEможна керуватися неявно за допомогою позначення іноземних ключів як ON CASCADE DELETE), оскільки транзакція буде триматися відкритою трохи довше на таблиці базового класу (тобто Foo) [мізерно, але щось слід пам’ятати]
  • відсутні прямі знання про фактичний тип - Aабо B- в таблиці базового класу Foo; він знає лише тип, Brякий може бути Aабо B:

    Тобто, якщо вам потрібно робити запити за загальною базовою інформацією, але вам потрібно або класифікувати за типом сутності, або відфільтрувати один або декілька типів сутності, тоді таблиця базового класу не має достатньо інформації, і в цьому випадку вам потрібно стіл. Це також знизить ефективність індексації стовпця.LEFT JOINBarFooTypeCode

  • немає послідовного підходу до взаємодії з A& Bvs C:

    Тобто, якщо кожна сутність стосується безпосередньо таблиці базового класу так, що існує лише будь-який ПРИЄДНАЙТЕ, щоб отримати повну сутність, то кожен може швидше і легше наростити знайомство з точки зору роботи з моделлю даних. Існує загальний підхід до запитів / Збережених процедур, що робить їх швидше розвиватися і менше ймовірність виникнення помилок. Послідовний підхід також дозволяє швидше і простіше додавати нові підтипи в майбутньому.

  • потенційно менш пристосований до правил бізнесу, які змінюються з часом:

    Це означає, що все завжди змінюється, і досить легко перейти Eдо таблиці базового класу, якщо вона стане загальною для всіх підтипів. Також досить просто перемістити загальну властивість до підтипів, якщо зміни в природі сутностей роблять зміни на користь. Досить просто розбити підтип на два підтипи (просто створити інше SubTypeIDзначення) або об'єднати два або більше підтипів в один. І навпаки, що робити, якщо Eзгодом стане загальною властивістю всіх підтипів? Тоді посередницький шар Barтаблиці був би безглуздим, а додаткова складність не вартувала б. Звичайно, неможливо знати, чи відбудеться така зміна через 5, а то й за 10 років, тому Barтаблиця не обов’язково, і навіть не дуже ймовірно, це погана ідея (саме тому я сказав " потенційно менш пристосований"). Це лише моменти, які слід врахувати; це гра в будь-який бік.

  • потенційно невідповідне групування:

    Значення, тільки тому , що Eвласність поділена між типами сутностей Aі Bне означає , що Aі B повинні бути згруповані разом. Просто те, що речі "виглядають" однаково (тобто однакові властивості), не означає, що вони однакові.

Підсумок

Як і рішення про те, коли / коли денормалізувати, як найкраще підійти до цієї конкретної ситуації, залежить від врахування наступних аспектів використання моделі даних та переконання, що переваги перевищують витрати:

  • скільки рядків у вас буде для кожного EntityType (дивіться принаймні 5 років вниз, припускаючи вище середнього зростання)
  • скільки ГБ складе кожна з цих таблиць (базового типу та підтипів) через 5 років?
  • який тип даних є властивістю E
  • це лише одна властивість, чи є декілька, а то й декілька властивостей
  • які запити вам знадобляться, Eі як часто вони виконуватимуться
  • які запити вам знадобляться, які не потрібні, Eі як часто вони будуть виконуватись

Я думаю, що я за замовчуванням зберігаю Eв окремих підтипових таблицях, оскільки це, принаймні, "чистіше". Я б розглядав можливість переходу Eдо базового типу IF: більшість рядків не були для EntityType of C; а кількість рядів була принаймні мільйонами; і я частіше, ніж невиконані запити, які потрібні Eта / або запити, які отримали б користь від індексу, (D, E)або виконуються дуже часто і / або вимагають достатньої кількості системних ресурсів, щоб наявність індексу зменшила загальне використання ресурсів або, принаймні, запобігала сплески споживання ресурсів, які перевищують прийнятний рівень або тривають досить довго, щоб викликати надмірне блокування та / або збільшення тупикових ситуацій.


ОНОВЛЕННЯ

ОП прокоментував цю відповідь, що:

Мої роботодавці змінили бізнес-логіку, видаливши Е зовсім!

Ця зміна є особливо важливою, тому що саме те, що я передбачав, може трапитися в підрозділі "CONs" підрозділу "Нормалізувати Eдо посередницької таблиці між базовим класом та A& B" вище (6-а точка). Конкретне питання полягає в тому, наскільки легко / важко переробити модель даних, коли такі зміни відбуваються (і вони завжди є). Деякі запевнятимуть, що будь-яку модель даних можна відновити / змінити, тому почніть з ідеалу. Але хоча на технічному рівні вірно, що все можна відновити, реальність ситуації - це питання масштабу.

Ресурси не є нескінченними, не тільки процесор / диск / оперативна пам’ять, але й ресурси розвитку: час і гроші. Підприємства постійно визначають пріоритети в проектах, оскільки ці ресурси дуже обмежені. І досить часто (принаймні, з мого досвіду) проекти для підвищення ефективності (навіть як продуктивність системи, так і прискорення розвитку / менша кількість помилок) надають пріоритет нижче проектам, що підвищують функціональність. Хоча для нас технічних людей це неприємно, тому що ми розуміємо, що є довготривалими перевагами проектів рефакторингу, саме природа бізнесу полягає в тому, що менш технічним, діловим людям легше бачити прямий зв’язок між новою функціональністю та новою функціональністю дохід. Це зводиться до цього: "ми повернемось, щоб виправити це пізніше" == "

Зважаючи на це, якщо розмір даних є досить малим, щоб зміни можна було зробити дуже запитними, і / або у вас є вікно технічного обслуговування, яке достатньо довге, щоб не тільки внести зміни, але і відкати, якщо щось піде неправильно, то які нормалізують Eдо проміжної таблиці між таблицею базового класу і A& Bпідклас таблиця може працювати (хоча це все ще залишає вас без безпосереднього знання типу конкретної ( AабоB) у таблиці базового класу). АЛЕ, якщо у цих таблицях є сотні мільйонів рядків та неймовірна кількість коду, що посилається на таблиці (код, який повинен бути перевірений при внесенні змін), зазвичай це виходить більш прагматичним, ніж ідеалістичним. І це середовище, з яким мені довелося стикатися роками: 987 мільйонів рядків і 615 ГБ в таблиці базового класу, розкинуті на 18 серверах. І настільки багато коду потрапило в ці таблиці (таблиці базового класу та підкласу), що було багато опору - в основному з боку менеджменту, але іноді й з боку іншої команди - вносити будь-які зміни через кількість розробки та Ресурси забезпечення якості, які потрібно було б виділити.

Отже, знову ж таки, "найкращий" підхід можна визначити лише в кожному конкретному випадку: вам потрібно знати свою систему (тобто, скільки даних і як пов’язані таблиці та код), як виконати рефакторинг та людей з яким ви працюєте (ваша команда та, можливо, керівництво - чи можете ви придбати їх за такий проект?). Є деякі зміни, про які я згадував і планував протягом 1 - 2 років, і було потрібно кілька спринтів / релізів, щоб реалізувати, можливо, 85% з них. Але якщо у вас лише <1 мільйон рядків і не так багато коду прив’язане до цих таблиць, ви, ймовірно, зможете почати з більш ідеальної / "чистої" сторони речей.

Пам'ятайте лише, яким би способом ви не їхали, зверніть увагу на те, як ця модель працює принаймні протягом наступних 2 років (якщо можливо). Зверніть увагу на те, що спрацьовувало і що спричинило біль, навіть якщо це здавалося найбільшою ідеєю на той момент (а це означає, що вам також потрібно дозволити собі прийняти викручування - ми все це робимо - щоб ви могли чесно оцінити больові бали ). І зверніть увагу на те, чому певні рішення спрацювали чи ні, щоб наступний раз ви могли приймати рішення, які, швидше, будуть "кращими" :-).


17

За словами Мартіна Фаулера, існує три підходи до проблеми успадкування таблиці:

  • Успадкування однієї таблиці: одна таблиця представляє всі типи. Невикористані атрибути NULLed.
  • Успадкування конкретних таблиць : одна таблиця на конкретний тип, кожен стовпчик таблиці для кожного атрибуту типу. Немає відношення між таблицями.
  • Наслідування таблиці класів : одна таблиця на тип, кожна таблиця має атрибути лише для нових, не успадкованих атрибутів. Таблиці пов'язані між собою, що відображають фактичну ієрархію успадкування типу.

Ви можете почати з них як відправну точку пошуку плюсів і мінусів кожного підходу. Суть у тому, що всі підходи мають основні недоліки, і жоден не має переважних переваг. Більш відома як невідповідність об'єктного реляційного опору , ця проблема ще не повинна знайти рішення.

Особисто я вважаю , що тип проблем поганий реляційна конструкція може привести до порядки більш серйозним , ніж вид проблем , що виникають в зв'язку з поганим типом конструкції. Неправильний дизайн бази даних призводить до повільних запитів, аномалій оновлення, вибуху розміру даних, тупиків та невідповідних додатків, а також від десятків до сотень гігабайт даних, просочених у неправильному форматі . Неправильний дизайн призводить до утруднення технічного обслуговування та оновлення коду , а не до виконання. Тому в моїй книзі правильна реляційна конструкція перетворює будь-яку чистоту OO знову і знову.


@AlwaysLearningNewStuff Я думаю, що це питання є продовженням на dba.stackexchange.com/questions/139092 , правильно? У реалізації є у вас зробити є успадкування таблиці.
Рем Русану

Так, перш ніж задати це питання, я хотів переконатися, що я правильно зрозумів, як реалізувати конструкцію типу / підтипу. Зараз я стикаюся з описаною вище проблемою, коли деякі (але не всі!) Підкласи мають спільні атрибути. Мені було цікаво, чи є щось, що я можу зробити, щоб оптимізувати модель даних у такому випадку, а не ігнорувати цей нюанс ...
AlwaysLearningNewStuff

6

Відповідно до моєї інтерпретації ваших специфікацій, ви хочете знайти спосіб реалізації двох різних (але пов'язаних ) структур підтипів-підтипу .

Для того, щоб розкрити підхід до досягнення формульованого завдання, я збираюся додати до розглядуваного сценарію два класичні гіпотетичні типи сутності, які називаються, Fooі про Barякі я детальніше опишу нижче.

Правила бізнесу

Ось кілька тверджень, які допоможуть мені створити логічну модель:

  • A Foo is either one Bar or one C
  • A Foo is categorized by one FooType
  • A Bar is either one A or one C
  • A Bar is classified by one BarType

Логічна модель

Після цього отримана логічна модель IDEF1X [1] показана на рисунку 1 (і ви можете також завантажити її з Dropbox як PDF ):

Малюнок 1 - Модель даних про відносини гіпотетичного супертипу та підтипу

Додавання Foo і Bar

Я не додав Fooі Barщоб модель виглядала краще, але щоб зробити її більш виразною. Я вважаю, що вони важливі через наступне:

  • Як Aі Bподіляється названий атрибут E, ця ознака говорить про те, що вони є типами суб'єктності виразного (але спорідненого) свого роду поняття , події , особи , вимірювання тощо, які я представляв за допомогою типу Barнадхідності, який, у свою чергу, є тип підменю Foo, який містить Dатрибут у верхній частині ієрархії.

  • Так як Cтільки акції одного атрибута з іншими типами сутностей обговорюваних, тобто D, цей аспект інсінуірует , що це тип subentity іншого роду концепції , події , особи , вимірювання і т.д., тому я зобразив цю обставину, в силу Fooсупер тип об'єкта.

Однак це лише припущення, і оскільки реляційна база даних покликана точно відображати семантику певного бізнес-контексту , ви повинні визначити та класифікувати всі цікаві речі у вашому конкретному домені, щоб ви могли, точно, захопити більше сенсу .

Важливі фактори на стадії проектування

Цілком корисно усвідомлювати той факт, що, відкладаючи всю термінологію, виключний кластер супертипу і підтипу - це звичайне відношення. Опишемо ситуацію наступним чином:

  • Кожен випадок виняткової суперентності пов'язаний лише з одним доповненням типу підменю .

Таким чином, існує відповідність (або кардинальність) один-до-одного (1: 1) у цих випадках.

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

Бетонна структура DDL

І тоді я написав структуру DDL, засновану на логічній моделі, представленій вище:

CREATE TABLE FooType -- Look-up table.
(
    FooTypeCode     CHAR(2)  NOT NULL,
    Description     CHAR(90) NOT NULL, 
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_FooType             PRIMARY KEY (FooTypeCode),
    CONSTRAINT AK_FooType_Description UNIQUE      (Description)
);

CREATE TABLE Foo -- Supertype
(
    FooId           INT      NOT NULL, -- This PK migrates (1) to ‘Bar’ as ‘BarId’, (2) to ‘A’ as ‘AId’, (3) to ‘B’ as ‘BId’, and (4) to ‘C’ as ‘CId’.
    FooTypeCode     CHAR(2)  NOT NULL, -- Discriminator column.
    D               INT      NOT NULL, -- Column that applies to ‘Bar’ (and therefore to ‘A’ and ‘B’) and ‘C’.
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_Foo                 PRIMARY KEY (FooId),
    CONSTRAINT FK_from_Foo_to_FooType FOREIGN KEY (FooTypeCode)
        REFERENCES FooType (FooTypeCode)
);

CREATE TABLE BarType -- Look-up table.
(
    BarTypeCode CHAR(1)  NOT NULL,  
    Description CHAR(90) NOT NULL,  
    CONSTRAINT PK_BarType             PRIMARY KEY (BarTypeCode),
    CONSTRAINT AK_BarType_Description UNIQUE      (Description)
);

CREATE TABLE Bar -- Subtype of ‘Foo’.
(
    BarId       INT     NOT NULL, -- PK and FK.
    BarTypeCode CHAR(1) NOT NULL, -- Discriminator column. 
    E           INT     NOT NULL, -- Column that applies to ‘A’ and ‘B’.
    CONSTRAINT PK_Bar             PRIMARY KEY (BarId),
    CONSTRAINT FK_from_Bar_to_Foo FOREIGN KEY (BarId)
        REFERENCES Foo (FooId),
    CONSTRAINT FK_from_Bar_to_BarType FOREIGN KEY (BarTypeCode)
        REFERENCES BarType (BarTypeCode)    
);

CREATE TABLE A -- Subtype of ‘Bar’.
(
    AId INT NOT NULL, -- PK and FK.
    X   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_A             PRIMARY KEY (AId),
    CONSTRAINT FK_from_A_to_Bar FOREIGN KEY (AId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE B -- (1) Subtype of ‘Bar’ and (2) supertype of ‘A’ and ‘B’.
(
    BId INT NOT NULL, -- PK and FK.
    Y   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_B             PRIMARY KEY (BId),
    CONSTRAINT FK_from_B_to_Bar FOREIGN KEY (BId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE C -- Subtype of ‘Foo’.
(
    CId INT NOT NULL, -- PK and FK.
    Z   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_C             PRIMARY KEY (CId),
    CONSTRAINT FK_from_C_to_Foo FOREIGN KEY (FooId)
        REFERENCES Foo (FooId)  
);

Завдяки цій структурі ви уникаєте зберігання знаків NULL у ваших базових таблицях (або відношеннях ), що введе двозначність у вашу базу даних.

Цілісність, послідовність та інші міркування

Після реалізації вашої бази даних ви повинні переконатися, що (a) кожен ексклюзивний рядок супертипу завжди доповнюється відповідним підтипом підтипу, і, у свою чергу, гарантувати, що (b) такий рядок підтипу сумісний зі значенням, що міститься в стовпці дискримінатора супертипу . Тому досить зручно використовувати ACID TRANSACTIONSдля того, щоб переконатися, що ці умови виконуються у вашій базі даних.

Не слід відмовлятися від логічної обгрунтованості, самовираженості та точності вашої бази даних - це аспекти, які вирішально роблять вашу базу даних більш міцною.

Два раніше опубліковані відповіді вже містять відповідні моменти, які, безумовно, варто враховувати при розробці, створенні та керуванні вашою базою даних та її прикладними програмами.

Отримання даних за допомогою визначення VIEW

Ви можете налаштувати кілька представлень, що поєднують стовпці різних підтипових підтипових груп , щоб ви могли отримати дані під рукою без, наприклад, кожного разу писати необхідні пропозиції JOIN. Таким чином, ви можете ВИБІРИТИ безпосередньо З ГОЛОВНЯ ( похідне відношення або таблицю ), що цікавить, з легкістю.

Як бачите, «Тед» Кодд, безперечно, був генієм. Інструменти, які він заповів, досить міцні та елегантні, і, звичайно, добре поєднуються між собою.

Супутні ресурси

Якщо ви хочете проаналізувати деяку розгалужену базу даних, яка передбачає зв'язки між підтипом і підтипом, ви знайдете цінні неординарні відповіді, запропоновані компанією @PerformanceDBA на наступні питання переповнення стека:


Примітка

1. Визначення інтеграції для інформаційного моделювання ( IDEF1X ) - це дуже рекомендована методика моделювання даних, яка була встановлена ​​як стандарт в грудні 1993 р. Національним інститутом стандартів і технологій США ( NIST ). Він ґрунтується на (а) ранніх теоретичних матеріалах, автором яких є доктор Є.Ф. Кодд; на (б) в сутності-зв'язку з урахуванням даних, розробленого д - ром П. Ченом ; а також на (c) техніку проектування логічної бази даних, створену Робертом Г. Брауном. Варто зазначити, що IDEF1X був оформлений за допомогою логіки першого порядку.


Мої роботодавці змінили ділову логіку, Eзовсім усунувши ! Причина прийняття відповіді користувача srutzky полягає в тому, що вона дає хороші моменти, які допомагають мені прийняти рішення щодо вибору найбільш ефективного маршруту. Якби не це, я би прийняв вашу відповідь. Я вже відкликав вашу відповідь. Знову дякую!
ЗавждиНавчанняNewStuff
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.