Почему реляционная модель для базы данных имеет значение?


61

Я подхожу к проекту, где мне придется создать базу данных с моим боссом; мы очень маленький старт, поэтому рабочая среда глубоко личная.

Ранее он дал мне одну из баз данных компании, и она полностью противоречила тому, чему меня учили (и читали) в школе для RDBMS. Например, здесь есть целые базы данных, которые состоят из одной таблицы (для каждой независимой базы данных). Одна из этих таблиц имеет более 20 столбцов и для контекста, вот некоторые имена столбцов из одной таблицы:

lngStoreID | vrStoreName | lngCompanyID | vrCompanyName | lngProductID | vrProductName

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

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

Почему хорошая реляционная схема улучшает базу данных?


33
Одним словом: нормализация.
Роберт Харви

9
Близкий избиратель - оправдывайся! :-)
Робби Ди

6
Новые сотрудники часто критикуют установленные процедуры, не понимая их причин, даже если эти причины не являются технически обоснованными. Сначала выясните, почему ваш босс построил это так. Он / она может очень хорошо знать, что это не очень хороший дизайн, но у него нет знаний (или, скорее, времени), чтобы сделать это лучше. Любые предлагаемые вами изменения, вероятно, будут восприняты более позитивно, если вы с уважением признаете причины нынешнего дизайна.
Педро

5
He [the boss] had given me one of his databases before and it completely went against what I was taught (and read about) in school for RDBMS<- Добро пожаловать в реальный мир!
Möoz

5
Мне вспоминается моя любимая цитата из реляционной базы данных: «Нормализуй, пока не повредит, денормализуй, пока не сработает»
Джейк,

Ответы:


70

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

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

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


19
Одним из основных крайних случаев денормализации является хранилище данных , в частности, если у вас есть большой объем данных, который гарантированно никогда не изменится, и вы хотите запросить его быстрее и эффективнее за счет места для хранения. Хороший ответ, это просто FYI для любого новичка SQL, который не уверен, почему что-то кроме 3NF было бы желательно.


11
Я не уверен, почему аргумент последовательности «труднее понять». Мне кажется, намного проще: если значение изменяется, то все копии этого значения должны быть обновлены. Обновление одной копии намного менее подвержено ошибкам, чем обновление сотен или тысяч копий одних и тех же данных. Это в равной степени относится и к отношениям между данными. (Если у меня отношение хранится двумя способами, я должен обновить обе копии отношения.) Это чрезвычайно распространенная проблема в денормализованных БД; это очень трудно предотвратить эту коррупцию на практике (исключение материализуется использование типа вида).
jpmc26

4
Последний абзац должен быть выделен жирным шрифтом. :-) Без нормализации невозможно гарантировать целостность данных. Управление вводом данных исключительно на уровне бизнес-логики - дурацкое поручение, поскольку каждая ненормализованная база данных в конечном итоге демонстрирует своего рода аномалию данных.
Данк

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

24

Мне придется создать базу данных с моим боссом ...

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

lngStoreID | vrStoreName | lngCompanyID | vrCompanyName | lngProductID | vrProductName

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

Но ...

Приложение / запросы, использующие эти данные, отвечают медленно / медленно? Если нет, то настоящей проблемы не существует. Конечно, это выглядит и кажется уродливым, но если это сработает, вы не получите никаких «баллов», если предположить, что «могло бы быть» лучше.

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

Вполне возможно воссоздать его «просмотр одной таблицы» данных с помощью… ну .. видов.


11
Сопротивление единой таблице часто происходит от тех, кто не имеет опыта работы с SQL и не разбирается в соединениях - особенно в отношении отсутствующих данных, то есть внешних соединений.
Робби Ди

6
@RobbieDee Чаще всего это от людей, которые видели, как денормализованные данные искажаются из-за их непоследовательности. Я один такой человек. Я хотел бы рассмотреть такую ​​структуру только в ситуации, которую предлагает Фил: это своего рода таблица регистрации / отчетности, в которой данные никогда не будут обновляться или обновляться только после того, как будут очищены и полностью получены из других источников.
jpmc26

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

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

Возможно, практическим правилом было бы то, что если единственная сложность, представленная предложенной нормализацией, состоит в том, что теперь нескольким запросам нужно объединить их, чтобы выбрать все столбцы, о которых они должны сообщить, тогда вам нужно выполнить не ходить, чтобы внести это изменение: - )
Стив Джессоп

14

Почему хорошая реляционная схема улучшает базу данных?

Ответ: это не всегда улучшает базу данных. Вы должны знать, что то, чему вас, вероятно, учили, называется Третьей нормальной формой .

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

