SQL-запрос возвращает данные из нескольких таблиц

349

Я хотел бы знать следующее:

  • как получить данные из нескольких таблиц в моей базе данных?
  • какие типы методов существуют для этого?
  • Что такое объединения и союзы и как они отличаются друг от друга?
  • Когда я должен использовать каждый из них по сравнению с другими?

Я планирую использовать это в своем (например, PHP) приложении, но не хочу запускать несколько запросов к базе данных, какие параметры мне нужно получить из нескольких таблиц в одном запросе?

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

Ответы охватывают следующее:

Теги:
select

6 ответов

392
Лучший ответ

Часть 1 - Соединения и союзы

Этот ответ охватывает:

Существует несколько способов извлечения данных из нескольких таблиц в базе данных. В этом ответе я буду использовать синтаксис объединения ANSI-92. Это может отличаться от ряда других обучающих программ, которые используют более старый синтаксис ANSI-89 (и если вы привыкли к 89, может показаться гораздо менее интуитивным), но все, что я могу сказать, это попробовать), поскольку это намного проще чтобы понять, когда запросы начинают усложняться. Зачем использовать его? Есть ли выигрыш в производительности? короткий ответ - нет, но его легче читать, как только вы привыкнете к нему. Легче читать запросы, написанные другими людьми, используя этот синтаксис.

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

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

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

mysql> create table colors(id int(3) not null auto_increment primary key, 
    -> color varchar(15), paint varchar(10));
Query OK, 0 rows affected (0.01 sec)

mysql> show columns from colors;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(3)      | NO   | PRI | NULL    | auto_increment |
| color | varchar(15) | YES  |     | NULL    |                |
| paint | varchar(10) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
3 rows in set (0.01 sec)

mysql> insert into colors (color, paint) values ('Red', 'Metallic'), 
    -> ('Green', 'Gloss'), ('Blue', 'Metallic'), 
    -> ('White' 'Gloss'), ('Black' 'Gloss');
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> select * from colors;
+----+-------+----------+
| id | color | paint    |
+----+-------+----------+
|  1 | Red   | Metallic |
|  2 | Green | Gloss    |
|  3 | Blue  | Metallic |
|  4 | White | Gloss    |
|  5 | Black | Gloss    |
+----+-------+----------+
5 rows in set (0.00 sec)

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

mysql> create table brands (id int(3) not null auto_increment primary key, 
    -> brand varchar(15));
Query OK, 0 rows affected (0.01 sec)

mysql> show columns from brands;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(3)      | NO   | PRI | NULL    | auto_increment |
| brand | varchar(15) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.01 sec)

mysql> insert into brands (brand) values ('Ford'), ('Toyota'), 
    -> ('Nissan'), ('Smart'), ('BMW');
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> select * from brands;
+----+--------+
| id | brand  |
+----+--------+
|  1 | Ford   |
|  2 | Toyota |
|  3 | Nissan |
|  4 | Smart  |
|  5 | BMW    |
+----+--------+
5 rows in set (0.00 sec)

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

mysql> create table models (id int(3) not null auto_increment primary key, 
    -> model varchar(15));
Query OK, 0 rows affected (0.01 sec)

mysql> show columns from models;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(3)      | NO   | PRI | NULL    | auto_increment |
| model | varchar(15) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> insert into models (model) values ('Sports'), ('Sedan'), ('4WD'), ('Luxury');
Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> select * from models;
+----+--------+
| id | model  |
+----+--------+
|  1 | Sports |
|  2 | Sedan  |
|  3 | 4WD    |
|  4 | Luxury |
+----+--------+
4 rows in set (0.00 sec)

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

mysql> create table cars (id int(3) not null auto_increment primary key, 
    -> color int(3), brand int(3), model int(3));
Query OK, 0 rows affected (0.01 sec)

mysql> show columns from cars;
+-------+--------+------+-----+---------+----------------+
| Field | Type   | Null | Key | Default | Extra          |
+-------+--------+------+-----+---------+----------------+
| id    | int(3) | NO   | PRI | NULL    | auto_increment |
| color | int(3) | YES  |     | NULL    |                |
| brand | int(3) | YES  |     | NULL    |                |
| model | int(3) | YES  |     | NULL    |                |
+-------+--------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> insert into cars (color, brand, model) values (1,2,1), (3,1,2), (5,3,1), 
    -> (4,4,2), (2,2,3), (3,5,4), (4,1,3), (2,2,1), (5,2,3), (4,5,1);
Query OK, 10 rows affected (0.00 sec)
Records: 10  Duplicates: 0  Warnings: 0

