【问题标题】:Is it necessary to execute additional SELECT and INSERT IGNORE queries each time while inserting data into MySQL table that depends on FOREIGN KEYs?每次将数据插入依赖 FOREIGN KEY 的 MySQL 表时,是否需要执行额外的 SELECT 和 INSERT IGNORE 查询?
【发布时间】:2019-12-25 09:32:29
【问题描述】:

我有 4 个 MySQL 表,它们通过 FOREIGN KEYs 相互依赖。

请查看以下架构以了解表格结构:

CREATE DATABASE IF NOT EXISTS courses
    CHARACTER SET latin1
    COLLATE latin1_bin;

CREATE TABLE IF NOT EXISTS courses.institution
(
    icode INT UNSIGNED NOT NULL AUTO_INCREMENT,
    iname VARCHAR(255) NOT NULL,
    PRIMARY KEY (icode),
    UNIQUE (iname)
)
    ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS courses.cities
(
    ccode INT UNSIGNED NOT NULL AUTO_INCREMENT,
    cname VARCHAR(255) NOT NULL,
    PRIMARY KEY (ccode),
    UNIQUE (cname)
)
    ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS courses.skills
(
    scode INT UNSIGNED NOT NULL AUTO_INCREMENT,
    sname VARCHAR(255) NOT NULL,
    PRIMARY KEY (scode),
    UNIQUE (sname)
)
    ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS courses.relation
(
    icode INT UNSIGNED NOT NULL,
    scode INT UNSIGNED NOT NULL,
    ccode INT UNSIGNED NOT NULL,
    UNIQUE KEY ucols (icode, scode, ccode),
    FOREIGN KEY (icode) REFERENCES courses.institution (icode),
    FOREIGN KEY (scode) REFERENCES courses.skills (scode),
    FOREIGN KEY (ccode) REFERENCES courses.cities (ccode)
)
    ENGINE = InnoDB;

目前我正在执行以下查询以在 relation 表中仅插入一条记录。

每次只插入一次就需要 4 次 INSERT 查询和 3 次 SELECT 子查询。

INSERT IGNORE INTO institution(iname) VALUES ('ABC Learners');
INSERT IGNORE INTO skills(sname) VALUES ('PHP');
INSERT IGNORE INTO cities(cname) VALUES ('Bangalore');

INSERT IGNORE INTO relation (icode, scode, ccode) VALUES (
    (SELECT icode FROM institution WHERE iname = 'ABC Learners'),
    (SELECT scode FROM skills WHERE sname = 'PHP'),
    (SELECT ccode FROM cities WHERE cname = 'Bangalore')
);

是否有必要每次都执行所有这些查询?或者在那里 在单个或几个查询中执行此操作的更好方法?

查看下面的简单 PHP 代码。在这段代码中,仅在 relation 表上插入 7 条记录,它执行 4 INSERT 查询,每条记录有 3 SELECT 子查询。

总共需要花费7 * 7 = 49 查询 7 条记录。如何解决?

<?php

$db = new mysqli('localhost', 'user', '****', 'courses');

$records = [
    ['ABC Learners', 'CSS', 'Bangalore'],
    ['ABC Learners', 'PHP', 'Bangalore'],
    ['ABC Learners', 'HTML', 'Bangalore'],
    ['ABC Learners', 'PHP', 'Hyderabad'],
    ['XYZ Solutions', 'PHP', 'Hyderabad'],
    ['XYZ Solutions', 'JAVA', 'Hyderabad'],
    ['XYZ Solutions', 'JAVA', 'Bangalore'],
];

foreach ($records as $record) {

    list($institute, $skill, $city) = $record;

    $db->query("INSERT IGNORE INTO institution (iname) VALUES ('{$institute}')");
    $db->query("INSERT IGNORE INTO skills (sname) VALUES ('{$skill}')");
    $db->query("INSERT IGNORE INTO cities (cname) VALUES ('{$city}')");

    $db->query(
        "INSERT IGNORE INTO relation (icode, scode, ccode) VALUES (" .
        "(SELECT icode FROM institution WHERE iname = '{$institute}'), " .
        "(SELECT scode FROM skills WHERE sname = '{$skill}'), " .
        "(SELECT ccode FROM cities WHERE cname = '{$city}'))"
    );
}

