问题 准备好的陈述是否是正常查询的浪费? (PHP)


如今,“准备好的语句”似乎是任何人建议向数据库发送查询的唯一方式。我甚至看到了为存储过程使用预准备语句的建议。但是,对于额外的查询准备语句需要 - 以及它们持续的短时间 - 我被说服它们仅对一行INSERT / UPDATE查询有用。

我希望有人可以纠正我,但这似乎是整个“桌子是邪恶的”CSS重复的事情。如果用于布局,表格只是邪恶的 - 而不是表格数据。将DIV用于表格数据是违反WC3的风格。

同样明智的,纯SQL(或从AR生成的)似乎对80%的查询更有用,在大多数站点上都是单个SELECT,不再重复页面加载(我说的是脚本语言)像这里的PHP)。为什么我会让我过度征税的数据库准备一个声明,它只是在删除之前运行一次?

MySQL的:

准备好的声明是特定的   它创建的会话。   如果你没有终止会话   解除分配先前准备的   声明,服务器解除分配它   自动。

因此,在脚本结束时,PHP将自动关闭连接,您将丢失准备好的语句,只是为了让您的脚本在下次加载时重新创建它。

我错过了什么或者这只是一种降低性能的方法吗?

:更新时间:

我突然意识到我正在为每个脚本假设新的连接。我认为如果使用持久连接,那么这些问题就会消失。它是否正确?

:UPDATE2:

似乎即使持久连接是解决方案 - 它们也是 不是一个很好的选择 对于大多数网络 - 特别是如果您使用交易。所以我回到原点只有下面的基准测试继续......

:UPDATE3:

大多数人只是重复“准备语句防止SQL注入”这一短语,但并未完全解释问题。为每个DB库提供的“转义”方法 还可以防止SQL注入。但它不止于此:

以正常方式发送查询时,   客户端(脚本) 转换数据   成为字符串 然后传递给   数据库服务器。然后DB服务器使用   CPU供电 将它们转换回来 成   正确的二进制数据类型。该   数据库引擎然后解析   语句并查找语法错误。

当使用准备好的陈述时......   数据以原生二进制形式发送,   这节省了转换CPU使用率,   并使数据传输更多   高效。显然,这也将   如果客户端减少带宽使用   与DB服务器不在同一位置。

...变量类型是预定义的,   因此MySQL考虑到了   这些人物,他们不需要   被逃脱。

http://www.webdesignforums.net/showthread.php?t=18762

感谢OIS最终让我对这个问题感到不满。


8199
2017-12-04 21:37


起源



答案:


与CSS表辩论不同,准备好的陈述有明显的安全隐患。

如果您使用预准备语句作为将用户提供的数据放入查询的唯一方法,那么它们在SQL注入时绝对是防弹的。


6
2017-12-04 21:44



在查询中使用DB提供的escape()/ quote()方法也是防弹的。 - Xeoncross
除非将这些值传递到不准备语句的存储过程(或以其他方式清理其输入) - Vinko Vrsalovic
@Xeoncross:这需要更多解释。准备好的语句是100%安全的,因为没有转义/引用/转换。你不是在谈论mysql_real_escape_string吗? - OIS
准备不进行清理。获得该计划。它将数据作为数据传输,而不是字符串。没有引用。这是 为什么 准备好的陈述是 100%安全 引用的字符串是 不。 - OIS
在你的情况下,最好的来源当然是mysql。 dev.mysql.com/tech-resources/articles/4.1/... - OIS


答案:


与CSS表辩论不同,准备好的陈述有明显的安全隐患。

如果您使用预准备语句作为将用户提供的数据放入查询的唯一方法,那么它们在SQL注入时绝对是防弹的。


6
2017-12-04 21:44



在查询中使用DB提供的escape()/ quote()方法也是防弹的。 - Xeoncross
除非将这些值传递到不准备语句的存储过程(或以其他方式清理其输入) - Vinko Vrsalovic
@Xeoncross:这需要更多解释。准备好的语句是100%安全的,因为没有转义/引用/转换。你不是在谈论mysql_real_escape_string吗? - OIS
准备不进行清理。获得该计划。它将数据作为数据传输,而不是字符串。没有引用。这是 为什么 准备好的陈述是 100%安全 引用的字符串是 不。 - OIS
在你的情况下,最好的来源当然是mysql。 dev.mysql.com/tech-resources/articles/4.1/... - OIS


当您在数据库上执行sql语句时,sql解析器需要事先对其进行分析,这与准备工作完全相同。

因此,将执行sql语句直接与准备和执行进行比较没有任何缺点,但有一些优点:

  • 首先,正如longneck已经说过的那样,将用户输入传递到预准备语句会自动转义输入。就好像数据库已经为值准备了过滤器,只允许那些适合的值。

  • 其次,如果彻底使用预处理语句,并且遇到需要多次执行它的情况,则不需要重写代码来准备和执行,但只需执行它即可。

  • 第三:如果正确完成,代码将变得更具可读性:


$sql = 'SELECT u.id, u.user, u.email, sum(r.points)
        FROM users u
        LEFT JOIN reputation r on (u.id=r.user_id)
        LEFT JOIN badge b on (u.id=b.user_id and badge=:badge)
        WHERE group=:group';

