【问题标题】:SELECT between dynamically queried rows在动态查询的行之间选择
【发布时间】:2019-08-27 20:46:27
【问题描述】:

假设我有一个书桌:

CREATE TABLE book (
    -- NOTE: the app guarantees that content is ordered by id
    id INTEGER PRIMARY KEY,
    section TEXT NOT NULL,
    verse INTEGER NOT NULL,
    content TEXT NOT NULL
);

INSERT INTO book (id, section, verse, content) VALUES
    (0, "Prelude", 0, "A long long time ago"),
    (1, "Prelude", 1, "I can still remember"),
    (2, "Chap",    0, "Something happened"),
    (3, "Chap",    1, "Something else happened"),
    (4, "Chap",    2, "A weighty climax"),
    (5, "End",     0, "The end")
;

我希望能够只用一个 SQL 查询来查询起始诗句和章节中的所有诗句。我可以使用以下 SQL 来做到这一点:

SELECT id, content
FROM book
WHERE
    id BETWEEN
        (SELECT id FROM book WHERE section == "Prelude" AND verse == 1 LIMIT 1)
    AND
        (SELECT id FROM book WHERE section == "Chap" AND verse == 2 LIMIT 1)

λ sqlite3 :memory: < tmp.sql
id          content
----------  --------------------
1           I can still remember
2           Something happened
3           Something else happe
4           A weighty climax

这涉及 2 个子查询,我不确定这是不是最好的方法。我可以改进此查询以使其不包含子查询(考虑到子查询越少效率越高)?

【问题讨论】:

    标签: sql sqlite subquery range


    【解决方案1】:

    BETWEEN 子句之后的代码是扫描两次表以返回 2 个 id。
    但还有另一个问题:
    你提前知道哪个id最小哪个最高?
    如果不是(可能),那么您无法在AND 之前或之后安全地设置每个返回的 ID。
    例如,如果您这样做:

    id BETWEEN
        (SELECT id FROM book WHERE section == "Chap" AND verse == 2 LIMIT 1)
    AND
        (SELECT id FROM book WHERE section == "Prelude" AND verse == 1 LIMIT 1)
    

    不会返回任何内容。
    因此,您必须将 min id 设置为下限,将 max id 设置为上限。

    使用CTE,这样表只会被扫描一次以获取开始和结束ID:

    WITH cte AS (
      SELECT MIN(id) AS fromId, MAX(id) AS toId FROM book 
      WHERE (section = "Prelude" AND verse = 1) OR (section = "Chap" AND verse = 2)
    )
    SELECT id, content
    FROM book
    WHERE id BETWEEN (SELECT fromId FROM cte) AND (SELECT toId FROM cte)
    

    请参阅demo
    或者使用 CTE 进行 CROSS JOIN:

    WITH cte AS (
      SELECT MIN(id) AS fromId, MAX(id) AS toId FROM book 
      WHERE (section = "Prelude" AND verse = 1) OR (section = "Chap" AND verse = 2)
    )
    SELECT b.id, b.content
    FROM book AS b CROSS JOIN cte AS c
    WHERE b.id BETWEEN c.fromId AND c.toId
    

    请参阅demo
    结果:

    | id  | content                 |
    | --- | ----------------------- |
    | 1   | I can still remember    |
    | 2   | Something happened      |
    | 3   | Something else happened |
    | 4   | A weighty climax        |
    

    【讨论】:

      【解决方案2】:

      您的查询很好(尽管它可能与我写的不完全一样)。您想要的性能是book(section, verse) 上的索引:

      create index idx_book_section_verse on book(section, verse);
      

      有了这样的索引,我可能会将条件移至FROM 子句:

      SELECT b.id, b.content
      FROM book b CROSS JOIN
           (SELECT id
            FROM book
            WHERE section = 'Prelude' AND verse = 1 
           ) id1 CROSS JOIN
           (SELECT id
            FROM book
            WHERE section = 'Chap' AND verse = 2 
           ) id2
      WHERE b.id BETWEEN id1.id AND id2.id;
      

      这种形式(或您的形式)中的每个子查询实际上只是“浸入”索引中以获取一个值。这相当快。然后WHERE 子句在主键上进行过滤,这也很快。

      【讨论】:

      • ... 只是“浸入...” 两次。问题是如何避免这种情况。
      • @forpas 。 . .并不真地。索引查找非常快。不需要优化它,至少如果你打算使用数据库的话。
      • 不是真的什么? (快速)索引查找将发生 两次。问题是如何避免这种情况。
      • @forpas 。 . .祝你好运。任何使用数据库并关心性能的人都会对两次索引查找和一次主键扫描感到满意。这绝对比全表扫描好,这就是为什么这个答案侧重于为查询提供适当的索引。
      • 任何使用数据库并关心性能的人都会更乐意仅使用 1 个索引查找,这就是问题所在。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-02
      • 2012-03-12
      • 1970-01-01
      • 2011-09-05
      • 1970-01-01
      相关资源
      最近更新 更多