news 2026/10/6 8:58:16

SQL数据过滤从入门到实战:WHERE、NULL、索引与安全防护全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL数据过滤从入门到实战:WHERE、NULL、索引与安全防护全解析

说实话,写了这么多年SQL,数据过滤是我见过最容易被低估的话题。很多人觉得WHERE后面加条件谁不会,可真到线上调慢查询、抠数据正确性、或者被面试官追问三值逻辑的时候,才发现自己以前写过的过滤条件到处都是雷。这一篇是SQL大师之路系列的第七篇,我会把数据过滤整个摊开——从最基础的WHERE运算符,到NULL处理、去重、窗口函数过滤、慢SQL性能体检,再到注入防护,一次讲透。不管是刚入行的新手,还是写了几年SQL想系统补基础的同学,这篇都能用在实战里。

1. WHERE子句的底牌:比较运算与优先级陷阱

1.1 基础比较运算符的细节

数据过滤的地基就是WHERE子句里的比较运算。=、<>、>、>=、<、<=这六个运算符算是最人畜无害的,但细节上有几个容易被忽略的点。

第一个是<>和!=的区别。在绝大多数数据库里两者完全等价,但在某些偏老旧的数据库版本或特定方言里,建议统一用<>,因为它是SQL标准里的写法,!=是后来才被广泛接受的别名。团队协作时风格统一比什么都重要。

第二个是字符串比较。你以为WHERE name = '张三'只会匹配到张三,但在MySQL里如果字段定义为CHAR或VARCHAR,且排序规则(collation)不区分大小写,那'ZHANG SAN'和'zhang san'会被视为相等。更关键的是,字符串尾部的空格在某些场景下会被忽略,MySQL的VARCHAR比较默认会忽略尾部空格。这就是为什么有时候你查出来的数据看起来"不对",实际是数据库层面的比较规则在起作用。

第三个是浮点数的等值比较。做金融或统计类的过滤时,千万别直接写WHERE amount = 0.1。浮点数在计算机内部是不精确的,0.1实际上可能是0.10000000000000000555。正确的做法是使用ABS(amount - 0.1) < 0.000001这样的范围判断,或者干脆用DECIMAL类型来存金额。这是一个很多线上Bug的根源。

1.2 面试常问:AND与OR的优先级问题

这个知识点几乎是我每一次做技术面试时都会问到的,而且答错率出奇的高。

先看一条SQL:

SELECT * FROM orders WHERE status = 'paid' OR status = 'pending' AND amount > 1000;

请问这条SQL的过滤逻辑是什么?是"状态为已支付或待支付,且金额大于1000"吗?

错。SQL里AND的优先级高于OR,所以这条SQL实际含义是:

SELECT * FROM orders WHERE status = 'paid' OR (status = 'pending' AND amount > 1000);

也就是:已支付的所有订单,加上待支付且金额大于1000的订单。这两个结果天差地别。如果你想要前一种理解,必须自己加括号:

SELECT * FROM orders WHERE (status = 'paid' OR status = 'pending') AND amount > 1000;

1.3 括号与可读性

我见过太多线上事故都是因为少了一层括号造成的。而且这种Bug往往很难排查,因为数据量大的时候结果看起来"好像也没什么问题",只有对账时才能发现。

所以我的建议非常朴素:只要同时使用AND和OR,不管有没有歧义,一律加括号。这不仅是给自己看的,更是给后来维护代码的人看的。你要知道,一段SQL的生命周期往往比写它的人在这个公司的在职时间还要长。

2. LIKE、IN、BETWEEN:模式与范围的边界意识

2.1 LIKE通配符与转义

LIKE是模糊匹配的主力。%代表任意长度字符串(包括空字符串),_代表任意单个字符。这俩基本用法大家都知道,但有几个坑值得说清楚。

第一个坑是转义。如果你要搜索一个真正包含百分号的字符串,比如查询合同编号中带%的记录,直接写LIKE '%.%'会把所有包含任意前缀的记录都查出来。正确的写法是利用ESCAPE关键字:

SELECT * FROM contracts WHERE contract_no LIKE '%\%%' ESCAPE '\';