mysql> select * from cars;
+----+-------+-------+-------+
| id | color | brand | model |
+----+-------+-------+-------+
|  1 |     1 |     2 |     1 |
|  2 |     3 |     1 |     2 |
|  3 |     5 |     3 |     1 |
|  4 |     4 |     4 |     2 |
|  5 |     2 |     2 |     3 |
|  6 |     3 |     5 |     4 |
|  7 |     4 |     1 |     3 |
|  8 |     2 |     2 |     1 |
|  9 |     5 |     2 |     3 |
| 10 |     4 |     5 |     1 |
+----+-------+-------+-------+
10 rows in set (0.00 sec)

Это даст нам достаточное количество данных (я надеюсь), чтобы скрыть приведенные ниже примеры разных типов объединений, а также предоставить достаточное количество данных, чтобы сделать их полезными.

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

Это простое объединение двух таблиц. У нас есть таблица, которая идентифицирует модель и таблицу с доступным запасом в ней. Как вы можете видеть, данные в столбце model таблицы cars относятся к столбцу models таблицы cars, которую мы имеем. Теперь мы знаем, что таблица моделей имеет идентификатор 1 для Sports, поэтому давайте напишем соединение.

select
    ID,
    model
from
    cars
        join models
            on model=ID

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

ERROR 1052 (23000): Column 'ID' in field list is ambiguous

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

select
    cars.ID,
    models.model
from
    cars
        join models
            on cars.model=models.ID

+----+--------+
| ID | model  |
+----+--------+
|  1 | Sports |
|  3 | Sports |
|  8 | Sports |
| 10 | Sports |
|  2 | Sedan  |
|  4 | Sedan  |
|  5 | 4WD    |
|  7 | 4WD    |
|  9 | 4WD    |
|  6 | Luxury |
+----+--------+
10 rows in set (0.00 sec)

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

select
    a.ID,
    b.model
from
    cars a
        join models b
            on a.model=b.ID

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

Очевидно, нам нужно добавить предложение where к нашему запросу. Мы можем идентифицировать спортивные автомобили либо ID=1, либо model='Sports'. По мере индексирования идентификатора и первичного ключа (а это, как правило, меньше ввода), используйте его в нашем запросе.

select
    a.ID,
    b.model
from
    cars a
        join models b
            on a.model=b.ID
where
    b.ID=1

+----+--------+
| ID | model  |
+----+--------+
|  1 | Sports |
|  3 | Sports |
|  8 | Sports |
| 10 | Sports |
+----+--------+
4 rows in set (0.00 sec)

Бинго! Босс счастлив. Конечно, будучи боссом и никогда не доволен тем, что он просил, он просматривает информацию, затем говорит, что мне нужны и цвета.

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

select
    a.ID,
    b.model
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
where
    b.ID=1

+----+--------+
| ID | model  |
+----+--------+
|  1 | Sports |
|  3 | Sports |
|  8 | Sports |
| 10 | Sports |
+----+--------+
4 rows in set (0.00 sec)

Черт, хотя таблица была правильно соединена и связанные столбцы были связаны, мы забыли извлечь фактическую информацию из новой таблицы, которую мы только что связали.

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
where
    b.ID=1

+----+--------+-------+
| ID | model  | color |
+----+--------+-------+
|  1 | Sports | Red   |
|  8 | Sports | Green |
| 10 | Sports | White |
|  3 | Sports | Black |
+----+--------+-------+
4 rows in set (0.00 sec)

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

Вполне возможно сохранить связь между таблицами и таблицами таким образом.

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
where
    b.ID=1

Хотя я забыл включить таблицу, в которой мы могли бы присоединиться к более чем одному столбцу в инструкции join, вот пример. Если таблица models имела брендовые модели и поэтому также имела столбец с именем brand, который привязан к таблице brands в поле ID, это можно сделать следующим образом:

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
            and b.brand=d.ID
where
    b.ID=1

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

Итак, чтобы привести пример декартового объединения, запустите следующий запрос:

select
    a.ID,
    b.model
from
    cars a
        join models b

+----+--------+
| ID | model  |
+----+--------+
|  1 | Sports |
|  1 | Sedan  |
|  1 | 4WD    |
|  1 | Luxury |
|  2 | Sports |
|  2 | Sedan  |
|  2 | 4WD    |
|  2 | Luxury |
|  3 | Sports |
|  3 | Sedan  |
|  3 | 4WD    |
|  3 | Luxury |
|  4 | Sports |
|  4 | Sedan  |
|  4 | 4WD    |
|  4 | Luxury |
|  5 | Sports |
|  5 | Sedan  |
|  5 | 4WD    |
|  5 | Luxury |
|  6 | Sports |
|  6 | Sedan  |
|  6 | 4WD    |
|  6 | Luxury |
|  7 | Sports |
|  7 | Sedan  |
|  7 | 4WD    |
|  7 | Luxury |
|  8 | Sports |
|  8 | Sedan  |
|  8 | 4WD    |
|  8 | Luxury |
|  9 | Sports |
|  9 | Sedan  |
|  9 | 4WD    |
|  9 | Luxury |
| 10 | Sports |
| 10 | Sedan  |
| 10 | 4WD    |
| 10 | Luxury |
+----+--------+
40 rows in set (0.00 sec)

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

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

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

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
where
    b.ID=1
    or b.ID=3

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