Правила 3NF устанавливают отношения между данными, которые «улучшают» базу данных:

  1. Предотвращение попадания недопустимых данных в вашу систему (если отношение равно 1: 1, оно вызывает ошибку, несмотря на код, написанный поверх него). Если ваши данные согласованы в базе данных, это менее вероятно приведет к несоответствиям за пределами вашей базы данных.

  2. Он обеспечивает способ проверки кода (например, отношение «многие к одному» является сигналом для ограничения свойств / поведения объекта). При написании кода для использования базы данных, иногда программисты замечают структуру данных как индикатор того, как должен работать их код. Или они могут предоставить полезную обратную связь, если база данных не соответствует их коду. (К сожалению, это больше похоже на желаемое за действительное)

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

  4. Правильные структуры базы данных приводят к повышению производительности благодаря соединению данных способами, которые минимизируют хранение данных, минимизируют обращения к хранилищу для извлечения данных, максимизируют ресурсы в памяти и / или минимизируют сортировку / манипулирование данными для конкретного набора данных, который у вас есть, по сравнению с запросом, которым вы являетесь выполняется против этого. Но «правильная» структура зависит от объема данных, характера данных, типа запроса, системных ресурсов и т. Д. Нормализация может ухудшить производительность (т. Е. Если вы загрузите все данные в виде 1 таблицы - объединение может замедлиться запрос). Обработка транзакций (OLTP) и бизнес-аналитика (хранилище данных) очень разные.

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

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

Удачи!


4
Не упомянутое, но я думаю, что важным моментом для программистов является то, что редактирование одной «вещи» требует редактирования только одной строки, а не зацикливания всей базы данных, чтобы найти и заменить эту единственную вещь.
slebetman

@slebetman У вас никогда не должно быть цикла на стороне кода для обновления нескольких строк в одной таблице, независимо от того, нормализована ли она. Используйте WHEREпредложение. Конечно, они все еще могут пойти не так, но это менее вероятно в нормализованной ситуации, поскольку вам нужно сопоставить только одну строку с помощью первичного ключа.
jpmc26

@ jpmc26: Зацикливая базу данных, я имею в виду создание запроса для обновления всех затронутых строк. Иногда достаточно ГДЕ. Но я видел нечестивые структуры, которые требуют подвыборов в одну и ту же таблицу, чтобы получить все затронутые строки, не затрагивая строки, которые не должны изменяться. Я даже видел структуры, в которых один запрос не может выполнить работу (сущность, которая нуждается в изменении, находится в разных столбцах в зависимости от строки)
slebetman

Много отличных ответов на этот вопрос, и это не было исключением.
Майк Чемберлен

11

Существует множество причин, по которым использование одной большой «таблицы богов» плохо. Я попытаюсь проиллюстрировать проблемы с помощью примера базы данных. Предположим, вы пытаетесь смоделировать спортивные события. Скажем, вы хотите смоделировать игры и команды, играющие в эти игры. Конструкция с несколькими таблицами может выглядеть следующим образом (это очень упрощенно, поэтому не попадите в места, где можно применить больше нормализации):

Teams
Id | Name | HomeCity

Games
Id | StartsAt | HomeTeamId | AwayTeamId | Location

и база данных одной таблицы будет выглядеть так

TeamsAndGames
Id | TeamName | TeamHomeCity | GameStartsAt | GameHomeTeamId | GameAwayTeamId | Location

Во-первых, давайте посмотрим на создание индексов на этих таблицах. Если бы мне нужен был указатель на родной город для команды, я мог бы легко добавить его в Teamsтаблицу или TeamsAndGamesтаблицу. Помните, что всякий раз, когда вы создаете индекс, его нужно где-то хранить на диске и обновлять по мере добавления строк в таблицу. В случае с Teamsтаблицей это довольно просто. Я положил в новую команду, база данных обновляет индекс. Но как насчет TeamsAndGames? Ну, то же самое относится и кTeamsпример. Я добавляю команду, индекс обновляется. Но это также происходит, когда я добавляю игру! Даже если это поле будет нулевым для игры, индекс все равно должен быть обновлен и сохранен на диске для этой игры в любом случае. Для одного индекса это звучит не так уж плохо. Но когда вам нужно много индексов для множества сущностей, втиснутых в эту таблицу, вы тратите много места на хранение индексов и много процессорного времени, обновляя их для вещей, к которым они не применяются.

