使用SELECT ... GROUP BY ...在SQLite中使用

时间:2014-08-19 23:14:16

标签: sql sqlite

我在Teach Yourself SQL程序GalaXQL(基于SQLite)中练习练习17。我有三张桌子:

  • Stars包含starid;
  • Planets包含planetidstarid;
  • Moons包含moonidplanetid

我想要返回与最大数量的行星和卫星相关联的starid

我的查询将返回staridplanetidtotal行星+卫星。

如何更改此查询以使其仅返回与starid对应的单个max(total)而不是表格?这是我到目前为止的查询:

select
    stars.starid as sid,
    planets.planetid as pid,
    (count(moons.moonid)+count(planets.planetid)) as total
from stars, planets, moons
where planets.planetid=moons.planetid and stars.starid=planets.starid
group by stars.starid

2 个答案:

答案 0 :(得分:28)

让我们可视化可能由此数据库结构表示的系统,并查看我们是否无法将您的问题转换为可用的SQL。

我画了一个星系:

badly drawn planetary systems

为了区分恒星和行星与卫星,我使用了starid值的大写罗马数字和moonid值的小写罗马数字。既然每个人都知道天文学家在天文台的那些漫长的夜晚与饮酒无关,我会在planetid值的中间留下无法解释的空隙。当使用所谓的"代理"时会发生这样的差距。 ID,因为它们的值没有意义;它们只是行的唯一标识符。

如果你想跟随,here's the galaxy naively loaded into SQL Fiddle(如果你得到一个关于切换到WebSQL的弹出窗口,你可能需要点击"取消"并坚持使用SQL.js这个例子可以工作)。

让我们看看,你又想要什么?

  

我想要返回与最大数量的行星和卫星相结合的starid

真棒。重新说明,问题是:哪个恒星与最大数量的轨道物体有关?

  • Star(I)有1个拥有3个卫星的行星;
  • 星(II)有1个行星,1个月球,1个行星,2个月亮;
  • 星(III)有1个行星,1个月球和2个没有卫星的行星。

我们在这里所做的只是计算与每颗恒星相关的不同实体。共有5个轨道物体,星(II)是胜利者!因此,我们对工作查询的最终结果是:

| starid |
|--------|
| 2      |

我故意画了这个真棒星系,以便赢得"赢得"恒星没有最多的行星,也没有与卫星最多的行星相关联。如果那些天文学家不是所有的三张风,我也可能从行星(1)中获得额外的月亮,这样我们的获胜明星就不会被大部分卫星所束缚。如果星标(II)仅回答我们要求的问题,而不是任何其他与可能类似的查询相关的问题,那么在此演示中对我们来说非常方便,以减少通过错误查询得出正确答案的机会。

在翻译中丢失

我想要做的第一件事是向您介绍显式的JOIN语法。这将是你非常亲密的朋友。无论一些愚蠢的教程说什么,你总是JOIN你的桌子。相信我的愚蠢建议(可选择阅读Explicit vs implicit SQL joins)。

显式JOIN语法显示了我们如何要求我们的表彼此关联并保留WHERE子句仅用于过滤行的唯一目的结果集。有a few different types,但我们要开始的是一个普通的INNER JOIN。这基本上是您的原始查询执行的内容,它意味着您希望在结果集中看到的所有内容都是在所有三个表中重叠的数据。查看原始查询的框架:

SELECT ... FROM stars, planets, moons
WHERE planets.planetid = moons.planetid 
    AND planets.starid = stars.starid;

鉴于这些条件,在空间中某个与星形无关的孤立星球(即其staridNULL)会发生什么?由于孤立的行星与stars表格无重叠,因此INNER JOIN不会将其包含在结果集中。

在SQL中,NULL的任何相等或不等式比较都会得到NULL的结果 - 即使NULL = NULL也不是真的!现在您的查询有问题,因为另一个条件是planets.planetid = moons.planetid。如果存在没有相应月球的行星,则会变成planets.planetid = NULL并且行星将不会出现在您的查询结果中。这不好!必须计算孤独的行星!

OUTER限制

幸运的是,JOINOUTER JOINLEFT,这将确保表中至少一个始终显示在我们的结果集中。它们有RIGHTJOIN种类,以指示相对于INNER关键字的位置,哪个表获得特殊处理。 What joins does SQLite support?确认OUTERLEFT JOIN关键字是可选的,因此我们可以使用stars,注意:

  • planetsstarid由公共planets;
  • 相关联
  • moonsplanetid由公共stars;
  • 相关联
  • moonsSELECT * FROM stars LEFT JOIN planets ON stars.starid = planets.starid LEFT JOIN moons ON planets.planetid = moons.planetid; 通过以上两个链接间接链接;
  • 我们总是想要计算所有行星和所有卫星。