Мы знаем, что следующее возвращение всех спортивных автомобилей:

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
where
    b.ID=1

И следующее вернет все 4WD:

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
where
    b.ID=3

Итак, добавив предложение union all между ними, результаты второго запроса будут добавлены к результатам первого запроса.

select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
where
    b.ID=1
union all
select
    a.ID,
    b.model,
    c.color
from
    cars a
        join models b
            on a.model=b.ID
        join colors c
            on a.color=c.ID
        join brands d
            on a.brand=d.ID
where
    b.ID=3

+----+--------+-------+
| ID | model  | color |
+----+--------+-------+
|  1 | Sports | Red   |
|  8 | Sports | Green |
| 10 | Sports | White |
|  3 | Sports | Black |
|  5 | 4WD    | Green |
|  7 | 4WD    | White |
|  9 | 4WD    | Black |
+----+--------+-------+
7 rows in set (0.00 sec)

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

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

  • Типы столбцов из первого запроса должны соответствовать типам столбцов из каждого другого запроса ниже.
  • Имена столбцов из первого запроса будут использоваться для идентификации всего набора результатов.
  • Количество столбцов в каждом запросе должно быть одинаковым.

Теперь вы можете задаваться вопросом, что именноразница заключается в использовании union и union all. Запрос union будет удалять дубликаты, а union all - нет. Это означает, что при использовании union over union all наблюдается небольшое повышение производительности, но результаты могут быть полезны - я не буду спекулировать на таких вещах в этом, хотя.

В этой заметке, возможно, стоит отметить некоторые дополнительные примечания.

  • Если мы хотим заказать результаты, мы можем использовать order by, но вы больше не сможете использовать этот псевдоним. В вышеприведенном запросе добавление order by a.ID приведет к ошибке - насколько это касается результатов, столбец называется ID, а не a.ID - хотя в обоих запросах использовался один и тот же псевдоним.
  • У нас может быть только один оператор order by, и он должен быть последним.

В следующих примерах я добавляю несколько дополнительных строк в наши таблицы.

Я добавил Holden в таблицу брендов. Я также добавил строку в cars, которая имеет значение color 12 - которое не имеет ссылки в таблице цветов.

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

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

select
    a.brand
from
    brands a

+--------+
| brand  |
+--------+
| Ford   |
| Toyota |
| Nissan |
| Smart  |
| BMW    |
| Holden |
+--------+
6 rows in set (0.00 sec)

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

select
    a.brand
from
    brands a
        join cars b
            on a.ID=b.brand
group by
    a.brand

+--------+
| brand  |
+--------+
| BMW    |
| Ford   |
| Nissan |
| Smart  |
| Toyota |
+--------+
5 rows in set (0.00 sec)

Что, конечно, проблема - мы не видим упоминания о прекрасном бренде Holden, который я добавил.

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

select
    a.brand
from
    brands a
        left outer join cars b
            on a.ID=b.brand
group by
    a.brand

+--------+
| brand  |
+--------+
| BMW    |
| Ford   |
| Holden |
| Nissan |
| Smart  |
| Toyota |
+--------+
6 rows in set (0.00 sec)

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

select
    a.brand,
    count(b.id) as countOfBrand
from
    brands a
        left outer join cars b
            on a.ID=b.brand
group by
    a.brand

+--------+--------------+
| brand  | countOfBrand |
+--------+--------------+
| BMW    |            2 |
| Ford   |            2 |
| Holden |            0 |
| Nissan |            1 |
| Smart  |            1 |
| Toyota |            5 |
+--------+--------------+
6 rows in set (0.00 sec)

И с этим, прочь босс скроется.

Теперь, чтобы объяснить это более подробно, внешние соединения могут быть типа left или right. Левая или правая определяет, какая таблица полностью включена. A left outer join будет включать в себя все строки из таблицы слева, а (как вы догадались) a right outer join выводит все результаты из таблицы справа в результаты.

Некоторые базы данных позволят full outer join, которые возвращают результаты (независимо от того, соответствует ли они или нет) из обеих таблиц, но это не поддерживается во всех базах данных.

Теперь, вероятно, я думаю, что в этот момент вам интересно, можете ли вы объединить типы соединений в запросе - и ответ да, вы абсолютно можете.

