【问题标题】:SQL Views for single row (with where clause)单行的 SQL 视图(带有 where 子句)
【发布时间】:2021-12-12 20:43:30
【问题描述】:

我正在开发一个管理课程的系统,该系统包含多个课程、多个课程和多个使用者。随着系统的增长,需要更多的数据,因此由于一些性能问题,我决定使用 SQL 视图。我们正在使用 MySQL。

所以我已经替换了对数据库的旧调用(例如单节课)

select * from `courses_classes_lessons` where `courses_classes_lessons`.`deleted_at` is null limit 1;
select count(consumer_id) as consumers_count from `courses_classes_lessons_consumers` where `lesson_id` = '448' limit 1;
select `max_consumers` from `courses_classes` where `id` = '65' limit 1;
select `id` from `courses_classes_lessons` left join `courses_classes_lessons_consumers` on `courses_classes_lessons_consumers`.`lesson_id` = `courses_classes_lessons`.`id` where `id` = '448' group by `courses_classes_lessons`.`id` having count(courses_classes_lessons_consumers.consumer_id) < '4' limit 1;
select courses_classes.max_consumers - LEAST(count(courses_classes_lessons_consumers.consumer_id), courses_classes.max_consumers) as available_spaces from `courses_classes_lessons` left join `courses_classes_lessons_consumers` on `courses_classes_lessons_consumers`.`lesson_id` = `courses_classes_lessons`.`id` left join `courses_classes` on `courses_classes_lessons`.`class_id` = `courses_classes`.`id` where `courses_classes_lessons`.`id` = '448' group by `courses_classes`.`id` limit 1;

以上花费了大约 4-5ms

SQL 视图如下:

CREATE OR REPLACE VIEW `courses_classes_lessons_view` AS
SELECT
    courses_classes_lessons.id AS lesson_id,
    (SELECT
            max_consumers
        FROM
            courses_classes
        WHERE
            id = courses_classes_lessons.class_id
        LIMIT 1) AS class_max_consumers,
    (SELECT
        count(consumer_id)
    FROM
        courses_classes_lessons_consumers
    WHERE
        lesson_id = courses_classes_lessons.id) AS consumers_count, 
    (SELECT
        CASE WHEN consumers_count >= class_max_consumers THEN
            TRUE
        ELSE
            FALSE
        END AS is_full) AS is_full, 
    (CASE WHEN courses_classes_lessons.completed_at > NOW() THEN
        'completed'
    WHEN courses_classes_lessons.cancelled_at > NOW() THEN
        'cancelled'
    WHEN courses_classes_lessons.starts_at > NOW() THEN
        'upcoming'
    ELSE
        'incomplete'
    END) AS status, 
    (SELECT
        class_max_consumers - LEAST(consumers_count, class_max_consumers)) AS available_spaces
    FROM
        courses_classes_lessons

我遇到的问题是,无论我是加载整个视图还是从中加载一行都无关紧要 - 加载总是需要大约 6-9 秒!但是当我用 WHERE 子句尝试相同的查询时,它需要大约 500μs。我是 SQL View 的新手并且很困惑 - 为什么没有可以用来快速加载单行的索引/主键?我做错了吗?

解释结果

INSERT INTO `courses_classes` (`id`, `select_type`, `table`, `partitions`, `type`, `possible_keys`, `key`, `key_len`, `ref`, `rows`, `filtered`, `Extra`) VALUES
(1, 'PRIMARY', 'courses_classes_lessons', NULL, 'ALL', NULL, NULL, NULL, NULL, 478832, 100.00, NULL),
(3, 'DEPENDENT SUBQUERY', 'courses_classes_lessons_consumers', NULL, 'ref', 'PRIMARY,courses_classes_lessons_consumers_lesson_id_index', 'courses_classes_lessons_consumers_lesson_id_index', '4', 'api.courses_classes_lessons.id', 3, 100.00, 'Using index'),
(2, 'DEPENDENT SUBQUERY', 'courses_classes', NULL, 'eq_ref', 'PRIMARY,courses_classes_id_parent_id_index', 'PRIMARY', '4', 'api.courses_classes_lessons.class_id', 1, 100.00, NULL);

