代码之家  ›  专栏  ›  技术社区  ›  AVEbrahimi

改进SQL查询性能

  •  -1
  • AVEbrahimi  · 技术社区  · 6 年前

    我有一个复杂的查询,在我的机器上运行需要700毫秒。我发现瓶颈是 按“firstname.value”排序 子句,但如何使用索引来改进这一点?

    SELECT 
        `e`.*
        , `at_default_billing`.`value` AS `default_billing`
        , `at_billing_postcode`.`value` AS `billing_postcode`
        , `at_billing_city`.`value` AS `billing_city`
        , `at_billing_region`.`value` AS `billing_region`
        , `at_billing_country_id`.`value` AS `billing_country_id`
        , `at_company`.`value` AS `company`
        , `at_firstname`.`value` AS `firstname`
        , `at_lastname`.`value` AS `lastname`
        , CONCAT(at_firstname.value
        , " "
        , at_lastname.value) AS `full_name`
        , `at_phone`.`value` AS `phone`
        , IFNULL(at_phone.value,"N/A") AS `telephone`
        , `e`.`entity_id` AS `id` 
    FROM 
        `customer_entity` AS `e`  
    LEFT JOIN 
        `customer_entity_int` AS `at_default_billing` 
        ON (`at_default_billing`.`entity_id` = `e`.`entity_id`) 
        AND (`at_default_billing`.`attribute_id` = '13')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_postcode` 
        ON (`at_billing_postcode`.`entity_id` = `at_default_billing`.`value`)        
        AND (`at_billing_postcode`.`attribute_id` = '30')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_city` 
        ON (`at_billing_city`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_billing_city`.`attribute_id` = '26')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_region` 
        ON (`at_billing_region`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_billing_region`.`attribute_id` = '28')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_country_id` 
        ON (`at_billing_country_id`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_billing_country_id`.`attribute_id` = '27')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_company` 
        ON (`at_company`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_company`.`attribute_id` = '24')  
    LEFT JOIN 
        `customer_entity_varchar` AS `at_firstname` 
        ON (`at_firstname`.`entity_id` = `e`.`entity_id`) 
        AND (`at_firstname`.`attribute_id` = '5')  
    LEFT JOIN 
        `customer_entity_varchar` AS `at_lastname` 
        ON (`at_lastname`.`entity_id` = `e`.`entity_id`) 
        AND (`at_lastname`.`attribute_id` = '7')  
    LEFT JOIN 
        `customer_entity_varchar` AS `at_phone` 
        ON (`at_phone`.`entity_id` = `e`.`entity_id`) 
        AND (`at_phone`.`attribute_id` = '136')  
    ORDER BY 
        `at_firstname`.`value` ASC LIMIT 20
    

    这是执行计划: enter image description here

    查询说明:

    '1','SIMPLE','e',NULL,'ALL',NULL,NULL,NULL,NULL,'19951','100.00','Using temporary; Using filesort'
    '1','SIMPLE','at_default_billing',NULL,'eq_ref','UNQ_CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_INT_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_INT_ENTITY_ID,IDX_CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.e.entity_id,const','1','100.00',NULL
    '1','SIMPLE','at_billing_postcode',NULL,'eq_ref','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.at_default_billing.value,const','1','100.00','Using where'
    '1','SIMPLE','at_billing_city',NULL,'eq_ref','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.at_default_billing.value,const','1','100.00','Using where'
    '1','SIMPLE','at_billing_region',NULL,'eq_ref','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.at_default_billing.value,const','1','100.00','Using where'
    '1','SIMPLE','at_billing_country_id',NULL,'eq_ref','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.at_default_billing.value,const','1','100.00','Using where'
    '1','SIMPLE','at_company',NULL,'eq_ref','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.at_default_billing.value,const','1','100.00','Using where'
    '1','SIMPLE','at_firstname',NULL,'eq_ref','UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.e.entity_id,const','1','100.00',NULL
    '1','SIMPLE','at_lastname',NULL,'eq_ref','UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.e.entity_id,const','1','100.00',NULL
    '1','SIMPLE','at_phone',NULL,'eq_ref','UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID,IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE','UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID','6','lazurd.e.entity_id,const','1','100.00',NULL
    

    表结构:

    CREATE TABLE `customer_entity_varchar` (
      `value_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Value Id',
      `entity_type_id` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Entity Type Id',
      `attribute_id` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Attribute Id',
      `entity_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Entity Id',
      `value` varchar(255) DEFAULT NULL COMMENT 'Value',
      PRIMARY KEY (`value_id`),
      UNIQUE KEY `UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID` (`entity_id`,`attribute_id`),
      KEY `IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_TYPE_ID` (`entity_type_id`),
      KEY `IDX_CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID` (`attribute_id`),
      KEY `IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID` (`entity_id`),
      KEY `IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE` (`entity_id`,`attribute_id`,`value`),
      CONSTRAINT `FK_CSTR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID` FOREIGN KEY (`attribute_id`) REFERENCES `eav_attribute` (`attribute_id`) ON DELETE CASCADE ON UPDATE CASCADE,
      CONSTRAINT `FK_CSTR_ENTT_VCHR_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID` FOREIGN KEY (`entity_type_id`) REFERENCES `eav_entity_type` (`entity_type_id`) ON DELETE CASCADE ON UPDATE CASCADE,
      CONSTRAINT `FK_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID` FOREIGN KEY (`entity_id`) REFERENCES `customer_entity` (`entity_id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB AUTO_INCREMENT=131094 DEFAULT CHARSET=utf8 COMMENT='Customer Entity Varchar';
    
    3 回复  |  直到 6 年前
        1
  •  3
  •   The Impaler    6 年前

    截至目前,您的查询是:

    1. 首先执行所有左外部联接。
    2. 然后 ORDER 排成一行。
    3. 然后 LIMIT 排成一行。

    我将首先执行严格需要的外部联接,然后排序和限制(减少到20行),最后我将执行其余的外部联接。简而言之,我会这样做:

    1. 首先执行最小左外部联接。也就是说,只有两张桌子。
    2. 然后 秩序 排成一行。
    3. 然后 极限 排成一行。这最多可产生20行。
    4. 执行所有其余的外部连接。在这一点上,这不再是成千上万行,而是只有20行。

    此更改应大大减少“唯一键查找”的执行。修改后的查询将如下所示:

    select
      e.*
      , `at_default_billing`.`value` AS `default_billing`
      , `at_billing_postcode`.`value` AS `billing_postcode`
      , `at_billing_city`.`value` AS `billing_city`
      , `at_billing_region`.`value` AS `billing_region`
      , `at_billing_country_id`.`value` AS `billing_country_id`
      , `at_company`.`value` AS `company`
      , `at_lastname`.`value` AS `lastname`
      , CONCAT(firstname
      , " "
      , at_lastname.value) AS `full_name`
      , `at_phone`.`value` AS `phone`
      , IFNULL(at_phone.value,"N/A") AS `telephone`
    from ( -- Step #1: joining customer_entity with customer_entity_varchar
    SELECT 
        `e`.*
        , `at_firstname`.`value` AS `firstname`
        , `e`.`entity_id` AS `id` 
    FROM 
        `customer_entity` AS `e`  
    LEFT JOIN 
        `customer_entity_varchar` AS `at_firstname` 
        ON (`at_firstname`.`entity_id` = `e`.`entity_id`) 
        AND (`at_firstname`.`attribute_id` = '5')  
    ORDER BY -- Step #2: Sorting (the bare minimum)
        `at_firstname`.`value` ASC 
    LIMIT 20 -- Step #3: Limiting (to 20 rows)
    ) e
    LEFT JOIN -- Step #4: Performing all the rest of outer joins (only few rows now)
        `customer_entity_int` AS `at_default_billing` 
        ON (`at_default_billing`.`entity_id` = `e`.`entity_id`) 
        AND (`at_default_billing`.`attribute_id` = '13')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_postcode` 
        ON (`at_billing_postcode`.`entity_id` = `at_default_billing`.`value`)        
        AND (`at_billing_postcode`.`attribute_id` = '30')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_city` 
        ON (`at_billing_city`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_billing_city`.`attribute_id` = '26')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_region` 
        ON (`at_billing_region`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_billing_region`.`attribute_id` = '28')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_billing_country_id` 
        ON (`at_billing_country_id`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_billing_country_id`.`attribute_id` = '27')  
    LEFT JOIN 
        `customer_address_entity_varchar` AS `at_company` 
        ON (`at_company`.`entity_id` = `at_default_billing`.`value`) 
        AND (`at_company`.`attribute_id` = '24')  
    LEFT JOIN 
        `customer_entity_varchar` AS `at_lastname` 
        ON (`at_lastname`.`entity_id` = `e`.`entity_id`) 
        AND (`at_lastname`.`attribute_id` = '7')  
    LEFT JOIN 
        `customer_entity_varchar` AS `at_phone` 
        ON (`at_phone`.`entity_id` = `e`.`entity_id`) 
        AND (`at_phone`.`attribute_id` = '136')  
    
        2
  •  3
  •   O. Jones    6 年前

    不幸的是, SELECT whole_mess_of_rows FROM many_tables ORDER BY one_col LIMIT small_number 是一个臭名昭著的性能反模式。为什么?因为它对一个大的结果集排序,只是为了丢弃其中的大部分。

    诀窍是便宜地找出其中的行 LIMIT small_number ,然后只从较大的查询中检索这些行。

    您要哪一行?在我看来,此查询将检索 customer_entity.id 价值观。但是很难确定,所以您应该测试这个子查询。

               SELECT customer_entity.entity_id
                 FROM customer_entity
                 LEFT JOIN customer_entity_varchar AS at_firstname 
                           ON (at_firstname.entity_id = e.entity_id) 
                          AND (at_firstname.attribute_id = '5') 
                ORDER BY at_firstname.value ASC
                LIMIT 20
    

    这将给出20个相关实体的ID值。测试它。看看它的执行计划。向添加适当的索引 customer_entity 如果需要的话。这个指数可能是 (firstname_attribute_id, firstname_entity_id, firstname_value) 但我猜。

    然后您可以将它放在主查询的末尾,就在ORDERBY之前。

     WHERE e.entity_id IN (
               SELECT customer_entity.entity_id
                 FROM customer_entity
                 LEFT JOIN customer_entity_varchar AS at_firstname 
                           ON (at_firstname.entity_id = e.entity_id) 
                          AND (at_firstname.attribute_id = '5') 
                ORDER BY at_firstname.value ASC
                LIMIT 20
          )
    

    事情应该快一点。

        3
  •  1
  •   Rick James diyism    6 年前

    我同意前面的答案,但我想强调更多的反模式:过度正规化。

    您的模式是对已经不好的EAV模式的一个奇怪的(低效的)变体。

    拆分的好处不大,也有一些缺点。 customer_address_entity_varchar 5张桌子。同样地 customer_entity_varchar .

    地址(通常)应存储为单个表中的几列;否 JOINs 其他桌子。

    名字+姓也是这样。

    Phone 可能是另一个问题,因为一个人/公司/实体可能有多个电话号码(手机、家庭、工作、传真等)。但情况不同。