Во-вторых, согласованность данных. В случае использования двух отдельных таблиц я могу использовать внешние ключи от Gamesтаблицы к Teamsстолу, чтобы определить, какие команды играют в игре. И при условии , я делаю HomeTeamIdи AwayTeamIdстолбцы не обнуляемым, база данных будет гарантировать , что каждая игра , которую я поставил в есть 2 команды , и что существуют эти команды в моей базе данных. Но как насчет сценария с одним столом? Ну, поскольку в этой таблице есть несколько сущностей, эти столбцы должны быть обнуляемыми (вы можете сделать их не обнуляемыми и засунуть туда данные мусора, но это просто ужасная идея). Если эти столбцы обнуляются, база данных больше не может гарантировать, что при вставке игры в нее входят две команды.

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

Вы можете попытаться смягчить эти проблемы, сказав: «Ну, мы просто позаботимся о том, чтобы мы никогда не делали этого в коде». Если вы уверены в своей способности писать код без ошибок в первый раз и в своей способности учитывать каждую странную комбинацию вещей, которые может попробовать пользователь, продолжайте. Лично я не уверен в своей способности делать что-либо из этого, поэтому я позволю базе данных дать мне дополнительную сеть безопасности.

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

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

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

Наконец, разработайте что-нибудь, что будет легко поддерживать. То, что это «работает», не означает, что все в порядке. Попытка поддерживать таблицы богов (например, классы богов) - это кошмар. Вы просто настраиваете себя на боль позже.


1
"Команды: Id | Имя | HomeCity". Просто убедитесь, что ваша схема данных не заставляет ваше приложение неправильно утверждать, что Супер Боул XXXIV был выигран LA Rams. Принимая во внимание, что SB XXXIV должен появиться в запросе для всех чемпионатов, выигранных командой, в настоящее время известной как LA Rams. Есть все лучше и хуже "бог таблицы", и вы, безусловно, представил плохой. Лучше было бы «идентификатор игры | название команды хозяев | город команды хозяев | название команды гостей | город гостей | игра начинается в | и т. Д.». Это первая попытка смоделировать такую ​​информацию, как «Святые из Нового Орлеана @ Chicago Bears 1p Eastern».
Стив Джессоп

6

Цитата дня: « Теория и практика должны быть одинаковыми ... в теории »

Денормализованный стол

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

  • Он содержит избыточные копии отношений (например, IngCompanyIDи vrCompanyName). Обновление основных данных может потребовать обновления гораздо большего количества строк, чем в нормализованной схеме.
  • Это смешивает все. Вы не можете обеспечить простой контроль доступа на уровне базы данных, например, гарантировать, что пользователь A может обновлять только информацию о компании, а пользователь B - только информацию о продукте.
  • Вы не можете обеспечить правила согласованности на уровне базы данных (например, первичный ключ для обеспечения того, чтобы для идентификатора компании было только одно название компании).
  • Вы не можете полностью воспользоваться оптимизатором БД, который может определить оптимальные стратегии доступа для сложного запроса, используя преимущества размера нормализованных таблиц и статистики нескольких индексов. Это может быстро компенсировать ограниченную выгоду от избежания объединений.

Нормализованный стол

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

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


5

Я думаю, что есть как минимум две части вашего вопроса:

1. Почему объекты разных типов не должны храниться в одной и той же таблице?

Наиболее важными ответами здесь являются читаемость кода и скорость. Буква A SELECT name FROM companies WHERE id = ?гораздо более удобочитаема, чем буква A, SELECT companyName FROM masterTable WHERE companyId = ?и вы с меньшей вероятностью случайно запросите ерунду (например, SELECT companyName FROM masterTable WHERE employeeId = ?это будет невозможно, если компании и сотрудники хранятся в разных таблицах). Что касается скорости, данные из таблицы базы данных извлекаются либо путем последовательного чтения полной таблицы, либо путем чтения из индекса. И то, и другое быстрее, если таблица / индекс содержит меньше данных, и это так, если данные хранятся в разных таблицах (и вам нужно только прочитать одну из таблиц / индексов).

2. Почему объекты одного типа должны быть разделены на дочерние объекты, которые хранятся в разных таблицах?

Здесь причина в основном для предотвращения несоответствия данных. При использовании подхода единой таблицы для системы управления заказами вы можете сохранить имя клиента, адрес клиента и идентификатор продукта, заказанного клиентом, как единое целое. Если бы клиент заказал несколько продуктов, в вашей базе данных было бы несколько экземпляров имени и адреса клиента. В лучшем случае, вы просто получили дубликаты данных в вашей базе данных, что может немного замедлить их. Но хуже всего то, что кто-то (или какой-то код) допустил ошибку, когда данные были введены так, что компании в итоге получат разные адреса в вашей базе данных. Это само по себе достаточно плохо. Но если бы вы запросили адрес компании на основе ее названия (например,SELECT companyAddress FROM orders WHERE companyName = ? LIMIT 1) вы просто произвольно вернули бы один из двух адресов и даже не поняли бы, что было несоответствие. Но каждый раз, когда вы запускаете запрос, вы можете фактически получить другой адрес, в зависимости от того, как ваш запрос решается внутри СУБД. Это, вероятно, сломает ваше приложение где-то еще, и найти его причину будет очень сложно.