select
    b.brand,
    c.color,
    count(a.id) as countOfBrand
from
    cars a
        right outer join brands b
            on b.ID=a.brand
        join colors c
            on a.color=c.ID
group by
    a.brand,
    c.color

+--------+-------+--------------+
| brand  | color | countOfBrand |
+--------+-------+--------------+
| Ford   | Blue  |            1 |
| Ford   | White |            1 |
| Toyota | Black |            1 |
| Toyota | Green |            2 |
| Toyota | Red   |            1 |
| Nissan | Black |            1 |
| Smart  | White |            1 |
| BMW    | Blue  |            1 |
| BMW    | White |            1 |
+--------+-------+--------------+
9 rows in set (0.00 sec)

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

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

select
    a.brand,
    c.color,
    count(b.id) as countOfBrand
from
    brands a
        left outer join cars b
            on a.ID=b.brand
        left outer join colors c
            on b.color=c.ID
group by
    a.brand,
    c.color

+--------+-------+--------------+
| brand  | color | countOfBrand |
+--------+-------+--------------+
| BMW    | Blue  |            1 |
| BMW    | White |            1 |
| Ford   | Blue  |            1 |
| Ford   | White |            1 |
| Holden | NULL  |            0 |
| Nissan | Black |            1 |
| Smart  | White |            1 |
| Toyota | NULL  |            1 |
| Toyota | Black |            1 |
| Toyota | Green |            2 |
| Toyota | Red   |            1 |
+--------+-------+--------------+
11 rows in set (0.00 sec)

Как мы видим, у нас есть два внешних объединения в запросе, и результаты идут, как ожидалось.

Теперь, как насчет тех других типов объединений, которые вы задаете? Что относительно пересечений?

Ну, не все базы данных поддерживают intersection, но почти все базы данных позволят вам создать пересечение через объединение (или хорошо структурированный оператор where).

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

Простой пример будет таким:

select
    *
from
    colors
where
    ID>2
intersect
select
    *
from
    colors
where
    id<4

В то время как обычный запрос union возвращает все строки таблицы (первый запрос возвращает что-либо по ID>2, а второе - ID<4), что приведет к полному набору, запрос на пересечение будет только верните строку, соответствующую id=3, поскольку она соответствует обоим критериям.

Теперь, если ваша база данных не поддерживает запрос intersect, вышесказанное может быть легко выполнено со следующим запросом:

select
    a.ID,
    a.color,
    a.paint
from
    colors a
        join colors b
            on a.ID=b.ID
where
    a.ID>2
    and b.ID<4

+----+-------+----------+
| ID | color | paint    |
+----+-------+----------+
|  3 | Blue  | Metallic |
+----+-------+----------+
1 row in set (0.00 sec)

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

  • 2
    @Fluffeh Хорошие ответы. У меня есть предложение: если вы хотите сделать его учебником по SQL, вам не хватает только добавления диаграмм Венна; Я сразу понял, что левое и правое соединения благодаря им. Личный запрос: есть ли у вас учебник по распространенным ошибкам / настройке производительности?
  • 22
    Боже мой Мое колесо прокрутки сломано. Отличный вопрос и ответ. Я бы хотел, чтобы я проголосовал за это 10 раз.
Показать ещё 5 комментариев
92

Хорошо, я нашел этот пост очень интересным, и я хотел бы поделиться некоторыми своими знаниями о создании запроса. Спасибо за это Fluffeh. Другие, которые могут это прочитать и могут чувствовать, что я ошибаюсь, на 101% свободны в редактировании и критике моего ответа. (Честно говоря, я очень благодарен за исправление моей ошибки.)

Я буду размещать некоторые часто задаваемые вопросы в теге MySQL.


Трюк № 1 (строки, соответствующие нескольким условиям)

Учитывая эту схему

CREATE TABLE MovieList
(
    ID INT,
    MovieName VARCHAR(25),
    CONSTRAINT ml_pk PRIMARY KEY (ID),
    CONSTRAINT ml_uq UNIQUE (MovieName)
);

INSERT INTO MovieList VALUES (1, 'American Pie');
INSERT INTO MovieList VALUES (2, 'The Notebook');
INSERT INTO MovieList VALUES (3, 'Discovery Channel: Africa');
INSERT INTO MovieList VALUES (4, 'Mr. Bean');
INSERT INTO MovieList VALUES (5, 'Expendables 2');

CREATE TABLE CategoryList
(
    MovieID INT,
    CategoryName VARCHAR(25),
    CONSTRAINT cl_uq UNIQUE(MovieID, CategoryName),
    CONSTRAINT cl_fk FOREIGN KEY (MovieID) REFERENCES MovieList(ID)
);