$db->close();

注意:上面的脚本是示例目的。使用 批处理模式 或 禁用自动提交 对我没有用。因为很多时候我需要向关系表中添加一条新记录(基于用户通过网络面板的请求)


更新 1:

经过一些研究和基准测试,我创建了一个 MySQL 存储函数来加速这个过程,并将性能提高了 250 - 300%

请在此处结帐功能:

DELIMITER $$
CREATE FUNCTION courses.record(i_name VARCHAR(255), s_name VARCHAR(255), c_name VARCHAR(255)) RETURNS INT
BEGIN
    DECLARE _icode, _scode, _ccode INT UNSIGNED;

    SELECT icode INTO _icode FROM institution WHERE iname = i_name;
    SELECT scode INTO _scode FROM skills WHERE sname = s_name;
    SELECT ccode INTO _ccode FROM cities WHERE cname = c_name;

    IF _icode IS NULL THEN
        INSERT IGNORE INTO institution (iname) VALUES (i_name);
        SELECT icode INTO _icode FROM institution WHERE iname = i_name;
    END IF;

    IF _scode IS NULL THEN
        INSERT IGNORE INTO skills (sname) VALUES (s_name);
        SELECT scode INTO _scode FROM skills WHERE sname = s_name;
    END IF;

    IF _ccode IS NULL THEN
        INSERT IGNORE INTO cities (cname) VALUES (c_name);
        SELECT ccode INTO _ccode FROM cities WHERE cname = c_name;
    END IF;

    INSERT IGNORE INTO relation (icode, scode, ccode) VALUES (_icode, _scode, _ccode);

    RETURN ROW_COUNT();
END $$
DELIMITER ;

现在,下面这个 PHP 脚本可以通过一个查询在关系表中插入一条记录

<?php

$db = new mysqli('localhost', 'user', '***', 'courses');

$records = [
    ['ABC Learners', 'CSS', 'Bangalore'],
    ['ABC Learners', 'PHP', 'Bangalore'],
    ['ABC Learners', 'HTML', 'Bangalore'],
    ['ABC Learners', 'PHP', 'Hyderabad'],
    ['XYZ Solutions', 'PHP', 'Hyderabad'],
    ['XYZ Solutions', 'JAVA', 'Hyderabad'],
    ['XYZ Solutions', 'JAVA', 'Bangalore'],
];

$query = $db->prepare("SELECT record (?, ?, ?)");
$query->bind_param('sss', $institute, $skill, $city);

foreach ($records as $record) {
    list($institute, $skill, $city) = $record;
    $query->execute();
}

$db->close();

这个 MySQL 存储函数提高了性能。但是,我还是 在此函数中使用多个 INSERT 和 SELECT 语句。 是否有可能用很少的语句来优化这个函数来获得 性能更高?

【问题讨论】:

  • 一次插入只能插入一张表。您的最后一个插入不需要 selects ,为什么您没有像在其他插入上那样使用 insert..values ?
  • @P.Salmon 因为最后一次插入所需的值是auto_increment ids,所以在成功插入其他 3 个之前,OP 不知道它们
  • 一个建议:在relation 表中,不要将组合(icode, scode, ccode) 定义为UNIQUE 键,而是将其定义为PRIMARY 键。
  • @ChandraNakka 现在您正在循环中多次执行一个准备好的语句,正如我在回答中提出的那样。但是你为什么不使用事务呢?

标签: php mysql foreign-keys


【解决方案1】:

如果您在插入时已经有外键 ID,则可以克服,在您的客户端中,您需要显示 institution 和 cities 和 skills 的列表,它们都包含 id,@987654325 @ 和其他相关信息,因此当用户选择特定的 institution,cities 和 skills 时,您需要将相应的 Id 添加到您的请求数据中。前

const { institutionId, skillId, cityId } = req.body;