$params = array(
    ':group' => $group, 
    ':badge' => $_GET['badge']
);

$stmt = $pdo->prepare($sql);
$result = $stmt->execute($params);

代替


$sql = 'SELECT u.id, u.user, u.email, sum(r.points)
        FROM users u
        LEFT JOIN reputation r on (u.id=r.user_id)
        LEFT JOIN badge b on (u.id=b.user_id and badge="'.mysql_real_escape_string($_GET['badge']).'")
        WHERE group="'.mysql_real_escape_string($group).'"';

$result = mysql_query($sql);

想象一下你必须改变sql语句,哪个代码是你最喜欢的? ;-)


1
2017-12-04 21:59



实际上,第二个是 - 但是调用$ this-> escape()而不是那个丑陋的函数。但是,就像我上面所说的那样,实际应该是一个查询 需要 不止一次运行我同意PS是要走的路。但是,对于仅在您从额外的数据库调用中遇到性能损失时才运行的查询。 - Xeoncross


准备好的声明在几种情况下派上用场:

  • 查询数据与不受信任的用户数据的完美分离。
  • 多次执行相同查询时性能会提高
  • 传输二进制数据时性能会提高,因为预准备语句可以使用二进制协议,而传统查询最终会进行编码等。

在正常情况下(不重复,没有二进制数据)会有性能损失,因为您现在必须来回两次。第一个“准备”查询,第二个是将令牌与要插入的数据一起发送。大多数人都愿意为安全利益做出这种牺牲。

关于持久连接: MySQL拥有市场上最快的连接建立时间之一。它对于大多数设置来说基本上是免费的,所以你不会使用持久连接看到太多的变化。


1
2017-12-04 23:49



查询数据和用户数据应该用你准备好的语句分开。带有$ _GET的@Cassy示例形式非常糟糕。至于性能提升 - 如果你看到我的基准测试,很难用一种方法调用一种方法比另一种方法慢(但如果我遗漏了某些东西请尽快)。但是,二进制数据传输是一个很好的加分,我没有想到 - 我想知道PDO是否使用它......? - Xeoncross


答案与安全性和抽象性有关。其他人都已经提到了安全性,但真正的好处是你的输入完全是从查询本身中抽象出来的。这允许在使用抽象层时具有真正的数据库不可知性,而内联输入通常是依赖于数据库的过程。如果你关心任何可移植性,准备好的陈述是要走的路。

在现实世界中,我很少编写DML查询。我的所有INSERTS / UPDATES都是由抽象层自动构建的,只需传递一个输入数组即可执行。对于所有意图和目的,准备查询然后执行它们确实没有“性能损失”(保存初始PREPARE中的​​连接延迟)。但是当使用UDS(Unix域套接字)连接时,您不会注意到(甚至能够进行基准测试)差异。它通常在几微秒的量级。

考虑到安全性和抽象优势,我几乎不称它为浪费。


1
2017-12-05 00:13



实际上,随着ActiveRecord(和其他有趣的ORM)以及procs的出现 - 我不认为大多数人实际上已经写了SQL。因此,无论您的抽象层是生成SQL字符串然后运行准备语句 - 还是直接跳过运行它 - 都会对您的代码产生一些影响。另外,我不确定为什么“安全”这个话题不断涌现。使用认可的escape()方法可确保在预准备语句上运行的相同代码在标量值上运行。 - Xeoncross
对不起,我对这个安全/引用的事情不感兴趣。现在我已经纠正了,我会说(从我到目前为止所读的所有内容)是使用PS的唯一原因,因为它也节省了字符串/二进制转换时间。 - Xeoncross
大多数正在为报告,数据仓库,商业智能(以及在较小程度上的事务性应用程序中)开发复杂sql的人要么编写自己的SQL,要么使用像Microstrategy或Cognos这样的OLAP工具来生成SQL。 ORM在这个领域没有帮助。 - KenFar


性能优势并非来自较少的解析 - 它来自于只需要一次而不是重复地计算访问路径。当您发出数千个查询时,这会有很大帮助。

鉴于mysql非常简单的优化器/规划器,这可能不像使用更复杂的优化器的更成熟的数据库。

但是,如果您有一个了解数据偏差的复杂优化器,那么这种性能优势实际上会变得有害。在这种情况下,通常可以更好地使用不同的文字值为同一查询获取不同的访问路径,而不是重用预先存在的路径。


1
2017-12-05 05:19



我认为这个例子可能是Postgre? - Xeoncross
实际上,几乎任何更复杂的数据库 - oracle,db2,sybase,sql server,informix,postgresql等。 - KenFar


当使用sql查询时 SELECT x,y,z FROM foo WHERE c='mary had a little lamb' 服务器必须解析包含数据的sql语句+你必须清理“mary have ...”部分(对每个参数调用mysql_real_escape()或类似)。 使用预准备语句,服务器也必须解析语句,但没有数据,只返回语句的标识符(一个微小的数据包)。然后发送实际数据,而不先清理它。我没有看到这里的开销,但我承认我从未测试过它。你呢? ;-)

