web-dev-qa-db-ja.com

SQLクエリが複数のテーブルからデータを返す

私は以下のことを知りたいのですが。

  • データベース内の複数のテーブルからデータを取得する方法
  • これを行うにはどのような種類の方法がありますか?
  • 結合と共用体とは何ですか。また、それらは互いにどう違うのですか。
  • 他のものと比較して、いつそれぞれを使用すべきですか?

私は(例えば - PHP)アプリケーションでこれを使用することを計画していますが、データベースに対して複数のクエリを実行したくない、単一のクエリで複数のテーブルからデータを取得するためにどのようなオプションがありますか?

注:PHPキューで頻繁に出くわす多くの質問について、よく書かれたガイドにリンクできるように、これを書いています。答えを投稿します。

答えは次のとおりです。

  1. 第1部 - 結合と組合
  2. パート2 - サブクエリ
  3. パート3 - トリックと効率的なコード
  4. 第4部 - From句の中の副問い合わせ
  5. 第5部 - John's Tricksのミックスバッグ
414
Fluffeh

わかりました。この投稿は非常に興味深いものであり、クエリの作成に関する知識を共有したいと思います。これをありがとうFluffeh。これを読んで、私が間違っていると感じるかもしれない他の人は、私の答えを自由に編集して批判することができます。 (正直なところ、私は自分の間違いを修正してくれたことにとても感謝している。

MySQLタグでよくある質問をいくつか投稿します。


トリックNo.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の有効な値が1つしかないということです。たとえば、最初の条件はtrueを返し、2番目の条件は常にfalseです。したがって、AND演算子を使用すると、両方の条件が真になります。そうでない場合、falseになります。別のクエリは次のようなものです。

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少なくともが1つ一致するレコードと一致するため、結果は依然として正しくありません。 実際のソリューション映画ごとのレコードインスタンスの数をカウントすることによります。インスタンスの数は、条件で指定された値の総数と一致する必要があります。

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(答え)


トリックNo.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');

質問

各ソフトウェアの最新バージョンを見つけます。次の列を表示します:SoftwareNameDescriptionsLatestVersionfrom VersionNo column)、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は、いくつかのRDBMSが既にサポートしているように、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(答え)


それで終わりました。 MySQLタグで他のFAQを思い出すと、すぐに別の投稿を行います。この小さな記事を読んでくれてありがとう。これから少しでも知識を得ていただければ幸いです。

更新1


トリックNo. 3(2つのID間の最新レコードの検索

与えられたスキーマ

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');

質問

2人のユーザー間の最新の会話を見つけます。

ソリューション

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

98
John Woo

パート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)

残念ながら、それはできません。その理由は、車のID(列a.ID)を追加するときにグループに追加する必要があるためです。したがって、カウント関数が機能するとき、IDごとに一致するIDは1つだけです。

ただし、ここでサブクエリを使用できます。実際には、これに必要な同じ結果を返す2つの完全に異なるタイプのサブクエリを実行できます。最初の方法は、サブクエリを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)

そして、バム!ただし、気付いた場合は、返されるデータの各行ごとにこのサブクエリを実行する必要があります。この小さな例でも、車のブランドは5つしかありませんが、返されるデータの行が11行あるため、サブクエリは11回実行されました。したがって、この場合、コードを記述する最も効率的な方法とは思えません。

別のアプローチでは、サブクエリを実行して、テーブルのふりをしましょう。

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)

わかりましたので、結果は同じです(順序は少し異なります-データベースは今回選択した最初の列で並べられた結果を返したいようです)-正しい数字は同じです。