表格结构

课程

CREATE TABLE `courses_classes_lessons` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `franchisee_id` int(10) unsigned NOT NULL,
  `class_id` int(10) unsigned NOT NULL,
  `instructor_id` int(10) unsigned NOT NULL,
  `instructor_rate` int(10) unsigned NOT NULL DEFAULT '0',
  `instructor_total` int(10) unsigned NOT NULL DEFAULT '0',
  `instructor_paid` tinyint(1) NOT NULL DEFAULT '0',
  `starts_at` timestamp NULL DEFAULT NULL,
  `ends_at` timestamp NULL DEFAULT NULL,
  `completed_at` timestamp NULL DEFAULT NULL,
  `cancelled_at` timestamp NULL DEFAULT NULL,
  `cancelled_reason` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `cancelled_reason_extra` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `courses_classes_lessons_franchisee_id_foreign` (`franchisee_id`),
  KEY `courses_classes_lessons_class_id_foreign` (`class_id`),
  KEY `courses_classes_lessons_instructor_id_foreign` (`instructor_id`),
  KEY `courses_classes_lessons_starts_at_ends_at_index` (`starts_at`,`ends_at`),
  KEY `courses_classes_lessons_completed_at_index` (`completed_at`),
  KEY `courses_classes_lessons_cancelled_at_index` (`cancelled_at`),
  KEY `courses_classes_lessons_class_id_deleted_at_index` (`class_id`,`deleted_at`),
  KEY `courses_classes_lessons_deleted_at_index` (`deleted_at`),
  KEY `class_ownership_index` (`class_id`,`starts_at`,`cancelled_at`,`deleted_at`),
  CONSTRAINT `courses_classes_lessons_class_id_foreign` FOREIGN KEY (`class_id`) REFERENCES `courses_classes` (`id`) ON DELETE CASCADE,
  CONSTRAINT `courses_classes_lessons_franchisee_id_foreign` FOREIGN KEY (`franchisee_id`) REFERENCES `franchisees` (`id`) ON DELETE CASCADE,
  CONSTRAINT `courses_classes_lessons_instructor_id_foreign` FOREIGN KEY (`instructor_id`) REFERENCES `instructors` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=487853 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

课程消费者

CREATE TABLE `courses_classes_lessons_consumers` (
  `lesson_id` int(10) unsigned NOT NULL,
  `consumer_id` int(10) unsigned NOT NULL,
  `present` tinyint(1) DEFAULT NULL,
  `plan_id` int(10) unsigned DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`lesson_id`,`consumer_id`),
  KEY `courses_classes_lessons_consumers_consumer_id_foreign` (`consumer_id`),
  KEY `courses_classes_lessons_consumers_plan_id_foreign` (`plan_id`),
  KEY `courses_classes_lessons_consumers_lesson_id_index` (`lesson_id`),
  KEY `courses_classes_lessons_consumers_present_index` (`present`),
  CONSTRAINT `courses_classes_lessons_consumers_consumer_id_foreign` FOREIGN KEY (`consumer_id`) REFERENCES `customers_consumers` (`id`) ON DELETE CASCADE,
  CONSTRAINT `courses_classes_lessons_consumers_lesson_id_foreign` FOREIGN KEY (`lesson_id`) REFERENCES `courses_classes_lessons` (`id`) ON DELETE CASCADE,
  CONSTRAINT `courses_classes_lessons_consumers_plan_id_foreign` FOREIGN KEY (`plan_id`) REFERENCES `customers_plans` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

从类中它只使用max_consumers int(10) unsigned NOT NULL DEFAULT '0',

更新 1

我已将 SQL 视图更改为以下:

CREATE OR REPLACE VIEW `courses_classes_lessons_view` AS
SELECT
    courses_classes_lessons.id AS lesson_id,
    courses_classes.max_consumers AS class_max_consumers,
    lessons_consumers.consumers_count AS consumers_count,
    (
        SELECT
            CASE WHEN consumers_count >= class_max_consumers THEN
                TRUE
            ELSE
                FALSE
            END AS is_full) AS is_full,
        (
            CASE WHEN courses_classes_lessons.completed_at > NOW() THEN
                'completed'
            WHEN courses_classes_lessons.cancelled_at > NOW() THEN
                'cancelled'
            WHEN courses_classes_lessons.starts_at > NOW() THEN
                'upcoming'
            ELSE
                'incomplete'
            END) AS status,
        (
            SELECT
                class_max_consumers - LEAST(consumers_count, class_max_consumers)) AS available_spaces
        FROM
            courses_classes_lessons
            JOIN courses_classes ON courses_classes.id = courses_classes_lessons.class_id
            JOIN (
                SELECT
                    lesson_id,
                    count(*) AS consumers_count
                FROM
                    courses_classes_lessons_consumers
                GROUP BY
                    courses_classes_lessons_consumers.lesson_id) AS lessons_consumers ON lessons_consumers.lesson_id = courses_classes_lessons.id;

尽管 SELECT 查询本身似乎比前一个查询慢得多,但 View 似乎执行得更好。它仍然没有我希望的那么快,但它向前迈进了一步。

整体改进从 6-7s 跃升到 800ms 左右,这里的目标是在 500μs-1ms 范围内。有什么建议可以改进我的 SQL 视图吗?

更新 2

好的,我找到了瓶颈!再次 - 它有点类似于最后一个(SELECT 查询对单行运行很快,但 SQL VIEW 每次都尝试一次访问整个表。

我的新课 SQL VIEW:

CREATE OR REPLACE VIEW `courses_classes_lessons_view` AS
                SELECT
                courses_classes_lessons.id AS lesson_id,
                courses_classes.max_consumers AS class_max_consumers,
                IFNULL(lessons_consumers.consumers_count,0) AS consumers_count,
                (
                    SELECT
                        CASE WHEN consumers_count >= class_max_consumers THEN
                            TRUE
                        ELSE
                            FALSE
                        END AS is_full) AS is_full,
                    (
                        CASE WHEN courses_classes_lessons.completed_at > NOW() THEN
                            'completed'
                        WHEN courses_classes_lessons.cancelled_at > NOW() THEN
                            'cancelled'
                        WHEN courses_classes_lessons.starts_at > NOW() THEN
                            'upcoming'
                        ELSE
                            'incomplete'
                        END) AS status,
                    (
                        SELECT
                            IFNULL(class_max_consumers, 0) - LEAST(IFNULL(consumers_count,0), class_max_consumers)) AS available_spaces
                    FROM
                        courses_classes_lessons
                        JOIN courses_classes ON courses_classes.id = courses_classes_lessons.class_id
                        LEFT JOIN courses_classes_lessons_consumers_view AS lessons_consumers ON lessons_consumers.lesson_id = courses_classes_lessons.id;

另一个 SQL 视图 - 这次是针对消费者的:

CREATE OR REPLACE VIEW `courses_classes_lessons_consumers_view` AS
                SELECT
                    lesson_id,
                    IFNULL(count(
                        consumer_id),0) AS consumers_count
                FROM
                    courses_classes_lessons_consumers
                GROUP BY
                    courses_classes_lessons_consumers.lesson_id;

看起来这个是麻烦制造者!消费者表在上面,这里是上面 SELECT 查询的解释:

INSERT INTO `courses_classes_lessons_consumers` (`id`, `select_type`, `table`, `partitions`, `type`, `possible_keys`, `key`, `key_len`, `ref`, `rows`, `filtered`, `Extra`)
        VALUES(1, 'SIMPLE', 'courses_classes_lessons_consumers', NULL, 'index', 'PRIMARY,courses_classes_lessons_consumers_consumer_id_foreign,courses_classes_lessons_consumers_plan_id_foreign,courses_classes_lessons_consumers_lesson_id_index,courses_classes_lessons_consumers_present_index', 'courses_classes_lessons_consumers_lesson_id_index', '4', NULL, 1330649, 100.00, 'Using index');

知道如何扩展这个计数吗?

【问题讨论】:

  • 您需要在完整查询中使用EXPLAIN 以查看其执行情况。很可能它将视图的完整结果加载到临时表中,然后扫描您想要的行,并且不使用索引。
  • VIEWs 是语法糖,而不是性能。当您简单地执行查询时,它的运行速度有多快?如果这也很慢,那么让我们专注于它。相关表格请提供SHOW CREATE TABLE
  • @Barmar 这似乎正是我遇到的问题 - 从加载时间来看,它似乎每次都在加载它。任何方式我只能从视图表中加载一行? select * from courses_classes_lessons_view where lesson_id = 448select * from courses_classes_lessons_view 的加载时间完全相同。
  • @RickJames 我用于创建视图的查询似乎在加载时间上与视图中的任何选择相匹配,但如果我添加到查询where courses_classes_lessons.id = 448 它将在大约 500μs 内加载,这就是我追求的时代。看起来就像@Barmar 所说的那样 - 它正在加载整个内容,然后,它正在寻找没有意义的匹配。我所追求的是将视图与表格链接起来——创建某种关系或至少一个索引。

标签: mysql sql sql-view


【解决方案1】:

考虑写一个存储过程;它也许能够让 448 到位,以便更好地优化。

  • 如果您知道只有一行(例如在执行COUNT(*) 时),请跳过LIMIT 1
  • 除非consumer_id 可能是NULL,否则请使用COUNT(*) 而不是COUNT(consumer_id)
  • 没有ORDER BYLIMIT 会让你得到一个随机行。
  • 如果courses_classes_lessons_consumers是多对多的映射表,我看到SHOW CREATE TABLE之后大概会有一些索引建议。
  • 5 个SELECTs 中哪个最慢?

【讨论】:

  • 我已经添加了LIMIT 作为我绝望的一部分-在我的原始查询中没有:) 无论如何-他们没有改变结果,所以我会摆脱它们。你要的是前四个中最慢的?此时,来自 VIEW 的任何 SELECT 都非常慢。
  • 是的; VIEWs 有时超级慢。我还没有想出要避免的模式。