所以你只需要执行一个insert 查询;

INSERT IGNORE INTO relation (icode, scode, ccode) VALUES ( 
institutionId, skillId, cityId);

哪个更快。

【讨论】:

  • 如果服务器可以为多个客户端提供服务,这通常是不安全的:如果另一个客户端同时进行插入而第一个客户端没有,则不能保证客户端拥有正确的 id '还没有更新的列表..(请注意,您正在用 Javascript 回答 PHP 问题)
  • 在这种情况下,我们有用户数据 (req.user) 来识别哪个用户发起了该请求,显然,有一个验证来检查 id 是否存在于 DB 中。
【解决方案2】:

1.将 UNIQUE 更改为 PRIMARY 键:

在没有明确定义主键的情况下,InnoDB 将为每一行创建自己的隐藏主键。但在您的情况下,在表relation 中,当前的UNIQUE 键是一个更好的候选者,可以改为定义为PRIMARY 键(NOT NULL 和满足唯一性)。所以改为PRIMARY KEY。

2。利用批量插入/选择:

通过使用批量插入和选择,并利用一些应用程序捕获,我们基本上可以将其分为 4 个插入和 3 个选择。检查下面的代码(使用 cmets):

$records = [
    ['ABC Learners', 'CSS', 'Bangalore'],
    ['ABC Learners', 'PHP', 'Bangalore'],
    ['ABC Learners', 'HTML', 'Bangalore'],
    ['ABC Learners', 'PHP', 'Hyderabad'],
    ['XYZ Solutions', 'PHP', 'Hyderabad'],
    ['XYZ Solutions', 'JAVA', 'Hyderabad'],
    ['XYZ Solutions', 'JAVA', 'Bangalore'],
];

// Create a copy of records to avoid changing the original
$records_copy = $records;

// Get unique institutions, skills and cities
$i = array_unique(array_map('array_shift', $records_copy)); 
$s = array_unique(array_map('array_shift', $records_copy)); 
$c = array_unique(array_map('array_shift', $records_copy)); 

// Prepare batch insert and select SQL for institution table
$i_ins_sql = "INSERT IGNORE INTO institution (iname) VALUES ";
$i_sel_sql = "SELECT icode, iname FROM institution WHERE iname IN (";

foreach ($i as $v) {

    $i_ins_sql .= "('" . $db->real_escape_string($v) . "'),";
    $i_sel_sql .= "'" . $db->real_escape_string($v) . "',";
}

// Execute the INSERT query
$db->query( rtrim($i_ins_sql, ',') );

// Execute the SELECT query and fetch the query result and store (cache) it
// Key of the cache array would be name (string) and the value would be 
// the code (integer)
$i_cache = array();
$i_sel_q = $db->query( rtrim($i_sel_sql, ',') . ")" );
while ( $row = $i_sel_q->fetch_assoc() ) {
    $i_cache[$row['iname']] = $row['icode'];
}

/**
 * REPEAT the INSERT and SELECT steps for the skills and cities tables, 
 * using $s and $c arrays respectively, with appropriate field names
 * Eventually, you would have cache arrays, $s_cache and $c_cache.
 **/

// Now let's do a batch INSERT INTO relation table 
$sql = "INSERT IGNORE INTO relation (icode, scode, ccode) VALUES ";

// Loop over original records array
foreach ($records as $record) {

    $sql .= "(" . (int)$i_cache[$record[0]] . ","
                . (int)$i_cache[$record[1]] . "," 
                . (int)$i_cache[$record[2]] . "),";
}

// Execute the Batch INSERT query into relation table
$db->query( rtrim($sql, ',') );

最后但同样重要的是:您的代码对SQL injection 相关的攻击是开放的。即使real_escape_string 也无法完全保护它。请学会改用Prepared Statements。

