当你添加了一个索引后,你的查询语句出错了...

本文通过一个具体案例揭示了在SQL查询中假设条件评估顺序可能导致的问题。作者展示了即使看似简单的查询也可能因索引添加而失败,并提供了安全重构建议。

When you add an index and your query blows up...

Alexander Kuznetsov

from: http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/07/16/when-you-add-an-index-and-your-query-blows-up.aspx 

You cannot assume that the conditions in your WHERE clause will evaluate in the left-to-write order - making such assumptions leads to unsafe queries. For example, the following query is not safe:

SELECT FROM dbo.Messages
  WHERE ISDATE(VarcharColumn)=
    
AND CAST(
VarcharColumn) AS DATETIME)='20090707'
 

It can blow up at any time, and the reason is simple: the conditions in your WHERE clause can evaluate in any order, and the order can change the next time your query executes. If an invalid value casts before the check if it is valid, the query will blow up. Of course, I am not the first to point this out. Right from the top of my head I recall that you can find similar recommendations in Itzik Ben Gan's book on T-SQl Fundamentals, and in a recent Plamen Ratchev's post "Predicates in SQL". However, I would like to demonstrate how little it may take to break such code, how brittle it may be. I will provide a repro script in which a query originally succeeds, but fails after I have added an index.

 

Do not make assumptions which you cannot guarantee

 

Whenever you write such code as the previous sample, you clearly assume that the first condition, the call to ISDATE, will evaluate before the CAST. However, this assumption cannot be guaranteed, and when this assumption is not true, the query blows up. Even if this query always succeeds in your test runs, you cannot assume that it will always succeed in the future. The safe way to ensure that invalid values are never cast to datetime is this:

 

SELECT FROM dbo.Messages
  
WHERE CASE WHEN ISDATE(
VarcharColumn)=1
    