【解决方案2】:

经过多次尝试,看起来过程方式是最好的方法,我不会花更多时间在 SQL 视图上

这是我写的程序:

CREATE PROCEDURE `LessonData`(
                IN lessonId INT(10)
                )
            BEGIN
                SELECT
                courses_classes_lessons.id AS lesson_id,
                courses_classes.max_consumers AS class_max_consumers,
                IFNULL((SELECT
                    count(consumer_id) as consumers_count
                FROM
                    courses_classes_lessons_consumers
                WHERE
                    lesson_id = courses_classes_lessons.id
                GROUP BY
                    courses_classes_lessons_consumers.lesson_id), 0) AS consumers_count,
                (
                    SELECT
                        CASE WHEN consumers_count >= class_max_consumers THEN
                            TRUE
                        ELSE
                            FALSE
                        END) AS is_full,
                    (
                        CASE WHEN courses_classes_lessons.completed_at > NOW() THEN
                            'completed'
                        WHEN courses_classes_lessons.cancelled_at > NOW() THEN
                            'cancelled'
                        WHEN courses_classes_lessons.starts_at > NOW() THEN
                            'upcoming'
                        ELSE
                            'incomplete'
                        END) AS status,
                    (
                        SELECT
                            class_max_consumers - LEAST(consumers_count, class_max_consumers)) AS available_spaces
                    FROM
                        courses_classes_lessons
                        JOIN courses_classes ON courses_classes.id = courses_classes_lessons.class_id
                    WHERE courses_classes_lessons.id = lessonId;    
            END

它的执行时间约为500μs-1ms。

感谢大家的帮助!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-06
    • 1970-01-01
    相关资源
    最近更新 更多