编辑:使用预处理语句可以消除将每个参数(输入/输出)转换为字符串的需要。如果您的php版本使用,可能更是如此 mysqlnd (而不是“旧的”libmysql客户端库)。还没有测试过那个性能方面。


0
2017-12-04 21:58



好的一点,我会尝试一起测试。 - Xeoncross
开销在于您必须向服务器发送两个请求(查询 然后 query_id + data)vs(query + data)的一次传输。 - Xeoncross
但另一方面,您消除了转义例程,数据可以以另一种(可能是“更好”)格式发送。它仍然是相同的连接(通常在同一局域网的范围内)。创建新连接可能成本很高。但只是在服务器和客户端之间再次发送一个或两个数据包......这有多大的代价?在现实世界中。我的 猜测 您是否需要进行详细的测试以考虑所涉及的所有因素(服务器配置,缓冲区,延迟等等)。我只能说我没有注意到 重大 表现失去了。 - VolkerK


我似乎没有找到使用持久连接的任何好处 - 或为该主管准备好的语句。看看这些数字 - 对于6000个选择语句(在页面请求中永远不会发生!),你几乎无法区分它们。我的大多数页面使用的查询少于10个。

更新 我刚把我的测试修改为   包括4k SELECT和4k INSERT   声明!自己动手让我   知道是否有任何设计错误。

如果我的MySQL服务器没有在与Apache相同的机器上运行,那么差异可能更大。

Persistent: TRUE
Prepare: TRUE
2.3399310112 seconds

Persistent: FALSE
Prepare: TRUE
2.3265211582184 seconds

Persistent: TRUE
Prepare: FALSE
2.3666892051697 seconds

Persistent: FALSE
Prepare: FALSE
2.3496441841125 seconds

这是我的测试代码:

$hostname = 'localhost';
$username = 'root';
$password = '';
$dbname = 'db_name';

$persistent = FALSE;
$prepare = FALSE;

try 
{

    // Force PDO to use exceptions for all errors
    $attrs = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);

    if($persistent) 
    { 
        // Make the connection persistent
        $attrs[PDO::ATTR_PERSISTENT] = TRUE;
    }

    $db = new PDO("mysql:host=$hostname;dbname=$dbname", $username, $password, $attrs);

    // What type of connection?
    print 'Persistent: '.($db->getAttribute(PDO::ATTR_PERSISTENT) ? 'TRUE' : 'FALSE').'<br />';
    print 'Prepare: '.($prepare ? 'TRUE' : 'FALSE').'<br />';

    //Clean table from last run
    $db->exec('TRUNCATE TABLE `pdo_insert`');

}
catch(PDOException $e)
{
    echo $e->getMessage();
}

$start = microtime(TRUE);

$name = 'Jack';
$body = 'This is the text "body"';

if( $prepare ) {

    // Select
    $select = $db->prepare('SELECT * FROM pdo_insert WHERE id = :id');
    $select->bindParam(':id', $x);

    // Insert
    $insert = $db->prepare('INSERT INTO pdo_insert (`name`, `body`, `author_id`) 
    VALUES (:name, :body, :author_id)');
    $insert->bindParam(':name', $name);
    $insert->bindParam(':body', $body);
    $insert->bindParam(':author_id', $x);


    $run = 0;
    for($x=0;$x<4000;++$x) 
    {
        if( $insert->execute() && $select->execute() ) 
        {
            $run++;
        }
    }

}
else
{

    $run = 0;
    for($x=0;$x<4000;++$x) {

        // Insert
        if( $db->query('INSERT INTO pdo_insert (`name`, `body`, `author_id`) 
        VALUES ('.$db->quote($name).', '. $db->quote($body).', '. $db->quote($x).')') 

        AND

        // Select
        $db->query('SELECT * FROM pdo_insert WHERE id = '. $db->quote($x)) )
        {
            $run++;
        }

    }

}





print (microtime(true) - $start).' seconds and '.($run * 2).' queries';

0
2017-12-04 22:42



我想看看如何在没有准备好的语句的情况下获得100%防弹不可修复的sql注入数据。 - OIS
通过使用与DB API用于预准备语句相同的内部方法:$ db-> quote($ x) - Xeoncross
见其他答案。 PDO->引用远不及100%sql注入安全,就像准备语句一样。它也不能用于所有数据库(在您之前,您永远不需要它) 需要 它, 现在)。 - OIS
如果您认为数据库在执行准备语句时只引用字符串,那么我就有一个桥梁可以卖给您。 - Donnie
对不起,OIS花了我所有的钱 - Xeoncross


卡西是对的。如果您不准备/编译它,dbms必须在任何情况下才能运行它。

此外,优点是您可以检查准备结果,如果准备失败,您的算法可以分支处理异常而不浪费数据库资源来运行失败的查询。


0
2017-12-05 00:10



第二点是一个有趣的想法,但是只有在我们调试/构建项目时(在测试SQL语法之前)它才有用,并且只会节省带宽。当然,在 开发模式 错误的带宽几乎不算什么。 ;) - Xeoncross
我必须编写动态生成sql语句的Web应用程序。 - Blessed Geek