【讨论】:

    【解决方案3】:

    您应该使用带有占位符的预处理语句来防止 SQL 注入。您应该使用事务来实现数据一致性。

    好消息:在单个事务中多次执行准备好的语句也可以提高性能。所以我会尝试以下方法:

    1. 准备所有四个 INSERT 语句
    2. 绑定参数
    3. 开始事务
    4. 对每个学院-技能-城市组合执行一次循环语句
    5. 提交事务
    $db = new mysqli('localhost', 'user', '****', 'courses');
    
    $records = [
        ['ABC Learners', 'CSS', 'Bangalore'],
        ['ABC Learners', 'PHP', 'Bangalore'],
        ['ABC Learners', 'HTML', 'Bangalore'],
        ['ABC Learners', 'PHP', 'Hyderabad'],
        ['XYZ Solutions', 'PHP', 'Hyderabad'],
        ['XYZ Solutions', 'JAVA', 'Hyderabad'],
        ['XYZ Solutions', 'JAVA', 'Bangalore'],
    ];
    
    $stmtInstitution = $db->prepare("INSERT IGNORE INTO institution (iname) VALUES (?)");
    $stmtSkills = $db->prepare("INSERT IGNORE INTO skills (sname) VALUES (?)");
    $stmtCities = $db->prepare("INSERT IGNORE INTO cities (cname) VALUES (?)");
    $stmtRelation = $db->prepare("
        INSERT IGNORE INTO relation (icode, scode, ccode) VALUES (
          (SELECT icode FROM institution WHERE iname = ?),
          (SELECT scode FROM skills      WHERE sname = ?),
          (SELECT ccode FROM cities      WHERE cname = ?)
        )
    ");
    
    $stmtInstitution->bind_param('s', $institute);
    $stmtSkills->bind_param('s', $skill);
    $stmtCities->bind_param('s', $city);
    $stmtRelation->bind_param('sss', $institute, $skill, $city);
    
    $db->begin_transaction();
    
    foreach ($records as $record) {
        list($institute, $skill, $city) = $record;
    
        $stmtInstitution->execute();
        $stmtSkills->execute();
        $stmtCities->execute();
        $stmtRelation->execute();
    }
    
    $db->commit();
    $db->close();
    

    这可能不如批量插入快。但它应该比你原来的解决方案快得多。而且也不是很复杂。

    更新

    关于性能:请参阅 Bill Karwin 的摘要(幻灯片 50)Load Data Fast! 并比较“multi stmt per txn”(如本答案所示)、“multi row per stmt”(如您最初要求的那样)和“one row per stmt/txn”(如您的原始代码中所示)。

    【讨论】:

      【解决方案4】:

      两句警告:

      • INSERT IGNORE(以及 IODKU 和几乎任何其他技术)将“烧毁”ID。也就是说,它预先分配了 AUTO_INCREMENT 值,它们会丢失。
      • 注意主事务中的内容和不存在的内容。失败时无需回滚规范化。

      当有很多行要插入时,批量插入是最有效的。

      • 使用名称和 id 列构建一个临时表。
      • 规范化一个 id-name 对(无事务):
        • INSERT ... SELECT ... LEFT JOIN ... -- 只插入新的。
        • UPDATE temp_table SET id = ... --(来自规范化表)获取所有 id。
      • (对每个标准化表重复)
      • INSERT INTO real_table SELECT id, ... FROM temp_table

      详情:http://mysql.rjweb.org/doc.php/staging_table#normalization

      (这可能是固定的 9 个查询,而不是您的示例中的 7*7。)

      【讨论】:

      • 第一个警告提供了非常有价值的信息。非常感谢。
      • 这也不是我的问题的解决方案。尽管如此,我还没有从此页面上的其他答案中收到任何有价值的解决方案。但是,如果与其他答案相比,这个答案提供了非常有价值的信息。所以,我想为此分配赏金。
      • @ChandraNakka - 是的,它可能需要 2 个 SQL。但是,如果可以对数据进行批处理,则提供的链接显示了如何在 2 个 SQL 语句每批而不是 2 个每行中完成任务。
      猜你喜欢
      • 2011-08-25
      • 1970-01-01
      • 1970-01-01
      • 2010-09-24
      • 2012-08-25
      • 2019-11-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多