【大数据 - Doris 实践】数据表的基本使用(三):数据模型
数据表的基本使用(三):数据模型
- 1.Aggregate 模型
- 1.1 例一:导入数据聚合
- 1.2 例二:保留明细数据
- 1.3 例三:导入数据与已有数据聚合
- 2.Uniq 模型
- 3.Duplicate 模型
- 4.数据模型的选择建议
- 5.聚合模型的局限性
Doris 的数据模型主要分为 3 类:Aggregate
、Uniq
、Duplicate
。
1.Aggregate 模型
表中的列按照是否设置了 AggregationType,分为 Key(维度列)和 Value(指标列)。没有设置 AggregationType 的称为 Key,设置了 AggregationType 的称为 Value。
当我们导入数据时,对于 Key 列相同的行会聚合成一行,而 Value 列会按照设置的 AggregationType 进行聚合。AggregationType 目前有以下四种聚合方式:
SUM
:求和,多行的 Value 进行累加。REPLACE
:替代,下一批数据中的 Value 会替换之前导入过的行中的 Value。REPLACE_IF_NOT_NULL
:当遇到null
值则不更新。MAX
:保留最大值。MIN
:保留最小值。
数据的聚合,在 Doris 中有如下三个阶段发生:
- 每一批次数据导入的 ETL 阶段。该阶段会在每一批次导入的数据内部进行聚合。
- 底层 BE 进行数据 Compaction 的阶段。该阶段,BE 会对已导入的不同批次的数据进行进一步的聚合。
- 数据查询阶段。在数据查询时,对于查询涉及到的数据,会进行对应的聚合。数据在不同时间,可能聚合的程度不一致。比如一批数据刚导入时,可能还未与之前已存在的数据进行聚合。但是对于用户而言,用户只能查询到聚合后的数据。即不同的聚合程度对于用户查询而言是透明的。用户需始终认为数据以最终的完成的聚合程度存在,而不应假设某些聚合还未发生。
1.1 例一:导入数据聚合
创建表:
CREATE TABLE IF NOT EXISTS test_db.example_site_visit
(`user_id` LARGEINT NOT NULL COMMENT "用户 id",`date` DATE NOT NULL COMMENT "数据灌入日期时间",`city` VARCHAR(20) COMMENT "用户所在城市",`age` SMALLINT COMMENT "用户年龄",`sex` TINYINT COMMENT "用户性别",`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",`last_visit_date_not_null` DATETIME REPLACE_IF_NOT_NULL DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10;
插入数据:
insert into test_db.example_site_visit
values (10000,'2017-10-01','北京',20,0,'2017-10-01 06:00:00','2017-10-01 06:00:00',20,10,10),(10000,'2017-10-01','北京',20,0,'2017-10-01 07:00:00','2017-10-01 07:00:00',15,2,2),(10001,'2017-10-01','北京',30,1,'2017-10-01 17:05:45','2017-10-01 07:00:00',2,22,22),(10002,'2017-10-02','上海',20,1,'2017-10-02 12:59:12',null,200,5,5),(10003,'2017-10-02','广州',32,0,'2017-10-02 11:20:00','2017-10-02 11:20:00',30,11,11),(10004,'2017-10-01','深圳',35,0,'2017-10-01 10:00:15','2017-10-01 10:00:15',100,3,3),(10004,'2017-10-03','深圳',35,0,'2017-10-03 10:20:22','2017-10-03 10:20:22',11,6,6);
注意:
insert into
单条数据这种操作在 Doris 里只能演示不能在生产使用,会引发写阻塞。
查看表:
select * from test_db.example_site_visit;
最终,用户 10000 只剩下了一行聚合后的数据。而其余用户的数据和原始数据保持一致。经过聚合,Doris 中最终只会存储聚合后的数据。换句话说,即明细数据会丢失,用户不能够再查询到聚合前的明细数据了。
1.2 例二:保留明细数据
创建表:
CREATE TABLE IF NOT EXISTS test_db.example_site_visit2
(`user_id` LARGEINT NOT NULL COMMENT "用户 id",`date` DATE NOT NULL COMMENT "数据灌入日期时间",`timestamp` DATETIME COMMENT "数据灌入时间,精确到秒",`city` VARCHAR(20) COMMENT "用户所在城市",`age` SMALLINT COMMENT "用户年龄",`sex` TINYINT COMMENT "用户性别",`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",`cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10;
插入数据:
insert into test_db.example_site_visit2
values (10000,'2017-10-01','2017-10-01 08:00:05','北京',20,0,'2017-10-01 06:00:00',20,10,10),(10000,'2017-10-01','2017-10-01 09:00:05','北京',20,0,'2017-10-01 07:00:00',15,2,2),(10001,'2017-10-01','2017-10-01 18:12:10','北京',30,1,'2017-10-01 17:05:45',2,22,22),(10002,'2017-10-02','2017-10-02 13:10:00','上海',20,1,'2017-10-02 12:59:12',200,5,5),(10003,'2017-10-02','2017-10-02 13:15:00','广州',32,0,'2017-10-02 11:20:00',30,11,11),(10004,'2017-10-01','2017-10-01 12:12:48','深圳',35,0,'2017-10-01 10:00:15',100,3,3),(10004,'2017-10-03','2017-10-03 12:38:20','深圳',35,0,'2017-10-03 10:20:22',11,6,6);
查看表:
select * from test_db.example_site_visit2;
存储的数据,和导入数据完全一样,没有发生任何聚合。这是因为,这批数据中,因为加入了 timestamp
列,所有行的 Key 都不完全相同。也就是说,只要保证导入的数据中,每一行的 Key 都不完全相同,那么即使在聚合模型下,Doris 也可以保存完整的明细数据。
1.3 例三:导入数据与已有数据聚合
往例一中继续插入数据:
insert into test_db.example_site_visit
values (10004,'2017-10-03','深圳',35,0,'2017-10-03 11:22:00',null,44,19,19),(10005,'2017-10-03','长沙',29,1,'2017-10-03 18:11:02','2017-10-03 18:11:02',3,1,1);
查看表:
select * from test_db.example_site_visit;
最终,用户 10004 的已有数据和新导入的数据发生了聚合。同时新增了 10005 用户的数据。
2.Uniq 模型
在某些多维分析场景下,用户更关注的是如何保证 Key 的唯一性,即如何获得 Primary Key 唯一性约束。因此,我们引入了 Uniq 的数据模型。该模型本质上是聚合模型的一个特例,也是一种简化的表结构表示方式。
创建表:
CREATE TABLE IF NOT EXISTS test_db.user
(`user_id` LARGEINT NOT NULL COMMENT "用户 id",`username` VARCHAR(50) NOT NULL COMMENT "用户昵称",`city` VARCHAR(20) COMMENT "用户所在城市",`age` SMALLINT COMMENT "用户年龄",`sex` TINYINT COMMENT "用户性别",`phone` LARGEINT COMMENT "用户电话",`address` VARCHAR(500) COMMENT "用户地址",`register_time` DATETIME COMMENT "用户注册时间"
)
UNIQUE KEY(`user_id`, `username`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10;
插入数据
insert into test_db.user
values (10000,'wuyanzu','北京',18,0,12345678910,'北京朝阳区','2017-10-01 07:00:00'),(10000,'wuyanzu','北京',19,0,12345678910,'北京朝阳区','2017-10-01 07:00:00'),(10000,'zhangsan','北京',20,0,12345678910,'北京海淀区','2017-11-15 06:10:20');
查询表:
select * from test_db.user;
Uniq 模型完全可以用聚合模型中的 REPLACE 方式替代。其内部的实现方式和数据存储方式也完全一样。
3.Duplicate 模型
在某些多维分析场景下,数据既没有主键,也没有聚合需求。Duplicate 数据模型可以满足这类需求。数据完全按照导入文件中的数据进行存储,不会有任何聚合。即使两行数据完全相同,也都会保留。 而在建表语句中指定的 DUPLICATE KEY
,只是用来指明底层数据按照那些列进行排序。
创建表:
CREATE TABLE IF NOT EXISTS test_db.example_log
(`timestamp` DATETIME NOT NULL COMMENT "日志时间",`type` INT NOT NULL COMMENT "日志类型",`error_code` INT COMMENT "错误码",`error_msg` VARCHAR(1024) COMMENT "错误详细信息",`op_id` BIGINT COMMENT "负责人 id",`op_time` DATETIME COMMENT "处理时间"
)
DUPLICATE KEY(`timestamp`, `type`)
DISTRIBUTED BY HASH(`timestamp`) BUCKETS 10;
插入数据:
insert into test_db.example_log
values ('2017-10-01 08:00:05',1,404,'not found page', 101, '2017-10-01 08:00:05'),('2017-10-01 08:00:05',1,404,'not found page', 101, '2017-10-01 08:00:05'),('2017-10-01 08:00:05',2,404,'not found page', 101, '2017-10-01 08:00:06'),('2017-10-01 08:00:06',2,404,'not found page', 101, '2017-10-01 08:00:07');
查看表:
select * from test_db.example_log;
4.数据模型的选择建议
因为数据模型在建表时就已经确定,且无法修改。所以,选择一个合适的数据模型非常重要。
- Aggregate 模型可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,非常适合有固定模式的报表类查询场景。但是该模型对
count(*)
查询很不友好。同时因为固定了 Value 列上的聚合方式,在进行其他类型的聚合查询时,需要考虑语意正确性。 - Uniq 模型针对需要唯一主键约束的场景,可以保证主键唯一性约束。但是无法利用 ROLLUP 等预聚合带来的查询优势(因为本质是
REPLACE
,没有SUM
这种聚合方式)。 - Duplicate 适合任意维度的
Ad-hoc
查询。虽然同样无法利用预聚合的特性,但是不受聚合模型的约束,可以发挥列存模型的优势(只读取相关列,而不需要读取所有 Key 列)。
5.聚合模型的局限性
这里我们针对 Aggregate 模型(包括 Uniq 模型),来介绍下聚合模型的局限性。
在聚合模型中,模型对外展现的,是最终聚合后的数据。也就是说,任何还未聚合的数据(比如说两个不同导入批次的数据),必须通过某种方式,以保证对外展示的一致性。我们举例说明。
假设表结构如下:
ColumnName | Type | AggregationType | Comment |
---|---|---|---|
user_id | LARGEINT | 用户 id | |
date | DATE | 数据灌入日期 | |
cost | BIGINT | SUM | 用户总消费 |
假设存储引擎中有如下两个已经导入完成的批次的数据:
batch 1
user_id | date | cost |
---|---|---|
10001 | 2017-11-20 | 50 |
10002 | 2017-11-21 | 39 |
batch 2
user_id | date | cost |
---|---|---|
10001 | 2017-11-20 | 1 |
10001 | 2017-11-21 | 5 |
10003 | 2017-11-22 | 22 |
可以看到,用户 10001 分属在两个导入批次中的数据还没有聚合。但是为了保证用户只能查询到如下最终聚合后的数据,在查询引擎中加入了聚合算子,来保证数据对外的一致性。
user_id | date | cost |
---|---|---|
10001 | 2017-11-20 | 51 |
10001 | 2017-11-21 | 5 |
10002 | 2017-11-21 | 39 |
10003 | 2017-11-22 | 22 |
另外,在聚合列(Value)上,执行与聚合类型不一致的聚合类查询时,要注意语意。比如我们在如上示例中执行如下查询:
SELECT MIN(cost) FROM table;
得到的结果是 5 5 5,而不是 1 1 1。
同时,这种一致性保证,在某些查询中,会极大的降低查询效率。
我们以最基本的 count(*)
查询为例:
SELECT COUNT(*) FROM table;
在其他数据库中,这类查询都会很快的返回结果。因为在实现上,我们可以通过如 “导入时对行进行计数,保存 count 的统计信息”,或者在查询时 “仅扫描某一列数据,获得 count 值” 的方式,只需很小的开销,即可获得查询结果。但是在 Doris 的聚合模型中,这种查询开销非常大。
上面的例子,select count(*) from table
的正确结果应该为 4 4 4。但如果我们只扫描 user_id
这一列,如果加上查询时聚合,最终得到的结果是 3 3 3(10001,10002,10003)。而如果不加查询时聚合,则得到的结果是 5 5 5(两批次一共 5 5 5 行数据)。可见这两个结果都是不对的。
为了得到正确的结果,我们必须同时读取 user_id
和 date
这两列的数据,再加上查询时聚合,才能返回 4 4 4 这个正确的结果。也就是说,在 count(*)
查询中,Doris 必须扫描所有的 AGGREGATE KEY
列(这里就是 user_id
和 date
),并且聚合后,才能得到语意正确的结果。当聚合列非常多时,count(*)
查询需要扫描大量的数据。
因此,当业务上有频繁的 count(*)
查询时,我们建议用户通过增加一个值恒为 1 1 1 的,聚合类型为 SUM 的列来模拟 count()
。如刚才的例子中的表结构,我们修改如下:
ColumnName | Type | AggregateType | Comment |
---|---|---|---|
user_id | BIGINT | 用户 id | |
date | DATE | 数据灌入日期 | |
cost | BIGINT | SUM | 用户总消费 |
count | BIGINT | SUM | 用于计算 count |
增加一个 count
列,并且导入数据中,该列值恒为 1 1 1。则 select count(*) from table
的结果等价于 select sum(count) from table
。而后者的查询效率将远高于前者。不过这种方式也有使用限制,就是用户需要自行保证,不会重复导入 AGGREGATE KEY
列都相同的行。否则,select sum(count) from table
只能表述原始导入的行数,而不是 select count(*) from table
的语义。
另一种方式,就是将如上的 count
列的聚合类型改为 REPLACE,且依然值恒为 1。那么 select sum(count) from table
和 select count(*) from table
的结果将是一致的。并且这种方式,没有导入重复行的限制。
相关文章:
【大数据 - Doris 实践】数据表的基本使用(三):数据模型
数据表的基本使用(三):数据模型 1.Aggregate 模型1.1 例一:导入数据聚合1.2 例二:保留明细数据1.3 例三:导入数据与已有数据聚合 2.Uniq 模型3.Duplicate 模型4.数据模型的选择建议5.聚合模型的局限性 Dori…...
PMP和CSPM证书,怎么选?
最近有宝子们在问,从事项目管理行业到底建议考什么证书?是不是CSPM证书一出来,PMP证书就没用了?其实不是。今天胖圆给大家解释一下二者都适合什么人群考~ PMP证书是什么? PMP项目管理专业人士资格认证,由…...
企业宣传为何要重视领军人物包装?领军人物对企业营销的价值和作用分析
在企业的完整形象中,产品、品牌、高管是最重要的组成部分。而大部分企业会把品牌形象放在首位,将公司所有的推广资源都倾斜在这一块,但其实,企业高管形象的塑造和传播也非常重要。小马识途建议中小企业在成长过程中提早对高管形象…...
什么是内存泄漏?JavaScript 垃圾回收机制原理及方式有哪些?哪些操作会造成内存泄漏?
1、什么是内存泄漏? 内存泄漏是前端开发中的一个常见问题,可能导致项目变得缓慢、不稳定甚至崩溃。内存泄漏是指不再用到的内存没有及时被释放,从而造成内存上的浪费。 2、 JavaScript 垃圾回收机制 1) 原理: JavaS…...

C++项目实战——基于多设计模式下的同步异步日志系统-⑫-日志宏全局接口设计(代理模式)
文章目录 专栏导读日志宏&全局接口设计全局接口测试项目目录结构整理示例代码拓展示例代码 专栏导读 🌸作者简介:花想云 ,在读本科生一枚,C/C领域新星创作者,新星计划导师,阿里云专家博主,C…...

京东数据接口:京东数据分析怎么做?
电商运营中数据分析的重要性不言而喻,而想要做数据分析,就要先找到数据,利用数据接口我们能够更轻松的获得比较全面的数据。因此,目前不少品牌商家都选择使用一些数据接口来获取相关电商数据、以更好地做好数据分析。 鲸参谋电商…...
使用Git在本地创建一个仓库并将其推送到GitHub
前记: git svn sourcetree gitee github gitlab gitblit gitbucket gitolite gogs 版本控制 | 仓库管理 ---- 系列工程笔记. Platform:Windows 10 Git version:git version 2.32.0.windows.1 Function: 使用Git在本地创建一个…...

5.覆盖增强技术——PUCCHPUSCH
PUSCH增强方案的标准化工作 1.PUSCH重复传输类型A增强,包括两种增强机制:增加最大重复传输次数,以及基于可用上行时隙的重复传输次数技术方式。 2.基于频域的解决方案,包括时隙间/时隙内跳频的增强 3.支持跨多个时隙的传输块&…...

徐建鸿:深耕中医康养的“托钵行者”
为什么是“庄人堂”?杭州“庄人堂”医药科技公司董事长徐建鸿很乐意和别人分享这个名称的由来,一方面是庄子首先提出“养生”这个概念,接近上工治未病的上医,取名“庄人堂”代表庄子门生,向古哲先贤致敬!另…...

基于svg+js实现简单动态时钟
实现思路 创建SVG容器:首先,创建一个SVG容器元素,用于容纳时钟的各个部分。指定SVG的宽度、高度以及命名空间。 <svg width"200" height"200" xmlns"http://www.w3.org/2000/svg"><!-- 在此添加时钟…...

端到端测试(End-to-end tests)重试策略
作者|Giuseppe Donati,Trivago公司Web测试自动化工程师 整理|TesterHome 失败后重试,是好是坏? 为什么要在失败时重试所有测试?为什么不? 作为Trivago(德国酒店搜索服务平台&…...

三相交错LLC软启动控制驱动波形分析--死区时间与占空比关系
三相交错LLC软启动控制驱动波形分析 文章目录 三相交错LLC软启动控制驱动波形分析一、电路原理二、时序分析三、环路分析四、控制策略1.软启动驱动波形趋势2.软启动驱动波形占空图3.软启动驱动波形详细图4.软启动代码分析5.Debug调试界面5.死区时间与实际输出5.1 死区时间50--对…...

数据结构详细笔记——栈与队列
文章目录 栈的三要素逻辑结构(定义)数据的运算(基本操作)存储结构(物理结构)顺序栈(顺序存储)链栈(链式存储) 队列的三要素逻辑结构(定义…...

JVM调试命令与调试工具
目录 一、JDK自带命令 1、jps 2、jstat(FullGC频繁解决方案) 3、jmap 4、jhat 5、jstack(cpu占用高解决方案) 6、jinfo 二、JDK的可视化工具JConsole 1、JConsole 2、VisualVM 一、JDK自带命令 Sun JDK监控和故障处理命令如: 1、jps JVM Proc…...

《软件方法》第1章2023版连载(07)UML的历史和现状
DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 1.3 统一建模语言UML 1.3.1 UML的历史和现状 上一节阐述了A→B→C→D的推导是不可避免的,但具体如何推导,有各种不同的做法,这些做法可以称为“方…...
chromium 54 chrome 各个版本发布功能列表(109-119)
chromium Features 109-119 From https://chromestatus.com/features chromium109 Features:12 Auto range support for font descriptors inside font-face rule Auto range support for variable fonts in ‘font-weight’, ‘font-style’ and ‘font-stretch’ descrip…...

Linux实现原理 — I/O 处理流程与优化手段
Linux I/O 接口 Linux I/O 接口可以分为以下几种类型: 文件 I/O 接口:用于对文件进行读写操作的接口,包括 open()、read()、write()、close()、lseek() 等。 网络 I/O 接口:用于网络通信的接口,包括 socket()、conne…...

第 367 场 LeetCode 周赛题解
A 找出满足差值条件的下标 I 模拟 class Solution { public:vector<int> findIndices(vector<int> &nums, int indexDifference, int valueDifference) {int n nums.size();for (int i 0; i < n; i)for (int j 0; j < i; j)if (i - j > indexDiffe…...

最新百度统计配置图文教程,获取siteId、百度统计AccessToken、百度统计代码教程
一、前言 很多网友开发者都不知道百度统计siteId、百度统计token怎么获取,在网上找的教程都是几年前老的教程,因此给大家出一期详细百度统计siteId、百度统计token、百度统计代码获取详细步骤教程。 二、登录到百度统计 1.1 登录到百度统计官网 使用…...

【C++ 学习 ㉘】- 详解 C++11 的列表初始化
目录 一、C11 简介 二、列表初始化 2.1 - 统一初始化 2.2 - 列表初始化的使用细节 2.2.1 - 聚合类型的定义 2.2.2 - 注意事项 2.3 - initializer_list 2.3.1 - 基本使用 2.3.2 - 源码剖析 一、C11 简介 1998 年,C 标准委员会发布了第一版 C 标准࿰…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...

对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...

04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...