WHERE

请注意,没有一个大包o'表和ON子句,现在每个JOIN都有一个starid子句。当您发现自己使用更多表时,这将更容易阅读;并且因为这是标准语法,所以它在SQL数据库之间相对可移植。

在太空中迷失

我们的新查询基本上抓住了我们数据库中的所有。但这是否与我们银河系中的所有相对应?实际上,这里有一些冗余,因为我们的两个ID字段(planetidSELECT *)存在于多个表中。这只是在实际用例中避免table_name AS alias catch-all语法的众多原因之一。我们只需要三个ID字段,而且在我们处理它的时候我会再投入两个技巧:

  1. 别名!您可以使用starid语法为表提供更方便的名称。当您必须在多表查询中引用许多不同的列并且您不希望每次都输入完整的表名时,这非常方便。
  2. planets表中抓取stars,然后将JOIN完全从stars LEFT JOIN planets ON stars.starid = planets.starid中删除!拥有starid意味着SELECT p.starid, -- This could be S.starid, if we kept using `stars` p.planetid, m.moonid FROM planets AS p LEFT JOIN moons AS m ON p.planetid = m.planetid; 字段将是相同的,无论我们从哪个表中得到它 - 只要该星有任何行星。如果我们计算星星,我们需要此表,但我们正在计算行星和卫星;根据定义轨道行星的卫星,所以没有行星的恒星也没有卫星,可以忽略。 (这是一个假设;检查你的数据,以确保它是合理的!也许你的天文学家比平时喝得更多!)
  3. | starid | planetid | moonid |
    |--------|----------|--------|
    |      1 |        1 |      1 |
    |      1 |        1 |      2 |
    |      1 |        1 |      3 |
    |      2 |        2 |      6 |
    |      2 |        3 |      4 |
    |      2 |        3 |      5 |
    |      3 |        7 |        |
    |      3 |        8 |      7 |
    |      3 |        9 |        |
    

    结果:

    SELECT
        p.starid,
        p.planetid,
        COUNT(m.moonid) AS moon_count
    FROM
        planets AS p
            LEFT JOIN
        moons AS m ON p.planetid = m.planetid
    GROUP BY p.starid, p.planetid;
    

    数学!

    现在我们的任务是决定哪个星星是赢家,为此我们必须做一些简单的计算。让我们先计算一下卫星;因为他们没有孩子"并且只有一个父母"每个,他们很容易聚合:

    | starid | planetid | moon_count |
    |--------|----------|------------|
    |      1 |        1 |          3 |
    |      2 |        2 |          1 |
    |      2 |        3 |          2 |
    |      3 |        7 |          0 |
    |      3 |        8 |          1 |
    |      3 |        9 |          0 |
    

    结果:

    COUNT(*)

    (注意:通常我们喜欢使用NULL,因为输入和阅读都很简单,但这会让我们遇到麻烦!因为我们的两行都有moonid moon_countwe have to use COUNT(moonid) to avoid counting moons that don't exist的价值。)

    到目前为止,非常好 - 我看到六颗行星,我们知道每颗恒星属于哪颗恒星,并为每颗行星显示正确数量的卫星。下一步,计算行星。您可能认为这需要一个子查询,以便为每个行星添加GROUP BY列,但它实际上比这简单;如果我们moon_count明星,我们的SELECT p.starid, COUNT(p.planetid) AS planet_count, COUNT(m.moonid) AS moon_count FROM planets AS p LEFT JOIN moons AS m ON p.planetid = m.planetid GROUP BY p.starid; 将从每个星球上计算"卫星,每颗星"到每颗星的卫星"卫星这很好:

    | starid | planet_count | moon_count |
    |--------|--------------|------------|
    |      1 |            3 |          3 |
    |      2 |            3 |          3 |
    |      3 |            3 |          1 |
    

    结果:

    moon_count

    现在我们遇到了麻烦。 planet_count是正确的,但您应该立即看到starid错误。为什么是这样?回顾未分组的查询结果,注意有九行,每行planetid有三行,每行有一个非空值 DISTINCT。这就是我们要求数据库使用此查询计算的内容,当我们真正要问的是有多少不同的行星?行星(1)与恒星(I)出现三次,但每次都是同一行星。修复方法是将COUNT()关键字粘贴到<{em} SELECT p.starid, COUNT(DISTINCT p.planetid)+ COUNT(m.moonid) AS total_bodies FROM planets AS p LEFT JOIN moons AS m ON p.planetid = m.planetid GROUP BY p.starid; 函数调用中。同时,我们可以将两列一起添加:

    | starid | total_bodies |
    |--------|--------------|
    |      1 |            4 |
    |      2 |            5 |
    |      3 |            4 |
    

    结果:

    total_bodies

    获胜者是......

    计算图形中每颗星周围的轨道体,我们可以看到ORDER BY列是正确的。但你没有要求所有这些信息;你只想知道谁赢了。嗯,有很多方法可以实现,并且根据银河系(数据库)的大小和构成,有些方法可能比其他方法更有效。一种方法是total_bodies LIMIT 1表达式,以便&#34;赢家&#34;显示在顶部starid,以便我们看不到输家,只选择ORDER BY列(see it on SQL Fiddle)。

    这种方法的问题在于它隐藏了联系。如果我们把星系中的失落恒星分别给予额外的行星或月亮呢?现在我们已经获得了三方关系 - 每个人都是赢家!但是当我们WITH body_counts AS (SELECT p.starid, COUNT(DISTINCT p.planetid) + COUNT(m.moonid) AS total_bodies FROM planets AS p LEFT JOIN moons AS m ON p.planetid = m.planetid GROUP BY p.starid) SELECT starid FROM body_counts WHERE total_bodies = (SELECT MAX(total_bodies) FROM body_counts); 一个总是相同的价值时,谁首先出现?在SQL标准中,这是未定义的;没有人知道谁会名列前茅。您可以对同一数据运行两次相同的查询,并获得两个不同的结果!

    出于这个原因,您可能更愿意询问哪些明星拥有最多的轨道物体,而不是在您的问题中指明您知道只有一个值。这是一种更典型的基于集合的方法,在使用关系数据库时习惯基于集合的思维并不是一个坏主意。在执行查询之前,您不知道结果集的大小;如果你认为那里并不是第一名的并列,你必须以某种方式证明这一假设。 (由于天文学家经常发现新的卫星和行星,我很难证明这一天!)

    我更喜欢编写此查询的方式是使用称为公用表表达式(CTE)的方法。这些在SQLite的最新版本和many other databases中都受支持,但最后我检查过GalaXQL使用的是旧版本的SQLite引擎并未包含此功能。 CTE允许您使用别名多次引用子查询,而不是每次都必须完整地写出子查询。使用CTE的解决方案可能如下所示:

    | STARID |
    |--------|
    |      2 |
    

    结果:

    MAX()

    Check out this query in action on SQLFiddle。要确认此查询在平局情况下可以显示多行,请尝试将最后一行的MIN()更改为body_counts

    只为你

    在没有CTE的情况下执行此操作很难看,但如果表大小可管理,则可以执行此操作。查看上面的查询,我们的CTE别名为FROM,我们在WHERE子句和body_counts子句中将其引用两次。我们可以用我们用来定义SELECT starid FROM (SELECT p.starid, COUNT(DISTINCT p.planetid) + COUNT(m.moonid) AS total_bodies FROM planets AS p LEFT JOIN moons AS m ON p.planetid = m.planetid GROUP BY p.starid) WHERE total_bodies = (SELECT MAX(total_bodies) FROM (SELECT COUNT(DISTINCT p.planetid)+ COUNT(m.moonid) AS total_bodies FROM planets AS p LEFT JOIN moons AS m ON p.planetid = m.planetid GROUP BY p.starid) ); 的语句替换这两个引用(在第二个子查询中删除id列一次,但不使用它):

    {{1}}

    这是一种适合友好的方法,应该在GalaXQL中适用于您。看到它正常工作here in SQLFiddle

    既然您已经看过两者,那么CTE版本是否更容易理解? MySQL,didn't support CTEs until the 2018 release of version 8.0additionally demand aliases for our subqueries。幸运的是,SQLite没有,因为在这种情况下,它只是额外的措辞,可以添加到已经过于复杂的查询中。

    嗯,这很有趣 - 你有没有问过这个问题? ;)

    (P.S。,如果你想知道第9号行星是什么:巨型太空薯片往往有非常古怪的轨道。)

答案 1 :(得分:1)

也许这就是你想要的东西?

select
stars.starid as sid, 
(count(distinct moons.moonid)+count(distinct planets.planetid)) as total 
from stars
left join planets on stars.starid=planets.starid 
left join moons on planets.planetid=moons.planetid 
group by stars.starid
order by 2 desc
limit 1

Sample SQL Fiddle