第二个坑是大小写敏感。LIKE匹配是否区分大小写同样取决于数据库的排序规则。在MySQL默认的utf8_general_ci排序规则下,LIKE 'abc'会匹配到ABC。在SQL Server默认排序规则下通常不区分大小写,但在PostgreSQL里默认是区分大小写的。跨数据库迁移时这个差异很容易造成线上数据对不上。

第三个坑是逃逸下划线。_代表任意单个字符,所以你要匹配下划线本身时,也需要转义处理。比如查所有以a_b开头的表名:

SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'a\_b%' ESCAPE '\';

2.2 BETWEEN的闭区间陷阱

BETWEEN是个看似贴心实则容易出错的语法。它最大的特点是包含边界值。WHERE amount BETWEEN 100 AND 200等价于amount >= 100 AND amount <= 200,是闭区间。

这在处理日期时尤其危险。假设你要查2024年1月份的订单,很多人会写:

SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31';

这条SQL会漏掉1月31日当天的一部分数据。因为created_at如果是DATETIME类型,'2024-01-31 00:00:00'只是那一天的开始时间,1月31日白天产生的订单会被排除在外。正确做法是:

SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01';

这是一个非常经典的时间段过滤写法,左闭右开,永远不会漏数据。我处理过无数次这种因为BETWEEN日期踩坑的case,最后统一改成左闭右开写法,再也没出过问题。

2.3 IN列表与NULL的互动

IN列表用起来很直观,但它和NULL的互动是隐藏的雷区。

SELECT * FROM users WHERE id IN (1, 2, 3);

这条没问题。但如果你这么写:

SELECT * FROM users WHERE id IN (1, 2, 3, NULL);

这不会报错,但结果和上面那条完全一样——第三条永远不会匹配,id IN (NULL)永远是UNKNOWN,不会返回任何行。NULL不是值,而是"不知道",数据库无法判断一个值是否等于"不知道",所以直接忽略。

IN的另一个常见问题是列表过长。有些ORM框架会把子查询的结果直接展开成几百上千个参数的IN列表,这种情况下不仅SQL语句冗长,执行计划也可能退化成全表扫描。一般建议超过几百个值时改用连表或临时表方案。

2.4 模式匹配的性能提醒

聊完了功能,再说说性能。LIKE的%开头会直接导致索引失效,因为B+树索引是按值顺序排列的,无法利用无法确定前缀的匹配。这一点放到后面性能章节再细讲,这里先记住一个结论:LIKE 'abc%'还能走索引,LIKE '%abc%'基本只能全表扫。

3. NULL与三值逻辑:数据过滤中最隐秘的坑

3.1 SQL的三值逻辑

普通编程语言里布尔值只有TRUE和FALSE,但SQL里任何布尔表达式都可能产生第三个结果:UNKNOWN。原因就在于NULL。NULL不是一个值,它表示"未知"或"缺失"。

这个设计带来的直接影响是:WHERE column = NULL永远查不到数据。为什么?因为NULL等于NULL仍然是UNKNOWN,不是TRUE。你必须用IS NULL来判断。这条规则百分之九十的初级开发都踩过。

三值逻辑对过滤条件的完整影响可以归结为:WHERE子句只保留结果为TRUE的行,UNKNOWN和FALSE的行都会被过滤掉。这个规则在处理NOT时尤其反直觉——NOT UNKNOWN仍然是UNKNOWN,所以WHERE NOT column = NULL同样查不到任何数据。

3.2 NOT IN子查询返回空结果的经典案例

这是我面试时必问的第二道题,也是实战里极其隐蔽的坑。

SELECT * FROM customers WHERE customer_id NOT IN ( SELECT customer_id FROM orders );

看上去逻辑很清楚:查所有没下过单的客户。但如果orders表里有没有填过customer_id的记录,也就是存在NULL值,那么这条SQL的返回结果就是空集。

原因就是三值逻辑:当子查询返回的列表中存在NULL时,NOT IN本质上就是在说"不等于这个值,且不等于NULL"。"不等于NULL"的结果是UNKNOWN,所以整行都被过滤掉了。

这个问题在MySQL、Oracle、SQL Server、PostgreSQL里都存在,因为它根植于SQL标准本身。

解决方式有三种:

-- 方案一:排除NULL SELECT * FROM customers WHERE customer_id NOT IN ( SELECT customer_id FROM orders WHERE customer_id IS NOT NULL ); -- 方案二:改用NOT EXISTS(推荐) SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id ); -- 方案三:LEFT JOIN + IS NULL SELECT c.* FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.customer_id IS NULL;

我个人的建议是:能写NOT EXISTS就写NOT EXISTS,不仅避开NULL陷阱,执行效率通常也更高。

3.3 处理NULL的常用函数

实际业务里,我们不能只是避坑,还要主动把NULL转化成有意义的值。最常用的两个函数是COALESCE和IFNULL/ISNULL。

COALESCE是SQL标准函数,接受多个参数,返回第一个非NULL的值:

SELECT COALESCE(phone, email, '无联系方式') FROM users;

这个函数在处理多字段优先级时非常好用,比如取联系方式时电话优先、其次邮箱、最后给默认值。

IFNULL是MySQL的写法,ISNULL是SQL Server的写法,作用和COALESCE类似,但只能处理两个参数。跨数据库开发时统一用COALESCE会更稳妥。

另外要提醒一点:对NULL做聚合函数也容易被坑。COUNT(*)会统计所有行,COUNT(column)只会统计该列非NULL的行。SUM、AVG等函数会自动忽略NULL,但如果一列全是NULL,SUM返回NULL而不是0,在前端展示时可能直接空白。这时同样需要COALESCE(SUM(amount), 0)来兜底。

4. DISTINCT与窗口函数:更高级的过滤玩法

4.1 DISTINCT的适用边界

去重是数据过滤的一个重要分支。热搜词里"sql语句去重"出现频率相当高,说明大家都被重复数据困扰过。

最简单的去重是SELECT DISTINCT:

SELECT DISTINCT department FROM employees;

但DISTINCT有几个使用边界必须清楚:

第一,DISTINCT是对整个SELECT列组合去重,不是对某一列单独去重。SELECT DISTINCT department, grade FROM employees会把相同department但不同grade的组合都保留下来,这通常不是你想要的效果。

第二,DISTINCT会消耗额外的排序或哈希资源。大表上做DISTINCT往往很慢,因为数据库需要在内存或临时表里构建去重结构。如果只是为了快速看一眼有哪些取值,可以考虑用GROUP BY:

SELECT department FROM employees GROUP BY department;

这两条SQL的结果在大多数情况下是一致的,但GROUP BY在某些数据分布下更能让优化器发挥索引的优势。

第三,如果需要去重后还要拿到其他字段的完整数据,DISTINCT就力不从心了。比如"每个部门里工资最高的人是谁"——这种诉求需要的是分组过滤,用窗口函数更合适。

4.2 ROW_NUMBER()做组内过滤

窗口函数是目前SQL面试和实战的高频考点。它的优势在于可以在不丢失明细数据的前提下,对每组数据进行排序编号,然后基于编号做过滤。

经典的"每个用户最近一单"场景:

SELECT * FROM ( SELECT order_id, user_id, amount, created_at, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY created_at DESC ) AS rn FROM orders ) t WHERE rn = 1;

这里有两个关键点。

第一,窗口函数不能直接出现在WHERE子句里。因为窗口函数是在FROM、WHERE、GROUP BY、HAVING之后才执行的,这时候行已经被过滤过一轮了。所以必须套一层子查询或使用CTE,把窗口函数算出来的列先投影出来,再在外面用WHERE过滤。

第二,PARTITION BY决定分组的维度,ORDER BY决定组内排序。当ORDER BY字段有重复值导致并列时,ROW_NUMBER()会随机分配序号,这会带来不确定性。如果要求同一排序值内取哪一行也稳定,需要追加一个唯一字段作为第二排序键:

ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY created_at DESC, order_id DESC ) AS rn

4.3 用CTE组织过滤逻辑

窗口函数子查询嵌套一多,SQL就变得很难读。CTE(公共表表达式)是更好的组织方式:

WITH ranked_orders AS ( SELECT order_id, user_id, amount, created_at, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY created_at DESC, order_id DESC ) AS rn FROM orders ) SELECT * FROM ranked_orders WHERE rn = 1;