При использовании многостолового подхода вы бы поняли, что существует функциональная зависимость от названия компании до адреса компании (если у компании может быть только один адрес), вы бы хранили кортеж (companyName, companyAddress) в одной таблице (например, company) и кортеж (productId, companyName) в другой таблице (например order). UNIQUEОграничение на companyстоле может затем обеспечивать , что каждая компания имеет только один адрес в базе данных , так что никакого несоответствия адресов компании никогда не может возникнуть.

Примечание: на практике по причинам производительности вы, вероятно, сгенерировали бы уникальный идентификатор компании для каждой компании и использовали бы его в качестве внешнего ключа вместо непосредственного использования companyName. Но общий подход остается прежним.


3

TL; DR - Они проектируют базу данных, основываясь на том, как их учили в школе.

Я мог бы написать этот вопрос 10 лет назад. Мне потребовалось некоторое время, чтобы понять, почему мои предшественники создавали свои базы данных так, как они это делали. Вы работаете с кем-то, кто либо:

  1. Получил большинство своих навыков проектирования баз данных, используя Excel в качестве базы данных или
  2. Они используют лучшие практики, когда они вышли из школы.

Я не подозреваю, что это № 1, так как у вас на самом деле есть идентификационные номера в вашей таблице, поэтому я буду считать № 2

После того, как я вышел из школы, я работал в магазине, который использовал AS / 400 (он же IBM i). Я обнаружил некоторые странные вещи в том, как они проектировали свои базы данных, и начал выступать за то, чтобы мы внесли изменения, чтобы следовать тому, как меня учили, как проектировать базы данных. (Я тогда был глуп)

Потребовался терпеливый программист старшего возраста, чтобы объяснить мне, почему все так было сделано. Они не изменили схему, потому что это привело бы к поломке программ, которые были старше меня. Буквально, исходный код для одной программы имел дату создания за год до моего рождения. В системе, над которой мы работали, их программы должны были реализовывать всю логику и операции, которые планировщик запросов вашей базы данных обрабатывает для вас. (Вы можете увидеть это, запустив EXPLAIN для одного из ваших запросов)

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


Некоторые примеры того, с чем я столкнулся, не соответствовало реляционной модели:

  • Даты были сохранены как номера юлианских дней, которые требовали объединения таблицы дат, чтобы получить фактическую дату.
  • Денормализованные таблицы с последовательными столбцами одного типа (например code1,code2, ..., code20)
  • NXM длины столбцов CHAR, представляющих массив из N строк длиной M.

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

Даты - мне сказали, что для использования функции даты (какой месяц, день или день недели) для обработки даты понадобилось больше времени, чем для создания таблицы каждой возможной даты со всей этой информацией.

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

Столбцы CHAR длины NxM. Было проще поместить значения конфигурации в один столбец, чтобы уменьшить количество операций чтения файлов.

Плохо продуманный пример на С, эквивалентный для отражения среды программирования, которую они имели:

#define COURSE_LENGTH 4
#define NUM_COURSES 4
#define PERIOD_LENGTH 2

struct mytable {
    int id;
    char periodNames[NUM_COURSES * PERIOD_LENGTH];  // NxM CHAR Column
    char course1[COURSE_LENGTH];
    char course2[COURSE_LENGTH];
    char course3[COURSE_LENGTH];
    char course4[COURSE_LENGTH];
};

...

// Example row
struct mytable row = {.id= 1, .periodNames="HRP1P2P8", .course1="MATH", .course2="ENGL", .course3 = "SCI ", .course4 = "READ"};

char *courses; // Pointer used to access the sequential columns
courses = (char *)&row.course1;


for(int i = 0; i < NUM_COURSES; i++) {

    printf("%d: %.*s -> %.*s\n",i+1, PERIOD_LENGTH, &row.periodNames[PERIOD_LENGTH * i], COURSE_LENGTH,&courses[COURSE_LENGTH*i]);
}

Выходы

1: HR -> MATH
2: P1 -> ENGL
3: P2 -> SCI
4: P8 -> READ

Согласно тому, что мне сказали, в то время это считалось лучшей практикой.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.