Если, чисто теоретически представить, что пользователей миллионы, а постов десятки миллионов, является ли такая структура оптимальной?
Почти. Нужно применить дублирование и прокинуть в эту таблицу сразу все те поля, которые вам могут пригодиться для отображения, иначе каждый раз придется делать join, чего бы не хотелось при highload. То есть нужно добавить в таблицу сразу поля вроде user_name, post_title, post_body, и т.д(в общем все то, что вы планировали доставать с помощью join).
На счет "пользователей миллионы, а постов десятки миллионов":
Если у вас будет такое количество данных, то вам в любом случае в какой-то момент придется прибегнуть к горизонтальному шардингу, поэтому если считаете что проект реально может дорасти до такого количества данных лучше сразу учесть это и спроектировать базу данных так, чтобы горизонтальный шардинг не стал проблемой.
нужно ли тут поле id, или PK сделать составной (post_id, user_id) или PK вообще не нужен? Это влияет на селект?
Зависит от сценариев использования(подумайте, в каком случае вам нужно будет поле id), но в большинстве случаев оно не нужно и такие поля вводят для душевного спокойствия и гармонии. На селект это не влияет, ведь все равно вы будете делать выборку либо по user_id либо по post_id(опять же, это в большинстве распространенных сценариев, если у вас есть какая-то логика, где нужно будет выбирать из таблицы likes записи по какому-то намеренно введенному идентификтаору, то вводите).