CTE最大的好处是把"计算"和"过滤"两个逻辑层分开。内层负责把每个用户最近的订单算出来并打上序号,外层只负责过滤序号为1的行。我自己在写复杂报表SQL时,几乎都会用CTE把中间结果逐层构建,这样即使三个月后再回来看这段SQL,也还能快速理解每一步在干什么。

除了ROW_NUMBER(),LAG()和LEAD()也常用于行间比较类的过滤。比如查"与前一天相比销售额下降超过20%的日期":

WITH daily_sales AS ( SELECT sale_date, amount, LAG(amount) OVER (ORDER BY sale_date) AS prev_amount FROM sales ) SELECT * FROM daily_sales WHERE prev_amount IS NOT NULL AND amount < prev_amount * 0.8;

这种跨行比较的过滤,用传统子查询写需要复杂的自连接,窗口函数一行搞定。

5. 过滤条件的性能体检:慢SQL排查

5.1 索引失效的典型写法

数据过滤写得再正确,如果跑得太慢,在生产环境一样是事故。与过滤直接相关的性能问题,九成都出在索引上没有用上。下面这几种写法是典型的索引杀手。

第一种是函数包裹索引列:

SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';

只要对索引列使用了函数,数据库就无法利用B+树有序结构,只能全表扫描。解决办法是改写成范围条件:

SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';

第二种是前导通配符模糊匹配:

SELECT * FROM products WHERE name LIKE '%手机%';

前导%让索引无法定位起点,只能遍历所有叶子节点。如果业务确实需要任意位置的模糊搜索,只有两个靠谱选择:搜索引擎(如Elasticsearch)或者数据库自带的全文索引。

第三种是OR连接不同列的过滤条件。

SELECT * FROM users WHERE age = 30 OR city = '杭州';

如果age和city各有独立索引,优化器也可能使用index merge或全表扫描,取决于成本估算。通常情况下,OR条件除非每列单查都能高效走索引且优化器能合并,否则性能很难保证。

第四种是过滤条件中对列做了隐式类型转换或表达式运算。这在下一个小节展开。

5.2 隐藏的类型转换

隐式类型转换是最坑人的,因为它没有任何报错,但结果完全偏离预期。

最常见的是字符串字段被传入数字条件:

SELECT * FROM users WHERE phone = 13800138000;

如果phone列是VARCHAR类型,MySQL会把列里的字符串值逐一转换成数字再比较。这个转换过程不仅会导致索引失效,还可能引发一个更诡异的问题:'13800138000abc'转成数字也是13800138000,因为转换时从左到右遇到非数字字符就停。结果就是一条乱数据也能被匹配上。

另一个常见场景是字符集或排序规则不一致导致的隐式转换,比如UTF8的列和GBK的字面量比较。最优解法是保持字段定义和查询参数的类型一致,不要让数据库做任何类型转换。

5.3 OR改UNION ALL的思路

OR条件在多数情况下的执行计划不如UNION ALL理想。两种查询的语义有差别:

  • OR可能产生重复行,需要去重
  • UNION ALL直接合并结果,不去重

如果OR连接的条件分属不同的索引列,改写成效更好的UNION ALL往往能显著提速:

SELECT * FROM users WHERE age = 30 UNION ALL SELECT * FROM users WHERE city = '杭州' AND age <> 30;

注意第二段要显式排除第一段已经取走的age = 30的行,否则UNION ALL会产生重复数据。如果你能确定两段查询的结果集没有交集,那就不需要加排除条件。

这个优化思路的本质是:把数据库优化器难以处理的复杂条件,拆成它擅长处理的简单条件,再合并结果。但这个改写要非常小心,确定行不重复才用UNION ALL。如果拿不准,就先用UNION,再用执行计划对比开销,别盲目为了性能引入数据错误。

5.4 多种数据库的执行计划查看方法

排查慢SQL不能靠猜。不同数据库查看执行计划的方式差异很大,我把常用的命令列在下面供参考:

数据库查看执行计划的方式
MySQLEXPLAIN SELECT ...;
PostgreSQLEXPLAIN ANALYZE SELECT ...;
SQL Server选中SQL后按Ctrl+M或SET STATISTICS PROFILE ON
OracleEXPLAIN PLAN FOR SELECT ...;再查DBMS_XPLAN.DISPLAY