INSERT INTO CategoryList VALUES (1, 'Comedy');
INSERT INTO CategoryList VALUES (1, 'Romance');
INSERT INTO CategoryList VALUES (2, 'Romance');
INSERT INTO CategoryList VALUES (2, 'Drama');
INSERT INTO CategoryList VALUES (3, 'Documentary');
INSERT INTO CategoryList VALUES (4, 'Comedy');
INSERT INTO CategoryList VALUES (5, 'Comedy');
INSERT INTO CategoryList VALUES (5, 'Action');

Вопрос

Найдите все фильмы, которые принадлежат, по крайней мере, к категории Comedy и Romance.

Решение

Этот вопрос может быть очень сложным. Может показаться, что такой запрос будет ответом: -

SELECT  DISTINCT a.MovieName
FROM    MovieList a
        INNER JOIN CategoryList b
            ON a.ID = b.MovieID
WHERE   b.CategoryName = 'Comedy' AND
        b.CategoryName = 'Romance'

SQLFiddle Demo

что, безусловно, очень неправильно, потому что оно не дает результата. Объяснение этого состоит в том, что в каждой строке есть только одно допустимое значение CategoryName. Например, первое условие возвращает true, второе условие всегда ложно. Таким образом, используя оператор AND, оба условия должны быть истинными; в противном случае оно будет ложным. Другой запрос выглядит следующим образом:

SELECT  DISTINCT a.MovieName
FROM    MovieList a
        INNER JOIN CategoryList b
            ON a.ID = b.MovieID
WHERE   b.CategoryName IN ('Comedy','Romance')

SQLFiddle Demo

и результат по-прежнему неверен, так как он соответствует записи, имеющей хотя бы одно совпадение на CategoryName. реальное решение было бы путем подсчета количества экземпляров записей на фильм. Число экземпляров должно совпадать с общим количеством значений, заданных в условии.

SELECT  a.MovieName
FROM    MovieList a
        INNER JOIN CategoryList b
            ON a.ID = b.MovieID
WHERE   b.CategoryName IN ('Comedy','Romance')
GROUP BY a.MovieName
HAVING COUNT(*) = 2

SQLFiddle Demo (ответ)


Трюк № 2 (максимальная запись для каждой записи)

Данная схема,

CREATE TABLE Software
(
    ID INT,
    SoftwareName VARCHAR(25),
    Descriptions VARCHAR(150),
    CONSTRAINT sw_pk PRIMARY KEY (ID),
    CONSTRAINT sw_uq UNIQUE (SoftwareName)  
);

INSERT INTO Software VALUES (1,'PaintMe','used for photo editing');
INSERT INTO Software VALUES (2,'World Map','contains map of different places of the world');
INSERT INTO Software VALUES (3,'Dictionary','contains description, synonym, antonym of the words');

CREATE TABLE VersionList
(
    SoftwareID INT,
    VersionNo INT,
    DateReleased DATE,
    CONSTRAINT sw_uq UNIQUE (SoftwareID, VersionNo),
    CONSTRAINT sw_fk FOREIGN KEY (SOftwareID) REFERENCES Software(ID)
);

INSERT INTO VersionList VALUES (3, 2, '2009-12-01');
INSERT INTO VersionList VALUES (3, 1, '2009-11-01');
INSERT INTO VersionList VALUES (3, 3, '2010-01-01');
INSERT INTO VersionList VALUES (2, 2, '2010-12-01');
INSERT INTO VersionList VALUES (2, 1, '2009-12-01');
INSERT INTO VersionList VALUES (1, 3, '2011-12-01');
INSERT INTO VersionList VALUES (1, 2, '2010-12-01');
INSERT INTO VersionList VALUES (1, 1, '2009-12-01');
INSERT INTO VersionList VALUES (1, 4, '2012-12-01');

Вопрос

Найдите последнюю версию для каждого программного обеспечения. Отобразите следующие столбцы: SoftwareName, Descriptions, LatestVersion (из столбца VersionNo), DateReleased

Решение

Некоторые разработчики SQL ошибочно используют агрегированную функцию MAX(). Они имеют тенденцию создавать, как это,

SELECT  a.SoftwareName, a.Descriptions,
        MAX(b.VersionNo) AS LatestVersion, b.DateReleased
FROM    Software a
        INNER JOIN VersionList b
            ON a.ID = b.SoftwareID
GROUP BY a.ID
ORDER BY a.ID

SQLFiddle Demo