それで、2つの違いは何ですか?また、各タイプのサブクエリをいつ使用する必要がありますか?最初に、その2番目のクエリがどのように機能するかを確認します。クエリのfrom句で2つのテーブルを選択し、クエリを作成して、実際にはテーブルであるとデータベースに伝えました。データベースはこれで問題ありません。 canこの方法を使用するといくつかの利点があります(いくつかの制限もあります)。何よりもまず、このサブクエリはonceを実行したことです。データベースに大量のデータが含まれている場合、最初の方法よりも大幅に改善される可能性があります。ただし、これをテーブルとして使用しているため、追加のデータ行を取り込む必要があります。これにより、実際にデータ行に結合できるようになります。また、上記のクエリのように単純な結合を使用する場合は、十分な行のデータがあることを確認する必要があります。思い出すと、結合はboth結合の両側に一致するデータがある行のみをプルバックします。注意を怠ると、このサブクエリに一致する行がない場合、有効なデータがcarsテーブルから返されない可能性があります。

さて、最初のサブクエリを振り返ると、いくつかの制限もあります。データを単一の行に戻すので、ONLYデータの1行を戻すことができます。クエリのselect句で使用されるサブクエリは、sumcountmaxなどの集計関数または他の同様の集計関数のみを使用することが非常に多くあります。それらはhave toではありませんが、多くの場合、そのように書かれています。

したがって、次に進む前に、サブクエリを使用できる他の場所を簡単に見てみましょう。 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という文字が含まれるブランドIDとブランド名のリストが返されます(2番目の列はブランドを表示するためにのみ追加されます)。

これで、このクエリの結果をwhere句thisで使用できます。

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)

ご覧のとおり、サブクエリは3つのブランドIDを返していましたが、carsテーブルにはそのうち2つのブランドIDのみが含まれていました。

この場合、詳細については、次のコードを記述したかのようにサブクエリが機能しています。

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句に、さらに2つはwhere句に配置できます。クエリをより複雑にし、実行に時間がかかる可能性があります。

効率的なコードを作成する必要がある場合は、さまざまな方法でクエリを作成し、結果を取得するための最適なクエリを(タイミングを計るか、またはEXPLAIN PLANを使用して)確認することをお勧めします。動作する最初の方法は、常にそれを行う最良の方法であるとは限りません。

62
Fluffeh

パート3-トリックと効率的なコード

MySQL in()効率

出てきたヒントやコツのために、もう少し追加するつもりだ。

2つのテーブルから一致しない行を取得するにはどうすればよいですかです。そして、次のようなものとして最も一般的に受け入れられている答えがわかります(車とブランドのテーブルに基づく) -Holdenがブランドとしてリストされていますが、carsテーブルには表示されません):

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

そして、yes動作します。

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

ただし、一部のデータベースではnotで効率的です。ここに Stack Overflowの質問へのリンク について質問し、ここに 詳細な記事 がらくたになりたい場合にあります。

簡単な答えは、オプティマイザーがそれを効率的に処理しない場合、次のようなクエリを使用して一致しない行を取得する方がはるかに良い場合があるということです。

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)

サブクエリの同じテーブルでテーブルを更新する

ああ、もう一つの古いけれど良いもの-古いFROM句で更新するターゲットテーブル 'brands'を指定することはできません

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

それで、私たちはそれをすることができませんね?まあ、正確ではありません。驚くほど多くのユーザーが知らない卑劣な回避策があります-ただし、注意が必要なハッカーがいくつか含まれています。

サブクエリを別のサブクエリ内に固定することができます。これにより、2つのクエリ間に十分なギャップができ、機能します。ただし、クエリをトランザクション内に固定するのが最も安全である可能性があることに注意してください。これにより、クエリの実行中にテーブルに他の変更が加えられなくなります。

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
58
Fluffeh

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

必要なだけテーブルを使用できます。テーブルサブクエリの中であっても、必要なところであればどこでも外部ジョインと共用体を使用してください。

これは、テーブルやフィールドと同じ数の要素を含めるのが非常に簡単な方法です。

17
prashant1988

この記事を読んでいるときに、テーブルが見つかることを願います。

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    |                |
+-------+-------------+------+-----+---------+----------------+
6
Anton Chan