看执行计划时重点看三样东西:type字段是否出现了ALL(全表扫描)、rows估算是否与真实行数偏差过大、Extra里有没有Using filesort或Using temporary。这三个信号出现任何一个,都需要回到前面的索引失效检查清单里找原因。

另外一个实用技巧是:不要在高峰期对生产大表直接执行大范围过滤测试。先EXPLAIN,看执行计划,再考虑是否需要建索引或改写SQL。慢SQL优化最忌讳的就是拿线上数据库当试验场。

6. 过滤与安全的红线:SQL注入防护

6.1 拼接SQL为什么危险

数据过滤不仅可以筛数据,也会被不怀好意的输入利用。SQL注入的根本原因是在过滤条件和用户输入之间没有建立隔离边界。

看最典型的错误写法:

# 错误示范:直接拼接用户输入 sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"

如果用户在用户名输入框里填的是:

' OR '1'='1' --

拼接出来的SQL会变成:

SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = '...'

--后面的内容全部变成注释,前面的OR '1'='1'让WHERE条件恒真,整个用户表就暴露了。更危险的是,如果攻击者确定后端是MySQL,还可以通过分号拼接多条语句,利用堆叠注入直接删表。这类攻击的破坏力远超出过滤层的预期。

这里必须强调一句:永远不要信任任何外部输入,包括URL参数、表单字段、HTTP Header,甚至Cookie。数据过滤的第一道防线,就是不在SQL字符串里直接拼外部数据。

6.2 参数化查询的核心原理

参数化查询(Prepared Statement)是防御注入的黄金标准。它的核心原理是:SQL语句的语法结构先被数据库编译好,用户输入只作为参数值传入,完全不会被解析为SQL语法的一部分。

以Python的MySQL驱动为例:

sql = "SELECT * FROM users WHERE username = %s AND password = %s" cursor.execute(sql, (username, password))

即使username的值是' OR '1'='1' --,数据库也只会把它当成一个普通的字符串字面量去和username字段比较,永远不可能变成SQL逻辑的一部分。这就是参数化查询和字符串拼接在本质上的区别。

不同语言和数据库的参数占位符写法有差异:

环境占位符写法
Python + MySQLdb/PyMySQL%s
Python + psycopg2%s
Java + JDBC?
Go + database/sql?
Node.js + mysql2?

但底层原理一致:SQL骨架与参数分离,用户在过滤器上只能影响"值",无法影响"逻辑"。

ORM框架(比如SQLAlchemy、MyBatis、Hibernate)本身都基于Prepared Statement,正常使用情况下也是安全的。但要注意两个例外:

  • 自定义Query原生SQL时如果用了字符串拼接,ORM也救不了
  • LIKE查询里的通配符需要自己处理转义,但参数化仍然有效

6.3 应对排序和表名的白名单策略

参数化查询能解决值的问题,但解决不了结构的问题。如果用户传入的是排序字段名、表名、列名,这些属于SQL结构的一部分,无法用占位符替代。

比如:

SELECT * FROM products ORDER BY {sort_field} {sort_order}

如果直接拼sort_field,攻击者可能传入id; DROP TABLE products; --之类的字符串。正确做法是使用白名单映射:

allowed_sort_fields = { 'price': 'price', 'created_at': 'created_at', 'sales': 'sales_count', } sort_field = allowed_sort_fields.get(request_args.get('sort', 'created_at'), 'created_at')

同理,排序方向也只接受两个值:ASC或DESC,其他一律走默认值。白名单的核心思想是:结构选的自由度由服务端定义,客户端只能从预设选项里选择,而不是任意传字符串。

这个思路不仅用于顺序和排序,也适用于动态表名、动态列名等所有不可参数化的场景。过滤逻辑再怎么复杂,都不能把结构控制权交到用户手里。

6.4 补充:权限与最小暴露

注入防护不只是SQL层面的技术问题,还与数据库账号权限有关。如果应用连接的数据库账号只有SELECT权限,即使真的发生了注入,攻击者也无法删表改数据。我见过一些团队为了方便,应用账号直接用root或dba权限,这就等于把全部家底暴露在过滤器外层。