(большинство RDBMS генерируют синтаксическую ошибку для этого из-за отсутствия указания некоторых неагрегированных столбцов в предложении group by), результат дает правильное LatestVersion для каждого программного обеспечения, но, очевидно, DateReleased неверны. MySQL не поддерживает Window Functions и Common Table Expression, но, как это делают некоторые СУБД. Обходной путь этой проблемы заключается в создании subquery, который получает индивидуальный максимум versionNo для каждого программного обеспечения, а затем присоединяется к другим таблицам.

SELECT  a.SoftwareName, a.Descriptions,
        b.LatestVersion, c.DateReleased
FROM    Software a
        INNER JOIN
        (
            SELECT  SoftwareID, MAX(VersionNO) LatestVersion
            FROM    VersionList
            GROUP BY SoftwareID
        ) b ON a.ID = b.SoftwareID
        INNER JOIN VersionList c
            ON  c.SoftwareID = b.SoftwareID AND
                c.VersionNO = b.LatestVersion
GROUP BY a.ID
ORDER BY a.ID

SQLFiddle Demo (ответ)


Вот и все. Я скоро отправлю еще раз, когда вспомню другие FAQ по тегу MySQL. Спасибо, что прочитали эту небольшую статью. Я надеюсь, что вы по крайней мере получите от этого немного знаний.

ОБНОВЛЕНИЕ 1


Трюк № 3 (поиск последней записи между двумя идентификаторами)

Данная схема

CREATE TABLE userList
(
    ID INT,
    NAME VARCHAR(20),
    CONSTRAINT us_pk PRIMARY KEY (ID),
    CONSTRAINT us_uq UNIQUE (NAME)  
);

INSERT INTO userList VALUES (1, 'Fluffeh');
INSERT INTO userList VALUES (2, 'John Woo');
INSERT INTO userList VALUES (3, 'hims056');

CREATE TABLE CONVERSATION
(
    ID INT,
    FROM_ID INT,
    TO_ID INT,
    MESSAGE VARCHAR(250),
    DeliveryDate DATE
);

INSERT INTO CONVERSATION VALUES (1, 1, 2, 'hi john', '2012-01-01');
INSERT INTO CONVERSATION VALUES (2, 2, 1, 'hello fluff', '2012-01-02');
INSERT INTO CONVERSATION VALUES (3, 1, 3, 'hey hims', '2012-01-03');
INSERT INTO CONVERSATION VALUES (4, 1, 3, 'please reply', '2012-01-04');
INSERT INTO CONVERSATION VALUES (5, 3, 1, 'how are you?', '2012-01-05');
INSERT INTO CONVERSATION VALUES (6, 3, 2, 'sample message!', '2012-01-05');

Вопрос

Найдите последний разговор между двумя пользователями.

Решение

SELECT    b.Name SenderName,
          c.Name RecipientName,
          a.Message,
          a.DeliveryDate
FROM      Conversation a
          INNER JOIN userList b
            ON a.From_ID = b.ID
          INNER JOIN userList c
            ON a.To_ID = c.ID
WHERE     (LEAST(a.FROM_ID, a.TO_ID), GREATEST(a.FROM_ID, a.TO_ID), DeliveryDate)
IN
(
    SELECT  LEAST(FROM_ID, TO_ID) minFROM,
            GREATEST(FROM_ID, TO_ID) maxTo,
            MAX(DeliveryDate) maxDate
    FROM    Conversation
    GROUP BY minFROM, maxTo
)

SQLFiddle Demo

  • 0
    Потрясающие! Предостережение Джон, ваше первое решение работает только потому, что существует уникальное ограничение на два поля. Вы могли бы использовать более общее решение, чтобы помочь с общей проблемой. На мой взгляд, единственное решение - сделать индивидуальный выбор для comedy и romance . Having подходит тогда ..
  • 0
    @nawfal не совсем, если уникальное ограничение не было добавлено, вам нужно добавить distinct в имеющее предложение SQLFiddle Demo : D
Показать ещё 1 комментарий
54

Часть 3 - Трюки и эффективный код

MySQL в() эффективности

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

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

select
    a.ID,
    a.brand
from
    brands a
where
    a.ID not in(select brand from cars)

И да, это сработает.

+----+--------+
| ID | brand  |
+----+--------+
|  6 | Holden |
+----+--------+
1 row in set (0.00 sec)

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

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

select
    a.brand
from
    brands a
        left join cars b
            on a.id=b.brand
where
    b.brand is null

+--------+
| brand  |
+--------+
| Holden |
+--------+
1 row in set (0.00 sec)

Обновить таблицу с той же таблицей в подзапросе

Ahhh, другое старое, но goodie - старое. Вы не можете указывать целевые таблицы "бренды" для обновления в предложении FROM.

MySQL не позволит вам запускать запрос update... с подзапросом в той же таблице. Теперь, возможно, вы думаете, почему бы просто не пощекотать его в предложение where? Но что делать, если вы хотите обновить только строку с датой max(), чтобы добавить несколько строк? Вы не можете точно сделать это в предложении where.