THEN CAST(
VarcharColumn AS DATETIMEEND ='20090707'

 

It is safe becasue it is documented that CASE expression evaluates the WHEN clause before evaluating the THEN clause.

 

When order in which conditions are evaluated changes - a repro script.

 

I am going to provide a repro script demostrating that provide that the order in which conditions are evaluated can change. However, I have to begin with a big disclaimer: there is no guarantee that this repro script will work as expected on your server. My repro script does work for me, and most likely will work for you too, but I cannot promise that.

 

Setting up test data

 

The following script adds 1M messages, all of them except one have invalid dates:

 

 

-- helper table
CREATE TABLE dbo.Numbers(INT NOT NULL PRIMARY KEY)
GO
DECLARE @i INT;
SET @i 1;
INSERT INTO dbo.Numbers(nSELECT 1;
WHILE @i<1024000 BEGIN
  INSERT INTO 
dbo.Numbers(n)
    
SELECT @i FROM dbo.Numbers;
  
SET @i @i 2;
END;
GO
CREATE TABLE dbo.Messages(MessageID INT NOT NULL PRIMARY KEY,
  
SenderID INT NOT NULL,
  
ReceiverID INT NOT NULL,
  
MessageDateAs
VarcharColumn VARCHAR(30) NULL,
  
SomeMoreData CHAR(200) NULL);
GO
INSERT INTO dbo.Messages(MessageID,
  
SenderID,
  
ReceiverID,
 
MessageDateAsVarcharColumn,
  
SomeMoreData)
SELECT nn%1000n/1000'Wrong Date''SomeMoreData'
  
FROM dbo.Numbers;
GO
-- only one message has a valid date
UPDATE dbo.Messages SET
MessageDateAsVarcharColumn='20090707' 
  
WHERE SenderID 123 AND ReceiverID 456;
 


 

Originally an unsafe query against test data succeeds


The following query succeeds on my server, selecting the only one row which has a valid date:

 

SELECT FROM dbo.Messages
  
WHERE SenderID 123 
    
AND ReceiverID 456
    
AND CAST(
MessageDateAsVarcharColumn AS DATETIME)='20090707'
 

 

Because the query succeeds, clearly for all the rows the first two conditions are evaluated before MessageDate is cast to DATETIME, because only the row for which the first two conditions are turn has a valid datetime value. Let me repeat my disclaimer: there is no guarantee that this query will succeed on your server. Even if you reshuffle the conditions in your WHERE clause, the query still succeeds, so clearly the cast is attempted only after both other conditions are evaluated:

 

 

SELECT FROM dbo.Messages
  
WHERE
CAST(MessageDateAsVarcharColumn AS DATETIME)='20090707'
    
AND ReceiverID 456
    
AND
SenderID 123 
 

 

As you have seen, conditions in WHERE clause are not evaluated in left-to-right order.  You can also play with parenthesis trying to enforce one particular order, but that will not make any difference either.

 

After adding an index, the same query blows up

 

Here is the index:

 

CREATE INDEX Messages_SenderID_MessageDate
  
ON dbo.Messages(SenderID,
MessageDateAsVarcharColumn);

 

After it is created, the query blows up:

Msg 241, Level 16, State 1, Line 1
Conversion failed when converting datetime from character string.

My explanation is simple: the optimizer chooses to use the new non-clustered index, and it chooses to cast the MessageDate value and evaluate the third condition CAST(MessageDateAsVarcharColumn AS DATETIME)='20090707') before doing an expensive bookmark lookup. Let me repeat my disclaimer again: there is no guarantee that this query will blow upon your server.

Also I played a little bit with parenthesis, trying to use them to enforce a particular order of evaluation - of course that did not make any difference.

As you have seen, it is unsafe to make assumptions about the order in which conditions in your WHERE clause are evaluated. Whenever you see a query developed under such an assumption, rewrite it eliminating the unsafe assumption altogether.

 

If you are still reading this, can you do me a favor and post if this repro script worked on your server the way it did on mine. Please also post the output of SELECT @@VERSION.  I will be on vacation for a couple of weeks. This means that I will respond to questions, but not immediately.

 

This post continues my series on defensive database programming. Here are my previous posts  from the series: 

Defensive database programming: fun with triggers

Defensive database programming: fun with ROWCOUNT

Summarizing previous posts about defensive database programming

Defensive database programming: SET vs. SELECT.

Stress testing UPSERTs

Defensive database programming: fun with UPDATE.

Defensive database programming: eliminating IF statements.

Defensive database programming: fun with changing column widths.

Avoid mixing old and new styles of error handling.

Defensive database programming: adding ESCAPE clauses.

Defensive database programming: qualifying column names.

Defensive database programming: rewriting queries with NOT IN().

<think>在WPS表格JSA中,`EmbeddedShapes`对象用于表示嵌入到单元格中的形状(如图片)。使用`EmbeddedShapes.Count`时出错,可能是由于以下原因: 1. **对象不支持`EmbeddedShapes`属性**:在JSA中,并非所有对象都具有`EmbeddedShapes`属性。`EmbeddedShapes`是`Range`对象的属性,用于返回该单元格区域内的嵌入形状集合。因此,必须确保对`Range`对象调用此属性。 2. **单元格区域包含多个单元格**:如果尝试获取一个包含多个单元格的区域(如`Range("D:D")`)的`EmbeddedShapes`,则可能无法正确获取。因为`EmbeddedShapes`属性是针对单个单元格设计的。当区域包含多个单元格时,可能需要遍历每个单元格。 3. **没有嵌入形状**:如果单元格中没有嵌入形状,那么`EmbeddedShapes.Count`可能会返回0,但不会出错。但是,如果尝试访问不存在的嵌入形状(如`EmbeddedShapes.Item(1)`),则会导致错误。 4. **WPS版本问题**:某些WPS版本可能存在对JSA支持不完善的情况,导致`EmbeddedShapes`属性不可用。 ### 解决方法: 1. **确保对单个单元格使用`EmbeddedShapes`**:在遍历D列时,必须针对每个单元格单独检查嵌入形状。 例如,以下代码遍历D列的每个单元格: ```javascript function processEmbeddedShapes() { var sheet = ActiveSheet; var lastRow = sheet.Range("D" + sheet.Rows.Count).End(xlUp).Row; // 获取D列最后一行 for (var row = 1; row <= lastRow; row++) { var cell = sheet.Range("D" + row); var embeddedShapes = cell.EmbeddedShapes; if (embeddedShapes.Count > 0) { // 处理嵌入形状 } } } ``` 2. **检查对象是否存在**:在访问`EmbeddedShapes.Item(i)`之前,确保`embeddedShapes.Count`大于0,并且索引`i`在有效范围内。 3. **错误处理**:使用`try-catch`语句捕获可能的错误,避免宏中断。 ```javascript try { var embeddedShapes = cell.EmbeddedShapes; if (embeddedShapes.Count > 0) { // 处理嵌入形状 } } catch (e) { console.log("Error in cell D" + row + ": " + e.message); } ``` 4. **更新WPS**:如果问题依然存在,尝试更新WPS到最新版本,以确保JSA功能完整。 ### 示例代码 以下代码展示了如何安全地处理D列中的嵌入图片(转换为浮动图片): ```javascript function convertEmbeddedToFloating() { var sheet = ActiveSheet; // 获取D列最后一个非空行 var lastRow = sheet.Range("D" + sheet.Rows.Count).End(xlUp).Row; for (var row = 1; row <= lastRow; row++) { var cell = sheet.Range("D" + row); try { var embeddedShapes = cell.EmbeddedShapes; if (embeddedShapes.Count > 0) { for (var i = 1; i <= embeddedShapes.Count; i++) { var embeddedShape = embeddedShapes.Item(i); // 转换为浮动图片 var floatingShape = sheet.Shapes.AddPicture( embeddedShape.PictureFormat.Filename, false, // 不链接到文件 true, // 保存时随文档一起保存 cell.Left, cell.Top, embeddedShape.Width, embeddedShape.Height ); embeddedShape.Delete(); // 删除原嵌入图片 } } } catch (e) { console.log("Error in row " + row + ": " + e.message); } } } ``` ### 注意事项 - 在转换图片时,浮动图片将放置在对应单元格的左上角,大小与原嵌入图片相同。 - 如果单元格中有多个嵌入图片,它们会重叠在一起(因为嵌入图片在单元格内是层叠的),转换为浮动图片后也会重叠。可能需要调整位置。 ### 相关问题 1. 如何将浮动图片重新嵌入到指定的单元格中? 2. 在转换图片时,如何处理单元格中的多个嵌入图片以避免重叠? 3. 如何批量调整浮动图片的位置使其与单元格对齐? </think> ### WPS表格JSA中`embeddedShapes.Count`出错原因与解决方案 在WPS表格JSA宏环境中,使用`embeddedShapes.Count`语句时常见以下错误原因及解决方法: #### 错误原因分析 1. **对象引用错误**: - `EmbeddedShapes`是`Range`对象的属性,必须通过有效单元格引用获取 - 错误示例:`ActiveSheet.EmbeddedShapes.Count` ❌(工作表对象无此属性) - 正确引用:`Range("D1").EmbeddedShapes.Count` ✔️ 2. **空对象访问**: - 当单元格不存在嵌入图片时,`EmbeddedShapes`返回`null` - 直接访问`null.Count`会引发"对象不支持此属性"错误 3. **区域选择问题**: - `EmbeddedShapes`仅支持**单个单元格**(如`Range("D3")`) - 多单元格区域(如`Range("D:D")`)会返回`null` 4. **API兼容性问题**: - WPS JSA部分版本存在`EmbeddedShapes`支持不完善的情况 #### 解决方案(附代码) ```javascript function handleEmbeddedImages() { const sheet = ActiveSheet; const lastRow = sheet.Range("D" + sheet.Rows.Count).End(xlUp).Row; for (let row = 1; row <= lastRow; row++) { const cell = sheet.Range("D" + row); try { // 关键:验证对象有效性 if (cell.EmbeddedShapes && cell.EmbeddedShapes.Count > 0) { const count = cell.EmbeddedShapes.Count; console.log(`D${row} 发现 ${count} 张嵌入图片`); // 示例:转换为浮动图片 for (let i = 1; i <= count; i++) { const img = cell.EmbeddedShapes.Item(i); sheet.Shapes.AddPicture( img.PictureFormat.Filename, false, true, cell.Left, cell.Top, img.Width, img.Height ); img.Delete(); } } } catch (e) { console.error(`D${row}处理错误: ${e.message}`); } } } ``` #### 关键修复点 1. **单元格级访问**: ```javascript // 正确 const cell = sheet.Range("D3"); if (cell.EmbeddedShapes.Count > 0) {...} // 错误 const column = sheet.Range("D:D"); column.EmbeddedShapes.Count // 必然出错 ``` 2. **空值防护**: ```javascript // 添加空对象检查 if (cell.EmbeddedShapes && cell.EmbeddedShapes.Count > 0) { // 安全操作 } ``` 3. **异常处理**: ```javascript try { // 可能出错的操作 } catch (e) { console.error(`错误: ${e.message}`); } ``` #### 替代方案 若仍遇到问题,可使用底层API遍历所有形状: ```javascript function getAllEmbeddedImages() { const shapes = ActiveSheet.Shapes; for (let i = 1; i <= shapes.Count; i++) { const shape = shapes.Item(i); if (shape.Type === 13 && shape.TopLeftCell.Address.includes("$D$")) { console.log("找到D列嵌入图片: " + shape.Name); } } } ``` 注:`shape.Type === 13`表示图片类型,`$D$`定位D列[^1][^2]。 ### 注意事项 1. 执行图片操作前建议备份文档 2. 不同WPS版本API可能存在差异 3. 批量操作时添加进度提示避免界面假死
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值