正确的做法是遵循最小权限原则:应用账号只授予业务必需的权限。查询账号只给SELECT,需要写入的模块单独用写入账号,且只授予对应表的INSERT/UPDATE权限。这样即使过滤层出现漏洞,破坏半径也被圈在最小范围内。

写在最后

数据过滤这个主题,看起来是SQL入门的第一课,但任何一个点深挖下去都是无底洞:三值逻辑、UNKNOWN与NULL的关系、索引与表达式之间的博弈、结构与值的边界。我做了这么多年数据相关工作,最大的体会是:写SQL跟写其他代码一样,不能只追求"看起来对",要能解释清楚每一条条件的执行逻辑,能预判它在特殊数据下的行为,能知道它在性能上是怎么落地的。

如果你正在学习SQL,建议从今天开始做一件事:把你曾经写过的所有WHERE条件拿出来,逐条检查三件事——有没有碰NULL、能不能走索引、是不是拼接传入的。这三关过了,数据过滤这块就算真正入门了。后续如果对执行计划、窗口函数的更多玩法感兴趣,可以继续关注这个系列。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 8:58:03

Linux压力测试工具详解:用stress模拟CPU、内存与磁盘高负载

简介&#xff1a;这是Linux平台上一款经典的压力测试工具stress的完整源码与文档包&#xff0c;面向系统管理员、运维工程师和性能测试开发者&#xff0c;可通过模拟CPU、内存的高负载场景&#xff0c;检验服务器在多任务并发下的稳定性、散热表现与极限吞吐能力&#xff0c;可…

作者头像 李华
网站建设 2026/10/6 8:57:56

Snowflake三层解耦架构:存储计算分离如何重构大数据数仓

运维自建大数据平台的人&#xff0c;应该都有过这种深夜体验&#xff1a;线上报表凌晨三点还没跑完&#xff0c;集群里几十个节点忙个不停&#xff0c;你能做的只有加机器、调参数&#xff0c;或者干等。大数据领域这些年一直在谈数据架构&#xff0c;但“架构”这个词常常停留…

作者头像 李华
网站建设 2026/10/6 8:56:56

基于Python与SnowNLP的旅游评论情感分析可视化系统

1. 项目概述1.1 核心需求解析先说结论&#xff1a;这个项目本质上是做了一套“旅游评论的情感分析流水线”&#xff0c;从数据采集、文本清洗、情感打分到可视化大屏展示&#xff0c;一整套闭环。对于计算机类毕业设计来说&#xff0c;它的亮点在于覆盖面广——爬虫、自然语言处…

作者头像 李华
网站建设 2026/10/6 8:55:53

Qt多线程入门:从界面卡死到线程方案选型与实战

写Qt多线程最怕什么&#xff1f;绝大多数小伙伴第一次遇到“界面假死”的时候&#xff0c;都以为是自己代码写崩了&#xff0c;其实是把耗时任务直接丢到了GUI线程里跑。我这个系列打算把Qt多线程的使用从头捋一遍&#xff0c;今天先讲最基础也最核心的东西——线程到底是什么、…

作者头像 李华
网站建设 2026/10/6 8:55:28

鸿蒙应用移植自动签名实战:HAP/HSP打包与hap-sign-tool排错指南

1. 移植Windows/Linux应用时被签名卡住的那一下1.1 IDE签名模式在批量移植场景下为什么不够用鸿蒙PC版出来之后&#xff0c;很多团队第一件事就是把手头Windows、Linux上的工具软件往这个系统搬。搬的方式无非两种&#xff1a;源码重新适配编译&#xff0c;或者通过兼容层直接拉…

作者头像 李华
网站建设 2026/10/6 8:55:23

Velvet Flag Atlas:用天鹅绒材质重塑国旗的视觉设计实验

做视觉设计这几年&#xff0c;我越来越觉得“看图”和“看物”是两回事。屏幕上的扁平色块和现实中指尖碰触到的纹理&#xff0c;完全是两种感知维度。所以当我第一次看到“Velvet Flag Atlas”这个概念时&#xff0c;立刻就被吸引住了——它把两个看似毫不相干的词汇拼在一起&…

作者头像 李华