update 
    brands 
set 
    brand='Holden' 
where 
    id=
        (select 
            id 
        from 
            brands 
        where 
            id=6);
ERROR 1093 (HY000): You can't specify target table 'brands' 
for update in FROM clause

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

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

update 
    brands 
set 
    brand='Holden' 
where id=
    (select 
        id 
    from 
        (select 
            id 
        from 
            brands 
        where 
            id=6
        ) 
    as updateTable);

Query OK, 0 rows affected (0.02 sec)
Rows matched: 1  Changed: 0  Warnings: 0
  • 3
    Просто хочу заметить, что конструкция WHERE NOT EXISTS () в значительной степени идентична с «точки зрения эффективности», но, на мой взгляд, намного легче читать / понимать. С другой стороны, мои знания ограничены MSSQL, и я не могу поклясться, верно ли это на других платформах.
  • 0
    Я только что попробовал этот тип сравнения на днях, когда NOT IN () имел список из нескольких сотен идентификаторов, и не было никакой разницы между ним и присоединенной версией запроса. Возможно, это имеет значение, когда вы встаете на тысячи или миллиарды.
54

Часть 2 - Подзапросы

Хорошо, теперь босс снова ворвался - мне нужен список всех наших автомобилей с брендом и в целом, сколько у этого бренда есть!

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

По нашему запросу сначала дадим простой запрос, в котором будут указаны каждый автомобиль и бренд:

select
    a.ID,
    b.brand
from
    cars a
        join brands b
            on a.brand=b.ID

Теперь, если бы мы хотели просто получить количество машин, отсортированных по марке, мы могли бы, конечно, написать это:

select
    b.brand,
    count(a.ID) as countCars
from
    cars a
        join brands b
            on a.brand=b.ID
group by
    b.brand

+--------+-----------+
| brand  | countCars |
+--------+-----------+
| BMW    |         2 |
| Ford   |         2 |
| Nissan |         1 |
| Smart  |         1 |
| Toyota |         5 |
+--------+-----------+

Итак, мы должны иметь возможность просто добавить функцию count в наш исходный запрос?

select
    a.ID,
    b.brand,
    count(a.ID) as countCars
from
    cars a
        join brands b
            on a.brand=b.ID
group by
    a.ID,
    b.brand

+----+--------+-----------+
| ID | brand  | countCars |
+----+--------+-----------+
|  1 | Toyota |         1 |
|  2 | Ford   |         1 |
|  3 | Nissan |         1 |
|  4 | Smart  |         1 |
|  5 | Toyota |         1 |
|  6 | BMW    |         1 |
|  7 | Ford   |         1 |
|  8 | Toyota |         1 |
|  9 | Toyota |         1 |
| 10 | BMW    |         1 |
| 11 | Toyota |         1 |
+----+--------+-----------+
11 rows in set (0.00 sec)

К сожалению, нет, мы не можем этого сделать. Причина в том, что когда мы добавляем идентификатор автомобиля (столбец a.ID), мы должны добавить его в группу с помощью - так что теперь, когда функция count работает, есть только один ID для каждого идентификатора.

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

select
    a.ID,
    b.brand,
    (
    select
        count(c.ID)
    from
        cars c
    where
        a.brand=c.brand
    ) as countCars
from
    cars a
        join brands b
            on a.brand=b.ID

+----+--------+-----------+
| ID | brand  | countCars |
+----+--------+-----------+
|  2 | Ford   |         2 |
|  7 | Ford   |         2 |
|  1 | Toyota |         5 |
|  5 | Toyota |         5 |
|  8 | Toyota |         5 |
|  9 | Toyota |         5 |
| 11 | Toyota |         5 |
|  3 | Nissan |         1 |
|  4 | Smart  |         1 |
|  6 | BMW    |         2 |
| 10 | BMW    |         2 |
+----+--------+-----------+
11 rows in set (0.00 sec)

И Бам!, это сделало бы нас. Если вы заметили, этот подпроцесс должен будет запускаться для каждой и каждой строки данных, которые мы возвращаем. Даже в этом маленьком примере у нас есть только пять разных брендов автомобилей, но подзапрос работает одиннадцать раз, так как у нас есть одиннадцать строк данных, которые мы возвращаем. Таким образом, в этом случае это не похоже на самый эффективный способ написания кода.

Для другого подхода давайте запустим подзапрос и сделаем вид, что это таблица:

select
    a.ID,
    b.brand,
    d.countCars
from
    cars a
        join brands b
            on a.brand=b.ID
        join
            (
            select
                c.brand,
                count(c.ID) as countCars
            from
                cars c
            group by
                c.brand
            ) d
            on a.brand=d.brand

+----+--------+-----------+
| ID | brand  | countCars |
+----+--------+-----------+
|  1 | Toyota |         5 |
|  2 | Ford   |         2 |
|  3 | Nissan |         1 |
|  4 | Smart  |         1 |
|  5 | Toyota |         5 |
|  6 | BMW    |         2 |
|  7 | Ford   |         2 |
|  8 | Toyota |         5 |
|  9 | Toyota |         5 |
| 10 | BMW    |         2 |
| 11 | Toyota |         5 |
+----+--------+-----------+
11 rows in set (0.00 sec)

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

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

Теперь, оглядываясь на первый подзапрос, есть некоторые ограничения. потому что мы возвращаем данные обратно в одну строку, мы можем ТОЛЬКО отбрасывать одну строку данных. В подзапросах, используемых в предложении select запроса, очень часто используется только агрегатная функция, такая как sum, count, max или другая аналогичная агрегированная функция. Они не обязательно, но это часто, как они написаны.

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

select
    ID,
    brand
from
    brands
where
    brand like '%o%'

+----+--------+
| ID | brand  |
+----+--------+
|  1 | Ford   |
|  2 | Toyota |
|  6 | Holden |
+----+--------+
3 rows in set (0.00 sec)

Это возвращает нам список идентификаторов брендов и фирменных наименований (второй столбец добавляется только для показа наших брендов), которые содержат название o в названии.

Теперь мы можем использовать результаты этого запроса в предложении where:

select
    a.ID,
    b.brand
from
    cars a
        join brands b
            on a.brand=b.ID
where
    a.brand in
        (
        select
            ID
        from
            brands
        where
            brand like '%o%'
        )

+----+--------+
| ID | brand  |
+----+--------+
|  2 | Ford   |
|  7 | Ford   |
|  1 | Toyota |
|  5 | Toyota |
|  8 | Toyota |
|  9 | Toyota |
| 11 | Toyota |
+----+--------+
7 rows in set (0.00 sec)

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

В этом случае, для более подробной информации, подзапрос работает так, как если бы мы написали следующий код:

select
    a.ID,
    b.brand
from
    cars a
        join brands b
            on a.brand=b.ID
where
    a.brand in (1,2,6)

+----+--------+
| ID | brand  |
+----+--------+
|  1 | Toyota |
|  2 | Ford   |
|  5 | Toyota |
|  7 | Ford   |
|  8 | Toyota |
|  9 | Toyota |
| 11 | Toyota |
+----+--------+
7 rows in set (0.00 sec)

Опять же, вы можете увидеть, как подзапрос и ручные входы изменили порядок строк при возврате из базы данных.

Пока мы обсуждаем подзапросы, давайте посмотрим, что еще мы можем сделать с подзапросом:

  • Вы можете поместить подзапрос в другой подзапрос и т.д. и т.д. Существует предел, который зависит от вашей базы данных, но меньше рекурсивных функций какого-то безумного и маниакального программиста, большинство людей никогда не достигнут этого предела.
  • Вы можете поместить несколько подзапросов в один запрос, несколько в предложении select, некоторые в предложении from и еще пара в предложении where - просто помните, что каждый из них вы помещаете в делает ваш запрос более сложным и, скорее всего, займет больше времени.

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

  • 0
    Очень важно для новых разработчиков: подзапросы, вероятно, запускаются один раз для каждого результата, если вы не можете использовать подзапрос как объединение (показано выше).
15

Вы можете использовать концепцию нескольких запросов в ключе FROM. Позвольте мне показать вам один пример:

SELECT DISTINCT e.id,e.name,d.name,lap.lappy LAPTOP_MAKE,c_loc.cnty COUNTY    
FROM  (
          SELECT c.id cnty,l.name
          FROM   county c, location l
          WHERE  c.id=l.county_id AND l.end_Date IS NOT NULL
      ) c_loc, emp e 
      INNER JOIN dept d ON e.deptno =d.id
      LEFT JOIN 
      ( 
         SELECT l.id lappy, c.name cmpy
         FROM   laptop l, company c
         WHERE l.make = c.name
      ) lap ON e.cmpy_id=lap.cmpy

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

Это очень простой способ привлечь столько же таблиц и полей.

2

Надеюсь, это заставит его найти таблицы, когда вы читаете эту вещь:

jsfiddle

mysql> show columns from colors;                                                         
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+           
| id    | int(3)      | NO   | PRI | NULL    | auto_increment |
| color | varchar(15) | YES  |     | NULL    |                |
| paint | varchar(10) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+

Ещё вопросы

Сообщество Overcoder
Наверх
Меню