From d04f8702ee1e4417b00eaa42bdf13ff523f985f1 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Wed, 14 Jan 2026 22:00:57 +0400 Subject: [PATCH 001/179] OS-52,53,54 --- .../greenplum/dsolovianov-202601142129.sql | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 dsolovianov/greenplum/dsolovianov-202601142129.sql diff --git a/dsolovianov/greenplum/dsolovianov-202601142129.sql b/dsolovianov/greenplum/dsolovianov-202601142129.sql new file mode 100644 index 0000000..045de55 --- /dev/null +++ b/dsolovianov/greenplum/dsolovianov-202601142129.sql @@ -0,0 +1,417 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- ЭТАП 1 +-- 1.1 - СОЗДАНИЕ СХЕМ + +CREATE SCHEMA core; + + +ALTER SCHEMA core OWNER TO dsolovianov; + + +CREATE SCHEMA dm; + + +ALTER SCHEMA dm OWNER TO dsolovianov; + + +CREATE SCHEMA raw; + + +ALTER SCHEMA raw OWNER TO dsolovianov; + +SET default_tablespace = ''; + + + +-- 1.2 - Создание raw-таблиц + +CREATE TABLE raw.courses ( + id integer, + title character varying(50), + category character varying(50), + created_at character varying(50) +); + + +ALTER TABLE raw.courses OWNER TO dsolovianov; + + +CREATE TABLE raw.enrollments ( + id integer, + user_id integer, + course_id integer, + enrolled_at character varying(50) +); + + +ALTER TABLE raw.enrollments OWNER TO dsolovianov; + + +CREATE TABLE raw.lesson_views ( + id integer, + user_id integer, + lesson_id integer, + viewed_at character varying(50) +); + + +ALTER TABLE raw.lesson_views OWNER TO dsolovianov; + + +CREATE TABLE raw.lessons ( + id integer, + course_id integer, + title character varying(50), + duration_min integer +); + + +ALTER TABLE raw.lessons OWNER TO dsolovianov; + + +CREATE TABLE raw.users ( + id integer, + name character varying(50), + age integer, + email character varying(50), + registration_date character varying(50) +); + + +ALTER TABLE raw.users OWNER TO dsolovianov; + + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Этап 3. Слой Core + +CREATE TABLE core.dim_course ( + course_id integer NOT NULL, + title text, + category text +); + + +ALTER TABLE core.dim_course OWNER TO dsolovianov; + + +CREATE TABLE core.dim_date ( + date date NOT NULL, + year integer, + month integer, + day integer, + weekday text +); + + +ALTER TABLE core.dim_date OWNER TO dsolovianov; + + +CREATE TABLE core.dim_lesson ( + lesson_id integer NOT NULL, + title text, + duration_min integer, + course_id integer +); + + +ALTER TABLE core.dim_lesson OWNER TO dsolovianov; + + +CREATE TABLE core.dim_user ( + user_id integer NOT NULL, + name text, + age integer, + email text, + registration_date date +); + + +ALTER TABLE core.dim_user OWNER TO dsolovianov; + + +CREATE TABLE core.enrollments ( + user_id integer, + course_id integer, + enrolled_at timestamp without time zone +); + + +ALTER TABLE core.enrollments OWNER TO dsolovianov; + + +CREATE TABLE core.fact_lesson_views ( + user_id integer, + lesson_id integer, + course_id integer, + viewed_at timestamp without time zone +); + + +ALTER TABLE core.fact_lesson_views OWNER TO dsolovianov; + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +COPY core.dim_course (course_id, title, category) FROM stdin; +2 Python для анализа данных programming +1 SQL для начинающих data +3 BI с нуля business +\. + + +COPY core.dim_date (date, year, month, day, weekday) FROM stdin; +2023-01-15 2023 1 15 Sunday +2023-01-16 2023 1 16 Monday +2023-01-21 2023 1 21 Saturday +2023-01-20 2023 1 20 Friday +2023-03-05 2023 3 5 Sunday +2023-02-01 2023 2 1 Wednesday +2023-04-02 2023 4 2 Sunday +2023-03-06 2023 3 6 Monday +2023-04-01 2023 4 1 Saturday +2023-04-03 2023 4 3 Monday +\. + + +-- +-- TOC entry 2500 (class 0 OID 25643) +-- Dependencies: 274 +-- Data for Name: dim_lesson; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_lesson (lesson_id, title, duration_min, course_id) FROM stdin; +4 Основы BI 12 3 +5 Метрики и дашборды 18 3 +2 JOIN 15 1 +1 SELECT и FROM 10 1 +3 Pandas 20 2 +\. + + +-- +-- TOC entry 2498 (class 0 OID 25627) +-- Dependencies: 272 +-- Data for Name: dim_user; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_user (user_id, name, age, email, registration_date) FROM stdin; +3 Charlie 22 charlie@mail.com 2023-02-20 +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2502 (class 0 OID 25872) +-- Dependencies: 276 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.enrollments (user_id, course_id, enrolled_at) FROM stdin; +4 3 2023-04-01 00:00:00 +1 2 2023-02-01 00:00:00 +2 1 2023-01-20 00:00:00 +1 1 2023-01-15 00:00:00 +3 2 2023-03-05 00:00:00 +\. + + +-- +-- TOC entry 2503 (class 0 OID 25885) +-- Dependencies: 277 +-- Data for Name: fact_lesson_views; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) FROM stdin; +2 1 1 2023-01-21 09:00:00 +1 2 1 2023-01-16 10:15:00 +3 3 2 2023-03-06 12:00:00 +1 1 1 2023-01-16 10:00:00 +4 4 3 2023-04-02 10:00:00 +4 5 3 2023-04-03 10:00:00 +\. + + +-- +-- TOC entry 2494 (class 0 OID 25615) +-- Dependencies: 268 +-- Data for Name: courses; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.courses (id, title, category, created_at) FROM stdin; +3 BI с нуля business 2023-03-01 +1 SQL для начинающих data 2023-01-01 +2 Python для анализа данных programming 2023-01-15 +\. + + +-- +-- TOC entry 2496 (class 0 OID 25621) +-- Dependencies: 270 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.enrollments (id, user_id, course_id, enrolled_at) FROM stdin; +1 1 1 2023-01-15 +2 1 2 2023-02-01 +3 2 1 2023-01-20 +4 3 2 2023-03-05 +5 4 3 2023-04-01 +\. + + +-- +-- TOC entry 2497 (class 0 OID 25624) +-- Dependencies: 271 +-- Data for Name: lesson_views; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lesson_views (id, user_id, lesson_id, viewed_at) FROM stdin; +2 1 2 2023-01-16 10:15:00.000 +1 1 1 2023-01-16 10:00:00.000 +3 2 1 2023-01-21 09:00:00.000 +5 4 4 2023-04-02 10:00:00.000 +4 3 3 2023-03-06 12:00:00.000 +6 4 5 2023-04-03 10:00:00.000 +\. + + +-- +-- TOC entry 2495 (class 0 OID 25618) +-- Dependencies: 269 +-- Data for Name: lessons; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lessons (id, course_id, title, duration_min) FROM stdin; +1 1 SELECT и FROM 10 +2 1 JOIN 15 +3 2 Pandas 20 +4 3 Основы BI 12 +5 3 Метрики и дашборды 18 +\. + + +-- +-- TOC entry 2493 (class 0 OID 25612) +-- Dependencies: 267 +-- Data for Name: users; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.users (id, name, age, email, registration_date) FROM stdin; +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +3 Charlie 22 charlie@mail.com 2023-02-20 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2314 (class 2606 OID 25642) +-- Name: dim_course dim_course_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_course + ADD CONSTRAINT dim_course_pkey PRIMARY KEY (course_id); + + +-- +-- TOC entry 2318 (class 2606 OID 25663) +-- Name: dim_date dim_date_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_date + ADD CONSTRAINT dim_date_pkey PRIMARY KEY (date); + + +-- +-- TOC entry 2316 (class 2606 OID 25650) +-- Name: dim_lesson dim_lesson_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_pkey PRIMARY KEY (lesson_id); + + +-- +-- TOC entry 2312 (class 2606 OID 25634) +-- Name: dim_user dim_user_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_user + ADD CONSTRAINT dim_user_pkey PRIMARY KEY (user_id); + + +-- +-- TOC entry 2319 (class 2606 OID 25651) +-- Name: dim_lesson dim_lesson_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2320 (class 2606 OID 25880) +-- Name: enrollments enrollments_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2321 (class 2606 OID 25875) +-- Name: enrollments enrollments_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- +-- TOC entry 2322 (class 2606 OID 25898) +-- Name: fact_lesson_views fact_lesson_views_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2323 (class 2606 OID 25893) +-- Name: fact_lesson_views fact_lesson_views_lesson_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_lesson_id_fkey FOREIGN KEY (lesson_id) REFERENCES core.dim_lesson(lesson_id); + + +-- +-- TOC entry 2324 (class 2606 OID 25888) +-- Name: fact_lesson_views fact_lesson_views_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- Completed on 2026-01-14 21:29:24 + +-- +-- PostgreSQL database dump complete +-- + From a583e8eb1ad3db1c09880bd082825aa93e3ef788 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Wed, 14 Jan 2026 22:04:16 +0400 Subject: [PATCH 002/179] OS-53 --- dsolovianov/greenplum/courses_202601031845.csv | 4 ++++ dsolovianov/greenplum/enrollments_202601031845.csv | 6 ++++++ dsolovianov/greenplum/lesson_views_202601031846.csv | 7 +++++++ dsolovianov/greenplum/lessons_202601031846.csv | 6 ++++++ dsolovianov/greenplum/users_202601031846.csv | 6 ++++++ 5 files changed, 29 insertions(+) create mode 100644 dsolovianov/greenplum/courses_202601031845.csv create mode 100644 dsolovianov/greenplum/enrollments_202601031845.csv create mode 100644 dsolovianov/greenplum/lesson_views_202601031846.csv create mode 100644 dsolovianov/greenplum/lessons_202601031846.csv create mode 100644 dsolovianov/greenplum/users_202601031846.csv diff --git a/dsolovianov/greenplum/courses_202601031845.csv b/dsolovianov/greenplum/courses_202601031845.csv new file mode 100644 index 0000000..f604a2e --- /dev/null +++ b/dsolovianov/greenplum/courses_202601031845.csv @@ -0,0 +1,4 @@ +"id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 +2,Python для анализа данных,programming,2023-01-15 +3,BI с нуля,business,2023-03-01 diff --git a/dsolovianov/greenplum/enrollments_202601031845.csv b/dsolovianov/greenplum/enrollments_202601031845.csv new file mode 100644 index 0000000..7b27fa5 --- /dev/null +++ b/dsolovianov/greenplum/enrollments_202601031845.csv @@ -0,0 +1,6 @@ +"id","user_id","course_id","enrolled_at" +1,1,1,2023-01-15 +2,1,2,2023-02-01 +3,2,1,2023-01-20 +4,3,2,2023-03-05 +5,4,3,2023-04-01 diff --git a/dsolovianov/greenplum/lesson_views_202601031846.csv b/dsolovianov/greenplum/lesson_views_202601031846.csv new file mode 100644 index 0000000..23e3561 --- /dev/null +++ b/dsolovianov/greenplum/lesson_views_202601031846.csv @@ -0,0 +1,7 @@ +"id","user_id","lesson_id","viewed_at" +1,1,1,2023-01-16 10:00:00.000 +2,1,2,2023-01-16 10:15:00.000 +3,2,1,2023-01-21 09:00:00.000 +4,3,3,2023-03-06 12:00:00.000 +5,4,4,2023-04-02 10:00:00.000 +6,4,5,2023-04-03 10:00:00.000 diff --git a/dsolovianov/greenplum/lessons_202601031846.csv b/dsolovianov/greenplum/lessons_202601031846.csv new file mode 100644 index 0000000..24505cf --- /dev/null +++ b/dsolovianov/greenplum/lessons_202601031846.csv @@ -0,0 +1,6 @@ +"id","course_id","title","duration_min" +1,1,SELECT и FROM,10 +2,1,JOIN,15 +3,2,Pandas,20 +4,3,Основы BI,12 +5,3,Метрики и дашборды,18 diff --git a/dsolovianov/greenplum/users_202601031846.csv b/dsolovianov/greenplum/users_202601031846.csv new file mode 100644 index 0000000..54c69df --- /dev/null +++ b/dsolovianov/greenplum/users_202601031846.csv @@ -0,0 +1,6 @@ +"id","name","age","email","registration_date" +1,Alice,25,alice@mail.com,2023-01-10 +2,Bob,30,bob@gmail.com,2023-02-05 +3,Charlie,22,charlie@mail.com,2023-02-20 +4,Diana,28,diana@mail.com,2023-03-01 +5,Ethan,35,ethan@gmail.com,2023-03-10 From 3b1d09f219cb54786f487504e5b65837436ef067 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Wed, 14 Jan 2026 22:09:24 +0400 Subject: [PATCH 003/179] pstgsql --- dsolovianov/{greenplum => postgresql}/courses_202601031845.csv | 0 .../{greenplum => postgresql}/enrollments_202601031845.csv | 0 .../{greenplum => postgresql}/lesson_views_202601031846.csv | 0 dsolovianov/{greenplum => postgresql}/lessons_202601031846.csv | 0 dsolovianov/{greenplum => postgresql}/users_202601031846.csv | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename dsolovianov/{greenplum => postgresql}/courses_202601031845.csv (100%) rename dsolovianov/{greenplum => postgresql}/enrollments_202601031845.csv (100%) rename dsolovianov/{greenplum => postgresql}/lesson_views_202601031846.csv (100%) rename dsolovianov/{greenplum => postgresql}/lessons_202601031846.csv (100%) rename dsolovianov/{greenplum => postgresql}/users_202601031846.csv (100%) diff --git a/dsolovianov/greenplum/courses_202601031845.csv b/dsolovianov/postgresql/courses_202601031845.csv similarity index 100% rename from dsolovianov/greenplum/courses_202601031845.csv rename to dsolovianov/postgresql/courses_202601031845.csv diff --git a/dsolovianov/greenplum/enrollments_202601031845.csv b/dsolovianov/postgresql/enrollments_202601031845.csv similarity index 100% rename from dsolovianov/greenplum/enrollments_202601031845.csv rename to dsolovianov/postgresql/enrollments_202601031845.csv diff --git a/dsolovianov/greenplum/lesson_views_202601031846.csv b/dsolovianov/postgresql/lesson_views_202601031846.csv similarity index 100% rename from dsolovianov/greenplum/lesson_views_202601031846.csv rename to dsolovianov/postgresql/lesson_views_202601031846.csv diff --git a/dsolovianov/greenplum/lessons_202601031846.csv b/dsolovianov/postgresql/lessons_202601031846.csv similarity index 100% rename from dsolovianov/greenplum/lessons_202601031846.csv rename to dsolovianov/postgresql/lessons_202601031846.csv diff --git a/dsolovianov/greenplum/users_202601031846.csv b/dsolovianov/postgresql/users_202601031846.csv similarity index 100% rename from dsolovianov/greenplum/users_202601031846.csv rename to dsolovianov/postgresql/users_202601031846.csv From 69e227b59fab5fc17491c61d6aeaa365cad156d1 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 Jan 2026 06:03:41 +0300 Subject: [PATCH 004/179] SQL homework done 1st lesson --- aharlamcev/postgresql/sqlhomework.sql | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 aharlamcev/postgresql/sqlhomework.sql diff --git a/aharlamcev/postgresql/sqlhomework.sql b/aharlamcev/postgresql/sqlhomework.sql new file mode 100644 index 0000000..4fb1078 --- /dev/null +++ b/aharlamcev/postgresql/sqlhomework.sql @@ -0,0 +1,23 @@ +create table products_extended ( +id INT primary key, +name VARCHAR(100) not null, +category VARCHAR(50), +price DECIMAL(10, 2), +in_stock BOOLEAN default true, +email_support VARCHAR(50) +); + +insert into products_extended (id, name, category, price, in_stock, email_support) +values (1, 'Mi Band 7', 'smartwatch', 49, true, 'miband@xiaomi.com'); + +insert into products_extended (id, name, category, price, in_stock, email_support) +values (2, 'Surface Pro', 'tablet', 1299, true, 'surface@microsoft.com'); + +select * from products_extended -- проверил как выглядит таблица + +alter table products_extended +add column rating DECIMAL(5, 2); + +truncate table products_extended; + +drop table products_extended From f050458471e3959dc4bf7ad2740a56f95104dae3 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 17 Jan 2026 10:02:47 +0300 Subject: [PATCH 005/179] Created tables users, products, orders, order_items. --- aharlamcev/postgresql/homeworkDDL.sql | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 aharlamcev/postgresql/homeworkDDL.sql diff --git a/aharlamcev/postgresql/homeworkDDL.sql b/aharlamcev/postgresql/homeworkDDL.sql new file mode 100644 index 0000000..7bc5255 --- /dev/null +++ b/aharlamcev/postgresql/homeworkDDL.sql @@ -0,0 +1,32 @@ +create table users ( +id int primary key, +name VARCHAR(100) not null, +email VARCHAR(100), +country VARCHAR(50), +crated_at date not null +) ; + +create table products ( +id int primary key, +name VARCHAR(100) not null, +category VARCHAR(50), +price DECIMAL(10, 2), +in_stock BOOLEAN default true +) ; + +create table orders ( +id int primary key, +user_id int, +order_date DATE not null, +status VARCHAR(20), +foreign key (user_id) references users(id) +) ; + +create table order_items ( +order_id int, +product_id int, +quantity int not null, +price_at_purchase DECIMAL(10, 2) not null, +foreign key (order_id) references orders(id), +foreign key (product_id) references products(id) +) ; \ No newline at end of file From 65b40c69a0bb5d64f32233aa021a0728ffc19157 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 17 Jan 2026 14:45:48 +0300 Subject: [PATCH 006/179] renamed column created_at --- aharlamcev/postgresql/homeworkDDL.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aharlamcev/postgresql/homeworkDDL.sql b/aharlamcev/postgresql/homeworkDDL.sql index 7bc5255..5e43015 100644 --- a/aharlamcev/postgresql/homeworkDDL.sql +++ b/aharlamcev/postgresql/homeworkDDL.sql @@ -29,4 +29,7 @@ quantity int not null, price_at_purchase DECIMAL(10, 2) not null, foreign key (order_id) references orders(id), foreign key (product_id) references products(id) -) ; \ No newline at end of file +) ; + +alter table users +rename column crated_at to created_at; \ No newline at end of file From 8ff9c227dd2b55e492614fb238ddba05660679cc Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Sat, 17 Jan 2026 22:46:31 +0400 Subject: [PATCH 007/179] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D1=8B=20=D0=B2=20CORE-?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../greenplum/dsolovianov-202601172242.sql | 508 ++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 dsolovianov/greenplum/dsolovianov-202601172242.sql diff --git a/dsolovianov/greenplum/dsolovianov-202601172242.sql b/dsolovianov/greenplum/dsolovianov-202601172242.sql new file mode 100644 index 0000000..7608706 --- /dev/null +++ b/dsolovianov/greenplum/dsolovianov-202601172242.sql @@ -0,0 +1,508 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 9.4.26 +-- Dumped by pg_dump version 17.5 + +-- Started on 2026-01-17 22:42:00 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 10 (class 2615 OID 25610) +-- Name: core; Type: SCHEMA; Schema: -; Owner: dsolovianov +-- + +CREATE SCHEMA core; + + +ALTER SCHEMA core OWNER TO dsolovianov; + +-- +-- TOC entry 11 (class 2615 OID 25611) +-- Name: dm; Type: SCHEMA; Schema: -; Owner: dsolovianov +-- + +CREATE SCHEMA dm; + + +ALTER SCHEMA dm OWNER TO dsolovianov; + +-- +-- TOC entry 9 (class 2615 OID 25609) +-- Name: raw; Type: SCHEMA; Schema: -; Owner: dsolovianov +-- + +CREATE SCHEMA raw; + + +ALTER SCHEMA raw OWNER TO dsolovianov; + +SET default_tablespace = ''; + +-- +-- TOC entry 273 (class 1259 OID 25635) +-- Name: dim_course; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_course ( + course_id integer NOT NULL, + title text, + category text +); + + +ALTER TABLE core.dim_course OWNER TO dsolovianov; + +-- +-- TOC entry 275 (class 1259 OID 25656) +-- Name: dim_date; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_date ( + date date NOT NULL, + year integer, + month integer, + day integer, + weekday text +); + + +ALTER TABLE core.dim_date OWNER TO dsolovianov; + +-- +-- TOC entry 274 (class 1259 OID 25643) +-- Name: dim_lesson; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_lesson ( + lesson_id integer NOT NULL, + title text, + duration_min integer, + course_id integer +); + + +ALTER TABLE core.dim_lesson OWNER TO dsolovianov; + +-- +-- TOC entry 272 (class 1259 OID 25627) +-- Name: dim_user; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_user ( + user_id integer NOT NULL, + name text, + age integer, + email text, + registration_date date +); + + +ALTER TABLE core.dim_user OWNER TO dsolovianov; + +-- +-- TOC entry 276 (class 1259 OID 25872) +-- Name: enrollments; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.enrollments ( + user_id integer, + course_id integer, + enrolled_at timestamp without time zone +); + + +ALTER TABLE core.enrollments OWNER TO dsolovianov; + +-- +-- TOC entry 277 (class 1259 OID 25885) +-- Name: fact_lesson_views; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.fact_lesson_views ( + user_id integer, + lesson_id integer, + course_id integer, + viewed_at timestamp without time zone +); + + +ALTER TABLE core.fact_lesson_views OWNER TO dsolovianov; + +-- +-- TOC entry 268 (class 1259 OID 25615) +-- Name: courses; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.courses ( + id integer, + title character varying(50), + category character varying(50), + created_at character varying(50) +); + + +ALTER TABLE raw.courses OWNER TO dsolovianov; + +-- +-- TOC entry 270 (class 1259 OID 25621) +-- Name: enrollments; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.enrollments ( + id integer, + user_id integer, + course_id integer, + enrolled_at character varying(50) +); + + +ALTER TABLE raw.enrollments OWNER TO dsolovianov; + +-- +-- TOC entry 271 (class 1259 OID 25624) +-- Name: lesson_views; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.lesson_views ( + id integer, + user_id integer, + lesson_id integer, + viewed_at character varying(50) +); + + +ALTER TABLE raw.lesson_views OWNER TO dsolovianov; + +-- +-- TOC entry 269 (class 1259 OID 25618) +-- Name: lessons; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.lessons ( + id integer, + course_id integer, + title character varying(50), + duration_min integer +); + + +ALTER TABLE raw.lessons OWNER TO dsolovianov; + +-- +-- TOC entry 267 (class 1259 OID 25612) +-- Name: users; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.users ( + id integer, + name character varying(50), + age integer, + email character varying(50), + registration_date character varying(50) +); + + +ALTER TABLE raw.users OWNER TO dsolovianov; + +-- +-- TOC entry 2506 (class 0 OID 25635) +-- Dependencies: 273 +-- Data for Name: dim_course; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_course (course_id, title, category) FROM stdin; +2 Python для анализа данных programming +1 SQL для начинающих data +3 BI с нуля business +\. + + +-- +-- TOC entry 2508 (class 0 OID 25656) +-- Dependencies: 275 +-- Data for Name: dim_date; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_date (date, year, month, day, weekday) FROM stdin; +2023-01-15 2023 1 15 Sunday +2023-01-16 2023 1 16 Monday +2023-01-21 2023 1 21 Saturday +2023-01-20 2023 1 20 Friday +2023-03-05 2023 3 5 Sunday +2023-02-01 2023 2 1 Wednesday +2023-04-02 2023 4 2 Sunday +2023-03-06 2023 3 6 Monday +2023-04-01 2023 4 1 Saturday +2023-04-03 2023 4 3 Monday +\. + + +-- +-- TOC entry 2507 (class 0 OID 25643) +-- Dependencies: 274 +-- Data for Name: dim_lesson; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_lesson (lesson_id, title, duration_min, course_id) FROM stdin; +4 Основы BI 12 3 +5 Метрики и дашборды 18 3 +2 JOIN 15 1 +1 SELECT и FROM 10 1 +3 Pandas 20 2 +\. + + +-- +-- TOC entry 2505 (class 0 OID 25627) +-- Dependencies: 272 +-- Data for Name: dim_user; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_user (user_id, name, age, email, registration_date) FROM stdin; +3 Charlie 22 charlie@mail.com 2023-02-20 +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2509 (class 0 OID 25872) +-- Dependencies: 276 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.enrollments (user_id, course_id, enrolled_at) FROM stdin; +4 3 2023-04-01 00:00:00 +1 2 2023-02-01 00:00:00 +2 1 2023-01-20 00:00:00 +1 1 2023-01-15 00:00:00 +3 2 2023-03-05 00:00:00 +\. + + +-- +-- TOC entry 2510 (class 0 OID 25885) +-- Dependencies: 277 +-- Data for Name: fact_lesson_views; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) FROM stdin; +2 1 1 2023-01-21 09:00:00 +1 2 1 2023-01-16 10:15:00 +3 3 2 2023-03-06 12:00:00 +1 1 1 2023-01-16 10:00:00 +4 4 3 2023-04-02 10:00:00 +4 5 3 2023-04-03 10:00:00 +\. + + +-- +-- TOC entry 2501 (class 0 OID 25615) +-- Dependencies: 268 +-- Data for Name: courses; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.courses (id, title, category, created_at) FROM stdin; +3 BI с нуля business 2023-03-01 +1 SQL для начинающих data 2023-01-01 +2 Python для анализа данных programming 2023-01-15 +\. + + +-- +-- TOC entry 2503 (class 0 OID 25621) +-- Dependencies: 270 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.enrollments (id, user_id, course_id, enrolled_at) FROM stdin; +1 1 1 2023-01-15 +2 1 2 2023-02-01 +3 2 1 2023-01-20 +4 3 2 2023-03-05 +5 4 3 2023-04-01 +\. + + +-- +-- TOC entry 2504 (class 0 OID 25624) +-- Dependencies: 271 +-- Data for Name: lesson_views; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lesson_views (id, user_id, lesson_id, viewed_at) FROM stdin; +2 1 2 2023-01-16 10:15:00.000 +1 1 1 2023-01-16 10:00:00.000 +3 2 1 2023-01-21 09:00:00.000 +5 4 4 2023-04-02 10:00:00.000 +4 3 3 2023-03-06 12:00:00.000 +6 4 5 2023-04-03 10:00:00.000 +\. + + +-- +-- TOC entry 2502 (class 0 OID 25618) +-- Dependencies: 269 +-- Data for Name: lessons; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lessons (id, course_id, title, duration_min) FROM stdin; +1 1 SELECT и FROM 10 +2 1 JOIN 15 +3 2 Pandas 20 +4 3 Основы BI 12 +5 3 Метрики и дашборды 18 +\. + + +-- +-- TOC entry 2500 (class 0 OID 25612) +-- Dependencies: 267 +-- Data for Name: users; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.users (id, name, age, email, registration_date) FROM stdin; +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +3 Charlie 22 charlie@mail.com 2023-02-20 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2314 (class 2606 OID 25642) +-- Name: dim_course dim_course_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_course + ADD CONSTRAINT dim_course_pkey PRIMARY KEY (course_id); + + +-- +-- TOC entry 2319 (class 2606 OID 25663) +-- Name: dim_date dim_date_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_date + ADD CONSTRAINT dim_date_pkey PRIMARY KEY (date); + + +-- +-- TOC entry 2316 (class 2606 OID 25650) +-- Name: dim_lesson dim_lesson_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_pkey PRIMARY KEY (lesson_id); + + +-- +-- TOC entry 2312 (class 2606 OID 25634) +-- Name: dim_user dim_user_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_user + ADD CONSTRAINT dim_user_pkey PRIMARY KEY (user_id); + + +-- +-- TOC entry 2317 (class 1259 OID 25906) +-- Name: idx_dim_lesson_course_id; Type: INDEX; Schema: core; Owner: dsolovianov +-- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +/* +1) Создание INDEX на таблицы в CORE-слое +*/ + +CREATE INDEX idx_dim_lesson_course_id ON core.dim_lesson USING btree (course_id); + +CREATE INDEX idx_enrollments_course_id ON core.enrollments USING btree (course_id); + +CREATE INDEX idx_fact_views_course_id ON core.fact_lesson_views USING btree (course_id); + +CREATE INDEX idx_fact_views_course_viewed_at ON core.fact_lesson_views USING btree (course_id, viewed_at); + +CREATE INDEX idx_fact_views_lesson_id ON core.fact_lesson_views USING btree (lesson_id); + +CREATE INDEX idx_fact_views_user_id ON core.fact_lesson_views USING btree (user_id); + +CREATE INDEX idx_fact_views_user_viewed_at ON core.fact_lesson_views USING btree (user_id, viewed_at); +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2327 (class 2606 OID 25880) +-- Name: enrollments enrollments_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2328 (class 2606 OID 25875) +-- Name: enrollments enrollments_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- +-- TOC entry 2329 (class 2606 OID 25898) +-- Name: fact_lesson_views fact_lesson_views_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2330 (class 2606 OID 25893) +-- Name: fact_lesson_views fact_lesson_views_lesson_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_lesson_id_fkey FOREIGN KEY (lesson_id) REFERENCES core.dim_lesson(lesson_id); + + +-- +-- TOC entry 2331 (class 2606 OID 25888) +-- Name: fact_lesson_views fact_lesson_views_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- Completed on 2026-01-17 22:42:02 + +-- +-- PostgreSQL database dump complete +-- + From ac9a2e0906cbe1bc6f4a838ac9acd41dfb113ec9 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Sat, 17 Jan 2026 23:03:17 +0400 Subject: [PATCH 008/179] =?UTF-8?q?=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20dm.inactive=5Fusers=5Fsummary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../greenplum/dm.lesson_popularity_summary.png | Bin 0 -> 106442 bytes ...-202601172242.sql => Добавлены индексы.sql} | 0 ...lovianov-202601142129.sql => Этапы 1-3.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dsolovianov/greenplum/dm.lesson_popularity_summary.png rename dsolovianov/greenplum/{dsolovianov-202601172242.sql => Добавлены индексы.sql} (100%) rename dsolovianov/greenplum/{dsolovianov-202601142129.sql => Этапы 1-3.sql} (100%) diff --git a/dsolovianov/greenplum/dm.lesson_popularity_summary.png b/dsolovianov/greenplum/dm.lesson_popularity_summary.png new file mode 100644 index 0000000000000000000000000000000000000000..f43c67732827fa9f96e8c251e520e22c2440aebc GIT binary patch literal 106442 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 literal 0 HcmV?d00001 diff --git a/dsolovianov/greenplum/dsolovianov-202601172242.sql b/dsolovianov/greenplum/Добавлены индексы.sql similarity index 100% rename from dsolovianov/greenplum/dsolovianov-202601172242.sql rename to dsolovianov/greenplum/Добавлены индексы.sql diff --git a/dsolovianov/greenplum/dsolovianov-202601142129.sql b/dsolovianov/greenplum/Этапы 1-3.sql similarity index 100% rename from dsolovianov/greenplum/dsolovianov-202601142129.sql rename to dsolovianov/greenplum/Этапы 1-3.sql From 631cf7fe4fbbd7256d2ed75857c1e3fec778c5f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 18 Jan 2026 13:27:47 +0300 Subject: [PATCH 009/179] DML inserted rows --- aharlamcev/postgresql/DMLhomework | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 aharlamcev/postgresql/DMLhomework diff --git a/aharlamcev/postgresql/DMLhomework b/aharlamcev/postgresql/DMLhomework new file mode 100644 index 0000000..b50c436 --- /dev/null +++ b/aharlamcev/postgresql/DMLhomework @@ -0,0 +1,52 @@ +select * from users + +insert into users (id, name, email, country, created_at) +values (1, 'Alice', null, 'KZ', '2026-01-17'); + +insert into users (id, name, email, country, created_at) +values (2, 'Bob', null, 'USA', '2026-01-17'); + +insert into users (id, name, email, country, created_at) +values (3, 'Charlie', null, 'RU', '2026-01-17'); + +select * from products + +insert into products (id, name, category, price, in_stock) +values (1, 'Mi Band 7', 'smartwatch', 49, true) + +insert into products (id, name, category, price, in_stock) +values (2, 'Surface Pro', 'tablet', 1299, true) + +insert into products (id, name, category, price, in_stock) +values (3, 'Iphone 15', 'smartphone', 999, true) + +insert into products (id, name, category, price, in_stock) +values (4, 'Logitech MX Master', 'mouse', 120, true) + +insert into orders (id, user_id, order_date, status) +values (1, 1, '2026-01-17', 'in delivery'); + +insert into orders (id, user_id, order_date, status) +values (2, 1, '2026-01-17', 'shipped'); + +insert into orders (id, user_id, order_date, status) +values (3, 2, '2026-01-17', 'preparing'); + +select * from order_items + +insert into order_items (order_id, product_id, quantity, price_at_purchase) +values (1,1, 2, 49) + +insert into order_items (order_id, product_id, quantity, price_at_purchase) +values (1, 2, 1, 1299) + +insert into order_items (order_id, product_id, quantity, price_at_purchase) +values (2, 3, 1, 999) + +insert into order_items (order_id, product_id, quantity, price_at_purchase) +values (3, 4, 2, 120) + +insert into order_items (order_id, product_id, quantity, price_at_purchase) +values (3, 1, 2, 49) + +-- Сейчас уже понял, что можно после insert - values добавлять сразу в скобках несколько штук, но изначально делал так. \ No newline at end of file From 93c3846ead451c5bb60e26b917f46f891cdcd6c1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 18 Jan 2026 13:57:09 +0300 Subject: [PATCH 010/179] Base SQL homework --- aharlamcev/postgresql/BaseSQLhomework.sql | 28 +++++++++++++++++++ .../{DMLhomework => DMLhomework.sql} | 0 2 files changed, 28 insertions(+) create mode 100644 aharlamcev/postgresql/BaseSQLhomework.sql rename aharlamcev/postgresql/{DMLhomework => DMLhomework.sql} (100%) diff --git a/aharlamcev/postgresql/BaseSQLhomework.sql b/aharlamcev/postgresql/BaseSQLhomework.sql new file mode 100644 index 0000000..92666f2 --- /dev/null +++ b/aharlamcev/postgresql/BaseSQLhomework.sql @@ -0,0 +1,28 @@ +select * from users + +select id, name, country from users +order by name; + +select id, name, country, created_at from users +where country = 'KZ' +order by created_at desc; + +select id, name from users +where email is null; + +select id, name from users +where email is not null; + +update users +set email = 'alice@mail.com' +where name = 'Alice'; -- в моей таблице ни у кого нет email, добавил чтобы посмотреть. + +select id, name, UPPER(email) as email_upper from users; + +select id, name, LOWER(email) as email_lower from users +where email is not null; + +select id, name, created_at from users +where created_at > '2024-01-11' +order by created_at desc +limit 2; diff --git a/aharlamcev/postgresql/DMLhomework b/aharlamcev/postgresql/DMLhomework.sql similarity index 100% rename from aharlamcev/postgresql/DMLhomework rename to aharlamcev/postgresql/DMLhomework.sql From e3ddb8f5b22a6bf4af9aadacfe6b105c3ada7281 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 18 Jan 2026 14:30:51 +0300 Subject: [PATCH 011/179] filtration homework --- aharlamcev/postgresql/Filtrationhomework.sql | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 aharlamcev/postgresql/Filtrationhomework.sql diff --git a/aharlamcev/postgresql/Filtrationhomework.sql b/aharlamcev/postgresql/Filtrationhomework.sql new file mode 100644 index 0000000..b8d9407 --- /dev/null +++ b/aharlamcev/postgresql/Filtrationhomework.sql @@ -0,0 +1,22 @@ +select * from products + +select name, category, price, price * 1.3 as price_with_markup, price * 1.5 as price_with_vat +from products +where in_stock is true and category != 'smartwatch' and price between 100 and 1300; + +select name, price, price * 0.85 as price_with_discount +from products +where price < 100 or price > 1000; + +select id, name, category, price from products +where category in ('tablet', 'smartphone', 'mouse') and price > 100; + +select name, category, price from products +where name not ilike '%iphone%'; + +select name, category, price from products +where name ilike '%pro%'; + +select name, category, price from products +where name ilike '%mx%' +order by price desc; \ No newline at end of file From 93b3777a426c2b9b27ab985efaf46a4f0778896b Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Fri, 23 Jan 2026 00:12:06 +0400 Subject: [PATCH 012/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=20Greenplum=20(=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=D1=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsolovianov/greenplum/Script-1.sql | 105 ++++++++++++++++++ .../greenplum/dm.course_completion_rate.png | Bin 0 -> 60318 bytes .../greenplum/dm.inactive_users_summary.png | Bin 0 -> 44471 bytes 3 files changed, 105 insertions(+) create mode 100644 dsolovianov/greenplum/Script-1.sql create mode 100644 dsolovianov/greenplum/dm.course_completion_rate.png create mode 100644 dsolovianov/greenplum/dm.inactive_users_summary.png diff --git a/dsolovianov/greenplum/Script-1.sql b/dsolovianov/greenplum/Script-1.sql new file mode 100644 index 0000000..bb78b2a --- /dev/null +++ b/dsolovianov/greenplum/Script-1.sql @@ -0,0 +1,105 @@ +--Витрина dm.lesson_popularity_summary +CREATE MATERIALIZED VIEW dm.lesson_popularity_summary as +select + l.lesson_id, + l.title as lesson_title, + c.course_id, + c.title as course_title, + COUNT(f.user_id) as total_views, + COUNT(DISTINCT f.user_id) as unique_users, + MIN(f.viewed_at) as first_view, + MAX(f.viewed_at) as last_view +FROM core.fact_lesson_views f +JOIN core.dim_lesson l + ON f.lesson_id = l.lesson_id +JOIN core.dim_course c + ON l.course_id = c.course_id +GROUP BY + l.lesson_id, + l.title, + c.course_id, + c.title +order by lesson_id; + +select * from dm.lesson_popularity_summary; + +--Удалил лишний индекс +DROP INDEX core.idx_fact_views_course_id; + +-- Все индексы для CORE-таблиц +SELECT + tablename, + indexname, + indexdef +FROM pg_indexes +WHERE schemaname = 'core' + AND tablename IN ( + 'dim_course', + 'dim_date', + 'dim_lesson', + 'dim_user', + 'enrollments', + 'fact_lesson_views' + ) +ORDER by indexname, tablename; + + +--Витрина dm.inactive_users_summary +CREATE MATERIALIZED VIEW dm.inactive_users_summary as +select + du.user_id, + du.name as user_name, + du.email, + du.age, + COUNT(e.course_id) as enrollments_count, + du.registration_date +from core.dim_user du +left join core.enrollments e + on du.user_id = e.user_id +left join core.fact_lesson_views flv + on du.user_id = flv.user_id +where flv.user_id is NULL +group by + du.user_id, + du.name, + du.email, + du.age, + du.registration_date +order by du.user_id; + +select * from dm.inactive_users_summary; + +--Витрина dm.course_completion_rate +CREATE MATERIALIZED VIEW dm.course_completion_rate as +select + du.user_id, + du.name as user_name, + dc.course_id, + dc.title as course_title, + COUNT(DISTINCT dl.lesson_id) AS lessons_in_course, + COUNT(DISTINCT flv.lesson_id) AS lessons_viewed, + ROUND(COUNT(DISTINCT flv.lesson_id)::numeric / NULLIF(COUNT(DISTINCT dl.lesson_id), 0), 2) AS completion_rate +from core.enrollments e +left join core.dim_user du + on du.user_id = e.user_id +left join core.dim_course dc + on dc.course_id = e.course_id +left join core.dim_lesson dl + on dl.course_id = e.course_id +left join core.fact_lesson_views flv + on flv.user_id = e.user_id + and flv.lesson_id = dl.lesson_id +group by + du.user_id, + du.name, + dc.course_id, + dc.title +order by du.user_id; + +select * from dm.course_completion_rate order by user_id; + +--Этап 5. Проверка +SELECT * FROM dm.lesson_popularity_summary LIMIT 10; +SELECT * FROM dm.inactive_users_summary LIMIT 10; +SELECT * FROM dm.course_completion_rate LIMIT 10; +--Все витрины отображаются корректно. Скрины: "название витрины".png \ No newline at end of file diff --git a/dsolovianov/greenplum/dm.course_completion_rate.png b/dsolovianov/greenplum/dm.course_completion_rate.png new file mode 100644 index 0000000000000000000000000000000000000000..142dbefcd10168da9de6f9b24aaf169da6f13252 GIT binary patch literal 60318 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

%hWLcaxnV#3v^W4VP7XGH@ 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 literal 0 HcmV?d00001 From 5d31d01875c478fb285ec8fb839367d7bb2f4f39 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 1 Feb 2026 10:04:31 +0300 Subject: [PATCH 013/179] window function Task 1 completed --- .../postgresql/WindowFunctionHomework.sql | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 aharlamcev/postgresql/WindowFunctionHomework.sql diff --git a/aharlamcev/postgresql/WindowFunctionHomework.sql b/aharlamcev/postgresql/WindowFunctionHomework.sql new file mode 100644 index 0000000..72dd993 --- /dev/null +++ b/aharlamcev/postgresql/WindowFunctionHomework.sql @@ -0,0 +1,39 @@ +create table products_extended ( +id INT primary key, +name VARCHAR(50) not null, +category VARCHAR(50) not null, +price INT, +in_stock BOOLEAN default true, +email_support VARCHAR(50) +); + +insert into products_extended (id, name, category, price, in_stock, email_support) +values (1, 'iPhone 14', 'smartphone', 799, true, 'iphone@apple.com'), +(2, 'MacBook Air', 'laptop', 1199, false, NULL), +(3, 'Galaxy S22', 'smartphone', 699, true, 'galaxy@support.com'), +(4, 'ThinkPad X1', 'laptop', 1399, true, 'thinkpad@lenovo.com'), +(5, 'iPad Pro', 'tablet', 999, false, 'ipad@apple.com'), +(6, 'Kindle Paperwhite', 'ebook_reader', 149, true, NULL), +(7, 'Mi Band 7', 'smartwatch', 49, true, 'miband@xiaomi.com'), +(8, 'Surface Pro', 'tablet', 1299, true, 'surface@microsoft.com'), +(9, 'Honor Pad X9', 'tablet', 219, true, 'honor@support.com'), +(10, 'Dell XPS 13', 'laptop', 1099, false, 'dell@support.com'); + +select * from products_extended + +with email_false as (select * from +( +select category, name, price, email_support, row_number() over (partition by category order by price desc) as rn +from products_extended where email_support is null) +), + +email_true as (select * from +( +select category, name, price, email_support, row_number() over (partition by category order by price desc) as rn +from products_extended where email_support is not null) where rn = 1 +) + +select category, name, price, email_support from email_false +union all +select category, name, price, email_support from email_true +order by category, price desc From 63f391d689884e46364143b8ec220a659efa03d1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 1 Feb 2026 13:17:20 +0300 Subject: [PATCH 014/179] fixed --- aharlamcev/postgresql/WindowFunctionHomework.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aharlamcev/postgresql/WindowFunctionHomework.sql b/aharlamcev/postgresql/WindowFunctionHomework.sql index 72dd993..bd81ac9 100644 --- a/aharlamcev/postgresql/WindowFunctionHomework.sql +++ b/aharlamcev/postgresql/WindowFunctionHomework.sql @@ -19,12 +19,15 @@ values (1, 'iPhone 14', 'smartphone', 799, true, 'iphone@apple.com'), (9, 'Honor Pad X9', 'tablet', 219, true, 'honor@support.com'), (10, 'Dell XPS 13', 'laptop', 1099, false, 'dell@support.com'); +alter table products_extended +alter column price type DECIMAL(10, 2) + select * from products_extended with email_false as (select * from ( select category, name, price, email_support, row_number() over (partition by category order by price desc) as rn -from products_extended where email_support is null) +from products_extended where email_support is null) where rn = 1 ), email_true as (select * from From b7c7dcf012aee9db4c8db0e22d656366046b8611 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Feb 2026 13:44:24 +0300 Subject: [PATCH 015/179] WindowFuncs Homework done --- .../postgresql/WindowFunctionHomework.sql | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/aharlamcev/postgresql/WindowFunctionHomework.sql b/aharlamcev/postgresql/WindowFunctionHomework.sql index bd81ac9..0e16bf1 100644 --- a/aharlamcev/postgresql/WindowFunctionHomework.sql +++ b/aharlamcev/postgresql/WindowFunctionHomework.sql @@ -22,21 +22,51 @@ values (1, 'iPhone 14', 'smartphone', 799, true, 'iphone@apple.com'), alter table products_extended alter column price type DECIMAL(10, 2) -select * from products_extended - +------------------ Задание №1 (По нему ты говорил, что нечитаемо, объясни как переделать pls <3) with email_false as (select * from ( -select category, name, price, email_support, row_number() over (partition by category order by price desc) as rn -from products_extended where email_support is null) where rn = 1 +select category, name, price, email_support, +row_number() over (partition by category order by price desc) as rn +from products_extended +where email_support is null) +where rn = 1 ), email_true as (select * from ( -select category, name, price, email_support, row_number() over (partition by category order by price desc) as rn -from products_extended where email_support is not null) where rn = 1 +select category, name, price, email_support, +row_number() over (partition by category order by price desc) as rn +from products_extended +where email_support is not null) +where rn = 1 ) select category, name, price, email_support from email_false union all select category, name, price, email_support from email_true order by category, price desc + +---------------------- Задание №2 (Переделал с двух оконок в одном запросе на СТЕ с одной оконкой внутри) +with get_data as ( +select name, category, price, +LEAD(price) OVER (PARTITION BY category order by price) AS next_price +from products_extended +) + +select name, category, price, next_price, +price - next_price AS diff +from get_data + +--------------------- Задание №3 +select category, name, price, +SUM(price) over (partition by category order by price) as cumulative_sales +from products_extended + +---------------------- Задание №4 +with get_data as ( +select category, name, price, AVG(price) over (partition by category) as avg_price +from products_extended +) +select category, name, price, avg_price, +case when price > avg_price then 'true' else 'false' end as is_above_avg +from get_data From 311f6b259beae97829e4ca6084a580e986dcf82c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Feb 2026 14:19:52 +0300 Subject: [PATCH 016/179] Pet project DDL&DML done --- aharlamcev/postgresql/Pet-projectsql.sql | 202 +++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 aharlamcev/postgresql/Pet-projectsql.sql diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql new file mode 100644 index 0000000..8b1d51f --- /dev/null +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -0,0 +1,202 @@ +create table users ( +id INT primary key, +name VARCHAR(50), +age INT, +email VARCHAR(50), +registration_date date +); + +INSERT INTO users (id, name, age, email, registration_date) +VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-10'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'), +(4, 'Diana', 28, 'diana@mail.com', '2023-03-01'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-05'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-12'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-20'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-10'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-15'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-20'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-15'), +(13, 'Mia', 26, 'mia@gmail.com', '2023-01-18'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-08'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-10'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-15'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-20'), +(18, 'Rita', 27, 'rita@mail.com', '2023-04-01'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-10'), +(20, 'Tina', 25, 'tina@mail.com', '2023-04-18'); + +create table courses ( +id INT primary key, +title VARCHAR(100), +category VARCHAR(50), +created_at date +); + +INSERT INTO courses (id, title, category, created_at) +VALUES +(1, 'SQL для начинающих', 'data', '2023-01-01'), +(2, 'Python для анализа данных', 'programming', '2023-01-15'), +(3, 'BI с нуля', 'business', '2023-03-01'), +(4, 'Excel для аналитиков', 'business', '2023-02-01'), +(5, 'Основы машинного обучения', 'data', '2023-03-15'); + + +create table lessons ( +id INT primary key, +course_id INT, +title VARCHAR(100), +duration_min INT, +foreign key (course_id) references courses(id) +); + +INSERT INTO lessons (id, course_id, title, duration_min) +VALUES +(1, 1, 'SELECT и FROM', 10), +(2, 1, 'JOIN', 15), +(3, 1, 'WHERE и фильтрация', 12), +(4, 1, 'Агрегации и GROUP BY', 18), +(5, 1, 'Подзапросы', 25), +(6, 2, 'Pandas основы', 20), +(7, 2, 'NumPy', 15), +(8, 2, 'Визуализация данных', 22), +(9, 3, 'Основы BI', 12), +(10, 3, 'Метрики и дашборды', 18), +(11, 3, 'Power BI введение', 20), +(12, 4, 'Формулы в Excel', 15), +(13, 4, 'Сводные таблицы', 18), +(14, 4, 'Графики и диаграммы', 14), +(15, 5, 'Введение в ML', 20), +(16, 5, 'Линейная регрессия', 30), +(17, 5, 'Классификация', 28), +(18, 5, 'Кластеризация', 25); + +create table enrollments ( +id INT primary key, +user_id INT, +course_id INT, +enrolled_at date, +foreign key (user_id) references users(id), +foreign key (course_id) references courses(id) +); + +INSERT INTO enrollments (id, user_id, course_id, enrolled_at) +VALUES +(1, 1, 1, '2023-01-15'), +(2, 1, 2, '2023-02-01'), +(3, 2, 1, '2023-01-20'), +(4, 3, 2, '2023-03-05'), +(5, 4, 3, '2023-04-01'), +(6, 6, 1, '2023-01-06'), +(7, 6, 2, '2023-01-10'), +(8, 6, 4, '2023-02-05'), +(9, 7, 1, '2023-01-13'), +(10, 7, 3, '2023-03-10'), +(11, 8, 1, '2023-01-22'), +(12, 8, 2, '2023-01-22'), +(13, 8, 3, '2023-01-22'), +(14, 9, 4, '2023-02-12'), +(15, 10, 1, '2023-04-16'), +(16, 11, 2, '2023-04-21'), +(17, 12, 1, '2023-01-16'), +(18, 13, 2, '2023-01-19'), +(19, 13, 4, '2023-02-20'), +(20, 14, 1, '2023-01-09'), +(21, 14, 2, '2023-01-09'), +(22, 14, 3, '2023-01-09'), +(23, 14, 4, '2023-02-01'), +(24, 14, 5, '2023-03-20'), +(25, 15, 1, '2023-01-11'), +(26, 15, 3, '2023-03-05'), +(27, 16, 2, '2023-02-16'), +(28, 16, 4, '2023-02-16'), +(29, 17, 3, '2023-03-22'), +(30, 17, 5, '2023-03-25'), +(31, 18, 1, '2023-04-02'), +(32, 19, 1, '2023-04-11'), +(33, 19, 2, '2023-04-11'), +(34, 20, 2, '2023-04-19'), +(35, 5, 5, '2023-03-20'), +(36, 1, 4, '2023-02-10'), +(37, 2, 3, '2023-03-15'), +(38, 3, 1, '2023-02-25'), +(39, 4, 2, '2023-03-10'), +(40, 6, 3, '2023-03-08'), +(41, 7, 2, '2023-02-01'), +(42, 9, 1, '2023-02-15'), +(43, 12, 3, '2023-03-10'), +(44, 15, 2, '2023-02-01'), +(45, 18, 4, '2023-04-05'); + +create table lesson_views ( +id INT primary key, +user_id INT, +lesson_id INT, +viewed_at timestamp, +foreign key (user_id) references users(id), +foreign key (lesson_id) references lessons(id) +); + +INSERT INTO lesson_views (id, user_id, lesson_id, viewed_at) +VALUES +(1, 1, 1, '2023-01-16 10:00:00'), +(2, 1, 2, '2023-01-16 10:15:00'), +(3, 1, 3, '2023-01-17 09:00:00'), +(4, 1, 4, '2023-01-17 09:30:00'), +(5, 1, 4, '2023-01-17 11:00:00'), +(6, 1, 4, '2023-01-18 10:00:00'), +(7, 1, 6, '2023-02-02 14:00:00'), +(8, 1, 12, '2023-02-15 10:00:00'), +(9, 1, 13, '2023-02-15 10:30:00'), +(10, 2, 1, '2023-01-21 09:00:00'), +(11, 2, 9, '2023-03-16 10:00:00'), +(12, 2, 10, '2023-03-16 10:30:00'), +(13, 3, 6, '2023-03-06 12:00:00'), +(14, 3, 1, '2023-02-26 11:00:00'), +(15, 3, 2, '2023-02-26 11:20:00'), +(16, 4, 9, '2023-04-02 10:00:00'), +(17, 4, 10, '2023-04-03 10:00:00'), +(18, 4, 6, '2023-03-12 14:00:00'), +(19, 4, 7, '2023-03-13 09:00:00'), +(20, 5, 15, '2023-03-21 10:00:00'), +(21, 5, 16, '2023-03-22 11:00:00'), +(22, 6, 1, '2023-01-07 10:00:00'), +(23, 6, 2, '2023-01-07 10:15:00'), +(24, 6, 3, '2023-01-08 09:00:00'), +(25, 6, 4, '2023-01-08 09:30:00'), +(26, 6, 5, '2023-01-09 10:00:00'), +(27, 6, 6, '2023-01-11 14:00:00'), +(28, 6, 7, '2023-01-12 10:00:00'), +(29, 6, 8, '2023-01-13 11:00:00'), +(30, 6, 12, '2023-02-06 09:00:00'), +(31, 6, 13, '2023-02-06 09:30:00'), +(32, 6, 14, '2023-02-07 10:00:00'), +(33, 6, 9, '2023-03-09 11:00:00'), +(34, 7, 1, '2023-01-14 10:00:00'), +(35, 7, 2, '2023-01-14 10:20:00'), +(36, 7, 3, '2023-01-15 09:00:00'), +(37, 7, 9, '2023-03-11 14:00:00'), +(38, 7, 10, '2023-03-11 14:30:00'), +(39, 9, 12, '2023-02-13 10:00:00'), +(40, 9, 13, '2023-02-13 10:30:00'), +(41, 9, 1, '2023-02-16 11:00:00'), +(42, 12, 1, '2023-01-17 10:00:00'), +(43, 12, 2, '2023-01-17 10:20:00'), +(44, 12, 1, '2023-04-10 11:00:00'), +(45, 12, 3, '2023-04-10 11:30:00'), +(46, 12, 9, '2023-03-12 10:00:00'), +(47, 13, 6, '2023-01-20 14:00:00'), +(48, 13, 7, '2023-01-21 10:00:00'), +(49, 13, 12, '2023-02-21 09:00:00'), +(50, 14, 1, '2023-01-09 08:00:00'), +(51, 14, 2, '2023-01-09 08:15:00'), +(52, 14, 3, '2023-01-10 10:00:00'), +(53, 14, 4, '2023-01-10 10:30:00'), +(54, 14, 5, '2023-01-11 09:00:00'), +(55, 14, 6, '2023-01-10 14:00:00'), +(56, 14, 7, '2023-01-11 10:00:00'), +(57, 14, 8, '2023-01-12 11:00:00'), +(58, 14, 9, '2023-01-10 08:00:00'); From 0bdc2ef4523263e9e3183464782ec6e6a29057c0 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 8 Feb 2026 12:55:07 +0300 Subject: [PATCH 017/179] =?UTF-8?q?fixed=20task=E2=84=964=20boolean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aharlamcev/postgresql/WindowFunctionHomework.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aharlamcev/postgresql/WindowFunctionHomework.sql b/aharlamcev/postgresql/WindowFunctionHomework.sql index 0e16bf1..2cf8068 100644 --- a/aharlamcev/postgresql/WindowFunctionHomework.sql +++ b/aharlamcev/postgresql/WindowFunctionHomework.sql @@ -68,5 +68,5 @@ select category, name, price, AVG(price) over (partition by category) as avg_pri from products_extended ) select category, name, price, avg_price, -case when price > avg_price then 'true' else 'false' end as is_above_avg +case when price > avg_price then true else false end as is_above_avg from get_data From d00110c7ae5afe08915f7aff8be0d6fc7b943ee5 Mon Sep 17 00:00:00 2001 From: pavel Date: Wed, 11 Feb 2026 18:54:44 +0100 Subject: [PATCH 018/179] ddl, dml tasks done --- pmikhailov/postgresql/.project | 11 +++++ pmikhailov/postgresql/DDL-DML.sql | 80 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 pmikhailov/postgresql/.project create mode 100644 pmikhailov/postgresql/DDL-DML.sql diff --git a/pmikhailov/postgresql/.project b/pmikhailov/postgresql/.project new file mode 100644 index 0000000..92f941b --- /dev/null +++ b/pmikhailov/postgresql/.project @@ -0,0 +1,11 @@ + + + postgresql + + + + + + + + diff --git a/pmikhailov/postgresql/DDL-DML.sql b/pmikhailov/postgresql/DDL-DML.sql new file mode 100644 index 0000000..afeb254 --- /dev/null +++ b/pmikhailov/postgresql/DDL-DML.sql @@ -0,0 +1,80 @@ +CREATE TABLE users ( + id int PRIMARY KEY, -- NOT NULL + "name" varchar(100) NOT NULL, + age int, + email varchar(100), + country varchar(2), + is_active boolean, + created_at timestamptz NOT NULL +); + +CREATE TABLE products ( + id int PRIMARY KEY, + "name" varchar(100) NOT NULL, + category varchar(50), + price decimal(10,2) NOT NULL, + in_stock BOOLEAN -- или int, потому что или на складе или не на складе +); + +CREATE TABLE orders ( + id int PRIMARY KEY, + user_id int, + order_date timestamptz NOT NULL, + status varchar(20) +); + +CREATE TABLE order_items ( + order_id int, + product_id int, + quantity int NOT NULL, + price_at_purchase decimal(10,2) NOT NULL, + PRIMARY KEY (order_id, product_id) +); + +INSERT INTO pmikhailov.users +(id, "name", age, email, country, is_active, created_at) VALUES +(1, 'Alice', 25, 'alice@example.com', 'US', TRUE, '2024-01-10 10:00:00'), +(2, 'Bob', 30, 'bob@example.com', 'UK', TRUE, '2024-02-05 09:30:00'), +(3, 'Carol', 27, 'carol@example.com', 'US', FALSE, '2024-03-01 15:20:00'), +(4, 'Diana', 22, 'diana@gmail.com', 'US', TRUE, '2024-01-12 08:15:00'), +(5, 'Evan', 35, 'evan@example.com', 'UK', TRUE, '2024-01-18 19:40:00'), +(6, 'Farida', 29, 'farida@example.com', 'KZ', TRUE, '2024-02-20 11:05:00'), +(7, 'George', 31, 'george+test@example.com', 'US', TRUE, '2024-03-05 16:45:00'), +(8, 'Helen', 24, NULL, 'UK', FALSE, '2024-03-06 08:20:00'), +(9, 'Ivan', 26, 'ivan@example.com', 'RU', TRUE, '2024-01-11 14:30:00'), +(10, 'Julia', 28, 'julia@example.com', 'US', TRUE, '2024-03-07 18:05:00'); + + +INSERT INTO pmikhailov.products +(id, "name", category, price, in_stock) VALUES +(1, 'iPhone 15', 'Электроника', 80000, TRUE), +(2, 'Kindle', 'Электроника', 15000, TRUE), +(3, 'Кроссовки Nike', 'Одежда', 9000, TRUE), +(4, 'Кроссовки NoName', 'Одежда', 3000, FALSE), +(5, 'Наушники Pro', 'Электроника', 12000, TRUE), +(6, 'Фитнес-браслет', 'Гаджеты', 6000, TRUE), +(7, 'Подарочная карта', 'Сервисы', 1000, TRUE); + +INSERT INTO pmikhailov.orders +(id, user_id, order_date, status) VALUES +(1, 1, '2025-01-10 12:00:00', 'paid'), +(2, 3, '2025-01-12 09:30:00', 'cancelled'), +(3, 1, '2025-01-15 18:20:00', 'paid'), +(4, 2, '2025-01-20 11:05:00', 'created'), +(5, 4, '2025-02-01 17:45:00', 'paid'), +(6, 5, '2025-02-10 10:10:00', 'refunded'), +(7, 7, '2025-03-01 09:00:00', 'paid'), +(8, 9, '2025-03-05 19:30:00', 'paid'); + +INSERT INTO pmikhailov.order_items +(order_id, product_id, quantity, price_at_purchase) VALUES +(1, 1, 1, 78000), +(1, 3, 2, 8500), +(2, 2, 1, 14000), +(3, 5, 1, 11000), +(3, 7, 1, 1000), +(4, 2, 1, 15000), +(5, 6, 1, 6000), +(6, 1, 1, 82000), +(7, 3, 1, 9000), +(8, 4, 2, 2800); From 35283077b6fad1f1f5981cb32b9a9da33d151e9f Mon Sep 17 00:00:00 2001 From: pavel Date: Wed, 11 Feb 2026 23:57:08 +0100 Subject: [PATCH 019/179] base sql done --- .../postgresql/{DDL-DML.sql => base_sql.sql} | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) rename pmikhailov/postgresql/{DDL-DML.sql => base_sql.sql} (61%) diff --git a/pmikhailov/postgresql/DDL-DML.sql b/pmikhailov/postgresql/base_sql.sql similarity index 61% rename from pmikhailov/postgresql/DDL-DML.sql rename to pmikhailov/postgresql/base_sql.sql index afeb254..743a24a 100644 --- a/pmikhailov/postgresql/DDL-DML.sql +++ b/pmikhailov/postgresql/base_sql.sql @@ -1,6 +1,6 @@ CREATE TABLE users ( id int PRIMARY KEY, -- NOT NULL - "name" varchar(100) NOT NULL, + name varchar(100) NOT NULL, age int, email varchar(100), country varchar(2), @@ -10,7 +10,7 @@ CREATE TABLE users ( CREATE TABLE products ( id int PRIMARY KEY, - "name" varchar(100) NOT NULL, + name varchar(100) NOT NULL, category varchar(50), price decimal(10,2) NOT NULL, in_stock BOOLEAN -- или int, потому что или на складе или не на складе @@ -78,3 +78,111 @@ INSERT INTO pmikhailov.order_items (6, 1, 1, 82000), (7, 3, 1, 9000), (8, 4, 2, 2800); + +SELECT id, name, country, created_at, email +FROM users +ORDER BY created_at DESC, name ASC; + +SELECT id, name, country +FROM users +WHERE country = 'US' +ORDER BY name ASC; + +SELECT id, name, country +FROM users +WHERE country = 'UK' +ORDER BY name ASC; + +SELECT id, name, email +FROM users u +WHERE email IS NOT NULL +ORDER BY name ASC; + +SELECT DISTINCT lower(email) +FROM users u +WHERE email IS NOT null; + +SELECT id, name, created_at +FROM users +WHERE created_at >= '2024-02-05 09:30:00.000 +0100' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; + + +/* Задача 1 */ + +SELECT + name, + category, + price, + price * 1.3 AS price_with_markup, + price * 1.5 AS price_with_vat +FROM + products p +WHERE + in_stock IS TRUE + AND price BETWEEN 100 AND 1300; + + +/* Задача 2 */ + +SELECT + name, + price, + price * 0.85 AS price_with_discount +FROM + products p +WHERE + (price <= 100) OR (price >= 1000); + + +/* Задача 3 */ + +SELECT + id, + name, + category, + price +FROM + products p +WHERE + category IN ('tablet', 'smartphone', 'mouse') + AND price >= 100; + + +/* Задача 4 */ + +SELECT + name, + category, + price +FROM + products p +WHERE name NOT ILIKE '%iPhone%'; + + +/* Задача 5 */ + +SELECT + name, + category, + price +FROM + products p +WHERE + name NOT ILIKE '%pro%' +ORDER BY + price DESC; + +SELECT + name, + category, + price +FROM + products p +WHERE + name NOT ILIKE '%mx%'; +ORDER BY + price DESC; + + From d7f84de43f9e799ac36594d6956e1cf09c45049a Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 12 Feb 2026 20:35:06 +0400 Subject: [PATCH 020/179] =?UTF-8?q?Clickhouse=20-=20=D0=A2=D0=B8=D0=BF?= =?UTF-8?q?=D1=8B=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=20-=20=D0=94?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=88=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsolovianov/clickhouse/image.png | Bin 0 -> 179648 bytes .../clickhouse/Типы таблиц - Домашки.sql | 72 ++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 dsolovianov/clickhouse/image.png create mode 100644 dsolovianov/clickhouse/Типы таблиц - Домашки.sql diff --git a/dsolovianov/clickhouse/image.png b/dsolovianov/clickhouse/image.png new file mode 100644 index 0000000000000000000000000000000000000000..76386007578a1f079eb47946ae6ac087d69141fd GIT binary patch literal 179648 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 literal 0 HcmV?d00001 diff --git a/dsolovianov/clickhouse/Типы таблиц - Домашки.sql b/dsolovianov/clickhouse/Типы таблиц - Домашки.sql new file mode 100644 index 0000000..04cda20 --- /dev/null +++ b/dsolovianov/clickhouse/Типы таблиц - Домашки.sql @@ -0,0 +1,72 @@ +--Задача 1: Создай таблицу page_views + +CREATE TABLE page_views ( + user_id UInt64, + url String, + view_time DateTime + ) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + +--Задача 2: Создай таблицу users + +CREATE TABLE users ( +id UInt64, +name String, +updated_at DateTime + ) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +-- Задача 3: Создай таблицу region_sales + +CREATE TABLE region_sales ( +region String, +date Date, +sales UInt64 + ) +ENGINE = SummingMergeTree() +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +--Задача 4.1: Создай таблицу user_metrics + +CREATE TABLE user_metrics ( +region String, +avg_age_state AggregateFunction(avg, UInt8), +active_users_state AggregateFunction(uniq, UInt64) + ) +ENGINE = AggregatingMergeTree() +ORDER BY region; + +--Задача 4.3: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` + +INSERT INTO user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(toUInt64(user_id)) AS active_users_state +FROM +( + SELECT 'Samara' AS region, 25 AS age, 1 AS user_id + UNION ALL + SELECT 'Moscow', 30, 2 + UNION ALL + SELECT 'Moscow', 22, 3 + UNION ALL + SELECT 'Samara', 22, 4 + UNION ALL + SELECT 'Sochi', 34, 5 +) +GROUP BY region; + +--Задача 4.4: - Читай их через `avgMerge()` и `uniqMerge()` + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY region; + From 1cf0709bbcbd8ba2e00f5620a23443e0bf9f797f Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 12 Feb 2026 21:48:35 +0100 Subject: [PATCH 021/179] join tasks done --- pmikhailov/postgresql/base_sql.sql | 188 ----------------------------- pmikhailov/postgresql/joins.sql | 158 ++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 188 deletions(-) delete mode 100644 pmikhailov/postgresql/base_sql.sql create mode 100644 pmikhailov/postgresql/joins.sql diff --git a/pmikhailov/postgresql/base_sql.sql b/pmikhailov/postgresql/base_sql.sql deleted file mode 100644 index 743a24a..0000000 --- a/pmikhailov/postgresql/base_sql.sql +++ /dev/null @@ -1,188 +0,0 @@ -CREATE TABLE users ( - id int PRIMARY KEY, -- NOT NULL - name varchar(100) NOT NULL, - age int, - email varchar(100), - country varchar(2), - is_active boolean, - created_at timestamptz NOT NULL -); - -CREATE TABLE products ( - id int PRIMARY KEY, - name varchar(100) NOT NULL, - category varchar(50), - price decimal(10,2) NOT NULL, - in_stock BOOLEAN -- или int, потому что или на складе или не на складе -); - -CREATE TABLE orders ( - id int PRIMARY KEY, - user_id int, - order_date timestamptz NOT NULL, - status varchar(20) -); - -CREATE TABLE order_items ( - order_id int, - product_id int, - quantity int NOT NULL, - price_at_purchase decimal(10,2) NOT NULL, - PRIMARY KEY (order_id, product_id) -); - -INSERT INTO pmikhailov.users -(id, "name", age, email, country, is_active, created_at) VALUES -(1, 'Alice', 25, 'alice@example.com', 'US', TRUE, '2024-01-10 10:00:00'), -(2, 'Bob', 30, 'bob@example.com', 'UK', TRUE, '2024-02-05 09:30:00'), -(3, 'Carol', 27, 'carol@example.com', 'US', FALSE, '2024-03-01 15:20:00'), -(4, 'Diana', 22, 'diana@gmail.com', 'US', TRUE, '2024-01-12 08:15:00'), -(5, 'Evan', 35, 'evan@example.com', 'UK', TRUE, '2024-01-18 19:40:00'), -(6, 'Farida', 29, 'farida@example.com', 'KZ', TRUE, '2024-02-20 11:05:00'), -(7, 'George', 31, 'george+test@example.com', 'US', TRUE, '2024-03-05 16:45:00'), -(8, 'Helen', 24, NULL, 'UK', FALSE, '2024-03-06 08:20:00'), -(9, 'Ivan', 26, 'ivan@example.com', 'RU', TRUE, '2024-01-11 14:30:00'), -(10, 'Julia', 28, 'julia@example.com', 'US', TRUE, '2024-03-07 18:05:00'); - - -INSERT INTO pmikhailov.products -(id, "name", category, price, in_stock) VALUES -(1, 'iPhone 15', 'Электроника', 80000, TRUE), -(2, 'Kindle', 'Электроника', 15000, TRUE), -(3, 'Кроссовки Nike', 'Одежда', 9000, TRUE), -(4, 'Кроссовки NoName', 'Одежда', 3000, FALSE), -(5, 'Наушники Pro', 'Электроника', 12000, TRUE), -(6, 'Фитнес-браслет', 'Гаджеты', 6000, TRUE), -(7, 'Подарочная карта', 'Сервисы', 1000, TRUE); - -INSERT INTO pmikhailov.orders -(id, user_id, order_date, status) VALUES -(1, 1, '2025-01-10 12:00:00', 'paid'), -(2, 3, '2025-01-12 09:30:00', 'cancelled'), -(3, 1, '2025-01-15 18:20:00', 'paid'), -(4, 2, '2025-01-20 11:05:00', 'created'), -(5, 4, '2025-02-01 17:45:00', 'paid'), -(6, 5, '2025-02-10 10:10:00', 'refunded'), -(7, 7, '2025-03-01 09:00:00', 'paid'), -(8, 9, '2025-03-05 19:30:00', 'paid'); - -INSERT INTO pmikhailov.order_items -(order_id, product_id, quantity, price_at_purchase) VALUES -(1, 1, 1, 78000), -(1, 3, 2, 8500), -(2, 2, 1, 14000), -(3, 5, 1, 11000), -(3, 7, 1, 1000), -(4, 2, 1, 15000), -(5, 6, 1, 6000), -(6, 1, 1, 82000), -(7, 3, 1, 9000), -(8, 4, 2, 2800); - -SELECT id, name, country, created_at, email -FROM users -ORDER BY created_at DESC, name ASC; - -SELECT id, name, country -FROM users -WHERE country = 'US' -ORDER BY name ASC; - -SELECT id, name, country -FROM users -WHERE country = 'UK' -ORDER BY name ASC; - -SELECT id, name, email -FROM users u -WHERE email IS NOT NULL -ORDER BY name ASC; - -SELECT DISTINCT lower(email) -FROM users u -WHERE email IS NOT null; - -SELECT id, name, created_at -FROM users -WHERE created_at >= '2024-02-05 09:30:00.000 +0100' -ORDER BY created_at DESC -LIMIT 2 OFFSET 1; - - -/* Задача 1 */ - -SELECT - name, - category, - price, - price * 1.3 AS price_with_markup, - price * 1.5 AS price_with_vat -FROM - products p -WHERE - in_stock IS TRUE - AND price BETWEEN 100 AND 1300; - - -/* Задача 2 */ - -SELECT - name, - price, - price * 0.85 AS price_with_discount -FROM - products p -WHERE - (price <= 100) OR (price >= 1000); - - -/* Задача 3 */ - -SELECT - id, - name, - category, - price -FROM - products p -WHERE - category IN ('tablet', 'smartphone', 'mouse') - AND price >= 100; - - -/* Задача 4 */ - -SELECT - name, - category, - price -FROM - products p -WHERE name NOT ILIKE '%iPhone%'; - - -/* Задача 5 */ - -SELECT - name, - category, - price -FROM - products p -WHERE - name NOT ILIKE '%pro%' -ORDER BY - price DESC; - -SELECT - name, - category, - price -FROM - products p -WHERE - name NOT ILIKE '%mx%'; -ORDER BY - price DESC; - - diff --git a/pmikhailov/postgresql/joins.sql b/pmikhailov/postgresql/joins.sql new file mode 100644 index 0000000..7a90e1b --- /dev/null +++ b/pmikhailov/postgresql/joins.sql @@ -0,0 +1,158 @@ +/* Задача 1 + * Джойним в таком порядке т.к. не каждый юзер делал заказ + * + судя по порядку колонок мы хотим анализировать заказы + * (по сути выглядит так же как INNER) + * Но в обратном порядке можем увидеть кто не делал заказ + * и добавить их в условную рассылку + */ +SELECT + o.id AS order_id, + o.order_date, + status, + u."name" AS user_name, + country +FROM + orders o +LEFT JOIN + users u ON o.user_id = u.id + + +/* Задача 2 */ + +SELECT + order_id, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + order_items oi +LEFT JOIN + products p ON oi.product_id = p.id -- AND oi.order_id = 1 из правой таблицы будут NULL там, где оба условия не выполнены +WHERE order_id = 1 -- в задании другой номер - 101, надо исправить в Notion, данные на п.4 уже другие + + +/* Задача 3 */ + +SELECT + order_id, + u."name" AS user_name, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + users u ON o.id = u.id +INNER JOIN + products p ON oi.product_id = p.id; + + +/* Задача 4 */ + +SELECT + u."name" AS user_name, + o.id AS order_id, + status +FROM + users u +LEFT JOIN + orders o ON u.id = o.user_id + + +/* Задача 5 */ + +SELECT + o.user_id, + u."name" AS user_name +FROM + users u +LEFT JOIN + orders o ON u.id = o.id +WHERE o.user_id IS NOT NULL + + +/* Задача 6 */ + +SELECT + o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + quantity +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + products p ON oi.product_id = p.id +INNER JOIN + users u ON o.user_id = u.id +WHERE p."name" ILIKE '%Mi Band 7%'; + + +/* Задача 7 */ + +SELECT + p.name AS product_name, + order_id, + quantity +FROM + products p +FULL JOIN + order_items oi ON p.id = oi.product_id +ORDER BY product_name; + + +/* Задача 8 */ + +SELECT + o.id, + u."name" AS user_name, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + users u ON o.user_id = u.id +LEFT JOIN + products p ON oi.product_id = p.id +WHERE + o.status = 'paid'; + + +/* Задача 9 */ + +SELECT + o.id AS order_id, + o.order_date, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + products p ON oi.product_id = p.id +INNER JOIN + users u ON o.user_id = u.id +WHERE u."name" ILIKE '%alice%'; + + +/* Задача 10 +* Задача противоречит изначальным органичениям NOT NULL +*/ + +SELECT + o.id AS order_id, + user_id, + status +FROM + orders o +FULL JOIN + order_items oi ON oi.order_id = o.id +WHERE product_id IS NULL AND o.status = 'created'; \ No newline at end of file From df8159a2872ec46afea9ace160bff0268d9a0c66 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Feb 2026 16:10:48 +0300 Subject: [PATCH 022/179] deleted old homework --- aharlamcev/postgresql/BaseSQLhomework.sql | 28 -------- aharlamcev/postgresql/DMLhomework.sql | 52 -------------- aharlamcev/postgresql/Filtrationhomework.sql | 22 ------ .../postgresql/WindowFunctionHomework.sql | 72 ------------------- aharlamcev/postgresql/homeworkDDL.sql | 35 --------- aharlamcev/postgresql/sqlhomework.sql | 23 ------ 6 files changed, 232 deletions(-) delete mode 100644 aharlamcev/postgresql/BaseSQLhomework.sql delete mode 100644 aharlamcev/postgresql/DMLhomework.sql delete mode 100644 aharlamcev/postgresql/Filtrationhomework.sql delete mode 100644 aharlamcev/postgresql/WindowFunctionHomework.sql delete mode 100644 aharlamcev/postgresql/homeworkDDL.sql delete mode 100644 aharlamcev/postgresql/sqlhomework.sql diff --git a/aharlamcev/postgresql/BaseSQLhomework.sql b/aharlamcev/postgresql/BaseSQLhomework.sql deleted file mode 100644 index 92666f2..0000000 --- a/aharlamcev/postgresql/BaseSQLhomework.sql +++ /dev/null @@ -1,28 +0,0 @@ -select * from users - -select id, name, country from users -order by name; - -select id, name, country, created_at from users -where country = 'KZ' -order by created_at desc; - -select id, name from users -where email is null; - -select id, name from users -where email is not null; - -update users -set email = 'alice@mail.com' -where name = 'Alice'; -- в моей таблице ни у кого нет email, добавил чтобы посмотреть. - -select id, name, UPPER(email) as email_upper from users; - -select id, name, LOWER(email) as email_lower from users -where email is not null; - -select id, name, created_at from users -where created_at > '2024-01-11' -order by created_at desc -limit 2; diff --git a/aharlamcev/postgresql/DMLhomework.sql b/aharlamcev/postgresql/DMLhomework.sql deleted file mode 100644 index b50c436..0000000 --- a/aharlamcev/postgresql/DMLhomework.sql +++ /dev/null @@ -1,52 +0,0 @@ -select * from users - -insert into users (id, name, email, country, created_at) -values (1, 'Alice', null, 'KZ', '2026-01-17'); - -insert into users (id, name, email, country, created_at) -values (2, 'Bob', null, 'USA', '2026-01-17'); - -insert into users (id, name, email, country, created_at) -values (3, 'Charlie', null, 'RU', '2026-01-17'); - -select * from products - -insert into products (id, name, category, price, in_stock) -values (1, 'Mi Band 7', 'smartwatch', 49, true) - -insert into products (id, name, category, price, in_stock) -values (2, 'Surface Pro', 'tablet', 1299, true) - -insert into products (id, name, category, price, in_stock) -values (3, 'Iphone 15', 'smartphone', 999, true) - -insert into products (id, name, category, price, in_stock) -values (4, 'Logitech MX Master', 'mouse', 120, true) - -insert into orders (id, user_id, order_date, status) -values (1, 1, '2026-01-17', 'in delivery'); - -insert into orders (id, user_id, order_date, status) -values (2, 1, '2026-01-17', 'shipped'); - -insert into orders (id, user_id, order_date, status) -values (3, 2, '2026-01-17', 'preparing'); - -select * from order_items - -insert into order_items (order_id, product_id, quantity, price_at_purchase) -values (1,1, 2, 49) - -insert into order_items (order_id, product_id, quantity, price_at_purchase) -values (1, 2, 1, 1299) - -insert into order_items (order_id, product_id, quantity, price_at_purchase) -values (2, 3, 1, 999) - -insert into order_items (order_id, product_id, quantity, price_at_purchase) -values (3, 4, 2, 120) - -insert into order_items (order_id, product_id, quantity, price_at_purchase) -values (3, 1, 2, 49) - --- Сейчас уже понял, что можно после insert - values добавлять сразу в скобках несколько штук, но изначально делал так. \ No newline at end of file diff --git a/aharlamcev/postgresql/Filtrationhomework.sql b/aharlamcev/postgresql/Filtrationhomework.sql deleted file mode 100644 index b8d9407..0000000 --- a/aharlamcev/postgresql/Filtrationhomework.sql +++ /dev/null @@ -1,22 +0,0 @@ -select * from products - -select name, category, price, price * 1.3 as price_with_markup, price * 1.5 as price_with_vat -from products -where in_stock is true and category != 'smartwatch' and price between 100 and 1300; - -select name, price, price * 0.85 as price_with_discount -from products -where price < 100 or price > 1000; - -select id, name, category, price from products -where category in ('tablet', 'smartphone', 'mouse') and price > 100; - -select name, category, price from products -where name not ilike '%iphone%'; - -select name, category, price from products -where name ilike '%pro%'; - -select name, category, price from products -where name ilike '%mx%' -order by price desc; \ No newline at end of file diff --git a/aharlamcev/postgresql/WindowFunctionHomework.sql b/aharlamcev/postgresql/WindowFunctionHomework.sql deleted file mode 100644 index 2cf8068..0000000 --- a/aharlamcev/postgresql/WindowFunctionHomework.sql +++ /dev/null @@ -1,72 +0,0 @@ -create table products_extended ( -id INT primary key, -name VARCHAR(50) not null, -category VARCHAR(50) not null, -price INT, -in_stock BOOLEAN default true, -email_support VARCHAR(50) -); - -insert into products_extended (id, name, category, price, in_stock, email_support) -values (1, 'iPhone 14', 'smartphone', 799, true, 'iphone@apple.com'), -(2, 'MacBook Air', 'laptop', 1199, false, NULL), -(3, 'Galaxy S22', 'smartphone', 699, true, 'galaxy@support.com'), -(4, 'ThinkPad X1', 'laptop', 1399, true, 'thinkpad@lenovo.com'), -(5, 'iPad Pro', 'tablet', 999, false, 'ipad@apple.com'), -(6, 'Kindle Paperwhite', 'ebook_reader', 149, true, NULL), -(7, 'Mi Band 7', 'smartwatch', 49, true, 'miband@xiaomi.com'), -(8, 'Surface Pro', 'tablet', 1299, true, 'surface@microsoft.com'), -(9, 'Honor Pad X9', 'tablet', 219, true, 'honor@support.com'), -(10, 'Dell XPS 13', 'laptop', 1099, false, 'dell@support.com'); - -alter table products_extended -alter column price type DECIMAL(10, 2) - ------------------- Задание №1 (По нему ты говорил, что нечитаемо, объясни как переделать pls <3) -with email_false as (select * from -( -select category, name, price, email_support, -row_number() over (partition by category order by price desc) as rn -from products_extended -where email_support is null) -where rn = 1 -), - -email_true as (select * from -( -select category, name, price, email_support, -row_number() over (partition by category order by price desc) as rn -from products_extended -where email_support is not null) -where rn = 1 -) - -select category, name, price, email_support from email_false -union all -select category, name, price, email_support from email_true -order by category, price desc - ----------------------- Задание №2 (Переделал с двух оконок в одном запросе на СТЕ с одной оконкой внутри) -with get_data as ( -select name, category, price, -LEAD(price) OVER (PARTITION BY category order by price) AS next_price -from products_extended -) - -select name, category, price, next_price, -price - next_price AS diff -from get_data - ---------------------- Задание №3 -select category, name, price, -SUM(price) over (partition by category order by price) as cumulative_sales -from products_extended - ----------------------- Задание №4 -with get_data as ( -select category, name, price, AVG(price) over (partition by category) as avg_price -from products_extended -) -select category, name, price, avg_price, -case when price > avg_price then true else false end as is_above_avg -from get_data diff --git a/aharlamcev/postgresql/homeworkDDL.sql b/aharlamcev/postgresql/homeworkDDL.sql deleted file mode 100644 index 5e43015..0000000 --- a/aharlamcev/postgresql/homeworkDDL.sql +++ /dev/null @@ -1,35 +0,0 @@ -create table users ( -id int primary key, -name VARCHAR(100) not null, -email VARCHAR(100), -country VARCHAR(50), -crated_at date not null -) ; - -create table products ( -id int primary key, -name VARCHAR(100) not null, -category VARCHAR(50), -price DECIMAL(10, 2), -in_stock BOOLEAN default true -) ; - -create table orders ( -id int primary key, -user_id int, -order_date DATE not null, -status VARCHAR(20), -foreign key (user_id) references users(id) -) ; - -create table order_items ( -order_id int, -product_id int, -quantity int not null, -price_at_purchase DECIMAL(10, 2) not null, -foreign key (order_id) references orders(id), -foreign key (product_id) references products(id) -) ; - -alter table users -rename column crated_at to created_at; \ No newline at end of file diff --git a/aharlamcev/postgresql/sqlhomework.sql b/aharlamcev/postgresql/sqlhomework.sql deleted file mode 100644 index 4fb1078..0000000 --- a/aharlamcev/postgresql/sqlhomework.sql +++ /dev/null @@ -1,23 +0,0 @@ -create table products_extended ( -id INT primary key, -name VARCHAR(100) not null, -category VARCHAR(50), -price DECIMAL(10, 2), -in_stock BOOLEAN default true, -email_support VARCHAR(50) -); - -insert into products_extended (id, name, category, price, in_stock, email_support) -values (1, 'Mi Band 7', 'smartwatch', 49, true, 'miband@xiaomi.com'); - -insert into products_extended (id, name, category, price, in_stock, email_support) -values (2, 'Surface Pro', 'tablet', 1299, true, 'surface@microsoft.com'); - -select * from products_extended -- проверил как выглядит таблица - -alter table products_extended -add column rating DECIMAL(5, 2); - -truncate table products_extended; - -drop table products_extended From 4aa11c013629701ee6a5992f63ef7b844fd98f69 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 15 Feb 2026 15:30:39 +0300 Subject: [PATCH 023/179] All homework SQL2.0 done in one file --- aharlamcev/postgresql/SQL2.0FULL.sql | 523 +++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 aharlamcev/postgresql/SQL2.0FULL.sql diff --git a/aharlamcev/postgresql/SQL2.0FULL.sql b/aharlamcev/postgresql/SQL2.0FULL.sql new file mode 100644 index 0000000..7cc305b --- /dev/null +++ b/aharlamcev/postgresql/SQL2.0FULL.sql @@ -0,0 +1,523 @@ +----------------------------------------------------------- +--DDL +create table users ( +id INT primary key not null, +name VARCHAR(100), +age INT, +email VARCHAR(100), +country VARCHAR(50), +is_active BOOLEAN, +created_at DATE +); + +create table products ( +id INT primary key, +name VARCHAR(100) not null, +category VARCHAR(50), +price DECIMAL(10, 2) not null, +in_stock BOOLEAN +); + +create table orders ( +id INT primary key, +user_id INT, +order_date DATE not null, +status VARCHAR(20) +); + +create table order_items ( +order_id INT, +product_id INT, +quantity INT not null, +price_at_purchase DECIMAL(10, 2) not null, +primary key (order_id, product_id) +); + +alter table users +alter column created_at type timestamptz; + +-------------------------------------------------------------------------------------- +-- DML +INSERT INTO users (id, name, age, email, country, is_active, created_at) +VALUES +(1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'), +(2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'), +(3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'), +(4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'), +(5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'), +(6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'), +(7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'), +(8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'), +(9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'), +(10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00'); + + +INSERT INTO products (id, name, category, price, in_stock) +VALUES +(1, 'iPhone 15', 'Электроника', 80000, true), +(2, 'Kindle', 'Электроника', 15000, true), +(3, 'Кроссовки Nike', 'Одежда', 9000, true), +(4, 'Кроссовки NoName', 'Одежда', 3000, false), +(5, 'Наушники Pro', 'Электроника', 12000, true), +(6, 'Фитнес‑браслет', 'Гаджеты', 6000, true), +(7, 'Подарочная карта', 'Сервисы', 1000, true); + + +INSERT INTO orders (id, user_id, order_date, status) +VALUES +(1, 1, '2025-01-10 12:00:00', 'paid'), +(2, 3, '2025-01-12 09:30:00', 'cancelled'), +(3, 1, '2025-01-15 18:20:00', 'paid'), +(4, 2, '2025-01-20 11:05:00', 'created'), +(5, 4, '2025-02-01 17:45:00', 'paid'), +(6, 5, '2025-02-10 10:10:00', 'refunded'), +(7, 7, '2025-03-01 09:00:00', 'paid'), +(8, 9, '2025-03-05 19:30:00', 'paid'); + + +INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) +VALUES +(1, 1, 1, 78000), +(1, 3, 2, 8500), +(2, 2, 1, 14000), +(3, 5, 1, 11000), +(3, 7, 1, 1000), +(4, 2, 1, 15000), +(5, 6, 1, 6000), +(6, 1, 1, 82000), +(7, 3, 1, 9000), +(8, 4, 2, 2800); + + +-------------------------------------------------------------------------------------------- +--BASE SQL +select id, name, country, created_at, email from users +order by created_at desc, name; + +select id, name, country from users +where country = 'US' -- 'UK' +order by name; + +select id, name, email from users +where email is not null +order by name; + +select distinct LOWER(email) as email_lower from users +where email is not null; + +select id, name, created_at from users +where created_at > '2024-01-11' +order by created_at desc +limit 2 offset 1; +------------------------------------------------------------------------------------------------------ +-- FILTRATION +select name, category, price, +price * 1.3 as price_with_markup, +price * 1.5 as price_with_vat from products +where in_stock is true +and category not in ('Гаджеты') +and price >= 100 and price <=1300; + +select name, price, +price * 0.85 as price_with_discount +from products +where price < 100 or price > 1000; + +select id, name, category, price from products +where category in ('Электроника') and price >= 15000; + +select name, category, price from products +where name not like '%iPhone%'; + +select * from products +where name ilike '%pro%'; + +--------------------------------------------------------------------------------------------------- +-- JOIN +-- 1 задание +select o.id as order_id, order_date, status, u.name as user_name, country from orders o +join users u on o.user_id = u.id; + +-- 2 задание + +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where o.id = 1; + +-- 3 задание +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id; + +-- 4 задание +select u.name as user_name, o.id as order_id, o.status from users u +left join orders o on u.id = o.user_id; + +-- 5 задание +select u.id as user_id, u.name as user_name from users u +left join orders o on u.id = o.user_id +where o.user_id is null; + +-- 6 задание + +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where p.name = 'Фитнес‑браслет'; + +-- 7 задание + +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where o.status = 'paid'; + +-- 8 задание + +select o.id, o.order_date, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where o.user_id in (select id from users where name = 'Alice'); + +-- 9 задание + +select o.id as order_id, o.user_id, o.status from orders o +left join order_items oi on o.id = oi.order_id +where oi.product_id is null; -- таких у нас заказов нет, во всех заказах есть товары. + +-------------------------------------------------------------------------------------------- +-- UNION +-- 1 задание +select id, name, is_active from users +where is_active is false +union +select id, name, is_active from users +where is_active is true +order by name; + +-- 2 задание +select id as order_id, user_id, order_date, +case when o.order_date < '2025-02-01' then 'old' else 'new' end as period +from orders o; +-- 3е задание не имеет смысла, т.к. все заказы покупались, там нечего соединять + +---------------------------------------------------------------------------------------------- +-- Agregation +-- 1 задание +select category, COUNT(*) as products_count from products +group by category +order by products_count desc; + +-- 2 задание +select order_id, SUM(quantity) as total_quantity, SUM(quantity * price_at_purchase) as order_revenue from order_items +group by order_id +order by order_revenue desc; + +-- 3 задание +select p.category, SUM(oi.quantity * oi.price_at_purchase) as category_revenue from products p +join order_items oi on oi.product_id = p.id +group by p.category +order by category_revenue desc; + +-- 4 задание +select order_id, SUM(quantity * price_at_purchase) as order_revenue from order_items oi +join orders o on oi.order_id = o.id +where o.status = 'paid' +group by oi.order_id +order by order_revenue desc; + +-- 5 задание +select p.category, SUM(oi.quantity * oi.price_at_purchase) as category_revenue from products p +join order_items oi on oi.product_id = p.id +group by p.category +having SUM(oi.quantity * oi.price_at_purchase) > 1000 +order by category_revenue desc; + +-- 6 задание +select country, COUNT(*) as active_users_count +from users +where is_active is true +group by country +having COUNT(*) >= 2 +order by active_users_count desc; + +-- 7 задание +select p.name, SUM(oi.quantity) as total_quantity_sold from products p +join order_items oi on oi.product_id = p.id +group by p.name +order by total_quantity_sold desc +limit 3; + +-- 8 задание + +select order_id, SUM(quantity * price_at_purchase) as order_revenue +from order_items +group by order_id +having SUM(quantity * price_at_purchase) < 10000 -- сделал меньше 10000 потому что заказов дешевле 100 нет. +order by order_revenue; + +-------------------------------------------------------------------------------------------------------------- +--Functions:date strings logic +-- 1 задание +select name, +created_at::date as created_date, +CURRENT_DATE - created_at::date as days_since_registration +from users +where CURRENT_DATE - created_at::date > 750 +order by days_since_registration desc; + +-- 2 задание +select name, LOWER(email) as email_clean, +substring(LOWER(email) from position('@' in email) + 1) as domain +from users +where email is not null +order by domain, name; + +-- 3 задание +select name, age, +case when age < 25 then 'young' + when age between 20 and 30 then 'adult' + else 'senior' + end as age_group +from users +order by age; + +-- 4 задание +with CTE as + (select name, created_at::date, + current_date - created_at::date as days_since_registration, + case when is_active = true then 'hot' + when current_date - created_at::date < 30 then 'hot' + else 'regular' + end as hot_flag + from users) +select * from CTE +where hot_flag = 'hot'; + +-- 5 задание +with CTE as + (select count(id) as total_users, + COUNT(email) as with_email, + (select count(*) from users where email is null) as without_email + from users) +select total_users, with_email, without_email, + ROUND(100.0 * without_email / total_users, 2) as percentage_no_email + from CTE; + + -- 6 задание + select name, + CURRENT_DATE - created_at::date as days_since_registration, + case + when CURRENT_DATE - created_at::date <= 720 then 'new' + when CURRENT_DATE - created_at::date BETWEEN 721 and 750 then 'middle' + else 'old' + end as tenure_group + from users + order by days_since_registration; + + ---------------------------------------------------------------------------------------------------- + --Subquery/CTE + -- 1 задание +select name +from users +where id in (select distinct user_id from orders) +order by name; + +-- 2 задание +select id, name, email +from users u +where not exists ( +select user_id +from orders o +where o.user_id = u.id) + +-- 3 задание +select name, +(select COUNT(*) orders_count from orders o +where o.user_id = u.id) +from users u +order by orders_count desc, name; + +-- 4 задание +with CTE as ( + select user_id, SUM(quantity * price_at_purchase) as total_revenue + from orders o + join order_items oi on o.id = oi.order_id + group by user_id + ) +select u.name, c.total_revenue from users u +join CTE c on u.id = c.user_id +order by c.total_revenue desc; + +-- 5 задание +with CTE as ( + select product_id, SUM(quantity * price_at_purchase) as product_revenue + from order_items + group by product_id + ) +select p.name as product_name, p.category, c.product_revenue from CTE c +join products p on c.product_id = p.id +order by c.product_revenue desc +limit 3; + +-- 6 задание +with CTE as ( + select o.user_id, SUM(quantity * price_at_purchase) as total_revenue from orders o + join order_items oi on o.id = oi.order_id + group by o.user_id + ) +select u.name, u.email, c.total_revenue from users u +join CTE c on u.id = c.user_id +where u.is_active is true +and c.total_revenue > 10000; + +-- 7 задание + +with CTE as ( + select order_id, SUM(quantity * price_at_purchase) as order_revenue + from order_items oi + join orders o on oi.order_id = o.id + where o.status = 'paid' + group by order_id + ) +select * from CTE +where order_revenue < 10000; + +--------------------------------------------------------------------------------------------- +--WINDOW FUNCTIONS +-- 1 задание +select u.id as user_id, u.name, o.id as order_id, o.order_date, +row_number() over (partition by o.user_id order by o.order_date) as order_number_for_user +from users u +join orders o on u.id = o.user_id; + +-- 2 задание +with CTE as ( + select u.id as user_id, u.name as user_name, order_id, o.order_date, SUM(quantity * price_at_purchase) as order_revenue + from order_items oi + join orders o on oi.order_id = o.id + join users u on o.user_id = u.id + group by u.id, u.name, order_id, o.order_date + order by user_id + ) +select user_id, user_name, order_id, order_date, order_revenue, +SUM(order_revenue) over (partition by user_id order by order_date) as running_revenue_for_user +from CTE + +-- 3 задание +with CTE as ( + select u.id as user_id, u.name as user_name, SUM(quantity * price_at_purchase) as total_revenue + from order_items oi + join orders o on oi.order_id = o.id + join users u on o.user_id = u.id + group by u.id, u.name + ) +select user_name, total_revenue, +RANK() over (order by total_revenue desc) as revenue_rank, +dense_rank() over (order by total_revenue desc) as dense_revenue_rank +from CTE; + + +-- 4 задание +select u.name, o.id as order_id, order_date, +first_value(o.order_date) over (partition by user_id order by order_date) as first_order_date, +last_value(o.order_date) over (partition by user_id order by order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as last_order_date +from orders o +join users u on u.id = o.user_id; + + +-- 5 задание +select u.name as user_name, o.id as order_id, +COUNT(*) over (partition by u.id) as orders_per_user +from users u +join orders o on u.id = o.user_id; + +-- 6 задание +with CTE as ( + select u.id as user_id, u.name as user_name, order_id, o.order_date as order_date, + SUM(quantity * price_at_purchase) as order_revenue + from order_items oi + join orders o on oi.order_id = o.id + join users u on o.user_id = u.id + group by u.id, u.name, order_id, o.order_date + order by user_id + ) +select user_name, order_id, order_date, order_revenue, +LAG(order_revenue) over (partition by user_id order by order_date) as prev_order_revenue +from CTE; + +-- 7 задание +with CTEa as ( + select product_id, + o.order_date::date as order_date, + SUM(quantity * price_at_purchase) as daily_category_revenue + from order_items oi + join orders o on oi.order_id = o.id + group by product_id, order_date + order by oi.product_id + ), + CTEb as ( + select p.category, order_date, daily_category_revenue + from CTEa a + join products p on a.product_id = p.id + ) +select category, order_date, daily_category_revenue, +SUM(daily_category_revenue) over (partition by category order by order_date) as running_category_revenue +from CTEb; + +----------------------------------------------------------------------------------------------------------- +-- VIEW and MATERIALIZED VIEW +-- 1 задание +create MATERIALIZED view mv_category_revenue +as + select p.category, SUM(quantity * price_at_purchase) as total_revenue + from products p + join order_items oi on oi.product_id = p.id + join orders o on oi.order_id = o.id and o.status = 'paid' + group by p.category + order by total_revenue desc +; + +-- 2 задание +create materialized view +as + select u.id, u.name, COUNT(o.id) as order_count + from users u + join orders o on u.id = o.user_id and o.status = 'paid' + group by u.id, u.name + order by order_count desc +; + +-- 3 задание +create materialized view mv_daily_revenue +as + select order_date::date as sales_date, SUM(quantity * price_at_purchase) as daily_revenue from orders o + join order_items oi on o.id = oi.order_id + where o.status = 'paid' + group by sales_date + order by sales_date +; + +-- 4 задание +refresh materialized view mv_daily_revenue; + +-- В 1 случае витрину можно обновлять 1 раз в день в 10:00 или 2 раза в день в 10:00 и в 22:00 +-- Во 2 случае витрину нужно обновлять несколько раз в день, думаю каждые часа 2-3 то есть в 8 в 10 в 12 и тд. + + + + + + + + + + + + + + + + + From 87ba57befe7b54dae8889e7ae059c107503c7561 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Feb 2026 11:05:00 +0300 Subject: [PATCH 024/179] FIXED --- aharlamcev/postgresql/SQL2.0FULL.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aharlamcev/postgresql/SQL2.0FULL.sql b/aharlamcev/postgresql/SQL2.0FULL.sql index 7cc305b..c44e8e7 100644 --- a/aharlamcev/postgresql/SQL2.0FULL.sql +++ b/aharlamcev/postgresql/SQL2.0FULL.sql @@ -23,6 +23,7 @@ id INT primary key, user_id INT, order_date DATE not null, status VARCHAR(20) +FOREIGN KEY (user_id) REFERENCES users(id) ); create table order_items ( @@ -31,6 +32,7 @@ product_id INT, quantity INT not null, price_at_purchase DECIMAL(10, 2) not null, primary key (order_id, product_id) +FOREIGN KEY () ); alter table users @@ -276,7 +278,7 @@ order by domain, name; -- 3 задание select name, age, -case when age < 25 then 'young' +case when age < 20 then 'young' when age between 20 and 30 then 'adult' else 'senior' end as age_group From 0c961319b9745adcb91eb4e2636bc45948a56f17 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Feb 2026 13:16:37 +0300 Subject: [PATCH 025/179] PET-project 1st task completed --- aharlamcev/postgresql/Pet-projectsql.sql | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index 8b1d51f..d9b0c35 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -200,3 +200,38 @@ VALUES (56, 14, 7, '2023-01-11 10:00:00'), (57, 14, 8, '2023-01-12 11:00:00'), (58, 14, 9, '2023-01-10 08:00:00'); + + +--------------------------------------------------------- +-- 1 задание Активные пользователи + +create materialized view user_activity_summary AS +with enroll_count as ( + select u.id as user_id, u.name as user_name, + COUNT(e.course_id) as enrollment_count + from users u + join enrollments e on u.id = e.user_id + group by u.id, u.name + ), +viewed_count as ( + select ec.user_id, ec.user_name, enrollment_count, + COUNT(distinct lv.lesson_id) as viewed_lessons_count, + MIN(lv.viewed_at) as first_view, + MAX(lv.viewed_at) as last_view + from enroll_count ec + left join lesson_views lv on ec.user_id = lv.user_id + group by ec.user_id, ec.user_name, enrollment_count + ), +possible_lessons as ( + select u.id as user_id, COUNT(l.id) as poss_lessons + from users u + left join enrollments e on u.id = e.user_id + left join lessons l on e.course_id = l.course_id + group by u.id + ) +select vc.user_id, user_name, enrollment_count, viewed_lessons_count, first_view, last_view, +ROUND(100.0 * viewed_lessons_count / pl.poss_lessons, 2) as engagement_rate +from viewed_count vc +left join possible_lessons pl on vc.user_id = pl.user_id +; + From 0efca05db5aee0be461d64658b74ae87e341d64f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Feb 2026 06:41:34 +0300 Subject: [PATCH 026/179] NOT NULL added into users table --- aharlamcev/postgresql/Pet-projectsql.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index d9b0c35..d6cdd9d 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -1,8 +1,8 @@ create table users ( id INT primary key, -name VARCHAR(50), +name VARCHAR(50) NOT NULL, age INT, -email VARCHAR(50), +email VARCHAR(50) NOT NULL, registration_date date ); From 0c4ad4965d11717a5c79484629b04631e71dff00 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Feb 2026 15:08:15 +0300 Subject: [PATCH 027/179] PET 2nd task completed --- aharlamcev/postgresql/Pet-projectsql.sql | 40 +++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index d6cdd9d..071765c 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -234,4 +234,42 @@ ROUND(100.0 * viewed_lessons_count / pl.poss_lessons, 2) as engagement_rate from viewed_count vc left join possible_lessons pl on vc.user_id = pl.user_id ; - + +------------------------------------------------------------------------------------------------------------------ +-- 2 задание Эффективность курсов + +create materialized view course_summary AS + with lessons_per_course as ( + select c.id as course_id, c.title as course_title, + COUNT(distinct e.user_id) as unique_users, + COUNT(distinct l.id) as lessons_count + from courses c + left join enrollments e on c.id = e.course_id + join lessons l on e.course_id = l.course_id + group by c.id, c.title + ), + course_views as ( + select l.course_id, COUNT(lv.id) as total_views + from lesson_views lv + join lessons l on lv.lesson_id = l.id + group by l.course_id + ), + view_date as ( + select l.course_id, + MIN(lv.viewed_at) as first_viewed_at, + MAX(lv.viewed_at) as last_viewed_at + from lessons l + join lesson_views lv on l.id = lv.lesson_id + group by l.course_id + ) + select lpc.course_id, lpc.course_title, lpc.unique_users, lpc.lessons_count, + ROUND(1.00 * cv.total_views / lpc.unique_users, 2) as avg_views_per_user, + vd.first_viewed_at, + vd.last_viewed_at + from lessons_per_course lpc + join course_views cv on lpc.course_id = cv.course_id + left join view_date vd on cv.course_id = vd.course_id + order by course_id +; + + From 1fa2d88bf2e1801df01d5fad411680fe8e18f4b0 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Feb 2026 14:41:23 +0300 Subject: [PATCH 028/179] PET project task 3 completed --- aharlamcev/postgresql/Pet-projectsql.sql | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index 071765c..bc21d74 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -272,4 +272,22 @@ create materialized view course_summary AS order by course_id ; +-------------------------------------------------------------------------------------------------- +-- 3 задание Общая отчетность по взаимодействию +create materialized view platform_summary as +with total_counts as ( + select COUNT(id) as total_users, + (select COUNT(id) from courses) as total_courses, + (select COUNT(id) from lessons) as total_lessons, + (select COUNT(id) from lesson_views) as total_views, + (select COUNT(distinct user_id) from lesson_views) as users_with_views + from users + ) +SELECT total_users, total_courses, total_lessons, users_with_views, +ROUND(1.00 * total_lessons / total_courses, 2) as avg_lessons_per_course, +ROUND(1.00 * total_views / total_lessons, 2) as avg_views_per_lesson +from total_counts +; + + From 93ae8af223a66b83d75e37b0076d9a76c8c055f1 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 Feb 2026 14:31:52 +0300 Subject: [PATCH 029/179] PET 4th task completed --- aharlamcev/postgresql/Pet-projectsql.sql | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index bc21d74..1fb75f6 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -289,5 +289,38 @@ ROUND(1.00 * total_views / total_lessons, 2) as avg_views_per_lesson from total_counts ; +----------------------------------------------------------------------------------------------------- +-- 4 Задание Когортный анализ пользователей +create materialized view user_cohort_analysis AS +with needed_users as ( + select distinct u.id as user_id, u.registration_date as reg_date, e.course_id, lv.id as view_id, + lv.lesson_id as viewed_lesson_id, + FIRST_VALUE(lv.viewed_at) over (partition by lv.user_id order by lv.viewed_at) as first_view_date, + e.user_id as enrolled_users, + lv.user_id as active_users + from users u + left join enrollments e on u.id = e.user_id + left join lesson_views lv on e.user_id = lv.user_id + order by user_id + ), +CTE as ( + select + TO_CHAR(reg_date, 'YYYY-MM') as cohort_month, + COUNT(distinct user_id) as users_in_cohort, + COUNT(distinct enrolled_users) as enrolled_users, + COUNT(distinct active_users) as active_users, + COUNT(course_id) as courses_count, + extract('days' from AVG(first_view_date - reg_date)) as avg_time_to_first_view + from needed_users + where date_trunc('month', reg_date)::date = '2023-03-01' + group by cohort_month + ) +select cohort_month, users_in_cohort, enrolled_users, active_users, +ROUND(100.0 * active_users / users_in_cohort, 2) as retention_rate, +courses_count / users_in_cohort as avg_courses_per_user, +avg_time_to_first_view +from CTE +; + From f8bf5e5b06ba4367f65690d80263ffdb855d5abb Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 19 Feb 2026 23:21:05 +0400 Subject: [PATCH 030/179] Clickhouse-Homewok --- dsolovianov/clickhouse/{ => Homework}/image.png | Bin .../{ => Homework}/Типы таблиц - Домашки.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename dsolovianov/clickhouse/{ => Homework}/image.png (100%) rename dsolovianov/clickhouse/{ => Homework}/Типы таблиц - Домашки.sql (100%) diff --git a/dsolovianov/clickhouse/image.png b/dsolovianov/clickhouse/Homework/image.png similarity index 100% rename from dsolovianov/clickhouse/image.png rename to dsolovianov/clickhouse/Homework/image.png diff --git a/dsolovianov/clickhouse/Типы таблиц - Домашки.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql similarity index 100% rename from dsolovianov/clickhouse/Типы таблиц - Домашки.sql rename to dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql From 1f660822715ef71935574b75299f38fa64178a91 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Feb 2026 15:13:14 +0300 Subject: [PATCH 031/179] PET 5th task completed --- aharlamcev/postgresql/Pet-projectsql.sql | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index 1fb75f6..4bf94af 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -322,5 +322,40 @@ avg_time_to_first_view from CTE ; +------------------------------------------------------------------------------------------------------------------ +-- 5 Задание Популярность курсов +create materialized view lesson_popularity_ranking AS +with lessons_in_courses as ( + select l.id as lesson_id, l.title as lesson_title, c.title as course_title, + COUNT(e.user_id) as enrolled_users + from lessons l + join courses c on l.course_id = c.id + join enrollments e on e.course_id = c.id + group by l.id, l.title, c.title + order by lesson_id + ), +views_cte as ( + select lic.lesson_id, + COUNT(lv.id) as total_views, + COUNT(distinct lv.user_id) as unique_viewers, + 1.00 * COUNT(lv.id) / COUNT(distinct lv.user_id) as avg_views_per_user, + COUNT(lic.enrolled_users) as enrolled_to_course + from lessons_in_courses lic + join lesson_views lv on lic.lesson_id = lv.lesson_id + group by lic.lesson_id + ) +select lic.lesson_id, lic.lesson_title, lic.course_title, +vc.total_views, +vc.unique_viewers, +vc.avg_views_per_user, +dense_rank() OVER (ORDER BY vc.total_views desc) as popularity_rank, +ROUND(100.0 * unique_viewers / enrolled_to_course, 2) as completion_rate  +from lessons_in_courses lic +join views_cte vc on lic.lesson_id = vc.lesson_id +; +-- Единственный момент в том, что в итоговом MV нет уроков и курсов без просмотров, +--то есть тут только те уроки, у которых есть хотя бы 1 просмотр. +-- Я не смог разобраться с NULL после LEFT JOIN таблиц и выдывало ошибку деления на 0 при подсчете completion_rate + From 27f67be9da1d3721c11631def791b081567ce4e8 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 20 Feb 2026 22:03:55 +0100 Subject: [PATCH 032/179] feat(sql): complete window functions homework --- .../postgresql/homework_window_functions.sql | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 pmikhailov/postgresql/homework_window_functions.sql diff --git a/pmikhailov/postgresql/homework_window_functions.sql b/pmikhailov/postgresql/homework_window_functions.sql new file mode 100644 index 0000000..81b0974 --- /dev/null +++ b/pmikhailov/postgresql/homework_window_functions.sql @@ -0,0 +1,151 @@ +/* Задание 1 */ + +SELECT + u.id, + u."name", + o.id AS order_id, + o.order_date, + ROW_NUMBER() OVER ( + PARTITION BY u.id + ORDER BY o.order_date) AS order_number_for_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id + + +/* Задание 2 */ + +WITH order_revenue_cte AS ( + SELECT + oi.order_id, + oi.quantity * oi.price_at_purchase AS order_revenue + FROM + order_items oi + ) +SELECT + u.id, + u."name", + o.id AS order_id, + o.order_date, + orc.order_revenue, + sum(orc.order_revenue) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ROWS UNBOUNDED PRECEDING + ) AS running_revenue_for_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id +INNER JOIN + order_revenue_cte orc ON o.id = orc.order_id + + +/* Задание 3 */ + +WITH user_revenue_cte AS ( + SELECT + o.user_id, + oi.quantity * oi.price_at_purchase AS order_revenue, + sum(oi.quantity * oi.price_at_purchase) OVER ( + PARTITION BY o.user_id) AS user_total_revenue + FROM + orders o + INNER JOIN + order_items oi ON o.id = oi.order_id) +SELECT DISTINCT + u."name", + urc.user_total_revenue, + RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_rank, + DENSE_RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_dense_rank +FROM + users u +INNER JOIN + user_revenue_cte urc ON u.id = urc.user_id + + +/* Задание 4 */ + +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date, + FIRST_VALUE(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS first_order_date, + LAST_VALUE(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id; + + +/* Задание 5 */ + +SELECT + u.name, + o.id, + count(o.id) OVER ( + PARTITION BY o.user_id) AS orders_per_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id + + +/* Задание 6 */ + +WITH order_revenue_cte AS ( + SELECT + oi.order_id, + oi.quantity * oi.price_at_purchase AS order_revenue + FROM + order_items oi + ) +SELECT + u.name, + o.id, + o.order_date, + orc.order_revenue, + lag(orc.order_revenue) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS prev_order_revenue +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id +INNER JOIN + order_revenue_cte orc ON o.id = orc.order_id + + +/* Задание 7 */ + +WITH category_revenue_cte AS ( + SELECT + p.category, + date_trunc('day', o.order_date)::date AS order_period, + sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM + products p + LEFT JOIN + order_items oi ON p.id = oi.product_id + INNER JOIN + orders o ON oi.order_id = o.id + GROUP BY 1, 2) +SELECT + cr_cte.*, + sum(cr_cte.daily_category_revenue) OVER ( + PARTITION BY cr_cte.category + ORDER BY cr_cte.order_period + ROWS UNBOUNDED PRECEDING + ) AS running_category_revenue +FROM + category_revenue_cte cr_cte; + \ No newline at end of file From a109cadca200fe5e9bed81672bcb0bb57a83816a Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Feb 2026 13:22:24 +0300 Subject: [PATCH 033/179] Added LEFT JOIN and NULLIF to Task 5 --- aharlamcev/postgresql/Pet-projectsql.sql | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index 4bf94af..a7cee09 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -324,7 +324,7 @@ from CTE ------------------------------------------------------------------------------------------------------------------ -- 5 Задание Популярность курсов -create materialized view lesson_popularity_ranking AS +create materialized view lesson_popularity_ranking as with lessons_in_courses as ( select l.id as lesson_id, l.title as lesson_title, c.title as course_title, COUNT(e.user_id) as enrolled_users @@ -338,24 +338,22 @@ views_cte as ( select lic.lesson_id, COUNT(lv.id) as total_views, COUNT(distinct lv.user_id) as unique_viewers, - 1.00 * COUNT(lv.id) / COUNT(distinct lv.user_id) as avg_views_per_user, + 1.00 * COUNT(lv.id) / nullif(COUNT(distinct lv.user_id), 0) as avg_views_per_user, COUNT(lic.enrolled_users) as enrolled_to_course from lessons_in_courses lic - join lesson_views lv on lic.lesson_id = lv.lesson_id + left join lesson_views lv on lic.lesson_id = lv.lesson_id group by lic.lesson_id ) select lic.lesson_id, lic.lesson_title, lic.course_title, vc.total_views, vc.unique_viewers, -vc.avg_views_per_user, +coalesce(vc.avg_views_per_user, 0), dense_rank() OVER (ORDER BY vc.total_views desc) as popularity_rank, -ROUND(100.0 * unique_viewers / enrolled_to_course, 2) as completion_rate  +ROUND(100.0 * unique_viewers / enrolled_to_course, 2) as completion_rate from lessons_in_courses lic join views_cte vc on lic.lesson_id = vc.lesson_id ; --- Единственный момент в том, что в итоговом MV нет уроков и курсов без просмотров, ---то есть тут только те уроки, у которых есть хотя бы 1 просмотр. --- Я не смог разобраться с NULL после LEFT JOIN таблиц и выдывало ошибку деления на 0 при подсчете completion_rate + From 4d56aa1f5b92082564bcbb9f02e733447e85b0fc Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Feb 2026 15:02:11 +0300 Subject: [PATCH 034/179] Pet 6th task completed --- aharlamcev/postgresql/Pet-projectsql.sql | 95 +++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index a7cee09..1621bf6 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -353,7 +353,100 @@ ROUND(100.0 * unique_viewers / enrolled_to_course, 2) as completion_rate from lessons_in_courses lic join views_cte vc on lic.lesson_id = vc.lesson_id ; - +----------------------------------------------------------------------------------------------- +-- 6 Задания Воронка Конверсии +create materialized view conversion_funnel AS +with course_lessons as ( + select c.id as course_id, c.title as course_title, + COUNT(l.id) as total_lessons + from courses c + join lessons l on l.course_id = c.id + group by c.id, c.title + ), +registered_users as ( + select COUNT(distinct id) as total_users + from users + ), +enrolled_users as ( + select e.course_id, + COUNT(distinct e.user_id) as enrolled_count + from enrollments e + group by e.course_id + ), +started_users as ( + select l.course_id, + COUNT(distinct lv.user_id) as started_count + from lessons l + join lesson_views lv on l.id = lv.lesson_id + group by l.course_id + ), +user_progress as ( + select l.course_id, lv.user_id, + COUNT(distinct lv.lesson_id) as viewed_lessons + from lesson_views lv + join lessons l on lv.lesson_id = l.id + group by l.course_id, lv.user_id + ), +mid_progress_users as ( + select up.course_id, + COUNT(distinct up.user_id) as mid_progress_count + from user_progress up + join course_lessons cl on up.course_id = cl.course_id + where up.viewed_lessons >= cl.total_lessons * 0.5 + group by up.course_id + ), +completed_users as ( + select up.course_id, + COUNT(distinct up.user_id) as completed_count + from user_progress up + join course_lessons cl on up.course_id = cl.course_id + where up.viewed_lessons >= cl.total_lessons + group by up.course_id + ), +funnel_stages as ( + select cl.course_id, cl.course_title, + 'registered' as stage_name, + ru.total_users as users_count, + 1 as stage_order + from course_lessons cl + cross join registered_users ru + union all + select cl.course_id, cl.course_title, + 'enrolled' as stage_name, + eu.enrolled_count as users_count, + 2 as stage_order + from course_lessons cl + left join enrolled_users eu on cl.course_id = eu.course_id + union all + select cl.course_id, cl.course_title, + 'started' as stage_name, + su.started_count as users_count, + 3 as stage_order + from course_lessons cl + left join started_users su on cl.course_id = su.course_id + union all + select cl.course_id, cl.course_title, + 'mid_progress' as stage_name, + coalesce(mpu.mid_progress_count, 0) as users_count, + 4 as stage_order + from course_lessons cl + left join mid_progress_users mpu on cl.course_id = mpu.course_id + union all + select cl.course_id, cl.course_title, + 'completed' as stage_name, + coalesce(cu.completed_count, 0) as users_count, + 5 as stage_order + from course_lessons cl + left join completed_users cu on cl.course_id = cu.course_id + ) + select course_id, course_title, stage_name, users_count, + ROUND(100.0 * users_count / NULLIF(LAG(users_count) over (partition by course_id order by stage_order), 0), 2) + as conversion_from_previous, + ROUND(100.0 * users_count / NULLIF(FIRST_VALUE(users_count) over (partition by course_id order by stage_order), 0), 2) + as conversion_from_start + from funnel_stages +; +-- Признаюсь 6-е задание сделал с помощью нейросети, потому что сам вообще никак к нему подобраться не мог. From 9dedbfc3eb258c863f129d9368221f752c504f7d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 23 Feb 2026 15:06:03 +0300 Subject: [PATCH 035/179] PET 2nd task fixed --- aharlamcev/postgresql/Pet-projectsql.sql | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index 1621bf6..98debeb 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -237,7 +237,6 @@ left join possible_lessons pl on vc.user_id = pl.user_id ------------------------------------------------------------------------------------------------------------------ -- 2 задание Эффективность курсов - create materialized view course_summary AS with lessons_per_course as ( select c.id as course_id, c.title as course_title, @@ -249,26 +248,20 @@ create materialized view course_summary AS group by c.id, c.title ), course_views as ( - select l.course_id, COUNT(lv.id) as total_views - from lesson_views lv - join lessons l on lv.lesson_id = l.id - group by l.course_id - ), - view_date as ( - select l.course_id, + select l.course_id, + COUNT(lv.id) as total_views, MIN(lv.viewed_at) as first_viewed_at, MAX(lv.viewed_at) as last_viewed_at - from lessons l - join lesson_views lv on l.id = lv.lesson_id + from lesson_views lv + join lessons l on lv.lesson_id = l.id group by l.course_id ) select lpc.course_id, lpc.course_title, lpc.unique_users, lpc.lessons_count, ROUND(1.00 * cv.total_views / lpc.unique_users, 2) as avg_views_per_user, - vd.first_viewed_at, - vd.last_viewed_at + cv.first_viewed_at, + cv.last_viewed_at from lessons_per_course lpc join course_views cv on lpc.course_id = cv.course_id - left join view_date vd on cv.course_id = vd.course_id order by course_id ; From c95a867e13b3e892e8520d009c9e135eb5e43ad0 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Feb 2026 14:58:16 +0300 Subject: [PATCH 036/179] PET Extra Tasks completed --- aharlamcev/postgresql/Pet-projectsql.sql | 31 ++++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql index 98debeb..7ad986c 100644 --- a/aharlamcev/postgresql/Pet-projectsql.sql +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -283,7 +283,7 @@ from total_counts ; ----------------------------------------------------------------------------------------------------- --- 4 Задание Когортный анализ пользователей +-- 4 Задание Когортный анализ пользователей (Для каждого месяца регистрации) create materialized view user_cohort_analysis AS with needed_users as ( select distinct u.id as user_id, u.registration_date as reg_date, e.course_id, lv.id as view_id, @@ -305,7 +305,6 @@ CTE as ( COUNT(course_id) as courses_count, extract('days' from AVG(first_view_date - reg_date)) as avg_time_to_first_view from needed_users - where date_trunc('month', reg_date)::date = '2023-03-01' group by cohort_month ) select cohort_month, users_in_cohort, enrolled_users, active_users, @@ -340,7 +339,7 @@ views_cte as ( select lic.lesson_id, lic.lesson_title, lic.course_title, vc.total_views, vc.unique_viewers, -coalesce(vc.avg_views_per_user, 0), +coalesce(vc.avg_views_per_user, 0) as avg_views_per_user, dense_rank() OVER (ORDER BY vc.total_views desc) as popularity_rank, ROUND(100.0 * unique_viewers / enrolled_to_course, 2) as completion_rate from lessons_in_courses lic @@ -441,5 +440,27 @@ funnel_stages as ( ; -- Признаюсь 6-е задание сделал с помощью нейросети, потому что сам вообще никак к нему подобраться не мог. - - +----------------------------------------------------------------------------------------------------------------- +-- Доп. Задание №1 Анализ drop-off точек +select course_id, stage_name, users_count, +100 - conversion_from_previous as lost_precentage +from conversion_funnel +where stage_name = 'started' +order by lost_precentage desc +; +-------------------------------------------------------------------------------------------------------------------- +-- Доп. Задание №2 Топ-3 урока с повторными просмотрами +select lesson_id +from lesson_popularity_ranking +where avg_views_per_user > 1 +limit 3 +; +--------------------------------------------------------------------------------------------------------------------- +-- Доп. Задание №3 Лучшая когорта по retention +select cohort_month, retention_rate +from user_cohort_analysis +order by retention_rate desc +limit 2 +; +-- В нашем случае два месяца с одинаковыми лучшими показателями retention это 2023-01 и 2023-02. Это является следствием того, что у нас +-- наибольшее количество зарегистрированных пользователей именно в эти месяцы, а именно 2023-01 = 40%, а 2023-02 = 20%, соотвественно 60% пользователей были зарегистрированы в эти месяцы. \ No newline at end of file From c5a5df7c25be961c46e991ee7c1dd2402fa08a0e Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 01:54:22 +0400 Subject: [PATCH 037/179] =?UTF-8?q?1)=20=D0=9F=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B4=D0=BE=D0=BC=D0=B0=D1=88=D0=BA?= =?UTF-8?q?=D1=83=20=D0=B2=20=D0=A2=D0=B8=D0=BF=D1=8B=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=20=D0=BF=D0=BE=D0=B4=20ON=20CLUSTER.=202)=20?= =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B4=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D1=88=D0=BA=D1=83=20=D0=B2=20=D0=A2=D0=B8=D0=BF=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20-=20=D0=97=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Типы данных - Домашка.sql | 26 ++++++++++++++++++ .../Homework/Типы таблиц - Домашки.sql | 27 ++++++++++++------- 2 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 dsolovianov/clickhouse/Homework/Типы данных - Домашка.sql diff --git a/dsolovianov/clickhouse/Homework/Типы данных - Домашка.sql b/dsolovianov/clickhouse/Homework/Типы данных - Домашка.sql new file mode 100644 index 0000000..963af35 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Типы данных - Домашка.sql @@ -0,0 +1,26 @@ +--Типы данных + +--Задача 1 +CREATE TABLE raw_data +ON CLUSTER clickhouse_cluster +( + user_id String, + balance String, + created_at String +) ENGINE = MergeTree ORDER BY tuple(); + +SELECT DISTINCT + toDecimal64OrNull(balance, 2) AS balance_decimal, + toDateTime64OrNull(created_at, 3) AS created_at_date +FROM raw_data +WHERE + toDecimal64OrNull(balance, 2) IS NOT NULL + AND toDateTime64OrNull(created_at, 3) IS NOT NULL + +/* + CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL + + Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть: +toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL +toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль. +*/ \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql index 04cda20..af9dcc3 100644 --- a/dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql +++ b/dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql @@ -1,6 +1,9 @@ --Задача 1: Создай таблицу page_views +DROP TABLE page_views; -CREATE TABLE page_views ( +CREATE TABLE page_views +ON CLUSTER clickhouse_cluster +( user_id UInt64, url String, view_time DateTime @@ -11,7 +14,9 @@ ORDER BY (user_id, view_time); --Задача 2: Создай таблицу users -CREATE TABLE users ( +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( id UInt64, name String, updated_at DateTime @@ -21,7 +26,9 @@ ORDER BY id; -- Задача 3: Создай таблицу region_sales -CREATE TABLE region_sales ( +CREATE TABLE region_sales +ON CLUSTER clickhouse_cluster +( region String, date Date, sales UInt64 @@ -30,17 +37,19 @@ ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) ORDER BY (region, date); ---Задача 4.1: Создай таблицу user_metrics +--Задача 4: Создай таблицу user_metrics -CREATE TABLE user_metrics ( +CREATE TABLE user_metrics +ON CLUSTER clickhouse_cluster +( region String, avg_age_state AggregateFunction(avg, UInt8), active_users_state AggregateFunction(uniq, UInt64) ) -ENGINE = AggregatingMergeTree() -ORDER BY region; +ENGINE = AggregatingMergeTree() --Подзадача 4.2 +ORDER BY region; --Подзадача 4.3 ---Задача 4.3: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` +--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` INSERT INTO user_metrics SELECT @@ -61,7 +70,7 @@ FROM ) GROUP BY region; ---Задача 4.4: - Читай их через `avgMerge()` и `uniqMerge()` +--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` SELECT region, From ae3392a2a3540a7d38f7b5836e40cfa9b1536376 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 02:05:53 +0400 Subject: [PATCH 038/179] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=BB=20=D0=B4=D0=BE=D0=BC=D0=B0=D1=88=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=A2=D0=B8=D0=BF=D1=8B=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Типы таблиц - Домашки.sql => CH - Типы таблиц - Домашка.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dsolovianov/clickhouse/Homework/{Типы таблиц - Домашки.sql => CH - Типы таблиц - Домашка.sql} (100%) diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql b/dsolovianov/clickhouse/Homework/CH - Типы таблиц - Домашка.sql similarity index 100% rename from dsolovianov/clickhouse/Homework/Типы таблиц - Домашки.sql rename to dsolovianov/clickhouse/Homework/CH - Типы таблиц - Домашка.sql From 555b38e192e1ab2d61b5302b493c0860ebb60ce1 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 02:06:46 +0400 Subject: [PATCH 039/179] delete --- .../Homework/CH - Типы таблиц - Домашка.sql | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 dsolovianov/clickhouse/Homework/CH - Типы таблиц - Домашка.sql diff --git a/dsolovianov/clickhouse/Homework/CH - Типы таблиц - Домашка.sql b/dsolovianov/clickhouse/Homework/CH - Типы таблиц - Домашка.sql deleted file mode 100644 index af9dcc3..0000000 --- a/dsolovianov/clickhouse/Homework/CH - Типы таблиц - Домашка.sql +++ /dev/null @@ -1,81 +0,0 @@ ---Задача 1: Создай таблицу page_views -DROP TABLE page_views; - -CREATE TABLE page_views -ON CLUSTER clickhouse_cluster -( - user_id UInt64, - url String, - view_time DateTime - ) -ENGINE = MergeTree() -PARTITION BY toYYYYMM(view_time) -ORDER BY (user_id, view_time); - ---Задача 2: Создай таблицу users - -CREATE TABLE users -ON CLUSTER clickhouse_cluster -( -id UInt64, -name String, -updated_at DateTime - ) -ENGINE = ReplacingMergeTree(updated_at) -ORDER BY id; - --- Задача 3: Создай таблицу region_sales - -CREATE TABLE region_sales -ON CLUSTER clickhouse_cluster -( -region String, -date Date, -sales UInt64 - ) -ENGINE = SummingMergeTree() -PARTITION BY toYYYYMM(date) -ORDER BY (region, date); - ---Задача 4: Создай таблицу user_metrics - -CREATE TABLE user_metrics -ON CLUSTER clickhouse_cluster -( -region String, -avg_age_state AggregateFunction(avg, UInt8), -active_users_state AggregateFunction(uniq, UInt64) - ) -ENGINE = AggregatingMergeTree() --Подзадача 4.2 -ORDER BY region; --Подзадача 4.3 - ---Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` - -INSERT INTO user_metrics -SELECT - region, - avgState(age) AS avg_age_state, - uniqState(toUInt64(user_id)) AS active_users_state -FROM -( - SELECT 'Samara' AS region, 25 AS age, 1 AS user_id - UNION ALL - SELECT 'Moscow', 30, 2 - UNION ALL - SELECT 'Moscow', 22, 3 - UNION ALL - SELECT 'Samara', 22, 4 - UNION ALL - SELECT 'Sochi', 34, 5 -) -GROUP BY region; - ---Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` - -SELECT - region, - avgMerge(avg_age_state), - uniqMerge(active_users_state) -FROM user_metrics -GROUP BY region; - From 13f1c9af89d523b8bfaadf3cb221400eadc32b72 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 02:07:40 +0400 Subject: [PATCH 040/179] new vers. --- .../Homework/Типы таблиц - Homework.sql | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql new file mode 100644 index 0000000..af9dcc3 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql @@ -0,0 +1,81 @@ +--Задача 1: Создай таблицу page_views +DROP TABLE page_views; + +CREATE TABLE page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime + ) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + +--Задача 2: Создай таблицу users + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( +id UInt64, +name String, +updated_at DateTime + ) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +-- Задача 3: Создай таблицу region_sales + +CREATE TABLE region_sales +ON CLUSTER clickhouse_cluster +( +region String, +date Date, +sales UInt64 + ) +ENGINE = SummingMergeTree() +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +--Задача 4: Создай таблицу user_metrics + +CREATE TABLE user_metrics +ON CLUSTER clickhouse_cluster +( +region String, +avg_age_state AggregateFunction(avg, UInt8), +active_users_state AggregateFunction(uniq, UInt64) + ) +ENGINE = AggregatingMergeTree() --Подзадача 4.2 +ORDER BY region; --Подзадача 4.3 + +--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` + +INSERT INTO user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(toUInt64(user_id)) AS active_users_state +FROM +( + SELECT 'Samara' AS region, 25 AS age, 1 AS user_id + UNION ALL + SELECT 'Moscow', 30, 2 + UNION ALL + SELECT 'Moscow', 22, 3 + UNION ALL + SELECT 'Samara', 22, 4 + UNION ALL + SELECT 'Sochi', 34, 5 +) +GROUP BY region; + +--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY region; + From 8076bfd5ed82db2ed02096d392942c13ae18ce96 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 02:10:30 +0400 Subject: [PATCH 041/179] =?UTF-8?q?=D0=A2=D0=B8=D0=BF=D1=8B=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20-=20=D0=97=D0=B0=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Типы данных - Домашка.sql => Типы данных - Homework.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dsolovianov/clickhouse/Homework/{Типы данных - Домашка.sql => Типы данных - Homework.sql} (100%) diff --git a/dsolovianov/clickhouse/Homework/Типы данных - Домашка.sql b/dsolovianov/clickhouse/Homework/Типы данных - Homework.sql similarity index 100% rename from dsolovianov/clickhouse/Homework/Типы данных - Домашка.sql rename to dsolovianov/clickhouse/Homework/Типы данных - Homework.sql From 266da092457be285a5552c79883f9de76fa1e101 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 02:12:05 +0400 Subject: [PATCH 042/179] delete --- .../Homework/Типы данных - Homework.sql | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 dsolovianov/clickhouse/Homework/Типы данных - Homework.sql diff --git a/dsolovianov/clickhouse/Homework/Типы данных - Homework.sql b/dsolovianov/clickhouse/Homework/Типы данных - Homework.sql deleted file mode 100644 index 963af35..0000000 --- a/dsolovianov/clickhouse/Homework/Типы данных - Homework.sql +++ /dev/null @@ -1,26 +0,0 @@ ---Типы данных - ---Задача 1 -CREATE TABLE raw_data -ON CLUSTER clickhouse_cluster -( - user_id String, - balance String, - created_at String -) ENGINE = MergeTree ORDER BY tuple(); - -SELECT DISTINCT - toDecimal64OrNull(balance, 2) AS balance_decimal, - toDateTime64OrNull(created_at, 3) AS created_at_date -FROM raw_data -WHERE - toDecimal64OrNull(balance, 2) IS NOT NULL - AND toDateTime64OrNull(created_at, 3) IS NOT NULL - -/* - CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL - - Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть: -toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL -toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль. -*/ \ No newline at end of file From f0e4c59112bfb68d6ced13ceb643cbdb52462d51 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 26 Feb 2026 02:13:06 +0400 Subject: [PATCH 043/179] =?UTF-8?q?=D0=A2=D0=B8=D0=BF=D1=8B=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20v.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Homework - Типы данных.sql | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Homework - Типы данных.sql diff --git a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql new file mode 100644 index 0000000..963af35 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql @@ -0,0 +1,26 @@ +--Типы данных + +--Задача 1 +CREATE TABLE raw_data +ON CLUSTER clickhouse_cluster +( + user_id String, + balance String, + created_at String +) ENGINE = MergeTree ORDER BY tuple(); + +SELECT DISTINCT + toDecimal64OrNull(balance, 2) AS balance_decimal, + toDateTime64OrNull(created_at, 3) AS created_at_date +FROM raw_data +WHERE + toDecimal64OrNull(balance, 2) IS NOT NULL + AND toDateTime64OrNull(created_at, 3) IS NOT NULL + +/* + CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL + + Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть: +toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL +toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль. +*/ \ No newline at end of file From ace9b8b292636e5e4491e6ea694d4c0a9ad7c9a7 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 26 Feb 2026 15:17:39 +0300 Subject: [PATCH 044/179] Task 2 fixed --- aharlamcev/PET-project_task2 | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 aharlamcev/PET-project_task2 diff --git a/aharlamcev/PET-project_task2 b/aharlamcev/PET-project_task2 new file mode 100644 index 0000000..0de3482 --- /dev/null +++ b/aharlamcev/PET-project_task2 @@ -0,0 +1,26 @@ +CREATE MATERIALIZED VIEW platform_summary AS +with users_counts as ( + select + COUNT(distinct u.id) as total_users, + COUNT(distinct lv.user_id) as users_with_views + from users u + left join lesson_views lv on lv.user_id = u.id + ), +courses_lessons_count as ( + select COUNT(distinct c.id) as total_courses, + COUNT(distinct l.id) as total_lessons + from courses c + left join lessons l on l.course_id = c.id + ), +views_count as ( + select COUNT(distinct lv.lesson_id) as lessons_with_views, + COUNT(distinct lv.id) as total_views + from lesson_views lv + ) +select uc.total_users, clc.total_courses, clc.total_lessons, uc.users_with_views, +ROUND(1.00 * clc.total_lessons / nullif(clc.total_courses, 0), 2) as avg_lessons_per_course, +ROUND(1.00 * vc.total_views / nullif(clc.total_lessons, 0), 2) as avg_views_per_lesson +from users_counts uc +cross join courses_lessons_count clc +cross join views_count vc +; \ No newline at end of file From 5854cdbd21bc290f8985cfe807822cc339e0e6f3 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 26 Feb 2026 15:21:31 +0300 Subject: [PATCH 045/179] Name changed to task3 --- aharlamcev/{PET-project_task2 => PET-project_task3} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aharlamcev/{PET-project_task2 => PET-project_task3} (100%) diff --git a/aharlamcev/PET-project_task2 b/aharlamcev/PET-project_task3 similarity index 100% rename from aharlamcev/PET-project_task2 rename to aharlamcev/PET-project_task3 From de4a0fa2ec2c560455bdfea071f15d6f513fa21c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 27 Feb 2026 15:31:56 +0300 Subject: [PATCH 046/179] PET task 4 fixed --- aharlamcev/PET-project_task4 | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 aharlamcev/PET-project_task4 diff --git a/aharlamcev/PET-project_task4 b/aharlamcev/PET-project_task4 new file mode 100644 index 0000000..5cb8fcd --- /dev/null +++ b/aharlamcev/PET-project_task4 @@ -0,0 +1,26 @@ +with needed_users as ( + select distinct u.id as user_id, + u.registration_date as reg_date, e.course_id, lv.id as view_id, + lv.lesson_id as viewed_lesson_id, + FIRST_VALUE(lv.viewed_at) over (partition by lv.user_id order by lv.viewed_at) as first_view_date + from users u + left join enrollments e on u.id = e.user_id + left join lesson_views lv on e.user_id = lv.user_id + order by user_id + ), +CTE as ( + select + TO_CHAR(reg_date, 'YYYY-MM') as cohort_month, + COUNT(distinct user_id) as users_in_cohort, + COUNT(distinct case when user_id in (select user_id from enrollments) then user_id end) as enrolled_users, + COUNT(distinct case when user_id in (select user_id from lesson_views) then user_id end) as active_users, + COUNT(course_id) as courses_count, + extract('days' from AVG(first_view_date - reg_date)) as avg_time_to_first_view + from needed_users + group by cohort_month + ) +select cohort_month, users_in_cohort, enrolled_users, active_users, +ROUND(100.0 * active_users / users_in_cohort, 2) as retention_rate, +courses_count::numeric / users_in_cohort as avg_courses_per_user, +avg_time_to_first_view +from CTE From f7b9920af2f7251ac0340841cbcbe149cb7eedcb Mon Sep 17 00:00:00 2001 From: dmitry solovianov Date: Fri, 27 Feb 2026 19:02:32 +0400 Subject: [PATCH 047/179] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D0=B0=D1=88=D0=BA=D1=83=20=D0=A2=D0=B8=D0=BF?= =?UTF-8?q?=D1=8B=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перезалью в новой ветке --- .../Homework/Типы таблиц - Homework.sql | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql deleted file mode 100644 index af9dcc3..0000000 --- a/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql +++ /dev/null @@ -1,81 +0,0 @@ ---Задача 1: Создай таблицу page_views -DROP TABLE page_views; - -CREATE TABLE page_views -ON CLUSTER clickhouse_cluster -( - user_id UInt64, - url String, - view_time DateTime - ) -ENGINE = MergeTree() -PARTITION BY toYYYYMM(view_time) -ORDER BY (user_id, view_time); - ---Задача 2: Создай таблицу users - -CREATE TABLE users -ON CLUSTER clickhouse_cluster -( -id UInt64, -name String, -updated_at DateTime - ) -ENGINE = ReplacingMergeTree(updated_at) -ORDER BY id; - --- Задача 3: Создай таблицу region_sales - -CREATE TABLE region_sales -ON CLUSTER clickhouse_cluster -( -region String, -date Date, -sales UInt64 - ) -ENGINE = SummingMergeTree() -PARTITION BY toYYYYMM(date) -ORDER BY (region, date); - ---Задача 4: Создай таблицу user_metrics - -CREATE TABLE user_metrics -ON CLUSTER clickhouse_cluster -( -region String, -avg_age_state AggregateFunction(avg, UInt8), -active_users_state AggregateFunction(uniq, UInt64) - ) -ENGINE = AggregatingMergeTree() --Подзадача 4.2 -ORDER BY region; --Подзадача 4.3 - ---Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` - -INSERT INTO user_metrics -SELECT - region, - avgState(age) AS avg_age_state, - uniqState(toUInt64(user_id)) AS active_users_state -FROM -( - SELECT 'Samara' AS region, 25 AS age, 1 AS user_id - UNION ALL - SELECT 'Moscow', 30, 2 - UNION ALL - SELECT 'Moscow', 22, 3 - UNION ALL - SELECT 'Samara', 22, 4 - UNION ALL - SELECT 'Sochi', 34, 5 -) -GROUP BY region; - ---Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` - -SELECT - region, - avgMerge(avg_age_state), - uniqMerge(active_users_state) -FROM user_metrics -GROUP BY region; - From 3acf36e7c82a0105c8de0a23c5e8c908f1a60b48 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Fri, 27 Feb 2026 22:08:09 +0400 Subject: [PATCH 048/179] =?UTF-8?q?+=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=202=20=D0=B2=20=D0=A2=D0=B8=D0=BF=D1=8B=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20-=20Homework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Homework - Типы данных.sql | 37 +++++++++++++++++- .../Homework/Типы данных - Задача 2.png | Bin 0 -> 112412 bytes .../{image.png => Типы таблиц - Homework.png} | Bin 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 dsolovianov/clickhouse/Homework/Типы данных - Задача 2.png rename dsolovianov/clickhouse/Homework/{image.png => Типы таблиц - Homework.png} (100%) diff --git a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql index 963af35..f15301b 100644 --- a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql +++ b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql @@ -7,7 +7,7 @@ ON CLUSTER clickhouse_cluster user_id String, balance String, created_at String -) ENGINE = MergeTree ORDER BY tuple(); +) ENGINE = MergeTree ORDER BY tuple(); SELECT DISTINCT toDecimal64OrNull(balance, 2) AS balance_decimal, @@ -23,4 +23,37 @@ WHERE Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть: toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль. -*/ \ No newline at end of file +*/ + +--Задача 2 +CREATE TABLE events +ON CLUSTER clickhouse_cluster +( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = MergeTree +ORDER BY event_id; + +--Заполнил данными для проверки +INSERT INTO events (event_id, event_date, event_time) VALUES +('1', '2023-01-01', '12:30:45'), +('2', '01.02.2023', '08:15:00'), +('3', 'invalid', '25:99:99'), +('4', '2023-05-20', 'invalid'), +('5', '31.12.2023', '23:59:59'); + +SELECT * FROM events; + +SELECT DISTINCT + toDate(DateTime_Valid) AS Event_Date, + toHour(DateTime_Valid) AS Event_Hour, + DateTime_Valid +FROM ( + SELECT + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS DateTime_Valid + FROM events +) +WHERE DateTime_Valid IS NOT NULL; + diff --git a/dsolovianov/clickhouse/Homework/Типы данных - Задача 2.png b/dsolovianov/clickhouse/Homework/Типы данных - Задача 2.png new file mode 100644 index 0000000000000000000000000000000000000000..3e8757a70e49bd7cec059bddf534fb262cd007c6 GIT binary patch literal 112412 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

    1Bor0tk7k1}a_EU$IX8@lzF#ed0{Q|>sP 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

    S6sXME1(=eOFFI)}P3ea&K?6G^nTa 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 QLBOFbuP#^l)a=cF08!H*7XSbN literal 0 HcmV?d00001 diff --git a/dsolovianov/clickhouse/Homework/image.png b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png similarity index 100% rename from dsolovianov/clickhouse/Homework/image.png rename to dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png From 860d31c75c469e5ee453102823c94ff69609df6a Mon Sep 17 00:00:00 2001 From: dmitry solovianov Date: Fri, 27 Feb 2026 22:23:37 +0400 Subject: [PATCH 049/179] revert f7b9920af2f7251ac0340841cbcbe149cb7eedcb --- .../Homework/Типы таблиц - Homework.sql | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql new file mode 100644 index 0000000..af9dcc3 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql @@ -0,0 +1,81 @@ +--Задача 1: Создай таблицу page_views +DROP TABLE page_views; + +CREATE TABLE page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime + ) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + +--Задача 2: Создай таблицу users + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( +id UInt64, +name String, +updated_at DateTime + ) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +-- Задача 3: Создай таблицу region_sales + +CREATE TABLE region_sales +ON CLUSTER clickhouse_cluster +( +region String, +date Date, +sales UInt64 + ) +ENGINE = SummingMergeTree() +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +--Задача 4: Создай таблицу user_metrics + +CREATE TABLE user_metrics +ON CLUSTER clickhouse_cluster +( +region String, +avg_age_state AggregateFunction(avg, UInt8), +active_users_state AggregateFunction(uniq, UInt64) + ) +ENGINE = AggregatingMergeTree() --Подзадача 4.2 +ORDER BY region; --Подзадача 4.3 + +--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` + +INSERT INTO user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(toUInt64(user_id)) AS active_users_state +FROM +( + SELECT 'Samara' AS region, 25 AS age, 1 AS user_id + UNION ALL + SELECT 'Moscow', 30, 2 + UNION ALL + SELECT 'Moscow', 22, 3 + UNION ALL + SELECT 'Samara', 22, 4 + UNION ALL + SELECT 'Sochi', 34, 5 +) +GROUP BY region; + +--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY region; + From dfc094300c266acbc4b3ebe150aa5c25a64e9fa7 Mon Sep 17 00:00:00 2001 From: dmitry solovianov Date: Fri, 27 Feb 2026 22:23:59 +0400 Subject: [PATCH 050/179] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20dsolovianov/clickhouse/Homework/=D0=A2=D0=B8=D0=BF?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20-=20=D0=97?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=202.png?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Типы данных - Задача 2.png | Bin 112412 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dsolovianov/clickhouse/Homework/Типы данных - Задача 2.png diff --git a/dsolovianov/clickhouse/Homework/Типы данных - Задача 2.png b/dsolovianov/clickhouse/Homework/Типы данных - Задача 2.png deleted file mode 100644 index 3e8757a70e49bd7cec059bddf534fb262cd007c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112412 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

    1Bor0tk7k1}a_EU$IX8@lzF#ed0{Q|>sP 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

    S6sXME1(=eOFFI)}P3ea&K?6G^nTa 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 QLBOFbuP#^l)a=cF08!H*7XSbN From 27f21ebd444e7a3de4b8dd1e46489f9fe93d6884 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Fri, 27 Feb 2026 22:25:27 +0400 Subject: [PATCH 051/179] =?UTF-8?q?=D0=9D=D0=B5=D1=82=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clickhouse/Homework/Типы таблиц.sql | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Типы таблиц.sql diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц.sql b/dsolovianov/clickhouse/Homework/Типы таблиц.sql new file mode 100644 index 0000000..af9dcc3 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Типы таблиц.sql @@ -0,0 +1,81 @@ +--Задача 1: Создай таблицу page_views +DROP TABLE page_views; + +CREATE TABLE page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime + ) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + +--Задача 2: Создай таблицу users + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( +id UInt64, +name String, +updated_at DateTime + ) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +-- Задача 3: Создай таблицу region_sales + +CREATE TABLE region_sales +ON CLUSTER clickhouse_cluster +( +region String, +date Date, +sales UInt64 + ) +ENGINE = SummingMergeTree() +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +--Задача 4: Создай таблицу user_metrics + +CREATE TABLE user_metrics +ON CLUSTER clickhouse_cluster +( +region String, +avg_age_state AggregateFunction(avg, UInt8), +active_users_state AggregateFunction(uniq, UInt64) + ) +ENGINE = AggregatingMergeTree() --Подзадача 4.2 +ORDER BY region; --Подзадача 4.3 + +--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` + +INSERT INTO user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(toUInt64(user_id)) AS active_users_state +FROM +( + SELECT 'Samara' AS region, 25 AS age, 1 AS user_id + UNION ALL + SELECT 'Moscow', 30, 2 + UNION ALL + SELECT 'Moscow', 22, 3 + UNION ALL + SELECT 'Samara', 22, 4 + UNION ALL + SELECT 'Sochi', 34, 5 +) +GROUP BY region; + +--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY region; + From e943afb57d0b3865fead1e00f64ee1506cf8e3b4 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Fri, 27 Feb 2026 22:30:04 +0400 Subject: [PATCH 052/179] - --- .../clickhouse/Homework/Типы таблиц.sql | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 dsolovianov/clickhouse/Homework/Типы таблиц.sql diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц.sql b/dsolovianov/clickhouse/Homework/Типы таблиц.sql deleted file mode 100644 index af9dcc3..0000000 --- a/dsolovianov/clickhouse/Homework/Типы таблиц.sql +++ /dev/null @@ -1,81 +0,0 @@ ---Задача 1: Создай таблицу page_views -DROP TABLE page_views; - -CREATE TABLE page_views -ON CLUSTER clickhouse_cluster -( - user_id UInt64, - url String, - view_time DateTime - ) -ENGINE = MergeTree() -PARTITION BY toYYYYMM(view_time) -ORDER BY (user_id, view_time); - ---Задача 2: Создай таблицу users - -CREATE TABLE users -ON CLUSTER clickhouse_cluster -( -id UInt64, -name String, -updated_at DateTime - ) -ENGINE = ReplacingMergeTree(updated_at) -ORDER BY id; - --- Задача 3: Создай таблицу region_sales - -CREATE TABLE region_sales -ON CLUSTER clickhouse_cluster -( -region String, -date Date, -sales UInt64 - ) -ENGINE = SummingMergeTree() -PARTITION BY toYYYYMM(date) -ORDER BY (region, date); - ---Задача 4: Создай таблицу user_metrics - -CREATE TABLE user_metrics -ON CLUSTER clickhouse_cluster -( -region String, -avg_age_state AggregateFunction(avg, UInt8), -active_users_state AggregateFunction(uniq, UInt64) - ) -ENGINE = AggregatingMergeTree() --Подзадача 4.2 -ORDER BY region; --Подзадача 4.3 - ---Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` - -INSERT INTO user_metrics -SELECT - region, - avgState(age) AS avg_age_state, - uniqState(toUInt64(user_id)) AS active_users_state -FROM -( - SELECT 'Samara' AS region, 25 AS age, 1 AS user_id - UNION ALL - SELECT 'Moscow', 30, 2 - UNION ALL - SELECT 'Moscow', 22, 3 - UNION ALL - SELECT 'Samara', 22, 4 - UNION ALL - SELECT 'Sochi', 34, 5 -) -GROUP BY region; - ---Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` - -SELECT - region, - avgMerge(avg_age_state), - uniqMerge(active_users_state) -FROM user_metrics -GROUP BY region; - From 980aaf9638d814ce56c6f5b9c2f5d0d36bedd40e Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Sat, 28 Feb 2026 00:02:04 +0400 Subject: [PATCH 053/179] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Homework - Типы данных.sql | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql index f15301b..68c8236 100644 --- a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql +++ b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql @@ -57,3 +57,35 @@ FROM ( ) WHERE DateTime_Valid IS NOT NULL; +--Задача 3 +DROP TABLE IF EXISTS user_activity ON CLUSTER clickhouse_cluster; + +CREATE TABLE user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) ENGINE = MergeTree ORDER BY tuple(); + +--Тест данные +INSERT INTO user_activity (user_id, actions) VALUES +(1, '["login", "purchase", "logout"]'), +(2, '["login", "logout"]'), +(3, '["login"]'), +(4, '["login","purchase"]'); + +SELECT + user_id, + -- Сначала реобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Потом считваем длину массива и дальше суммируем длину всех массивов действий для каждого пользователя + sum(length(JSONExtract(actions, 'Array(String)'))) AS count_actions +FROM user_activity +WHERE + -- Проверяем наличие 'purchase' в массиве + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id; +/* +Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase". +Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно. +То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout") +*/ \ No newline at end of file From 8c2a5fcfc8d015a6b269665b8a7fb40285c6fd02 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Sat, 28 Feb 2026 00:46:40 +0400 Subject: [PATCH 054/179] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=20?= =?UTF-8?q?4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Homework - Типы данных.sql | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql index 68c8236..05b588e 100644 --- a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql +++ b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql @@ -88,4 +88,30 @@ GROUP BY Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase". Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно. То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout") -*/ \ No newline at end of file +*/ + +--Задача 4 +DROP TABLE IF EXISTS sensor_data ON CLUSTER clickhouse_cluster; + +CREATE TABLE sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) ORDER BY sensor_id; + +--Тест данные +INSERT INTO sensor_data (sensor_id, value, status) VALUES +(1, 25.5, 'ok'), +(2, 120.75, 'error'), +(3, 15.0, 1), +(4, 9999999.99, 'ok'), +(5, 0.001, 'ok'); + +SELECT + toString(status) AS status_string, + avg(value) AS avg_value --Среднее значение value для каждого status +FROM sensor_data +WHERE accurateCastOrNull(value, 'Decimal(10,2)') IS NOT NULL --Фильтруем строки, где value нельзя преобразовать в Decimal(10,2) +GROUP BY status_string; \ No newline at end of file From cd4e5335aa0564050ec477057ae4186a03c2bce4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 28 Feb 2026 16:07:09 +0300 Subject: [PATCH 055/179] PET task 4 finished --- aharlamcev/PET-project_task4 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aharlamcev/PET-project_task4 b/aharlamcev/PET-project_task4 index 5cb8fcd..d25c088 100644 --- a/aharlamcev/PET-project_task4 +++ b/aharlamcev/PET-project_task4 @@ -1,3 +1,4 @@ +CREATE MATERIALIZED VIEW users_cohort_analysis AS with needed_users as ( select distinct u.id as user_id, u.registration_date as reg_date, e.course_id, lv.id as view_id, @@ -24,3 +25,4 @@ ROUND(100.0 * active_users / users_in_cohort, 2) as retention_rate, courses_count::numeric / users_in_cohort as avg_courses_per_user, avg_time_to_first_view from CTE +; \ No newline at end of file From 7d0193e046ad8ccf20ade7641170761d345a2be0 Mon Sep 17 00:00:00 2001 From: Aleksey Bogatyrev Date: Mon, 2 Mar 2026 11:21:28 +0400 Subject: [PATCH 056/179] add postgresql homework.sql --- test/postgresql/homework.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/postgresql/homework.sql diff --git a/test/postgresql/homework.sql b/test/postgresql/homework.sql new file mode 100644 index 0000000..3df145e --- /dev/null +++ b/test/postgresql/homework.sql @@ -0,0 +1 @@ +select * from users; \ No newline at end of file From e2029b1f7405eb5c2c80bddad7b8d1185ba15f42 Mon Sep 17 00:00:00 2001 From: Aleksey Bogatyrev Date: Mon, 2 Mar 2026 11:24:16 +0400 Subject: [PATCH 057/179] add homework2 --- test/postgresql/homework2.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/postgresql/homework2.sql diff --git a/test/postgresql/homework2.sql b/test/postgresql/homework2.sql new file mode 100644 index 0000000..e194d83 --- /dev/null +++ b/test/postgresql/homework2.sql @@ -0,0 +1 @@ +select * from users1; \ No newline at end of file From 868ea8ca03eadc71ff1e61a06ec4cca7936bf151 Mon Sep 17 00:00:00 2001 From: Aleksey Bogatyrev Date: Mon, 2 Mar 2026 11:26:12 +0400 Subject: [PATCH 058/179] fix homework2.sql --- test/postgresql/homework2.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/postgresql/homework2.sql b/test/postgresql/homework2.sql index e194d83..3df145e 100644 --- a/test/postgresql/homework2.sql +++ b/test/postgresql/homework2.sql @@ -1 +1 @@ -select * from users1; \ No newline at end of file +select * from users; \ No newline at end of file From 83c172e5465157f4fe053726e6c6343ba640dea3 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 7 Mar 2026 11:01:55 +0300 Subject: [PATCH 059/179] first DDL --- .../DDL_create_first_tables.sql | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql diff --git a/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql new file mode 100644 index 0000000..8d6f3e2 --- /dev/null +++ b/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql @@ -0,0 +1,47 @@ +-- ПРАКТИКА DDL +-- Задание 1 +DROP TABLE IF EXISTS mnoskov.users; +CREATE TABLE mnoskov.users ( + id bigint PRIMARY KEY, + name varchar(100) NOT NULL, + age SMALLINT, + email varchar(100), + country char(2), -- прогуглил что распространено сохранять iso-код а не все название + is_active boolean, + created_at timestamptz NOT NULL -- думаю timestamptz, если не учитывать часовой пояс, то даты могут отображаться неверно +); + + +-- Задание 2 +DROP TABLE IF EXISTS mnoskov.products; +CREATE TABLE mnoskov.products ( + id bigint PRIMARY KEY, + name varchar(100) NOT NULL, + category varchar(50), + price decimal(10,2) NOT NULL, + in_stock boolean +); + + +-- Задание 3 +DROP TABLE IF EXISTS mnoskov.orders; +CREATE TABLE mnoskov.orders ( + id bigint PRIMARY KEY, + user_id bigint, + order_date date NOT NULL, -- в названии date, и тип думаю date + status int, -- по идее отдельная таблица с тремя типами займет меньше места, чем каждый раз прописывать в varchar статус + FOREIGN KEY (user_id) REFERENCES users(id) +); + + +-- Задание 4 +DROP TABLE IF EXISTS mnoskov.order_items; +CREATE TABLE mnoskov.order_items ( + order_id bigint, + product_id bigint, + quantity int, -- думал надо smallint но потом подумал что вдруг 50к гвоздей + price_at_purchase decimal(10,2), + PRIMARY KEY (order_id, product_id), + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); \ No newline at end of file From d13281f9cba2d60d217a86ca83077ef26ed2ac25 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 7 Mar 2026 12:17:44 +0300 Subject: [PATCH 060/179] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20first=20DDl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql index 8d6f3e2..0c6d42b 100644 --- a/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql +++ b/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql @@ -29,7 +29,7 @@ CREATE TABLE mnoskov.orders ( id bigint PRIMARY KEY, user_id bigint, order_date date NOT NULL, -- в названии date, и тип думаю date - status int, -- по идее отдельная таблица с тремя типами займет меньше места, чем каждый раз прописывать в varchar статус + status varchar(20), -- думал лучше int, но судя по заданию DML лучше varchar FOREIGN KEY (user_id) REFERENCES users(id) ); From 68d4c8b02e43b63251bfcce5663e46dbc33f1cfc Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 7 Mar 2026 12:34:29 +0300 Subject: [PATCH 061/179] first insert --- .../DML_first_inserts_in_tables.sql | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql diff --git a/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql new file mode 100644 index 0000000..7c3e15f --- /dev/null +++ b/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql @@ -0,0 +1,50 @@ +-- Задание 1 +INSERT INTO mnoskov.users (id, name, age, email, country, is_active, created_at) +VALUES +(1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'), +(2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'), +(3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'), +(4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'), +(5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'), +(6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'), +(7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'), +(8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'), +(9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'), +(10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00'); + +-- Задание 2 +INSERT INTO mnoskov.products (id, name, category, price, in_stock) +VALUES +(1, 'iPhone 15', 'Электроника', 80000, true), +(2, 'Kindle', 'Электроника', 15000, true), +(3, 'Кроссовки Nike', 'Одежда', 9000, true), +(4, 'Кроссовки NoName', 'Одежда', 3000, false), +(5, 'Наушники Pro', 'Электроника', 12000, true), +(6, 'Фитнес-браслет', 'Гаджеты', 6000, true), +(7, 'Подарочная карта', 'Сервисы', 1000, true); + +-- Задание 3 +INSERT INTO mnoskov.orders (id, user_id, order_date, status) +VALUES +(1, 1, '2025-01-10 12:00:00', 'paid'), +(2, 3, '2025-01-12 09:30:00', 'cancelled'), +(3, 1, '2025-01-15 18:20:00', 'paid'), +(4, 2, '2025-01-20 11:05:00', 'created'), +(5, 4, '2025-02-01 17:45:00', 'paid'), +(6, 5, '2025-02-10 10:10:00', 'refunded'), +(7, 7, '2025-03-01 09:00:00', 'paid'), +(8, 9, '2025-03-05 19:30:00', 'paid'); + +-- Задание 4 +INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) +VALUES +(1, 1, 1, 78000), +(1, 3, 2, 8500), +(2, 2, 1, 14000), +(3, 5, 1, 11000), +(3, 7, 1, 1000), +(4, 2, 1, 15000), +(5, 6, 1, 6000), +(6, 1, 1, 82000), +(7, 3, 1, 9000), +(8, 4, 2, 2800); \ No newline at end of file From 51295e9a4f4f9d76b83635a64c7586d6210daf58 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 7 Mar 2026 13:42:20 +0300 Subject: [PATCH 062/179] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=BA=D1=83=20=D0=BF=D0=BE?= =?UTF-8?q?=20'=D0=91=D0=B0=D0=B7=D0=BE=D0=B2=D1=8B=D0=B9=20SQL'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Базовый_SQL/DQL_basic_SQL_practice.sql | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 mnoskov/postgresql/Основы_sql/Базовый_SQL/DQL_basic_SQL_practice.sql diff --git a/mnoskov/postgresql/Основы_sql/Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/Основы_sql/Базовый_SQL/DQL_basic_SQL_practice.sql new file mode 100644 index 0000000..641bfa7 --- /dev/null +++ b/mnoskov/postgresql/Основы_sql/Базовый_SQL/DQL_basic_SQL_practice.sql @@ -0,0 +1,59 @@ +-- Задание 1 +SELECT + id, + name, + country, + created_at, + email +FROM mnoskov.users +ORDER BY created_at DESC, name; +-- выбрал эти столбцы потому что так сказано в задании, я бы добавил ещё is_active +-- такая сортировка для того, чтобы даты шли от новых к старым и имена людей за одну дату шли по алфавиту + + +-- Задание 2 +SELECT + id, + name, + country +FROM mnoskov.users +WHERE country = 'US' +ORDER BY name; + +SELECT + id, + name, + country +FROM mnoskov.users +WHERE country = 'UK' +ORDER BY name; +-- для маркетолого полезнее первый запрос т.к. там больше данных + + +-- Задание 3 +SELECT + id, + name, + email +FROM mnoskov.users +WHERE email IS NOT NULL +ORDER BY name; + +SELECT DISTINCT + lower(email) AS email_lower +FROM mnoskov.users +WHERE email IS NOT NULL; +-- приводить email к одному регистру мб для красоты, вроде везде email принимается и в нижем и в верхнем регистре +-- дубликаты убираем чтобы вытрина занимала меньше места, и чтобы не было повторных рассылок + + +-- Задание 4 +SELECT + id, + name, + created_at +FROM mnoskov.users +WHERE created_at > '2024-02-20 23:59' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; +-- OFFSET может пригодиться, чтобы взять интервал данных \ No newline at end of file From 936be276bbd4402ec8816362d5903fd2e936aa21 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 7 Mar 2026 15:20:23 +0300 Subject: [PATCH 063/179] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=BA=D1=83=20=D0=BF=D0=BE?= =?UTF-8?q?=20'=D0=A4=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Фильтрация/DQL_filtration_practice.sql | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 mnoskov/postgresql/Основы_sql/Фильтрация/DQL_filtration_practice.sql diff --git a/mnoskov/postgresql/Основы_sql/Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/Основы_sql/Фильтрация/DQL_filtration_practice.sql new file mode 100644 index 0000000..df898a7 --- /dev/null +++ b/mnoskov/postgresql/Основы_sql/Фильтрация/DQL_filtration_practice.sql @@ -0,0 +1,78 @@ +-- Задание 1 +SELECT + name, + category, + price, + price * 1.30 AS price_with_markup, + price * 1.50 AS price_with_vat +FROM + mnoskov.products +WHERE + in_stock = TRUE AND + category != 'smartwatch' AND -- категории smartwatch нет в предоставленных данных для заполнения + price BETWEEN 100 AND 1300; +-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? +-- Лучше читается первый вариант, потому что просто прочитал и понял, а не думаешь над знаками, и первый вариант короче + + +-- Задание 2 +SELECT + name, + price, + price * 0.85 AS price_with_discount +FROM + mnoskov.products +WHERE + (price < 100 OR price > 1000); +-- Нужно отделять OR скобками, потому что если в условии будет ещё и AND, то логика может поломаться потому что AND выполняется раньше чем OR + + +-- Задание 3 +SELECT + id, + name, + category, + price +FROM + mnoskov.products +WHERE + category IN ('tablet', 'smartphone', 'mouse') AND + price >= 100 +ORDER BY + price DESC; + + +--Задание 4 +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name NOT LIKE '%iPhone%'; + + +-- Задание 5 +-- 1) +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name ILIKE '%pro%'; + +-- 2) +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name ILIKE '%mx%' +ORDER BY + price DESC; +-- Для поисковой строи лучше ILIKE, потому что он не учитывает регистр - кто-то пишет с большой буквы, кто-то с маленькой, а кто-то случайно включил капс. \ No newline at end of file From 221ca37da69a8d9300147c0e5ab16fb3a3527b9a Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 8 Mar 2026 18:21:19 +0300 Subject: [PATCH 064/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=BE=20JOIN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mnoskov/postgresql/JOIN/DQL_join_practice.sql | 188 ++++++++++++++++++ .../Базовый_SQL/DQL_basic_SQL_practice.sql | 0 .../DDL_create_first_tables.sql | 0 .../DML_first_inserts_in_tables.sql | 0 .../Фильтрация/DQL_filtration_practice.sql | 0 5 files changed, 188 insertions(+) create mode 100644 mnoskov/postgresql/JOIN/DQL_join_practice.sql rename mnoskov/postgresql/{Основы_sql => Основы}/Базовый_SQL/DQL_basic_SQL_practice.sql (100%) rename mnoskov/postgresql/{Основы_sql => Основы}/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql (100%) rename mnoskov/postgresql/{Основы_sql => Основы}/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql (100%) rename mnoskov/postgresql/{Основы_sql => Основы}/Фильтрация/DQL_filtration_practice.sql (100%) diff --git a/mnoskov/postgresql/JOIN/DQL_join_practice.sql b/mnoskov/postgresql/JOIN/DQL_join_practice.sql new file mode 100644 index 0000000..8ba0e40 --- /dev/null +++ b/mnoskov/postgresql/JOIN/DQL_join_practice.sql @@ -0,0 +1,188 @@ +-- Задание 1 +SELECT + o.id AS order_id, + o.order_date, + o.status, + u.name AS user_name, + u.country +FROM + mnoskov.orders o +LEFT JOIN + mnoskov.users u ON o.user_id = u.id; +-- Логичнее начать с orders, тк надо вывести пользователей по заказам, а не наоборот. +-- Здесть уместнее LEFT, потому что в условии сказано ВСЕ заказы и уже по ним пользователей. Попробовал и INNER и LEFT - вывод один и тот же. + + +-- Задание 2 +SELECT + oi.order_id, + p.name AS prouct_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +WHERE + oi.order_id = 1; -- если 101, как в условии то ничего не будет +-- Если фильтрацию поставить после ON, то выведутся все продукты по 2 раза, у всех id=1, quantity и price возьмутсы из двух строк в order_items где id=1. + + +-- Задание 3 +SELECT + oi.order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Заказ с id=3 повторяется два раза, потому что в заказе два товара, и они отображаются на двух строках. +-- Поскольку мы выводис все заказы, у нас больше строк по сравнению с заданием-2, и ещё мы выводим имена заказчиков. + + +--Задание 4 +SELECT + u.name AS user_name, + o.id AS order_id, + o.status +FROM + mnoskov.users u +LEFT JOIN + mnoskov.orders o ON u.id = o.user_id; +-- Этот запрос отличается от INNER тем, что тут отображаются ещё и пользователи без заказов. + + +-- Задание 5 +SELECT + u.id AS user_id, + u.name AS user_name +FROM + mnoskov.users u +LEFT JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.user_id IS NULL; +-- Важно фильтровать по колонке из orders, потому что она будет NULL из-за LEFT JOIN. Если есть несостыковки, то NULL будет в правой таблице после LEFT JOIN. + + +-- Задание 6 +SELECT + oi.order_id, + u.id AS user_name, + p.name AS product_name, + oi.quantity +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE + p.name LIKE 'Kindle'; +-- Заказ 2 встречается 1 раз, потому что там всего один товар, и потому что мы ищем именно товар с названием - Kindle. + + +-- Задание 7 +SELECT + p.name AS product_name, + oi.order_id, + oi.quantity +FROM + mnoskov.products p +LEFT JOIN + mnoskov.order_items oi ON p.id = oi.product_id; +-- Если товар не покупали то в order_id будет NULL +-- Если использовать INNER, то выведутся только товары у которых есть заказы, в случае с LEFT выведутся все заказы. + + +-- Задание 8 +SELECT + oi.order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE + o.status = 'paid'; +-- Если написать status = 'paid' после ON в orders, то ничего не поменяется, потому что после ON пишутся условия соединения таблиц также как и в WHERE, но как я понял лучше писать в WHERE. + + +-- Задание 9 +SELECT + oi.order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +JOIN + mnoskov.order_items oi2 ON oi.product_id = oi2.product_id +JOIN + mnoskov.orders o2 ON oi2.order_id = o2.id +JOIN + mnoskov.products p ON oi2.product_id = p.id +WHERE + u.name = 'Alice'; +-- Если у Alice не будет заказов то ничего не выведется. +-- Мы получается берем Alice -> идем до id товаров Alice -> идем обратно к заказам и выводим товары которые были у Alice. + +-- Мне подсказала нейронка как использовать SELF JOIN - я понимал что он нужен, но не понимал как именно им воспользоваться. +-- Только после такого запроса я разобрался как это работает: +/*SELECT + oi.order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase, + u2.name, + u2.id +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +JOIN + mnoskov.order_items oi2 ON oi.product_id = oi2.product_id +JOIN + mnoskov.orders o2 ON oi2.order_id = o2.id +JOIN + mnoskov.users u2 ON o2.user_id = u2.id +JOIN + mnoskov.products p ON oi2.product_id = p.id +WHERE + u.name = 'Alice';*/ + + +-- Задание 10 +SELECT + o.id AS order_id, + o.user_id, + o.status +FROM + mnoskov.orders o +LEFT JOIN + mnoskov.order_items oi ON o.id = oi.order_id; +-- Если написать INNER JOIN, то появятся только существующие заказы, которые есть в обеих таблицах. \ No newline at end of file diff --git a/mnoskov/postgresql/Основы_sql/Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/Основы/Базовый_SQL/DQL_basic_SQL_practice.sql similarity index 100% rename from mnoskov/postgresql/Основы_sql/Базовый_SQL/DQL_basic_SQL_practice.sql rename to mnoskov/postgresql/Основы/Базовый_SQL/DQL_basic_SQL_practice.sql diff --git a/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql similarity index 100% rename from mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql rename to mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql diff --git a/mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql similarity index 100% rename from mnoskov/postgresql/Основы_sql/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql rename to mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql diff --git a/mnoskov/postgresql/Основы_sql/Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/Основы/Фильтрация/DQL_filtration_practice.sql similarity index 100% rename from mnoskov/postgresql/Основы_sql/Фильтрация/DQL_filtration_practice.sql rename to mnoskov/postgresql/Основы/Фильтрация/DQL_filtration_practice.sql From 398e11f4d74aec77f7783ead17ab4693bef2190c Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 9 Mar 2026 21:07:03 +0300 Subject: [PATCH 065/179] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20DDL=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql index 0c6d42b..db89114 100644 --- a/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql +++ b/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql @@ -32,6 +32,8 @@ CREATE TABLE mnoskov.orders ( status varchar(20), -- думал лучше int, но судя по заданию DML лучше varchar FOREIGN KEY (user_id) REFERENCES users(id) ); +ALTER TABLE mnoskov.orders +ALTER COLUMN order_date TYPE timestamptz; -- Задание 4 From 53368945d1cdeac4f7d3ea307e650f00cf41b33d Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 9 Mar 2026 22:21:17 +0300 Subject: [PATCH 066/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20UNION?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../postgresql/UNION/DQL_union_practice.sql | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 mnoskov/postgresql/UNION/DQL_union_practice.sql diff --git a/mnoskov/postgresql/UNION/DQL_union_practice.sql b/mnoskov/postgresql/UNION/DQL_union_practice.sql new file mode 100644 index 0000000..7ff2b1c --- /dev/null +++ b/mnoskov/postgresql/UNION/DQL_union_practice.sql @@ -0,0 +1,75 @@ +-- Задание 1 +SELECT + id, + name, + 'active' AS user_status +FROM + mnoskov.users +WHERE + is_active = TRUE + +UNION ALL + +SELECT + id, + name, + 'inactive' AS user_status +FROM + mnoskov.users +WHERE + is_active = FALSE +ORDER BY name; +-- Если написать просто UNION, то в данном задании ничего не изменится + + +-- Задание 2 +SELECT + id AS order_id, + user_id, + order_date, + 'old' AS period +FROM + mnoskov.orders +WHERE + order_date < '2025-02-01' + +UNION ALL + +SELECT + id AS order_id, + user_id, + order_date, + 'new' AS period +FROM + mnoskov.orders +WHERE + order_date >= '2025-02-01' +ORDER BY + order_date, order_id; +-- По колонке period я пойму старый(old) заказ или новый(new) +-- Я не могу ответить, потому что CASE WHEN ещё не проходили + + +-- Задание 3 +SELECT DISTINCT + oi.product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id + +UNION ALL + +SELECT DISTINCT + p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM + mnoskov.products p +LEFT JOIN + mnoskov.order_items oi ON p.id = oi.product_id +WHERE oi.product_id = NULL; +-- Проверка IS NULL по полю из order_items, потому что это права таблица, и null будут добавляться в неё +-- 1) Понизить цену товара 2) лучше прорекламировать 3) снять с продажи \ No newline at end of file From b52fa039323d0677f44bacd8bcc726b53cb88e53 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 10 Mar 2026 10:25:45 +0300 Subject: [PATCH 067/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=90=D0=B3=D1=80=D0=B5=D0=B3=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Базовый_SQL/DQL_basic_SQL_practice.sql | 0 .../DDL_create_first_tables.sql | 0 .../DML_first_inserts_in_tables.sql | 0 .../Фильтрация/DQL_filtration_practice.sql | 0 .../{JOIN => 2. JOIN}/DQL_join_practice.sql | 0 .../DQL_union_practice.sql | 0 .../DQL_aggregations&groupings_practice.sql | 130 ++++++++++++++++++ 7 files changed, 130 insertions(+) rename mnoskov/postgresql/{Основы => 1. Основы}/Базовый_SQL/DQL_basic_SQL_practice.sql (100%) rename mnoskov/postgresql/{Основы => 1. Основы}/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql (100%) rename mnoskov/postgresql/{Основы => 1. Основы}/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql (100%) rename mnoskov/postgresql/{Основы => 1. Основы}/Фильтрация/DQL_filtration_practice.sql (100%) rename mnoskov/postgresql/{JOIN => 2. JOIN}/DQL_join_practice.sql (100%) rename mnoskov/postgresql/{UNION => 3. UNION}/DQL_union_practice.sql (100%) create mode 100644 mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql diff --git a/mnoskov/postgresql/Основы/Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/1. Основы/Базовый_SQL/DQL_basic_SQL_practice.sql similarity index 100% rename from mnoskov/postgresql/Основы/Базовый_SQL/DQL_basic_SQL_practice.sql rename to mnoskov/postgresql/1. Основы/Базовый_SQL/DQL_basic_SQL_practice.sql diff --git a/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql similarity index 100% rename from mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql rename to mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql diff --git a/mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql similarity index 100% rename from mnoskov/postgresql/Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql rename to mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql diff --git a/mnoskov/postgresql/Основы/Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/1. Основы/Фильтрация/DQL_filtration_practice.sql similarity index 100% rename from mnoskov/postgresql/Основы/Фильтрация/DQL_filtration_practice.sql rename to mnoskov/postgresql/1. Основы/Фильтрация/DQL_filtration_practice.sql diff --git a/mnoskov/postgresql/JOIN/DQL_join_practice.sql b/mnoskov/postgresql/2. JOIN/DQL_join_practice.sql similarity index 100% rename from mnoskov/postgresql/JOIN/DQL_join_practice.sql rename to mnoskov/postgresql/2. JOIN/DQL_join_practice.sql diff --git a/mnoskov/postgresql/UNION/DQL_union_practice.sql b/mnoskov/postgresql/3. UNION/DQL_union_practice.sql similarity index 100% rename from mnoskov/postgresql/UNION/DQL_union_practice.sql rename to mnoskov/postgresql/3. UNION/DQL_union_practice.sql diff --git a/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql b/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql new file mode 100644 index 0000000..28ee152 --- /dev/null +++ b/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql @@ -0,0 +1,130 @@ +-- Задание 1 +SELECT + category, + count(*) products_count +FROM + mnoskov.products +GROUP BY + category +ORDER BY + products_count; +-- Если убрать order by, то статистика будет в случайном разрбосанном виде, а не по возрастанию +-- Увеличится products_count на одно значение + + +-- Задание 2 +SELECT + order_id, + sum(quantity) AS total_quantity, + sum(quantity * price_at_purchase) AS order_revenue +FROM + mnoskov.order_items +GROUP BY + order_id +ORDER BY + order_revenue DESC; +-- Если убрать GROUP BY, то будет ошибка +-- Нельзя просто добавить в SELECT ещё одну колонку (например, product_id) без добавления её в GROUP BY или агрегат, потому что вылезет ошибка. Ну и колонка должна быть либо сгруппирована либо в агрегате, она не может существовать сама по себе в запросе с grouup by + + +-- Задание 3 +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.category +ORDER BY + category_revenue DESC; +-- Группировка по p.category - потому что мы хотим узнать сумму прибли по каждой категории +-- Если добавить p.name - будет ошибка потому что p.name тоже нужно будет заносить в GROUP BY + + +-- Задание 4 +SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + oi.order_id +ORDER BY + order_revenue DESC; +-- В задаче-2 были все заказы, а тут только оплаченные +-- Я бы смотрел толлько на paid, чтобы понять прибыль которая уже есть + + +-- Задание 5 +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.category +HAVING + sum(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY + category_revenue DESC; +-- Если добавить это условие в WHERE, то терминал будет ругаться мол агрегаты недопустимы в WHERE +-- Обычно условие(прим: WHERE price > 1000) отличается от агрегированного условия(прим: HAVING SUM(...) > 1000) тем, что в первом случае ничего считать не нужно, значение уже есть, а во втором нужно сначала чтобы функция все посчитала. Ну и HAVING запускается уже после группировки + + +-- Задание 6 +SELECT + country, + count(*) AS active_user_count +FROM + mnoskov.users +WHERE + is_active = true +GROUP BY + country +HAVING + count(*) >= 2 +ORDER BY + active_user_count DESC; +-- is_active = true логичнее писать в HAVING, потому что is_active - не агрегат +-- Если нет страны где больше двух активных юзеров, то ничего не выведется + + +-- Задание 7 +SELECT + p.name AS product_name, + sum(oi.quantity) AS total_quantity_sold +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.name +ORDER BY + total_quantity_sold DESC +LIMIT 3; +-- total_quantity_sold - количество проданного товара, а product_revenue - выручка каждого товара +-- Бизнес сморит на количество, может быть когда товара очень много и нужно понять какой % уже продан, и когда нужно найти популярные или непопулярные товары + + +-- Задание 8 +SELECT + order_id, + sum(quantity * price_at_purchase) AS order_revenue +FROM + mnoskov.order_items +GROUP BY + order_id +HAVING + sum(quantity * price_at_purchase) < 10000 +ORDER BY + order_revenue; +-- Не понял вопрос: Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? +-- Проще увидеть глазами отфильтрованные микро-заказы \ No newline at end of file From 74745c2a125bb5c62e42e3f5ce6c235e494bf744 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 10 Mar 2026 13:58:26 +0300 Subject: [PATCH 068/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_functions_practice.sql | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql diff --git a/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql b/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql new file mode 100644 index 0000000..f37e1ef --- /dev/null +++ b/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql @@ -0,0 +1,90 @@ +-- Задание 1 +SELECT + name, + created_at::date, + current_date - created_at::date AS days_since_registration +FROM + mnoskov.users +WHERE + is_active = TRUE + AND current_date - created_at::date > 180 +ORDER BY + days_since_registration desc; +-- Очень старым пользователям можно кинуть рассылку на почту на подобии 'Представляете, мы вместе уже 777 дней, вот вам купон на скидку' +-- Удобнее вычитать ::date, чтобы days_since_registration красиво выглядел в таблице + + +-- Задание 2 +SELECT + name, + lower(email) AS email_clean, + substring(email FROM POSITION('@' IN email) + 1) AS domain +FROM + mnoskov.users +WHERE email IS NOT NULL +ORDER BY domain, name; +-- email приводят к нижнему регистру для нормализации записи, а зачем выделяют домен не понимаю, может быть для аналитики по типу кто какой почтой пользуется +-- Получу NULL в domain + + +-- Задание 3 +SELECT + name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + END AS age_group +FROM + mnoskov.users +ORDER BY + age; +-- Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? Я не понял этот вопрос. +-- Если забыть в ветку ELSE, то будет NULL у тех кто никуда не попал + + +-- Задание 4 +SELECT + name, + created_at::date, + current_date - created_at::date AS days_since_registration, + CASE + WHEN created_at >= current_date - INTERVAL '30 days' THEN 'hot' ELSE 'regular' + END AS hot_flag +FROM + mnoskov.users +WHERE + created_at >= current_date - INTERVAL '30 days'; +-- Фильтрация по дате в WHERE исключает неподходящие варианты, а простой CASE в нашем случае возвращает результат в виде двух вариантов - подошло/не подошло и эти оба варианта остаются в таблице +-- Стоит запускать какие нибудь привлекательные варианты со скидкам и акциями, чтобы удержать пользователся + + +-- Задание 5 +SELECT + count(*) AS total_users, + count(email) AS with_email, + count(*) - count(email) AS without_email, + ((count(*) - count(email))::float / count(*)) * 100.0 AS percent_without_email +FROM + mnoskov.users; +-- Агрегаты дают более точную информацию чем просто на глаз +-- Я бы предолжил запустить активно вылезающие блоки с предложением зарегестрироваться, или же запретить пользоваение продуктом без регистрации + + +-- Задание 6 +SELECT + name, + current_date - created_at::date AS days_since_registraion, + CASE + WHEN current_date - created_at::date <= 30 THEN 'new' + WHEN current_date - created_at::date BETWEEN 31 AND 180 THEN 'middle' + WHEN current_date - created_at::date > 180 THEN 'old' + END AS tenure_group, + is_active +FROM + mnoskov.users +ORDER BY + tenure_group, days_since_registraion; +-- Сегментация по стажу говорит о опыте человека, а по возрасту только о возрасте +-- Пересчитывать каждый день - плюсы: не занимает места на диске, минусы: тратит больше вычислительных ресурсов, а если хранить статическое поле, то все наоборот \ No newline at end of file From 2be43992c8e50c13783c4f5113515cd400dd6a0c Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 10 Mar 2026 17:23:31 +0300 Subject: [PATCH 069/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=9F=D0=BE=D0=B4=D0=B7=D0=B0=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D1=8B=20=D0=B8=20CTE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../6. Подзапросы и CTE/DQL_cte_practice.sql | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql diff --git a/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql b/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql new file mode 100644 index 0000000..3682f94 --- /dev/null +++ b/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql @@ -0,0 +1,155 @@ +-- Задание 1 +SELECT + name +FROM + mnoskov.users +WHERE id IN ( + SELECT user_id + FROM mnoskov.orders +) +ORDER BY + name; +-- Тут не приходится делать JOIN и DISTINCT, а просто ищем айдишник в списке +-- N (подзапрос) может быть более читаемым, когда подзапрос очень простой и короткий + + +-- Задание 2 +SELECT + u.id, + u.name, + u.email +FROM + mnoskov.users u +WHERE NOT EXISTS ( + SELECT user_id + FROM mnoskov.orders o + WHERE + u.id = o.user_id +) +-- Работает быстрее чем NOT IN +-- Определить какое количество людей не делают заказы, и кинуть им например купон на скидку на первый заказ + + +-- Задание 3 +SELECT + u.name, + ( + SELECT + count(*) + FROM + mnoskov.orders o + WHERE + u.id = o.user_id + ) AS orders_count +FROM + mnoskov.users u +ORDER BY + orders_count DESC, u.name; +-- Подзапрос в SELECT не может возвращать несколько строк, потому что SELECT ожидает толбко одну +-- Как изменится сложность запроса, если пользователей станет, например, 1 млн? Не совсем понял этот вопрос, но думаю запрос просто будет выполняться дольше + + +-- Задание 4 +WITH revenue_by_user AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id +) +SELECT + u.name, + rbu.total_revenue +FROM + mnoskov.users u +JOIN + revenue_by_user rbu ON u.id = rbu.user_id +ORDER BY + rbu.total_revenue desc; +-- Проще читать когда шаг «посчитать выручку по пользователям» вынесен отдельно +-- Чтобы избежать дублирования импользуется CTE, а не простой подзапрос + + +-- Задание 5 +WITH revenue_by_product AS ( + SELECT + product_id, + sum(quantity * price_at_purchase) AS product_revenue + FROM + mnoskov.order_items + GROUP BY + product_id +) +SELECT + p.name, + p.category, + rbp.product_revenue +FROM + mnoskov.products p +JOIN + revenue_by_product rbp ON p.id = rbp.product_id +ORDER BY + rbp.product_revenue +LIMIT 3; +-- Сначала удобнее считать агрегаты, чтобы код не выгляжел грооздким и чтобы в нем было легче разобраться +-- Отличае в удобстве чтения и написания. И этот агрегат можно потом ещё и переиспользовать + + +-- Задание 6 +WITH revenue_user AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.order_items oi + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + o.user_id +) +SELECT + u.name, + u.email, + ru.total_revenue +FROM + mnoskov.users u +JOIN + revenue_user ru ON u.id = ru.user_id +WHERE + u.is_active = TRUE AND + ru.total_revenue > 10000 +ORDER BY + ru.total_revenue DESC, u.name; +-- Я бы назвал этот сегмегт - frequent_buyers +-- Таким пользователям надо предлагать какую ниюудь специальную программу лояльности + + +-- Задание 7 +WITH revenue_order AS ( + SELECT + order_id, + sum(quantity * price_at_purchase) AS order_revenue + FROM + mnoskov.order_items + GROUP BY + order_id +) +SELECT + ro.order_id, + o.order_date::date, + ro.order_revenue +FROM + mnoskov.orders o +JOIN + revenue_order ro ON o.id = ro.order_id +WHERE + o.status = 'paid' AND + ro.order_revenue < 10000 +ORDER BY + ro.order_revenue; +-- Причины: злоупотребление промокодами, ошибки +-- По 'paid' удобнее фильтровать снаржуи, потому тут мы подключаем orders, а подключать orders ещё и внутри было бы бессмысленно \ No newline at end of file From 4b96e6012fe6d877b49d3d290b04ad5244ff7db9 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 11 Mar 2026 13:40:31 +0300 Subject: [PATCH 070/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=9E=D0=BA=D0=BE=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_window_functions_practice.sql | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 mnoskov/postgresql/7. Оконные функции/DQL_window_functions_practice.sql diff --git a/mnoskov/postgresql/7. Оконные функции/DQL_window_functions_practice.sql b/mnoskov/postgresql/7. Оконные функции/DQL_window_functions_practice.sql new file mode 100644 index 0000000..4d582be --- /dev/null +++ b/mnoskov/postgresql/7. Оконные функции/DQL_window_functions_practice.sql @@ -0,0 +1,168 @@ +-- Задание 1 +SELECT + o.user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date::date, + row_number() OVER (PARTITION BY o.user_id ORDER BY o.order_date) AS order_number_for_user +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Отличае от обычного order by в том, что мы не просто сортируем, а сортируем в пределах одного пользователя, и ещё выводим нумерацию заказов + + +-- Задание 2 +WITH order_total_revenue AS ( + SELECT + o.user_id, + oi.order_id, + o.order_date::date, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + mnoskov.order_items oi + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + o.user_id, oi.order_id, o.order_date +) +SELECT + otr.user_id, + u.name AS user_name, + otr.order_id, + otr.order_date::date, + otr.order_revenue, + sum(otr.order_revenue) OVER (PARTITION BY otr.user_id ORDER BY otr.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_revenue_for_user +FROM + mnoskov.users u +JOIN + order_total_revenue otr ON u.id = otr.user_id; +-- Если не задвать рамку, то могут пояиться проблемы с дубликатами +-- Если накопительная выручка пользователя становится больше, значит он начинает тратить больше + + +-- Задание 3 +WITH user_total_revenue AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id +) +SELECT + u.name AS user_name, + utr.total_revenue, + rank() OVER (ORDER BY utr.total_revenue) AS revenue_rank, + dense_rank() OVER (ORDER BY utr.total_revenue) AS revenue_dense_rank +FROM + mnoskov.users u +JOIN + user_total_revenue utr ON u.id = utr.user_id; +-- Когда появляется одинаковая выручка, dense_rank нумерует по порядку, без пропусков(1,2,2,3), а rank по 'расположению'(1,2,2,4) + + +-- Задание 4 +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date::date, + first_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date) AS first_order_date, + last_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order_date +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Первый заказ можно использовать для отчета 'как давно клиент начал активничать', а последний заказ, например если давно был последний заказ, то кинуть рассылку с промокодом, чтобы клиент снова начала заказывать как вариант +-- Если не задать рамку, то дата последнего заказа будет равна текущей строке, а не последней + + +-- Задание 5 +SELECT + u.name AS user_name, + o.id AS order_id, + count(*) OVER (PARTITION BY u.id) AS orders_per_user +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Если использовать просто group by, то пользователи сожмутся в одну ячейку + + +-- Задание 6 +WITH order_total_revenue AS ( + SELECT + o.user_id, + oi.order_id, + o.order_date::date, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id, oi.order_id, o.order_date +) +SELECT + u.name AS user_name, + otr.order_id, + otr.order_date::date, + otr.order_revenue, + lag(otr.order_revenue) OVER (PARTITION BY u.id ORDER BY otr.order_date) AS prev_order_revenue +FROM + mnoskov.users u +JOIN + order_total_revenue otr ON u.id = otr.user_id; +-- если текущее значение среднего чека больше чем предидущие, то увеличивается, а если наоборот, то уменьшается + + +-- Задание 7 +-- По месяцам: +WITH category_total_revenue AS ( + SELECT + p.category, + date_trunc('month', o.order_date)::date AS order_date_month, + sum(oi.quantity * oi.price_at_purchase) AS monthly_category_revenue + FROM + mnoskov.products p + JOIN + mnoskov.order_items oi ON p.id = oi.product_id + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + p.category, order_date_month +) +SELECT + category, + order_date_month, + monthly_category_revenue, + sum(monthly_category_revenue) OVER (PARTITION BY category ORDER BY order_date_month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue +FROM + category_total_revenue; + +-- По дням: +WITH category_total_revenue AS ( + SELECT + p.category, + o.order_date::date AS order_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM + mnoskov.products p + JOIN + mnoskov.order_items oi ON p.id = oi.product_id + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + p.category, order_date +) +SELECT + category, + order_date, + daily_category_revenue, + sum(daily_category_revenue) OVER (PARTITION BY category ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue +FROM + category_total_revenue; +-- По такому графику мы можем видеть РОСТ выручки по дням например, а не просто сколько в какой день принесла категория \ No newline at end of file From 2bb2b78dce785e563adf68c17a70821242f1870a Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 12 Mar 2026 23:14:26 +0300 Subject: [PATCH 071/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=9F=D1=80=D0=B5=D0=B4=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=BD=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_views_practice.sql | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 mnoskov/postgresql/8. Представления и витрины данных/DQL_views_practice.sql diff --git a/mnoskov/postgresql/8. Представления и витрины данных/DQL_views_practice.sql b/mnoskov/postgresql/8. Представления и витрины данных/DQL_views_practice.sql new file mode 100644 index 0000000..2e53170 --- /dev/null +++ b/mnoskov/postgresql/8. Представления и витрины данных/DQL_views_practice.sql @@ -0,0 +1,66 @@ +-- Задание 1 +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id +JOIN + mnoskov.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY + p.category +ORDER BY + total_revenue desc; +-- удобнее иметь wiev, чтобы небыло дублирования кода, и чтобы легко обращаться к запросу не переписывая все заново + + +-- Задание 2 +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.id AS user_id, + u.name AS user_name, + count(*) AS orders_count +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.status = 'paid' +GROUP BY + u.id, u.name +HAVING + count(*) > 0 +ORDER BY + orders_count desc; +-- Чем больше у пользователя заказов тем он более 'лояльный' +-- Тогда будет отображаться статистика только за последний месяц - 30 дней + + +-- Задание 3 +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + o.order_date +ORDER BY + sales_date; +-- Можно построить график, и видеть пики с просадками +-- Быстрее считать выручку из mv_daily_revenue, тк витрина уже готова, и её не нужно высчитывать + + +-- Задание 4 +REFRESH MATERIALIZED VIEW mv_daily_revenue; +-- 1. интернет‑магазин с несколькими заказами в день - каждые сутки в 8 утра например +-- 2. крупный магазин с тысячами заказов в час - каждые 30 минут +-- Слишком редкий REFRESH сделает витрину бесполезной для аналитиков +-- Обновлять каждую секунжу нельзя, тк это тратит ресурсы компьютера, и тогда уж проще просто VIEW сделать, и вызывать когда надо \ No newline at end of file From da6270bf90a64a70083298ea6c0c29a0cba6946a Mon Sep 17 00:00:00 2001 From: mnoskov Date: Fri, 13 Mar 2026 10:39:23 +0300 Subject: [PATCH 072/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A2=D1=80=D0=B0=D0=BD=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D0=B8'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DDL_create_first_tables.sql | 0 .../DML_first_inserts_in_tables.sql | 0 .../DQL_basic_SQL_practice.sql | 0 .../DQL_filtration_practice.sql | 0 .../1. Транзакции/DQL_transactions_practice.sql | 26 +++++++++++++++++++ 5 files changed, 26 insertions(+) rename mnoskov/postgresql/1. Основы/{Создание_и_изменение_таблиц_и_данных => 1. Создание_и_изменение_таблиц_и_данных}/DDL_create_first_tables.sql (100%) rename mnoskov/postgresql/1. Основы/{Создание_и_изменение_таблиц_и_данных => 1. Создание_и_изменение_таблиц_и_данных}/DML_first_inserts_in_tables.sql (100%) rename mnoskov/postgresql/1. Основы/{Базовый_SQL => 2. Базовый_SQL}/DQL_basic_SQL_practice.sql (100%) rename mnoskov/postgresql/1. Основы/{Фильтрация => 3. Фильтрация}/DQL_filtration_practice.sql (100%) create mode 100644 mnoskov/postgresql/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql diff --git a/mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql rename to mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql diff --git a/mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql rename to mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql diff --git a/mnoskov/postgresql/1. Основы/Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/Базовый_SQL/DQL_basic_SQL_practice.sql rename to mnoskov/postgresql/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql diff --git a/mnoskov/postgresql/1. Основы/Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/1. Основы/3. Фильтрация/DQL_filtration_practice.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/Фильтрация/DQL_filtration_practice.sql rename to mnoskov/postgresql/1. Основы/3. Фильтрация/DQL_filtration_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql b/mnoskov/postgresql/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql new file mode 100644 index 0000000..c4a8939 --- /dev/null +++ b/mnoskov/postgresql/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql @@ -0,0 +1,26 @@ +-- Задание 1. Оформление закзаза +INSERT INTO mnoskov.orders (id, user_id, order_date, status) VALUES + (9, 10, '2026-03-13', 'paid'), + (10, 10, '2026-03-14', 'paid'); + +INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES + (9, 3, 2, 88000), + (10, 6, 1, 5900); + +UPDATE mnoskov.products +SET in_stock = FALSE +WHERE id IN(3, 6); +-- Если выполнятся не все шаги, то транзакция откатится и все останется как было +-- Потому что заказ если оформляется, то полностью, частично он выполняться не должен + + +-- Задание 2 +-- При таком сценарии все должно откатаиться к самому началу, ничего не сохранится +-- Если не делать rollback, то можно получить заказы без продуктов, или без стомости, или полностью оформленный заказ, но продуктов нет в наличии +-- Любые 'битые' заказы повлияют на отчеты и витрины + + +-- Задание 3 +-- При изменении цены товара в products, это действие могут начать выполнять два человека одновременное, и если это не будет транзакцией то получится непонятно что, а если как транзакция, то сначала запишется 1 результат, а потом 2, и не будет никаких ошибок +-- Если выполняетя как транзакция, то просто откатится назад и все будет супер +-- Тк база сама откатывает транзакции в случае надобности, это освобождает инженера от доли ответственности \ No newline at end of file From f4eaf9d6403210a5d768e6a36d606c8ec9385979 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Fri, 13 Mar 2026 12:18:33 +0300 Subject: [PATCH 073/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=91=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B8'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Блокировки/DQL_blocking_prectice.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 mnoskov/postgresql/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql b/mnoskov/postgresql/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql new file mode 100644 index 0000000..1cf1cbd --- /dev/null +++ b/mnoskov/postgresql/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql @@ -0,0 +1,21 @@ +-- Задание 1 +SELECT * FROM mnoskov.users WHERE id = 1; +-- Если две одновременные сессии выполнят этот запрос, то в обеих все выведется, и ошибок или очереди не бкдет +-- Потому что этот запрос только на чтение + + +-- Задание 2 +-- Первая сессия будет просто ждать окончания выполнения первой +-- Если разрешить обоимUPDATE пройти одновременно, то получится какая-то херня пологаю, они смешаются, или че-то такое +-- У Alice в итоге останется второй email, тк он выполнится последним. Порядок COMMIT важен, тк в последствии этого порядка будут выполняться запросы + + +-- Задание 3 +-- select выполнится тк просто читает, а update встанет в очередь, тк он попытается изменить 'забронированные данные' +-- FOR UPDATE помогает избежать ошибок когда происходит покупка товара или че то такое, когда идет процесс изменений в данных + + +-- Задание 4 +-- Дедлок возникает потому что каждая сессия заблокировал то, что нужно другой сессии. База отменит одну из сессий, чтобы прошла хоть какая-то +-- Чтобы дедлок не возникал, нужно обновления все производить в одном порядке +-- Если при массовых обновлениях не соблюдать порядок строк, то будет возникать жопа из дедлоков \ No newline at end of file From 508e077da3e386f57f932e8fea3452643f562c5a Mon Sep 17 00:00:00 2001 From: mnoskov Date: Fri, 13 Mar 2026 16:07:02 +0300 Subject: [PATCH 074/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BE=D0=B2'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../3. Оптимизатор запросов/DQL_optimizer_pracitce.sql | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 mnoskov/postgresql/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql diff --git a/mnoskov/postgresql/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql b/mnoskov/postgresql/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql new file mode 100644 index 0000000..ec5086f --- /dev/null +++ b/mnoskov/postgresql/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql @@ -0,0 +1,165 @@ +-- Задание 1 +-- Вариант А +EXPLAIN ANALYZE +SELECT name, age +FROM mnoskov.users +WHERE country = 'RU' AND age > 25; + +-- Вариант Б +EXPLAIN ANALYZE +SELECT name, age +FROM mnoskov.users +WHERE age > 25 AND country = 'RU'; +-- Выводят они один и тот же результат, потому что условия у них одинаковые +-- По скорости выполнения судя по планировщику тоже одинаковоы + + +-- Задание 2 +-- Вариант 1 +EXPLAIN ANALYZE +SELECT + u.name, + u.email, + o.order_date::date, + o.status +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE o.status = 'refunded'; + +-- Вариант 2 +EXPLAIN ANALYZE +SELECT + u.name, + u.email, + o.order_date::date, + o.status +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE o.status = 'refunded'; +-- Между первой и второй таблицей результаты одинаковые. Если в запросе нет никаких условий связанных с таблицами, то первой оптимизатор смотреть users, но если если условие по orders.status, то сначала он сканирует orders + + +-- Задание 3 +EXPLAIN ANALYZE +SELECT name, email +FROM mnoskov.users +WHERE country = 'RU'; +-- Тип сканирования: Seq Scan +-- Количество прочитанных строк: 1 +-- Время выполнения примерно: 0.023 ms(+-5) + + +-- Задание 4 +-- Запрос А +EXPLAIN ANALYZE +SELECT * +FROM mnoskov.users +WHERE country = 'RU'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, email +FROM mnoskov.users +WHERE country = 'RU'; +-- Поскольку данных в таблице мало у меня время выполнения одинаковое, но могу предположить, что второй выполнился быстрее, тк у него в селекте два поля, а не вся таблица + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, o.id, o.order_date +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + u.country = 'RU'; +-- Читать начала с users, тк мы в where пишем условие для users, и быстрее сначала отсеять ненужных юзеров + + +-- Задание 6 +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND LOWER(u.name) LIKE '%алекс%' +GROUP BY u.name; +-- Его замедляет LOWER(не надо использовать функции), и думаю что LIKE(лучше использовать просто =), и ещэ group by по имени - это дольше чем по первичному ключу(надо группировать по ПК) + + +-- Задание 7 +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU'; +-- users -> orders -> order_items -> products - читает в таком порядке, потому что в where условие по users +-- Если убрать where, то будет - order_items -> users -> orders -> products +-- Как ускорить не знаю, может индексов добавть, но это пока не проходил + +-- Задание 8 +-- Вариант А +EXPLAIN ANALYZE +SELECT name, price +FROM mnoskov.products +WHERE category = 'Электроника' + AND price > 50000; + +-- Вариант Б +EXPLAIN ANALYZE +SELECT * +FROM mnoskov.products +WHERE category = 'Электроника' + AND price > 50000; +-- Данных в таблицах мало, врем выполнения не оценить, но смотря просто со стороны понятно что выриант А будет быстрее, потому что у него пару колонок в селекте, а не вся таблица как в Б + + +-- Задание 9 +-- Вариант коллег +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; + +-- Мой вариант +SELECT + id, name, age, is_active +FROM + mnoskov.users +WHERE + created_at BETWEEN '2026-01-01' AND '2027-01-02' AND + country = 'RU' AND + age > 45; +-- Мой вариант лучше потому что: +-- нет функций и мат. вычислений в where, все приведено к самому легкочитаемому формату +-- нет звездочки в select, а только то, что нужно + + +-- Задание 10 +INSERT INTO users (id, name, age, email, country, created_at) +SELECT + g AS id, + 'user_' || g AS name, + 27 AS age, + 'user_' || g || '@example.com' AS email, + 'RU' AS country, + '2026-02-03' AS created_at +FROM generate_series(20001, 50000) AS g; -- GPT сделал запрос мне + +SELECT * FROM mnoskov.users WHERE age IS NOT NULL; + +-- Запрос 1 +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; + +-- Запрос 2 +EXPLAIN ANALYZE +SELECT name, email FROM users WHERE age > 25; +-- Ну чето все равно не видна разница во времени выполнения, но по логике должен быть быстрее второй запрос \ No newline at end of file From 50bb355a95b3b5bf951dad9612f5acfe405a4ef6 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 14 Mar 2026 14:09:08 +0300 Subject: [PATCH 075/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'EXPLAIN=20=D0=B8=20EXPLAIN=20ANALYZE'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_explain_analyze_practice.sql | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql b/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql new file mode 100644 index 0000000..9db5b58 --- /dev/null +++ b/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql @@ -0,0 +1,203 @@ +-- Задание 1 +EXPLAIN +SELECT + name, email +FROM + users +WHERE + id = 1; +-- Использует index scan +-- Index Cond: (id = 1) - это значит что условие id = 1, и оно выполняется по индексу, и что id - индекс +-- База использует индекс, потому что у нас по полю id создан индекс, и база выбирает наиболее быстрый пусть ввыполнения запроса - через индекс + + +-- Задание 2 +-- Запрос А +EXPLAIN +SELECT * FROM users WHERE id = 2; + +-- Запрос Б +EXPLAIN +SELECT * FROM users WHERE age > 25; +-- В первом запрос index scan, потому что у нас уловие по индексу - id, во втором запросы seq scan, потому что у поля age не индексов, и таблица сканируется медленее + + +-- Задание 3 +EXPLAIN ANALYZE +SELECT + name, price +FROM + products +WHERE + category = 'Электроника'; +-- Execution Time: 0.020 ms +-- Тип сканирования: Seq Scan +-- Ожидалось вернуть 1, а вернул 3 + + +-- Задание 4 +EXPLAIN ANALYZE +SELECT * FROM users WHERE country = 'RU'; +-- Без индекса - Execution Time: 6.769 ms +CREATE INDEX idx_country ON users(country); +-- С индексом - Execution Time: 2.076 ms +-- Сначала идет Bitmap Heap Scan on users - как я понял на этом этапе 'создается' таблица где country = 'RU', а потом Bitmap INdex Scan on idx_country - тут уже мы читам информацию из таблицы где все country = 'RU'. +-- Запрос стал быстрее примерно в три раза +-- Индекс помог, потому что база искала не среди всех данных, а среди 'проиндексированных' + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, o.order_date, o.status +FROM + users u +JOIN + orders o ON u.id = o.user_id +WHERE + u.country = 'RU'; +-- База исполщовала Nested Loop +-- Чтение началось с orders +-- Execution Time: 0.075 ms + + +-- Задание 6 +CREATE INDEX idx_email ON users(email); +EXPLAIN ANALYZE +SELECT + * +FROM + users +WHERE + lower(email) = 'user_28@example.com'; +-- База не использует индекс пока в where есть функция lower +-- Потому что в условии функция, а с ними индексы не в ладах как я понял +-- Чтобы индекс заработал, надо убрать функцию lower, и сравнивать email как есть +-- Если убрать lower, то Execution Time намного быстрее, прям очень намного быстрее + + +-- Задание 7 +CREATE INDEX idx_age ON users(age); +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM users WHERE age + 5 > 30; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; +-- Быстрее работает запрос Б, но че то он тоже не хочет индекс использовать, хотя я его создал +-- Первый запрос не может использовать индекс, потому что там в условии математическое вычисление, а почему второй запрос не использует индек - я хз +-- Чтобы исправить запрос надо убрать сложение, и просто написать age > 25, как в запросе Б + + +-- Задание 8 +CREATE INDEX idx_created_at ON users(created_at); +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE EXTRACT(YEAR FROM u.created_at) = 2026 +GROUP BY u.name; +-- Функция EXTRACT - это плохая идея, потому что это функция которой быть в where по возможности не должно +-- WHERE i.crated_at BETWEEN '2026-01-01' AND '2027-01-01' +-- Индекс не используется в текущеа виде + + +-- Задание 9 +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM products WHERE price > 50000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, price FROM products WHERE price > 50000; +-- Если будет много данных в таблице, то быстрее будет работать второй запрос +-- Второй эффективнее тк в селекте указаны определнные поля, а не все(*) +-- Разница будет особо замета при больших объемах данных + + +-- Задание 10 +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU' AND p.category = 'Электроника'; +-- Порядок чтения таблиц: order_items -> products -> orders -> users +-- Количество строк: order_items - 12, products - 3, orders - 1, users - 0 +-- Если у country и category будут индексы, то запрос ьудет выполняться побыстре, потому что будет Index Scan, а не Seq Scan + + +-- Задание 11 +-- До +EXPLAIN ANALYZE +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; +-- Execution Time: 0.219 ms + +-- После +EXPLAIN ANALYZE +SELECT * +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.name = 'иван' AND + o.order_date BETWEEN '2024-01-01' AND '2024-02-01' AND + O.order_date BETWEEN '2026-01-01' AND '2026-02-01' AND + u.age > 25; +-- Execution Time: 0.032 ms +-- Мой вариант лучще, потому что у меня нет функций и вычисление мешаюзих использовать индексы в условии, и оптимизатор может успешно использовать индексы + + +-- Задание 12 +-- Запрос 1 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + users u +JOIN + orders o ON u.id = o.user_id AND u.country = 'RU'; + +-- Запрос 2 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + orders o +WHERE EXISTS ( + SELECT + 1 + FROM + users u + WHERE + o.user_id = u.id AND + u.country = 'RU' +); + +-- Запрос 3 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + orders o +WHERE o.user_id IN ( + SELECT + u.id + FROM + users u + WHERE + u.country = 'RU' +); + +-- Запрос 1: Execution Time: 0.055 ms +-- Запрос 2: Execution Time: 0.051 ms +-- Запрос 3: Execution Time: 0.073 ms +-- Они все дют примерно одинаковый разультат, потому что мало данных в таблице. А планы выполнения у них у всех одинаковые, вот прям 1 в 1. Ну по логие 3 запрос самый медленный, а первые два шустрые \ No newline at end of file From fec0f8e103c170999e90e55ff351229cbbf4116f Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 15 Mar 2026 17:23:18 +0300 Subject: [PATCH 076/179] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=BA=D1=83?= =?UTF-8?q?=20'EXPLAIN=20=D0=B8=20EXPLAIN=20ANALYZE'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql b/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql index 9db5b58..d3c362e 100644 --- a/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql +++ b/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql @@ -124,8 +124,8 @@ JOIN orders o ON u.id = o.user_id JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id WHERE u.country = 'RU' AND p.category = 'Электроника'; --- Порядок чтения таблиц: order_items -> products -> orders -> users --- Количество строк: order_items - 12, products - 3, orders - 1, users - 0 +-- Порядок чтения таблиц: products -> order_items -> orders -> users +-- Количество строк: products - 3, order_items - 12, orders - 1, users - 0 -- Если у country и category будут индексы, то запрос ьудет выполняться побыстре, потому что будет Index Scan, а не Seq Scan From ed2b990a3653b45f57b3c9f33865ccf6c1500587 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 15 Mar 2026 20:26:54 +0300 Subject: [PATCH 077/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=9F=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_command_execution_order_practice.sql | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 mnoskov/postgresql/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql b/mnoskov/postgresql/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql new file mode 100644 index 0000000..55377f9 --- /dev/null +++ b/mnoskov/postgresql/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql @@ -0,0 +1,217 @@ +-- Задание 1 +EXPLAIN ANALYZE +SELECT country, count(*) AS users_count +FROM users +WHERE is_active = TRUE +GROUP BY country +HAVING count(*) > 1 +ORDER BY users_count DESC; +-- WHERE применится до GROUP BY +-- HAVING применится после GROUP BY +-- Использовать users_count в HAVING нельзя, а в ORDER BY можно + + +-- Задание 2 +EXPLAIN ANALYZE +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' AND o.status = 'paid' +ORDER BY o.order_date DESC; +-- Порядок выполнения по QUERY PLAN: +-- 1) сначала сканирутся таблица orders и выполняется фильтрация o.status = 'paid', +-- 2) потом сканируется таблица users по условию u.id = o.user_id, и выполняется вильтрация u.country = 'RU', +-- 3) далее Nested Loop соединяет orders и users, которые уже прошли фильтры, +-- 4) теперь выводится выполняется сортирвка o.order_date DESC + +-- Порядок выполнения по логике: +-- 1) сначала сканируем таблицу users и присоединяем orders по условию u.id = o.user_id +-- 2) потом фильтруем соединенные таблицы по u.country = 'RU' и o.status = 'paid' +-- 3) в SELECT выбираем поля которые хотим вывести - u.name, o.order_date, o.status +-- 4) сортируем выбранные поля по o.order_date по убыванию + + +-- Задание 3 +-- Запрос А +SELECT country, AVG(age) AS avg_age +FROM users +GROUP BY country +HAVING country = 'Россия'; +-- Работает медленно потому having фильтрет после группировки а не до + +-- Запрос Б +SELECT country, avg(age) AS avg_age +FROM users +WHERE country = 'RU' +GROUP BY country; +-- Этот вариант быстрее, тк он фильтрует строки до группировки, те группируется меньше строк а не все + + +-- Задание 4 +-- Запрос А +EXPLAIN ANALYZE +SELECT category, AVG(price) AS avg_price +FROM products +GROUP BY category +HAVING category = 'Электроника' AND AVG(price) > 20000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT category, AVG(price) AS avg_price +FROM products +WHERE category = 'Электроника' +GROUP BY category +HAVING AVG(price) > 20000; +-- Количество обработанных строк у запроса А: 4 +-- Количество обработанных строк у запроса Б: 4 +-- PostgreSQL походу оптимизирует даже хуйню написанную в запросе А, планы запросов А и Б одинаковые - 1 в 1, хотя по сути у запроса А должно быть больше строк + + +-- Задание 5 +-- Изначальный запрос +SELECT + country, + COUNT(*) AS users_count +FROM users +WHERE users_count > 1 +GROUP BY country +ORDER BY country; + +-- Исправленный запрос +SELECT + country, + COUNT(*) AS users_count +FROM users +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY country; + +-- Задание 6 +SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.is_active = true +GROUP BY u.country +HAVING COUNT(o.id) > 1 +ORDER BY orders_count DESC +LIMIT 3; +-- Полный порядок выполнения: +-- 1) FROM/JOIN - сканируется таблица users, и к ней присоединяется таблица orders по условию u.id = o.user_id +-- 2) WHERE - из двух соединенных таблиц фильтруются и остаются только строки где u.is_active = true +-- 3) GROUP BY - происходится группировка по u.country, те все строки с одной и той же страной(записью в столбце) сжимаются в одну +-- 4) HAVING - уже по сгруппированным строкам проводится фильтрация по COUNT(o.id) > 1, остются только те страны(строки) где кол-во пользователей больше одного +-- 5) SELECT - выбираются столбцы которые будут выведены: u.country, COUNT(o.id) AS orders_count, AVG(o.id) AS avg_order_id +-- 6) ORDER BY - выбранные в SELECT строки сортируются по orders_count по убыванию +-- 7) LIMIT - из отсортированных строк остаются только первые 3 + + +-- Задание 7 +-- Запрос А +EXPLAIN ANALYZE +SELECT category, COUNT(*) +FROM products +GROUP BY category +HAVING category = 'Электроника'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT category, COUNT(*) +FROM products +WHERE category = 'Электроника' +GROUP BY category; +-- по логике запрос Б должен быть быстрее тк он сначала филтрует строки а потом группирует уже отфильтрованное, +-- НО PostgreSql может оптимизровать и запрос А, и получается что выполняются оба запроса одинаково, сначала фильтрация а потом группировка +-- В обоих запросах на шаге 'Seq Scan on products' обрабатываются: 3 строки, а на 'GroupAggregate': 1 строка +-- Execution Time одно и тоже + + +-- Задание 8 +SELECT + u.name, + COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' +GROUP BY u.name +HAVING COUNT(o.id) >= 2 +ORDER BY orders_count DESC; +-- WHERE применяется между FROM/JOIN и GROUP BY +-- HAVING применяется между GROUP BY и SELECT +-- фильтрация по o.status в WHERE происходит группировки строк, а фильтрация по COUNT(o.id) в HAVING происходит уже после группирациюю, по самим сгруппированным строкам + + +-- Задание 9 +SELECT + u.name, + p.name AS product_name, + p.price +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' +ORDER BY p.price DESC +LIMIT 5; +-- ORDER BY применяется после SELECT, перед LIMIT/OFFSET(если есть) +-- LIMIT идет после GROUP BY, тк сначала нужно отфильтровать строки чтобы взять их кусок(5 строк) +-- если LIMIT(5) выполнить до сортировки, то это будет бесполезно, возьмутся просто первые 5 строк, и уже они будут сортироваться, и по итогу выйдет хуйня + + +-- Задание 10 +EXPLAIN ANALYZE +SELECT + category, + count(id) AS count_product_in_category, + avg(price) AS avg_category_price +FROM + products +GROUP BY + category +HAVING + count(id) >= 2 OR avg(price) > 15000 +ORDER BY + avg_category_price DESC +LIMIT 3; +-- 1) FROM - сканируем poducts +-- 2) GROUP BY - группируем по category +-- 3) HAVING - фильтруем по агрегированной функции count(id) +-- 4) SELECT - делаем выборку по столбцам category, count_product_in_category, avg_category_price +-- 5) ORDER BY - сортируем по avg_category_price по убыванию +-- 6) LIMIT - выбираем первые 3 строки из отсортированных +-- Ипсользовал HAVING тк по условию все фильтрации по агрегированным функциям +-- Выполнил с EXPLAIN ANALYZE, сначала читаетя porducts, потом делается фильтрации по count(id) >= 2 и avg(price) > 15000, далее sort, а потом Limit + + +-- Задание 11 +-- Запрос А +EXPLAIN ANALYZE +SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count +FROM users u +WHERE u.country = 'RU' +ORDER BY orders_count DESC; +-- Подзапрос в SELECT выполняется самым первым, до начала остального запроса, и называется в планировщие - SubPlan 1 +-- orders_count нельзя использовать в WHERE +-- orders_count можно использовать в ORDER BY + +-- Запрос Б(исправленный) +EXPLAIN ANALYZE +SELECT + u.name, + u.age, + count(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' +GROUP BY u.name, u.age +ORDER BY orders_count DESC; +-- Запрос Б(исправленный) - быстрее, тк подзапрос в SELECT делается для каждого пользователя заново, а JOIN всего один раз \ No newline at end of file From 240ccccba3e11e8175157310d2b54232d7643c2f Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 15 Mar 2026 21:56:20 +0300 Subject: [PATCH 078/179] =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=20=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86,=20JOIN,=20?= =?UTF-8?q?UNION,=20=D0=90=D0=B3=D1=80=D0=B5=D1=86=D0=B8=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8,=20=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8,=20CTE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DDL_create_first_tables.sql | 4 +--- .../postgresql/2. JOIN/DQL_join_practice.sql | 4 ++-- .../postgresql/3. UNION/DQL_union_practice.sql | 2 +- .../DQL_aggregations&groupings_practice.sql | 4 ++-- .../DQL_functions_practice.sql | 4 ++-- .../6. Подзапросы и CTE/DQL_cte_practice.sql | 18 +++++++++--------- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql index db89114..676ee2f 100644 --- a/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql +++ b/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql @@ -28,12 +28,10 @@ DROP TABLE IF EXISTS mnoskov.orders; CREATE TABLE mnoskov.orders ( id bigint PRIMARY KEY, user_id bigint, - order_date date NOT NULL, -- в названии date, и тип думаю date + order_date timestamptz NOT NULL, -- в названии date, и тип думаю date status varchar(20), -- думал лучше int, но судя по заданию DML лучше varchar FOREIGN KEY (user_id) REFERENCES users(id) ); -ALTER TABLE mnoskov.orders -ALTER COLUMN order_date TYPE timestamptz; -- Задание 4 diff --git a/mnoskov/postgresql/2. JOIN/DQL_join_practice.sql b/mnoskov/postgresql/2. JOIN/DQL_join_practice.sql index 8ba0e40..639da44 100644 --- a/mnoskov/postgresql/2. JOIN/DQL_join_practice.sql +++ b/mnoskov/postgresql/2. JOIN/DQL_join_practice.sql @@ -16,7 +16,7 @@ LEFT JOIN -- Задание 2 SELECT oi.order_id, - p.name AS prouct_name, + p.name AS product_name, oi.quantity, oi.price_at_purchase FROM @@ -75,7 +75,7 @@ WHERE -- Задание 6 SELECT oi.order_id, - u.id AS user_name, + u.id AS user_id, p.name AS product_name, oi.quantity FROM diff --git a/mnoskov/postgresql/3. UNION/DQL_union_practice.sql b/mnoskov/postgresql/3. UNION/DQL_union_practice.sql index 7ff2b1c..b284f5e 100644 --- a/mnoskov/postgresql/3. UNION/DQL_union_practice.sql +++ b/mnoskov/postgresql/3. UNION/DQL_union_practice.sql @@ -70,6 +70,6 @@ FROM mnoskov.products p LEFT JOIN mnoskov.order_items oi ON p.id = oi.product_id -WHERE oi.product_id = NULL; +WHERE oi.product_id is NULL; -- Проверка IS NULL по полю из order_items, потому что это права таблица, и null будут добавляться в неё -- 1) Понизить цену товара 2) лучше прорекламировать 3) снять с продажи \ No newline at end of file diff --git a/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql b/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql index 28ee152..1058267 100644 --- a/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql +++ b/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql @@ -93,7 +93,7 @@ HAVING count(*) >= 2 ORDER BY active_user_count DESC; --- is_active = true логичнее писать в HAVING, потому что is_active - не агрегат +-- is_active = true логичнее писать в WHERE, потому что is_active - не агрегат -- Если нет страны где больше двух активных юзеров, то ничего не выведется @@ -126,5 +126,5 @@ HAVING sum(quantity * price_at_purchase) < 10000 ORDER BY order_revenue; --- Не понял вопрос: Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? +-- Не понял вопрос: Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? Ответ: «доставка пробников/тестовой партии», «использование купонов/бонусов», «технический тест витрины» -- Проще увидеть глазами отфильтрованные микро-заказы \ No newline at end of file diff --git a/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql b/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql index f37e1ef..30d30fb 100644 --- a/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql +++ b/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql @@ -40,7 +40,7 @@ FROM mnoskov.users ORDER BY age; --- Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? Я не понял этот вопрос. +-- Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? В каком коде? Если в 'коде приложения' - это какая-то отдельная программа, то могут предположить, чтобы передавать в приложение уже отфильтрованные данные, чтобы задачи базы данных не выносить из базы. -- Если забыть в ветку ELSE, то будет NULL у тех кто никуда не попал @@ -50,7 +50,7 @@ SELECT created_at::date, current_date - created_at::date AS days_since_registration, CASE - WHEN created_at >= current_date - INTERVAL '30 days' THEN 'hot' ELSE 'regular' + WHEN created_at >= current_date - INTERVAL '30 days' THEN 'hot' ELSE NULL END AS hot_flag FROM mnoskov.users diff --git a/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql b/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql index 3682f94..cb6ad44 100644 --- a/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql +++ b/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql @@ -26,7 +26,7 @@ WHERE NOT EXISTS ( WHERE u.id = o.user_id ) --- Работает быстрее чем NOT IN +-- NOT EXISTS безопаснее в плане NULL, и часто планируется лучше чем NOT IN -- Определить какое количество людей не делают заказы, и кинуть им например купон на скидку на первый заказ @@ -50,7 +50,7 @@ ORDER BY -- Задание 4 -WITH revenue_by_user AS ( +WITH revenue_user AS ( SELECT o.user_id, sum(oi.quantity * oi.price_at_purchase) AS total_revenue @@ -63,19 +63,19 @@ WITH revenue_by_user AS ( ) SELECT u.name, - rbu.total_revenue + ru.total_revenue FROM mnoskov.users u JOIN - revenue_by_user rbu ON u.id = rbu.user_id + revenue_user ru ON u.id = ru.user_id ORDER BY - rbu.total_revenue desc; + ru.total_revenue desc; -- Проще читать когда шаг «посчитать выручку по пользователям» вынесен отдельно -- Чтобы избежать дублирования импользуется CTE, а не простой подзапрос -- Задание 5 -WITH revenue_by_product AS ( +WITH revenue_product AS ( SELECT product_id, sum(quantity * price_at_purchase) AS product_revenue @@ -87,13 +87,13 @@ WITH revenue_by_product AS ( SELECT p.name, p.category, - rbp.product_revenue + rp.product_revenue FROM mnoskov.products p JOIN - revenue_by_product rbp ON p.id = rbp.product_id + revenue_product rp ON p.id = rp.product_id ORDER BY - rbp.product_revenue + rp.product_revenue LIMIT 3; -- Сначала удобнее считать агрегаты, чтобы код не выгляжел грооздким и чтобы в нем было легче разобраться -- Отличае в удобстве чтения и написания. И этот агрегат можно потом ещё и переиспользовать From 3f307b95acf233d76a8e11b7efee6c7d724ef304 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 16 Mar 2026 22:40:53 +0300 Subject: [PATCH 079/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'JOIN=20(=D1=84=D0=B8=D0=B7=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B5)'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../6. JOIN (физические)/DQL_physical_JOIN_practice.sql | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 mnoskov/postgresql/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql b/mnoskov/postgresql/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql new file mode 100644 index 0000000..79fc169 --- /dev/null +++ b/mnoskov/postgresql/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql @@ -0,0 +1,260 @@ +-- Задание 1 +-- Ситуация -- Какой JOIN выберет база? -- Почему? +-------------------------------------------------------------------------------------- +-- 10 пользователей, 1 млн заказов,| Nested | Маленькая таблица и большая с индексом. | +-- есть индекс на orders.user_id | Loop | База будет искать совпадения из | +-- | | маленькой по индексу из большой. | +-------------------------------------------------------------------------------------- +-- 100 000 пользователей, | Hash | Таблицы среднего размера, нет индексов. | +-- 100 000 заказов, нет индексов | Join | База построит хэш-таблицу одной из них, | +-- | | и будет искать совпадения в другой. | +-------------------------------------------------------------------------------------- +-- Обе таблицы отсортированы по | Merge | Потому что таблицы отсортированы по | +-- ключу, есть индексы | Join | ключу. Будет происходить соединение - | +-- | | 'Одновременное&последовательное'. | +-------------------------------------------------------------------------------------- +-- Маленькая таблица (5 строк) | Nested | База будет искать совпадения из | +-- соединяется с большой (1 млн), | Loop | маленькой в большой. Но тк нет индексов | +-- нет индекса | | - это будет медленно. | +-- |--------| Я засомневался и разобрал с ГПТ: | +-- | Hash | Nested Loop будет дорого(без индексов - | +-- | Join | 5.000.000 проврок), поэтому скорее всего| +-- | | Hash Join, просто построится хэш-таблица| +-- | | по маленькой и 5 раз отсканирует большую| +-------------------------------------------------------------------------------------- +-- Я заебался строить эту таблицу из пробелов. Хуй когда решусь на это снова. | +-------------------------------------------------------------------------------------- + + +-- Задание 2 +-- Маленькая таблица (10 строк) +CREATE TABLE mnoskov.small_users AS +SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + +-- Большая таблица (100 000 строк) +CREATE TABLE mnoskov.big_orders AS +SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; + +-- Вариант 1: Без индекса +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; + +-- Вариант 2: С индексом +CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); +--DROP INDEX idx_big_orders_user_id; + +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +-- Вариант 1: Merge Join - Потому что обе таблицы отсортированы с самого создания +-- Варинат 2: Тоже Merge Join +-- База могла бы выбрать Nested Loop, потому что у нас одна таблица маленькая а другая большая и с индексом, но выбрала Merge Join, потому что таблицы отсортированы +-- С индексом Execution Time: 31.579 ms, а без примерно 80.000 ms + + +-- Задание 3 +-- Вариант 1: Без ORDER BY +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: С ORDER BY по ключу соединения +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; + +-- Вариант 3: С ORDER BY по другому полю +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY o.order_date; +-- Во втором варианте также как и в первом - Nested Loop +-- Merge Join не появился +-- ORDER BY может повлиять на выбор JOIN, тк если есть стортировка, то база может выбрать Merge Join + + +-- Задание 4 +EXPLAIN ANALYZE +SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU'; +-- (u.id = o.user_id) - Nested Loop; (o.id = oi.order_id) - Nested Loop; (oi.product_id = p.id) - Hash Join +-- Seq Scan на этапах сканирования orders и products +-- Думаю стоит создать индекс на orders.user_id; и надо было бы на users.countyr, но я создавал уже в предидущих укроках; подумал, что надо ещё products.id, но там id и так первичный ключ, значит и индекс есть +CREATE INDEX idx_orders_user_id ON orders(user_id); -- DROP INDEX idx_orders_user_id; +-- После создания индекса типы JOIN не поменялись, план остался тем же, скорость выполнения запроса та же + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +LEFT JOIN order_items oi ON o.id = oi.order_id +WHERE u.country = 'RU' +GROUP BY u.name; +-- на этапе LEFT JOIN order_items oi ON o.id = oi.order_id - оптимизатор выполнил Hash Right Join по условию oi.order_id = o.id, то есть Hash Join +-- на этапе LEFT JOIN orders o ON u.id = o.user_id - оптимизатор выполнил Hash Left Join по условию u.id = o.user_id, то есть тоже Hash Join +-- Метод по сути один и тот же - Hash Join, но отличаются Right и Left - я думаю это выбрано для лучшей оптимизации +-- Через Hash Right Join проходит - 12 строк +-- Через Hash Left Join проходит - 11281 строк + + +-- Задание 6 +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +-- Разный тип используется, в варианте А - Nested Loop, в варианте B - Hash Left Join +-- Да, у варанта B Execution Time в очень много раз больше +-- LEFT JOIN медленне, потому что он не использует индекс, а делает Seq Scan, те читает таблицы полностью, потому что подругому не получится выполнить LEFT JOIN + + +-- Задание 7 +-- Вариант 1: users JOIN orders +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: orders JOIN users +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM orders o +JOIN users u ON o.user_id = u.id; +-- Тип JOIN не изменился +-- Порядок чтения таблиц не изменился +-- Execution Time одинаковое +-- Оптимизатор выбрал один и тот же порядок потому что по сути это два одинаковых запроса, с одной и той же логикой, но написаны чуть чуть по разному, и оптимизатор в обоих случаях выполняет так, как быстрее. Почему порядок может быть разным, я не предполагаю. + + +-- Задание 8 +-- Запрос 1 +EXPLAIN ANALYZE +SELECT + * +FROM + orders o +JOIN + order_items oi ON o.id = oi.order_id +ORDER BY + o.id, oi.order_id; +-- Уверен такой запрос привел бы к Merge Join, если бы данных в таблицах было побольше + +-- Запрос 2(из задания 2) +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +-- Потому что обе таблицы уже отсортированы в момент создания + +-- Задание 9 +-- Nested Loop лучше Hash Join, когда есть индексы по полям фильтрации, когда одна таблица небольшая, а у второй есть индекс. Подглядел: когда нужно найти немного совпадений. +-- Hash Join лучше чем Nested Loop, когда нет индексов, когда таблицы среднего размера и больше, и когдау условие фильтрации '=' + +-- Оба запроса из задания 6: +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; +-- тут nested loop быстрее, тк соеденияется все по индексам + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +-- а тут быстрее merge join, тк по индексам соединить не получится, тут нужны обе полные таблицы, и создается по одной из них хэш-таблица + + +-- Задание 10 +-- Запрос A +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id +JOIN + mnoskov.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY + p.category +ORDER BY + total_revenue desc; +-- До: Execution Time: 0.309 ms +CREATE INDEX idx_orders_status ON orders(status); +-- После добавления индекса ничего не поменялось, план запроса остался тот же, скорее всего потому что мало данных, думаю будь их больше с индексом запрос был бы быстрее + +-- Запрос B +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.id AS user_id, + u.name AS user_name, + count(*) AS orders_count +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.status = 'paid' +GROUP BY + u.id, u.name +HAVING + count(*) > 0 +ORDER BY + orders_count desc; +-- И тут тоже несмотря на то что индекс на orders статус я уже создал, таблица orders скаринруется как Seq Scan, я думаю это потому, что мало данных, будь их больше с индексом запрос был бы быстрее + +-- Запрос C +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + o.order_date +ORDER BY + sales_date; +-- Тут ситуация аналогичная и для первый двух запросов, нужные индексы уже созданы, в связи выполнения всех предидущих занятий и тем, и единственное что я мог сделать для улучшения времени выполнения - это добавить индекс на orders(status) \ No newline at end of file From 69d49c501d1d1898b53969c6505e7843c799b3aa Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 17 Mar 2026 17:29:45 +0300 Subject: [PATCH 080/179] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?DDL=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DDL_create_first_tables.sql | 0 .../DML_first_inserts_in_tables.sql | 0 .../2. Базовый_SQL/DQL_basic_SQL_practice.sql | 0 .../3. Фильтрация/DQL_filtration_practice.sql | 0 .../2. JOIN/DQL_join_practice.sql | 0 .../3. UNION/DQL_union_practice.sql | 0 .../DQL_aggregations&groupings_practice.sql | 0 .../DQL_functions_practice.sql | 0 .../6. Подзапросы и CTE/DQL_cte_practice.sql | 0 .../DQL_window_functions_practice.sql | 0 .../DQL_views_practice.sql | 0 .../1. Транзакции/DQL_transactions_practice.sql | 0 .../2. Блокировки/DQL_blocking_prectice.sql | 0 .../3. Оптимизатор запросов/DQL_optimizer_pracitce.sql | 0 .../DQL_explain_analyze_practice.sql | 0 .../DQL_command_execution_order_practice.sql | 0 .../6. JOIN (физические)/DQL_physical_JOIN_practice.sql | 0 .../DDL_create_tables_for_pet_project.sql | 45 +++++++++++++++++++ 18 files changed, 45 insertions(+) rename mnoskov/postgresql/{ => SQL с нуля}/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/1. Основы/3. Фильтрация/DQL_filtration_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/2. JOIN/DQL_join_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/3. UNION/DQL_union_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/5. Функции: даты, строки, логика/DQL_functions_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/6. Подзапросы и CTE/DQL_cte_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/7. Оконные функции/DQL_window_functions_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/8. Представления и витрины данных/DQL_views_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql (100%) rename mnoskov/postgresql/{ => SQL с нуля}/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql (100%) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql diff --git a/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql rename to mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql diff --git a/mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql rename to mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql diff --git a/mnoskov/postgresql/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql rename to mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql diff --git a/mnoskov/postgresql/1. Основы/3. Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql similarity index 100% rename from mnoskov/postgresql/1. Основы/3. Фильтрация/DQL_filtration_practice.sql rename to mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql diff --git a/mnoskov/postgresql/2. JOIN/DQL_join_practice.sql b/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql similarity index 100% rename from mnoskov/postgresql/2. JOIN/DQL_join_practice.sql rename to mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql diff --git a/mnoskov/postgresql/3. UNION/DQL_union_practice.sql b/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql similarity index 100% rename from mnoskov/postgresql/3. UNION/DQL_union_practice.sql rename to mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql diff --git a/mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql b/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql similarity index 100% rename from mnoskov/postgresql/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql rename to mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql diff --git a/mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/5. Функции: даты, строки, логика/DQL_functions_practice.sql similarity index 100% rename from mnoskov/postgresql/5. Функции: даты, строки, логика/DQL_functions_practice.sql rename to mnoskov/postgresql/SQL с нуля/5. Функции: даты, строки, логика/DQL_functions_practice.sql diff --git a/mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql b/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql similarity index 100% rename from mnoskov/postgresql/6. Подзапросы и CTE/DQL_cte_practice.sql rename to mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql diff --git a/mnoskov/postgresql/7. Оконные функции/DQL_window_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql similarity index 100% rename from mnoskov/postgresql/7. Оконные функции/DQL_window_functions_practice.sql rename to mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql diff --git a/mnoskov/postgresql/8. Представления и витрины данных/DQL_views_practice.sql b/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql similarity index 100% rename from mnoskov/postgresql/8. Представления и витрины данных/DQL_views_practice.sql rename to mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql similarity index 100% rename from mnoskov/postgresql/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql rename to mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql similarity index 100% rename from mnoskov/postgresql/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql rename to mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql similarity index 100% rename from mnoskov/postgresql/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql rename to mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql diff --git a/mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql similarity index 100% rename from mnoskov/postgresql/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql rename to mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql similarity index 100% rename from mnoskov/postgresql/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql rename to mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql diff --git a/mnoskov/postgresql/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql similarity index 100% rename from mnoskov/postgresql/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql rename to mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql new file mode 100644 index 0000000..68c5a40 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql @@ -0,0 +1,45 @@ +--DROP TABLE IF EXISTS users; +CREATE TABLE users ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar(100) NOT NULL, + age SMALLINT, + email varchar(100), + registration_date timestamptz NOT NULL +); + +--DROP TABLE IF EXISTS courses; +CREATE TABLE courses ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + title varchar(150) NOT NULL, + category varchar(50), + created_at timestamptz NOT NULL +); + +--DROP TABLE IF EXISTS lessons; +CREATE TABLE lessons ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + course_id bigint NOT NULL, + title varchar(150) NOT NULL, + duration_min SMALLINT NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +--DROP TABLE IF EXISTS enrollments; +CREATE TABLE enrollments ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id bigint NOT NULL, + course_id bigint NOT NULL, + enrolled_at timestamptz NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +--DROP TABLE IF EXISTS lesson_views; +CREATE TABLE lesson_views ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id bigint NOT NULL, + lesson_id bigint NOT NULL, + viewed_at timestamptz NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (lesson_id) REFERENCES lessons(id) +); \ No newline at end of file From f9db7e0b8475cce90a451052e9ba5a5c8c9bd97b Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 17 Mar 2026 18:04:44 +0300 Subject: [PATCH 081/179] =?UTF-8?q?DML=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=82=20'=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE=D0=B5=D0=BA?= =?UTF-8?q?=D1=82:=20=D0=9E=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-=D0=BA=D1=83?= =?UTF-8?q?=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DML_insert_in_tables_for_pet_project.sql | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql new file mode 100644 index 0000000..7438241 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql @@ -0,0 +1,155 @@ +INSERT INTO users (name, age, email, registration_date) VALUES +('Alice',25,'alice@mail.com','2023-01-10'), +('Bob',30,'bob@gmail.com','2023-02-05'), +('Charlie',22,'charlie@mail.com','2023-02-20'), +('Diana',28,'diana@mail.com','2023-03-01'), +('Ethan',35,'ethan@gmail.com','2023-03-10'), +('Frank',29,'frank@mail.com','2023-01-05'), +('Grace',26,'grace@gmail.com','2023-01-12'), +('Henry',32,'henry@mail.com','2023-01-20'), +('Iris',24,'iris@gmail.com','2023-02-10'), +('Jack',27,'jack@mail.com','2023-04-15'), +('Kelly',23,'kelly@gmail.com','2023-04-20'), +('Leo',31,'leo@mail.com','2023-01-15'), +('Mia',26,'mia@gmail.com','2023-01-18'), +('Nina',28,'nina@mail.com','2023-01-08'), +('Oscar',33,'oscar@gmail.com','2023-01-10'), +('Paul',29,'paul@mail.com','2023-02-15'), +('Quinn',24,'quinn@gmail.com','2023-03-20'), +('Rita',27,'rita@mail.com','2023-04-01'), +('Sam',30,'sam@gmail.com','2023-04-10'), +('Tina',25,'tina@mail.com','2023-04-18'); + +INSERT INTO courses (title, category, created_at) VALUES +('SQL для начинающих','data','2023-01-01'), +('Python для анализа данных','programming','2023-01-15'), +('BI с нуля','business','2023-03-01'), +('Excel для аналитиков','business','2023-02-01'), +('Основы машинного обучения','data','2023-03-15'); + +INSERT INTO lessons (course_id, title, duration_min) VALUES +(1,'SELECT и FROM',10), +(1,'JOIN',15), +(1,'WHERE и фильтрация',12), +(1,'Агрегации и GROUP BY',18), +(1,'Подзапросы',25), +(2,'Pandas основы',20), +(2,'NumPy',15), +(2,'Визуализация данных',22), +(3,'Основы BI',12), +(3,'Метрики и дашборды',18), +(3,'Power BI введение',20), +(4,'Формулы в Excel',15), +(4,'Сводные таблицы',18), +(4,'Графики и диаграммы',14), +(5,'Введение в ML',20), +(5,'Линейная регрессия',30), +(5,'Классификация',28), +(5,'Кластеризация',25); + +INSERT INTO enrollments (user_id, course_id, enrolled_at) VALUES +(1,1,'2023-01-15'), +(1,2,'2023-02-01'), +(2,1,'2023-01-20'), +(3,2,'2023-03-05'), +(4,3,'2023-04-01'), +(6,1,'2023-01-06'), +(6,2,'2023-01-10'), +(6,4,'2023-02-05'), +(7,1,'2023-01-13'), +(7,3,'2023-03-10'), +(8,1,'2023-01-22'), +(8,2,'2023-01-22'), +(8,3,'2023-01-22'), +(9,4,'2023-02-12'), +(10,1,'2023-04-16'), +(11,2,'2023-04-21'), +(12,1,'2023-01-16'), +(13,2,'2023-01-19'), +(13,4,'2023-02-20'), +(14,1,'2023-01-09'), +(14,2,'2023-01-09'), +(14,3,'2023-01-09'), +(14,4,'2023-02-01'), +(14,5,'2023-03-20'), +(15,1,'2023-01-11'), +(15,3,'2023-03-05'), +(16,2,'2023-02-16'), +(16,4,'2023-02-16'), +(17,3,'2023-03-22'), +(17,5,'2023-03-25'), +(18,1,'2023-04-02'), +(19,1,'2023-04-11'), +(19,2,'2023-04-11'), +(20,2,'2023-04-19'), +(5,5,'2023-03-20'), +(1,4,'2023-02-10'), +(2,3,'2023-03-15'), +(3,1,'2023-02-25'), +(4,2,'2023-03-10'), +(6,3,'2023-03-08'), +(7,2,'2023-02-01'), +(9,1,'2023-02-15'), +(12,3,'2023-03-10'), +(15,2,'2023-02-01'), +(18,4,'2023-04-05'); + +INSERT INTO lesson_views (user_id, lesson_id, viewed_at) VALUES +(1,1,'2023-01-16 10:00:00'), +(1,2,'2023-01-16 10:15:00'), +(1,3,'2023-01-17 09:00:00'), +(1,4,'2023-01-17 09:30:00'), +(1,4,'2023-01-17 11:00:00'), +(1,4,'2023-01-18 10:00:00'), +(1,6,'2023-02-02 14:00:00'), +(1,12,'2023-02-15 10:00:00'), +(1,13,'2023-02-15 10:30:00'), +(2,1,'2023-01-21 09:00:00'), +(2,9,'2023-03-16 10:00:00'), +(2,10,'2023-03-16 10:30:00'), +(3,6,'2023-03-06 12:00:00'), +(3,1,'2023-02-26 11:00:00'), +(3,2,'2023-02-26 11:20:00'), +(4,9,'2023-04-02 10:00:00'), +(4,10,'2023-04-03 10:00:00'), +(4,6,'2023-03-12 14:00:00'), +(4,7,'2023-03-13 09:00:00'), +(5,15,'2023-03-21 10:00:00'), +(5,16,'2023-03-22 11:00:00'), +(6,1,'2023-01-07 10:00:00'), +(6,2,'2023-01-07 10:15:00'), +(6,3,'2023-01-08 09:00:00'), +(6,4,'2023-01-08 09:30:00'), +(6,5,'2023-01-09 10:00:00'), +(6,6,'2023-01-11 14:00:00'), +(6,7,'2023-01-12 10:00:00'), +(6,8,'2023-01-13 11:00:00'), +(6,12,'2023-02-06 09:00:00'), +(6,13,'2023-02-06 09:30:00'), +(6,14,'2023-02-07 10:00:00'), +(6,9,'2023-03-09 11:00:00'), +(7,1,'2023-01-14 10:00:00'), +(7,2,'2023-01-14 10:20:00'), +(7,3,'2023-01-15 09:00:00'), +(7,9,'2023-03-11 14:00:00'), +(7,10,'2023-03-11 14:30:00'), +(9,12,'2023-02-13 10:00:00'), +(9,13,'2023-02-13 10:30:00'), +(9,1,'2023-02-16 11:00:00'), +(12,1,'2023-01-17 10:00:00'), +(12,2,'2023-01-17 10:20:00'), +(12,1,'2023-04-10 11:00:00'), +(12,3,'2023-04-10 11:30:00'), +(12,9,'2023-03-12 10:00:00'), +(13,6,'2023-01-20 14:00:00'), +(13,7,'2023-01-21 10:00:00'), +(13,12,'2023-02-21 09:00:00'), +(14,1,'2023-01-09 08:00:00'), +(14,2,'2023-01-09 08:15:00'), +(14,3,'2023-01-10 10:00:00'), +(14,4,'2023-01-10 10:30:00'), +(14,5,'2023-01-11 09:00:00'), +(14,6,'2023-01-10 14:00:00'), +(14,7,'2023-01-11 10:00:00'), +(14,8,'2023-01-12 11:00:00'), +(14,9,'2023-01-10 08:00:00'); \ No newline at end of file From 9a3e660595319995e72a97c949fd8ffc54d21f29 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 17 Mar 2026 22:52:04 +0300 Subject: [PATCH 082/179] =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=83=20'=D0=90=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_materialized_view_user_activity_summary.sql | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql new file mode 100644 index 0000000..46cfb4d --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql @@ -0,0 +1,24 @@ +CREATE MATERIALIZED VIEW user_activity_summary AS +SELECT + u.id AS user_id, + u.name, + count(DISTINCT e.course_id) AS enrollment_count, + count(DISTINCT lv.lesson_id) AS viewed_lessons_count, + max(lv.viewed_at)::date AS last_view, + min(lv.viewed_at)::date AS first_view, + ((count(DISTINCT lv.lesson_id)::numeric(5,2) / count(DISTINCT l.id)) * 100)::numeric(5, 2) AS engagement_rate +FROM + users u +JOIN + enrollments e ON u.id = e.user_id +JOIN + courses c ON e.course_id = c.id +JOIN + lessons l ON c.id = l.course_id +LEFT JOIN + lesson_views lv ON u.id = lv.user_id -- сюда - 'AND l.id = lv.lesson_id' +GROUP BY + u.id, u.name +ORDER BY + u.id; +-- Кинул его в нейронку на проверку, она сказала что надо добавить 'AND l.id = lv.lesson_id' при присоединении таблицы lesson_views(сделал пометку в коде), это нужно чтобы точно проверить, что урок который посмотрел пользователь точно принадлежит его курсу, чтобы избежать ошибок из-за некорректных данные если такие появятся в результате какого либо сбоя или ошибки. \ No newline at end of file From 3b9c6393df33abc30fbbfcc009770a18cea394c9 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 18 Mar 2026 13:14:45 +0300 Subject: [PATCH 083/179] =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20'=D0=AD=D1=84=D1=84=D0=B5=D0=BA=D1=82=D0=B8=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BA=D1=83=D1=80=D1=81=D0=BE?= =?UTF-8?q?=D0=B2'=20=D0=B4=D0=BB=D1=8F=20'=D0=9F=D0=B5=D1=82-=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20=D0=9E=D0=BD=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD-=D0=BA=D1=83=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_materialized_view_courses_effectivity.sql | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql new file mode 100644 index 0000000..4e670fc --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql @@ -0,0 +1,41 @@ +CREATE MATERIALIZED VIEW course_summary AS +WITH courses_unique_users AS ( + SELECT + e.course_id, + count(e.user_id) AS unique_users + FROM + enrollments e + GROUP BY + e.course_id +), +lessons_results AS ( + SELECT + l.course_id, + count(distinct l.id) AS lessons_count, + count(lv.lesson_id) AS total_views, + min(lv.viewed_at)::date AS first_viewed_at, + max(lv.viewed_at)::date AS last_viewed_at + FROM + lessons l + LEFT JOIN + lesson_views lv ON l.id = lv.lesson_id + GROUP BY + l.course_id +) +SELECT + c.id AS course_id, + c.title AS course_title, + cuu.unique_users, + lr.lessons_count, + lr.total_views, + round(lr.total_views / cuu.unique_users::numeric, 2) AS avg_views_per_user, + lr.first_viewed_at, + lr.last_viewed_at +FROM + courses c +JOIN + courses_unique_users cuu ON c.id = cuu.course_id +JOIN + lessons_results lr ON c.id = lr.course_id +ORDER BY + c.id; \ No newline at end of file From c5381da2481e094f9ad1dd821d0b2c2d5e0f5bcd Mon Sep 17 00:00:00 2001 From: mnoskov Date: Fri, 20 Mar 2026 12:56:57 +0300 Subject: [PATCH 084/179] =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20'=D0=9E=D0=B1=D1=89=D0=B0=D1=8F=20=D0=BE=D1=82=D1=87?= =?UTF-8?q?=D0=B5=D1=82=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D1=8E'=20=D0=B4=D0=BB=D1=8F=20'=D0=9F=D0=B5?= =?UTF-8?q?=D1=82-=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82:=20=D0=9E=D0=BD?= =?UTF-8?q?=D0=BB=D0=B0=D0=B9=D0=BD-=D0=BA=D1=83=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_materialized_view_reporting_on_interactions.sql | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql new file mode 100644 index 0000000..32fbe79 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql @@ -0,0 +1,48 @@ +CREATE MATERIALIZED VIEW platform_summary as +WITH users_count AS ( + SELECT + count(*) AS total_users + FROM + users +), +courses_count AS ( + SELECT + count(*) AS total_courses + FROM + courses +), +lessons_count AS ( + SELECT + count(*) AS total_lessons + FROM + lessons +), +users_with_views AS ( + SELECT + count(distinct user_id) AS users_with_views + FROM + lesson_views +), +views_count AS ( + SELECT + count(*) AS total_views + FROM + lesson_views lv +) +SELECT + uc.total_users, + cc.total_courses, + lc.total_lessons, + uwv.users_with_views, + round(lc.total_lessons / cc.total_courses::NUMERIC, 2) AS avg_lessons_per_course, + round(vc.total_views / lc.total_lessons::NUMERIC, 2) AS avg_views_per_lesson +FROM + users_count uc +CROSS JOIN + courses_count cc +CROSS JOIN + lessons_count lc +CROSS JOIN + users_with_views uwv +CROSS JOIN + views_count AS vc; \ No newline at end of file From 3e09a2cbc769d5e80a64f750cb482848722337cb Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 21 Mar 2026 19:49:00 +0300 Subject: [PATCH 085/179] =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20'=D0=9A=D0=BE=D0=B3=D0=BE=D1=80=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9'=20=D0=B4=D0=BB=D1=8F=20'=D0=9F=D0=B5=D1=82-=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20=D0=9E=D0=BD=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD-=D0=BA=D1=83=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_materialized_view_cohort_analysis_of_users.sql | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql new file mode 100644 index 0000000..19b8668 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql @@ -0,0 +1,40 @@ +CREATE MATERIALIZED VIEW user_cohort_analysis AS +WITH users_and_enrollments_results AS ( + SELECT + date_trunc('month', u.registration_date)::date AS cohort_month, + count(DISTINCT u.id) AS users_in_cohort, + count(DISTINCT e.user_id) AS enrolled_users, + count(DISTINCT e.course_id) courses_in_cohort + FROM + users u + JOIN + enrollments e ON u.id = e.user_id + GROUP BY + cohort_month +), +lesson_views_results AS ( + SELECT + date_trunc('month', u.registration_date)::date AS cohort_month, + count(DISTINCT lv.user_id) AS active_users, + round(avg(lv.viewed_at::date - u.registration_date::date), 0) AS avg_time_to_first_view + FROM + users u + LEFT JOIN + lesson_views lv ON u.id = lv.user_id + GROUP BY + cohort_month +) +SELECT + uer.cohort_month, + uer.users_in_cohort, + uer.enrolled_users, + lvr.active_users, + round((lvr.active_users / uer.users_in_cohort::numeric) * 100, 2) || '%' AS retention_rate, + round(uer.courses_in_cohort / uer.users_in_cohort::numeric, 2) AS avg_courses_per_user, + lvr.avg_time_to_first_view +FROM + users_and_enrollments_results uer +JOIN + lesson_views_results lvr ON uer.cohort_month = lvr.cohort_month +ORDER BY + uer.cohort_month; \ No newline at end of file From f7e6b757dfe3b7e664e61ff168dba4b0ddd7bd6e Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 21 Mar 2026 20:49:34 +0300 Subject: [PATCH 086/179] =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20'=D0=9F=D0=BE=D0=BF=D1=83=D0=BB=D1=8F=D1=80=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BA=D1=83=D1=80=D1=81=D0=BE=D0=B2?= =?UTF-8?q?'=20=D0=B4=D0=BB=D1=8F=20'=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20=D0=9E=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_materialized_view_courses_popularity.sql | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql new file mode 100644 index 0000000..3377eb5 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql @@ -0,0 +1,39 @@ +CREATE MATERIALIZED VIEW lesson_popularity_ranking as +WITH lesson_views_results AS ( + SELECT + lv.lesson_id, + count(lv.user_id) AS total_views, + count(DISTINCT lv.user_id) AS unique_viewers + FROM + lesson_views lv + GROUP BY + lv.lesson_id +), +enrollments_results AS ( + SELECT + e.course_id, + count(e.user_id) AS enrolled_to_course + FROM + enrollments e + GROUP BY + e.course_id +) +SELECT + l.id AS lesson_id, + l.title AS lesson_title, + c.title AS course_title, + lvr.total_views, + lvr.unique_viewers, + round(lvr.total_views / lvr.unique_viewers::numeric, 2) AS avg_views_per_viewer, + dense_rank() OVER (ORDER BY lvr.total_views desc) AS popularity_rank, + round((lvr.unique_viewers / er.enrolled_to_course::numeric) * 100, 0) || '%' AS completion_rate +FROM + lessons l +JOIN + courses c ON l.course_id = c.id +JOIN + lesson_views_results lvr ON l.id = lvr.lesson_id +JOIN + enrollments_results er ON c.id = er.course_id +ORDER BY + popularity_rank; \ No newline at end of file From ce82641efbca80929de1f86beaf85a7f7452ad46 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 22 Mar 2026 15:08:45 +0300 Subject: [PATCH 087/179] =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=20'=D0=92=D0=BE=D1=80=D0=BE=D0=BD=D0=BA=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8'=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20'=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82:=20=D0=9E=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-=D0=BA?= =?UTF-8?q?=D1=83=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_materialized_view_conversion_funnel.sql | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql new file mode 100644 index 0000000..6b208c0 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql @@ -0,0 +1,114 @@ +WITH stage_courses AS ( + -- registered + (SELECT + 0 AS course_id, + 1 AS stage_id, + 'registered' AS stage_name, + count(DISTINCT u.id) AS users_count + FROM + users u) + UNION ALL + -- enrolled + (SELECT + e.course_id, + 2 AS stage_id, + 'enrolled' AS stage_name, + count(DISTINCT e.user_id) AS users_count + FROM + enrollments e + GROUP BY + e.course_id + ORDER BY + e.course_id) + UNION ALL + -- started + (SELECT + l.course_id, + 3 AS stage_id, + 'started' AS stage_name, + count(DISTINCT lv.user_id) AS users_count + FROM + lessons l + JOIN + lesson_views lv ON l.id = lv.lesson_id + GROUP BY + l.course_id + ORDER BY + l.course_id) + UNION ALL + -- mid_progress + (WITH lessons_mid AS ( + SELECT + l.course_id, + ceil(percentile_cont(0.5) WITHIN GROUP (ORDER BY l.id)) AS mid_in_lesson_id + FROM + lessons l + GROUP BY + l.course_id + ) + SELECT + lm.course_id, + 4 AS stage_id, + 'mid_progress' AS stage_name, + count(lv.user_id) AS users_count + FROM + lessons_mid lm + LEFT JOIN + lesson_views lv ON lm.mid_in_lesson_id = lv.lesson_id + GROUP BY + lm.course_id, lm.mid_in_lesson_id + ORDER BY + lm.course_id) + UNION ALL + -- completed + (WITH courses_lessons_id AS ( + SELECT + l.course_id, + l.id AS lesson_id + FROM + lessons l + GROUP BY + l.course_id, l.id + ), + lessons_numbers AS ( + SELECT + cli.course_id, + cli.lesson_id, + ROW_NUMBER() OVER(PARTITION BY cli.course_id ORDER BY cli.lesson_id DESC) AS lesson_number + FROM + courses_lessons_id cli + ) + SELECT + ln.course_id, + 5 AS stage_id, + 'completed' AS stage_name, + count(lv.user_id) AS users_count + FROM + lessons_numbers ln + LEFT JOIN + lesson_views lv ON ln.lesson_id = lv.lesson_id + WHERE + ln.lesson_number = 1 + GROUP BY + ln.course_id, ln.lesson_id + ORDER BY + ln.course_id) +) +SELECT + sc.course_id, + c.title AS course_title, + sc.stage_id, + sc.stage_name, + sc.users_count, + CASE + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NULL THEN 'first stage' + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) = 0 THEN 0::text || '%' + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NOT NULL THEN round((sc.users_count / lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' + END AS conversion_from_previous, + round((sc.users_count / FIRST_VALUE(sc.users_count) OVER (ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' AS conversion_from_start +FROM + courses c +RIGHT JOIN + stage_courses sc ON c.id = sc.course_id +ORDER BY + sc.course_id \ No newline at end of file From d9c76de8833bf31b2569c8ef5a1121488de71c04 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 22 Mar 2026 15:49:29 +0300 Subject: [PATCH 088/179] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=83=20'=D0=92?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=BD=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BD=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D0=B8'=20=D0=B4=D0=BB=D1=8F=20'=D0=9F?= =?UTF-8?q?=D0=B5=D1=82-=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82:=20=D0=9E?= =?UTF-8?q?=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-=D0=BA=D1=83=D1=80=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql index 6b208c0..bade9b5 100644 --- a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql @@ -97,7 +97,6 @@ WITH stage_courses AS ( SELECT sc.course_id, c.title AS course_title, - sc.stage_id, sc.stage_name, sc.users_count, CASE From ac156fae0f70c384e32465cbd21f8feb3a5ab480 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 22 Mar 2026 18:02:49 +0300 Subject: [PATCH 089/179] =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=83=20'?= =?UTF-8?q?=D0=92=D0=BE=D1=80=D0=BE=D0=BD=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8'=20=D0=B4=D0=BB=D1=8F=20'?= =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82:=20?= =?UTF-8?q?=D0=9E=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-=D0=BA=D1=83=D1=80=D1=81?= =?UTF-8?q?=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql index bade9b5..9800c9f 100644 --- a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql @@ -1,3 +1,4 @@ +CREATE MATERIALIZED VIEW conversion_funnel AS WITH stage_courses AS ( -- registered (SELECT @@ -110,4 +111,4 @@ FROM RIGHT JOIN stage_courses sc ON c.id = sc.course_id ORDER BY - sc.course_id \ No newline at end of file + sc.course_id; \ No newline at end of file From cbaf0b1f4ad093eac6eb0e81f565b375066db777 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 22 Mar 2026 19:59:09 +0300 Subject: [PATCH 090/179] =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B2=D1=81=D0=B5=20=D0=94=D0=BE=D0=BF.=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../3. Доп. задания/DQL_additional_tasks.sql | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 mnoskov/postgresql/Пет-проект: Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql new file mode 100644 index 0000000..49c296d --- /dev/null +++ b/mnoskov/postgresql/Пет-проект: Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql @@ -0,0 +1,51 @@ +-- Задание 1 +SELECT + * +FROM + conversion_funnel +WHERE + stage_name = 'started' +ORDER BY + conversion_from_previous; +-- Первые 2 курса, гже отавлилось больше всего учеников между этапами enrolled → started + + +-- Задание 2 +SELECT + * +FROM + lesson_popularity_ranking lpr +WHERE + lpr.avg_views_per_viewer > 1 +LIMIT 3; + + +-- Задание 3 +SELECT + * +FROM + user_cohort_analysis uca +ORDER BY + retention_rate DESC; +-- Не разобрался как можжно соеденить с другими таблицам +-- А retention_rate такой, наверное потому что при запуске курсов была пиар компания большая, и делали рассылки с напоминанием позаниматься, а потом перестали, вот и удержание похерилось + + +-- Задание 4 +-- Витрина - user_activity_summary +-- Всяко разно попробовал подобавлять индексы, итог: без индексов быстрее. Просмотрел весь запрос, вроде ничего не нужно менять - лишнего ничего нет. + +-- Витрина - course_summary +-- Тут также как и в предидущем - и так и сяк попробовал, план запроса не меняется, время не улучшается + +-- Витрина - platform_summary +-- Тут тоже нечего менять - всё из CROSS JOIN. Попробовал вместо count(*) писать count(id) думал мол в ширину будет меньше читать, в итоге выяснил что и так и так одинаково читает + +-- Витрина - user_cohort_analysis +-- И тут че не делай, все одна фигня. План вроде хороший. + +-- Витрина - lesson_popularity_ranking +-- Тут тоже ничего не вышло - я начниаю чувствовать себя тупым от того что не могу улучшить нигде время запроса. + +-- Витрина - conversion_funnel +-- Здесь тоже ничего не вышло - со стороны план запроса выглядит хорошо. Ускорить время запроса не вышло. \ No newline at end of file From 783c87592556d0f84dc23b8a3f2f82768abf0b67 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Fri, 27 Mar 2026 15:47:16 +0300 Subject: [PATCH 091/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1. Индексы/practice_indexes.sql | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql new file mode 100644 index 0000000..d29f3e7 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql @@ -0,0 +1,111 @@ +/*Задание 1: По каким полям делать индексы(тут говорится смотреть на таблицы из пет проекта 'Онлайн магазины', надеюсь смотреть на таблицы а не на витрины): + 1) По каким полям, как тебе кажется, реальный продукт‑менеджер или аналитик будет чаще всего: + where: + users: age, email, registration_date, + enrollments: enrolled_at + courses: title, category + lessons: title, duration_min + lesson_views: viewed_at + order by: + users: id, age, registration_date + enrollments: user_id, enrolled_at + courses: id + lessons: id, duration_min + lesson_views: user_id, viewed_at + group by: + users: age, registration_date + enrollments: user_id, course_id, enrolled_at + courses: category + lessons: course_id + lesson_views: user_id, lesson_id + join: + users: id + enrollments: user_id, course_id + courses: id + lessons: id, course_id + lesson_views: user_id, lesson_id + 2) Для каждого поля, которое ты назвал, укажи: «индекс нужен / индекс не нужен» и коротко объясни почему (часто фильтруем, редко фильтруем, мало разных значений и т.п.). + users: + id(есть автоматически), + age(не нужен, мало разнах значений), + email(нужен, чтобы найти пользователя через where) + registration_date(нужен, часто будет сравниваться в BETWEEN наприме, и сортироваться) + enrollments: + user_id(нужен, будет часто джоиниться), + course_id(нужен, будет часто джоиниться), + enrolled_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться) + courses: + id(есть автоматически), + title(не нужен, мало разных значений), + category(не нужен, мало разных значений) + lessons: + id(есть автоматически), + course_id(нужен, будет часто джоиниться), + title(не нужен, мало разных значений), + duration_min(мне кажется не нужен, тк не очень часто будет использоваться) + lesson_views: + user_id(нужен, будет часто джоиниться), + lesson_id(нужен, будет часто джоиниться) + viewed_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться)*/ + +-- Задание 2: Свяжи запрос и индекс: + -- 1) + SELECT * FROM users WHERE email = 'какой-то email'; -- индекс на email ускорит where + -- 2) + SELECT * FROM users WHERE is_active = true AND created_at >= CURRENT_DATE - INTERVAL '1 year'; -- индекс на created_at ускоряет where + -- 3) + SELECT * FROM products WHERE category = 'Электроника' ORDER BY price; -- индекс по price ускорит order by + -- 4) + SELECT * FROM orders WHERE user_id = 1 AND order_date >= CURRENT_DATE - INTERVAL '30 days'; -- индекс на user_id и order_date ускорят where + -- 5) + SELECT order_id, product_id FROM order_items; -- на у тут по идее составной создается сразу, так как тут pkey составной. Ну и они ускоряют select + +/* Задание 3. Порядок столбцов в индексе + 1) Я бы сделал orders(order_date, user_id). + 2) Для первых двух будет работать четко, а для третьего хуже. Тк у третьего нет фильрации по order date, и он стоит вторым в индексе + 3) + * Когда один индекс - хорошо с фильтрацией по нескольким полям, и когда первое поле указанное в индексе используется часто, а второе нет. + * Когда индекса два - хорошо когда оба поля фильтруются по отдельности*/ + +/* Задание 4. Лишние индексы в наших таблицах + !!! Я тут не буду писать индексы которые создаются автоматически из за pkey + users: + Скорее нужен индекс: + email, created_at(нужны для where, и order by) + Скорее не нужен индекс: + name, age, country, is_active(либо мало рахных значений, либо не очень востребованы эти поля) + products: + Скорее нужен индекс: + price (для order by и where) + Скорее не нужен индекс: + name, ctegory, in_stock(либо мало разных значений, либо не очень востребованы эти поля) + orders: + Скорее нужен индекс: + user_id(для join), price(для order by и where) + Скорее не нужен индекс: + name, category, in_stock(либо мало разных значений, либо не очень востребованы эти поля) + order_items: + Скорее нужен индекс: + price_at_purchase (для order by и where) + Скорее не нужен индекс: + quantity(мало разных значения) + + Много индексов не есть хорошо, потому что они тоже занимают помять и их тоже нужно считать. Если их бездумно везде понатыкать то запрос может выполняться дольше.*/ + +/* Задание 5. Что должно быть уникальным + 1) id, email + 2) id, article_number(придумал вот такое) + 3) + * Если не будет уникаьности по id, то возникнут проблемы бд, что у разных пользователей будет один id и это жопа, ничего невозможно фильтровать и тд. + * Если email будет повторяться, то как вообще пользователи будут входить в аккаунт, и как делать персонализированную рассылку? Никак. + * Если article_number будет повторяться, то те же проблемы что и с id, будет нарушение логики работы бд. + + * Уникальный индекс обезопасит бд от сбоев и некорректной работы.*/ + +-- Задание 6. Где индексы помогут, а где нет + SELECT * FROM users WHERE id = 100; -- индекс уже создан тк это pkey + SELECT * FROM users WHERE email = 'user@example.com'; -- нужен, поможет с where, быстрее будет рабоать фильтрация + SELECT * FROM products WHERE price BETWEEN 100 AND 500; -- нужн, поможет с where, быстрее будте фильтрация + SELECT * FROM products WHERE name LIKE '%phone%'; -- не нужен, индекс не рвботает с like + SELECT * FROM orders WHERE status = 'DELIVERED'; -- не нужен, в status мало разных значений + SELECT * FROM order_items WHERE order_id = 123; -- нужен, поможет с where, быстрее будет рабоать фильтрация \ No newline at end of file From 6b1c388c710bec95dca1b33ae76b2eb37c7085df Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 29 Mar 2026 21:15:04 +0300 Subject: [PATCH 092/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=9F=D0=B0=D1=80=D1=82=D0=B8=D1=86=D0=B8?= =?UTF-8?q?=D0=B8'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Партиции/practice_partitions.sql | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql new file mode 100644 index 0000000..5445669 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql @@ -0,0 +1,190 @@ +/* Задание 1. Какой вид партиционирования выбрать + 1) + * range по полю order_date + * запросы с between в год + 1. where order_date between '2022-01-01' and '2023-01-01' + 2. where extract('year' from order_date) = '2025' + 3. where order_date < '2026-01-01' and order_date > '2025-01-01' + 2) + * тут два типа партиционирования подходят. range по created_at или list по country. + * если выбирать list по country то плюс - это при фильтрах по стране будут отсекаться лишние партиции, а минус, что год/месяц регистрации не будет разделен по партициям. + 3) + * логично range по created_at - чтобы разделить по месяцам/годам, а hash по user_id - почему так, точно сказать не могу, но в теории сказано что hash используется когда нет диапазона или списка и это частый сценарий для логов + * с range выиграют запросы с фильтрами по created_at, а с hash выигают запросы с фильтрами по user_id, а все что не указаны как поле для партиций, не почувствуют разницы.*/ + + +/* Задание 2. Размер партиций + 2) количество партициый за 10 лет по: + * годам - 10, тут вообще все четко, и удалять не сложно, и поддержка проще, и запросы будут хорошо делаться + * месяцам - 120, тут с удалением тарых данных труднее, поддержка тяжелая, но скорость запросов будет лучше, если фильтрация будет по месяцам + * дням - примерно 3650, а это я чувствую пиздец, с удалением и поддержкой жопа, и со скоростью запросов тоже жопа - данные будут читаться из кучи партиций что не есть хорошо + 3) + Я думаю прям для очень большого магазина оптимальнее будет партиционирование по месяцам - тк очень много покупателей и большинство статистик будет по месяцам, по годам тоэе конечно, но думаю приоритетнее по месяцам*/ + + +-- Задание 3. RANGE‑партиции по дате заказов +DROP TABLE IF EXISTS orders_part; +CREATE TABLE orders_part ( + id bigint, + user_id bigint, + order_date timestamptz NOT NULL, + status varchar(20), + PRIMARY KEY (id, order_date), + FOREIGN KEY (user_id) REFERENCES users_1(id) +) PARTITION BY RANGE (order_date); + +CREATE TABLE orders_2024 PARTITION OF orders_part +FOR VALUES FROM ('2024_01-01') TO ('2025-01-01'); + +CREATE TABLE orders_2025 PARTITION OF orders_part +FOR VALUES FROM ('2025_01-01') TO ('2026-01-01'); + +CREATE TABLE orders_2026 PARTITION OF orders_part +FOR VALUES FROM ('2026_01-01') TO ('2027-01-01'); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part +WHERE + order_date BETWEEN '2025-01-01' AND '2025-12-31 23:59:59'; -- пришлось сделать так, тк дата 2026-01-01 тоже есть в таблице +-- в этом запросе за год - читатется только одна партиция - orders_2025, НО если в условии все же написать AND '2026-01-01', то читаются две партиции orders_2025 и orders_2026 и время выполнения запроса намного больше тк выполняется full scan обеих таблиц + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part +WHERE + EXTRACT('year' FROM order_date) IN ('2024', '2025'); -- так читаются все три партиции, 2026 почему-то тоже захватывается, но по итогу после сканирование у таблицы orders_2026 - rows = 0 + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part; +-- так просто сканируются все три партиции, но без фильтров, как было в предидущем варианте + + +-- Задание 4. LIST‑партиции по стране пользователей +DROP TABLE IF EXISTS users_part; +CREATE TABLE users_part ( + id int8 NOT NULL, + "name" varchar(100) NOT NULL, + age int2 NULL, + email varchar(100) NULL, + country bpchar(2) NULL, + is_active bool NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, country) +) PARTITION BY LIST (country); + +CREATE TABLE users_ru PARTITION OF users_part +FOR VALUES IN ('RU'); +CREATE TABLE users_us PARTITION OF users_part +FOR VALUES IN ('US'); +CREATE TABLE users_gb PARTITION OF users_part +FOR VALUES IN ('GB'); +CREATE TABLE users_de PARTITION OF users_part +FOR VALUES IN ('DE'); +CREATE TABLE users_fr PARTITION OF users_part +FOR VALUES IN ('FR'); +CREATE TABLE users_jp PARTITION OF users_part +FOR VALUES IN ('JP'); +CREATE TABLE users_uk PARTITION OF users_part +FOR VALUES IN ('UK'); +CREATE TABLE users_kz PARTITION OF users_part +FOR VALUES IN ('KZ'); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + country = 'RU' +-- запрос скаинровал только партицию users_ru + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + country IN ('RU', 'KZ') +-- запрос сканировал только партиции users_ru и users_kz + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + age BETWEEN 44 AND 66; +-- сканировал не просто users_part, а каждую партицию по отдельности + + +-- Задание 5. HASH‑партиции по user_id в логах +DROP TABLE IF EXISTS user_logs_part; +CREATE TABLE user_logs_part ( + id int8, + user_id bigint NOT NULL, + created_at timestamp NOT null, + event_type text NOT NULL, + payload jsonb, + PRIMARY KEY (id, user_id) +) PARTITION BY hash (user_id) + +CREATE TABLE user_logs_p1 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 0); + +CREATE TABLE user_logs_p2 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 1); + +CREATE TABLE user_logs_p3 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 2); + +CREATE TABLE user_logs_p4 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 3); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT * FROM user_logs_part WHERE user_id = 228; +-- читает только user_logs_p2 + +EXPLAIN ANALYZE +SELECT * FROM user_logs_part WHERE created_at >= current_date - INTERVAL '7 days'; +-- читает все партиции + +-- Задание 6 +-- email, чтобы искать пользователя быстрее, created_at для сортировки и where +CREATE UNIQUE INDEX idx_users_ru_email ON users_ru(email); +CREATE INDEX idx_users_ru_created_at ON users_ru(created_at); + +CREATE UNIQUE INDEX idx_users_us_email ON users_us(email); +CREATE INDEX idx_users_us_created_at ON users_us(created_at); + +CREATE UNIQUE INDEX idx_users_gb_email ON users_gb(email); +CREATE INDEX idx_users_gb_created_at ON users_gb(created_at); + +CREATE UNIQUE INDEX idx_users_de_email ON users_de(email); +CREATE INDEX idx_users_de_created_at ON users_de(created_at); + +CREATE UNIQUE INDEX idx_users_fr_email ON users_fr(email); +CREATE INDEX idx_users_fr_created_at ON users_fr(created_at); + +CREATE UNIQUE INDEX idx_users_jp_email ON users_jp(email); +CREATE INDEX idx_users_jp_created_at ON users_jp(created_at); + +CREATE UNIQUE INDEX idx_users_uk_email ON users_uk(email); +CREATE INDEX idx_users_uk_created_at ON users_uk(created_at); + +CREATE UNIQUE INDEX idx_users_kz_email ON users_kz(email); +CREATE INDEX idx_users_kz_created_at ON users_kz(created_at); +-- должны ускориться запросы с поиском пользователей по email, и запросы с сортировкой по дате или с фильтрацией по интервалам \ No newline at end of file From ef0ab61829cc422e990b684cd70abcc0ec2ff183 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 29 Mar 2026 22:31:46 +0300 Subject: [PATCH 093/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A8=D0=B0=D1=80=D0=B4=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../3. Шарды/practice_sharding.sql | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql new file mode 100644 index 0000000..c3f5e12 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql @@ -0,0 +1,19 @@ +-- Задание 1. Выбор ключа шардирования +-- Ну я тут чето нихера не понял +-- Вот users лучше всего шардировать по id, а с orders непонятно. Я не до конца понял логику шардирования, если orders и users на разных шардах, то наверное лучше шардировать по user_id. Но в тоже время логично было бы сделать по order_date. +-- Короче я очень плохо понял шардирование, сейчас поищу объяснение на ютубчике и напишу новый ответ. +-- В нашем случае мы получается обсуждаем горизонтальное шардирование +-- Как я понял, если шардируются две таблицы, то они шардирются по одному полю у нас это users(id) и orders(user_id). +-- Я выбрал users(id), тк это уникальное поле и есть join по нему в orders - user_id +-- Шардирование будет происходить по хэшу. Быстрее будут выполняться запросы в пределах одного шарда, а запросы где данные берутся из разных шардов да ещё и join - явно станут сложнее. + +-- Задание 2. Добавление нового шарда +-- Проблемы: мне кажется временная приостановка записи и чтения таблиц на время 'перешардирования'. +-- План: 1. остановить запись и чтение данных; 2. создать 8 новых шардов; 3. перераспределить данные из старых четырех в восемь новых шардов; 4. удалить старые 4 шарда; 5. поменять логику изменения и чтение в бэк энде например 6. разрешить запись и чтение. конец + +-- Задание 3. Шардинг против партиционирования +-- При партицировании таблицы данные хранятся на одной бд, внутри одной таблицы, но эта таблица внутри себя разделена на партиции для ускорения запросов к ней. +-- При шардировании таблицы данные хранятся на разных серверах, то есть таблица делится на части(при горизонтальном шардировании), и каждая часть таблицы отправляется на разные сервер. + +-- Пример партицирования без шардирования: когда сервер не переполнен, но таблица слишком большая, и и запросы к ней замедляются, тогда таблица делится на партиции, для более быстрого выполнения запросов к ней. +-- Пример когда без шардирования не обойтись: это когда сервер перегружен, и физически невозможно добавлять новые данные и чтение таблиц очень медленное, тк для этого чтения сервер не может выделять должное количество ресурсов. \ No newline at end of file From eb89783316eba12bcf0170eb4a0fc614fc54eade Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 30 Mar 2026 11:37:31 +0300 Subject: [PATCH 094/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A0=D0=B5=D0=BF=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../4. Репликация/practice_replication.sql | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql new file mode 100644 index 0000000..d9adc7e --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql @@ -0,0 +1,40 @@ +-- Задание 1. Где и зачем понадобится репликация +-- На примере онлайн-магазина +-- Пример 1: мастер сервер перестал работать(по любым причинам), и чтобы не останавливать работу магазина - делаем реплику мастером. Если бы небыло репликации работа магазина приостановилась бы на какое время. +-- Пример 2: на мастер сервере даже без чтения высокая нагрузка с инсертами, апдейтами, а чтение происходит с реплики, например отчеты для аналитиков читаются с реплики чтобы не перегружать мастер. Если убрать репликацию, то сервер будет один и может перегрузиться и упасть. +-- Пример 3: я сдеал первыве два примера настоллько обобщенными, что они покрываются все более конкретные примеры, и я не могу придумать пример 3. + + +-- Задание 2. Сравнение синхронной и асинхронной репликации +-- * как ведёт себя запись при нормальной работе; +-- * синхронная репликация - мастер ждет пока реплика завершит копирование, наверное блокирует строку в таблице как в тарнзакции строка блокируется пока транзакция не завершится. +-- * асинхронная репликация - мастер ничего не ждет - его главная задача, что он отправил даные в реплику, и реплика уже пусть сама разбирается и сохраняетс всё. +-- * что происходит при падении мастера; +-- * я думаю при обоих видах репликаций - реплика становится мастером. +-- * какие риски потери/отставания данных есть. +-- * с синхронной - рисков с отставанием и потерями вроде нет. +-- * в случае с асинхронной - реплика может не успеть сохранить что-то одно, как придет уже что-то второе в туже строку например, или может произойти какой-то сбой, и данные прийдут некорректно - получается транзакция просто не откатится, а будет как будет. + +-- платежному сервису нужен синхронный вид репликации, тк ошибки и неточность недопустимы. +-- аналитической витрины нужен асинхронный вид репликации, тк он быстрее, и небольшая неточность не критична. + + +-- Задание 3. Логическая и физическая репликация +-- * думаю логическая репликация это больше про то, что мы видим и делаем сами, по типу sql запрос написать, а физическая репликация - это про то, что происходит на уровне субд/сервера, что данные перетекают байтах, WAL-журналов(я погуглил что-это - это грубо говоря план изменений, но пока не сами изменения) +-- Оказывается про логические физические репликации я тоже ничего не понял, сейчас прогуглю, и напишу ответ заново. +-- * у логической репликации изменения передаются как sql запросы, для отдельных талиц например, а физическаая репликация изменяется побайтово создавая полную копию мастера +-- * у логической репликации высокая гибкость, можно выбирать что обновлять, а что нет, а у физической репликации нулевая гибкость - создается просто копия мастера 1 в 1 +-- * как я понял для миграция лучше всего логическая репликация - тк высокая гибкость, а физическая подходит для резервной копии + +-- * для физической репликации - банковская система, тк физичская быстрее и копия 1 в 1 делается, а для логическрй репликации я не могу ничего придумать. + + +-- Задание 4. Схема один мастер — несколько реплик для чтения +-- * на мастер отправлять строго unsrt update delete только с изменениями, а на реплики select, только с чтением +-- * проблемы из-за отсавания: 1. пользователь обновил фото профиля, а изменилось оно на странице толькко через 1-2 минуты; 2. один пользователь купил последний товар, а реплика не успела обновиться, и другой пользователь тоже попытался заказать этот товар, и у него вылезла ошибка, получается что товар отображается в наличии а купить нельзя; 3. в банках если будут отставать то там вообще жопа будет со счетами, но там получается синхронная репликация. +-- * наверное можно закрыть доступ на изменения для строк которые ещё не до конца реплицировались + +-- Задание 5. Мульти‑мастер: когда это оправдано +-- * главные плюсы: 1. распределение нагрузки между серверами 2. каждая мастер репликация отвечает только за свою зону ответсвенности +-- * сложности: 1. конфликты, что на разных мульти репликах могут быть разные записи по одному и тому же значению и будет не ясно что важнее оставить; 2. согласованность - кадая мульти реплика должна работать только в пределах своей зоны ответственности; 3. сложность настройки - я думаю имеется ввиду защита от конфликтов и поддержка согласованности. +-- * у меня в голове только мысль о разделении отвественности по хэшу, что одна мастер реплика отвечает за один кусок, другая за другой, по типу шардирования, только без деления таблиц, они как бы и дальше между собой реплицируются, но каждая изменят обновляет и делает записи только на своем куске. Наверное такой способ поможет при больших таблицах с частыми изменениями(хотя по суть шардирование лучше испольщовать тогда, а не мульти-мастер). Неэффективно будет на таблицах с редкими изменениями, и не очень больших таблицах. \ No newline at end of file From 74237af6c6005ed8b0da6f47fa755b31a0d0f20b Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 30 Mar 2026 13:47:47 +0300 Subject: [PATCH 095/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D1=8B:=20?= =?UTF-8?q?=D0=90=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D1=8B'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../5. Индексы: Алгоритмы/practice_indexes_algorithms.sql | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы: Алгоритмы/practice_indexes_algorithms.sql diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы: Алгоритмы/practice_indexes_algorithms.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы: Алгоритмы/practice_indexes_algorithms.sql new file mode 100644 index 0000000..989b738 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы: Алгоритмы/practice_indexes_algorithms.sql @@ -0,0 +1,32 @@ +-- Задание 1 +-- 1. будет +-- 2. не будет. можно изменить запрос - убрать lower +-- 3. не будет. нужно убрать like +-- 4. не будет. нужно убрать like +-- 5. будет +-- 6. не будет, тк фильтрация по полю без индекса + + +-- Задание 2 +-- не понял задание, сначала нужно каждому запросу сказать что индекс не нужен/бсполезен, а потом создать индекс? +-- 1. по идее есть смысл создать два индекса - по user_id и created_at, или состваной. + CREATE INDEX idx_orders_user_id ON orders(user_id); + CREATE INDEX idx_orders_created_at ON orders(created_at); +-- 2. тут никакой индекс не нужен +-- 3. тут тоже индекс не нужен, тк у status всего 4 значения + + +-- Задание 3 +-- 1. тут hash, тк равенство +-- 2. здесь индекс и не нужен, тк у status всего 4 значения +-- 3. тут B-tree, тк интервал(<,>) +-- 4. конкретно в этом запросе лучше hash, тк равенство, но в целом думаю на user_id лучше B-tree, тк часто сортируют по нему + +-- * hash иммет смысл где просто равенство +-- * B-tree обязательно там где интервалы, > или <, и где order by +-- * в варианте 2, где у столбца всего 4 уникальных значения + + +-- Задание 4 +-- * первый составной нужен для запросов с фильтрацией по нескольким полям +-- * второй не нужен, тк user_id будет рабоать эффективно и в первом случае \ No newline at end of file From e9159267063f114ae323a596e5c36c9b1b10db4d Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 30 Mar 2026 15:16:10 +0300 Subject: [PATCH 096/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'ACID=20=D0=B8=20BASE'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1. ACID и BASE/practice_ASID&BASE.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql b/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql new file mode 100644 index 0000000..d9fa2a2 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql @@ -0,0 +1,12 @@ +-- Задание 1. Интернет‑банк +-- * в этом случае важна атомарность, и первый запрос должен был откатиться обратно, и не дублироваться как я понимаю. +-- * если не будеь атомарноти, то после сбоев могут произойте несостыковки, напрмер у пользователя деньги списались, а заказ не оформился, или наоборот + + +-- Задание 2. Лента новостей и лайки +-- * эта система ближе к BASE, тк согласование узлов происходит не сразу, и у кого то 22 лайка, а у кого-то 24. + + +-- Задание 3. Заказы в интернет‑магазине +-- * у первого случая принципы ACID - это нужно чтбы критическа важная информация обновлялась без ошибок, и чтобы небыло несостыковок +-- * во втором случае принципы BASE - можно немного потерпеть неточность, тк это некритично \ No newline at end of file From 176001d33c43604e4c2f7ebe5e71d2e6f96533ed Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 30 Mar 2026 16:08:24 +0300 Subject: [PATCH 097/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A3=D1=80=D0=BE=D0=B2=D0=BD=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=BB=D1=8F=D1=86=D0=B8=D0=B8'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Уровни изоляции/practice_isolation_levels.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql b/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql new file mode 100644 index 0000000..33e81b8 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql @@ -0,0 +1,13 @@ +-- Задание 1: Разбери ситуацию +-- 1) 200 +-- 2) 100 +-- 3) Результаты разные тк на уровне Read Committed разрешено неповторяемое чтение, а на уровне Repeatable Read неповторяемое чтенине запрщено, и мы видим то, что было на момент начала транщакции. + +-- Задание 2: Практика +-- При Repeatable Read, я увидел во втором select значение - 100, потому что изменение произошли до того как завершилась транзакция с чтением, и зафиксировалось то значение которые было на момент начала транзакции с чтением +-- При Read Committed, во втором select было значение - 200, тк этот уровень изолированности подразумевает чтение закоммиченных(другой транзакцией) данных даже если текущая транзакция ещё не завершена + +-- Задание 3: Найди ошибку +-- 1) Так произошло потому, что выбран некорректный уровень изоляции. +-- 2) Тут подойдет serializavle. +-- 3) Решить эту проблему без смены изоляции можно с помощью проверки баланса перед списанием, и если денег на балансе будет недостаточно, то откатить транзакцию. Такое решение подойдет только с уровнем изоляции Read Commited, для Repeatable Read такой способ уже не подойдет. \ No newline at end of file From 9765503642c96b6d184635044183d6e6ffab7bcc Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 31 Mar 2026 12:26:02 +0300 Subject: [PATCH 098/179] =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20"=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=BA=D0=B0=20'?= =?UTF-8?q?=D0=9F=D0=B0=D1=80=D1=82=D0=B8=D1=86=D0=B8=D0=B8'"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Партиции/practice_partitions.sql | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql index 5445669..fa096dc 100644 --- a/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql @@ -2,9 +2,8 @@ 1) * range по полю order_date * запросы с between в год - 1. where order_date between '2022-01-01' and '2023-01-01' + 1. where order_date >= '2022-01-01' and order_date < '2023-01-01' 2. where extract('year' from order_date) = '2025' - 3. where order_date < '2026-01-01' and order_date > '2025-01-01' 2) * тут два типа партиционирования подходят. range по created_at или list по country. * если выбирать list по country то плюс - это при фильтрах по стране будут отсекаться лишние партиции, а минус, что год/месяц регистрации не будет разделен по партициям. @@ -34,13 +33,13 @@ CREATE TABLE orders_part ( ) PARTITION BY RANGE (order_date); CREATE TABLE orders_2024 PARTITION OF orders_part -FOR VALUES FROM ('2024_01-01') TO ('2025-01-01'); +FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); CREATE TABLE orders_2025 PARTITION OF orders_part -FOR VALUES FROM ('2025_01-01') TO ('2026-01-01'); +FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); CREATE TABLE orders_2026 PARTITION OF orders_part -FOR VALUES FROM ('2026_01-01') TO ('2027-01-01'); +FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); -- сделал insert который попросил сгенерировать нейронку @@ -107,7 +106,7 @@ SELECT FROM users_part WHERE - country = 'RU' + country = 'RU'; -- запрос скаинровал только партицию users_ru EXPLAIN ANALYZE @@ -116,7 +115,7 @@ SELECT FROM users_part WHERE - country IN ('RU', 'KZ') + country IN ('RU', 'KZ'); -- запрос сканировал только партиции users_ru и users_kz EXPLAIN ANALYZE @@ -138,7 +137,7 @@ CREATE TABLE user_logs_part ( event_type text NOT NULL, payload jsonb, PRIMARY KEY (id, user_id) -) PARTITION BY hash (user_id) +) PARTITION BY hash (user_id); CREATE TABLE user_logs_p1 PARTITION OF user_logs_part FOR VALUES WITH (modulus 4, remainder 0); From 9234995fe26d0c185d6259af1d7eeee5a26f28f3 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 31 Mar 2026 14:23:24 +0300 Subject: [PATCH 099/179] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'CAP-=D1=82=D0=B5=D0=BE=D1=80=D0=B5=D0=BC=D0=B0'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../3. CAP-теорема/practice_CAP_theorem.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql b/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql new file mode 100644 index 0000000..d66aec9 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql @@ -0,0 +1,11 @@ +-- Задание 1: Определи тип базы +-- 1) CP +-- 2) AP +-- 3) CA +-- 4) AP + + +-- Задание 2: Что выбрать? +-- CP - должна быть согласованность, чтобы последний билет достался только одному, у двух людей не может быть одинаковый билет +-- Если выбрать CP, то все будет четко, и билет достентся только одному пользователю +-- Если выбрать AP, то билет может достаться и тому и тому, что недопустимо \ No newline at end of file From 1bd6b63cfa1362edb9fda53492651ab3e7087075 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 16 Apr 2026 22:44:37 +0300 Subject: [PATCH 100/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B'=20-?= =?UTF-8?q?=20GreenPlum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mnoskov/greenplum/practice_requests.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 mnoskov/greenplum/practice_requests.txt diff --git a/mnoskov/greenplum/practice_requests.txt b/mnoskov/greenplum/practice_requests.txt new file mode 100644 index 0000000..4c4fbde --- /dev/null +++ b/mnoskov/greenplum/practice_requests.txt @@ -0,0 +1,17 @@ +Задание 1: +- Будут собираться все 10млн строк с разных сегментов, а тольок потом сработает LIMIT. Это очень дорого стоит, и медленно тк очень много данных в пустую соединятся в мастер. +- Я думаю можно к текущему запросу добавить DENSE_RANK по total и написать условие where -тут имя dense_rank- <= 10. +!!! В теории не сказано про работу order by - пришлось загуглить !!! + +Задание 2: +- PARTITION BY по. region_id, а должен быть по order_id, чтобы работало без задержек, а при текущем варианте запроса будет происходить data motion как я понял, а это лишние вычисления. +- Тогда надо распределять таблицу по region_id полагаю. + +Задание 3: +- Ответ до подсказки: думаю все будет нормально, данные просто сгруппируются и вставятся, никаких зарежек быть не должно. +- Ответ после подсказки: greenplum будет пересылать(data motion) user_id между сегментами, чтобы собрать их в группы, а только потом собирать в мастер и инсертить, инсерт уже поидее будет происходить хорошо, тк данные после data motion будут лежать нормально. +- Orders надо дестрибьютить по user_id, тогда все четко и быстро будет - и group by, и join. + +Задание 4: +- Я хочу увидеть Motion, Broadcast, Gather +- Что означает Broadcast Motion, Redistribute Motion, Gather Motion? - в душе ни чаю, там вопрос с ответом сразу, взял оттуда: Broadcast Motion - разослать копии каждой строки на все сегменты; Redistribute Motion - перераспределить по новому ключу; Gather Motion - собрать на мастер. \ No newline at end of file From 7dcea750a3aae04ed7a687db9a7a13aa87149931 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 20 Apr 2026 00:14:25 +0400 Subject: [PATCH 101/179] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=8B=20-=20=D0=A1=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=81=20(Task=201=20and=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Запросы - Синтаксис.sql | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql diff --git a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql new file mode 100644 index 0000000..47d7629 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql @@ -0,0 +1,224 @@ +/* Запросы - Синтаксис*/ + +--! Задача 1 !-- + +DROP TABLE IF EXISTS users ON CLUSTER clickhouse_cluster; + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + age UInt8, + country String, + is_active UInt8 +) ENGINE = MergeTree() +ORDER BY user_id; + +INSERT INTO users VALUES +(1, 'Ivan', 30, 'RU', 1), +(2, 'Anna', 25, 'RU', 1), +(3, 'Petr', 45, 'RU', 0), +(4, 'Olga', 35, 'RU', 1), +(5, 'John', 55, 'US', 1), +(6, 'Alice', 42, 'US', 0), +(7, 'Tom', 38, 'US', 1), +(8, 'Lucas', 28, 'BR', 1), +(9, 'Maria', 60, 'BR', 1), +(10, 'João', 27, 'BR', 0), +(11, 'Kenji', 33, 'JP', 1), +(12, 'Yuki', 29, 'JP', 0), +(13, 'Chen', 40, 'CN', 1), +(14, 'Li', 48, 'CN', 1), +(15, 'Fatima', 31, 'AE', 1), +(16, 'Ahmed', 36, 'AE', 1), +(17, 'George', 24, 'US', 1), +(18, 'Nina', 34, 'RU', 1), +(19, 'Max', 50, 'DE', 1), +(20, 'Sophie', 22, 'DE', 0); + +--Получи 10 самых старших активных пользователей. +SELECT + user_id, + name, + age, + is_active +FROM users +WHERE is_active = true +ORDER BY age DESC +LIMIT 10; + +--Выведи пользователей из country = 'RU', отсортированных по имени. +SELECT + user_id, + name, + country +FROM users +WHERE country = 'RU' +ORDER BY name; + +--Что произойдёт, если написать LIMIT 10 без ORDER BY? Просто выведет 10 случайных активных прользователей без какой либо сортировки. +SELECT + user_id, + name, + age +FROM users +WHERE is_active = true +LIMIT 10; + +--Посчитай количество пользователей по странам. +SELECT + country, + COUNT(*) as user_country +FROM users +GROUP BY country; + +--Для каждой страны выведи минимальный и максимальный возраст. +SELECT + country, + MIN(age) as min_age, + MAX(age) as max_age +FROM users +GROUP BY country +ORDER BY country; + +--Отсортируй страны по убыванию количества пользователей. +SELECT + country, + COUNT(*) as user_country +FROM users +GROUP BY country +ORDER BY user_country DESC; + +--! Задача 2 !-- + +DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.events ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_time DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/events_v2', + '{replica}' +) +ORDER BY user_id; + +INSERT INTO dsolovianov.events (user_id, event_time, event_type) VALUES +(1, '2025-07-17 09:00:00', 'click'), +(1, '2025-07-17 09:05:00', 'view'), +(1, '2025-07-17 09:10:00', 'purchase'), +(2, '2025-07-17 10:00:00', 'click'), +(2, '2025-07-17 10:05:00', 'click'), +(2, '2025-07-17 11:00:00', 'view'), +(3, '2025-07-16 12:00:00', 'view'), +(3, '2025-07-16 12:30:00', 'click'), +(3, '2025-07-17 08:00:00', 'purchase'), +(4, '2025-07-17 13:00:00', 'click'), +(4, '2025-07-17 14:00:00', 'view'), +(4, '2025-07-17 15:00:00', 'view'), +(5, '2025-07-16 20:00:00', 'view'), +(5, '2025-07-17 01:00:00', 'click'), +(5, '2025-07-17 06:00:00', 'purchase'), +(6, '2025-07-17 07:30:00', 'view'), +(6, '2025-07-17 08:45:00', 'click'), +(7, '2025-07-16 23:59:59', 'click'), +(7, '2025-07-17 00:01:00', 'purchase'), +(7, '2025-07-17 10:10:00', 'view'); + +--1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`). +SELECT + user_id, + event_time, + event_type, + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) AS cumulative_events +FROM dsolovianov.events +ORDER BY user_id, event_time; + +--2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)` +SELECT + user_id, + event_time, + event_type, + ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS event_rank +FROM dsolovianov.events +ORDER BY user_id, event_rank; + +--3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли? +--Да, `ROWS BETWEEN` поддерживается: +SELECT + user_id, + event_time, + event_type, + -- кумулятивное окно (все предыдущие строки) + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_events, + -- скользящее окно: текущая + 2 предыдущих события + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_3_events, + -- окно: предыдущая + текущая + следующая + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS prev_curr_next +FROM dsolovianov.events +ORDER BY user_id, event_time; + +--4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа. +WITH recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT * FROM recent_events +ORDER BY event_time DESC; + +--5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий. +WITH recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT + user_id, + COUNT(*) AS event_count +FROM recent_events +GROUP BY user_id +ORDER BY event_count DESC +LIMIT 5; + +--6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе. +WITH +event_categories AS ( + SELECT + event_type, + CASE + WHEN event_type = 'click' THEN 'engagement' + WHEN event_type = 'view' THEN 'engagement' + WHEN event_type = 'purchase' THEN 'conversion' + ELSE 'other' + END AS category + FROM (SELECT DISTINCT event_type FROM dsolovianov.events) +), +recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT + re.user_id, + re.event_time, + re.event_type, + ec.category, + ROW_NUMBER() OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS event_rank, + COUNT(*) OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS cumulative_events +FROM recent_events re +LEFT JOIN event_categories ec ON re.event_type = ec.event_type +ORDER BY re.user_id, re.event_time; + +/* +Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE? +многоуровневые CTE - да +рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле. +*/ + From 8c6daac203ecdde1ad934919d6a763f36e85e6a9 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 20 Apr 2026 03:32:36 +0400 Subject: [PATCH 102/179] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=8B=20-=20=D0=A1=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=81=20(Task=201=20and=202)=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Запросы - Синтаксис.sql | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql index 47d7629..abd02d7 100644 --- a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql +++ b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql @@ -2,7 +2,7 @@ --! Задача 1 !-- -DROP TABLE IF EXISTS users ON CLUSTER clickhouse_cluster; +DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC; CREATE TABLE users ON CLUSTER clickhouse_cluster @@ -11,8 +11,11 @@ ON CLUSTER clickhouse_cluster name String, age UInt8, country String, - is_active UInt8 -) ENGINE = MergeTree() + is_active Boolean +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/users_v2', + '{replica}' +) ORDER BY user_id; INSERT INTO users VALUES @@ -73,6 +76,12 @@ SELECT FROM users GROUP BY country; +SELECT + country, + uniqExact(country) as unique_countries --возвращает точное количество уникальных значений в столбце country +FROM users +GROUP BY country; + --Для каждой страны выведи минимальный и максимальный возраст. SELECT country, @@ -90,6 +99,18 @@ FROM users GROUP BY country ORDER BY user_country DESC; +--Всё вместе +SELECT + country, + COUNT(*) as user_country, + SUM(is_active) as active_users, + MIN(age) as min_age, + MAX(age) as max_age, + uniqExact(country) as unique_countries +FROM users +GROUP BY country +ORDER BY country; + --! Задача 2 !-- DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; @@ -104,7 +125,7 @@ ENGINE = ReplicatedMergeTree( '/clickhouse/tables/{shard}/dsolovianov/events_v2', '{replica}' ) -ORDER BY user_id; +ORDER BY (user_id, event_time); INSERT INTO dsolovianov.events (user_id, event_time, event_type) VALUES (1, '2025-07-17 09:00:00', 'click'), @@ -220,5 +241,4 @@ ORDER BY re.user_id, re.event_time; Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE? многоуровневые CTE - да рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле. -*/ - +*/ \ No newline at end of file From a997d586da7ffc4ebe563b31c0737545bfa29484 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 20 Apr 2026 03:36:52 +0400 Subject: [PATCH 103/179] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=8B=20-=20=D0=A1=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=81=20(Task=201=20and=202)=20v3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql index abd02d7..120b341 100644 --- a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql +++ b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql @@ -11,7 +11,7 @@ ON CLUSTER clickhouse_cluster name String, age UInt8, country String, - is_active Boolean + is_active Bool ) ENGINE = ReplicatedMergeTree( '/clickhouse/tables/{shard}/dsolovianov/users_v2', '{replica}' From 9b0186715459a6db96bbb4c368e9e192d818d78b Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 20 Apr 2026 18:08:46 +0300 Subject: [PATCH 104/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'DISTRIBUTED=20BY'=20-=20GreenPlum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mnoskov/greenplum/practice_distributed_by.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 mnoskov/greenplum/practice_distributed_by.txt diff --git a/mnoskov/greenplum/practice_distributed_by.txt b/mnoskov/greenplum/practice_distributed_by.txt new file mode 100644 index 0000000..b9f5fc6 --- /dev/null +++ b/mnoskov/greenplum/practice_distributed_by.txt @@ -0,0 +1,9 @@ +Задание 1 + - запос не будет локальным + - гринплам будет делать data motion + +Задание 2 + - distributed by не подходит, тк таблица не будет учавствовать в join + +Задание 3 + - можно добавить event_id и распределять по нему, или же сделать destributed randomly \ No newline at end of file From 9520bd8bbc6532bfe6f138bfe9e09f690ada9fab Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 21 Apr 2026 18:04:37 +0300 Subject: [PATCH 105/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'EXPLAIN'=20-=20GreenPlum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mnoskov/greenplum/practice_explain.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 mnoskov/greenplum/practice_explain.txt diff --git a/mnoskov/greenplum/practice_explain.txt b/mnoskov/greenplum/practice_explain.txt new file mode 100644 index 0000000..9393ecd --- /dev/null +++ b/mnoskov/greenplum/practice_explain.txt @@ -0,0 +1,6 @@ +Задание 1 + - если orders DISTRIBUTED BY user_id, то будет 'Seq Scan on orders' -> 'HashAggregate'. Redistribute Motion(по user_id) пропадет, потому что все уже распределено по user_id; + +Задание 2 + - Это называется Skew. + - Чтобы избежать skew, нужно выбрать столбец для распределения с нормальной кардинальностью. Или делать destributed randomly если не нужно join. \ No newline at end of file From 863b58480e3425a24a8d738671e608215611071b Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 21 Apr 2026 19:54:45 +0300 Subject: [PATCH 106/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=92=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F?= =?UTF-8?q?'=20-=20GreenPlum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice_external_sources_and_integration.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 mnoskov/greenplum/practice_external_sources_and_integration.txt diff --git a/mnoskov/greenplum/practice_external_sources_and_integration.txt b/mnoskov/greenplum/practice_external_sources_and_integration.txt new file mode 100644 index 0000000..5270ff7 --- /dev/null +++ b/mnoskov/greenplum/practice_external_sources_and_integration.txt @@ -0,0 +1,10 @@ +Задание 1 + - данные физически находятся в Hive; + - писать обратно в Hive нельзя, тк таблица не writable; + - не знаю, в теорри это не сказано - думаю будет ошибка. После того как загуглил: + 1) если в hive добавились новые столбцы, то при чтении все будет отображаться так, как было во время создание внешней таблицы в greenplum, без ошибок; + 2) если в hive удалась часть столбцов, то возникнет ошибка тк, в greenplum по сути будут лишние столбцы. + +Задание 2 + - данные не копируются в greenplum, а только читаются из s3; + - я думаю в момент создания таблицы - быстрее external, тк ничего не копируется фищически, но при чтении должен быть быстрее CREATE TABLE AS SELECT, тк данные физически хранятся на нашем сервере, а не где-то в облаке. \ No newline at end of file From 8913ccb25ea37abfca51a1cdea457035974af59b Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 21 Apr 2026 21:05:39 +0300 Subject: [PATCH 107/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=201.=20=D0=98?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0.=20?= =?UTF-8?q?=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82:=20DW?= =?UTF-8?q?H?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Step_1_project_Initialization.sql | 46 +++++++++++++++++++ .../practice_distributed_by.txt | 0 .../practice_explain.txt | 0 .../practice_external_sources_and_integration.txt | 0 .../practice_requests.txt | 0 5 files changed, 46 insertions(+) create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql rename mnoskov/greenplum/{ => Практика greenplum}/practice_distributed_by.txt (100%) rename mnoskov/greenplum/{ => Практика greenplum}/practice_explain.txt (100%) rename mnoskov/greenplum/{ => Практика greenplum}/practice_external_sources_and_integration.txt (100%) rename mnoskov/greenplum/{ => Практика greenplum}/practice_requests.txt (100%) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql b/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql new file mode 100644 index 0000000..f76258c --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql @@ -0,0 +1,46 @@ +-- 1 +CREATE SCHEMA raw; +CREATE SCHEMA core; +CREATE SCHEMA dm; + +-- 2 +-- DROP TABLE IF EXISTS raw.users; +CREATE TABLE raw.users ( + id bigint, + name varchar(100), + age SMALLINT, + email varchar(100), + registration_date timestamptz +); + +--DROP TABLE IF EXISTS raw.courses; +CREATE TABLE raw.courses ( + id bigint, + title varchar(150), + category varchar(50), + created_at timestamptz +); + +--DROP TABLE IF EXISTS raw.lessons; +CREATE TABLE raw.lessons ( + id bigint, + course_id bigint, + title varchar(150), + duration_min SMALLINT +); + +--DROP TABLE IF EXISTS raw.enrollments; +CREATE TABLE raw.enrollments ( + id bigint, + user_id bigint, + course_id bigint, + enrolled_at timestamptz +); + +--DROP TABLE IF EXISTS raw.lesson_views; +CREATE TABLE raw.lesson_views ( + id bigint, + user_id bigint, + lesson_id bigint, + viewed_at timestamptz +); \ No newline at end of file diff --git a/mnoskov/greenplum/practice_distributed_by.txt b/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt similarity index 100% rename from mnoskov/greenplum/practice_distributed_by.txt rename to mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt diff --git a/mnoskov/greenplum/practice_explain.txt b/mnoskov/greenplum/Практика greenplum/practice_explain.txt similarity index 100% rename from mnoskov/greenplum/practice_explain.txt rename to mnoskov/greenplum/Практика greenplum/practice_explain.txt diff --git a/mnoskov/greenplum/practice_external_sources_and_integration.txt b/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt similarity index 100% rename from mnoskov/greenplum/practice_external_sources_and_integration.txt rename to mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt diff --git a/mnoskov/greenplum/practice_requests.txt b/mnoskov/greenplum/Практика greenplum/practice_requests.txt similarity index 100% rename from mnoskov/greenplum/practice_requests.txt rename to mnoskov/greenplum/Практика greenplum/practice_requests.txt From c456045950f6a6a69f18931eebfb1bbf8a494edd Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 21 Apr 2026 21:44:45 +0300 Subject: [PATCH 108/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=202.=20=D0=97?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20(=D1=8D=D0=BC=D1=83=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20Airflow).=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20DWH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Step 2: Loading Data/courses_202604212117.csv | 6 ++ .../Step 2: Loading Data/enrollments_202604212130.csv | 46 +++++++++++++++ .../Step 2: Loading Data/lesson_views_202604212133.csv | 59 +++++++++++++++++++ .../Step 2: Loading Data/lessons_202604212131.csv | 19 ++++++ .../Step 2: Loading Data/users_202604212132.csv | 21 +++++++ 5 files changed, 151 insertions(+) create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/courses_202604212117.csv create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/enrollments_202604212130.csv create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lesson_views_202604212133.csv create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lessons_202604212131.csv create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/users_202604212132.csv diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/courses_202604212117.csv b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/courses_202604212117.csv new file mode 100644 index 0000000..d348d4a --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/courses_202604212117.csv @@ -0,0 +1,6 @@ +"id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +0300 +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +0300 +3,BI с нуля,business,2023-03-01 00:00:00.000 +0300 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 +0300 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/enrollments_202604212130.csv b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/enrollments_202604212130.csv new file mode 100644 index 0000000..e5c5610 --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/enrollments_202604212130.csv @@ -0,0 +1,46 @@ +"id","user_id","course_id","enrolled_at" +1,1,1,2023-01-15 00:00:00.000 +0300 +2,1,2,2023-02-01 00:00:00.000 +0300 +3,2,1,2023-01-20 00:00:00.000 +0300 +4,3,2,2023-03-05 00:00:00.000 +0300 +5,4,3,2023-04-01 00:00:00.000 +0300 +6,6,1,2023-01-06 00:00:00.000 +0300 +7,6,2,2023-01-10 00:00:00.000 +0300 +8,6,4,2023-02-05 00:00:00.000 +0300 +9,7,1,2023-01-13 00:00:00.000 +0300 +10,7,3,2023-03-10 00:00:00.000 +0300 +11,8,1,2023-01-22 00:00:00.000 +0300 +12,8,2,2023-01-22 00:00:00.000 +0300 +13,8,3,2023-01-22 00:00:00.000 +0300 +14,9,4,2023-02-12 00:00:00.000 +0300 +15,10,1,2023-04-16 00:00:00.000 +0300 +16,11,2,2023-04-21 00:00:00.000 +0300 +17,12,1,2023-01-16 00:00:00.000 +0300 +18,13,2,2023-01-19 00:00:00.000 +0300 +19,13,4,2023-02-20 00:00:00.000 +0300 +20,14,1,2023-01-09 00:00:00.000 +0300 +21,14,2,2023-01-09 00:00:00.000 +0300 +22,14,3,2023-01-09 00:00:00.000 +0300 +23,14,4,2023-02-01 00:00:00.000 +0300 +24,14,5,2023-03-20 00:00:00.000 +0300 +25,15,1,2023-01-11 00:00:00.000 +0300 +26,15,3,2023-03-05 00:00:00.000 +0300 +27,16,2,2023-02-16 00:00:00.000 +0300 +28,16,4,2023-02-16 00:00:00.000 +0300 +29,17,3,2023-03-22 00:00:00.000 +0300 +30,17,5,2023-03-25 00:00:00.000 +0300 +31,18,1,2023-04-02 00:00:00.000 +0300 +32,19,1,2023-04-11 00:00:00.000 +0300 +33,19,2,2023-04-11 00:00:00.000 +0300 +34,20,2,2023-04-19 00:00:00.000 +0300 +35,5,5,2023-03-20 00:00:00.000 +0300 +36,1,4,2023-02-10 00:00:00.000 +0300 +37,2,3,2023-03-15 00:00:00.000 +0300 +38,3,1,2023-02-25 00:00:00.000 +0300 +39,4,2,2023-03-10 00:00:00.000 +0300 +40,6,3,2023-03-08 00:00:00.000 +0300 +41,7,2,2023-02-01 00:00:00.000 +0300 +42,9,1,2023-02-15 00:00:00.000 +0300 +43,12,3,2023-03-10 00:00:00.000 +0300 +44,15,2,2023-02-01 00:00:00.000 +0300 +45,18,4,2023-04-05 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lesson_views_202604212133.csv b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lesson_views_202604212133.csv new file mode 100644 index 0000000..b09d250 --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lesson_views_202604212133.csv @@ -0,0 +1,59 @@ +"id","user_id","lesson_id","viewed_at" +1,1,1,2023-01-16 10:00:00.000 +0300 +2,1,2,2023-01-16 10:15:00.000 +0300 +3,1,3,2023-01-17 09:00:00.000 +0300 +4,1,4,2023-01-17 09:30:00.000 +0300 +5,1,4,2023-01-17 11:00:00.000 +0300 +6,1,4,2023-01-18 10:00:00.000 +0300 +7,1,6,2023-02-02 14:00:00.000 +0300 +8,1,12,2023-02-15 10:00:00.000 +0300 +9,1,13,2023-02-15 10:30:00.000 +0300 +10,2,1,2023-01-21 09:00:00.000 +0300 +11,2,9,2023-03-16 10:00:00.000 +0300 +12,2,10,2023-03-16 10:30:00.000 +0300 +13,3,6,2023-03-06 12:00:00.000 +0300 +14,3,1,2023-02-26 11:00:00.000 +0300 +15,3,2,2023-02-26 11:20:00.000 +0300 +16,4,9,2023-04-02 10:00:00.000 +0300 +17,4,10,2023-04-03 10:00:00.000 +0300 +18,4,6,2023-03-12 14:00:00.000 +0300 +19,4,7,2023-03-13 09:00:00.000 +0300 +20,5,15,2023-03-21 10:00:00.000 +0300 +21,5,16,2023-03-22 11:00:00.000 +0300 +22,6,1,2023-01-07 10:00:00.000 +0300 +23,6,2,2023-01-07 10:15:00.000 +0300 +24,6,3,2023-01-08 09:00:00.000 +0300 +25,6,4,2023-01-08 09:30:00.000 +0300 +26,6,5,2023-01-09 10:00:00.000 +0300 +27,6,6,2023-01-11 14:00:00.000 +0300 +28,6,7,2023-01-12 10:00:00.000 +0300 +29,6,8,2023-01-13 11:00:00.000 +0300 +30,6,12,2023-02-06 09:00:00.000 +0300 +31,6,13,2023-02-06 09:30:00.000 +0300 +32,6,14,2023-02-07 10:00:00.000 +0300 +33,6,9,2023-03-09 11:00:00.000 +0300 +34,7,1,2023-01-14 10:00:00.000 +0300 +35,7,2,2023-01-14 10:20:00.000 +0300 +36,7,3,2023-01-15 09:00:00.000 +0300 +37,7,9,2023-03-11 14:00:00.000 +0300 +38,7,10,2023-03-11 14:30:00.000 +0300 +39,9,12,2023-02-13 10:00:00.000 +0300 +40,9,13,2023-02-13 10:30:00.000 +0300 +41,9,1,2023-02-16 11:00:00.000 +0300 +42,12,1,2023-01-17 10:00:00.000 +0300 +43,12,2,2023-01-17 10:20:00.000 +0300 +44,12,1,2023-04-10 11:00:00.000 +0300 +45,12,3,2023-04-10 11:30:00.000 +0300 +46,12,9,2023-03-12 10:00:00.000 +0300 +47,13,6,2023-01-20 14:00:00.000 +0300 +48,13,7,2023-01-21 10:00:00.000 +0300 +49,13,12,2023-02-21 09:00:00.000 +0300 +50,14,1,2023-01-09 08:00:00.000 +0300 +51,14,2,2023-01-09 08:15:00.000 +0300 +52,14,3,2023-01-10 10:00:00.000 +0300 +53,14,4,2023-01-10 10:30:00.000 +0300 +54,14,5,2023-01-11 09:00:00.000 +0300 +55,14,6,2023-01-10 14:00:00.000 +0300 +56,14,7,2023-01-11 10:00:00.000 +0300 +57,14,8,2023-01-12 11:00:00.000 +0300 +58,14,9,2023-01-10 08:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lessons_202604212131.csv b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lessons_202604212131.csv new file mode 100644 index 0000000..54dbb22 --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lessons_202604212131.csv @@ -0,0 +1,19 @@ +"id","course_id","title","duration_min" +1,1,SELECT и FROM,10 +2,1,JOIN,15 +3,1,WHERE и фильтрация,12 +4,1,Агрегации и GROUP BY,18 +5,1,Подзапросы,25 +6,2,Pandas основы,20 +7,2,NumPy,15 +8,2,Визуализация данных,22 +9,3,Основы BI,12 +10,3,Метрики и дашборды,18 +11,3,Power BI введение,20 +12,4,Формулы в Excel,15 +13,4,Сводные таблицы,18 +14,4,Графики и диаграммы,14 +15,5,Введение в ML,20 +16,5,Линейная регрессия,30 +17,5,Классификация,28 +18,5,Кластеризация,25 diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/users_202604212132.csv b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/users_202604212132.csv new file mode 100644 index 0000000..d6aa0da --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/users_202604212132.csv @@ -0,0 +1,21 @@ +"id","name","age","email","registration_date" +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +0300 +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +0300 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +0300 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +0300 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +0300 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +0300 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +0300 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +0300 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +0300 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +0300 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +0300 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +0300 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +0300 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +0300 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +0300 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +0300 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +0300 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +0300 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +0300 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 +0300 From 6c06823b68d718ddcfe49663bb997513dd3ea3de Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 22 Apr 2026 14:06:46 +0300 Subject: [PATCH 109/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=203.=20=D0=A1?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9=20Core=20=E2=80=94=20=D0=BE=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5.=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20DWH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Пет проект: DWH/Step_3_core_layer.sql | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql new file mode 100644 index 0000000..c21d4fa --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -0,0 +1,112 @@ +-- 4 + +--DROP TABLE IF EXISTS core.dim_user; +CREATE TABLE core.dim_user ( + user_id int PRIMARY KEY, + name text, + age int, + email text, + registration_date date +) DISTRIBUTED BY (user_id); + +--DROP TABLE IF EXISTS core.dim_course; +CREATE TABLE core.dim_course ( + course_id int PRIMARY KEY, + title text, + category text +) DISTRIBUTED BY (course_id); + +--DROP TABLE IF EXISTS core.dim_lesson; +CREATE TABLE core.dim_lesson ( + lesson_id int PRIMARY KEY, + title text, + duration_min int, + course_id int REFERENCES core.dim_course(course_id) +) DISTRIBUTED BY (lesson_id); + +--DROP TABLE IF EXISTS core.dim_date; +CREATE TABLE core.dim_date ( + date date PRIMARY KEY, + year int, + month int, + day int, + weekday text +) DISTRIBUTED BY (date); + + +-- 5 +--DROP TABLE IF EXISTS core.fact_lesson_views; +CREATE TABLE core.fact_lesson_views ( + user_id int REFERENCES core.dim_user(user_id), + lesson_id int REFERENCES core.dim_lesson(lesson_id), + course_id int REFERENCES core.dim_course(course_id), + viewed_at timestamp +) DISTRIBUTED BY (user_id); + +--DROP TABLE IF EXISTS core.enrollments; +CREATE TABLE core.enrollments ( + user_id int REFERENCES core.dim_user(user_id), + course_id int REFERENCES core.dim_course(course_id), + enrolled_at timestamp +) DISTRIBUTED BY (user_id); + + +--6 +INSERT INTO core.dim_user (user_id, name, age, email, registration_date) +SELECT DISTINCT + id::int AS user_id, + name::text, + age::int, + email::text, + registration_date::date +FROM raw.users +WHERE id IS NOT NULL AND email IS NOT NULL; + +INSERT INTO core.dim_course (course_id, title, category) +SELECT DISTINCT + id::int AS course_id, + title::text, + category::text +FROM + raw.courses +WHERE id IS NOT NULL AND title IS NOT NULL; + +INSERT INTO core.dim_lesson (lesson_id, title, duration_min, course_id) +SELECT DISTINCT + id::int AS lesson_id, + title::text, + duration_min::int, + course_id::int +FROM + raw.lessons +WHERE id IS NOT NULL; + +-- загуглил как заполнять: +INSERT INTO core.dim_date (date, year, month, day, weekday) +SELECT + d::date, + EXTRACT(YEAR FROM d)::int, + EXTRACT(MONTH FROM d)::int, + EXTRACT(DAY FROM d)::int, + to_char(d, 'day') +FROM + generate_series('2023-01-01'::date, '2025-12-31'::date, INTERVAL '1 day') AS d; + +INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) +SELECT + lv.user_id::int, + lv.lesson_id::int, + l.course_id::int, + lv.viewed_at::timestamp +FROM + raw.lesson_views lv +JOIN + raw.lessons l ON lv.lesson_id = l.id + +INSERT INTO core.enrollments (user_id, course_id, enrolled_at) +SELECT + user_id::int, + course_id::int, + enrolled_at::timestamp +FROM + raw.enrollments; \ No newline at end of file From 866fb95d2ee4b65ac01aaf380406108c27190ccb Mon Sep 17 00:00:00 2001 From: pavel Date: Wed, 22 Apr 2026 16:18:49 +0200 Subject: [PATCH 110/179] fix(sql): joins - resolved the comments --- pmikhailov/postgresql/joins.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pmikhailov/postgresql/joins.sql b/pmikhailov/postgresql/joins.sql index 7a90e1b..e83b069 100644 --- a/pmikhailov/postgresql/joins.sql +++ b/pmikhailov/postgresql/joins.sql @@ -44,7 +44,7 @@ FROM LEFT JOIN order_items oi ON o.id = oi.order_id INNER JOIN - users u ON o.id = u.id + users u ON o.user_id = u.id INNER JOIN products p ON oi.product_id = p.id; @@ -69,7 +69,7 @@ SELECT FROM users u LEFT JOIN - orders o ON u.id = o.id + orders o ON u.id = o.user_id WHERE o.user_id IS NOT NULL From b6341a52edcddd0a6948178b7f51db5d5714b62a Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 23 Apr 2026 21:58:27 +0300 Subject: [PATCH 111/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=201.=20=D0=98?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0.=20?= =?UTF-8?q?=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82:=20DW?= =?UTF-8?q?H.=20=D0=A0=D0=90=D0=91=D0=9E=D0=A2=D0=90=20=D0=9D=D0=90=D0=94?= =?UTF-8?q?=20=D0=9E=D0=A8=D0=98=D0=91=D0=9A=D0=90=D0=9C=D0=98=20-=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20NOT=20NULL=20?= =?UTF-8?q?=D0=BD=D0=B0=20id.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Step_1_project_Initialization.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql b/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql index f76258c..f542678 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql @@ -4,9 +4,9 @@ CREATE SCHEMA core; CREATE SCHEMA dm; -- 2 --- DROP TABLE IF EXISTS raw.users; +--DROP TABLE IF EXISTS raw.users; CREATE TABLE raw.users ( - id bigint, + id bigint NOT NULL, name varchar(100), age SMALLINT, email varchar(100), @@ -15,7 +15,7 @@ CREATE TABLE raw.users ( --DROP TABLE IF EXISTS raw.courses; CREATE TABLE raw.courses ( - id bigint, + id bigint NOT NULL, title varchar(150), category varchar(50), created_at timestamptz @@ -23,7 +23,7 @@ CREATE TABLE raw.courses ( --DROP TABLE IF EXISTS raw.lessons; CREATE TABLE raw.lessons ( - id bigint, + id bigint NOT NULL, course_id bigint, title varchar(150), duration_min SMALLINT @@ -31,7 +31,7 @@ CREATE TABLE raw.lessons ( --DROP TABLE IF EXISTS raw.enrollments; CREATE TABLE raw.enrollments ( - id bigint, + id bigint NOT NULL, user_id bigint, course_id bigint, enrolled_at timestamptz @@ -39,7 +39,7 @@ CREATE TABLE raw.enrollments ( --DROP TABLE IF EXISTS raw.lesson_views; CREATE TABLE raw.lesson_views ( - id bigint, + id bigint NOT NULL, user_id bigint, lesson_id bigint, viewed_at timestamptz From 243e79401b9379d8b57fcd2c9ed49038e54fada4 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 23 Apr 2026 22:25:25 +0300 Subject: [PATCH 112/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=203.=20=D0=A1?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9=20Core=20=E2=80=94=20=D0=BE=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5.=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=A0=D0=90=D0=91=D0=9E?= =?UTF-8?q?=D0=A2=D0=90=20=D0=9D=D0=90=D0=94=20=D0=9E=D0=A8=D0=98=D0=91?= =?UTF-8?q?=D0=9A=D0=90=D0=9C=D0=98=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20NULL=20=D0=B2=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Пет проект: DWH/Step_3_core_layer.sql | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql index c21d4fa..c2b6c59 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -5,23 +5,23 @@ CREATE TABLE core.dim_user ( user_id int PRIMARY KEY, name text, age int, - email text, - registration_date date + email text NOT NULL, + registration_date date NOT NULL ) DISTRIBUTED BY (user_id); --DROP TABLE IF EXISTS core.dim_course; CREATE TABLE core.dim_course ( course_id int PRIMARY KEY, - title text, + title text NOT NULL, category text ) DISTRIBUTED BY (course_id); --DROP TABLE IF EXISTS core.dim_lesson; CREATE TABLE core.dim_lesson ( lesson_id int PRIMARY KEY, - title text, + title text NOT NULL, duration_min int, - course_id int REFERENCES core.dim_course(course_id) + course_id int NOT NULL REFERENCES core.dim_course(course_id) ) DISTRIBUTED BY (lesson_id); --DROP TABLE IF EXISTS core.dim_date; @@ -37,17 +37,17 @@ CREATE TABLE core.dim_date ( -- 5 --DROP TABLE IF EXISTS core.fact_lesson_views; CREATE TABLE core.fact_lesson_views ( - user_id int REFERENCES core.dim_user(user_id), - lesson_id int REFERENCES core.dim_lesson(lesson_id), - course_id int REFERENCES core.dim_course(course_id), - viewed_at timestamp + user_id int NOT NULL REFERENCES core.dim_user(user_id), + lesson_id int NOT NULL REFERENCES core.dim_lesson(lesson_id), + course_id int NOT NULL REFERENCES core.dim_course(course_id), + viewed_at timestamp NOT NULL ) DISTRIBUTED BY (user_id); --DROP TABLE IF EXISTS core.enrollments; CREATE TABLE core.enrollments ( - user_id int REFERENCES core.dim_user(user_id), - course_id int REFERENCES core.dim_course(course_id), - enrolled_at timestamp + user_id int NOT NULL REFERENCES core.dim_user(user_id), + course_id int NOT NULL REFERENCES core.dim_course(course_id), + enrolled_at timestamp NOT NULL ) DISTRIBUTED BY (user_id); @@ -101,7 +101,7 @@ SELECT FROM raw.lesson_views lv JOIN - raw.lessons l ON lv.lesson_id = l.id + raw.lessons l ON lv.lesson_id = l.id; INSERT INTO core.enrollments (user_id, course_id, enrolled_at) SELECT From 5c2037a49affb52129cc7961368a92bd61e2adbf Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 23 Apr 2026 23:28:03 +0300 Subject: [PATCH 113/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=203.=20=D0=A1?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9=20Core=20=E2=80=94=20=D0=BE=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5.=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=A0=D0=90=D0=91=D0=9E?= =?UTF-8?q?=D0=A2=D0=90=20=D0=9D=D0=90=D0=94=20=D0=9E=D0=A8=D0=98=D0=91?= =?UTF-8?q?=D0=9A=D0=90=D0=9C=D0=98=20-=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=82=D0=B8=D0=BF=D1=8B=20=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=BB=D0=B1=D1=86=D0=BE=D0=B2=20=D0=B2=20core=20=D1=82=D0=B0?= =?UTF-8?q?=D0=BA=D0=B8=D0=BC=D0=B8=20=D0=B6=D0=B5=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=B2=20raw.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Пет проект: DWH/Step_3_core_layer.sql | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql index c2b6c59..eed6ab3 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -1,27 +1,27 @@ -- 4 - --DROP TABLE IF EXISTS core.dim_user; CREATE TABLE core.dim_user ( - user_id int PRIMARY KEY, - name text, - age int, - email text NOT NULL, - registration_date date NOT NULL + user_id bigint PRIMARY KEY, + name varchar(100), + age smallint, + email varchar(100) NOT NULL, + registration_date timestamptz NOT NULL ) DISTRIBUTED BY (user_id); --DROP TABLE IF EXISTS core.dim_course; CREATE TABLE core.dim_course ( - course_id int PRIMARY KEY, - title text NOT NULL, - category text + course_id bigint PRIMARY KEY, + title varchar(150) NOT NULL, + category varchar(50), + created_at timestamptz NOT NULL ) DISTRIBUTED BY (course_id); --DROP TABLE IF EXISTS core.dim_lesson; CREATE TABLE core.dim_lesson ( - lesson_id int PRIMARY KEY, - title text NOT NULL, - duration_min int, - course_id int NOT NULL REFERENCES core.dim_course(course_id) + lesson_id bigint PRIMARY KEY, + title varchar(150) NOT NULL, + duration_min smallint, + course_id bigint NOT NULL REFERENCES core.dim_course(course_id) ) DISTRIBUTED BY (lesson_id); --DROP TABLE IF EXISTS core.dim_date; @@ -37,17 +37,17 @@ CREATE TABLE core.dim_date ( -- 5 --DROP TABLE IF EXISTS core.fact_lesson_views; CREATE TABLE core.fact_lesson_views ( - user_id int NOT NULL REFERENCES core.dim_user(user_id), - lesson_id int NOT NULL REFERENCES core.dim_lesson(lesson_id), - course_id int NOT NULL REFERENCES core.dim_course(course_id), - viewed_at timestamp NOT NULL + user_id bigint NOT NULL REFERENCES core.dim_user(user_id), + lesson_id bigint NOT NULL REFERENCES core.dim_lesson(lesson_id), + course_id bigint NOT NULL REFERENCES core.dim_course(course_id), + viewed_at timestamptz NOT NULL ) DISTRIBUTED BY (user_id); --DROP TABLE IF EXISTS core.enrollments; CREATE TABLE core.enrollments ( - user_id int NOT NULL REFERENCES core.dim_user(user_id), - course_id int NOT NULL REFERENCES core.dim_course(course_id), - enrolled_at timestamp NOT NULL + user_id bigint NOT NULL REFERENCES core.dim_user(user_id), + course_id bigint NOT NULL REFERENCES core.dim_course(course_id), + enrolled_at timestamptz NOT NULL ) DISTRIBUTED BY (user_id); From 95456999b390eef71527212ec5ed7f61af9e4f60 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Fri, 24 Apr 2026 18:10:49 +0300 Subject: [PATCH 114/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=203.=20=D0=A1?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9=20Core=20=E2=80=94=20=D0=BE=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5.=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=A0=D0=90=D0=91=D0=9E?= =?UTF-8?q?=D0=A2=D0=90=20=D0=9D=D0=90=D0=94=20=D0=9E=D0=A8=D0=98=D0=91?= =?UTF-8?q?=D0=9A=D0=90=D0=9C=D0=98=20-=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D0=B5=D0=BD=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Пет проект: DWH/Step_3_core_layer.sql | 108 ++++++++++++++---- 1 file changed, 84 insertions(+), 24 deletions(-) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql index eed6ab3..7af360d 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -23,6 +23,7 @@ CREATE TABLE core.dim_lesson ( duration_min smallint, course_id bigint NOT NULL REFERENCES core.dim_course(course_id) ) DISTRIBUTED BY (lesson_id); +CREATE INDEX idx_dim_lesson_course_id ON core.dim_lesson(course_id); --DROP TABLE IF EXISTS core.dim_date; CREATE TABLE core.dim_date ( @@ -52,34 +53,92 @@ CREATE TABLE core.enrollments ( --6 +-- dim_user INSERT INTO core.dim_user (user_id, name, age, email, registration_date) -SELECT DISTINCT - id::int AS user_id, - name::text, - age::int, - email::text, - registration_date::date -FROM raw.users -WHERE id IS NOT NULL AND email IS NOT NULL; - -INSERT INTO core.dim_course (course_id, title, category) -SELECT DISTINCT - id::int AS course_id, - title::text, - category::text +SELECT + raw_u.id, + raw_u.name, + raw_u.age, + raw_u.email, + raw_u.registration_date FROM - raw.courses -WHERE id IS NOT NULL AND title IS NOT NULL; + raw.users raw_u +WHERE NOT EXISTS + (SELECT 1 FROM core.dim_user core_u WHERE core_u.user_id = raw_u.id) + AND raw_u.email IS NOT NULL AND raw_u.registration_date IS NOT NULL; +UPDATE core.dim_user core_u +SET + name = raw_u.name, + age = raw_u.age, + email = raw_u.email, + registration_date = raw_u.registration_date +FROM + raw.users raw_u +WHERE + core_u.user_id = raw_u.id + AND ( + core_u.name != raw_u.name OR + core_u.age != raw_u.age OR + core_u.email != raw_u.email OR + core_u.registration_date != raw_u.registration_date + ); + +-- dim_course +INSERT INTO core.dim_course (course_id, title, category, created_at) +SELECT + raw_c.id, + raw_c.title, + raw_c.category, + raw_c.created_at +FROM + raw.courses raw_c +WHERE NOT EXISTS + (SELECT 1 FROM core.dim_course core_c WHERE core_c.course_id = raw_c.id) + AND raw_c.title IS NOT NULL AND raw_c; + +UPDATE core.dim_course core_c +SET + title = raw_c.title, + category = raw_c.category, + created_at = raw_c.created_at +FROM + raw.courses raw_c +WHERE + core_c.course_id = raw_c.id + AND ( + core_c.title != raw_c.title OR + core_c.category != raw_c.category OR + core_c.created_at != raw_c.created_at + ); + +-- dim_lesson INSERT INTO core.dim_lesson (lesson_id, title, duration_min, course_id) -SELECT DISTINCT - id::int AS lesson_id, - title::text, - duration_min::int, - course_id::int +SELECT + raw_l.id, + raw_l.title, + raw_l.duration_min, + raw_l.course_id FROM - raw.lessons -WHERE id IS NOT NULL; + raw.lessons raw_l +WHERE NOT EXISTS + (SELECT 1 FROM core.dim_lesson core_l WHERE core_l.lesson_id = raw_l.id) + AND raw_l.title IS NOT NULL AND raw_l.course_id IS NOT NULL; + +UPDATE core.dim_lesson core_l +SET + title = raw_l.title, + duration_min = raw_l.duration_min, + course_id = raw_l.course_id -- решил оставить, тк чисто теоретические же урок может начать принадлежать другому курсу +FROM + raw.lessons raw_l +WHERE + core_l.lesson_id = raw_l.id + AND ( + core_l.title != raw_l.title OR + core_l.duration_min != raw_l.duration_min OR + core_l.course_id != raw_l.course_id + ); -- загуглил как заполнять: INSERT INTO core.dim_date (date, year, month, day, weekday) @@ -90,7 +149,8 @@ SELECT EXTRACT(DAY FROM d)::int, to_char(d, 'day') FROM - generate_series('2023-01-01'::date, '2025-12-31'::date, INTERVAL '1 day') AS d; + generate_series('2023-01-01'::date, '2030-12-31'::date, INTERVAL '1 day') AS d; +-- получается dim_date загружается один раз, и должно храниться где-то отдельно? INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) SELECT From add8d776e5d4c6c0ed39cd642b5edbd18dea6b0c Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sat, 25 Apr 2026 18:11:21 +0300 Subject: [PATCH 115/179] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=203.=20=D0=A1?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9=20Core=20=E2=80=94=20=D0=BE=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5.=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=A0=D0=90=D0=91=D0=9E?= =?UTF-8?q?=D0=A2=D0=90=20=D0=9D=D0=90=D0=94=20=D0=9E=D0=A8=D0=98=D0=91?= =?UTF-8?q?=D0=9A=D0=90=D0=9C=D0=98=20-=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BB=20insert/update=20=D0=B2=20core,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Пет проект: DWH/Step_3_core_layer.sql | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql index 7af360d..c44753d 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -43,6 +43,7 @@ CREATE TABLE core.fact_lesson_views ( course_id bigint NOT NULL REFERENCES core.dim_course(course_id), viewed_at timestamptz NOT NULL ) DISTRIBUTED BY (user_id); +CREATE INDEX idx_fact_lesson_views_viewed_at ON core.fact_lesson_views(viewed_at); --DROP TABLE IF EXISTS core.enrollments; CREATE TABLE core.enrollments ( @@ -63,8 +64,8 @@ SELECT raw_u.registration_date FROM raw.users raw_u -WHERE NOT EXISTS - (SELECT 1 FROM core.dim_user core_u WHERE core_u.user_id = raw_u.id) +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_user core_u WHERE core_u.user_id = raw_u.id) AND raw_u.email IS NOT NULL AND raw_u.registration_date IS NOT NULL; UPDATE core.dim_user core_u @@ -93,9 +94,9 @@ SELECT raw_c.created_at FROM raw.courses raw_c -WHERE NOT EXISTS - (SELECT 1 FROM core.dim_course core_c WHERE core_c.course_id = raw_c.id) - AND raw_c.title IS NOT NULL AND raw_c; +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_course core_c WHERE core_c.course_id = raw_c.id) + AND raw_c.title IS NOT NULL AND raw_c.created_at IS NOT NULL; UPDATE core.dim_course core_c SET @@ -121,8 +122,8 @@ SELECT raw_l.course_id FROM raw.lessons raw_l -WHERE NOT EXISTS - (SELECT 1 FROM core.dim_lesson core_l WHERE core_l.lesson_id = raw_l.id) +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_lesson core_l WHERE core_l.lesson_id = raw_l.id) AND raw_l.title IS NOT NULL AND raw_l.course_id IS NOT NULL; UPDATE core.dim_lesson core_l @@ -140,7 +141,7 @@ WHERE core_l.course_id != raw_l.course_id ); --- загуглил как заполнять: +-- загуглил как заполнять dim_date: INSERT INTO core.dim_date (date, year, month, day, weekday) SELECT d::date, @@ -150,23 +151,32 @@ SELECT to_char(d, 'day') FROM generate_series('2023-01-01'::date, '2030-12-31'::date, INTERVAL '1 day') AS d; --- получается dim_date загружается один раз, и должно храниться где-то отдельно? INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) SELECT - lv.user_id::int, - lv.lesson_id::int, - l.course_id::int, - lv.viewed_at::timestamp + raw_lv.user_id, + raw_lv.lesson_id, + raw_l.course_id, + raw_lv.viewed_at FROM - raw.lesson_views lv + raw.lesson_views raw_lv JOIN - raw.lessons l ON lv.lesson_id = l.id; + raw.lessons raw_l ON raw_lv.lesson_id = raw_l.id +WHERE + raw_lv.viewed_at > (SELECT max(viewed_at) FROM core.fact_lesson_views) -- но при первой загрузке это не сработало, тк max(vied_at) = NULL, как это исправить - я не знаю + AND raw_lv.user_id IS NOT NULL AND raw_lv.lesson_id IS NOT NULL + AND raw_l.course_id IS NOT NULL AND raw_lv.viewed_at IS NOT NULL; INSERT INTO core.enrollments (user_id, course_id, enrolled_at) SELECT - user_id::int, - course_id::int, - enrolled_at::timestamp + raw_e.user_id, + raw_e.course_id, + raw_e.enrolled_at FROM - raw.enrollments; \ No newline at end of file + raw.enrollments raw_e +WHERE + NOT EXISTS (SELECT 1 FROM core.enrollments core_e WHERE + core_e.user_id = raw_e.user_id + AND core_e.course_id = raw_e.course_id + AND core_e.enrolled_at = raw_e.enrolled_at) + AND raw_e.user_id IS NOT NULL AND raw_e.course_id IS NOT NULL AND raw_e.enrolled_at IS NOT NULL; \ No newline at end of file From 66aa47ef26ed1287e3929ae8faab8adabc29fb41 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 26 Apr 2026 13:44:42 +0300 Subject: [PATCH 116/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=AD=D1=82=D0=B0=D0=BF=203.=20?= =?UTF-8?q?=D0=A1=D0=BB=D0=BE=D0=B9=20Core=20=E2=80=94=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BE=D0=B3=D0=B0=D1=89=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5.=20=D0=A0=D0=90=D0=91=D0=9E?= =?UTF-8?q?=D0=A2=D0=90=20=D0=9D=D0=90=D0=94=20=D0=9E=D0=A8=D0=98=D0=91?= =?UTF-8?q?=D0=9A=D0=90=D0=9C=D0=98=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B5=D1=89=D1=91=20=D0=B8=D0=BD=D0=B4=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D0=BE=D0=B2=20=D0=B2=20fact=5Flesson=5Fviews=20?= =?UTF-8?q?=D0=B8=20=D0=B2=20enrollments.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql index c44753d..e03b401 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -44,6 +44,9 @@ CREATE TABLE core.fact_lesson_views ( viewed_at timestamptz NOT NULL ) DISTRIBUTED BY (user_id); CREATE INDEX idx_fact_lesson_views_viewed_at ON core.fact_lesson_views(viewed_at); +CREATE INDEX idx_fact_lesson_views_user_id ON core.fact_lesson_views(user_id); +CREATE INDEX idx_fact_lesson_views_lesson_id ON core.fact_lesson_views(lesson_id); +CREATE INDEX idx_fact_lesson_views_course_id ON core.fact_lesson_views(course_id); --DROP TABLE IF EXISTS core.enrollments; CREATE TABLE core.enrollments ( @@ -51,6 +54,8 @@ CREATE TABLE core.enrollments ( course_id bigint NOT NULL REFERENCES core.dim_course(course_id), enrolled_at timestamptz NOT NULL ) DISTRIBUTED BY (user_id); +CREATE INDEX idx_enrollments_user_id ON core.enrollments(user_id); +CREATE INDEX idx_enrollments_course_id ON core.enrollments(course_id); --6 From 8fd5510097ef80dcbc8e0d33d4cb37ff2f48769d Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 26 Apr 2026 18:14:40 +0300 Subject: [PATCH 117/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=AD=D1=82=D0=B0=D0=BF=204:=20?= =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=20=D0=B2=20Greenplum.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Step_4_preparing_display_cases_at_greenplum.sql | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql b/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql new file mode 100644 index 0000000..de2dac6 --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql @@ -0,0 +1,90 @@ +-- 7 +--DROP MATERIALIZED VIEW dm.lesson_popularity_summary; +CREATE MATERIALIZED VIEW dm.lesson_popularity_summary AS +WITH results_fact_lesson_views AS ( + SELECT + flv.lesson_id, + count(flv.user_id) AS total_views, + count(DISTINCT flv.user_id) AS unique_users, + min(flv.viewed_at)::date AS first_view, + max(flv.viewed_at)::date AS last_view + FROM + core.fact_lesson_views flv + GROUP BY + flv.lesson_id +) +SELECT + dl.lesson_id, + dl.title AS lesson_title, + dl.course_id, + dc.title AS course_title, + rflv.total_views, + rflv.unique_users, + rflv.first_view, + rflv.last_view +FROM + core.dim_lesson dl +JOIN + core.dim_course dc ON dl.course_id = dc.course_id +LEFT JOIN + results_fact_lesson_views rflv ON dl.lesson_id = rflv.lesson_id; + + +-- 8 +--DROP MATERIALIZED VIEW dm.inactive_users_summary; +CREATE MATERIALIZED VIEW dm.inactive_users_summary AS +SELECT + du.user_id, + du.name AS user_name, + du.email, + du.age, + 0 AS enrollments_count, + du.registration_date +FROM + core.dim_user du +WHERE + NOT EXISTS (SELECT 1 FROM core.fact_lesson_views flv WHERE flv.user_id = du.user_id); +-- тут странно, в условии сказано зачем-то enrollment присоединять, но делается все и без него отлично + + +-- 9 +--DROP MATERIALIZED VIEW dm.course_completion_rate; +CREATE MATERIALIZED VIEW dm.course_completion_rate AS +WITH results_dim_course_lesson AS ( + SELECT + dc.course_id, + dc.title, + count(lesson_id) AS lessons_in_course + FROM + core.dim_course dc + JOIN + core.dim_lesson dl ON dc.course_id = dl.course_id + GROUP BY + dc.course_id +), +results_fact_lesson_views AS ( + SELECT + flv.user_id, + flv.course_id, + count(DISTINCT lesson_id) AS lessons_viewed + FROM + core.fact_lesson_views flv + GROUP BY + flv.user_id, flv.course_id +) +SELECT + du.user_id, + du.name AS user_name, + rdcl.course_id, + rdcl.title AS course_title, + rdcl.lessons_in_course, + COALESCE(rflv.lessons_viewed, 0) AS lessons_viewed, + round(COALESCE(rflv.lessons_viewed, 0) / NULLIF(rdcl.lessons_in_course, 0)::numeric, 2) AS completion_rate +FROM + core.dim_user du +JOIN + core.enrollments e ON du.user_id = e.user_id +JOIN + results_dim_course_lesson rdcl ON e.course_id = rdcl.course_id +LEFT JOIN + results_fact_lesson_views rflv ON e.user_id = rflv.user_id AND e.course_id = rflv.course_id; \ No newline at end of file From 4b51ffb724c8fdb0eddf1defc1ea0d929918bcbe Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 26 Apr 2026 18:21:34 +0300 Subject: [PATCH 118/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20DWH.=20=D0=AD=D1=82=D0=B0=D0=BF=205.=20?= =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql b/mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql new file mode 100644 index 0000000..f2d8cb9 --- /dev/null +++ b/mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql @@ -0,0 +1,4 @@ +SELECT * FROM dm.lesson_popularity_summary ORDER BY lesson_id LIMIT 10; +SELECT * FROM dm.inactive_users_summary ORDER BY user_id LIMIT 10; +SELECT * FROM dm.course_completion_rate ORDER BY user_id, course_id; +--пришлось добавить order_by для красивого вывода, потому что создать витрины отсортированными не получается \ No newline at end of file From 347334dc26e23ad8121e35e62554986021306004 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 28 Apr 2026 04:30:01 +0300 Subject: [PATCH 119/179] =?UTF-8?q?=D0=A0=D0=90=D0=91=D0=9E=D0=A2=D0=90=20?= =?UTF-8?q?=D0=9D=D0=90=D0=94=20=D0=9E=D0=A8=D0=98=D0=91=D0=9A=D0=90=D0=9C?= =?UTF-8?q?=D0=98.=20=D0=9F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA?= =?UTF-8?q?=D1=82:=20DWH.=20=D0=AD=D1=82=D0=B0=D0=BF=204:=20=D0=9F=D0=BE?= =?UTF-8?q?=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=BD=20=D0=B2=20Greenplum.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Пет проект: DWH/Step_3_core_layer.sql | 2 ++ .../Step_4_preparing_display_cases_at_greenplum.sql | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql index e03b401..a804a5c 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql @@ -157,6 +157,7 @@ SELECT FROM generate_series('2023-01-01'::date, '2030-12-31'::date, INTERVAL '1 day') AS d; +-- fact_lesson_views INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) SELECT raw_lv.user_id, @@ -172,6 +173,7 @@ WHERE AND raw_lv.user_id IS NOT NULL AND raw_lv.lesson_id IS NOT NULL AND raw_l.course_id IS NOT NULL AND raw_lv.viewed_at IS NOT NULL; +-- enrollments INSERT INTO core.enrollments (user_id, course_id, enrolled_at) SELECT raw_e.user_id, diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql b/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql index de2dac6..be37917 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql @@ -38,13 +38,22 @@ SELECT du.name AS user_name, du.email, du.age, - 0 AS enrollments_count, + e.enrollments_count, du.registration_date FROM core.dim_user du +LEFT JOIN ( + SELECT + user_id, + count(course_id) AS enrollments_count + FROM + core.enrollments + GROUP BY + user_id +) e ON du.user_id = e.user_id WHERE NOT EXISTS (SELECT 1 FROM core.fact_lesson_views flv WHERE flv.user_id = du.user_id); --- тут странно, в условии сказано зачем-то enrollment присоединять, но делается все и без него отлично +-- интересно, что при проверки плана запроса, когда кусок с enrollments_count был в CTE, выдавал какую-то хуйню, а когда я засунул его в подзапрос, сразу инлексы заработали, и строки начали эффективней читаться -- 9 @@ -60,7 +69,7 @@ WITH results_dim_course_lesson AS ( JOIN core.dim_lesson dl ON dc.course_id = dl.course_id GROUP BY - dc.course_id + dc.course_id, dc.title ), results_fact_lesson_views AS ( SELECT @@ -79,7 +88,7 @@ SELECT rdcl.title AS course_title, rdcl.lessons_in_course, COALESCE(rflv.lessons_viewed, 0) AS lessons_viewed, - round(COALESCE(rflv.lessons_viewed, 0) / NULLIF(rdcl.lessons_in_course, 0)::numeric, 2) AS completion_rate + round(COALESCE(rflv.lessons_viewed, 0)::numeric / NULLIF(rdcl.lessons_in_course, 0), 2) AS completion_rate FROM core.dim_user du JOIN From 2ced7cf273b03896b1e2c1d2a3c63024b83dafe2 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 30 Apr 2026 22:58:25 +0400 Subject: [PATCH 120/179] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D1=87=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D1=82=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D1=83=20=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20-=20JOIN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clickhouse/Homework/Запросы - JOIN.sql | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Запросы - JOIN.sql diff --git a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql new file mode 100644 index 0000000..29496d9 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql @@ -0,0 +1,70 @@ + +-- Задачи на отработку Запросы - JOIN -- + +DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE products +ON CLUSTER clickhouse_cluster +( + product_id UInt32, + name String +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/products', + '{replica}' +) +ORDER BY product_id; + + +INSERT INTO products VALUES +(101, 'iPhone 14'), +(102, 'Galaxy S22'), +(103, 'Pixel 7'); + +CREATE TABLE prices +ON CLUSTER clickhouse_cluster +( + product_id UInt32, + price UInt32, + region String +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/prices', + '{replica}' +) +ORDER BY product_id; + +INSERT INTO prices VALUES +(101, 999, 'US'), +(101, 1049, 'EU'), +(102, 850, 'US'), +(102, 899, 'CA'), +(103, 799, 'JP'); + + +--№1 Сделай ANY LEFT JOIN, чтобы получить цену любой из регионов. + +SELECT + p.product_id, + p.name, + pr.price, + pr.region +FROM products AS p +ANY LEFT JOIN prices AS pr ON p.product_id = pr.product_id; + +--№2 Что будет, если в таблице `prices` несколько цен на один `product_id`? + +/* Для одного product_id будет выбрана только первая ЛЮБАЯ (ANY) подходящая в поиске.*/ + +--№3 Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают? + +SELECT + p.product_id, + p.name, + pr.price, + pr.region +FROM products AS p +ALL LEFT JOIN prices AS pr ON p.product_id = pr.product_id; + +/* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения. + Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU. + короче, по сути ALL LEFT JOIN создает декартово произведение*/ From 46a366c07a5fe579ae23b9cfac39c32ec69280ee Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Thu, 30 Apr 2026 23:59:43 +0400 Subject: [PATCH 121/179] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D1=85=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clickhouse/Homework/Запросы - JOIN.sql | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql index 29496d9..6d6edfa 100644 --- a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql +++ b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql @@ -4,7 +4,7 @@ DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC; DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC; -CREATE TABLE products +CREATE TABLE dsolovianov.products ON CLUSTER clickhouse_cluster ( product_id UInt32, @@ -16,12 +16,12 @@ ON CLUSTER clickhouse_cluster ORDER BY product_id; -INSERT INTO products VALUES +INSERT INTO dsolovianov.products VALUES (101, 'iPhone 14'), (102, 'Galaxy S22'), (103, 'Pixel 7'); -CREATE TABLE prices +CREATE TABLE dsolovianov.prices ON CLUSTER clickhouse_cluster ( product_id UInt32, @@ -33,7 +33,7 @@ ON CLUSTER clickhouse_cluster ) ORDER BY product_id; -INSERT INTO prices VALUES +INSERT INTO dsolovianov.prices VALUES (101, 999, 'US'), (101, 1049, 'EU'), (102, 850, 'US'), @@ -48,8 +48,8 @@ SELECT p.name, pr.price, pr.region -FROM products AS p -ANY LEFT JOIN prices AS pr ON p.product_id = pr.product_id; +FROM dsolovianov.products AS p +ANY LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id; --№2 Что будет, если в таблице `prices` несколько цен на один `product_id`? @@ -62,8 +62,8 @@ SELECT p.name, pr.price, pr.region -FROM products AS p -ALL LEFT JOIN prices AS pr ON p.product_id = pr.product_id; +FROM dsolovianov.products AS p +ALL LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id; /* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения. Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU. From 4a602481a386862925262b5674acafa30d5e837f Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Fri, 1 May 2026 00:02:20 +0400 Subject: [PATCH 122/179] ORDER BY product_id, region IN TABLE dsolovianov.prices --- dsolovianov/clickhouse/Homework/Запросы - JOIN.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql index 6d6edfa..21108c6 100644 --- a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql +++ b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql @@ -31,7 +31,7 @@ ON CLUSTER clickhouse_cluster '/clickhouse/tables/{shard}/dsolovianov/prices', '{replica}' ) -ORDER BY product_id; +ORDER BY product_id, region; INSERT INTO dsolovianov.prices VALUES (101, 999, 'US'), From d1acaf78a69955c61bbd38dd5a6e5a799a2006fb Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Fri, 1 May 2026 00:06:47 +0400 Subject: [PATCH 123/179] =?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsolovianov/clickhouse/Homework/Запросы - JOIN.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql index 21108c6..6d6edfa 100644 --- a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql +++ b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql @@ -31,7 +31,7 @@ ON CLUSTER clickhouse_cluster '/clickhouse/tables/{shard}/dsolovianov/prices', '{replica}' ) -ORDER BY product_id, region; +ORDER BY product_id; INSERT INTO dsolovianov.prices VALUES (101, 999, 'US'), From 209cce48b2d60233217735a5c452395f0429e247 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 4 May 2026 07:16:07 +0400 Subject: [PATCH 124/179] =?UTF-8?q?=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D1=81=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20-=20?= =?UTF-8?q?=D0=9F=D0=B0=D1=80=D1=82=D0=B8=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Индексы и сжатие - Партиции.sql | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql new file mode 100644 index 0000000..4e9ebb8 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql @@ -0,0 +1,146 @@ +-- Индексы и сжатие - Партиции -- + +-- #1 +/* +1. Создай таблицу sales с `PARTITION BY toYYYYMM(sale_date)`. +2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`. +3. Что произойдет, если не указывать `PARTITION BY`? <- удалит все данные из таблицы. партиция то одна :) + +*/ + +DROP TABLE IF EXISTS dsolovianov.sales ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.sales +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + sale_date Date, + amount Float64 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/sales', + '{replica}' +) +PARTITION BY toYYYYMM(sale_date) +ORDER BY (region, sale_date); + +INSERT INTO dsolovianov.sales (id, region, sale_date, amount) VALUES +(1, 'RU', '2024-05-01', '100.0'), +(2, 'RU', '2024-05-15', '120.0'), +(3, 'US', '2024-06-01', '90.0'), +(4, 'US', '2024-07-03', '150.0'), +(5, 'DE', '2024-07-04', '160.0'); + +ALTER TABLE dsolovianov.sales DROP PARTITION '202405'; + +SELECT * +FROM dsolovianov.sales; + +-- #2 +/* +1. Создай таблицу с `PARTITION BY event_type`. +2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`). +3. Чем такой подход может быть опасен (подумай про кардинальность значений)? +При большом количестве уникальных event_type создастся много мелких партиций. +*/ +DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.events +ON CLUSTER clickhouse_cluster +( + id UInt64, + event_type String, + event_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/events', + '{replica}' +) +PARTITION BY event_type +ORDER BY (event_date, id); + +INSERT INTO dsolovianov.events (id, event_type, event_date) VALUES +(1, 'login', '2024-06-01'), +(2, 'click', '2024-06-01'), +(3, 'click', '2024-06-02'), +(4, 'purchase', '2024-06-03'), +(5, 'login', '2024-06-04'); + +ALTER TABLE dsolovianov.events DROP PARTITION 'click'; + +-- #3 +/* +1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`. +2. Что ты выиграешь с точки зрения баланса? +Данные распределятся примерно поровну между 100 партициями. Упрощается управление удалением старых данных (можно дропать целые партиции) +3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`? +Потому что партиции определяются хэшем, а не прямым значением user_id и к тому же сортировка в партициях по log_time, а не user_id. + +*/ + +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + user_id UInt64, + log_time DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +PARTITION BY cityHash64(user_id) % 100 -- 100 партиций по хэшу user_id +ORDER BY (log_time, id); + +INSERT INTO dsolovianov.logs (id, user_id, log_time) VALUES +(1, 123, '2024-07-17 10:00:00'), +(2, 456, '2024-07-17 10:01:00'), +(3, 789, '2024-07-17 10:02:00'); + +SELECT + id, + user_id, + cityHash64(user_id) % 100 AS partition_number +FROM dsolovianov.logs +ORDER BY id; + +SELECT * FROM logs WHERE user_id = 456; + + +-- #4 +/* +1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`. +2. Как будет выглядеть имя партиции? +Имена партиций будут в формате: ('значение_region', 'YYYYMM'). Пример: ('RU', '202406') +3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? Да. Удаляется целая партиция без сканирования и нагрузки на систему. +*/ + +DROP TABLE IF EXISTS dsolovianov.visits ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.visits +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + visit_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/visits', + '{replica}' +) +PARTITION BY (region, toYYYYMM(visit_date)) +ORDER BY (region, visit_date, id); + +INSERT INTO dsolovianov.visits (id, region, visit_date) VALUES +(1, 'RU', '2024-06-01'), +(2, 'RU', '2024-06-15'), +(3, 'US', '2024-06-03'), +(4, 'US', '2024-07-01'), +(5, 'RU', '2024-07-05'); + +ALTER TABLE dsolovianov.visits ON CLUSTER clickhouse_cluster +DROP PARTITION ('RU', '202407'); + +SELECT * FROM dsolovianov.visits; \ No newline at end of file From 1de11c1d5e7a45835fa6c8ae39f36c1f734a2dd1 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 4 May 2026 08:09:54 +0400 Subject: [PATCH 125/179] =?UTF-8?q?=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D1=81=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20-=20?= =?UTF-8?q?=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Индексы и сжатие - Индексы.sql | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql new file mode 100644 index 0000000..9332b35 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql @@ -0,0 +1,166 @@ +-- Индексы и сжатие - Индексы -- + +-- #1 +/* +3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`). +Не посмотрю, так как нет доступа system.query_log, rows_read, rows (ACCESS_DENIED) +4. Что будет, если `ORDER BY log_id`, а не `ts`? +ORDER BY log_id → данные физически отсортированы по log_id. Значения ts расположены хаотично относительно физической сортировки. +Индекс minmax по ts будет почти бесполезен. Minmax индекс будет иметь широкие диапазоны +*/ + +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + log_id UInt64, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +ORDER BY ts +PARTITION BY toYYYYMM(ts) +PRIMARY KEY (ts); + +INSERT INTO dsolovianov.logs (log_id, ts) VALUES +(1, '2024-06-01 10:00:00'), +(2, '2024-06-01 11:00:00'), +(3, '2024-06-02 12:00:00'), +(4, '2024-06-03 13:00:00'), +(5, '2024-06-04 14:00:00'); + +-- 1. Добавляем minmax индекс: +ALTER TABLE dsolovianov.logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2; + +-- 2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'` +SELECT * +FROM dsolovianov.logs +WHERE ts + BETWEEN '2024-06-02' AND '2024-06-03'; + +-- Раз нет доступа, но можно предположить, что вот такой запрос сработает: +SELECT query, read_rows, result_rows +FROM system.query_log +WHERE query LIKE '%logs%' +ORDER BY event_time DESC +LIMIT 1; + + +-- #2 +/* +1. Создай таблицу с `ORDER BY id`, добавь индекс: + + ```sql + ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + ``` + + +3. Посмотри `EXPLAIN` — индекс используется? +Данные отсортированы по id, а не по country. Значения country распределены хаотически относительно физического порядка. +4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений? +Будет создано 10M партиций, что убьёт производительность. +**/ +DROP TABLE IF EXISTS dsolovianov.clicks ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.clicks +ON CLUSTER clickhouse_cluster +( + id UInt64, + country String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/clicks', + '{replica}' +) +ORDER BY id +PARTITION BY country +PRIMARY KEY (id); + +INSERT INTO dsolovianov.clicks (id, country) VALUES +(1, 'RU'), +(2, 'US'), +(3, 'RU'), +(4, 'DE'), +(5, 'RU'); + +ALTER TABLE dsolovianov.clicks ON CLUSTER clickhouse_cluster +ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + +-- 2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`. +EXPLAIN indexes = 1 +SELECT * FROM dsolovianov.clicks WHERE country = 'RU'; + +EXPLAIN +SELECT * FROM dsolovianov.clicks WHERE country = 'RU'; + +/* +Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dsolovianov.clicks) + Indexes: + MinMax + Keys: + country + Condition: (country in ['RU', 'RU']) + Parts: 1/3 + Granules: 1/3 + Partition + Keys: + country + Condition: (country in ['RU', 'RU']) + Parts: 1/1 + Granules: 1/1 + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_country + Description: set GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + */ + +-- #3 + +DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + email String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/users', + '{replica}' +) +ORDER BY id +PRIMARY KEY (id); + +INSERT INTO dsolovianov.users (id, email) VALUES +(1, 'alice@mail.com'), +(2, 'bob@mail.com'), +(3, 'charlie@mail.com'), +(4, 'dave@mail.com'), +(5, 'eve@example.com'); + +ALTER TABLE dsolovianov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email = 'alice@mail.com'; +-- Bloom filter может пропустить некоторые гранулы, но не все, ибо данные не отсортированы по email и индексные блоки покрывают случайные email'ы + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email LIKE '%mail%'; + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email LIKE 'alice%'; +/* +Bloom filter не поддерживает частичные совпадения (LIKE '..%', LIKE '%..', LIKE '%..%'). Из-за этого ClickHouse не может пропустить гранулы. +Клику становится сложно вычислить хэш для проверки, если подходящих значений по LIKE будет больше одного. Нет гарантии, что данные на выходе будут корректные. +*/ \ No newline at end of file From c654840ffdbbc003bb92a186980c507ac90b9b8e Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 4 May 2026 09:15:50 +0400 Subject: [PATCH 126/179] =?UTF-8?q?=D0=98=D0=BD=D0=B4=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D1=81=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20-=20?= =?UTF-8?q?=D0=A1=D0=B6=D0=B0=D1=82=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Homework/Индексы и сжатие - Сжатие.sql | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql new file mode 100644 index 0000000..ce28674 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql @@ -0,0 +1,39 @@ +-- Индексы и сжатие - Сжатие -- + +-- #1 Создай таблицу logs с колонкой message CODEC(ZSTD(3)). +-- #2 Добавь created_at CODEC(Delta, ZSTD) — это улучшит хранение по времени. +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + level String, + message String CODEC(ZSTD(3)), + created_at DateTime CODEC(Delta, ZSTD) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +ORDER BY (created_at, id) +PARTITION BY toYYYYMM(created_at); + +INSERT INTO dsolovianov.logs (id, level, message, created_at) VALUES +(1, 'INFO', 'User logged in', '2024-07-01 10:00:00'), +(2, 'WARN', 'Request took too long', '2024-07-01 10:05:00'), +(3, 'ERROR', 'Unable to connect to server', '2024-07-01 10:10:00'); + +-- #3 Посмотри статистику сжатия: +SELECT + column, + compression_codec +FROM system.columns +WHERE table = 'logs'; + +-- Стата вот так выглядит: +column | compression_codec +id +level +message CODEC(ZSTD(3)) +created_at CODEC(Delta(4), ZSTD(1)) \ No newline at end of file From 3f4fe5f0157a28f2eb0288aba523ba3120677bee Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Wed, 6 May 2026 22:31:35 +0300 Subject: [PATCH 127/179] add home work on ddl --- vberetar/postgresql/ddl.sql | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 vberetar/postgresql/ddl.sql diff --git a/vberetar/postgresql/ddl.sql b/vberetar/postgresql/ddl.sql new file mode 100644 index 0000000..19fd57f --- /dev/null +++ b/vberetar/postgresql/ddl.sql @@ -0,0 +1,39 @@ +CREATE TABLE users ( + id INT PRIMARY KEY NOT NULL, + name VARCHAR(100) NOT NULL, + age SMALLINT, + email VARCHAR(100), + country VARCHAR(50), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL +); +-- age - для возраста не нужны болшие числа +-- country - можно было бы использовать VARCHAR(2), я бы тут спросил у тимлида как будет удобней использовать поле) +-- created_at - нужно знать не только дату но и время создания/регистрации +-- id - если не прописать not null в primary key, то not null ставится автоматически? + +CREATE TABLE products ( + id INT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10, 2) NOT NULL, + in_stock BOOLEAN +); + +CREATE TABLE orders( + id INT PRIMARY KEY, + user_id INT, + order_date TIMESTAMPTZ NOT NULL, + status VARCHAR(20), + FOREING KEY (user_id) REFERENCES users(id) +); +-- status - лучше прописать словами статус, так понятней будет. Я бы сюда поставил ENUM + +CREATE TABLE order_items( + order_id INT, + product_id INT, + quantity INT NOT NULL, + price_at_purchase DECIMAL(10, 2) NOT NULL, + FOREING KEY (order_id) REFERENCES orders(id), + FOREING KEY (product_id) REFERENCES products(id) +); \ No newline at end of file From 06ad98f4faf3d7ad6812f0b5661247f12edae62c Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Fri, 8 May 2026 11:46:36 +0300 Subject: [PATCH 128/179] fix columns in tables --- vberetar/postgresql/ddl.sql | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/vberetar/postgresql/ddl.sql b/vberetar/postgresql/ddl.sql index 19fd57f..ce86724 100644 --- a/vberetar/postgresql/ddl.sql +++ b/vberetar/postgresql/ddl.sql @@ -1,8 +1,8 @@ CREATE TABLE users ( - id INT PRIMARY KEY NOT NULL, + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, - age SMALLINT, - email VARCHAR(100), + age SMALLINT CHECK (age > 0), + email VARCHAR(100) NOT NULL UNIQUE, country VARCHAR(50), is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL @@ -13,27 +13,26 @@ CREATE TABLE users ( -- id - если не прописать not null в primary key, то not null ставится автоматически? CREATE TABLE products ( - id INT PRIMARY KEY, + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50), price DECIMAL(10, 2) NOT NULL, - in_stock BOOLEAN + in_stock BOOLEAN DEFAULT true, ); CREATE TABLE orders( - id INT PRIMARY KEY, + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INT, order_date TIMESTAMPTZ NOT NULL, status VARCHAR(20), - FOREING KEY (user_id) REFERENCES users(id) + FOREIGN KEY (user_id) REFERENCES users(id) ); -- status - лучше прописать словами статус, так понятней будет. Я бы сюда поставил ENUM CREATE TABLE order_items( - order_id INT, - product_id INT, + order_id INT NOT NULL, + product_id INT NOT NULL, quantity INT NOT NULL, price_at_purchase DECIMAL(10, 2) NOT NULL, - FOREING KEY (order_id) REFERENCES orders(id), - FOREING KEY (product_id) REFERENCES products(id) + PRIMARY KEY (order_id, product_id) ); \ No newline at end of file From a3342be276339ca64133bd07969e8488f67aacd8 Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Fri, 8 May 2026 11:50:55 +0300 Subject: [PATCH 129/179] add DML home work --- vberetar/postgresql/dml.sql | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 vberetar/postgresql/dml.sql diff --git a/vberetar/postgresql/dml.sql b/vberetar/postgresql/dml.sql new file mode 100644 index 0000000..8829f99 --- /dev/null +++ b/vberetar/postgresql/dml.sql @@ -0,0 +1,42 @@ +INSERT INTO users (id, name, age, email, country, is_active, created_at) +VALUES (1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'), + (2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'), + (3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'), + (4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'), + (5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'), + (6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'), + (7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'), + (8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'), + (9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'), + (10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00'); + +INSERT INTO products (id, name, category, price, in_stock) +VALUES (1, 'iPhone 15', 'Электроника', 80000, true), + (2, 'Kindle', 'Электроника', 15000, true), + (3, 'Кроссовки Nike', 'Одежда', 9000, true), + (4, 'Кроссовки NoName', 'Одежда', 3000, false), + (5, 'Наушники Pro', 'Электроника', 12000, true), + (6, 'Фитнес‑браслет', 'Гаджеты', 6000, true), + (7, 'Подарочная карта', 'Сервисы', 1000, true); + +INSERT INTO orders (id, user_id, order_date, status) +VALUES (1, 1, '2025-01-10 12:00:00', 'paid'), + (2, 3, '2025-01-12 09:30:00', 'cancelled'), + (3, 1, '2025-01-15 18:20:00', 'paid'), + (4, 2, '2025-01-20 11:05:00', 'created'), + (5, 4, '2025-02-01 17:45:00', 'paid'), + (6, 5, '2025-02-10 10:10:00', 'refunded'), + (7, 7, '2025-03-01 09:00:00', 'paid'), + (8, 9, '2025-03-05 19:30:00', 'paid'); + +INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) +VALUES (1, 1, 1, 78000), + (1, 3, 2, 8500), + (2, 2, 1, 14000), + (3, 5, 1, 11000), + (3, 7, 1, 1000), + (4, 2, 1, 15000), + (5, 6, 1, 6000), + (6, 1, 1, 82000), + (7, 3, 1, 9000), + (8, 4, 2, 2800); From 4ffe6cac39463fa423246172f0df747f0b481817 Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Sun, 10 May 2026 21:57:26 +0300 Subject: [PATCH 130/179] fix query on dml.sql --- vberetar/postgresql/dml.sql | 55 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/vberetar/postgresql/dml.sql b/vberetar/postgresql/dml.sql index 8829f99..24e8d83 100644 --- a/vberetar/postgresql/dml.sql +++ b/vberetar/postgresql/dml.sql @@ -1,33 +1,32 @@ -INSERT INTO users (id, name, age, email, country, is_active, created_at) -VALUES (1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'), - (2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'), - (3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'), - (4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'), - (5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'), - (6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'), - (7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'), - (8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'), - (9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'), - (10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00'); +INSERT INTO users (name, age, email, country, is_active, created_at) +VALUES ('Alice', 25, LOWER('alice@example.com'), 'US', true, '2024-01-10 10:00:00'), + ('Bob', 30, LOWER('bob@example.com'), 'UK', true, '2024-02-05 09:30:00'), + ('Carol', 27, LOWER('carol@example.com'), 'US', false, '2024-03-01 15:20:00'), + ('Diana', 22, LOWER('diana@GMAIL.com'), 'US', true, '2024-01-12 08:15:00'), + ('Evan', 35, LOWER('EVAN@example.com'), 'UK', true, '2024-01-18 19:40:00'), + ('Farida', 29, LOWER('farida@example.com'), 'KZ', true, '2024-02-20 11:05:00'), + ('George', 31, LOWER('george+test@example.com'), 'US', true, '2024-03-05 16:45:00'), + ('Ivan', 26, LOWER('ivan@example.com'), 'RU', true, '2024-01-11 14:30:00'), + ('Julia', 28, LOWER('julia@example.com'), 'US', true, '2024-03-07 18:05:00'); -INSERT INTO products (id, name, category, price, in_stock) -VALUES (1, 'iPhone 15', 'Электроника', 80000, true), - (2, 'Kindle', 'Электроника', 15000, true), - (3, 'Кроссовки Nike', 'Одежда', 9000, true), - (4, 'Кроссовки NoName', 'Одежда', 3000, false), - (5, 'Наушники Pro', 'Электроника', 12000, true), - (6, 'Фитнес‑браслет', 'Гаджеты', 6000, true), - (7, 'Подарочная карта', 'Сервисы', 1000, true); +INSERT INTO products (name, category, price, in_stock) +VALUES ('iPhone 15', 'Электроника', 80000, true), + ('Kindle', 'Электроника', 15000, true), + ('Кроссовки Nike', 'Одежда', 9000, true), + ('Кроссовки NoName', 'Одежда', 3000, false), + ('Наушники Pro', 'Электроника', 12000, true), + ('Фитнес‑браслет', 'Гаджеты', 6000, true), + ('Подарочная карта', 'Сервисы', 1000, true); -INSERT INTO orders (id, user_id, order_date, status) -VALUES (1, 1, '2025-01-10 12:00:00', 'paid'), - (2, 3, '2025-01-12 09:30:00', 'cancelled'), - (3, 1, '2025-01-15 18:20:00', 'paid'), - (4, 2, '2025-01-20 11:05:00', 'created'), - (5, 4, '2025-02-01 17:45:00', 'paid'), - (6, 5, '2025-02-10 10:10:00', 'refunded'), - (7, 7, '2025-03-01 09:00:00', 'paid'), - (8, 9, '2025-03-05 19:30:00', 'paid'); +INSERT INTO orders (user_id, order_date, status) +VALUES (1, '2025-01-10 12:00:00', 'paid'), + (3, '2025-01-12 09:30:00', 'cancelled'), + (1, '2025-01-15 18:20:00', 'paid'), + (2, '2025-01-20 11:05:00', 'created'), + (4, '2025-02-01 17:45:00', 'paid'), + (5, '2025-02-10 10:10:00', 'refunded'), + (7, '2025-03-01 09:00:00', 'paid'), + (9, '2025-03-05 19:30:00', 'paid'); INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) VALUES (1, 1, 1, 78000), From d280456d0919ddc8f64382d0c7b16846ab8cb38b Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 04:21:02 +0400 Subject: [PATCH 131/179] =?UTF-8?q?=D0=BF=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 1. Загрузка тестовых данных.sql | 90 +++++++++ .../Этап 2. Создание витринных таблиц.sql | 186 ++++++++++++++++++ .../Pet-Project/Этап 3. Анализ и запросы.sql | 88 +++++++++ .../Этап 4. Тестирование и валидация.sql | 13 ++ 4 files changed, 377 insertions(+) create mode 100644 dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql create mode 100644 dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql create mode 100644 dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql create mode 100644 dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql b/dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql new file mode 100644 index 0000000..167607d --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql @@ -0,0 +1,90 @@ +DROP TABLE IF EXISTS dsolovianov.core_fact_lesson_views ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_course ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_date ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_enrollments ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.core_dim_user +ON CLUSTER clickhouse_cluster +( + user_id Int32, + name String, + age Int32, + email String, + registration_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dsolovianov.core_dim_course +ON CLUSTER clickhouse_cluster +( + course_id Int32, + title String, + category String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +CREATE TABLE dsolovianov.core_dim_lesson +ON CLUSTER clickhouse_cluster +( + lesson_id Int32, + title String, + duration_min Int32, + course_id Int32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_lesson', + '{replica}' +) +ORDER BY lesson_id; + +CREATE TABLE dsolovianov.core_dim_date +ON CLUSTER clickhouse_cluster +( + date Date, + year Int32, + month Int32, + day Int32, + weekday String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_date', + '{replica}' +) +ORDER BY date; + +CREATE TABLE dsolovianov.core_enrollments +ON CLUSTER clickhouse_cluster +( + user_id Int32, + course_id Int32, + enrolled_at DateTime64(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_enrollments', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dsolovianov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster +( + user_id Int32, + lesson_id Int32, + course_id Int32, + viewed_at DateTime64(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_fact_lesson_views', + '{replica}' +) +ORDER BY user_id; \ No newline at end of file diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql b/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql new file mode 100644 index 0000000..e2dc606 --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql @@ -0,0 +1,186 @@ +-- Этап 2. Создание витринных таблиц -- + +-- Витрина lesson_popularity_summary +DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster ( + lesson_id UInt32, + lesson_title String, + course_id UInt32, + course_title String, + total_views UInt64, + unique_users UInt64, + first_view DateTime, + last_view DateTime +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/lesson_popularity_summary', + '{replica}' +) +ORDER BY (course_id, lesson_id); + +CREATE MATERIALIZED VIEW dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster +TO dsolovianov.lesson_popularity_summary +AS +SELECT + l.lesson_id AS lesson_id, + l.title AS lesson_title, + l.course_id AS course_id, + c.title AS course_title, + COUNT(v.viewed_at) AS total_views, + COUNT(DISTINCT v.user_id) AS unique_users, + MIN(v.viewed_at) AS first_view, + MAX(v.viewed_at) AS last_view +FROM dsolovianov.core_fact_lesson_views v +LEFT JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id +LEFT JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id +GROUP BY l.lesson_id, l.title, l.course_id, c.title; + +INSERT INTO dsolovianov.lesson_popularity_summary +SELECT + l.lesson_id AS lesson_id, + l.title AS lesson_title, + l.course_id AS course_id, + c.title AS course_title, + COUNT(v.viewed_at) AS total_views, + COUNT(DISTINCT v.user_id) AS unique_users, + MIN(v.viewed_at) AS first_view, + MAX(v.viewed_at) AS last_view +FROM dsolovianov.core_dim_lesson l +JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id +LEFT JOIN dsolovianov.core_fact_lesson_views v ON l.lesson_id = v.lesson_id +GROUP BY l.lesson_id, l.title, l.course_id, c.title; + +-- Витрину inactive_users_summary + +DROP TABLE IF EXISTS dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster ( + user_id UInt32, + name String, + email String, + age UInt8, + registered_courses_count UInt32, + registration_date Date +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/inactive_users_summary', + '{replica}' +) +ORDER BY user_id; + +CREATE MATERIALIZED VIEW dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster +TO dsolovianov.inactive_users_summary +AS +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + COUNT(DISTINCT e.course_id) AS registered_courses_count, + u.registration_date AS registration_date +FROM dsolovianov.core_dim_user u +LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views) +GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date; + +INSERT INTO dsolovianov.inactive_users_summary +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + COUNT(DISTINCT e.course_id) AS registered_courses_count, + u.registration_date AS registration_date +FROM dsolovianov.core_dim_user u +LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views) +GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date; + +-- Витрина course_completion_rate + +DROP TABLE IF EXISTS dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster ( + user_id UInt32, + user_name String, + course_id UInt32, + course_title String, + lessons_in_course UInt32, + lessons_viewed UInt32, + completion_rate Float32 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id); + +DROP TABLE IF EXISTS dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster SYNC; + +CREATE MATERIALIZED VIEW dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster +TO dsolovianov.course_completion_rate +AS +SELECT + e.user_id AS user_id, + u.name AS user_name, + e.course_id AS course_id, + c.title AS course_title, + ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS lessons_in_course, + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ) AS lessons_viewed, + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ) / ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS completion_rate +FROM dsolovianov.core_enrollments e +JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id +JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id +GROUP BY e.user_id, u.name, e.course_id, c.title; + +INSERT INTO dsolovianov.course_completion_rate +SELECT + e.user_id AS user_id, + u.name AS user_name, + e.course_id AS course_id, + c.title AS course_title, + ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS lessons_in_course, + COALESCE( + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ), 0 + ) AS lessons_viewed, + COALESCE( + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ), 0 + ) / ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS completion_rate +FROM dsolovianov.core_enrollments e +JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id +JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id +GROUP BY e.user_id, u.name, e.course_id, c.title; diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql b/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql new file mode 100644 index 0000000..6550e5b --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql @@ -0,0 +1,88 @@ +-- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам -- Напиши свои вариации для практики-- +-- Топ-5 самых просматриваемых уроков + +SELECT + lesson_title, + course_title, + total_views +FROM lesson_popularity_summary +ORDER BY total_views DESC +LIMIT 5; + +-- Неактивные пользователи, записавшиеся на курсы + +SELECT + name, + email, + registered_courses_count +FROM inactive_users_summary +WHERE registered_courses_count > 0 +ORDER BY registration_date DESC; + +-- Курсы с самым высоким процентом завершения + +SELECT + course_title, + AVG(completion_rate) AS avg_completion +FROM course_completion_rate +GROUP BY course_title +ORDER BY avg_completion DESC +LIMIT 5; + +-- Мои вариации -- + +-- Среднее количество учеников на урок по каждому курсу + +SELECT + course_title, + ROUND(AVG(unique_users), 2) AS avg_users_lesson, + SUM(total_views) AS total_course_views +FROM dsolovianov.lesson_popularity_summary +GROUP BY course_title +ORDER BY avg_users_lesson DESC; + +-- Возрастное распределение неактивных пользователей + +SELECT + CASE + WHEN age < 25 THEN 'до 25' + WHEN age BETWEEN 25 AND 30 THEN '25-30' + WHEN age BETWEEN 31 AND 35 THEN '31-35' + ELSE '35+' + END AS age_group, + COUNT(*) AS inactive_users_count, + AVG(registered_courses_count) AS avg_courses_enrolled +FROM dsolovianov.inactive_users_summary +GROUP BY age_group +ORDER BY age_group; + +-- Пользователи, которые начали, но не закончили курс (completion_rate между 0 и 1) + +SELECT + user_name, + course_title, + lessons_viewed, + lessons_in_course, + ROUND(completion_rate * 100, 0) AS no_completion, + (lessons_in_course - lessons_viewed) AS remaining_lessons +FROM dsolovianov.course_completion_rate +WHERE completion_rate BETWEEN 0.01 AND 0.99 +ORDER BY completion_rate DESC, user_name; + +-- Прогресс каждого пользователя по курсам (с указанием статуса) + +SELECT + user_name, + course_title, + lessons_viewed, + lessons_in_course, + ROUND(completion_rate * 100, 0) AS progress_percent, + CASE + WHEN completion_rate = 1.0 THEN 'Пройден' + WHEN completion_rate = 0 THEN 'Не начат' + WHEN completion_rate >= 0.5 THEN 'В процессе (>50%)' + ELSE 'В процессе (<50%)' + END AS status +FROM dsolovianov.course_completion_rate +ORDER BY user_name, completion_rate DESC; + diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql b/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql new file mode 100644 index 0000000..f053160 --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql @@ -0,0 +1,13 @@ +-- Этап 4. Тестирование и валидация -- + +/* +### **Задание 6: Сравни данные с Greenplum** + +1. Выбери 2–3 значения из таблиц `_summary_source` в Greenplum +2. Сравни с ClickHouse-результатами: + - Совпадает ли `SUM`, `AVG`, `MIN`, `MAX` + - Проверяй `NULL`, `0`, округления +3. Напиши мини-отчёт: **"где совпадает, где нет, почему"** +*/ + +-- Ничего не понятно по этому этапу. У меня в GP нет таблиц "_summary_source" From 0f63e2135317f1e3b37f043769c246872b360fdd Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 06:50:16 +0400 Subject: [PATCH 132/179] DDL, DML, Basic, Filter --- dsolovianov/postgresql/DDL.sql | 32 +++++++ dsolovianov/postgresql/DML.sql | 59 ++++++++++++ dsolovianov/postgresql/Базовый SQL.sql | 89 +++++++++++++++++ dsolovianov/postgresql/Фильтрация.sql | 126 +++++++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 dsolovianov/postgresql/DDL.sql create mode 100644 dsolovianov/postgresql/DML.sql create mode 100644 dsolovianov/postgresql/Базовый SQL.sql create mode 100644 dsolovianov/postgresql/Фильтрация.sql diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql new file mode 100644 index 0000000..18d5050 --- /dev/null +++ b/dsolovianov/postgresql/DDL.sql @@ -0,0 +1,32 @@ +CREATE TABLE dsolovianov.users ( + id INT GENERATED ALWAYS AS IDENTITY NOT NULL, + name VARCHAR(100) NOT NULL, + age INT, + email VARCHAR(100), + country VARCHAR(100), + is_active BOOLEAN, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE dsolovianov.products ( + id INT GENERATED ALWAYS AS IDENTITY NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10,2) NOT NULL, + in_stock BOOLEAN + ); + +CREATE TABLE dsolovianov.orders ( + id INT GENERATED ALWAYS AS IDENTITY NOT NULL, + user_id INT, + order_date TIMESTAMPTZ NOT NULL, + status VARCHAR(20) +); + +CREATE TABLE dsolovianov.order_items ( + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + price_at_purchase DECIMAL(10,2) NOT NULL, + CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id) +); \ No newline at end of file diff --git a/dsolovianov/postgresql/DML.sql b/dsolovianov/postgresql/DML.sql new file mode 100644 index 0000000..ef18ac6 --- /dev/null +++ b/dsolovianov/postgresql/DML.sql @@ -0,0 +1,59 @@ +INSERT INTO dsolovianov.users (name, age, email, country, is_active, created_at) VALUES +('Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00+00'), +('Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00+00'), +('Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00+00'), +('Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00+00'), +('Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00+00'), +('Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00+00'), +('George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00+00'), +('Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00+00'), +('Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00+00'), +('Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00+00'); + +INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES +('iPhone 15', 'Электроника', 80000, true), +('Kindle', 'Электроника', 15000, true), +('Кроссовки Nike', 'Одежда', 9000, true), +('Кроссовки NoName', 'Одежда', 3000, false), +('Наушники Pro', 'Электроника', 12000, true), +('Фитнес-браслет', 'Гаджеты', 6000, true), +('Подарочная карта', 'Сервисы', 1000, true); + +INSERT INTO dsolovianov.orders (user_id, order_date, status) VALUES +(1, '2025-01-10 12:00:00+00', 'paid'), +(3, '2025-01-12 09:30:00+00', 'cancelled'), +(1, '2025-01-15 18:20:00+00', 'paid'), +(2, '2025-01-20 11:05:00+00', 'created'), +(4, '2025-02-01 17:45:00+00', 'paid'), +(5, '2025-02-10 10:10:00+00', 'refunded'), +(7, '2025-03-01 09:00:00+00', 'paid'), +(9, '2025-03-05 19:30:00+00', 'paid'); + +INSERT INTO dsolovianov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES +-- Заказ #1: iPhone 15 (1 шт) + Наушники Pro (2 шт) +(1, 1, 1, 80000.00), +(1, 5, 2, 12000.00), + +-- Заказ #2 (cancelled): Kindle (1 шт) +(2, 2, 1, 15000.00), + +-- Заказ #3: Кроссовки Nike (2 шт) +(3, 3, 2, 9000.00), + +-- Заказ #4 (created): Фитнес-браслет (1 шт) +(4, 6, 1, 6000.00), + +-- Заказ #5: iPhone 15 (1 шт) + Подарочная карта (3 шт) +(5, 1, 1, 80000.00), +(5, 7, 3, 1000.00), + +-- Заказ #6 (refunded): Наушники Pro (1 шт) +(6, 5, 1, 12000.00), + +-- Заказ #7: Kindle (1 шт) + Фитнес-браслет (2 шт) +(7, 2, 1, 15000.00), +(7, 6, 2, 6000.00), + +-- Заказ #8: iPhone 15 (1 шт) + Кроссовки Nike (1 шт) +(8, 1, 1, 80000.00), +(8, 3, 1, 9000.00); \ No newline at end of file diff --git a/dsolovianov/postgresql/Базовый SQL.sql b/dsolovianov/postgresql/Базовый SQL.sql new file mode 100644 index 0000000..7cf2ef7 --- /dev/null +++ b/dsolovianov/postgresql/Базовый SQL.sql @@ -0,0 +1,89 @@ +/* +- Задание 1 — Спроектируй «карточку пользователя» + + Ты делаешь экран “Пользователи” в админке и решаешь, какие поля там важны. + + Работаем с таблицей **`users`** + + **✅ Требования:** + + 1. Напиши запрос, который вернёт в результате только столбцы: **`id`**, **`name`**, **`country`**, **`created_at`**, **`email`** + 2. Потом измени запрос так, чтобы: + - сначала отсортировать по **`created_at`** (новые сверху) + - потом по **`name`** по алфавиту внутри одной даты + +- Задание 2 — Сегментация по странам (с выбором) + + Маркетолог хочет понять, какая страна сейчас интереснее. + + Работаем с таблицей **`users`** + + **✅ Требования:** + + 1. Выбери страну **`US`**и напиши запрос, который: + - вернёт **`id`**, **`name`**, **`country`**; + - покажет только пользователей из выбранной страны; + - отсортирует их по имени. + 2. Напиши второй запрос, который вернёт пользователей из страны **`UK`**. + +- Задание 3 — Контакты для рассылки (думать про качество) + + Нужно сделать список e‑mail для рассылки так, чтобы его не стыдно было отдавать маркетологу. + + **✅ Требования:** + + 1. Напиши один запрос, который: + - вернёт только пользователей, у которых email **есть**; + - вернёт столбцы **`id`**, **`name`**, **`email`**; + - отсортирует их по имени. + 2. Напиши второй запрос, который подготовит «чистый» вид email: + - вернёт только **`email`** в нижнем регистре (как **`email_lower`**); + - уберёт дубликаты email (нужно использовать **`DISTINCT`**). + - Задание 4 — Новички и активность + + «Тебе нужно посмотреть, кто пришёл недавно и кого стоит “подогреть”.» + + **✅ Требования:** + + 1. Напиши запрос, который: + - выберет пользователей, зарегистрированных **после** даты, которую ты сам выберешь (например, **`2024‑01‑11`**); + - вернёт **`id`**, **`name`**, **`created_at`**; + - отсортирует по **`created_at`** от новых к старым. + 2. Ограничь результат так, чтобы увидеть только 3 самых новых пользователя. + 3. Измени запрос так, чтобы пропустить самого нового и показать следующих двух (используй **`LIMIT`** и **`OFFSET`**). +*/ +SELECT id, + name, + country, + created_at, + email +FROM dsolovianov.users +ORDER BY created_at DESC, name ASC; + +SELECT id, + name, + country +FROM dsolovianov.users +WHERE country LIKE 'US'; + +SELECT id, + name, + country +FROM dsolovianov.users +WHERE country LIKE 'UK'; + +SELECT id, + name, + email, + LOWER(email) AS email_lower +FROM dsolovianov.users +WHERE email IS NOT NULL +ORDER BY name; + +SELECT id, + name, + created_at +FROM dsolovianov.users +WHERE created_at > '2024-01-11' +ORDER BY created_at DESC +LIMIT 3 OFFSET 1; diff --git a/dsolovianov/postgresql/Фильтрация.sql b/dsolovianov/postgresql/Фильтрация.sql new file mode 100644 index 0000000..d928d61 --- /dev/null +++ b/dsolovianov/postgresql/Фильтрация.sql @@ -0,0 +1,126 @@ +/*INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES + ('Mi Band 7', 'smartwatch', 49.00, true), + ('Surface Pro', 'tablet', 1299.00, true), + ('iPhone 15', 'smartphone', 999.00, true), + ('Logitech MX Master', 'mouse', 120.00, true);*/ + +/* +- Задача 1 — Товары для продажи + + Ты настраиваешь главную витрину магазина. Нужно показывать только товары, которые сейчас реально готовы продавать. + + **✅ Условия:** + + - товар **в наличии** + - категория **НЕ smartwatch** + - цена **от 100 до 1300** + - отдельно хотим видеть цену: + - с наценкой 30% ( price + 30%) + - с «виртуальным НДС» 50% (price + 50%) + + **✅ Вывести:** + + - `name`, `category`, `price` + - `price_with_markup` = price + 30% + - `price_with_vat` = price + 50% + +- Задача 2 — Скидочная корзина: дешёвые или дорогие + + Нужно собрать товары под акцию + + **✅ Условия:** + + - участвуют товары с ценой **меньше 100** или **больше 1000** + - скидка 15% (price - 15%) + + **✅ Вывести:** + + - `name`, `price` + - `price_with_discount` = price - 15% + +- Задача 3 — Категории для рекламы + + Маркетолог просит товары только из нужных категорий: + + **✅ Условия:** + + - category в списке: `tablet`, `smartphone`, `mouse` + - цена **не меньше 100** + + **✅ Вывести:** + + - `id`, `name`, `category`, `price` + - сортировка по цене по убыванию +- Задача 4 — Убрать iPhone из выборки + + Собираем список товаров, **кроме iPhone**, чтобы сделать подборку “не Apple”. + + **✅ Условия:** + + - название **НЕ содержит** `iPhone` + + **✅ Вывести:** + + - `name`, `category`, `price` +- Задача 5 — Поиск товаров по названию + + Представь, что у пользователя есть строка поиска на сайте. Нужно сделать **2 запроса**, которые помогут искать товары по названию. + + - 1) Найти товары, где в названии есть `"pro"` (без учёта регистра) + + 📌 Условия: + + - поиск должен сработать на `pro`, `PRO`, `Pro` + - вывести: `name`, `category`, `price` + - 2) Найти товары, где в названии есть `"mx"` (без учёта регистра) + + 📌 Условия: + + - вывести: `name`, `category`, `price` + - отсортировать по `price DESC` +*/ + + +SELECT name, + category, + price, + (price * 0.30) + price AS price_with_markup, + (price * 0.50) + price AS price_with_vat +FROM dsolovianov.products +WHERE category NOT IN ('smartwatch') + AND (price >= 100 AND price <= 1300); + +SELECT name, + price, + price - (price * 0.15) AS price_with_discount +FROM dsolovianov.products +WHERE price < 100 OR price > 1000; + +SELECT id, + name, + category, + price +FROM dsolovianov.products +WHERE category IN ('tablet', 'smartphone', 'mouse') AND price >= 100 +ORDER BY price DESC; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE category NOT LIKE '%iPhone%'; + +SELECT id, + name, + category, + price +FROM dsolovianov.products +WHERE name ILIKE '%pro%'; + +SELECT id, + name, + category, + price +FROM dsolovianov.products +WHERE name ILIKE '%mx%'; + From cbdb54a36b623f204776329bf21be4a286d0f862 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 07:02:07 +0400 Subject: [PATCH 133/179] fix postgres --- dsolovianov/postgresql/DDL.sql | 14 +++++++++----- dsolovianov/postgresql/Базовый SQL.sql | 6 ++++-- dsolovianov/postgresql/Фильтрация.sql | 6 ++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql index 18d5050..5d2cdd7 100644 --- a/dsolovianov/postgresql/DDL.sql +++ b/dsolovianov/postgresql/DDL.sql @@ -1,5 +1,5 @@ CREATE TABLE dsolovianov.users ( - id INT GENERATED ALWAYS AS IDENTITY NOT NULL, + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, age INT, email VARCHAR(100), @@ -9,7 +9,7 @@ CREATE TABLE dsolovianov.users ( ); CREATE TABLE dsolovianov.products ( - id INT GENERATED ALWAYS AS IDENTITY NOT NULL, + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50), price DECIMAL(10,2) NOT NULL, @@ -17,7 +17,7 @@ CREATE TABLE dsolovianov.products ( ); CREATE TABLE dsolovianov.orders ( - id INT GENERATED ALWAYS AS IDENTITY NOT NULL, + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id INT, order_date TIMESTAMPTZ NOT NULL, status VARCHAR(20) @@ -26,7 +26,11 @@ CREATE TABLE dsolovianov.orders ( CREATE TABLE dsolovianov.order_items ( order_id INT NOT NULL, product_id INT NOT NULL, - quantity INT NOT NULL, - price_at_purchase DECIMAL(10,2) NOT NULL, + quantity INT NOT NULL CHECK (quantity > 0), + price_at_purchase DECIMAL(10,2) NOT NULL CHECK (price_at_purchase > 0), CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id) + CONSTRAINT order_items_order_fk + FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id), + CONSTRAINT order_items_product_fk + FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id) ); \ No newline at end of file diff --git a/dsolovianov/postgresql/Базовый SQL.sql b/dsolovianov/postgresql/Базовый SQL.sql index 7cf2ef7..930f258 100644 --- a/dsolovianov/postgresql/Базовый SQL.sql +++ b/dsolovianov/postgresql/Базовый SQL.sql @@ -64,13 +64,15 @@ SELECT id, name, country FROM dsolovianov.users -WHERE country LIKE 'US'; +WHERE country LIKE 'US' +ORDER BY name; SELECT id, name, country FROM dsolovianov.users -WHERE country LIKE 'UK'; +WHERE country LIKE 'UK' +ORDER BY name; SELECT id, name, diff --git a/dsolovianov/postgresql/Фильтрация.sql b/dsolovianov/postgresql/Фильтрация.sql index d928d61..edf113a 100644 --- a/dsolovianov/postgresql/Фильтрация.sql +++ b/dsolovianov/postgresql/Фильтрация.sql @@ -110,15 +110,13 @@ SELECT name, FROM dsolovianov.products WHERE category NOT LIKE '%iPhone%'; -SELECT id, - name, +SELECT name, category, price FROM dsolovianov.products WHERE name ILIKE '%pro%'; -SELECT id, - name, +SELECT name, category, price FROM dsolovianov.products From 327bed019dac2a46eaa3b604170c939d742b079b Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 07:04:28 +0400 Subject: [PATCH 134/179] fix ddl --- dsolovianov/postgresql/DDL.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql index 5d2cdd7..033894c 100644 --- a/dsolovianov/postgresql/DDL.sql +++ b/dsolovianov/postgresql/DDL.sql @@ -32,5 +32,7 @@ CREATE TABLE dsolovianov.order_items ( CONSTRAINT order_items_order_fk FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id), CONSTRAINT order_items_product_fk - FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id) + FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id), + CONSTRAINT orders_user_fk + FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id) ); \ No newline at end of file From 6fe1e7ceb525e7520d7796e66c0ff9fc91d2d463 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 07:05:38 +0400 Subject: [PATCH 135/179] fix ddl v2 --- dsolovianov/postgresql/DDL.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql index 033894c..c3a8393 100644 --- a/dsolovianov/postgresql/DDL.sql +++ b/dsolovianov/postgresql/DDL.sql @@ -21,6 +21,8 @@ CREATE TABLE dsolovianov.orders ( user_id INT, order_date TIMESTAMPTZ NOT NULL, status VARCHAR(20) + CONSTRAINT orders_user_fk + FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id) ); CREATE TABLE dsolovianov.order_items ( @@ -32,7 +34,5 @@ CREATE TABLE dsolovianov.order_items ( CONSTRAINT order_items_order_fk FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id), CONSTRAINT order_items_product_fk - FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id), - CONSTRAINT orders_user_fk - FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id) + FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id) ); \ No newline at end of file From 52481d3186855477e48c74aa2c2339626c13cedf Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 07:06:15 +0400 Subject: [PATCH 136/179] fix ddl v3 --- dsolovianov/postgresql/DDL.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql index c3a8393..a578b7b 100644 --- a/dsolovianov/postgresql/DDL.sql +++ b/dsolovianov/postgresql/DDL.sql @@ -20,7 +20,7 @@ CREATE TABLE dsolovianov.orders ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id INT, order_date TIMESTAMPTZ NOT NULL, - status VARCHAR(20) + status VARCHAR(20), CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id) ); From bfc54f9dcfa39cdcd433dbf06da22512f51a3a1c Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 11 May 2026 07:07:01 +0400 Subject: [PATCH 137/179] ddl v4 --- dsolovianov/postgresql/DDL.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql index a578b7b..0c7c3b1 100644 --- a/dsolovianov/postgresql/DDL.sql +++ b/dsolovianov/postgresql/DDL.sql @@ -30,7 +30,7 @@ CREATE TABLE dsolovianov.order_items ( product_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), price_at_purchase DECIMAL(10,2) NOT NULL CHECK (price_at_purchase > 0), - CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id) + CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id), CONSTRAINT order_items_order_fk FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id), CONSTRAINT order_items_product_fk From 3d85f4b9de45e5d8ac35d1e7353793411064838a Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 12 May 2026 20:31:23 +0300 Subject: [PATCH 138/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A2=D0=B8=D0=BF=D1=8B=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86'=20-=20Clickhouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Семейство MergeTree/practice_table_types.sql | 120 ++++++++++++++++++ .../Step_4_preparing_display_cases_at_greenplum.sql | 2 +- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql diff --git a/mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql new file mode 100644 index 0000000..eff4e47 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql @@ -0,0 +1,120 @@ +-- Задание 1 + +CREATE TABLE mnoskov.page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + + +-- Задание 2 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + name String, + update_at DateTime +) +ENGINE = ReplacingMergeTree(update_at) +ORDER BY id; + + +-- Задание 3 +CREATE TABLE mnoskov.region_sales +ON CLUSTER clickhouse_cluster +( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date) + +INSERT INTO mnoskov.region_sales (region, date, sales) +VALUES + ('Moscow', '2026-05-01', 100), + ('Moscow', '2026-05-01', 150), + ('Moscow', '2026-05-01', 200), + ('Berlin', '2026-05-02', 300), + ('Berlin', '2026-05-02', 120), + ('Berlin', '2026-05-02', 80), + ('Paris', '2026-05-03', 500), + ('Paris', '2026-05-03', 250), + ('London', '2026-05-04', 400), + ('Tokyo', '2026-05-05', 700); + +SELECT * FROM mnoskov.region_sales; +-- Все четко работает. + + +-- Задание 4 +CREATE TABLE mnoskov.user_metrics +ON CLUSTER clickhouse_cluster +( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +CREATE TABLE mnoskov.user_metrics_source +ON CLUSTER clickhouse_cluster +( + region String, + age UInt8, + user_id UInt64 +) +ENGINE = MergeTree +ORDER BY region; + +INSERT INTO mnoskov.user_metrics_source +(region, age, user_id) +VALUES + ('Moscow', 25, 1001), + ('Moscow', 30, 1002), + ('Moscow', 22, 1003), + ('Moscow', 25, 1001), + ('Berlin', 40, 2001), + ('Berlin', 35, 2002), + ('Berlin', 29, 2003), + ('Berlin', 35, 2002), + ('Paris', 31, 3001), + ('Paris', 28, 3002), + ('Paris', 45, 3003), + ('Paris', 31, 3001), + ('London', 50, 4001), + ('London', 41, 4002), + ('London', 38, 4003), + ('London', 41, 4002), + ('Tokyo', 27, 5001), + ('Tokyo', 33, 5002), + ('Tokyo', 29, 5003), + ('Tokyo', 33, 5002); +SELECT * FROM mnoskov.user_metrics_source; + +INSERT INTO mnoskov.user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(user_id) AS active_users_state +FROM + mnoskov.user_metrics_source +GROUP BY + region; + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM + mnoskov.user_metrics +GROUP BY + region; +-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают. \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql b/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql index be37917..9c4dbb2 100644 --- a/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql +++ b/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql @@ -53,7 +53,7 @@ LEFT JOIN ( ) e ON du.user_id = e.user_id WHERE NOT EXISTS (SELECT 1 FROM core.fact_lesson_views flv WHERE flv.user_id = du.user_id); --- интересно, что при проверки плана запроса, когда кусок с enrollments_count был в CTE, выдавал какую-то хуйню, а когда я засунул его в подзапрос, сразу инлексы заработали, и строки начали эффективней читаться +-- интересно, что во время проверки плана запроса, когда кусок с enrollments_count был в CTE, план запроса выдавал какую-то хуйню, а когда я засунул его в подзапрос, сразу индексы заработали, и строки начали эффективней читаться -- 9 From 318c50b56307b40a2fe7bcc12eea430c0997d041 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 13 May 2026 18:28:46 +0300 Subject: [PATCH 139/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'View=20=D0=B8=20Distribute=20=D0=B4=D0=B2=D0=B8?= =?UTF-8?q?=D0=B6=D0=BA=D0=B8'=20-=20Clickhouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice_view_and_distribute_engines.sql | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql diff --git a/mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql new file mode 100644 index 0000000..0c3d9ee --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql @@ -0,0 +1,87 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster ( + dt DateTime, + user_id UInt32, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY dt; + +CREATE TABLE mnoskov.dist_logs +ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'mnoskov', + 'logs', + rand() +); +-- параметр rand() - это ключ распределния как я понял. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY (ts, user_id); + +CREATE VIEW top_active_users AS +SELECT + user_id, + count(*) AS cnt_user +FROM + mnoskov.events +GROUP BY + user_id +ORDER BY + cnt_user DESC, user_id +LIMIT 100; +-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW. + + +-- Задание 3 + +-- тут вообще жопа какая-то. Если смотреть на пример создания MV, то выдает ошибку мол engine указывать нельзя. Полез в документацию, там сказано что после указания имени mv нужно добавлять TO ... но все равно выдется ошибка. +-- этот способ я в хабре нашел +CREATE TABLE raw_events +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE = MergeTree +ORDER BY (event_time, user_id); + +CREATE TABLE agr_clicks_per_day +ON CLUSTER clickhouse_cluster +( + event_date Date, + count UInt64 +) +ENGINE = MergeTree +ORDER BY event_date; + +CREATE MATERIALIZED VIEW mv_clicks_per_day TO agr_clicks_per_day AS +SELECT + toDate(event_time) AS event_date, + count(*) AS count +FROM + mnoskov.raw_events +GROUP BY + event_date; + +SELECT * FROM agr_clicks_per_day; +-- ничего у меня получилось, и с SummingMergeTree ни с MergeTree. Как очень непонятно материализованные таблицы работают. Если делать как в теории в инфре, то тоже ничего не работает. \ No newline at end of file From 80023d6054076329ef5f3562a86a0ec7dfea62a3 Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Wed, 13 May 2026 20:14:51 +0300 Subject: [PATCH 140/179] completed my homework - sql base --- vberetar/postgresql/sql_query.sql | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 vberetar/postgresql/sql_query.sql diff --git a/vberetar/postgresql/sql_query.sql b/vberetar/postgresql/sql_query.sql new file mode 100644 index 0000000..a0681ba --- /dev/null +++ b/vberetar/postgresql/sql_query.sql @@ -0,0 +1,73 @@ +-- Задание 1 +SELECT id, name, country, created_at, email +FROM users +ORDER BY created_at DESC, name; + +-- Почему выбрал именно эти столбцы и зачем такая сортировка? +-- Ответ на вопрос: +-- Эти столбцы нужны для анализа пользователей, для тех же аналитиков. +-- Сортировка по дате и имени, я так предпологаю, нужня для удобства. + + +-- Задание 2 +-- Запрос 1 +SELECT id, name, country +FROM users +WHERE country = 'US' +ORDER BY name; + +-- Запрос 2 +SELECT id, name, country +FROM users +WHERE country = 'UK' +ORDER BY name; + +-- Какой из двух запросов маркетологу полезнее и почему? +-- Ответ на вопрос: +-- Я так понимаю маркетологу нужно понять какое кол-во пользователей в каждой из стран зарегестрировалось в приложении. +-- Возможно для понимания продвижения продукта и насколько успешна маркетинговая стратегия. И в дальнейшем её корректировка. + + +-- Задание 3 +-- Запрос 1 +SELECT id, name, email +FROM users +WHERE email IS NOT NULL -- я знаю что email и так должен быть NOT NULL по DDL, но на всякий прописал +ORDER BY name; + +-- Запрос 2 +SELECT DISTINCT LOWER(email) as email_lower +FROM users +WHERE email IS NOT NULL; + +-- Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы? +-- Ответ на вопрос: +-- Приводить email к нижнему регистру необходимо для того чтобы небыло дубликатов почты. +-- После гугления я узнал что в почтовых адресах могут допускаться верхние регистры букв, только потом создать email с теми же буквами но в нижнем регистре нельзя будет, они будут считаться индентичными. +-- Получается несколько пользователей могут залогинится под одним и тем же email адресом но с разными регистрами. А такое нельзя допускать. +-- Для рассылки дубликаты нужно убирать что-бы не приходила рассылка дважды в один и тот же email. + +-- Задание 4 +-- Запрос 1 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC; + +-- Запрос 2 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC +LIMIT 3; + +-- Запрос 3 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC +OFFSET 1 LIMIT 2; + +-- В каком реальном сценарии тебе пригодится OFFSET? +-- Ответ на вопрос: +-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка. From fb29b369f6c3203b21d57f11050835d3bcfc74c4 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 14 May 2026 13:13:47 +0300 Subject: [PATCH 141/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A2=D0=B8=D0=BF=D1=8B=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85'=20-=20ClickHouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1. Семейство MergeTree}/practice_table_types.sql | 2 +- .../practice_view_and_distribute_engines.sql | 10 +- .../2. Типы данных/practice_data_types.sql | 111 ++++++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) rename mnoskov/clickhouse/практика clickhouse/{Семейство MergeTree => 1. Движки/1. Семейство MergeTree}/practice_table_types.sql (99%) rename mnoskov/clickhouse/практика clickhouse/{ => 1. Движки}/practice_view_and_distribute_engines.sql (90%) create mode 100644 mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql diff --git a/mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Движки/1. Семейство MergeTree/practice_table_types.sql similarity index 99% rename from mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql rename to mnoskov/clickhouse/практика clickhouse/1. Движки/1. Семейство MergeTree/practice_table_types.sql index eff4e47..6140c2f 100644 --- a/mnoskov/clickhouse/практика clickhouse/Семейство MergeTree/practice_table_types.sql +++ b/mnoskov/clickhouse/практика clickhouse/1. Движки/1. Семейство MergeTree/practice_table_types.sql @@ -34,7 +34,7 @@ ON CLUSTER clickhouse_cluster ) ENGINE = SummingMergeTree PARTITION BY toYYYYMM(date) -ORDER BY (region, date) +ORDER BY (region, date); INSERT INTO mnoskov.region_sales (region, date, sales) VALUES diff --git a/mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql similarity index 90% rename from mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql rename to mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql index 0c3d9ee..67eacc4 100644 --- a/mnoskov/clickhouse/практика clickhouse/practice_view_and_distribute_engines.sql +++ b/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql @@ -38,7 +38,7 @@ ENGINE = ReplicatedMergeTree( ) ORDER BY (ts, user_id); -CREATE VIEW top_active_users AS +CREATE VIEW mnoskov.top_active_users AS SELECT user_id, count(*) AS cnt_user @@ -56,7 +56,7 @@ LIMIT 100; -- тут вообще жопа какая-то. Если смотреть на пример создания MV, то выдает ошибку мол engine указывать нельзя. Полез в документацию, там сказано что после указания имени mv нужно добавлять TO ... но все равно выдется ошибка. -- этот способ я в хабре нашел -CREATE TABLE raw_events +CREATE TABLE mnoskov.raw_events ( event_time DateTime, user_id UInt32, @@ -65,7 +65,7 @@ CREATE TABLE raw_events ENGINE = MergeTree ORDER BY (event_time, user_id); -CREATE TABLE agr_clicks_per_day +CREATE TABLE mnoskov.agr_clicks_per_day ON CLUSTER clickhouse_cluster ( event_date Date, @@ -74,7 +74,7 @@ ON CLUSTER clickhouse_cluster ENGINE = MergeTree ORDER BY event_date; -CREATE MATERIALIZED VIEW mv_clicks_per_day TO agr_clicks_per_day AS +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day TO mnosokv.agr_clicks_per_day AS SELECT toDate(event_time) AS event_date, count(*) AS count @@ -83,5 +83,5 @@ FROM GROUP BY event_date; -SELECT * FROM agr_clicks_per_day; +SELECT * FROM mnoskov.agr_clicks_per_day; -- ничего у меня получилось, и с SummingMergeTree ни с MergeTree. Как очень непонятно материализованные таблицы работают. Если делать как в теории в инфре, то тоже ничего не работает. \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql new file mode 100644 index 0000000..a92fb71 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql @@ -0,0 +1,111 @@ +-- Задание 1 + +CREATE TABLE mnoskov.raw_data1 +ON CLUSTER clickhouse_cluster ( + user_id String, + balance String, + created_at String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_data1', + '{replica}' +) +ORDER BY tuple(); + +SELECT + user_id, + toDecimal64OrNull(balance,2) AS balance, + toDateTime64OrNull(created_at, 3) AS created_at +FROM + mnoskov.raw_data1 +WHERE + balance IS NOT NULL AND created_at IS NOT NULL; + + +-- Задание 2 + +CREATE TABLE events +ON CLUSTER clickhouse_cluster ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY + event_id; + +SELECT * FROM mnoskov.events; + + +SELECT + event_id, + toDate(parseDateTimeBestEffortOrNull(event_date)) AS event_dates, + CASE + WHEN toDayOfMonth(parseDateTimeBestEffortOrNull(event_time)) = 1 THEN toHour(parseDateTimeBestEffortOrNull(event_time)) + ELSE (toDayOfMonth(parseDateTimeBestEffortOrNull(event_time)) - 1) * 24 + toHour(parseDateTimeBestEffortOrNull(event_time)) + END AS event_times, + toDateTime(parseDateTimeBestEffortOrNull(event_date)) + toIntervalHour(event_times) AS event_datetime +FROM + mnoskov.events +ORDER BY + event_dates; +-- Все преобразования с to... и parse... нашел тут https://clickhouse.com/docs/ru/sql-reference/functions/type-conversion-functions#toIntervalMonth. +-- explain analyze не работает, потэому не могу посмотреть насколько это эффективно, НО главное сам запрос работает. + + +-- Задание 3 + +CREATE TABLE mnoskov.user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/user_activity', + '{replica}' +) +ORDER BY + user_id; + +SELECT + user_id, + count(*) +FROM + mnoskov.user_activity +WHERE + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id +ORDER BY + user_id; + + +-- Задание 4 + +CREATE TABLE mnoskov.sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sensor_data', + '{replica}' +) +ORDER BY sensor_id; + +--1-2. +SELECT + toString(status) AS str_status, + avg(value) AS avg_value +FROM + mnoskov.sensor_data +GROUP BY + str_status; +--3 +-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0. \ No newline at end of file From 4240f34f24d1f6a642452a0ff885704fe527c92c Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Sat, 16 May 2026 20:16:10 +0300 Subject: [PATCH 142/179] add home work on 'sql filters' --- vberetar/postgresql/sql_filters.sql | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 vberetar/postgresql/sql_filters.sql diff --git a/vberetar/postgresql/sql_filters.sql b/vberetar/postgresql/sql_filters.sql new file mode 100644 index 0000000..423f131 --- /dev/null +++ b/vberetar/postgresql/sql_filters.sql @@ -0,0 +1,56 @@ +-- Задача 1 +SELECT name, category, price, + price * 1.3 AS price_with_markup, + price * 1.5 AS price_with_vat +FROM products +WHERE (price BETWEEN 100 AND 1300) + AND category NOT IN ('smartwatch') + AND in_stock = 'true'; + +-- Вопрос: +-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? +-- Ответ: +-- Для меня BETWEEN 100 AND 1300 читается лучше, сразу понятен срез от 100 и 1300. + + +-- Задача 2 +SELECT name, price, + price * 0.85 AS price_with_discount +FROM products +WHERE (price < 100 OR price > 1000); + +-- Вопрос: +-- Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно? +-- Ответ: +-- Потому что условие может не правильно отобразить результат, AND выполняется раньше OR. +-- Для надежности лучше скобками прописывать явно. + + +-- Задача 3 +SELECT id, name, category, price +FROM products +WHERE price >= 100 + AND category IN ('tablet', 'smartphone', 'mouse') +ORDER BY price DESC; + + +-- Задача 4 +SELECT name, category, price +FROM products +WHERE name NOT LIKE '%iPhone%'; + + +-- Задача 5 +SELECT name, category, price +FROM products +WHERE name ILIKE '%pro%'; + +SELECT name, category, price +FROM products +WHERE name ILIKE '%mx%' +ORDER BY price DESC; + +-- Вопрос: +-- В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'. +-- Ответ: +-- В названии товаров могут быть буквы (в словах) с разными регистрами, при поиске клиенту можен не показать часть товаров только потому что слово выглядить так - PRO а не так - Pro. From 19563dcfa3f67b8c1fc4ccdb590560bfbd05f234 Mon Sep 17 00:00:00 2001 From: dmezentsev Date: Sun, 17 May 2026 14:56:01 +0300 Subject: [PATCH 143/179] add postgresql homework --- dmezentsev/postgresql/homework/DDL_DML.sql | 86 ++++++++ dmezentsev/postgresql/homework/Filtering.sql | 69 +++++++ dmezentsev/postgresql/homework/Functions.sql | 144 +++++++++++++ dmezentsev/postgresql/homework/GROUP_BY.sql | 160 ++++++++++++++ dmezentsev/postgresql/homework/JOIN.sql | 207 +++++++++++++++++++ dmezentsev/postgresql/homework/UNION.sql | 107 ++++++++++ dmezentsev/postgresql/homework/base.sql | 73 +++++++ 7 files changed, 846 insertions(+) create mode 100644 dmezentsev/postgresql/homework/DDL_DML.sql create mode 100644 dmezentsev/postgresql/homework/Filtering.sql create mode 100644 dmezentsev/postgresql/homework/Functions.sql create mode 100644 dmezentsev/postgresql/homework/GROUP_BY.sql create mode 100644 dmezentsev/postgresql/homework/JOIN.sql create mode 100644 dmezentsev/postgresql/homework/UNION.sql create mode 100644 dmezentsev/postgresql/homework/base.sql diff --git a/dmezentsev/postgresql/homework/DDL_DML.sql b/dmezentsev/postgresql/homework/DDL_DML.sql new file mode 100644 index 0000000..3cb0128 --- /dev/null +++ b/dmezentsev/postgresql/homework/DDL_DML.sql @@ -0,0 +1,86 @@ +CREATE TABLE users ( +id BIGINT PRIMARY KEY, /* сделал первичные ключи в каждой таблице BIGINT, кол-во +записей может быть большим */ +name VARCHAR(100) NOT NULL, +age SMALLINT, -- Тип данных INT посчитал избыточным +email VARCHAR(100), +country VARCHAR(2), -- Идеально для ISO - кода страны +is_active BOOLEAN, +created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE products ( +id BIGINT PRIMARY KEY, +name VARCHAR(100) NOT NULL, +category VARCHAR(50), +price DECIMAL(10,2) NOT NULL, -- Сделал тип данных для денежных операций +in_stock BOOLEAN +); + +CREATE TABLE orders ( +id BIGINT PRIMARY KEY, +user_id BIGINT NOT NULL, +order_date TIMESTAMPTZ NOT NULL, +status VARCHAR(20), -- Читаемый статус нагляднее, не нужен справочник по кодам + +FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE order_items ( +order_id BIGINT, +product_id BIGINT, +quantity INT NOT NULL, /* Предположил, что типа данных SMALL INT возможно не +хватит для обозначения количества */ +price_at_purchase DECIMAL(10,2) NOT NULL, + +PRIMARY KEY(order_id, product_id), +FOREIGN KEY (order_id) REFERENCES orders(id), +FOREIGN KEY (product_id) REFERENCES products(id) +); + +INSERT INTO users (id, name, age, email, country, is_active, created_at) +VALUES + (1, 'Alice', 25, 'alice@example.com', 'US', TRUE, '2024-01-10 10:00:00'), + (2, 'Bob', 30, 'bob@example.com', 'UK', TRUE, '2024-02-05 09:30:00'), + (3, 'Carol', 27, 'carol@example.com', 'US', FALSE, '2024-03-01 15:20:00'), + (4, 'Diana', 22, 'diana@GMAIL.com', 'US', TRUE, '2024-01-12 08:15:00'), + (5, 'Evan', 35, 'EVAN@example.com', 'UK', TRUE, '2024-01-18 19:40:00'), + (6, 'Farida', 29, 'farida@example.com', 'KZ', TRUE, '2024-02-20 11:05:00'), + (7, 'George', 31, 'george+test@example.com', 'US', TRUE, '2024-03-05 16:45:00'), + (8, 'Helen', 24, NULL, 'UK', FALSE, '2024-03-06 08:20:00'), + (9, 'Ivan', 26, 'ivan@example.com', 'RU', TRUE, '2024-01-11 14:30:00'), + (10, 'Julia', 28, 'julia@example.com', 'US', TRUE, '2024-03-07 18:05:00'); + +INSERT INTO products (id, name, category, price, in_stock) +VALUES + (1, 'iPhone 15', 'Электроника', 80000.00, TRUE), + (2, 'Kindle', 'Электроника', 15000.00, TRUE), + (3, 'Кроссовки Nike', 'Одежда', 9000.00, TRUE), + (4, 'Кроссовки NoName', 'Одежда', 3000.00, FALSE), + (5, 'Наушники Pro', 'Электроника', 12000.00, TRUE), + (6, 'Фитнес-браслет', 'Гаджеты', 6000.00, TRUE), + (7, 'Подарочная карта', 'Сервисы', 1000.00, TRUE); + +INSERT INTO orders (id, user_id, order_date, status) +VALUES + (1, 1, '2025-01-10 12:00:00', 'paid'), + (2, 3, '2025-01-12 09:30:00', 'cancelled'), + (3, 1, '2025-01-15 18:20:00', 'paid'), + (4, 2, '2025-01-20 11:05:00', 'created'), + (5, 4, '2025-02-01 17:45:00', 'paid'), + (6, 5, '2025-02-10 10:10:00', 'refunded'), + (7, 7, '2025-03-01 09:00:00', 'paid'), + (8, 9, '2025-03-05 19:30:00', 'paid'); + +INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) +VALUES + (1, 1, 1, 78000.00), + (1, 3, 2, 8500.00), + (2, 2, 1, 14000.00), + (3, 5, 1, 11000.00), + (3, 7, 1, 1000.00), + (4, 2, 1, 15000.00), + (5, 6, 1, 6000.00), + (6, 1, 1, 82000.00), + (7, 3, 1, 9000.00), + (8, 4, 2, 2800.00); \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Filtering.sql b/dmezentsev/postgresql/homework/Filtering.sql new file mode 100644 index 0000000..89e366c --- /dev/null +++ b/dmezentsev/postgresql/homework/Filtering.sql @@ -0,0 +1,69 @@ +-- Задача 1 — Товары для продажи + +SELECT name, + category, + price, + (price * 1.3) price_with_markup, + (price * 1.5) price_with_vat +FROM dmezentsev.products p +WHERE in_stock = TRUE +AND category != '%smartwatch%' +AND price BETWEEN 100 AND 1300; + +/* - Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? + +По мне, так BETWEEN короче и лаконичнее. Разве что с датами надо быть осторожнее :) +*/ + +-- Задача 2 — Скидочная корзина: дешёвые или дорогие + +SELECT name, + price, + (price * 0.85) price_with_discount +FROM dmezentsev.products p +WHERE (price < 100) OR (price > 1000); + +/* - Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно? + +Если в условии есть и OR и AND — скобки обязательны. Можно ошибиться с логикой выполнения, +так как приоритет AND выше OR. Лучше перестраховаться +и ставить скобки везде */ + +-- Задача 3 — Категории для рекламы + +SELECT id, + name, + category, + price +FROM dmezentsev.products p +WHERE category IN ('tablet', 'smartphone', 'mouse') +AND price >= 100 +ORDER BY price DESC; + +-- Задача 4 — Убрать iPhone из выборки + +SELECT name, + category, + price +FROM dmezentsev.products p +WHERE name NOT LIKE '%iPhone%'; + +-- Задача 5 — Поиск товаров по названию + +SELECT name, + category, + price +FROM dmezentsev.products p +WHERE name ILIKE '%pro%'; + +SELECT name, + category, + price +FROM dmezentsev.products p +WHERE name ILIKE '%mx%'; + +/* - В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'. + +Потому что = 'Pro' найдёт только точное совпадение с учётом регистра, + а ILIKE '%pro%' найдёт любые вхождения "pro" в любом регистре +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Functions.sql b/dmezentsev/postgresql/homework/Functions.sql new file mode 100644 index 0000000..4fb08ef --- /dev/null +++ b/dmezentsev/postgresql/homework/Functions.sql @@ -0,0 +1,144 @@ +-- Задача 1. Давность регистрации пользователей + +SELECT name, + CAST(created_at AS DATE) AS created_date, + CURRENT_DATE - created_at::DATE AS days_since_registration, + is_active +FROM dmezentsev.users +WHERE CURRENT_DATE - created_at::DATE > 180 +ORDER BY CURRENT_DATE - created_at::DATE DESC; + +/* - Как бы ты использовал такое поле в реальном продукте +(что можно делать с очень «старыми» пользователями)? + +В реальности можно давать какие-то бонусы самым лояльным клиентам. Ну или +наоборот, удалить их, если зарегистрированы давно, а активности никакой + +- Почему удобнее сразу вычитать created_at::date, а не created_at как timestamp? + +Если вычитать ::date, то получается число, удобно сравнивать, применять при +фильтрации, в арифметических выражениях. Если timestamp, то интервал + */ + +-- Задача 2. Нормализация email и доменов + +SELECT name, + LOWER(email) AS email_clean, + SUBSTRING(LOWER(email) FROM POSITION('@' IN email) + 1) AS domain +FROM dmezentsev.users +WHERE email IS NOT NULL +ORDER BY SUBSTRING(email FROM POSITION('@' IN email) + 1), name; + +/* - Зачем в реальных системах приводят email к нижнему регистру и выделяют домен отдельно? + +email - адреса приводят к нижнему регистру, потому что в БД один и тот же +почтовый ящик в разном регистре будет считаться двумя почтовыми ящиками, что +приведет к дублям. Так же если пользователь введет свой почтовый ящик не в таком +регистре, в котором ящик сохранен в БД, то он не сможет залогиниться +Домен выделяют отдельно для анализа, маркетинга. Так же ради безопасности, +чтобы блочить подозрительные домены + */ + +-- Задача 3. Сегменты по возрасту + +SELECT name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + ELSE 'senior' + END AS age_group +FROM dmezentsev.users +ORDER BY age; + +/* - Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? + +Вот тут точно не знаю, но мне кажется, что сегментация в SQL вернет результат. +А если писать это в коде приложения, то выгрузка всех строк выльется в большой +объем данных, которые нужно будет еще и обработать + +- Что произойдёт, если в CASE забыть ветку ELSE  +(что получат пользователи, не попавшие ни в одно условие)? + +Если забыть ELSE, то пользователи, которые не попадут ни под одно из условий, +получат NULL + */ + +-- Задача 4. «Горячие» пользователи по дате и активности + +SELECT + name, + created_at::DATE AS created_at_date, + CURRENT_DATE - created_at::DATE AS days_since_registration, + CASE + WHEN is_active AND (CURRENT_DATE - created_at::DATE <= 30) THEN 'hot' + ELSE 'regular' + END AS hot_flag +FROM dmezentsev.users +WHERE is_active AND (CURRENT_DATE - created_at::DATE <= 30); + +/* - Чем отличается фильтрация по дате в WHERE от простого CASE с меткой 'hot', +но без фильтрации? + +С фильтрацией в WHERE получим горячих с датой регистрации не больше 30 дней. +Если без WHERE, то получим всех пользователей (и горячих, и обычных) + +- Какие типы рассылок/кампаний логично запускать только на «горячих»? + +Предположу, что горячим можно рассылать специальные предложения, чтобы +дополнительно завлечь. Так же можно сделать рассылку по онбордингу, чтобы +легче вкатиться в фичи сервиса + */ + +-- Задача 5. Качество данных по email + +SELECT COUNT(*) AS total_users, + COUNT(email) AS with_email, + COUNT(*) FILTER (WHERE email IS NULL) AS without_email, + COUNT(*) FILTER (WHERE email IS NULL)::NUMERIC + / COUNT(*) * 100 AS without_email_percent +FROM dmezentsev.users; + +/* - Почему для проверки «долей» и «заполненности» полей +лучше использовать агрегаты в SQL, а не просто «на глаз» смотреть на таблицу? + +Агрегаты гораздо легче воспринимать на глаз, чем смотреть всю таблицу. +По сути, это уже готовый ответ на поставленный вопрос. Другое дело +пытаться понять по всем данным, что вообще происходит. +Особенно, если там миллионы строк. + +- Какие действия по данным ты бы предложил продукту, +если доля пользователей без email слишком большая? + +Если бы доля юзеров, не заполняюших email, оказалась слишком большой, то я +бы советовал сделать ввод email обязательным, ну или использовать email как +логин. Как вариант, предложить какой-то бонус типа промокода + */ + +-- Задача 6. «Стаж» пользователя и его группа + +SELECT name, + CASE + WHEN CURRENT_DATE - created_AT::DATE <= 30 THEN 'new' + WHEN CURRENT_DATE - created_AT::DATE BETWEEN 31 AND 180 THEN 'middle' + ELSE 'old' + END AS tenure_group, + CURRENT_DATE - created_AT::DATE AS days_since_registration, + is_active +FROM dmezentsev.users +ORDER BY tenure_group, + days_since_registration; + +/* Чем сегментация по «стажу» отличается по смыслу от сегментации по возрасту? + +Стаж - это время с момента регистрации. Думаю, такая сегментация необходима для того, чтобы +по этим сегментам проводить какую-то свою аналитику, отличную для каждого из них. +Например: стаж - аналитика лояльности, возраст - аналитика платежеспособности + +- Если пересчитывать tenure_group прямо в SQL каждый день, +какие плюсы и минусы по сравнению с хранением статического поля segment? + +Плюс перерасчета стажа каждый день - это актуальность данных, которые потом +будут использоваться аналитиками + */ + \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/GROUP_BY.sql b/dmezentsev/postgresql/homework/GROUP_BY.sql new file mode 100644 index 0000000..5d641a4 --- /dev/null +++ b/dmezentsev/postgresql/homework/GROUP_BY.sql @@ -0,0 +1,160 @@ +-- Задача 1. Сколько товаров в каждой категории + +SELECT category, + count(id) AS products_count +FROM dmezentsev.products +GROUP BY category +ORDER BY count(id) DESC; + +/* - Что будет, если убрать ORDER BY — как изменится чтение результата +для продукта/аналитика? + +Если убрать сортировку, то строки в результирующей таблице будут выведены +произвольно, а точнее это будет зависеть от плана выполнения запроса + +- Если добавить в таблицу новый товар с категорией smartphone, +как изменится строка для этой категории? + +Если добавить новый товар с категорией, которая есть в таблице, то в поле +products_count будет +1 значение по этой категории +*/ + +-- Задача 2. Выручка по каждому заказу + +SELECT order_id, + SUM(quantity) AS total_quantity, + SUM(quantity * price_at_purchase) AS order_revenue +FROM dmezentsev.order_items +GROUP BY order_id +ORDER BY SUM(quantity * price_at_purchase) DESC; + +/* - Что будет, если убрать GROUP BY и оставить SUM(quantity * price_at_purchase)? + +Если убрать GROUP BY, то будет ошибка. Поле, запрашиваемое в SELECT и +не участвующее в агрегирующей функции, должно быть в GROUP BY + */ + +-- Задача 3. Выручка по категориям товаров + +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id +GROUP BY p.category +ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC; + +/* - Почему группировка идёт по p.category, а не по p.id? + +Группировка по p.category, а не по p.id, потому что нам нужно узнать +общую выручку по категориям, а не по товарам в категориях + +- Если добавить в SELECT ещё p.name, что произойдёт с результатом и почему? + +Если добавить в SELECT поле p.name, то запрос выдаст ошибку, что это +поле должно быть в GROUP BY, так как не является агрегирующей функцией + */ + +-- Задача 4. Только оплаченные заказы и их выручка + +SELECT o.id AS order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM dmezentsev.orders o +JOIN order_items oi ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY o.id +ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC; + +/* - Чем результат этой задачи будет отличаться от задачи 2 (где статус не учитывался)? + +В этом запросе в результирующую таблицу попадут только заказы со статусом 'paid' + +- В какой реальной ситуации ты бы смотрел только на paid заказы, а не на все? + +В реальной ситуации такая фильтрация была бы одной из основополагающих +и отображающих метрик успешности бизнеса + */ + +-- Задача 5. Категории с выручкой больше 1000 + +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id +GROUP BY p.category +HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC; + +/* - Почему нельзя написать WHERE category_revenue > 1000? + +Условие в WHERE после GROUP BY не сработает, так как оно фильтрует данные до +группировки + +- Чем WHERE price > 1000 принципиально отличается от HAVING SUM(...) > 1000? + +WHERE price > 1000 отличается от HAVING SUM(...) > 1000 тем, что в WHERE +будет отфильтрованы товары с ценой больше 1000, а HAVING отфильтрует сгруппированные +категории, у которых общая выручка больше 1000 + */ + +-- Задача 6. Страны с достаточным числом активных пользователей + +SELECT country, + COUNT(id) AS active_users_count +FROM dmezentsev.users +WHERE is_active = TRUE +GROUP BY country +HAVING COUNT(id) >= 2 +ORDER BY COUNT(id) DESC; + +/* - Почему фильтр по активности (is_active = true) логичнее писать в WHERE, а не в HAVING? + +Логичнее условие писать в WHERE, потому что производительнее сперва будет +отфильтровать нужные данные, а по ним уже потом проводить группировку + +- Что покажет запрос, если в данных пока нет ни одной страны с active_users_count >= 2? + +Если под условие в HAVING не попадет ни один агрегат, то запрос не выведет ни +одной строки + */ + +-- Задача 7. Топ‑товары по количеству продаж + +SELECT name AS product_name, + SUM(quantity) AS total_quantity_sold +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id +GROUP BY name +ORDER BY SUM(quantity) DESC +LIMIT 3; + +/* - Чем рейтинг по total_quantity_sold отличается от +рейтинга по выручке (SUM(quantity * price_at_purchase))? + +- В каких случаях бизнесу важнее смотреть на количество, а не на деньги? + +В поле total_quantity_sold выводим товары, которые продавались чаще всего. +SUM(quantity * price_at_purchase) вывел бы выручку наиболее часто продаваемых товаров +Предположу, что кол-во важнее выручки в случае, когда важно завоевать место в нише. +Как только увеличится база лояльных клиентов, обычно потом начинают поднимать цену + */ + +-- Задача 8. Заказы с «маленькой» выручкой + +SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue +FROM dmezentsev.order_items +GROUP BY order_id +HAVING SUM(quantity * price_at_purchase) < 100 +ORDER BY SUM(quantity * price_at_purchase); + +/* - Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? + +Маленький по выручке заказ - это покупка чего-то дешевого (либо повседневного). Или же +использование скидок/купонов. + +- Что проще увидеть глазами: просто все заказы с суммой или +именно отфильтрованные «микро‑заказы»? + +В этом полезность фильтрации, чтобы не размывать взгляд на абсолютно все +заказы, а оставить конкретные (< 100, например). И уже проанализировать их + */ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/JOIN.sql b/dmezentsev/postgresql/homework/JOIN.sql new file mode 100644 index 0000000..28f146b --- /dev/null +++ b/dmezentsev/postgresql/homework/JOIN.sql @@ -0,0 +1,207 @@ +-- Задание 1: Заказы + кто заказал + +SELECT u.name AS user_name, + u.country, + o.id AS order_id, + o.order_date, + o.status +FROM dmezentsev.users u +LEFT JOIN dmezentsev.orders o ON o.user_id = u.id; + +SELECT u.name AS user_name, + u.country, + o.id AS order_id, + o.order_date, + o.status +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON o.user_id = u.id; + +/* - С какой таблицы логичнее начать: orders или users? Почему? + +Логичнее начать с таблицы users. А дальше вариантами джойна выбирать, что +нужно: если оставить только тех пользователей, у которых был хотя бы один заказ, +то использовать INNER JOIN. Если вывести так же пользователей, у которых заказа +не было, то LEFT JOIN + +- Какой тип JOIN здесь уместнее: INNER или LEFT? Напиши запрос в обоих вариантах и посмотри, +где пропадут строки. + +При INNER JOIN пропадут три строки с пользователями, у которых не было ни одного заказа +*/ + +-- Задание 2: Чек одного заказа + +SELECT oi.order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.order_items oi +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE oi.order_id = 101; + +/* - Что изменится, если эту фильтрацию order_id = 101 перенести в JOIN (после ON)? + +При использовании INNER JOIN оба варианта (WHERE order_id = 101 или +перенести в JOIN (после ON)) дадут одинаковый результат. + +Но: +LEFT JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE oi.order_id = 101 +Сначала делается джойн, потом фильтруются данные по условию. Вернутся строки, +удовлетворяющие условию фильтрации + +LEFT JOIN dmezentsev.products p ON oi.product_id = p.id AND oi.order_id = 101 +Тут возможно будет больше строк, так как вернутся строки из order_items c NULL +в таблице products +*/ + +-- Задание 3: Полный чек по всем заказам + +SELECT o.id AS order_id, + u.name AS user_name, + p.name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id; + +/* - почему после JOIN заказ 3 появляется в нескольких строках? + +Заказ 3 появился в двух строках, потому что при джойне кол-во строк может +увеличиваться. Это связано с тем, что в этом заказе было два наименования, +отсюда и увеличение строк + +- Чем результат отличается от задания 2 (по количеству строк и информации)? + +В этом запросе строк и информации больше, потому что информация запрошена +по всему магазину, а не по конкретному заказу 101 + */ + +-- Задание 4: Все пользователи + их заказы (включая тех, у кого заказов нет) + +SELECT u.name AS user_name, + o.id AS order_id, + o.status +FROM dmezentsev.users u +LEFT JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Чем этот запрос отличается от такого же, но с INNER JOIN? + +Если бы тут был INNER JOIN, то пользователи без заказов не попали бы в +результирующую таблицу + */ + +-- Задание 5: Пользователи без заказов + +SELECT u.id AS user_id, + u.name AS user_name +FROM dmezentsev.users u +LEFT JOIN dmezentsev.orders o ON u.id = o.user_id +WHERE o.id IS NULL; + +/* - Почему тут важно фильтровать именно по колонке из **`orders`**, а не из **`users`**? + +Фильтрую по полю orders.id, потому что это первичный ключ таблицы, +который подразумевает собой факт покупки. То есть если после джойна +это поле IS NULL, то у пользователя не было ни одного заказа + */ + +-- Задание 6: Заказы, где есть Kindle + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE p.name LIKE '%Kindle%'; + +-- Задание 7: Товары и в каких заказах они покупались + +SELECT p.name AS product_name, + oi.order_id, + oi.quantity +FROM dmezentsev.products p +LEFT JOIN dmezentsev.order_items oi ON p.id = oi.product_id; + +/* - Что будет в order_id, если товар ни разу не покупали? + +Если бы товар не был ни разу куплен, то в поле order_id был NULL +из-за использования LEFT JOIN + +- Чем это отличается от варианта, если использовать INNER JOIN? + +При использовании INNER JOIN товар, который ни разу не заказывали, +не попал бы в итоговую таблицу + */ + +-- Задание 8: Только оплаченные заказы + их состав + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE o.status = 'paid'; + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id AND o.status = 'paid' +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id; + +/* - Что будет, если условие status = 'paid' написать +в JOIN с orders (там же, где ON)? Изменится ли результат сейчас и почему? + +Результат будет один и тот же, потому что используется INNER JOIN. +Строки по условию o.status = 'paid' отсекутся в любом случае, что +при WHERE, что на шаге JOIN + */ + +-- Задание 9: Заказы пользователя Alice + +SELECT o.id AS order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE u."name" = 'Alice'; + +/* - Если у Alice нет ни одного заказа, сколько строк вернёт твой запрос и почему? + +Если бы у Alice не было заказов, то запрос не вернул ничего, так как +не было бы ни одной строки, удовлетворяющей условию фильтрации + */ + +-- Задание 10: Заказы без товаров + +SELECT + o.id AS order_id, + o.user_id, + o.status +FROM dmezentsev.orders o +LEFT JOIN dmezentsev.order_items oi ON o.id = oi.order_id +WHERE oi.order_id IS NULL; + +/* - Что будет, если вместо LEFT JOIN написать INNER JOIN — изменится ли результат и как? + +Если изменить LEFT JOIN на INNER JOIN, то результирующих строк вообще не будет. +INNER JOIN оставит только те заказы, у которых есть хотя бы одна запись в order_items. +То есть при INNER JOIN колонка oi.order_id никогда не будет NULL + */ diff --git a/dmezentsev/postgresql/homework/UNION.sql b/dmezentsev/postgresql/homework/UNION.sql new file mode 100644 index 0000000..9e2a3c8 --- /dev/null +++ b/dmezentsev/postgresql/homework/UNION.sql @@ -0,0 +1,107 @@ +-- Задание 1. Активные и неактивные пользователи в одном списке +SELECT id, + name, + CASE + WHEN is_active THEN 'active' + END AS user_status +FROM dmezentsev.users +WHERE is_active = TRUE + +UNION ALL + +SELECT id, + name, + CASE + WHEN NOT is_active THEN 'inactive' + END AS user_status +FROM dmezentsev.users +WHERE is_active = FALSE + +ORDER BY name; + +/* - Что будет, если вместо UNION ALL написать UNION? + +Кол-во строк при UNION и UNION ALL не изменится, потому что все строки +в таблице users уникальны + +- Зачем в результат добавлять колонку user_status, если флаг активности +уже есть в таблице users? + +Предположу, что добавляем колонку user_status только лишь для удобства +понимания результирующей таблицы всем тем, кто далек от TRUE или FALSE. + */ + +-- Задание 2. Заказы до и после даты в одной ленте + +SELECT + id AS order_id, + user_id, + order_date, + 'old' AS period +FROM dmezentsev.orders +WHERE order_date < '2024-01-01' + +UNION ALL + +SELECT + id AS order_id, + user_id, + order_date, + 'new' AS period +FROM dmezentsev.orders +WHERE order_date >= '2024-01-01' + +ORDER BY order_date, order_id; + +/* - Как ты по колонке period поймёшь, сколько заказов было + до и после даты‑границы (словами, без SQL)? + +Нужно посчитать количество строк, где в колонке period стоит +значение 'old' — это и будет число заказов до 2024-01-01. +А количество строк со значением 'new' — это число заказов начиная с 2024-01-01 и позже. + +- Какой подход тебе кажется понятнее для новичка в команде: +два SELECT + UNION ALL или один SELECT с CASE WHEN? Почему? + +Вот совсем для новичка, пожалуй, вариант с UNION ALL выглядит понятнее. +Два простых запроса, которые потом объединены в общую таблицу, к тому же +их можно выполнить отдельно + */ + +-- Задание 3. Товары, которые покупали, и которые не покупали + +SELECT DISTINCT + p.id AS product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id + +UNION ALL + +SELECT + p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM dmezentsev.products p +LEFT JOIN dmezentsev.order_items oi ON p.id = oi.product_id +WHERE oi.product_id IS NULL + +ORDER BY product_name; + +/* - Почему в запросе на «не покупали» проверка IS NULL должна быть +по полю из order_items, а не из products? + +LEFT JOIN гарантирует, что все строки из products останутся в результате. +Если для товара нет записей в order_items, то все колонки из order_items будут NULL. + +- Какой сигнал для бизнеса даёт список с product_status = 'never_ordered'  +(1–2 идеи: что можно с этими товарами сделать)? + +Список never_ordered даст понимание бизнесу, что либо товар не востребован +(очень дорогой, непродающее описание), либо о товаре никто не знает (проблемы +с рекламой и продвижением). То есть решив эти кейсы, ситуацию можно изменить +в лучшую сторону. +*/ + + \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/base.sql b/dmezentsev/postgresql/homework/base.sql new file mode 100644 index 0000000..b5833e4 --- /dev/null +++ b/dmezentsev/postgresql/homework/base.sql @@ -0,0 +1,73 @@ +-- Задание 1 — Спроектируй «карточку пользователя» + +SELECT id, + name, + country, + created_at, + email +FROM dmezentsev.users u +ORDER BY created_at DESC, name; + +/* - Почему выбрал именно эти столбцы и зачем такая сортировка? + + Сортировка нужна для того, чтобы вывести пользователей с датой регистрации по убыванию. + Если даты бы совпадали, то была дополнительная сортировка по имени внутри одной даты + */ + +-- Задание 2 — Сегментация по странам (с выбором) + +SELECT id, + name, + country +FROM dmezentsev.users u +WHERE country = 'US' +ORDER BY name; + +SELECT id, + name, + country +FROM dmezentsev.users u +WHERE country = 'UK' +ORDER BY name; + +/* - Какой из двух запросов маркетологу полезнее и почему? + + Предположу, что маркетологу выгоднее рассматривать пользователей из US, + потому что их больше (5 US > 3 UK) + */ + +-- Задание 3 — Контакты для рассылки + +SELECT id, + name, + email +FROM dmezentsev.users u +WHERE email IS NOT NULL +ORDER BY name; + +SELECT DISTINCT(LOWER(email)) email +FROM dmezentsev.users u +WHERE email IS NOT NULL; + +/* - Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы? + +Нужно приводить email к одному виду, потому что хранение одного и того же email +в разных регистрах приведет к дублированию данных. +По факту же один пользователь - один email +*/ + +-- Задание 4 — Новички и активность + +SELECT id, + name, + created_at +FROM dmezentsev.users u +WHERE created_at < '2024-03-11' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; + +/* - В каком реальном сценарии тебе пригодится OFFSET? + +Предположу, OFFSET понадобится, когда нужно убрать из выборки +какой-то топ пользователей. Допустим, нужны все, кроме первой тройки + */ From 21aa48c237e08642e8f901586a7a9f5cede838fb Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 17 May 2026 17:18:10 +0300 Subject: [PATCH 144/179] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE:=20'=D0=A2=D0=B8=D0=BF=D1=8B=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86',=20'View=20=D0=B8=20Distribute?= =?UTF-8?q?=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BA=D0=B8'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice_view_and_distribute_engines.sql | 33 +++++++++---------- .../2. Типы данных/practice_data_types.sql | 30 +++++++++-------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql index 67eacc4..2a7de23 100644 --- a/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql +++ b/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql @@ -18,9 +18,9 @@ ENGINE = Distributed( 'clickhouse_cluster', 'mnoskov', 'logs', - rand() + user_id ); --- параметр rand() - это ключ распределния как я понял. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. +-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. -- Задание 2 @@ -54,16 +54,15 @@ LIMIT 100; -- Задание 3 --- тут вообще жопа какая-то. Если смотреть на пример создания MV, то выдает ошибку мол engine указывать нельзя. Полез в документацию, там сказано что после указания имени mv нужно добавлять TO ... но все равно выдется ошибка. --- этот способ я в хабре нашел CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster ( - event_time DateTime, - user_id UInt32, - event_type String + event_time DateTime, + user_id UInt32, + event_type String ) -ENGINE = MergeTree -ORDER BY (event_time, user_id); +ENGINE=MergeTree +ORDER BY(event_time,user_id); CREATE TABLE mnoskov.agr_clicks_per_day ON CLUSTER clickhouse_cluster @@ -71,17 +70,17 @@ ON CLUSTER clickhouse_cluster event_date Date, count UInt64 ) -ENGINE = MergeTree +ENGINE=SummingMergeTree ORDER BY event_date; -CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day TO mnosokv.agr_clicks_per_day AS +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day +ON CLUSTER clickhouse_cluster +TO mnoskov.agr_clicks_per_day AS SELECT toDate(event_time) AS event_date, count(*) AS count -FROM - mnoskov.raw_events -GROUP BY - event_date; +FROM mnoskov.raw_events +GROUP BY event_date; -SELECT * FROM mnoskov.agr_clicks_per_day; --- ничего у меня получилось, и с SummingMergeTree ни с MergeTree. Как очень непонятно материализованные таблицы работают. Если делать как в теории в инфре, то тоже ничего не работает. \ No newline at end of file +SELECT * FROM mnoskov.raw_events; +SELECT * FROM mnoskov.agr_clicks_per_day; \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql index a92fb71..b8d8218 100644 --- a/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql +++ b/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql @@ -13,18 +13,24 @@ ENGINE = ReplicatedMergeTree( ORDER BY tuple(); SELECT + user_id, + balance, + created_at +FROM ( + SELECT user_id, toDecimal64OrNull(balance,2) AS balance, toDateTime64OrNull(created_at, 3) AS created_at FROM mnoskov.raw_data1 +) WHERE balance IS NOT NULL AND created_at IS NOT NULL; -- Задание 2 -CREATE TABLE events +CREATE TABLE mnoskov.events ON CLUSTER clickhouse_cluster ( event_id String, event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" @@ -42,18 +48,16 @@ SELECT * FROM mnoskov.events; SELECT event_id, - toDate(parseDateTimeBestEffortOrNull(event_date)) AS event_dates, - CASE - WHEN toDayOfMonth(parseDateTimeBestEffortOrNull(event_time)) = 1 THEN toHour(parseDateTimeBestEffortOrNull(event_time)) - ELSE (toDayOfMonth(parseDateTimeBestEffortOrNull(event_time)) - 1) * 24 + toHour(parseDateTimeBestEffortOrNull(event_time)) - END AS event_times, - toDateTime(parseDateTimeBestEffortOrNull(event_date)) + toIntervalHour(event_times) AS event_datetime -FROM - mnoskov.events + event_datetime +FROM ( + SELECT + event_id, + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime + FROM + mnoskov.events +) ORDER BY - event_dates; --- Все преобразования с to... и parse... нашел тут https://clickhouse.com/docs/ru/sql-reference/functions/type-conversion-functions#toIntervalMonth. --- explain analyze не работает, потэому не могу посмотреть насколько это эффективно, НО главное сам запрос работает. + event_datetime; -- Задание 3 @@ -73,7 +77,7 @@ ORDER BY SELECT user_id, - count(*) + count(*) as user_actions_cnt FROM mnoskov.user_activity WHERE From 392f412bbdb2e8a675ac20efe375f166832006de Mon Sep 17 00:00:00 2001 From: mnoskov Date: Sun, 17 May 2026 22:14:14 +0300 Subject: [PATCH 145/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A1=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D1=81'=20-=20Clickhouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1. Движки/1. Семейство MergeTree/practice_table_types.sql | 0 .../2. View и Distribute движки}/practice_view_and_distribute_engines.sql | 0 .../2. Типы данных/practice_data_types.sql | 0 .../2. Запросы/Синтаксис/practice_syntax.sql | 215 ++++++++++++++++++ 4 files changed, 215 insertions(+) rename mnoskov/clickhouse/практика clickhouse/{ => 1. Таблицы и типы данных}/1. Движки/1. Семейство MergeTree/practice_table_types.sql (100%) rename mnoskov/clickhouse/практика clickhouse/{1. Движки => 1. Таблицы и типы данных/1. Движки/2. View и Distribute движки}/practice_view_and_distribute_engines.sql (100%) rename mnoskov/clickhouse/практика clickhouse/{ => 1. Таблицы и типы данных}/2. Типы данных/practice_data_types.sql (100%) create mode 100644 mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql diff --git a/mnoskov/clickhouse/практика clickhouse/1. Движки/1. Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql similarity index 100% rename from mnoskov/clickhouse/практика clickhouse/1. Движки/1. Семейство MergeTree/practice_table_types.sql rename to mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql diff --git a/mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql similarity index 100% rename from mnoskov/clickhouse/практика clickhouse/1. Движки/practice_view_and_distribute_engines.sql rename to mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql diff --git a/mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql similarity index 100% rename from mnoskov/clickhouse/практика clickhouse/2. Типы данных/practice_data_types.sql rename to mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql diff --git a/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql b/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql new file mode 100644 index 0000000..da7ae46 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql @@ -0,0 +1,215 @@ +-- Задание 1 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + age UInt8, + country String, + is_active UInt8 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/users', + '{replica}' +) +ORDER BY user_id; + +INSERT INTO mnoskov.users +(user_id,name,age,country,is_active) +VALUES +(1,'Ivan',30,'RU',1), +(2,'Anna',25,'RU',1), +(3,'Petr',45,'RU',0), +(4,'Olga',35,'RU',1), +(5,'John',55,'US',1), +(6,'Alice',42,'US',0), +(7,'Tom',38,'US',1), +(8,'Lucas',28,'BR',1), +(9,'Maria',60,'BR',1), +(10,'Joao',27,'BR',0), +(11,'Kenji',33,'JP',1), +(12,'Yuki',29,'JP',0), +(13,'Chen',40,'CN',1), +(14,'Li',48,'CN',1), +(15,'Fatima',31,'AE',1), +(16,'Ahmed',36,'AE',1), +(17,'George',24,'US',1), +(18,'Nina',34,'RU',1), +(19,'Max',50,'DE',1), +(20,'Sophie',22,'DE',0); + +SELECT * FROM mnoskov.users; + +--1 +SELECT + user_id, + age, + is_active +FROM + mnoskov.users +WHERE + is_active = TRUE +ORDER BY + age DESC +LIMIT 10; + +--2 +SELECT + name, + country +FROM + mnoskov.users +WHERE + country = 'RU' +ORDER BY + name; + +--3 +SELECT + user_id, + age, + is_active +FROM + mnoskov.users +WHERE + is_active = TRUE +LIMIT 10; +-- Данные вывелить так как лежат на диске. + +--4 +SELECT + country, + count() +FROM + mnoskov.users +GROUP BY + country; + +--5 +SELECT + country, + min(age) AS min_age, + max(age) AS max_age +FROM + mnoskov.users +GROUP BY + country; + +--6 +SELECT + country, + count() AS cnt_users +FROM + mnoskov.users +GROUP BY + country +ORDER BY + cnt_users DESC; + +--Доп +SELECT + uniqExact(country), + uniq(country) +FROM + mnoskov.users; +-- uniqExact(country) - кол-во уникальных значений в колонке country. Но uniq работает также. +-- В инете написано что uniq быстрее. + + +-- Задание 2 +CREATE TABLE mnoskov.events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_time DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events1', + '{replica}' +) +ORDER BY(user_id,event_time); + +INSERT INTO mnoskov.events1 +(user_id,event_time,event_type) +VALUES +(1,'2025-07-17 09:00:00','click'), +(1,'2025-07-17 09:05:00','view'), +(1,'2025-07-17 09:10:00','purchase'), +(2,'2025-07-17 10:00:00','click'), +(2,'2025-07-17 10:05:00','click'), +(2,'2025-07-17 11:00:00','view'), +(3,'2025-07-16 12:00:00','view'), +(3,'2025-07-16 12:30:00','click'), +(3,'2025-07-17 08:00:00','purchase'), +(4,'2025-07-17 13:00:00','click'), +(4,'2025-07-17 14:00:00','view'), +(4,'2025-07-17 15:00:00','view'), +(5,'2025-07-16 20:00:00','view'), +(5,'2025-07-17 01:00:00','click'), +(5,'2025-07-17 06:00:00','purchase'), +(6,'2025-07-17 07:30:00','view'), +(6,'2025-07-17 08:45:00','click'), +(7,'2025-07-16 23:59:59','click'), +(7,'2025-07-17 00:01:00','purchase'), +(7,'2025-07-17 10:10:00','view'); + +SELECT * FROM mnoskov.events1; + +--1 +SELECT + user_id, + event_time, + event_type, + count() OVER(PARTITION BY user_id ORDER BY event_time) AS com_users +FROM + mnoskov.events1; + +--2 +SELECT + user_id, + event_time, + event_type, + row_number() OVER (PARTITION BY user_id ORDER BY event_time) rn, + dense_rank() OVER (PARTITION BY user_id ORDER BY event_time) dr, + count() OVER(PARTITION BY user_id ORDER BY event_time) AS c, + sum(1) OVER(PARTITION BY user_id ORDER BY event_time) AS s +FROM + mnoskov.events1; + +--3 +SELECT + user_id, + event_time, + event_type, + count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS com_users, + count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS com_users2 +FROM + mnoskov.events1; +-- все четенько работает, никаких ошибкок нет + +--4-5-6 +WITH recent_events AS ( + SELECT + user_id, + event_time, + event_type + FROM + mnoskov.events1 + WHERE + event_time < now() - INTERVAL 1 DAY +) +SELECT + user_id, + count(*) AS cnt_user_actions +FROM + recent_events +GROUP BY + user_id +ORDER BY + cnt_user_actions DESC +LIMIT + 5; +-- не понял, как сделать 6 пункт. + +-- Ответ на вопрос - ну да - поддерживает. \ No newline at end of file From 80d333fe52da63203c936175e2bb65ffe59dbcf4 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 18 May 2026 09:55:03 +0300 Subject: [PATCH 146/179] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20-=20'=D0=A1=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=81'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2. Запросы/Синтаксис/practice_syntax.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql b/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql index da7ae46..c7c4d8c 100644 --- a/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql +++ b/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql @@ -6,7 +6,7 @@ ON CLUSTER clickhouse_cluster name String, age UInt8, country String, - is_active UInt8 + is_active Bool ) ENGINE = ReplicatedMergeTree( '/clickhouse/tables/{shard}/mnoskov/users', @@ -79,7 +79,7 @@ LIMIT 10; --4 SELECT country, - count() + count() as cnt_users FROM mnoskov.users GROUP BY From f427cfe22e29f9935da2e7c684ce42f5c6a039d8 Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Mon, 18 May 2026 11:55:15 +0300 Subject: [PATCH 147/179] fix WHERE in task 1 --- vberetar/postgresql/sql_filters.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vberetar/postgresql/sql_filters.sql b/vberetar/postgresql/sql_filters.sql index 423f131..74925dd 100644 --- a/vberetar/postgresql/sql_filters.sql +++ b/vberetar/postgresql/sql_filters.sql @@ -5,7 +5,7 @@ SELECT name, category, price, FROM products WHERE (price BETWEEN 100 AND 1300) AND category NOT IN ('smartwatch') - AND in_stock = 'true'; + AND in_stock = TRUE; -- Вопрос: -- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? From ae208a76fb8011336f8d8151569d54ed7e885ca3 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 18 May 2026 16:55:22 +0300 Subject: [PATCH 148/179] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8:=20'=D0=A1=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=81'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Практика: 'JOIN' - 'Массивы, Map и вложенные структуры*' - 'Партиции' - 'Индексы'. --- .../1. Движки/1. Семейство MergeTree/practice_table_types.sql | 120 ++++++++++++++++++ .../2. View и Distribute движки/practice_view_and_distribute_engines.sql | 86 +++++++++++++ .../2. Типы данных/practice_data_types.sql | 115 +++++++++++++++++ .../2. Запросы/1. Синтаксис}/practice_syntax.sql | 0 .../2. Запросы/2. JOIN/practice_join.sql | 57 +++++++++ .../practice_arrays_maps_and_nested_structures.sql | 44 +++++++ .../1. Партиции/practice_partitions.sql | 86 +++++++++++++ .../2. Индексы/practice_indexes.sql | 69 ++++++++++ 8 files changed, 577 insertions(+) create mode 100644 mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql create mode 100644 mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql create mode 100644 mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql rename mnoskov/clickhouse/{практика clickhouse/2. Запросы/Синтаксис => Практика clickhouse/2. Запросы/1. Синтаксис}/practice_syntax.sql (100%) create mode 100644 mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql create mode 100644 mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры*/practice_arrays_maps_and_nested_structures.sql create mode 100644 mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql create mode 100644 mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql new file mode 100644 index 0000000..6140c2f --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql @@ -0,0 +1,120 @@ +-- Задание 1 + +CREATE TABLE mnoskov.page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + + +-- Задание 2 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + name String, + update_at DateTime +) +ENGINE = ReplacingMergeTree(update_at) +ORDER BY id; + + +-- Задание 3 +CREATE TABLE mnoskov.region_sales +ON CLUSTER clickhouse_cluster +( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO mnoskov.region_sales (region, date, sales) +VALUES + ('Moscow', '2026-05-01', 100), + ('Moscow', '2026-05-01', 150), + ('Moscow', '2026-05-01', 200), + ('Berlin', '2026-05-02', 300), + ('Berlin', '2026-05-02', 120), + ('Berlin', '2026-05-02', 80), + ('Paris', '2026-05-03', 500), + ('Paris', '2026-05-03', 250), + ('London', '2026-05-04', 400), + ('Tokyo', '2026-05-05', 700); + +SELECT * FROM mnoskov.region_sales; +-- Все четко работает. + + +-- Задание 4 +CREATE TABLE mnoskov.user_metrics +ON CLUSTER clickhouse_cluster +( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +CREATE TABLE mnoskov.user_metrics_source +ON CLUSTER clickhouse_cluster +( + region String, + age UInt8, + user_id UInt64 +) +ENGINE = MergeTree +ORDER BY region; + +INSERT INTO mnoskov.user_metrics_source +(region, age, user_id) +VALUES + ('Moscow', 25, 1001), + ('Moscow', 30, 1002), + ('Moscow', 22, 1003), + ('Moscow', 25, 1001), + ('Berlin', 40, 2001), + ('Berlin', 35, 2002), + ('Berlin', 29, 2003), + ('Berlin', 35, 2002), + ('Paris', 31, 3001), + ('Paris', 28, 3002), + ('Paris', 45, 3003), + ('Paris', 31, 3001), + ('London', 50, 4001), + ('London', 41, 4002), + ('London', 38, 4003), + ('London', 41, 4002), + ('Tokyo', 27, 5001), + ('Tokyo', 33, 5002), + ('Tokyo', 29, 5003), + ('Tokyo', 33, 5002); +SELECT * FROM mnoskov.user_metrics_source; + +INSERT INTO mnoskov.user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(user_id) AS active_users_state +FROM + mnoskov.user_metrics_source +GROUP BY + region; + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM + mnoskov.user_metrics +GROUP BY + region; +-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql new file mode 100644 index 0000000..2a7de23 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster ( + dt DateTime, + user_id UInt32, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY dt; + +CREATE TABLE mnoskov.dist_logs +ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'mnoskov', + 'logs', + user_id +); +-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY (ts, user_id); + +CREATE VIEW mnoskov.top_active_users AS +SELECT + user_id, + count(*) AS cnt_user +FROM + mnoskov.events +GROUP BY + user_id +ORDER BY + cnt_user DESC, user_id +LIMIT 100; +-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW. + + +-- Задание 3 + +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE=MergeTree +ORDER BY(event_time,user_id); + +CREATE TABLE mnoskov.agr_clicks_per_day +ON CLUSTER clickhouse_cluster +( + event_date Date, + count UInt64 +) +ENGINE=SummingMergeTree +ORDER BY event_date; + +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day +ON CLUSTER clickhouse_cluster +TO mnoskov.agr_clicks_per_day AS +SELECT + toDate(event_time) AS event_date, + count(*) AS count +FROM mnoskov.raw_events +GROUP BY event_date; + +SELECT * FROM mnoskov.raw_events; +SELECT * FROM mnoskov.agr_clicks_per_day; \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql new file mode 100644 index 0000000..b8d8218 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql @@ -0,0 +1,115 @@ +-- Задание 1 + +CREATE TABLE mnoskov.raw_data1 +ON CLUSTER clickhouse_cluster ( + user_id String, + balance String, + created_at String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_data1', + '{replica}' +) +ORDER BY tuple(); + +SELECT + user_id, + balance, + created_at +FROM ( + SELECT + user_id, + toDecimal64OrNull(balance,2) AS balance, + toDateTime64OrNull(created_at, 3) AS created_at +FROM + mnoskov.raw_data1 +) +WHERE + balance IS NOT NULL AND created_at IS NOT NULL; + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY + event_id; + +SELECT * FROM mnoskov.events; + + +SELECT + event_id, + event_datetime +FROM ( + SELECT + event_id, + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime + FROM + mnoskov.events +) +ORDER BY + event_datetime; + + +-- Задание 3 + +CREATE TABLE mnoskov.user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/user_activity', + '{replica}' +) +ORDER BY + user_id; + +SELECT + user_id, + count(*) as user_actions_cnt +FROM + mnoskov.user_activity +WHERE + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id +ORDER BY + user_id; + + +-- Задание 4 + +CREATE TABLE mnoskov.sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sensor_data', + '{replica}' +) +ORDER BY sensor_id; + +--1-2. +SELECT + toString(status) AS str_status, + avg(value) AS avg_value +FROM + mnoskov.sensor_data +GROUP BY + str_status; +--3 +-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0. \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql similarity index 100% rename from mnoskov/clickhouse/практика clickhouse/2. Запросы/Синтаксис/practice_syntax.sql rename to mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql new file mode 100644 index 0000000..f29dbc3 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql @@ -0,0 +1,57 @@ +-- Задание 1 + +SELECT + pro.product_id, + pro.name, + pri.price +FROM + mnoskov.products pro +ANY LEFT JOIN + mnoskov.prices pri ON pro.product_id = pri.product_id; +-- 2 Выберется случайная из цен. +-- 3 ALL LEFT JOIN - работает также как и LEFT JOIN. Его избегают потому что он тянет больше строк в оперативную память. + + +-- Задание 2 + +SELECT + o.time, + o.symbol, + o.size, + t.price +FROM + mnoskov.orders o +ASOF JOIN + mnoskov.ticks t ON o.symbol = t.symbol AND t.time <= o.time +WHERE symbol = 'AAPL'; +-- Потому что дата сделки и дата последней фиксации цены - разные. + + +-- Задание 3 + +SELECT + user_id, + name +FROM + mnoskov.users +WHERE user_id IN ( + SELECT user_id + FROM mnoskov.payments + WHERE amount > 1000 +); +-- 3 Потому что это фильтрация по существованию. Как оптимизируется не знаю, наверное как подзапрос. + + +-- Задание 4 + +SELECT + bl.user_id, + u.name AS user_name, + bl.event_time, + bl.event_type +FROM + mnoskov.big_logs bl +JOIN + mnoskov.users u ON bl.user_id = u.user_id; +-- 2 потому что таблица users меньше, а значит в оператику полетит меньше данных. +-- 3 если user_id уникален, то нужно выбирать ALL, так как дублей нет. В текущем задании дубли есть, но мы все равно выбираем ALL, тк нужно вывести имя для каждого лога а не юзера. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры*/practice_arrays_maps_and_nested_structures.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры*/practice_arrays_maps_and_nested_structures.sql new file mode 100644 index 0000000..6ed16cc --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры*/practice_arrays_maps_and_nested_structures.sql @@ -0,0 +1,44 @@ +-- Задание 1 + +--1-2 +SELECT + user_id, + tags, + indexOf(tags, 'promo') AS pos_promo +FROM + mnoskov.events +WHERE + has(tags, 'promo'); + +--3 +SELECT + user_id, + arrayJoin(tags) AS tag +FROM + mnoskov.events; + + +-- Задание 2 + +--1 +SELECT user_id, scores['math'] FROM mnoskov.metrics; + +--2 +SELECT user_id, mapValues(scores) FROM mnoskov.metrics; + + +-- Заданик 3 + +--1 +SELECT * FROM mnoskov.users_nested WHERE has(contacts.type, 'email'); + +--2 +SELECT + id, + name, + c.type, + c.value +FROM + mnoskov.users_nested +ARRAY JOIN + contacts AS c; \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql new file mode 100644 index 0000000..2a855a6 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.sales +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + sale_date Date, + amount Float64 +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sales', + '{replica}' +) +PARTITION BY toYYYYMM(sale_date) +ORDER BY sale_date; + +ALTER TABLE mnoskov.sales DROP PARTITION '202405'; +SELECT * FROM mnoskov.sales; +-- Пропали строки где год/месяц 202405. +-- Если не указывать PARTITION BY, то строки бы искались по всей таблице, что дольше и затранее. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + id UInt64, + event_type String, + event_date Date +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events_partitioned', + '{replica}' +) +PARTITION BY event_type +ORDER BY event_date; + +ALTER TABLE mnoskov.events DROP PARTITION 'click'; +ELECT * FROM mnoskov.events; +-- Такой подход опасен, тк например login выполняется не часто по сравнению с click и получается жесткий перевес. + + +-- Задание 3 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + user_id UInt64, + log_time DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs_partitioned', + '{replica}' +) +PARTITION BY cityHash64(user_id) % 100 +ORDER BY log_time; + +SELECT * FROM mnoskov.logs + +-- 2 с точки зрения баланса, у нас юзеры распределные равномерно и нет никаких перекосов. +-- 3 хз если честно, мне кажется должны ускорять. + + +-- Задание 4 + +CREATE TABLE mnoskov.visits +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + visit_date Date +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/visits', + '{replica}' +) +PARTITION BY (region, toYYYYMM(visit_date)) +ORDER BY visit_date; + +SELECT * FROM mnoskov.visits; +-- 2 доступа к системным таблицам нет, потэтому я не могу посмотреть имя партиции. +-- 3 ну да +ALTER TABLE mnoskov.visits DROP PARTITION ('RU', '202407'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql new file mode 100644 index 0000000..7147e3e --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql @@ -0,0 +1,69 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + log_id UInt64, + ts DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs_ttl', + '{replica}' +) +ORDER BY ts; +ALTER TABLE mnoskov.logs ADD INDEX idx_ts ts TYPE minmax granularity 2; + +SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'; + +-- 2 У меня нет доступа к системным таблицам, я не могу посмотреть. +-- Если в order by будет log_id а не ts, то индекс не будет работать. + + +-- Задание 2 + +CREATE TABLE mnoskov.clicks +ON CLUSTER clickhouse_cluster +( + id UInt64, + country String +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/clicks', + '{replica}' +) +ORDER BY id; + +ALTER TABLE mnoskov.clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; +ALTER TABLE mnoskov.clicks DROP INDEX idx_country; + +explain +SELECT * FROM clicks WHERE country = 'RU'; +-- 3 не использутся индеск, я туда 11тыс строк заинсертил, все равно не используется. +-- 4 я думаю индекс не будет работать эффективно, а может быть и вообще будет запрос замедлять. + + +-- Задание 3 + +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + email String +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/users_simple', + '{replica}' +) +ORDER BY id; + +ALTER TABLE mnoskov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + +explain +SELECT * FROM users WHERE email = 'alice@mail.com'; +-- 2 Ну я не вижу в EXPLAIN какое либо упоминание индекса, но я ссылаюсь на то, что просто мало данных в таблице. + +-- 3 Потому что этот индекс работает только на точные совпадения - exact match. + +explain +SELECT * FROM users WHERE email LIKE 'alice%'; +-- 4 Индекс не будет использоваться. \ No newline at end of file From c2eacd2bde15422c0d22d6950e3c00e72ec2f226 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 18 May 2026 19:55:26 +0400 Subject: [PATCH 149/179] =?UTF-8?q?1.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D0=BE=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BA=D0=BB=D1=8E=D1=87=20=D0=B4=D0=BB=D1=8F=20course=5Fcom?= =?UTF-8?q?pletion=5Frate,=20=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20DROP?= =?UTF-8?q?=20TABLE=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8C=D1=8E=D1=85,=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8=D0=BB=20age=20UInt8=20?= =?UTF-8?q?=D0=BD=D0=B0=20UInt32=202.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D1=85=D0=B5=D0=BC=D1=8B=20=D0=B2=20=D0=AD?= =?UTF-8?q?=D1=82=D0=B0=D0=BF=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 2. Создание витринных таблиц.sql | 9 ++++++++- .../Pet-Project/Этап 3. Анализ и запросы.sql | 12 ++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql b/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql index e2dc606..8f6e555 100644 --- a/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql +++ b/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql @@ -18,6 +18,8 @@ CREATE TABLE dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster ) ORDER BY (course_id, lesson_id); +DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster SYNC; + CREATE MATERIALIZED VIEW dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster TO dsolovianov.lesson_popularity_summary AS @@ -58,7 +60,7 @@ CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster ( user_id UInt32, name String, email String, - age UInt8, + age UInt32, registered_courses_count UInt32, registration_date Date ) ENGINE = ReplicatedMergeTree( @@ -67,6 +69,8 @@ CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster ( ) ORDER BY user_id; +DROP TABLE IF EXISTS dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster SYNC; + CREATE MATERIALIZED VIEW dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster TO dsolovianov.inactive_users_summary AS @@ -184,3 +188,6 @@ FROM dsolovianov.core_enrollments e JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id GROUP BY e.user_id, u.name, e.course_id, c.title; + +SELECT * FROM dsolovianov.course_completion_rate ORDER BY user_id; + diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql b/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql index 6550e5b..875ee9a 100644 --- a/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql +++ b/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql @@ -1,21 +1,25 @@ --- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам -- Напиши свои вариации для практики-- +-- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам +-- Напиши свои вариации для практики-- + + -- Топ-5 самых просматриваемых уроков SELECT lesson_title, course_title, total_views -FROM lesson_popularity_summary +FROM dsolovianov.lesson_popularity_summary ORDER BY total_views DESC LIMIT 5; + -- Неактивные пользователи, записавшиеся на курсы SELECT name, email, registered_courses_count -FROM inactive_users_summary +FROM dsolovianov.inactive_users_summary WHERE registered_courses_count > 0 ORDER BY registration_date DESC; @@ -24,7 +28,7 @@ ORDER BY registration_date DESC; SELECT course_title, AVG(completion_rate) AS avg_completion -FROM course_completion_rate +FROM dsolovianov.course_completion_rate GROUP BY course_title ORDER BY avg_completion DESC LIMIT 5; From 4828bd4afbb78b994fb8704d40eb943cdd970622 Mon Sep 17 00:00:00 2001 From: dmitry solovianov Date: Mon, 18 May 2026 19:57:59 +0400 Subject: [PATCH 150/179] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20dsolovianov/clickhouse/Pet-Project/test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsolovianov/clickhouse/Pet-Project/test | 0 .../Этап 4. Тестирование и валидация.sql | 13 ------------- 2 files changed, 13 deletions(-) create mode 100644 dsolovianov/clickhouse/Pet-Project/test delete mode 100644 dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql diff --git a/dsolovianov/clickhouse/Pet-Project/test b/dsolovianov/clickhouse/Pet-Project/test new file mode 100644 index 0000000..e69de29 diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql b/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql deleted file mode 100644 index f053160..0000000 --- a/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Этап 4. Тестирование и валидация -- - -/* -### **Задание 6: Сравни данные с Greenplum** - -1. Выбери 2–3 значения из таблиц `_summary_source` в Greenplum -2. Сравни с ClickHouse-результатами: - - Совпадает ли `SUM`, `AVG`, `MIN`, `MAX` - - Проверяй `NULL`, `0`, округления -3. Напиши мини-отчёт: **"где совпадает, где нет, почему"** -*/ - --- Ничего не понятно по этому этапу. У меня в GP нет таблиц "_summary_source" From e8cd8858fc5a124d3b50e2a2223f67863b14d767 Mon Sep 17 00:00:00 2001 From: Bonifatsiy Date: Mon, 18 May 2026 19:58:38 +0400 Subject: [PATCH 151/179] skip --- .../Этап 4. Тестирование и валидация.sql | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql b/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql deleted file mode 100644 index f053160..0000000 --- a/dsolovianov/clickhouse/Pet-Project/Этап 4. Тестирование и валидация.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Этап 4. Тестирование и валидация -- - -/* -### **Задание 6: Сравни данные с Greenplum** - -1. Выбери 2–3 значения из таблиц `_summary_source` в Greenplum -2. Сравни с ClickHouse-результатами: - - Совпадает ли `SUM`, `AVG`, `MIN`, `MAX` - - Проверяй `NULL`, `0`, округления -3. Напиши мини-отчёт: **"где совпадает, где нет, почему"** -*/ - --- Ничего не понятно по этому этапу. У меня в GP нет таблиц "_summary_source" From 5949db1db8b9daac0ec80385c14ecf0141c9f8e6 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 19 May 2026 09:45:42 +0300 Subject: [PATCH 152/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'=D0=A1=D0=B6=D0=B0=D1=82=D0=B8=D0=B5'=20-=20Cli?= =?UTF-8?q?ckhouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../3. Сжатие/practice_compression.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql new file mode 100644 index 0000000..f8758f4 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql @@ -0,0 +1,27 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + level String, + message String CODEC(ZSTD(3)), + created_at DateTime CODEC(Delta, ZSTD) +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY created_at; + +SELECT + column, + compression_codec +FROM system.columns +WHERE table = 'logs'; +-- если делать с codec_expression, то ошибка вылазит мол такой колонки нет +-- Выглядит вот так: +-- id +-- level +-- message CODEC(ZSTD(3)) +-- created_at CODEC(Delta(4), ZSTD(1)) \ No newline at end of file From b6e36cafda5a8ecc355c18552858b48209b2f1f5 Mon Sep 17 00:00:00 2001 From: dmezentsev Date: Tue, 19 May 2026 10:39:11 +0300 Subject: [PATCH 153/179] fix some mistakes --- dmezentsev/postgresql/homework/DDL_DML.sql | 16 ++++++++-------- dmezentsev/postgresql/homework/Functions.sql | 4 ++-- dmezentsev/postgresql/homework/GROUP_BY.sql | 16 ++++++++-------- dmezentsev/postgresql/homework/JOIN.sql | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/dmezentsev/postgresql/homework/DDL_DML.sql b/dmezentsev/postgresql/homework/DDL_DML.sql index 3cb0128..9780290 100644 --- a/dmezentsev/postgresql/homework/DDL_DML.sql +++ b/dmezentsev/postgresql/homework/DDL_DML.sql @@ -1,4 +1,4 @@ -CREATE TABLE users ( +CREATE TABLE dmezentsev.users ( id BIGINT PRIMARY KEY, /* сделал первичные ключи в каждой таблице BIGINT, кол-во записей может быть большим */ name VARCHAR(100) NOT NULL, @@ -9,7 +9,7 @@ is_active BOOLEAN, created_at TIMESTAMPTZ NOT NULL ); -CREATE TABLE products ( +CREATE TABLE dmezentsev.products ( id BIGINT PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50), @@ -17,7 +17,7 @@ price DECIMAL(10,2) NOT NULL, -- Сделал тип данных для ден in_stock BOOLEAN ); -CREATE TABLE orders ( +CREATE TABLE dmezentsev.orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, order_date TIMESTAMPTZ NOT NULL, @@ -26,7 +26,7 @@ status VARCHAR(20), -- Читаемый статус нагляднее, не н FOREIGN KEY (user_id) REFERENCES users(id) ); -CREATE TABLE order_items ( +CREATE TABLE dmezentsev.order_items ( order_id BIGINT, product_id BIGINT, quantity INT NOT NULL, /* Предположил, что типа данных SMALL INT возможно не @@ -38,7 +38,7 @@ FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (product_id) REFERENCES products(id) ); -INSERT INTO users (id, name, age, email, country, is_active, created_at) +INSERT INTO dmezentsev.users (id, name, age, email, country, is_active, created_at) VALUES (1, 'Alice', 25, 'alice@example.com', 'US', TRUE, '2024-01-10 10:00:00'), (2, 'Bob', 30, 'bob@example.com', 'UK', TRUE, '2024-02-05 09:30:00'), @@ -51,7 +51,7 @@ VALUES (9, 'Ivan', 26, 'ivan@example.com', 'RU', TRUE, '2024-01-11 14:30:00'), (10, 'Julia', 28, 'julia@example.com', 'US', TRUE, '2024-03-07 18:05:00'); -INSERT INTO products (id, name, category, price, in_stock) +INSERT INTO dmezentsev.products (id, name, category, price, in_stock) VALUES (1, 'iPhone 15', 'Электроника', 80000.00, TRUE), (2, 'Kindle', 'Электроника', 15000.00, TRUE), @@ -61,7 +61,7 @@ VALUES (6, 'Фитнес-браслет', 'Гаджеты', 6000.00, TRUE), (7, 'Подарочная карта', 'Сервисы', 1000.00, TRUE); -INSERT INTO orders (id, user_id, order_date, status) +INSERT INTO dmezentsev.orders (id, user_id, order_date, status) VALUES (1, 1, '2025-01-10 12:00:00', 'paid'), (2, 3, '2025-01-12 09:30:00', 'cancelled'), @@ -72,7 +72,7 @@ VALUES (7, 7, '2025-03-01 09:00:00', 'paid'), (8, 9, '2025-03-05 19:30:00', 'paid'); -INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) +INSERT INTO dmezentsev.order_items (order_id, product_id, quantity, price_at_purchase) VALUES (1, 1, 1, 78000.00), (1, 3, 2, 8500.00), diff --git a/dmezentsev/postgresql/homework/Functions.sql b/dmezentsev/postgresql/homework/Functions.sql index 4fb08ef..2e54c35 100644 --- a/dmezentsev/postgresql/homework/Functions.sql +++ b/dmezentsev/postgresql/homework/Functions.sql @@ -6,7 +6,7 @@ SELECT name, is_active FROM dmezentsev.users WHERE CURRENT_DATE - created_at::DATE > 180 -ORDER BY CURRENT_DATE - created_at::DATE DESC; +ORDER BY days_since_registration DESC; /* - Как бы ты использовал такое поле в реальном продукте (что можно делать с очень «старыми» пользователями)? @@ -27,7 +27,7 @@ SELECT name, SUBSTRING(LOWER(email) FROM POSITION('@' IN email) + 1) AS domain FROM dmezentsev.users WHERE email IS NOT NULL -ORDER BY SUBSTRING(email FROM POSITION('@' IN email) + 1), name; +ORDER BY domain, name; /* - Зачем в реальных системах приводят email к нижнему регистру и выделяют домен отдельно? diff --git a/dmezentsev/postgresql/homework/GROUP_BY.sql b/dmezentsev/postgresql/homework/GROUP_BY.sql index 5d641a4..4e33a2b 100644 --- a/dmezentsev/postgresql/homework/GROUP_BY.sql +++ b/dmezentsev/postgresql/homework/GROUP_BY.sql @@ -4,7 +4,7 @@ SELECT category, count(id) AS products_count FROM dmezentsev.products GROUP BY category -ORDER BY count(id) DESC; +ORDER BY products_count DESC; /* - Что будет, если убрать ORDER BY — как изменится чтение результата для продукта/аналитика? @@ -26,7 +26,7 @@ SELECT order_id, SUM(quantity * price_at_purchase) AS order_revenue FROM dmezentsev.order_items GROUP BY order_id -ORDER BY SUM(quantity * price_at_purchase) DESC; +ORDER BY order_revenue DESC; /* - Что будет, если убрать GROUP BY и оставить SUM(quantity * price_at_purchase)? @@ -41,7 +41,7 @@ SELECT p.category, FROM dmezentsev.products p JOIN dmezentsev.order_items oi ON p.id = oi.product_id GROUP BY p.category -ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC; +ORDER BY category_revenue DESC; /* - Почему группировка идёт по p.category, а не по p.id? @@ -62,7 +62,7 @@ FROM dmezentsev.orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.status = 'paid' GROUP BY o.id -ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC; +ORDER BY order_revenue DESC; /* - Чем результат этой задачи будет отличаться от задачи 2 (где статус не учитывался)? @@ -82,7 +82,7 @@ FROM dmezentsev.products p JOIN dmezentsev.order_items oi ON p.id = oi.product_id GROUP BY p.category HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000 -ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC; +ORDER BY category_revenue DESC; /* - Почему нельзя написать WHERE category_revenue > 1000? @@ -104,7 +104,7 @@ FROM dmezentsev.users WHERE is_active = TRUE GROUP BY country HAVING COUNT(id) >= 2 -ORDER BY COUNT(id) DESC; +ORDER BY active_users_count DESC; /* - Почему фильтр по активности (is_active = true) логичнее писать в WHERE, а не в HAVING? @@ -124,7 +124,7 @@ SELECT name AS product_name, FROM dmezentsev.products p JOIN dmezentsev.order_items oi ON p.id = oi.product_id GROUP BY name -ORDER BY SUM(quantity) DESC +ORDER BY total_quantity_sold DESC LIMIT 3; /* - Чем рейтинг по total_quantity_sold отличается от @@ -145,7 +145,7 @@ SELECT order_id, FROM dmezentsev.order_items GROUP BY order_id HAVING SUM(quantity * price_at_purchase) < 100 -ORDER BY SUM(quantity * price_at_purchase); +ORDER BY order_revenue; /* - Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? diff --git a/dmezentsev/postgresql/homework/JOIN.sql b/dmezentsev/postgresql/homework/JOIN.sql index 28f146b..5cf57cc 100644 --- a/dmezentsev/postgresql/homework/JOIN.sql +++ b/dmezentsev/postgresql/homework/JOIN.sql @@ -59,7 +59,7 @@ LEFT JOIN dmezentsev.products p ON oi.product_id = p.id AND oi.order_id = 101 SELECT o.id AS order_id, u.name AS user_name, - p.name, + p.name AS product_name, oi.quantity, oi.price_at_purchase FROM dmezentsev.orders o @@ -181,7 +181,7 @@ FROM dmezentsev.orders o JOIN dmezentsev.users u ON o.user_id = u.id JOIN dmezentsev.order_items oi ON o.id = oi.order_id JOIN dmezentsev.products p ON oi.product_id = p.id -WHERE u."name" = 'Alice'; +WHERE u.name = 'Alice'; /* - Если у Alice нет ни одного заказа, сколько строк вернёт твой запрос и почему? From 84006e9b80a699d4e1b21591f98b0ea07ef2b450 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 19 May 2026 12:04:41 +0300 Subject: [PATCH 154/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=201:=20=D0=97=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=B8=D1=82=D1=8C=20CSV-=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql new file mode 100644 index 0000000..f2045ef --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql @@ -0,0 +1,61 @@ +--DROP TABLE IF EXISTS mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_user +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + name String, + age UInt8, + email String, + registration_date DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +--DROP TABLE IF EXISTS mnoskov.core_dim_course ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_course +ON CLUSTER clickhouse_cluster +( + course_id UInt64, + title String, + category LowCardinality(String), + created_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +--DROP TABLE IF EXISTS mnoskov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_lesson +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + title String, + duration_min UInt16, + course_id UInt64 +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_lesson', + '{replica}' +) +ORDER BY (lesson_id, course_id); + +--DROP TABLE IF EXISTS mnoskov.core_dim_date ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_date +ON CLUSTER clickhouse_cluster +( + date Date, + year Int16, + month Int8, + day Int8, + weekday LowCardinality(String) +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_date', + '{replica}' +) +ORDER BY date; \ No newline at end of file From a83112d1550cd3d3b5cbd05d0e262e362f2f1b54 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 19 May 2026 13:19:32 +0300 Subject: [PATCH 155/179] =?UTF-8?q?=D0=94=D0=9E=D0=9F=D0=9E=D0=9B=D0=9D?= =?UTF-8?q?=D0=98=D0=9B=20-=20=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=201:=20=D0=97=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=B8=D1=82=D1=8C=20CSV-=D1=84=D0=B0=D0=B9=D0=BB=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../csv files/dim_course_202605191311.csv | 6 + .../csv files/dim_date_202605191312.csv | 2923 +++++++++++++++++ .../csv files/dim_lesson_202605191312.csv | 19 + .../csv files/dim_user_202605191313.csv | 21 + .../csv files/enrollments_202605191313.csv | 46 + .../csv files/fact_lesson_views_202605191313.csv | 59 + .../Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql | 38 +- 7 files changed, 3111 insertions(+), 1 deletion(-) create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv new file mode 100644 index 0000000..0bae8f3 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv @@ -0,0 +1,6 @@ +"course_id","title","category","created_at" +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +0300 +3,BI с нуля,business,2023-03-01 00:00:00.000 +0300 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 +0300 +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +0300 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv new file mode 100644 index 0000000..319c0ad --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv @@ -0,0 +1,2923 @@ +"date","year","month","day","weekday" +2023-01-01,2023,1,1,sunday +2023-01-03,2023,1,3,tuesday +2023-01-05,2023,1,5,thursday +2023-01-09,2023,1,9,monday +2023-01-11,2023,1,11,wednesday +2023-01-13,2023,1,13,friday +2023-01-15,2023,1,15,sunday +2023-01-17,2023,1,17,tuesday +2023-01-21,2023,1,21,saturday +2023-01-23,2023,1,23,monday +2023-01-24,2023,1,24,tuesday +2023-01-26,2023,1,26,thursday +2023-01-27,2023,1,27,friday +2023-01-28,2023,1,28,saturday +2023-01-31,2023,1,31,tuesday +2023-02-02,2023,2,2,thursday +2023-02-06,2023,2,6,monday +2023-02-08,2023,2,8,wednesday +2023-02-10,2023,2,10,friday +2023-02-14,2023,2,14,tuesday +2023-02-18,2023,2,18,saturday +2023-02-21,2023,2,21,tuesday +2023-02-23,2023,2,23,thursday +2023-02-24,2023,2,24,friday +2023-02-25,2023,2,25,saturday +2023-02-26,2023,2,26,sunday +2023-02-27,2023,2,27,monday +2023-03-01,2023,3,1,wednesday +2023-03-05,2023,3,5,sunday +2023-03-07,2023,3,7,tuesday +2023-03-11,2023,3,11,saturday +2023-03-12,2023,3,12,sunday +2023-03-16,2023,3,16,thursday +2023-03-18,2023,3,18,saturday +2023-03-20,2023,3,20,monday +2023-03-21,2023,3,21,tuesday +2023-03-23,2023,3,23,thursday +2023-03-24,2023,3,24,friday +2023-03-28,2023,3,28,tuesday +2023-03-30,2023,3,30,thursday +2023-04-02,2023,4,2,sunday +2023-04-04,2023,4,4,tuesday +2023-04-05,2023,4,5,wednesday +2023-04-09,2023,4,9,sunday +2023-04-10,2023,4,10,monday +2023-04-11,2023,4,11,tuesday +2023-04-12,2023,4,12,wednesday +2023-04-16,2023,4,16,sunday +2023-04-18,2023,4,18,tuesday +2023-04-20,2023,4,20,thursday +2023-04-21,2023,4,21,friday +2023-04-22,2023,4,22,saturday +2023-04-25,2023,4,25,tuesday +2023-04-27,2023,4,27,thursday +2023-04-29,2023,4,29,saturday +2023-05-02,2023,5,2,tuesday +2023-05-06,2023,5,6,saturday +2023-05-09,2023,5,9,tuesday +2023-05-11,2023,5,11,thursday +2023-05-12,2023,5,12,friday +2023-05-14,2023,5,14,sunday +2023-05-15,2023,5,15,monday +2023-05-17,2023,5,17,wednesday +2023-05-19,2023,5,19,friday +2023-05-20,2023,5,20,saturday +2023-05-22,2023,5,22,monday +2023-05-23,2023,5,23,tuesday +2023-05-24,2023,5,24,wednesday +2023-05-25,2023,5,25,thursday +2023-05-26,2023,5,26,friday +2023-05-29,2023,5,29,monday +2023-06-01,2023,6,1,thursday +2023-06-02,2023,6,2,friday +2023-06-08,2023,6,8,thursday +2023-06-10,2023,6,10,saturday +2023-06-12,2023,6,12,monday +2023-06-14,2023,6,14,wednesday +2023-06-21,2023,6,21,wednesday +2023-06-24,2023,6,24,saturday +2023-06-25,2023,6,25,sunday +2023-06-26,2023,6,26,monday +2023-06-27,2023,6,27,tuesday +2023-06-29,2023,6,29,thursday +2023-06-30,2023,6,30,friday +2023-07-03,2023,7,3,monday +2023-07-06,2023,7,6,thursday +2023-07-08,2023,7,8,saturday +2023-07-10,2023,7,10,monday +2023-07-12,2023,7,12,wednesday +2023-07-14,2023,7,14,friday +2023-07-15,2023,7,15,saturday +2023-07-16,2023,7,16,sunday +2023-07-17,2023,7,17,monday +2023-07-20,2023,7,20,thursday +2023-07-21,2023,7,21,friday +2023-07-23,2023,7,23,sunday +2023-07-24,2023,7,24,monday +2023-07-25,2023,7,25,tuesday +2023-07-30,2023,7,30,sunday +2023-07-31,2023,7,31,monday +2023-08-01,2023,8,1,tuesday +2023-08-04,2023,8,4,friday +2023-08-08,2023,8,8,tuesday +2023-08-09,2023,8,9,wednesday +2023-08-10,2023,8,10,thursday +2023-08-12,2023,8,12,saturday +2023-08-13,2023,8,13,sunday +2023-08-21,2023,8,21,monday +2023-08-26,2023,8,26,saturday +2023-08-27,2023,8,27,sunday +2023-08-28,2023,8,28,monday +2023-08-29,2023,8,29,tuesday +2023-08-30,2023,8,30,wednesday +2023-09-01,2023,9,1,friday +2023-09-02,2023,9,2,saturday +2023-09-03,2023,9,3,sunday +2023-09-06,2023,9,6,wednesday +2023-09-08,2023,9,8,friday +2023-09-09,2023,9,9,saturday +2023-09-11,2023,9,11,monday +2023-09-13,2023,9,13,wednesday +2023-09-14,2023,9,14,thursday +2023-09-15,2023,9,15,friday +2023-09-17,2023,9,17,sunday +2023-09-19,2023,9,19,tuesday +2023-09-22,2023,9,22,friday +2023-09-23,2023,9,23,saturday +2023-09-25,2023,9,25,monday +2023-09-26,2023,9,26,tuesday +2023-09-27,2023,9,27,wednesday +2023-09-28,2023,9,28,thursday +2023-09-30,2023,9,30,saturday +2023-10-03,2023,10,3,tuesday +2023-10-11,2023,10,11,wednesday +2023-10-12,2023,10,12,thursday +2023-10-14,2023,10,14,saturday +2023-10-15,2023,10,15,sunday +2023-10-16,2023,10,16,monday +2023-10-18,2023,10,18,wednesday +2023-10-19,2023,10,19,thursday +2023-10-20,2023,10,20,friday +2023-10-21,2023,10,21,saturday +2023-10-22,2023,10,22,sunday +2023-10-23,2023,10,23,monday +2023-10-27,2023,10,27,friday +2023-10-28,2023,10,28,saturday +2023-10-31,2023,10,31,tuesday +2023-11-01,2023,11,1,wednesday +2023-11-02,2023,11,2,thursday +2023-11-05,2023,11,5,sunday +2023-11-06,2023,11,6,monday +2023-11-08,2023,11,8,wednesday +2023-11-10,2023,11,10,friday +2023-11-11,2023,11,11,saturday +2023-11-14,2023,11,14,tuesday +2023-11-15,2023,11,15,wednesday +2023-11-17,2023,11,17,friday +2023-11-19,2023,11,19,sunday +2023-11-21,2023,11,21,tuesday +2023-11-22,2023,11,22,wednesday +2023-11-24,2023,11,24,friday +2023-11-28,2023,11,28,tuesday +2023-11-30,2023,11,30,thursday +2023-12-01,2023,12,1,friday +2023-12-02,2023,12,2,saturday +2023-12-03,2023,12,3,sunday +2023-12-04,2023,12,4,monday +2023-12-07,2023,12,7,thursday +2023-12-12,2023,12,12,tuesday +2023-12-14,2023,12,14,thursday +2023-12-15,2023,12,15,friday +2023-12-17,2023,12,17,sunday +2023-12-18,2023,12,18,monday +2023-12-19,2023,12,19,tuesday +2023-12-21,2023,12,21,thursday +2023-12-22,2023,12,22,friday +2023-12-23,2023,12,23,saturday +2023-12-27,2023,12,27,wednesday +2023-12-31,2023,12,31,sunday +2024-01-02,2024,1,2,tuesday +2024-01-03,2024,1,3,wednesday +2024-01-06,2024,1,6,saturday +2024-01-07,2024,1,7,sunday +2024-01-08,2024,1,8,monday +2024-01-13,2024,1,13,saturday +2024-01-15,2024,1,15,monday +2024-01-18,2024,1,18,thursday +2024-01-19,2024,1,19,friday +2024-01-20,2024,1,20,saturday +2024-01-21,2024,1,21,sunday +2024-01-22,2024,1,22,monday +2024-01-24,2024,1,24,wednesday +2024-01-25,2024,1,25,thursday +2024-01-26,2024,1,26,friday +2024-01-27,2024,1,27,saturday +2024-01-28,2024,1,28,sunday +2024-01-31,2024,1,31,wednesday +2024-02-02,2024,2,2,friday +2024-02-04,2024,2,4,sunday +2024-02-06,2024,2,6,tuesday +2024-02-07,2024,2,7,wednesday +2024-02-11,2024,2,11,sunday +2024-02-12,2024,2,12,monday +2024-02-17,2024,2,17,saturday +2024-02-18,2024,2,18,sunday +2024-02-21,2024,2,21,wednesday +2024-02-22,2024,2,22,thursday +2024-02-25,2024,2,25,sunday +2024-02-26,2024,2,26,monday +2024-02-28,2024,2,28,wednesday +2024-03-01,2024,3,1,friday +2024-03-02,2024,3,2,saturday +2024-03-06,2024,3,6,wednesday +2023-01-02,2023,1,2,monday +2023-01-04,2023,1,4,wednesday +2023-01-06,2023,1,6,friday +2023-01-07,2023,1,7,saturday +2023-01-08,2023,1,8,sunday +2023-01-10,2023,1,10,tuesday +2023-01-12,2023,1,12,thursday +2023-01-14,2023,1,14,saturday +2023-01-16,2023,1,16,monday +2023-01-18,2023,1,18,wednesday +2023-01-19,2023,1,19,thursday +2023-01-20,2023,1,20,friday +2023-01-22,2023,1,22,sunday +2023-01-25,2023,1,25,wednesday +2023-01-29,2023,1,29,sunday +2023-01-30,2023,1,30,monday +2023-02-01,2023,2,1,wednesday +2023-02-03,2023,2,3,friday +2023-02-04,2023,2,4,saturday +2023-02-05,2023,2,5,sunday +2023-02-07,2023,2,7,tuesday +2023-02-09,2023,2,9,thursday +2023-02-11,2023,2,11,saturday +2023-02-12,2023,2,12,sunday +2023-02-13,2023,2,13,monday +2023-02-15,2023,2,15,wednesday +2023-02-16,2023,2,16,thursday +2023-02-17,2023,2,17,friday +2023-02-19,2023,2,19,sunday +2023-02-20,2023,2,20,monday +2023-02-22,2023,2,22,wednesday +2023-02-28,2023,2,28,tuesday +2023-03-02,2023,3,2,thursday +2023-03-03,2023,3,3,friday +2023-03-04,2023,3,4,saturday +2023-03-06,2023,3,6,monday +2023-03-08,2023,3,8,wednesday +2023-03-09,2023,3,9,thursday +2023-03-10,2023,3,10,friday +2023-03-13,2023,3,13,monday +2023-03-14,2023,3,14,tuesday +2023-03-15,2023,3,15,wednesday +2023-03-17,2023,3,17,friday +2023-03-19,2023,3,19,sunday +2023-03-22,2023,3,22,wednesday +2023-03-25,2023,3,25,saturday +2023-03-26,2023,3,26,sunday +2023-03-27,2023,3,27,monday +2023-03-29,2023,3,29,wednesday +2023-03-31,2023,3,31,friday +2023-04-01,2023,4,1,saturday +2023-04-03,2023,4,3,monday +2023-04-06,2023,4,6,thursday +2023-04-07,2023,4,7,friday +2023-04-08,2023,4,8,saturday +2023-04-13,2023,4,13,thursday +2023-04-14,2023,4,14,friday +2023-04-15,2023,4,15,saturday +2023-04-17,2023,4,17,monday +2023-04-19,2023,4,19,wednesday +2023-04-23,2023,4,23,sunday +2023-04-24,2023,4,24,monday +2023-04-26,2023,4,26,wednesday +2023-04-28,2023,4,28,friday +2023-04-30,2023,4,30,sunday +2023-05-01,2023,5,1,monday +2023-05-03,2023,5,3,wednesday +2023-05-04,2023,5,4,thursday +2023-05-05,2023,5,5,friday +2023-05-07,2023,5,7,sunday +2023-05-08,2023,5,8,monday +2023-05-10,2023,5,10,wednesday +2023-05-13,2023,5,13,saturday +2023-05-16,2023,5,16,tuesday +2023-05-18,2023,5,18,thursday +2023-05-21,2023,5,21,sunday +2023-05-27,2023,5,27,saturday +2023-05-28,2023,5,28,sunday +2023-05-30,2023,5,30,tuesday +2023-05-31,2023,5,31,wednesday +2023-06-03,2023,6,3,saturday +2023-06-04,2023,6,4,sunday +2023-06-05,2023,6,5,monday +2023-06-06,2023,6,6,tuesday +2023-06-07,2023,6,7,wednesday +2023-06-09,2023,6,9,friday +2023-06-11,2023,6,11,sunday +2023-06-13,2023,6,13,tuesday +2023-06-15,2023,6,15,thursday +2023-06-16,2023,6,16,friday +2023-06-17,2023,6,17,saturday +2023-06-18,2023,6,18,sunday +2023-06-19,2023,6,19,monday +2023-06-20,2023,6,20,tuesday +2023-06-22,2023,6,22,thursday +2023-06-23,2023,6,23,friday +2023-06-28,2023,6,28,wednesday +2023-07-01,2023,7,1,saturday +2023-07-02,2023,7,2,sunday +2023-07-04,2023,7,4,tuesday +2023-07-05,2023,7,5,wednesday +2023-07-07,2023,7,7,friday +2023-07-09,2023,7,9,sunday +2023-07-11,2023,7,11,tuesday +2023-07-13,2023,7,13,thursday +2023-07-18,2023,7,18,tuesday +2023-07-19,2023,7,19,wednesday +2023-07-22,2023,7,22,saturday +2023-07-26,2023,7,26,wednesday +2023-07-27,2023,7,27,thursday +2023-07-28,2023,7,28,friday +2023-07-29,2023,7,29,saturday +2023-08-02,2023,8,2,wednesday +2023-08-03,2023,8,3,thursday +2023-08-05,2023,8,5,saturday +2023-08-06,2023,8,6,sunday +2023-08-07,2023,8,7,monday +2023-08-11,2023,8,11,friday +2023-08-14,2023,8,14,monday +2023-08-15,2023,8,15,tuesday +2023-08-16,2023,8,16,wednesday +2023-08-17,2023,8,17,thursday +2023-08-18,2023,8,18,friday +2023-08-19,2023,8,19,saturday +2023-08-20,2023,8,20,sunday +2023-08-22,2023,8,22,tuesday +2023-08-23,2023,8,23,wednesday +2023-08-24,2023,8,24,thursday +2023-08-25,2023,8,25,friday +2023-08-31,2023,8,31,thursday +2023-09-04,2023,9,4,monday +2023-09-05,2023,9,5,tuesday +2023-09-07,2023,9,7,thursday +2023-09-10,2023,9,10,sunday +2023-09-12,2023,9,12,tuesday +2023-09-16,2023,9,16,saturday +2023-09-18,2023,9,18,monday +2023-09-20,2023,9,20,wednesday +2023-09-21,2023,9,21,thursday +2023-09-24,2023,9,24,sunday +2023-09-29,2023,9,29,friday +2023-10-01,2023,10,1,sunday +2023-10-02,2023,10,2,monday +2023-10-04,2023,10,4,wednesday +2023-10-05,2023,10,5,thursday +2023-10-06,2023,10,6,friday +2023-10-07,2023,10,7,saturday +2023-10-08,2023,10,8,sunday +2023-10-09,2023,10,9,monday +2023-10-10,2023,10,10,tuesday +2023-10-13,2023,10,13,friday +2023-10-17,2023,10,17,tuesday +2023-10-24,2023,10,24,tuesday +2023-10-25,2023,10,25,wednesday +2023-10-26,2023,10,26,thursday +2023-10-29,2023,10,29,sunday +2023-10-30,2023,10,30,monday +2023-11-03,2023,11,3,friday +2023-11-04,2023,11,4,saturday +2023-11-07,2023,11,7,tuesday +2023-11-09,2023,11,9,thursday +2023-11-12,2023,11,12,sunday +2023-11-13,2023,11,13,monday +2023-11-16,2023,11,16,thursday +2023-11-18,2023,11,18,saturday +2023-11-20,2023,11,20,monday +2023-11-23,2023,11,23,thursday +2023-11-25,2023,11,25,saturday +2023-11-26,2023,11,26,sunday +2023-11-27,2023,11,27,monday +2023-11-29,2023,11,29,wednesday +2023-12-05,2023,12,5,tuesday +2023-12-06,2023,12,6,wednesday +2023-12-08,2023,12,8,friday +2023-12-09,2023,12,9,saturday +2023-12-10,2023,12,10,sunday +2023-12-11,2023,12,11,monday +2023-12-13,2023,12,13,wednesday +2023-12-16,2023,12,16,saturday +2023-12-20,2023,12,20,wednesday +2023-12-24,2023,12,24,sunday +2023-12-25,2023,12,25,monday +2023-12-26,2023,12,26,tuesday +2023-12-28,2023,12,28,thursday +2023-12-29,2023,12,29,friday +2023-12-30,2023,12,30,saturday +2024-01-01,2024,1,1,monday +2024-01-04,2024,1,4,thursday +2024-01-05,2024,1,5,friday +2024-01-09,2024,1,9,tuesday +2024-01-10,2024,1,10,wednesday +2024-01-11,2024,1,11,thursday +2024-01-12,2024,1,12,friday +2024-01-14,2024,1,14,sunday +2024-01-16,2024,1,16,tuesday +2024-01-17,2024,1,17,wednesday +2024-01-23,2024,1,23,tuesday +2024-01-29,2024,1,29,monday +2024-01-30,2024,1,30,tuesday +2024-02-01,2024,2,1,thursday +2024-02-03,2024,2,3,saturday +2024-02-05,2024,2,5,monday +2024-02-08,2024,2,8,thursday +2024-02-09,2024,2,9,friday +2024-02-10,2024,2,10,saturday +2024-02-13,2024,2,13,tuesday +2024-02-14,2024,2,14,wednesday +2024-02-15,2024,2,15,thursday +2024-02-16,2024,2,16,friday +2024-02-19,2024,2,19,monday +2024-02-20,2024,2,20,tuesday +2024-02-23,2024,2,23,friday +2024-02-24,2024,2,24,saturday +2024-03-07,2024,3,7,thursday +2024-03-08,2024,3,8,friday +2024-03-09,2024,3,9,saturday +2024-03-10,2024,3,10,sunday +2024-03-11,2024,3,11,monday +2024-03-12,2024,3,12,tuesday +2024-03-13,2024,3,13,wednesday +2024-03-15,2024,3,15,friday +2024-03-17,2024,3,17,sunday +2024-03-18,2024,3,18,monday +2024-03-19,2024,3,19,tuesday +2024-03-22,2024,3,22,friday +2024-03-23,2024,3,23,saturday +2024-03-24,2024,3,24,sunday +2024-03-27,2024,3,27,wednesday +2024-03-28,2024,3,28,thursday +2024-03-29,2024,3,29,friday +2024-03-30,2024,3,30,saturday +2024-03-31,2024,3,31,sunday +2024-04-01,2024,4,1,monday +2024-04-02,2024,4,2,tuesday +2024-04-03,2024,4,3,wednesday +2024-04-04,2024,4,4,thursday +2024-04-06,2024,4,6,saturday +2024-04-07,2024,4,7,sunday +2024-04-09,2024,4,9,tuesday +2024-04-10,2024,4,10,wednesday +2024-04-13,2024,4,13,saturday +2024-04-16,2024,4,16,tuesday +2024-04-17,2024,4,17,wednesday +2024-04-18,2024,4,18,thursday +2024-04-19,2024,4,19,friday +2024-04-20,2024,4,20,saturday +2024-04-23,2024,4,23,tuesday +2024-04-27,2024,4,27,saturday +2024-04-28,2024,4,28,sunday +2024-04-29,2024,4,29,monday +2024-05-02,2024,5,2,thursday +2024-05-03,2024,5,3,friday +2024-05-04,2024,5,4,saturday +2024-05-05,2024,5,5,sunday +2024-05-07,2024,5,7,tuesday +2024-05-15,2024,5,15,wednesday +2024-05-16,2024,5,16,thursday +2024-05-17,2024,5,17,friday +2024-05-20,2024,5,20,monday +2024-05-21,2024,5,21,tuesday +2024-05-23,2024,5,23,thursday +2024-05-28,2024,5,28,tuesday +2024-05-29,2024,5,29,wednesday +2024-06-01,2024,6,1,saturday +2024-06-02,2024,6,2,sunday +2024-06-03,2024,6,3,monday +2024-06-05,2024,6,5,wednesday +2024-06-07,2024,6,7,friday +2024-06-10,2024,6,10,monday +2024-06-12,2024,6,12,wednesday +2024-06-14,2024,6,14,friday +2024-06-15,2024,6,15,saturday +2024-06-16,2024,6,16,sunday +2024-06-18,2024,6,18,tuesday +2024-06-19,2024,6,19,wednesday +2024-06-20,2024,6,20,thursday +2024-06-22,2024,6,22,saturday +2024-06-25,2024,6,25,tuesday +2024-06-26,2024,6,26,wednesday +2024-06-27,2024,6,27,thursday +2024-06-29,2024,6,29,saturday +2024-06-30,2024,6,30,sunday +2024-07-01,2024,7,1,monday +2024-07-02,2024,7,2,tuesday +2024-07-04,2024,7,4,thursday +2024-07-06,2024,7,6,saturday +2024-07-08,2024,7,8,monday +2024-07-09,2024,7,9,tuesday +2024-07-13,2024,7,13,saturday +2024-07-14,2024,7,14,sunday +2024-07-15,2024,7,15,monday +2024-07-18,2024,7,18,thursday +2024-07-21,2024,7,21,sunday +2024-07-23,2024,7,23,tuesday +2024-07-25,2024,7,25,thursday +2024-07-26,2024,7,26,friday +2024-07-28,2024,7,28,sunday +2024-08-01,2024,8,1,thursday +2024-08-03,2024,8,3,saturday +2024-08-06,2024,8,6,tuesday +2024-08-09,2024,8,9,friday +2024-08-10,2024,8,10,saturday +2024-08-11,2024,8,11,sunday +2024-08-12,2024,8,12,monday +2024-08-14,2024,8,14,wednesday +2024-08-20,2024,8,20,tuesday +2024-08-22,2024,8,22,thursday +2024-08-24,2024,8,24,saturday +2024-08-28,2024,8,28,wednesday +2024-08-29,2024,8,29,thursday +2024-08-30,2024,8,30,friday +2024-08-31,2024,8,31,saturday +2024-09-01,2024,9,1,sunday +2024-09-04,2024,9,4,wednesday +2024-09-07,2024,9,7,saturday +2024-09-10,2024,9,10,tuesday +2024-09-11,2024,9,11,wednesday +2024-09-13,2024,9,13,friday +2024-09-16,2024,9,16,monday +2024-09-19,2024,9,19,thursday +2024-09-20,2024,9,20,friday +2024-09-21,2024,9,21,saturday +2024-09-22,2024,9,22,sunday +2024-09-23,2024,9,23,monday +2024-09-24,2024,9,24,tuesday +2024-09-25,2024,9,25,wednesday +2024-09-26,2024,9,26,thursday +2024-09-28,2024,9,28,saturday +2024-09-30,2024,9,30,monday +2024-10-01,2024,10,1,tuesday +2024-10-03,2024,10,3,thursday +2024-10-04,2024,10,4,friday +2024-10-06,2024,10,6,sunday +2024-10-09,2024,10,9,wednesday +2024-10-11,2024,10,11,friday +2024-10-12,2024,10,12,saturday +2024-10-13,2024,10,13,sunday +2024-10-23,2024,10,23,wednesday +2024-10-24,2024,10,24,thursday +2024-10-26,2024,10,26,saturday +2024-10-27,2024,10,27,sunday +2024-10-29,2024,10,29,tuesday +2024-10-30,2024,10,30,wednesday +2024-10-31,2024,10,31,thursday +2024-11-01,2024,11,1,friday +2024-11-02,2024,11,2,saturday +2024-11-04,2024,11,4,monday +2024-11-05,2024,11,5,tuesday +2024-11-06,2024,11,6,wednesday +2024-11-07,2024,11,7,thursday +2024-11-08,2024,11,8,friday +2024-11-12,2024,11,12,tuesday +2024-11-13,2024,11,13,wednesday +2024-11-14,2024,11,14,thursday +2024-11-15,2024,11,15,friday +2024-11-16,2024,11,16,saturday +2024-11-19,2024,11,19,tuesday +2024-11-20,2024,11,20,wednesday +2024-11-21,2024,11,21,thursday +2024-11-22,2024,11,22,friday +2024-11-23,2024,11,23,saturday +2024-11-26,2024,11,26,tuesday +2024-11-29,2024,11,29,friday +2024-12-04,2024,12,4,wednesday +2024-12-07,2024,12,7,saturday +2024-12-12,2024,12,12,thursday +2024-12-15,2024,12,15,sunday +2024-12-21,2024,12,21,saturday +2024-12-22,2024,12,22,sunday +2024-12-25,2024,12,25,wednesday +2024-12-27,2024,12,27,friday +2024-12-28,2024,12,28,saturday +2024-12-30,2024,12,30,monday +2025-01-03,2025,1,3,friday +2025-01-04,2025,1,4,saturday +2025-01-07,2025,1,7,tuesday +2025-01-09,2025,1,9,thursday +2025-01-12,2025,1,12,sunday +2025-01-13,2025,1,13,monday +2025-01-16,2025,1,16,thursday +2025-01-17,2025,1,17,friday +2025-01-18,2025,1,18,saturday +2025-01-20,2025,1,20,monday +2025-01-23,2025,1,23,thursday +2025-01-24,2025,1,24,friday +2025-01-25,2025,1,25,saturday +2025-01-26,2025,1,26,sunday +2025-01-29,2025,1,29,wednesday +2025-01-31,2025,1,31,friday +2025-02-01,2025,2,1,saturday +2025-02-02,2025,2,2,sunday +2025-02-04,2025,2,4,tuesday +2025-02-06,2025,2,6,thursday +2025-02-07,2025,2,7,friday +2025-02-08,2025,2,8,saturday +2025-02-14,2025,2,14,friday +2025-02-15,2025,2,15,saturday +2025-02-16,2025,2,16,sunday +2025-02-17,2025,2,17,monday +2025-02-18,2025,2,18,tuesday +2025-02-20,2025,2,20,thursday +2025-02-22,2025,2,22,saturday +2025-02-23,2025,2,23,sunday +2025-02-26,2025,2,26,wednesday +2025-02-27,2025,2,27,thursday +2025-03-05,2025,3,5,wednesday +2025-03-06,2025,3,6,thursday +2025-03-07,2025,3,7,friday +2025-03-08,2025,3,8,saturday +2025-03-11,2025,3,11,tuesday +2025-03-12,2025,3,12,wednesday +2025-03-13,2025,3,13,thursday +2025-03-16,2025,3,16,sunday +2025-03-18,2025,3,18,tuesday +2025-03-21,2025,3,21,friday +2025-03-22,2025,3,22,saturday +2025-03-23,2025,3,23,sunday +2025-03-25,2025,3,25,tuesday +2025-03-26,2025,3,26,wednesday +2025-03-29,2025,3,29,saturday +2025-04-01,2025,4,1,tuesday +2025-04-02,2025,4,2,wednesday +2025-04-03,2025,4,3,thursday +2025-04-05,2025,4,5,saturday +2025-04-07,2025,4,7,monday +2025-04-12,2025,4,12,saturday +2024-02-27,2024,2,27,tuesday +2024-02-29,2024,2,29,thursday +2024-03-03,2024,3,3,sunday +2024-03-04,2024,3,4,monday +2024-03-05,2024,3,5,tuesday +2024-03-14,2024,3,14,thursday +2024-03-16,2024,3,16,saturday +2024-03-20,2024,3,20,wednesday +2024-03-21,2024,3,21,thursday +2024-03-25,2024,3,25,monday +2024-03-26,2024,3,26,tuesday +2024-04-05,2024,4,5,friday +2024-04-08,2024,4,8,monday +2024-04-11,2024,4,11,thursday +2024-04-12,2024,4,12,friday +2024-04-14,2024,4,14,sunday +2024-04-15,2024,4,15,monday +2024-04-21,2024,4,21,sunday +2024-04-22,2024,4,22,monday +2024-04-24,2024,4,24,wednesday +2024-04-25,2024,4,25,thursday +2024-04-26,2024,4,26,friday +2024-04-30,2024,4,30,tuesday +2024-05-01,2024,5,1,wednesday +2024-05-06,2024,5,6,monday +2024-05-08,2024,5,8,wednesday +2024-05-09,2024,5,9,thursday +2024-05-10,2024,5,10,friday +2024-05-11,2024,5,11,saturday +2024-05-12,2024,5,12,sunday +2024-05-13,2024,5,13,monday +2024-05-14,2024,5,14,tuesday +2024-05-18,2024,5,18,saturday +2024-05-19,2024,5,19,sunday +2024-05-22,2024,5,22,wednesday +2024-05-24,2024,5,24,friday +2024-05-25,2024,5,25,saturday +2024-05-26,2024,5,26,sunday +2024-05-27,2024,5,27,monday +2024-05-30,2024,5,30,thursday +2024-05-31,2024,5,31,friday +2024-06-04,2024,6,4,tuesday +2024-06-06,2024,6,6,thursday +2024-06-08,2024,6,8,saturday +2024-06-09,2024,6,9,sunday +2024-06-11,2024,6,11,tuesday +2024-06-13,2024,6,13,thursday +2024-06-17,2024,6,17,monday +2024-06-21,2024,6,21,friday +2024-06-23,2024,6,23,sunday +2024-06-24,2024,6,24,monday +2024-06-28,2024,6,28,friday +2024-07-03,2024,7,3,wednesday +2024-07-05,2024,7,5,friday +2024-07-07,2024,7,7,sunday +2024-07-10,2024,7,10,wednesday +2024-07-11,2024,7,11,thursday +2024-07-12,2024,7,12,friday +2024-07-16,2024,7,16,tuesday +2024-07-17,2024,7,17,wednesday +2024-07-19,2024,7,19,friday +2024-07-20,2024,7,20,saturday +2024-07-22,2024,7,22,monday +2024-07-24,2024,7,24,wednesday +2024-07-27,2024,7,27,saturday +2024-07-29,2024,7,29,monday +2024-07-30,2024,7,30,tuesday +2024-07-31,2024,7,31,wednesday +2024-08-02,2024,8,2,friday +2024-08-04,2024,8,4,sunday +2024-08-05,2024,8,5,monday +2024-08-07,2024,8,7,wednesday +2024-08-08,2024,8,8,thursday +2024-08-13,2024,8,13,tuesday +2024-08-15,2024,8,15,thursday +2024-08-16,2024,8,16,friday +2024-08-17,2024,8,17,saturday +2024-08-18,2024,8,18,sunday +2024-08-19,2024,8,19,monday +2024-08-21,2024,8,21,wednesday +2024-08-23,2024,8,23,friday +2024-08-25,2024,8,25,sunday +2024-08-26,2024,8,26,monday +2024-08-27,2024,8,27,tuesday +2024-09-02,2024,9,2,monday +2024-09-03,2024,9,3,tuesday +2024-09-05,2024,9,5,thursday +2024-09-06,2024,9,6,friday +2024-09-08,2024,9,8,sunday +2024-09-09,2024,9,9,monday +2024-09-12,2024,9,12,thursday +2024-09-14,2024,9,14,saturday +2024-09-15,2024,9,15,sunday +2024-09-17,2024,9,17,tuesday +2024-09-18,2024,9,18,wednesday +2024-09-27,2024,9,27,friday +2024-09-29,2024,9,29,sunday +2024-10-02,2024,10,2,wednesday +2024-10-05,2024,10,5,saturday +2024-10-07,2024,10,7,monday +2024-10-08,2024,10,8,tuesday +2024-10-10,2024,10,10,thursday +2024-10-14,2024,10,14,monday +2024-10-15,2024,10,15,tuesday +2024-10-16,2024,10,16,wednesday +2024-10-17,2024,10,17,thursday +2024-10-18,2024,10,18,friday +2024-10-19,2024,10,19,saturday +2024-10-20,2024,10,20,sunday +2024-10-21,2024,10,21,monday +2024-10-22,2024,10,22,tuesday +2024-10-25,2024,10,25,friday +2024-10-28,2024,10,28,monday +2024-11-03,2024,11,3,sunday +2024-11-09,2024,11,9,saturday +2024-11-10,2024,11,10,sunday +2024-11-11,2024,11,11,monday +2024-11-17,2024,11,17,sunday +2024-11-18,2024,11,18,monday +2024-11-24,2024,11,24,sunday +2024-11-25,2024,11,25,monday +2024-11-27,2024,11,27,wednesday +2024-11-28,2024,11,28,thursday +2024-11-30,2024,11,30,saturday +2024-12-01,2024,12,1,sunday +2024-12-02,2024,12,2,monday +2024-12-03,2024,12,3,tuesday +2024-12-05,2024,12,5,thursday +2024-12-06,2024,12,6,friday +2024-12-08,2024,12,8,sunday +2024-12-09,2024,12,9,monday +2024-12-10,2024,12,10,tuesday +2024-12-11,2024,12,11,wednesday +2024-12-13,2024,12,13,friday +2024-12-14,2024,12,14,saturday +2024-12-16,2024,12,16,monday +2024-12-17,2024,12,17,tuesday +2024-12-18,2024,12,18,wednesday +2024-12-19,2024,12,19,thursday +2024-12-20,2024,12,20,friday +2024-12-23,2024,12,23,monday +2024-12-24,2024,12,24,tuesday +2024-12-26,2024,12,26,thursday +2024-12-29,2024,12,29,sunday +2024-12-31,2024,12,31,tuesday +2025-01-01,2025,1,1,wednesday +2025-01-02,2025,1,2,thursday +2025-01-05,2025,1,5,sunday +2025-01-06,2025,1,6,monday +2025-01-08,2025,1,8,wednesday +2025-01-10,2025,1,10,friday +2025-01-11,2025,1,11,saturday +2025-01-14,2025,1,14,tuesday +2025-01-15,2025,1,15,wednesday +2025-01-19,2025,1,19,sunday +2025-01-21,2025,1,21,tuesday +2025-01-22,2025,1,22,wednesday +2025-01-27,2025,1,27,monday +2025-01-28,2025,1,28,tuesday +2025-01-30,2025,1,30,thursday +2025-02-03,2025,2,3,monday +2025-02-05,2025,2,5,wednesday +2025-02-09,2025,2,9,sunday +2025-02-10,2025,2,10,monday +2025-02-11,2025,2,11,tuesday +2025-02-12,2025,2,12,wednesday +2025-02-13,2025,2,13,thursday +2025-02-19,2025,2,19,wednesday +2025-02-21,2025,2,21,friday +2025-02-24,2025,2,24,monday +2025-02-25,2025,2,25,tuesday +2025-02-28,2025,2,28,friday +2025-03-01,2025,3,1,saturday +2025-03-02,2025,3,2,sunday +2025-03-03,2025,3,3,monday +2025-03-04,2025,3,4,tuesday +2025-03-09,2025,3,9,sunday +2025-03-10,2025,3,10,monday +2025-03-14,2025,3,14,friday +2025-03-15,2025,3,15,saturday +2025-03-17,2025,3,17,monday +2025-03-19,2025,3,19,wednesday +2025-03-20,2025,3,20,thursday +2025-03-24,2025,3,24,monday +2025-03-27,2025,3,27,thursday +2025-03-28,2025,3,28,friday +2025-03-30,2025,3,30,sunday +2025-03-31,2025,3,31,monday +2025-04-04,2025,4,4,friday +2025-04-06,2025,4,6,sunday +2025-04-08,2025,4,8,tuesday +2025-04-09,2025,4,9,wednesday +2025-04-10,2025,4,10,thursday +2025-04-11,2025,4,11,friday +2025-04-13,2025,4,13,sunday +2025-04-14,2025,4,14,monday +2025-04-15,2025,4,15,tuesday +2025-04-16,2025,4,16,wednesday +2025-04-17,2025,4,17,thursday +2025-04-18,2025,4,18,friday +2025-04-24,2025,4,24,thursday +2025-04-25,2025,4,25,friday +2025-04-28,2025,4,28,monday +2025-04-29,2025,4,29,tuesday +2025-04-30,2025,4,30,wednesday +2025-05-01,2025,5,1,thursday +2025-05-05,2025,5,5,monday +2025-05-07,2025,5,7,wednesday +2025-05-08,2025,5,8,thursday +2025-05-09,2025,5,9,friday +2025-05-10,2025,5,10,saturday +2025-05-11,2025,5,11,sunday +2025-05-14,2025,5,14,wednesday +2025-04-19,2025,4,19,saturday +2025-04-20,2025,4,20,sunday +2025-04-21,2025,4,21,monday +2025-04-22,2025,4,22,tuesday +2025-04-23,2025,4,23,wednesday +2025-04-26,2025,4,26,saturday +2025-04-27,2025,4,27,sunday +2025-05-02,2025,5,2,friday +2025-05-03,2025,5,3,saturday +2025-05-04,2025,5,4,sunday +2025-05-06,2025,5,6,tuesday +2025-05-12,2025,5,12,monday +2025-05-13,2025,5,13,tuesday +2025-05-17,2025,5,17,saturday +2025-05-19,2025,5,19,monday +2025-05-21,2025,5,21,wednesday +2025-05-25,2025,5,25,sunday +2025-05-27,2025,5,27,tuesday +2025-05-30,2025,5,30,friday +2025-06-01,2025,6,1,sunday +2025-06-02,2025,6,2,monday +2025-06-03,2025,6,3,tuesday +2025-06-05,2025,6,5,thursday +2025-06-08,2025,6,8,sunday +2025-06-10,2025,6,10,tuesday +2025-06-11,2025,6,11,wednesday +2025-06-13,2025,6,13,friday +2025-06-14,2025,6,14,saturday +2025-06-17,2025,6,17,tuesday +2025-06-19,2025,6,19,thursday +2025-06-20,2025,6,20,friday +2025-06-22,2025,6,22,sunday +2025-06-23,2025,6,23,monday +2025-06-24,2025,6,24,tuesday +2025-06-28,2025,6,28,saturday +2025-06-29,2025,6,29,sunday +2025-06-30,2025,6,30,monday +2025-07-01,2025,7,1,tuesday +2025-07-02,2025,7,2,wednesday +2025-07-03,2025,7,3,thursday +2025-07-04,2025,7,4,friday +2025-07-05,2025,7,5,saturday +2025-07-09,2025,7,9,wednesday +2025-07-11,2025,7,11,friday +2025-07-12,2025,7,12,saturday +2025-07-13,2025,7,13,sunday +2025-07-14,2025,7,14,monday +2025-07-15,2025,7,15,tuesday +2025-07-17,2025,7,17,thursday +2025-07-19,2025,7,19,saturday +2025-07-20,2025,7,20,sunday +2025-07-22,2025,7,22,tuesday +2025-07-24,2025,7,24,thursday +2025-07-25,2025,7,25,friday +2025-07-28,2025,7,28,monday +2025-07-29,2025,7,29,tuesday +2025-07-30,2025,7,30,wednesday +2025-08-02,2025,8,2,saturday +2025-08-03,2025,8,3,sunday +2025-08-06,2025,8,6,wednesday +2025-08-07,2025,8,7,thursday +2025-08-08,2025,8,8,friday +2025-08-09,2025,8,9,saturday +2025-08-10,2025,8,10,sunday +2025-08-15,2025,8,15,friday +2025-08-22,2025,8,22,friday +2025-08-25,2025,8,25,monday +2025-08-27,2025,8,27,wednesday +2025-08-29,2025,8,29,friday +2025-08-30,2025,8,30,saturday +2025-09-02,2025,9,2,tuesday +2025-09-03,2025,9,3,wednesday +2025-09-04,2025,9,4,thursday +2025-09-06,2025,9,6,saturday +2025-09-11,2025,9,11,thursday +2025-09-14,2025,9,14,sunday +2025-09-15,2025,9,15,monday +2025-09-20,2025,9,20,saturday +2025-09-21,2025,9,21,sunday +2025-09-23,2025,9,23,tuesday +2025-09-30,2025,9,30,tuesday +2025-10-02,2025,10,2,thursday +2025-10-03,2025,10,3,friday +2025-10-04,2025,10,4,saturday +2025-10-06,2025,10,6,monday +2025-10-09,2025,10,9,thursday +2025-10-13,2025,10,13,monday +2025-10-14,2025,10,14,tuesday +2025-10-15,2025,10,15,wednesday +2025-10-19,2025,10,19,sunday +2025-10-20,2025,10,20,monday +2025-10-23,2025,10,23,thursday +2025-10-24,2025,10,24,friday +2025-10-25,2025,10,25,saturday +2025-10-30,2025,10,30,thursday +2025-11-01,2025,11,1,saturday +2025-11-02,2025,11,2,sunday +2025-11-04,2025,11,4,tuesday +2025-11-05,2025,11,5,wednesday +2025-11-06,2025,11,6,thursday +2025-11-08,2025,11,8,saturday +2025-11-12,2025,11,12,wednesday +2025-11-13,2025,11,13,thursday +2025-11-14,2025,11,14,friday +2025-11-16,2025,11,16,sunday +2025-11-17,2025,11,17,monday +2025-11-18,2025,11,18,tuesday +2025-11-21,2025,11,21,friday +2025-11-22,2025,11,22,saturday +2025-11-26,2025,11,26,wednesday +2025-11-27,2025,11,27,thursday +2025-11-30,2025,11,30,sunday +2025-12-01,2025,12,1,monday +2025-12-02,2025,12,2,tuesday +2025-12-04,2025,12,4,thursday +2025-12-08,2025,12,8,monday +2025-12-11,2025,12,11,thursday +2025-12-12,2025,12,12,friday +2025-12-13,2025,12,13,saturday +2025-12-18,2025,12,18,thursday +2025-12-21,2025,12,21,sunday +2025-12-22,2025,12,22,monday +2025-12-23,2025,12,23,tuesday +2025-12-25,2025,12,25,thursday +2025-12-27,2025,12,27,saturday +2025-12-28,2025,12,28,sunday +2025-12-29,2025,12,29,monday +2026-01-04,2026,1,4,sunday +2026-01-06,2026,1,6,tuesday +2026-01-08,2026,1,8,thursday +2026-01-10,2026,1,10,saturday +2026-01-11,2026,1,11,sunday +2026-01-14,2026,1,14,wednesday +2026-01-15,2026,1,15,thursday +2026-01-17,2026,1,17,saturday +2026-01-19,2026,1,19,monday +2026-01-20,2026,1,20,tuesday +2026-01-21,2026,1,21,wednesday +2026-01-23,2026,1,23,friday +2026-01-24,2026,1,24,saturday +2026-01-25,2026,1,25,sunday +2026-01-26,2026,1,26,monday +2026-01-28,2026,1,28,wednesday +2026-01-29,2026,1,29,thursday +2026-01-30,2026,1,30,friday +2026-01-31,2026,1,31,saturday +2026-02-03,2026,2,3,tuesday +2026-02-04,2026,2,4,wednesday +2026-02-05,2026,2,5,thursday +2026-02-06,2026,2,6,friday +2026-02-07,2026,2,7,saturday +2026-02-09,2026,2,9,monday +2026-02-10,2026,2,10,tuesday +2026-02-12,2026,2,12,thursday +2026-02-13,2026,2,13,friday +2026-02-14,2026,2,14,saturday +2026-02-15,2026,2,15,sunday +2026-02-18,2026,2,18,wednesday +2026-02-20,2026,2,20,friday +2026-02-22,2026,2,22,sunday +2026-02-23,2026,2,23,monday +2026-02-24,2026,2,24,tuesday +2026-02-25,2026,2,25,wednesday +2026-02-26,2026,2,26,thursday +2026-03-02,2026,3,2,monday +2026-03-03,2026,3,3,tuesday +2026-03-06,2026,3,6,friday +2026-03-08,2026,3,8,sunday +2026-03-09,2026,3,9,monday +2026-03-10,2026,3,10,tuesday +2026-03-11,2026,3,11,wednesday +2026-03-12,2026,3,12,thursday +2026-03-14,2026,3,14,saturday +2026-03-17,2026,3,17,tuesday +2026-03-20,2026,3,20,friday +2026-03-24,2026,3,24,tuesday +2026-03-27,2026,3,27,friday +2026-03-29,2026,3,29,sunday +2026-04-02,2026,4,2,thursday +2026-04-03,2026,4,3,friday +2026-04-04,2026,4,4,saturday +2026-04-06,2026,4,6,monday +2026-04-09,2026,4,9,thursday +2026-04-11,2026,4,11,saturday +2026-04-13,2026,4,13,monday +2026-04-14,2026,4,14,tuesday +2026-04-20,2026,4,20,monday +2026-04-21,2026,4,21,tuesday +2026-04-24,2026,4,24,friday +2026-04-25,2026,4,25,saturday +2026-04-26,2026,4,26,sunday +2026-04-28,2026,4,28,tuesday +2026-04-30,2026,4,30,thursday +2026-05-01,2026,5,1,friday +2026-05-02,2026,5,2,saturday +2026-05-05,2026,5,5,tuesday +2026-05-06,2026,5,6,wednesday +2026-05-07,2026,5,7,thursday +2026-05-09,2026,5,9,saturday +2026-05-10,2026,5,10,sunday +2026-05-11,2026,5,11,monday +2026-05-12,2026,5,12,tuesday +2026-05-13,2026,5,13,wednesday +2026-05-15,2026,5,15,friday +2026-05-16,2026,5,16,saturday +2026-05-19,2026,5,19,tuesday +2026-05-20,2026,5,20,wednesday +2026-05-25,2026,5,25,monday +2026-05-27,2026,5,27,wednesday +2026-05-28,2026,5,28,thursday +2026-05-30,2026,5,30,saturday +2026-06-01,2026,6,1,monday +2026-06-03,2026,6,3,wednesday +2025-05-15,2025,5,15,thursday +2025-05-16,2025,5,16,friday +2025-05-18,2025,5,18,sunday +2025-05-20,2025,5,20,tuesday +2025-05-22,2025,5,22,thursday +2025-05-23,2025,5,23,friday +2025-05-24,2025,5,24,saturday +2025-05-26,2025,5,26,monday +2025-05-28,2025,5,28,wednesday +2025-05-29,2025,5,29,thursday +2025-05-31,2025,5,31,saturday +2025-06-04,2025,6,4,wednesday +2025-06-06,2025,6,6,friday +2025-06-07,2025,6,7,saturday +2025-06-09,2025,6,9,monday +2025-06-12,2025,6,12,thursday +2025-06-15,2025,6,15,sunday +2025-06-16,2025,6,16,monday +2025-06-18,2025,6,18,wednesday +2025-06-21,2025,6,21,saturday +2025-06-25,2025,6,25,wednesday +2025-06-26,2025,6,26,thursday +2025-06-27,2025,6,27,friday +2025-07-06,2025,7,6,sunday +2025-07-07,2025,7,7,monday +2025-07-08,2025,7,8,tuesday +2025-07-10,2025,7,10,thursday +2025-07-16,2025,7,16,wednesday +2025-07-18,2025,7,18,friday +2025-07-21,2025,7,21,monday +2025-07-23,2025,7,23,wednesday +2025-07-26,2025,7,26,saturday +2025-07-27,2025,7,27,sunday +2025-07-31,2025,7,31,thursday +2025-08-01,2025,8,1,friday +2025-08-04,2025,8,4,monday +2025-08-05,2025,8,5,tuesday +2025-08-11,2025,8,11,monday +2025-08-12,2025,8,12,tuesday +2025-08-13,2025,8,13,wednesday +2025-08-14,2025,8,14,thursday +2025-08-16,2025,8,16,saturday +2025-08-17,2025,8,17,sunday +2025-08-18,2025,8,18,monday +2025-08-19,2025,8,19,tuesday +2025-08-20,2025,8,20,wednesday +2025-08-21,2025,8,21,thursday +2025-08-23,2025,8,23,saturday +2025-08-24,2025,8,24,sunday +2025-08-26,2025,8,26,tuesday +2025-08-28,2025,8,28,thursday +2025-08-31,2025,8,31,sunday +2025-09-01,2025,9,1,monday +2025-09-05,2025,9,5,friday +2025-09-07,2025,9,7,sunday +2025-09-08,2025,9,8,monday +2025-09-09,2025,9,9,tuesday +2025-09-10,2025,9,10,wednesday +2025-09-12,2025,9,12,friday +2025-09-13,2025,9,13,saturday +2025-09-16,2025,9,16,tuesday +2025-09-17,2025,9,17,wednesday +2025-09-18,2025,9,18,thursday +2025-09-19,2025,9,19,friday +2025-09-22,2025,9,22,monday +2025-09-24,2025,9,24,wednesday +2025-09-25,2025,9,25,thursday +2025-09-26,2025,9,26,friday +2025-09-27,2025,9,27,saturday +2025-09-28,2025,9,28,sunday +2025-09-29,2025,9,29,monday +2025-10-01,2025,10,1,wednesday +2025-10-05,2025,10,5,sunday +2025-10-07,2025,10,7,tuesday +2025-10-08,2025,10,8,wednesday +2025-10-10,2025,10,10,friday +2025-10-11,2025,10,11,saturday +2025-10-12,2025,10,12,sunday +2025-10-16,2025,10,16,thursday +2025-10-17,2025,10,17,friday +2025-10-18,2025,10,18,saturday +2025-10-21,2025,10,21,tuesday +2025-10-22,2025,10,22,wednesday +2025-10-26,2025,10,26,sunday +2025-10-27,2025,10,27,monday +2025-10-28,2025,10,28,tuesday +2025-10-29,2025,10,29,wednesday +2025-10-31,2025,10,31,friday +2025-11-03,2025,11,3,monday +2025-11-07,2025,11,7,friday +2025-11-09,2025,11,9,sunday +2025-11-10,2025,11,10,monday +2025-11-11,2025,11,11,tuesday +2025-11-15,2025,11,15,saturday +2025-11-19,2025,11,19,wednesday +2025-11-20,2025,11,20,thursday +2025-11-23,2025,11,23,sunday +2025-11-24,2025,11,24,monday +2025-11-25,2025,11,25,tuesday +2025-11-28,2025,11,28,friday +2025-11-29,2025,11,29,saturday +2025-12-03,2025,12,3,wednesday +2025-12-05,2025,12,5,friday +2025-12-06,2025,12,6,saturday +2025-12-07,2025,12,7,sunday +2025-12-09,2025,12,9,tuesday +2025-12-10,2025,12,10,wednesday +2025-12-14,2025,12,14,sunday +2025-12-15,2025,12,15,monday +2025-12-16,2025,12,16,tuesday +2025-12-17,2025,12,17,wednesday +2025-12-19,2025,12,19,friday +2025-12-20,2025,12,20,saturday +2025-12-24,2025,12,24,wednesday +2025-12-26,2025,12,26,friday +2025-12-30,2025,12,30,tuesday +2025-12-31,2025,12,31,wednesday +2026-01-01,2026,1,1,thursday +2026-01-02,2026,1,2,friday +2026-01-03,2026,1,3,saturday +2026-01-05,2026,1,5,monday +2026-01-07,2026,1,7,wednesday +2026-01-09,2026,1,9,friday +2026-01-12,2026,1,12,monday +2026-01-13,2026,1,13,tuesday +2026-01-16,2026,1,16,friday +2026-01-18,2026,1,18,sunday +2026-01-22,2026,1,22,thursday +2026-01-27,2026,1,27,tuesday +2026-02-01,2026,2,1,sunday +2026-02-02,2026,2,2,monday +2026-02-08,2026,2,8,sunday +2026-02-11,2026,2,11,wednesday +2026-02-16,2026,2,16,monday +2026-02-17,2026,2,17,tuesday +2026-02-19,2026,2,19,thursday +2026-02-21,2026,2,21,saturday +2026-02-27,2026,2,27,friday +2026-02-28,2026,2,28,saturday +2026-03-01,2026,3,1,sunday +2026-03-04,2026,3,4,wednesday +2026-03-05,2026,3,5,thursday +2026-03-07,2026,3,7,saturday +2026-03-13,2026,3,13,friday +2026-03-15,2026,3,15,sunday +2026-03-16,2026,3,16,monday +2026-03-18,2026,3,18,wednesday +2026-03-19,2026,3,19,thursday +2026-03-21,2026,3,21,saturday +2026-03-22,2026,3,22,sunday +2026-03-23,2026,3,23,monday +2026-03-25,2026,3,25,wednesday +2026-03-26,2026,3,26,thursday +2026-03-28,2026,3,28,saturday +2026-03-30,2026,3,30,monday +2026-03-31,2026,3,31,tuesday +2026-04-01,2026,4,1,wednesday +2026-04-05,2026,4,5,sunday +2026-04-07,2026,4,7,tuesday +2026-04-08,2026,4,8,wednesday +2026-04-10,2026,4,10,friday +2026-04-12,2026,4,12,sunday +2026-04-15,2026,4,15,wednesday +2026-04-16,2026,4,16,thursday +2026-04-17,2026,4,17,friday +2026-04-18,2026,4,18,saturday +2026-04-19,2026,4,19,sunday +2026-04-22,2026,4,22,wednesday +2026-04-23,2026,4,23,thursday +2026-04-27,2026,4,27,monday +2026-04-29,2026,4,29,wednesday +2026-05-03,2026,5,3,sunday +2026-05-04,2026,5,4,monday +2026-05-08,2026,5,8,friday +2026-05-14,2026,5,14,thursday +2026-05-17,2026,5,17,sunday +2026-05-18,2026,5,18,monday +2026-05-21,2026,5,21,thursday +2026-05-22,2026,5,22,friday +2026-05-23,2026,5,23,saturday +2026-05-24,2026,5,24,sunday +2026-05-26,2026,5,26,tuesday +2026-05-29,2026,5,29,friday +2026-05-31,2026,5,31,sunday +2026-06-02,2026,6,2,tuesday +2026-06-06,2026,6,6,saturday +2026-06-07,2026,6,7,sunday +2026-06-08,2026,6,8,monday +2026-06-09,2026,6,9,tuesday +2026-06-10,2026,6,10,wednesday +2026-06-11,2026,6,11,thursday +2026-06-12,2026,6,12,friday +2026-06-14,2026,6,14,sunday +2026-06-16,2026,6,16,tuesday +2026-06-17,2026,6,17,wednesday +2026-06-19,2026,6,19,friday +2026-06-21,2026,6,21,sunday +2026-06-22,2026,6,22,monday +2026-06-23,2026,6,23,tuesday +2026-06-25,2026,6,25,thursday +2026-06-26,2026,6,26,friday +2026-06-27,2026,6,27,saturday +2026-07-02,2026,7,2,thursday +2026-07-04,2026,7,4,saturday +2026-07-05,2026,7,5,sunday +2026-07-08,2026,7,8,wednesday +2026-07-09,2026,7,9,thursday +2026-07-10,2026,7,10,friday +2026-07-12,2026,7,12,sunday +2026-07-17,2026,7,17,friday +2026-07-18,2026,7,18,saturday +2026-07-21,2026,7,21,tuesday +2026-07-23,2026,7,23,thursday +2026-06-04,2026,6,4,thursday +2026-06-05,2026,6,5,friday +2026-06-13,2026,6,13,saturday +2026-06-15,2026,6,15,monday +2026-06-18,2026,6,18,thursday +2026-06-20,2026,6,20,saturday +2026-06-24,2026,6,24,wednesday +2026-06-28,2026,6,28,sunday +2026-06-29,2026,6,29,monday +2026-06-30,2026,6,30,tuesday +2026-07-01,2026,7,1,wednesday +2026-07-03,2026,7,3,friday +2026-07-06,2026,7,6,monday +2026-07-07,2026,7,7,tuesday +2026-07-11,2026,7,11,saturday +2026-07-13,2026,7,13,monday +2026-07-14,2026,7,14,tuesday +2026-07-15,2026,7,15,wednesday +2026-07-16,2026,7,16,thursday +2026-07-19,2026,7,19,sunday +2026-07-20,2026,7,20,monday +2026-07-22,2026,7,22,wednesday +2026-07-25,2026,7,25,saturday +2026-07-27,2026,7,27,monday +2026-08-01,2026,8,1,saturday +2026-08-03,2026,8,3,monday +2026-08-05,2026,8,5,wednesday +2026-08-09,2026,8,9,sunday +2026-08-10,2026,8,10,monday +2026-08-14,2026,8,14,friday +2026-08-15,2026,8,15,saturday +2026-08-16,2026,8,16,sunday +2026-08-17,2026,8,17,monday +2026-08-20,2026,8,20,thursday +2026-08-21,2026,8,21,friday +2026-08-22,2026,8,22,saturday +2026-08-25,2026,8,25,tuesday +2026-08-26,2026,8,26,wednesday +2026-08-27,2026,8,27,thursday +2026-08-29,2026,8,29,saturday +2026-08-31,2026,8,31,monday +2026-09-01,2026,9,1,tuesday +2026-09-03,2026,9,3,thursday +2026-09-04,2026,9,4,friday +2026-09-08,2026,9,8,tuesday +2026-09-12,2026,9,12,saturday +2026-09-14,2026,9,14,monday +2026-09-16,2026,9,16,wednesday +2026-09-20,2026,9,20,sunday +2026-09-21,2026,9,21,monday +2026-09-22,2026,9,22,tuesday +2026-09-25,2026,9,25,friday +2026-09-26,2026,9,26,saturday +2026-09-28,2026,9,28,monday +2026-09-29,2026,9,29,tuesday +2026-09-30,2026,9,30,wednesday +2026-10-02,2026,10,2,friday +2026-10-03,2026,10,3,saturday +2026-10-04,2026,10,4,sunday +2026-10-05,2026,10,5,monday +2026-10-06,2026,10,6,tuesday +2026-10-07,2026,10,7,wednesday +2026-10-08,2026,10,8,thursday +2026-10-12,2026,10,12,monday +2026-10-15,2026,10,15,thursday +2026-10-18,2026,10,18,sunday +2026-10-20,2026,10,20,tuesday +2026-10-21,2026,10,21,wednesday +2026-10-23,2026,10,23,friday +2026-10-25,2026,10,25,sunday +2026-10-26,2026,10,26,monday +2026-10-27,2026,10,27,tuesday +2026-10-28,2026,10,28,wednesday +2026-10-29,2026,10,29,thursday +2026-10-30,2026,10,30,friday +2026-10-31,2026,10,31,saturday +2026-11-02,2026,11,2,monday +2026-11-04,2026,11,4,wednesday +2026-11-05,2026,11,5,thursday +2026-11-07,2026,11,7,saturday +2026-11-11,2026,11,11,wednesday +2026-11-14,2026,11,14,saturday +2026-11-15,2026,11,15,sunday +2026-11-19,2026,11,19,thursday +2026-11-22,2026,11,22,sunday +2026-11-30,2026,11,30,monday +2026-12-02,2026,12,2,wednesday +2026-12-03,2026,12,3,thursday +2026-12-04,2026,12,4,friday +2026-12-05,2026,12,5,saturday +2026-12-06,2026,12,6,sunday +2026-12-07,2026,12,7,monday +2026-12-09,2026,12,9,wednesday +2026-12-12,2026,12,12,saturday +2026-12-15,2026,12,15,tuesday +2026-12-17,2026,12,17,thursday +2026-12-20,2026,12,20,sunday +2026-12-22,2026,12,22,tuesday +2026-12-24,2026,12,24,thursday +2026-12-25,2026,12,25,friday +2026-12-26,2026,12,26,saturday +2026-12-27,2026,12,27,sunday +2026-12-28,2026,12,28,monday +2026-12-29,2026,12,29,tuesday +2027-01-03,2027,1,3,sunday +2027-01-04,2027,1,4,monday +2027-01-07,2027,1,7,thursday +2027-01-08,2027,1,8,friday +2027-01-10,2027,1,10,sunday +2027-01-11,2027,1,11,monday +2027-01-17,2027,1,17,sunday +2027-01-18,2027,1,18,monday +2027-01-20,2027,1,20,wednesday +2027-01-22,2027,1,22,friday +2027-01-24,2027,1,24,sunday +2027-01-25,2027,1,25,monday +2027-01-26,2027,1,26,tuesday +2027-01-27,2027,1,27,wednesday +2027-01-30,2027,1,30,saturday +2027-01-31,2027,1,31,sunday +2027-02-01,2027,2,1,monday +2027-02-03,2027,2,3,wednesday +2027-02-05,2027,2,5,friday +2027-02-09,2027,2,9,tuesday +2027-02-10,2027,2,10,wednesday +2027-02-15,2027,2,15,monday +2027-02-18,2027,2,18,thursday +2027-02-20,2027,2,20,saturday +2027-02-23,2027,2,23,tuesday +2027-02-26,2027,2,26,friday +2027-03-01,2027,3,1,monday +2027-03-03,2027,3,3,wednesday +2027-03-04,2027,3,4,thursday +2027-03-06,2027,3,6,saturday +2027-03-09,2027,3,9,tuesday +2027-03-11,2027,3,11,thursday +2027-03-13,2027,3,13,saturday +2027-03-14,2027,3,14,sunday +2027-03-15,2027,3,15,monday +2027-03-17,2027,3,17,wednesday +2027-03-19,2027,3,19,friday +2027-03-20,2027,3,20,saturday +2027-03-21,2027,3,21,sunday +2027-03-25,2027,3,25,thursday +2027-03-26,2027,3,26,friday +2027-03-27,2027,3,27,saturday +2027-03-28,2027,3,28,sunday +2027-04-03,2027,4,3,saturday +2027-04-04,2027,4,4,sunday +2027-04-05,2027,4,5,monday +2027-04-08,2027,4,8,thursday +2027-04-11,2027,4,11,sunday +2027-04-12,2027,4,12,monday +2027-04-16,2027,4,16,friday +2027-04-18,2027,4,18,sunday +2027-04-19,2027,4,19,monday +2027-04-20,2027,4,20,tuesday +2027-04-28,2027,4,28,wednesday +2027-05-02,2027,5,2,sunday +2027-05-05,2027,5,5,wednesday +2027-05-06,2027,5,6,thursday +2027-05-07,2027,5,7,friday +2027-05-08,2027,5,8,saturday +2027-05-12,2027,5,12,wednesday +2027-05-15,2027,5,15,saturday +2027-05-16,2027,5,16,sunday +2027-05-17,2027,5,17,monday +2027-05-20,2027,5,20,thursday +2027-05-21,2027,5,21,friday +2027-05-22,2027,5,22,saturday +2027-05-23,2027,5,23,sunday +2027-05-24,2027,5,24,monday +2027-05-25,2027,5,25,tuesday +2027-05-27,2027,5,27,thursday +2027-05-28,2027,5,28,friday +2027-05-29,2027,5,29,saturday +2027-05-30,2027,5,30,sunday +2027-05-31,2027,5,31,monday +2027-06-01,2027,6,1,tuesday +2027-06-04,2027,6,4,friday +2027-06-05,2027,6,5,saturday +2027-06-09,2027,6,9,wednesday +2027-06-10,2027,6,10,thursday +2027-06-12,2027,6,12,saturday +2027-06-13,2027,6,13,sunday +2027-06-14,2027,6,14,monday +2027-06-15,2027,6,15,tuesday +2027-06-16,2027,6,16,wednesday +2027-06-17,2027,6,17,thursday +2027-06-19,2027,6,19,saturday +2027-06-21,2027,6,21,monday +2027-06-23,2027,6,23,wednesday +2027-06-24,2027,6,24,thursday +2027-06-26,2027,6,26,saturday +2027-06-27,2027,6,27,sunday +2027-06-28,2027,6,28,monday +2027-06-29,2027,6,29,tuesday +2027-07-01,2027,7,1,thursday +2027-07-02,2027,7,2,friday +2027-07-04,2027,7,4,sunday +2027-07-05,2027,7,5,monday +2027-07-07,2027,7,7,wednesday +2027-07-10,2027,7,10,saturday +2027-07-12,2027,7,12,monday +2027-07-14,2027,7,14,wednesday +2027-07-16,2027,7,16,friday +2027-07-17,2027,7,17,saturday +2027-07-19,2027,7,19,monday +2027-07-20,2027,7,20,tuesday +2027-07-22,2027,7,22,thursday +2027-07-23,2027,7,23,friday +2027-07-26,2027,7,26,monday +2027-07-27,2027,7,27,tuesday +2026-07-24,2026,7,24,friday +2026-07-26,2026,7,26,sunday +2026-07-28,2026,7,28,tuesday +2026-07-29,2026,7,29,wednesday +2026-07-30,2026,7,30,thursday +2026-07-31,2026,7,31,friday +2026-08-02,2026,8,2,sunday +2026-08-04,2026,8,4,tuesday +2026-08-06,2026,8,6,thursday +2026-08-07,2026,8,7,friday +2026-08-08,2026,8,8,saturday +2026-08-11,2026,8,11,tuesday +2026-08-12,2026,8,12,wednesday +2026-08-13,2026,8,13,thursday +2026-08-18,2026,8,18,tuesday +2026-08-19,2026,8,19,wednesday +2026-08-23,2026,8,23,sunday +2026-08-24,2026,8,24,monday +2026-08-28,2026,8,28,friday +2026-08-30,2026,8,30,sunday +2026-09-02,2026,9,2,wednesday +2026-09-05,2026,9,5,saturday +2026-09-06,2026,9,6,sunday +2026-09-07,2026,9,7,monday +2026-09-09,2026,9,9,wednesday +2026-09-10,2026,9,10,thursday +2026-09-11,2026,9,11,friday +2026-09-13,2026,9,13,sunday +2026-09-15,2026,9,15,tuesday +2026-09-17,2026,9,17,thursday +2026-09-18,2026,9,18,friday +2026-09-19,2026,9,19,saturday +2026-09-23,2026,9,23,wednesday +2026-09-24,2026,9,24,thursday +2026-09-27,2026,9,27,sunday +2026-10-01,2026,10,1,thursday +2026-10-09,2026,10,9,friday +2026-10-10,2026,10,10,saturday +2026-10-11,2026,10,11,sunday +2026-10-13,2026,10,13,tuesday +2026-10-14,2026,10,14,wednesday +2026-10-16,2026,10,16,friday +2026-10-17,2026,10,17,saturday +2026-10-19,2026,10,19,monday +2026-10-22,2026,10,22,thursday +2026-10-24,2026,10,24,saturday +2026-11-01,2026,11,1,sunday +2026-11-03,2026,11,3,tuesday +2026-11-06,2026,11,6,friday +2026-11-08,2026,11,8,sunday +2026-11-09,2026,11,9,monday +2026-11-10,2026,11,10,tuesday +2026-11-12,2026,11,12,thursday +2026-11-13,2026,11,13,friday +2026-11-16,2026,11,16,monday +2026-11-17,2026,11,17,tuesday +2026-11-18,2026,11,18,wednesday +2026-11-20,2026,11,20,friday +2026-11-21,2026,11,21,saturday +2026-11-23,2026,11,23,monday +2026-11-24,2026,11,24,tuesday +2026-11-25,2026,11,25,wednesday +2026-11-26,2026,11,26,thursday +2026-11-27,2026,11,27,friday +2026-11-28,2026,11,28,saturday +2026-11-29,2026,11,29,sunday +2026-12-01,2026,12,1,tuesday +2026-12-08,2026,12,8,tuesday +2026-12-10,2026,12,10,thursday +2026-12-11,2026,12,11,friday +2026-12-13,2026,12,13,sunday +2026-12-14,2026,12,14,monday +2026-12-16,2026,12,16,wednesday +2026-12-18,2026,12,18,friday +2026-12-19,2026,12,19,saturday +2026-12-21,2026,12,21,monday +2026-12-23,2026,12,23,wednesday +2026-12-30,2026,12,30,wednesday +2026-12-31,2026,12,31,thursday +2027-01-01,2027,1,1,friday +2027-01-02,2027,1,2,saturday +2027-01-05,2027,1,5,tuesday +2027-01-06,2027,1,6,wednesday +2027-01-09,2027,1,9,saturday +2027-01-12,2027,1,12,tuesday +2027-01-13,2027,1,13,wednesday +2027-01-14,2027,1,14,thursday +2027-01-15,2027,1,15,friday +2027-01-16,2027,1,16,saturday +2027-01-19,2027,1,19,tuesday +2027-01-21,2027,1,21,thursday +2027-01-23,2027,1,23,saturday +2027-01-28,2027,1,28,thursday +2027-01-29,2027,1,29,friday +2027-02-02,2027,2,2,tuesday +2027-02-04,2027,2,4,thursday +2027-02-06,2027,2,6,saturday +2027-02-07,2027,2,7,sunday +2027-02-08,2027,2,8,monday +2027-02-11,2027,2,11,thursday +2027-02-12,2027,2,12,friday +2027-02-13,2027,2,13,saturday +2027-02-14,2027,2,14,sunday +2027-02-16,2027,2,16,tuesday +2027-02-17,2027,2,17,wednesday +2027-02-19,2027,2,19,friday +2027-02-21,2027,2,21,sunday +2027-02-22,2027,2,22,monday +2027-02-24,2027,2,24,wednesday +2027-02-25,2027,2,25,thursday +2027-02-27,2027,2,27,saturday +2027-02-28,2027,2,28,sunday +2027-03-02,2027,3,2,tuesday +2027-03-05,2027,3,5,friday +2027-03-07,2027,3,7,sunday +2027-03-08,2027,3,8,monday +2027-03-10,2027,3,10,wednesday +2027-03-12,2027,3,12,friday +2027-03-16,2027,3,16,tuesday +2027-03-18,2027,3,18,thursday +2027-03-22,2027,3,22,monday +2027-03-23,2027,3,23,tuesday +2027-03-24,2027,3,24,wednesday +2027-03-29,2027,3,29,monday +2027-03-30,2027,3,30,tuesday +2027-03-31,2027,3,31,wednesday +2027-04-01,2027,4,1,thursday +2027-04-02,2027,4,2,friday +2027-04-06,2027,4,6,tuesday +2027-04-07,2027,4,7,wednesday +2027-04-09,2027,4,9,friday +2027-04-10,2027,4,10,saturday +2027-04-13,2027,4,13,tuesday +2027-04-14,2027,4,14,wednesday +2027-04-15,2027,4,15,thursday +2027-04-17,2027,4,17,saturday +2027-04-21,2027,4,21,wednesday +2027-04-22,2027,4,22,thursday +2027-04-23,2027,4,23,friday +2027-04-24,2027,4,24,saturday +2027-04-25,2027,4,25,sunday +2027-04-26,2027,4,26,monday +2027-04-27,2027,4,27,tuesday +2027-04-29,2027,4,29,thursday +2027-04-30,2027,4,30,friday +2027-05-01,2027,5,1,saturday +2027-05-03,2027,5,3,monday +2027-05-04,2027,5,4,tuesday +2027-05-09,2027,5,9,sunday +2027-05-10,2027,5,10,monday +2027-05-11,2027,5,11,tuesday +2027-05-13,2027,5,13,thursday +2027-05-14,2027,5,14,friday +2027-05-18,2027,5,18,tuesday +2027-05-19,2027,5,19,wednesday +2027-05-26,2027,5,26,wednesday +2027-06-02,2027,6,2,wednesday +2027-06-03,2027,6,3,thursday +2027-06-06,2027,6,6,sunday +2027-06-07,2027,6,7,monday +2027-06-08,2027,6,8,tuesday +2027-06-11,2027,6,11,friday +2027-06-18,2027,6,18,friday +2027-06-20,2027,6,20,sunday +2027-06-22,2027,6,22,tuesday +2027-06-25,2027,6,25,friday +2027-06-30,2027,6,30,wednesday +2027-07-03,2027,7,3,saturday +2027-07-06,2027,7,6,tuesday +2027-07-08,2027,7,8,thursday +2027-07-09,2027,7,9,friday +2027-07-11,2027,7,11,sunday +2027-07-13,2027,7,13,tuesday +2027-07-15,2027,7,15,thursday +2027-07-18,2027,7,18,sunday +2027-07-21,2027,7,21,wednesday +2027-07-24,2027,7,24,saturday +2027-07-25,2027,7,25,sunday +2027-07-28,2027,7,28,wednesday +2027-07-30,2027,7,30,friday +2027-08-02,2027,8,2,monday +2027-08-05,2027,8,5,thursday +2027-08-06,2027,8,6,friday +2027-08-09,2027,8,9,monday +2027-08-10,2027,8,10,tuesday +2027-08-12,2027,8,12,thursday +2027-08-14,2027,8,14,saturday +2027-08-16,2027,8,16,monday +2027-08-17,2027,8,17,tuesday +2027-08-18,2027,8,18,wednesday +2027-08-20,2027,8,20,friday +2027-08-21,2027,8,21,saturday +2027-08-23,2027,8,23,monday +2027-08-24,2027,8,24,tuesday +2027-08-26,2027,8,26,thursday +2027-08-27,2027,8,27,friday +2027-08-29,2027,8,29,sunday +2027-08-30,2027,8,30,monday +2027-09-01,2027,9,1,wednesday +2027-09-03,2027,9,3,friday +2027-09-05,2027,9,5,sunday +2027-09-07,2027,9,7,tuesday +2027-09-08,2027,9,8,wednesday +2027-09-09,2027,9,9,thursday +2027-09-13,2027,9,13,monday +2027-09-14,2027,9,14,tuesday +2027-09-16,2027,9,16,thursday +2027-09-17,2027,9,17,friday +2027-09-18,2027,9,18,saturday +2027-09-19,2027,9,19,sunday +2027-09-21,2027,9,21,tuesday +2027-09-22,2027,9,22,wednesday +2027-09-23,2027,9,23,thursday +2027-07-29,2027,7,29,thursday +2027-07-31,2027,7,31,saturday +2027-08-01,2027,8,1,sunday +2027-08-03,2027,8,3,tuesday +2027-08-04,2027,8,4,wednesday +2027-08-07,2027,8,7,saturday +2027-08-08,2027,8,8,sunday +2027-08-11,2027,8,11,wednesday +2027-08-13,2027,8,13,friday +2027-08-15,2027,8,15,sunday +2027-08-19,2027,8,19,thursday +2027-08-22,2027,8,22,sunday +2027-08-25,2027,8,25,wednesday +2027-08-28,2027,8,28,saturday +2027-08-31,2027,8,31,tuesday +2027-09-02,2027,9,2,thursday +2027-09-04,2027,9,4,saturday +2027-09-06,2027,9,6,monday +2027-09-10,2027,9,10,friday +2027-09-11,2027,9,11,saturday +2027-09-12,2027,9,12,sunday +2027-09-15,2027,9,15,wednesday +2027-09-20,2027,9,20,monday +2027-09-24,2027,9,24,friday +2027-09-29,2027,9,29,wednesday +2027-10-07,2027,10,7,thursday +2027-10-08,2027,10,8,friday +2027-10-11,2027,10,11,monday +2027-10-13,2027,10,13,wednesday +2027-10-16,2027,10,16,saturday +2027-10-19,2027,10,19,tuesday +2027-10-20,2027,10,20,wednesday +2027-10-22,2027,10,22,friday +2027-10-24,2027,10,24,sunday +2027-10-26,2027,10,26,tuesday +2027-10-29,2027,10,29,friday +2027-10-30,2027,10,30,saturday +2027-11-01,2027,11,1,monday +2027-11-06,2027,11,6,saturday +2027-11-08,2027,11,8,monday +2027-11-09,2027,11,9,tuesday +2027-11-10,2027,11,10,wednesday +2027-11-11,2027,11,11,thursday +2027-11-12,2027,11,12,friday +2027-11-13,2027,11,13,saturday +2027-11-14,2027,11,14,sunday +2027-11-16,2027,11,16,tuesday +2027-11-17,2027,11,17,wednesday +2027-11-20,2027,11,20,saturday +2027-11-21,2027,11,21,sunday +2027-11-22,2027,11,22,monday +2027-11-23,2027,11,23,tuesday +2027-11-24,2027,11,24,wednesday +2027-11-25,2027,11,25,thursday +2027-11-26,2027,11,26,friday +2027-11-27,2027,11,27,saturday +2027-11-28,2027,11,28,sunday +2027-12-01,2027,12,1,wednesday +2027-12-05,2027,12,5,sunday +2027-12-06,2027,12,6,monday +2027-12-07,2027,12,7,tuesday +2027-12-10,2027,12,10,friday +2027-12-13,2027,12,13,monday +2027-12-14,2027,12,14,tuesday +2027-12-16,2027,12,16,thursday +2027-12-17,2027,12,17,friday +2027-12-19,2027,12,19,sunday +2027-12-20,2027,12,20,monday +2027-12-25,2027,12,25,saturday +2027-12-28,2027,12,28,tuesday +2027-12-29,2027,12,29,wednesday +2027-12-31,2027,12,31,friday +2028-01-02,2028,1,2,sunday +2028-01-05,2028,1,5,wednesday +2028-01-07,2028,1,7,friday +2028-01-09,2028,1,9,sunday +2028-01-10,2028,1,10,monday +2028-01-14,2028,1,14,friday +2028-01-16,2028,1,16,sunday +2028-01-17,2028,1,17,monday +2028-01-20,2028,1,20,thursday +2028-01-21,2028,1,21,friday +2028-01-22,2028,1,22,saturday +2028-01-26,2028,1,26,wednesday +2028-01-27,2028,1,27,thursday +2028-01-29,2028,1,29,saturday +2028-02-02,2028,2,2,wednesday +2028-02-03,2028,2,3,thursday +2028-02-05,2028,2,5,saturday +2028-02-11,2028,2,11,friday +2028-02-13,2028,2,13,sunday +2028-02-15,2028,2,15,tuesday +2028-02-18,2028,2,18,friday +2028-02-19,2028,2,19,saturday +2028-02-20,2028,2,20,sunday +2028-02-21,2028,2,21,monday +2028-02-22,2028,2,22,tuesday +2028-02-23,2028,2,23,wednesday +2028-02-24,2028,2,24,thursday +2028-02-25,2028,2,25,friday +2028-02-26,2028,2,26,saturday +2028-02-27,2028,2,27,sunday +2028-02-28,2028,2,28,monday +2028-03-01,2028,3,1,wednesday +2028-03-02,2028,3,2,thursday +2028-03-06,2028,3,6,monday +2028-03-07,2028,3,7,tuesday +2028-03-08,2028,3,8,wednesday +2028-03-10,2028,3,10,friday +2028-03-12,2028,3,12,sunday +2028-03-18,2028,3,18,saturday +2028-03-20,2028,3,20,monday +2028-03-22,2028,3,22,wednesday +2028-03-25,2028,3,25,saturday +2028-03-28,2028,3,28,tuesday +2028-03-29,2028,3,29,wednesday +2028-03-30,2028,3,30,thursday +2028-03-31,2028,3,31,friday +2028-04-02,2028,4,2,sunday +2028-04-03,2028,4,3,monday +2028-04-04,2028,4,4,tuesday +2028-04-06,2028,4,6,thursday +2028-04-08,2028,4,8,saturday +2028-04-10,2028,4,10,monday +2028-04-11,2028,4,11,tuesday +2028-04-13,2028,4,13,thursday +2028-04-17,2028,4,17,monday +2028-04-18,2028,4,18,tuesday +2028-04-19,2028,4,19,wednesday +2028-04-21,2028,4,21,friday +2028-04-22,2028,4,22,saturday +2028-04-26,2028,4,26,wednesday +2028-04-29,2028,4,29,saturday +2028-05-02,2028,5,2,tuesday +2028-05-06,2028,5,6,saturday +2028-05-09,2028,5,9,tuesday +2028-05-10,2028,5,10,wednesday +2028-05-11,2028,5,11,thursday +2028-05-12,2028,5,12,friday +2028-05-14,2028,5,14,sunday +2028-05-15,2028,5,15,monday +2028-05-20,2028,5,20,saturday +2028-05-23,2028,5,23,tuesday +2028-05-24,2028,5,24,wednesday +2028-05-25,2028,5,25,thursday +2028-05-27,2028,5,27,saturday +2028-05-28,2028,5,28,sunday +2028-05-29,2028,5,29,monday +2028-05-30,2028,5,30,tuesday +2028-06-01,2028,6,1,thursday +2028-06-06,2028,6,6,tuesday +2028-06-07,2028,6,7,wednesday +2028-06-09,2028,6,9,friday +2028-06-12,2028,6,12,monday +2028-06-13,2028,6,13,tuesday +2028-06-16,2028,6,16,friday +2028-06-17,2028,6,17,saturday +2028-06-20,2028,6,20,tuesday +2028-06-23,2028,6,23,friday +2028-06-25,2028,6,25,sunday +2028-06-29,2028,6,29,thursday +2028-07-04,2028,7,4,tuesday +2028-07-07,2028,7,7,friday +2028-07-09,2028,7,9,sunday +2028-07-12,2028,7,12,wednesday +2028-07-13,2028,7,13,thursday +2028-07-16,2028,7,16,sunday +2028-07-17,2028,7,17,monday +2028-07-18,2028,7,18,tuesday +2028-07-19,2028,7,19,wednesday +2028-07-22,2028,7,22,saturday +2028-07-23,2028,7,23,sunday +2028-07-24,2028,7,24,monday +2028-07-26,2028,7,26,wednesday +2028-07-27,2028,7,27,thursday +2028-07-28,2028,7,28,friday +2028-07-31,2028,7,31,monday +2028-08-02,2028,8,2,wednesday +2028-08-04,2028,8,4,friday +2028-08-05,2028,8,5,saturday +2028-08-06,2028,8,6,sunday +2028-08-07,2028,8,7,monday +2028-08-08,2028,8,8,tuesday +2028-08-13,2028,8,13,sunday +2028-08-14,2028,8,14,monday +2028-08-18,2028,8,18,friday +2028-08-23,2028,8,23,wednesday +2028-08-25,2028,8,25,friday +2028-08-26,2028,8,26,saturday +2028-08-27,2028,8,27,sunday +2028-08-28,2028,8,28,monday +2028-08-29,2028,8,29,tuesday +2028-08-30,2028,8,30,wednesday +2028-08-31,2028,8,31,thursday +2028-09-02,2028,9,2,saturday +2028-09-03,2028,9,3,sunday +2028-09-05,2028,9,5,tuesday +2028-09-08,2028,9,8,friday +2028-09-09,2028,9,9,saturday +2028-09-11,2028,9,11,monday +2028-09-12,2028,9,12,tuesday +2028-09-15,2028,9,15,friday +2028-09-17,2028,9,17,sunday +2028-09-19,2028,9,19,tuesday +2028-09-20,2028,9,20,wednesday +2028-09-25,2028,9,25,monday +2028-09-27,2028,9,27,wednesday +2028-09-28,2028,9,28,thursday +2028-09-30,2028,9,30,saturday +2028-10-01,2028,10,1,sunday +2028-10-05,2028,10,5,thursday +2028-10-06,2028,10,6,friday +2028-10-12,2028,10,12,thursday +2027-09-25,2027,9,25,saturday +2027-09-26,2027,9,26,sunday +2027-09-27,2027,9,27,monday +2027-09-28,2027,9,28,tuesday +2027-09-30,2027,9,30,thursday +2027-10-01,2027,10,1,friday +2027-10-02,2027,10,2,saturday +2027-10-03,2027,10,3,sunday +2027-10-04,2027,10,4,monday +2027-10-05,2027,10,5,tuesday +2027-10-06,2027,10,6,wednesday +2027-10-09,2027,10,9,saturday +2027-10-10,2027,10,10,sunday +2027-10-12,2027,10,12,tuesday +2027-10-14,2027,10,14,thursday +2027-10-15,2027,10,15,friday +2027-10-17,2027,10,17,sunday +2027-10-18,2027,10,18,monday +2027-10-21,2027,10,21,thursday +2027-10-23,2027,10,23,saturday +2027-10-25,2027,10,25,monday +2027-10-27,2027,10,27,wednesday +2027-10-28,2027,10,28,thursday +2027-10-31,2027,10,31,sunday +2027-11-02,2027,11,2,tuesday +2027-11-03,2027,11,3,wednesday +2027-11-04,2027,11,4,thursday +2027-11-05,2027,11,5,friday +2027-11-07,2027,11,7,sunday +2027-11-15,2027,11,15,monday +2027-11-18,2027,11,18,thursday +2027-11-19,2027,11,19,friday +2027-11-29,2027,11,29,monday +2027-11-30,2027,11,30,tuesday +2027-12-02,2027,12,2,thursday +2027-12-03,2027,12,3,friday +2027-12-04,2027,12,4,saturday +2027-12-08,2027,12,8,wednesday +2027-12-09,2027,12,9,thursday +2027-12-11,2027,12,11,saturday +2027-12-12,2027,12,12,sunday +2027-12-15,2027,12,15,wednesday +2027-12-18,2027,12,18,saturday +2027-12-21,2027,12,21,tuesday +2027-12-22,2027,12,22,wednesday +2027-12-23,2027,12,23,thursday +2027-12-24,2027,12,24,friday +2027-12-26,2027,12,26,sunday +2027-12-27,2027,12,27,monday +2027-12-30,2027,12,30,thursday +2028-01-01,2028,1,1,saturday +2028-01-03,2028,1,3,monday +2028-01-04,2028,1,4,tuesday +2028-01-06,2028,1,6,thursday +2028-01-08,2028,1,8,saturday +2028-01-11,2028,1,11,tuesday +2028-01-12,2028,1,12,wednesday +2028-01-13,2028,1,13,thursday +2028-01-15,2028,1,15,saturday +2028-01-18,2028,1,18,tuesday +2028-01-19,2028,1,19,wednesday +2028-01-23,2028,1,23,sunday +2028-01-24,2028,1,24,monday +2028-01-25,2028,1,25,tuesday +2028-01-28,2028,1,28,friday +2028-01-30,2028,1,30,sunday +2028-01-31,2028,1,31,monday +2028-02-01,2028,2,1,tuesday +2028-02-04,2028,2,4,friday +2028-02-06,2028,2,6,sunday +2028-02-07,2028,2,7,monday +2028-02-08,2028,2,8,tuesday +2028-02-09,2028,2,9,wednesday +2028-02-10,2028,2,10,thursday +2028-02-12,2028,2,12,saturday +2028-02-14,2028,2,14,monday +2028-02-16,2028,2,16,wednesday +2028-02-17,2028,2,17,thursday +2028-02-29,2028,2,29,tuesday +2028-03-03,2028,3,3,friday +2028-03-04,2028,3,4,saturday +2028-03-05,2028,3,5,sunday +2028-03-09,2028,3,9,thursday +2028-03-11,2028,3,11,saturday +2028-03-13,2028,3,13,monday +2028-03-14,2028,3,14,tuesday +2028-03-15,2028,3,15,wednesday +2028-03-16,2028,3,16,thursday +2028-03-17,2028,3,17,friday +2028-03-19,2028,3,19,sunday +2028-03-21,2028,3,21,tuesday +2028-03-23,2028,3,23,thursday +2028-03-24,2028,3,24,friday +2028-03-26,2028,3,26,sunday +2028-03-27,2028,3,27,monday +2028-04-01,2028,4,1,saturday +2028-04-05,2028,4,5,wednesday +2028-04-07,2028,4,7,friday +2028-04-09,2028,4,9,sunday +2028-04-12,2028,4,12,wednesday +2028-04-14,2028,4,14,friday +2028-04-15,2028,4,15,saturday +2028-04-16,2028,4,16,sunday +2028-04-20,2028,4,20,thursday +2028-04-23,2028,4,23,sunday +2028-04-24,2028,4,24,monday +2028-04-25,2028,4,25,tuesday +2028-04-27,2028,4,27,thursday +2028-04-28,2028,4,28,friday +2028-04-30,2028,4,30,sunday +2028-05-01,2028,5,1,monday +2028-05-03,2028,5,3,wednesday +2028-05-04,2028,5,4,thursday +2028-05-05,2028,5,5,friday +2028-05-07,2028,5,7,sunday +2028-05-08,2028,5,8,monday +2028-05-13,2028,5,13,saturday +2028-05-16,2028,5,16,tuesday +2028-05-17,2028,5,17,wednesday +2028-05-18,2028,5,18,thursday +2028-05-19,2028,5,19,friday +2028-05-21,2028,5,21,sunday +2028-05-22,2028,5,22,monday +2028-05-26,2028,5,26,friday +2028-05-31,2028,5,31,wednesday +2028-06-02,2028,6,2,friday +2028-06-03,2028,6,3,saturday +2028-06-04,2028,6,4,sunday +2028-06-05,2028,6,5,monday +2028-06-08,2028,6,8,thursday +2028-06-10,2028,6,10,saturday +2028-06-11,2028,6,11,sunday +2028-06-14,2028,6,14,wednesday +2028-06-15,2028,6,15,thursday +2028-06-18,2028,6,18,sunday +2028-06-19,2028,6,19,monday +2028-06-21,2028,6,21,wednesday +2028-06-22,2028,6,22,thursday +2028-06-24,2028,6,24,saturday +2028-06-26,2028,6,26,monday +2028-06-27,2028,6,27,tuesday +2028-06-28,2028,6,28,wednesday +2028-06-30,2028,6,30,friday +2028-07-01,2028,7,1,saturday +2028-07-02,2028,7,2,sunday +2028-07-03,2028,7,3,monday +2028-07-05,2028,7,5,wednesday +2028-07-06,2028,7,6,thursday +2028-07-08,2028,7,8,saturday +2028-07-10,2028,7,10,monday +2028-07-11,2028,7,11,tuesday +2028-07-14,2028,7,14,friday +2028-07-15,2028,7,15,saturday +2028-07-20,2028,7,20,thursday +2028-07-21,2028,7,21,friday +2028-07-25,2028,7,25,tuesday +2028-07-29,2028,7,29,saturday +2028-07-30,2028,7,30,sunday +2028-08-01,2028,8,1,tuesday +2028-08-03,2028,8,3,thursday +2028-08-09,2028,8,9,wednesday +2028-08-10,2028,8,10,thursday +2028-08-11,2028,8,11,friday +2028-08-12,2028,8,12,saturday +2028-08-15,2028,8,15,tuesday +2028-08-16,2028,8,16,wednesday +2028-08-17,2028,8,17,thursday +2028-08-19,2028,8,19,saturday +2028-08-20,2028,8,20,sunday +2028-08-21,2028,8,21,monday +2028-08-22,2028,8,22,tuesday +2028-08-24,2028,8,24,thursday +2028-09-01,2028,9,1,friday +2028-09-04,2028,9,4,monday +2028-09-06,2028,9,6,wednesday +2028-09-07,2028,9,7,thursday +2028-09-10,2028,9,10,sunday +2028-09-13,2028,9,13,wednesday +2028-09-14,2028,9,14,thursday +2028-09-16,2028,9,16,saturday +2028-09-18,2028,9,18,monday +2028-09-21,2028,9,21,thursday +2028-09-22,2028,9,22,friday +2028-09-23,2028,9,23,saturday +2028-09-24,2028,9,24,sunday +2028-09-26,2028,9,26,tuesday +2028-09-29,2028,9,29,friday +2028-10-02,2028,10,2,monday +2028-10-03,2028,10,3,tuesday +2028-10-04,2028,10,4,wednesday +2028-10-07,2028,10,7,saturday +2028-10-08,2028,10,8,sunday +2028-10-09,2028,10,9,monday +2028-10-10,2028,10,10,tuesday +2028-10-11,2028,10,11,wednesday +2028-10-14,2028,10,14,saturday +2028-10-15,2028,10,15,sunday +2028-10-17,2028,10,17,tuesday +2028-10-18,2028,10,18,wednesday +2028-10-19,2028,10,19,thursday +2028-10-22,2028,10,22,sunday +2028-10-25,2028,10,25,wednesday +2028-10-27,2028,10,27,friday +2028-10-29,2028,10,29,sunday +2028-10-31,2028,10,31,tuesday +2028-11-02,2028,11,2,thursday +2028-11-03,2028,11,3,friday +2028-11-05,2028,11,5,sunday +2028-11-07,2028,11,7,tuesday +2028-11-09,2028,11,9,thursday +2028-11-11,2028,11,11,saturday +2028-11-12,2028,11,12,sunday +2028-11-14,2028,11,14,tuesday +2028-10-13,2028,10,13,friday +2028-10-16,2028,10,16,monday +2028-10-20,2028,10,20,friday +2028-10-21,2028,10,21,saturday +2028-10-23,2028,10,23,monday +2028-10-24,2028,10,24,tuesday +2028-10-26,2028,10,26,thursday +2028-10-28,2028,10,28,saturday +2028-10-30,2028,10,30,monday +2028-11-01,2028,11,1,wednesday +2028-11-04,2028,11,4,saturday +2028-11-06,2028,11,6,monday +2028-11-08,2028,11,8,wednesday +2028-11-10,2028,11,10,friday +2028-11-13,2028,11,13,monday +2028-11-16,2028,11,16,thursday +2028-11-17,2028,11,17,friday +2028-11-19,2028,11,19,sunday +2028-11-20,2028,11,20,monday +2028-11-22,2028,11,22,wednesday +2028-11-24,2028,11,24,friday +2028-11-25,2028,11,25,saturday +2028-11-29,2028,11,29,wednesday +2028-11-30,2028,11,30,thursday +2028-12-03,2028,12,3,sunday +2028-12-09,2028,12,9,saturday +2028-12-11,2028,12,11,monday +2028-12-12,2028,12,12,tuesday +2028-12-16,2028,12,16,saturday +2028-12-17,2028,12,17,sunday +2028-12-18,2028,12,18,monday +2028-12-19,2028,12,19,tuesday +2028-12-21,2028,12,21,thursday +2028-12-22,2028,12,22,friday +2028-12-27,2028,12,27,wednesday +2028-12-29,2028,12,29,friday +2029-01-01,2029,1,1,monday +2029-01-03,2029,1,3,wednesday +2029-01-06,2029,1,6,saturday +2029-01-08,2029,1,8,monday +2029-01-13,2029,1,13,saturday +2029-01-17,2029,1,17,wednesday +2029-01-19,2029,1,19,friday +2029-01-21,2029,1,21,sunday +2029-01-23,2029,1,23,tuesday +2029-01-26,2029,1,26,friday +2029-01-28,2029,1,28,sunday +2029-01-31,2029,1,31,wednesday +2029-02-01,2029,2,1,thursday +2029-02-03,2029,2,3,saturday +2029-02-04,2029,2,4,sunday +2029-02-05,2029,2,5,monday +2029-02-08,2029,2,8,thursday +2029-02-17,2029,2,17,saturday +2029-02-18,2029,2,18,sunday +2029-02-19,2029,2,19,monday +2029-03-06,2029,3,6,tuesday +2029-03-13,2029,3,13,tuesday +2029-03-14,2029,3,14,wednesday +2029-03-15,2029,3,15,thursday +2029-03-16,2029,3,16,friday +2029-03-18,2029,3,18,sunday +2029-03-20,2029,3,20,tuesday +2029-03-21,2029,3,21,wednesday +2029-03-23,2029,3,23,friday +2029-03-25,2029,3,25,sunday +2029-03-28,2029,3,28,wednesday +2029-03-29,2029,3,29,thursday +2029-04-01,2029,4,1,sunday +2029-04-03,2029,4,3,tuesday +2029-04-06,2029,4,6,friday +2029-04-08,2029,4,8,sunday +2029-04-12,2029,4,12,thursday +2029-04-13,2029,4,13,friday +2029-04-14,2029,4,14,saturday +2029-04-16,2029,4,16,monday +2029-04-19,2029,4,19,thursday +2029-04-20,2029,4,20,friday +2029-04-25,2029,4,25,wednesday +2029-04-28,2029,4,28,saturday +2029-04-29,2029,4,29,sunday +2029-05-01,2029,5,1,tuesday +2029-05-02,2029,5,2,wednesday +2029-05-03,2029,5,3,thursday +2029-05-05,2029,5,5,saturday +2029-05-06,2029,5,6,sunday +2029-05-08,2029,5,8,tuesday +2029-05-10,2029,5,10,thursday +2029-05-16,2029,5,16,wednesday +2029-05-19,2029,5,19,saturday +2029-05-22,2029,5,22,tuesday +2029-05-23,2029,5,23,wednesday +2029-05-26,2029,5,26,saturday +2029-05-27,2029,5,27,sunday +2029-05-29,2029,5,29,tuesday +2029-06-03,2029,6,3,sunday +2029-06-05,2029,6,5,tuesday +2029-06-09,2029,6,9,saturday +2029-06-11,2029,6,11,monday +2029-06-12,2029,6,12,tuesday +2029-06-13,2029,6,13,wednesday +2029-06-17,2029,6,17,sunday +2029-06-19,2029,6,19,tuesday +2029-06-21,2029,6,21,thursday +2029-06-22,2029,6,22,friday +2029-06-23,2029,6,23,saturday +2029-06-25,2029,6,25,monday +2029-06-26,2029,6,26,tuesday +2029-06-27,2029,6,27,wednesday +2029-06-28,2029,6,28,thursday +2029-06-30,2029,6,30,saturday +2029-07-01,2029,7,1,sunday +2029-07-02,2029,7,2,monday +2029-07-03,2029,7,3,tuesday +2029-07-04,2029,7,4,wednesday +2029-07-08,2029,7,8,sunday +2029-07-09,2029,7,9,monday +2029-07-11,2029,7,11,wednesday +2029-07-12,2029,7,12,thursday +2029-07-17,2029,7,17,tuesday +2029-07-18,2029,7,18,wednesday +2029-07-19,2029,7,19,thursday +2029-07-20,2029,7,20,friday +2029-07-23,2029,7,23,monday +2029-07-25,2029,7,25,wednesday +2029-07-26,2029,7,26,thursday +2029-07-27,2029,7,27,friday +2029-07-28,2029,7,28,saturday +2029-08-02,2029,8,2,thursday +2029-08-04,2029,8,4,saturday +2029-08-05,2029,8,5,sunday +2029-08-08,2029,8,8,wednesday +2029-08-09,2029,8,9,thursday +2029-08-10,2029,8,10,friday +2029-08-12,2029,8,12,sunday +2029-08-13,2029,8,13,monday +2029-08-14,2029,8,14,tuesday +2029-08-15,2029,8,15,wednesday +2029-08-16,2029,8,16,thursday +2029-08-18,2029,8,18,saturday +2029-08-19,2029,8,19,sunday +2029-08-20,2029,8,20,monday +2029-08-22,2029,8,22,wednesday +2029-08-23,2029,8,23,thursday +2029-08-26,2029,8,26,sunday +2029-08-28,2029,8,28,tuesday +2029-08-29,2029,8,29,wednesday +2029-08-31,2029,8,31,friday +2029-09-01,2029,9,1,saturday +2029-09-04,2029,9,4,tuesday +2029-09-08,2029,9,8,saturday +2029-09-09,2029,9,9,sunday +2029-09-12,2029,9,12,wednesday +2029-09-16,2029,9,16,sunday +2029-09-19,2029,9,19,wednesday +2029-09-21,2029,9,21,friday +2029-09-22,2029,9,22,saturday +2029-09-24,2029,9,24,monday +2029-09-26,2029,9,26,wednesday +2029-09-27,2029,9,27,thursday +2029-09-30,2029,9,30,sunday +2029-10-01,2029,10,1,monday +2029-10-03,2029,10,3,wednesday +2029-10-04,2029,10,4,thursday +2029-10-07,2029,10,7,sunday +2029-10-13,2029,10,13,saturday +2029-10-19,2029,10,19,friday +2029-10-20,2029,10,20,saturday +2029-10-23,2029,10,23,tuesday +2029-10-26,2029,10,26,friday +2029-10-30,2029,10,30,tuesday +2029-10-31,2029,10,31,wednesday +2029-11-04,2029,11,4,sunday +2029-11-05,2029,11,5,monday +2029-11-06,2029,11,6,tuesday +2029-11-09,2029,11,9,friday +2029-11-10,2029,11,10,saturday +2029-11-18,2029,11,18,sunday +2029-11-19,2029,11,19,monday +2029-11-20,2029,11,20,tuesday +2029-11-22,2029,11,22,thursday +2029-11-25,2029,11,25,sunday +2029-11-26,2029,11,26,monday +2029-11-28,2029,11,28,wednesday +2029-11-30,2029,11,30,friday +2029-12-03,2029,12,3,monday +2029-12-05,2029,12,5,wednesday +2029-12-07,2029,12,7,friday +2029-12-08,2029,12,8,saturday +2029-12-09,2029,12,9,sunday +2029-12-11,2029,12,11,tuesday +2029-12-15,2029,12,15,saturday +2029-12-16,2029,12,16,sunday +2029-12-18,2029,12,18,tuesday +2029-12-19,2029,12,19,wednesday +2029-12-20,2029,12,20,thursday +2029-12-21,2029,12,21,friday +2029-12-22,2029,12,22,saturday +2029-12-23,2029,12,23,sunday +2029-12-24,2029,12,24,monday +2029-12-25,2029,12,25,tuesday +2029-12-26,2029,12,26,wednesday +2029-12-30,2029,12,30,sunday +2030-01-01,2030,1,1,tuesday +2030-01-04,2030,1,4,friday +2030-01-05,2030,1,5,saturday +2030-01-07,2030,1,7,monday +2030-01-12,2030,1,12,saturday +2030-01-13,2030,1,13,sunday +2030-01-16,2030,1,16,wednesday +2030-01-18,2030,1,18,friday +2030-01-20,2030,1,20,sunday +2030-01-21,2030,1,21,monday +2028-11-15,2028,11,15,wednesday +2028-11-18,2028,11,18,saturday +2028-11-21,2028,11,21,tuesday +2028-11-23,2028,11,23,thursday +2028-11-26,2028,11,26,sunday +2028-11-27,2028,11,27,monday +2028-11-28,2028,11,28,tuesday +2028-12-01,2028,12,1,friday +2028-12-02,2028,12,2,saturday +2028-12-04,2028,12,4,monday +2028-12-05,2028,12,5,tuesday +2028-12-06,2028,12,6,wednesday +2028-12-07,2028,12,7,thursday +2028-12-08,2028,12,8,friday +2028-12-10,2028,12,10,sunday +2028-12-13,2028,12,13,wednesday +2028-12-14,2028,12,14,thursday +2028-12-15,2028,12,15,friday +2028-12-20,2028,12,20,wednesday +2028-12-23,2028,12,23,saturday +2028-12-24,2028,12,24,sunday +2028-12-25,2028,12,25,monday +2028-12-26,2028,12,26,tuesday +2028-12-28,2028,12,28,thursday +2028-12-30,2028,12,30,saturday +2028-12-31,2028,12,31,sunday +2029-01-02,2029,1,2,tuesday +2029-01-04,2029,1,4,thursday +2029-01-05,2029,1,5,friday +2029-01-07,2029,1,7,sunday +2029-01-09,2029,1,9,tuesday +2029-01-10,2029,1,10,wednesday +2029-01-11,2029,1,11,thursday +2029-01-12,2029,1,12,friday +2029-01-14,2029,1,14,sunday +2029-01-15,2029,1,15,monday +2029-01-16,2029,1,16,tuesday +2029-01-18,2029,1,18,thursday +2029-01-20,2029,1,20,saturday +2029-01-22,2029,1,22,monday +2029-01-24,2029,1,24,wednesday +2029-01-25,2029,1,25,thursday +2029-01-27,2029,1,27,saturday +2029-01-29,2029,1,29,monday +2029-01-30,2029,1,30,tuesday +2029-02-02,2029,2,2,friday +2029-02-06,2029,2,6,tuesday +2029-02-07,2029,2,7,wednesday +2029-02-09,2029,2,9,friday +2029-02-10,2029,2,10,saturday +2029-02-11,2029,2,11,sunday +2029-02-12,2029,2,12,monday +2029-02-13,2029,2,13,tuesday +2029-02-14,2029,2,14,wednesday +2029-02-15,2029,2,15,thursday +2029-02-16,2029,2,16,friday +2029-02-20,2029,2,20,tuesday +2029-02-21,2029,2,21,wednesday +2029-02-22,2029,2,22,thursday +2029-02-23,2029,2,23,friday +2029-02-24,2029,2,24,saturday +2029-02-25,2029,2,25,sunday +2029-02-26,2029,2,26,monday +2029-02-27,2029,2,27,tuesday +2029-02-28,2029,2,28,wednesday +2029-03-01,2029,3,1,thursday +2029-03-02,2029,3,2,friday +2029-03-03,2029,3,3,saturday +2029-03-04,2029,3,4,sunday +2029-03-05,2029,3,5,monday +2029-03-07,2029,3,7,wednesday +2029-03-08,2029,3,8,thursday +2029-03-09,2029,3,9,friday +2029-03-10,2029,3,10,saturday +2029-03-11,2029,3,11,sunday +2029-03-12,2029,3,12,monday +2029-03-17,2029,3,17,saturday +2029-03-19,2029,3,19,monday +2029-03-22,2029,3,22,thursday +2029-03-24,2029,3,24,saturday +2029-03-26,2029,3,26,monday +2029-03-27,2029,3,27,tuesday +2029-03-30,2029,3,30,friday +2029-03-31,2029,3,31,saturday +2029-04-02,2029,4,2,monday +2029-04-04,2029,4,4,wednesday +2029-04-05,2029,4,5,thursday +2029-04-07,2029,4,7,saturday +2029-04-09,2029,4,9,monday +2029-04-10,2029,4,10,tuesday +2029-04-11,2029,4,11,wednesday +2029-04-15,2029,4,15,sunday +2029-04-17,2029,4,17,tuesday +2029-04-18,2029,4,18,wednesday +2029-04-21,2029,4,21,saturday +2029-04-22,2029,4,22,sunday +2029-04-23,2029,4,23,monday +2029-04-24,2029,4,24,tuesday +2029-04-26,2029,4,26,thursday +2029-04-27,2029,4,27,friday +2029-04-30,2029,4,30,monday +2029-05-04,2029,5,4,friday +2029-05-07,2029,5,7,monday +2029-05-09,2029,5,9,wednesday +2029-05-11,2029,5,11,friday +2029-05-12,2029,5,12,saturday +2029-05-13,2029,5,13,sunday +2029-05-14,2029,5,14,monday +2029-05-15,2029,5,15,tuesday +2029-05-17,2029,5,17,thursday +2029-05-18,2029,5,18,friday +2029-05-20,2029,5,20,sunday +2029-05-21,2029,5,21,monday +2029-05-24,2029,5,24,thursday +2029-05-25,2029,5,25,friday +2029-05-28,2029,5,28,monday +2029-05-30,2029,5,30,wednesday +2029-05-31,2029,5,31,thursday +2029-06-01,2029,6,1,friday +2029-06-02,2029,6,2,saturday +2029-06-04,2029,6,4,monday +2029-06-06,2029,6,6,wednesday +2029-06-07,2029,6,7,thursday +2029-06-08,2029,6,8,friday +2029-06-10,2029,6,10,sunday +2029-06-14,2029,6,14,thursday +2029-06-15,2029,6,15,friday +2029-06-16,2029,6,16,saturday +2029-06-18,2029,6,18,monday +2029-06-20,2029,6,20,wednesday +2029-06-24,2029,6,24,sunday +2029-06-29,2029,6,29,friday +2029-07-05,2029,7,5,thursday +2029-07-06,2029,7,6,friday +2029-07-07,2029,7,7,saturday +2029-07-10,2029,7,10,tuesday +2029-07-13,2029,7,13,friday +2029-07-14,2029,7,14,saturday +2029-07-15,2029,7,15,sunday +2029-07-16,2029,7,16,monday +2029-07-21,2029,7,21,saturday +2029-07-22,2029,7,22,sunday +2029-07-24,2029,7,24,tuesday +2029-07-29,2029,7,29,sunday +2029-07-30,2029,7,30,monday +2029-07-31,2029,7,31,tuesday +2029-08-01,2029,8,1,wednesday +2029-08-03,2029,8,3,friday +2029-08-06,2029,8,6,monday +2029-08-07,2029,8,7,tuesday +2029-08-11,2029,8,11,saturday +2029-08-17,2029,8,17,friday +2029-08-21,2029,8,21,tuesday +2029-08-24,2029,8,24,friday +2029-08-25,2029,8,25,saturday +2029-08-27,2029,8,27,monday +2029-08-30,2029,8,30,thursday +2029-09-02,2029,9,2,sunday +2029-09-03,2029,9,3,monday +2029-09-05,2029,9,5,wednesday +2029-09-06,2029,9,6,thursday +2029-09-07,2029,9,7,friday +2029-09-10,2029,9,10,monday +2029-09-11,2029,9,11,tuesday +2029-09-13,2029,9,13,thursday +2029-09-14,2029,9,14,friday +2029-09-15,2029,9,15,saturday +2029-09-17,2029,9,17,monday +2029-09-18,2029,9,18,tuesday +2029-09-20,2029,9,20,thursday +2029-09-23,2029,9,23,sunday +2029-09-25,2029,9,25,tuesday +2029-09-28,2029,9,28,friday +2029-09-29,2029,9,29,saturday +2029-10-02,2029,10,2,tuesday +2029-10-05,2029,10,5,friday +2029-10-06,2029,10,6,saturday +2029-10-08,2029,10,8,monday +2029-10-09,2029,10,9,tuesday +2029-10-10,2029,10,10,wednesday +2029-10-11,2029,10,11,thursday +2029-10-12,2029,10,12,friday +2029-10-14,2029,10,14,sunday +2029-10-15,2029,10,15,monday +2029-10-16,2029,10,16,tuesday +2029-10-17,2029,10,17,wednesday +2029-10-18,2029,10,18,thursday +2029-10-21,2029,10,21,sunday +2029-10-22,2029,10,22,monday +2029-10-24,2029,10,24,wednesday +2029-10-25,2029,10,25,thursday +2029-10-27,2029,10,27,saturday +2029-10-28,2029,10,28,sunday +2029-10-29,2029,10,29,monday +2029-11-01,2029,11,1,thursday +2029-11-02,2029,11,2,friday +2029-11-03,2029,11,3,saturday +2029-11-07,2029,11,7,wednesday +2029-11-08,2029,11,8,thursday +2029-11-11,2029,11,11,sunday +2029-11-12,2029,11,12,monday +2029-11-13,2029,11,13,tuesday +2029-11-14,2029,11,14,wednesday +2029-11-15,2029,11,15,thursday +2029-11-16,2029,11,16,friday +2029-11-17,2029,11,17,saturday +2029-11-21,2029,11,21,wednesday +2029-11-23,2029,11,23,friday +2029-11-24,2029,11,24,saturday +2029-11-27,2029,11,27,tuesday +2029-11-29,2029,11,29,thursday +2029-12-01,2029,12,1,saturday +2029-12-02,2029,12,2,sunday +2030-01-22,2030,1,22,tuesday +2030-01-23,2030,1,23,wednesday +2030-01-27,2030,1,27,sunday +2030-01-31,2030,1,31,thursday +2030-02-02,2030,2,2,saturday +2030-02-05,2030,2,5,tuesday +2030-02-06,2030,2,6,wednesday +2030-02-09,2030,2,9,saturday +2030-02-11,2030,2,11,monday +2030-02-14,2030,2,14,thursday +2030-02-15,2030,2,15,friday +2030-02-16,2030,2,16,saturday +2030-02-17,2030,2,17,sunday +2030-02-20,2030,2,20,wednesday +2030-02-23,2030,2,23,saturday +2030-02-24,2030,2,24,sunday +2030-02-27,2030,2,27,wednesday +2030-03-01,2030,3,1,friday +2030-03-04,2030,3,4,monday +2030-03-05,2030,3,5,tuesday +2030-03-07,2030,3,7,thursday +2030-03-08,2030,3,8,friday +2030-03-10,2030,3,10,sunday +2030-03-11,2030,3,11,monday +2030-03-12,2030,3,12,tuesday +2030-03-13,2030,3,13,wednesday +2030-03-15,2030,3,15,friday +2030-03-16,2030,3,16,saturday +2030-03-17,2030,3,17,sunday +2030-03-18,2030,3,18,monday +2030-03-19,2030,3,19,tuesday +2030-03-22,2030,3,22,friday +2030-03-24,2030,3,24,sunday +2030-03-26,2030,3,26,tuesday +2030-03-27,2030,3,27,wednesday +2030-03-28,2030,3,28,thursday +2030-03-29,2030,3,29,friday +2030-03-30,2030,3,30,saturday +2030-04-02,2030,4,2,tuesday +2030-04-04,2030,4,4,thursday +2030-04-05,2030,4,5,friday +2030-04-11,2030,4,11,thursday +2030-04-13,2030,4,13,saturday +2030-04-15,2030,4,15,monday +2030-04-16,2030,4,16,tuesday +2030-04-18,2030,4,18,thursday +2030-04-19,2030,4,19,friday +2030-04-20,2030,4,20,saturday +2030-04-25,2030,4,25,thursday +2030-04-28,2030,4,28,sunday +2030-04-29,2030,4,29,monday +2030-05-01,2030,5,1,wednesday +2030-05-06,2030,5,6,monday +2030-05-07,2030,5,7,tuesday +2030-05-11,2030,5,11,saturday +2030-05-15,2030,5,15,wednesday +2030-05-17,2030,5,17,friday +2030-05-18,2030,5,18,saturday +2030-05-21,2030,5,21,tuesday +2030-05-22,2030,5,22,wednesday +2030-05-25,2030,5,25,saturday +2030-05-28,2030,5,28,tuesday +2030-05-29,2030,5,29,wednesday +2030-05-30,2030,5,30,thursday +2030-06-01,2030,6,1,saturday +2030-06-04,2030,6,4,tuesday +2030-06-05,2030,6,5,wednesday +2030-06-06,2030,6,6,thursday +2030-06-08,2030,6,8,saturday +2030-06-13,2030,6,13,thursday +2030-06-22,2030,6,22,saturday +2030-06-25,2030,6,25,tuesday +2030-06-26,2030,6,26,wednesday +2030-06-27,2030,6,27,thursday +2030-06-30,2030,6,30,sunday +2030-07-01,2030,7,1,monday +2030-07-02,2030,7,2,tuesday +2030-07-05,2030,7,5,friday +2030-07-06,2030,7,6,saturday +2030-07-09,2030,7,9,tuesday +2030-07-14,2030,7,14,sunday +2030-07-16,2030,7,16,tuesday +2030-07-20,2030,7,20,saturday +2030-07-23,2030,7,23,tuesday +2030-07-24,2030,7,24,wednesday +2030-07-25,2030,7,25,thursday +2030-07-26,2030,7,26,friday +2030-07-28,2030,7,28,sunday +2030-07-30,2030,7,30,tuesday +2030-07-31,2030,7,31,wednesday +2030-08-02,2030,8,2,friday +2030-08-06,2030,8,6,tuesday +2030-08-10,2030,8,10,saturday +2030-08-12,2030,8,12,monday +2030-08-14,2030,8,14,wednesday +2030-08-17,2030,8,17,saturday +2030-08-18,2030,8,18,sunday +2030-08-24,2030,8,24,saturday +2030-08-26,2030,8,26,monday +2030-08-28,2030,8,28,wednesday +2030-08-29,2030,8,29,thursday +2030-08-30,2030,8,30,friday +2030-09-01,2030,9,1,sunday +2030-09-02,2030,9,2,monday +2030-09-03,2030,9,3,tuesday +2030-09-05,2030,9,5,thursday +2030-09-07,2030,9,7,saturday +2030-09-08,2030,9,8,sunday +2030-09-11,2030,9,11,wednesday +2030-09-12,2030,9,12,thursday +2030-09-13,2030,9,13,friday +2030-09-15,2030,9,15,sunday +2030-09-17,2030,9,17,tuesday +2030-09-18,2030,9,18,wednesday +2030-09-23,2030,9,23,monday +2030-09-24,2030,9,24,tuesday +2030-09-26,2030,9,26,thursday +2030-09-27,2030,9,27,friday +2030-09-28,2030,9,28,saturday +2030-09-29,2030,9,29,sunday +2030-09-30,2030,9,30,monday +2030-10-01,2030,10,1,tuesday +2030-10-02,2030,10,2,wednesday +2030-10-03,2030,10,3,thursday +2030-10-07,2030,10,7,monday +2030-10-19,2030,10,19,saturday +2030-10-22,2030,10,22,tuesday +2030-10-23,2030,10,23,wednesday +2030-10-28,2030,10,28,monday +2030-10-29,2030,10,29,tuesday +2030-10-31,2030,10,31,thursday +2030-11-01,2030,11,1,friday +2030-11-04,2030,11,4,monday +2030-11-05,2030,11,5,tuesday +2030-11-07,2030,11,7,thursday +2030-11-11,2030,11,11,monday +2030-11-15,2030,11,15,friday +2030-11-16,2030,11,16,saturday +2030-11-17,2030,11,17,sunday +2030-11-19,2030,11,19,tuesday +2030-11-20,2030,11,20,wednesday +2030-11-21,2030,11,21,thursday +2030-11-22,2030,11,22,friday +2030-11-25,2030,11,25,monday +2030-11-27,2030,11,27,wednesday +2030-11-29,2030,11,29,friday +2030-11-30,2030,11,30,saturday +2030-12-11,2030,12,11,wednesday +2030-12-14,2030,12,14,saturday +2030-12-15,2030,12,15,sunday +2030-12-19,2030,12,19,thursday +2030-12-20,2030,12,20,friday +2030-12-21,2030,12,21,saturday +2030-12-23,2030,12,23,monday +2030-12-30,2030,12,30,monday +2029-12-04,2029,12,4,tuesday +2029-12-06,2029,12,6,thursday +2029-12-10,2029,12,10,monday +2029-12-12,2029,12,12,wednesday +2029-12-13,2029,12,13,thursday +2029-12-14,2029,12,14,friday +2029-12-17,2029,12,17,monday +2029-12-27,2029,12,27,thursday +2029-12-28,2029,12,28,friday +2029-12-29,2029,12,29,saturday +2029-12-31,2029,12,31,monday +2030-01-02,2030,1,2,wednesday +2030-01-03,2030,1,3,thursday +2030-01-06,2030,1,6,sunday +2030-01-08,2030,1,8,tuesday +2030-01-09,2030,1,9,wednesday +2030-01-10,2030,1,10,thursday +2030-01-11,2030,1,11,friday +2030-01-14,2030,1,14,monday +2030-01-15,2030,1,15,tuesday +2030-01-17,2030,1,17,thursday +2030-01-19,2030,1,19,saturday +2030-01-24,2030,1,24,thursday +2030-01-25,2030,1,25,friday +2030-01-26,2030,1,26,saturday +2030-01-28,2030,1,28,monday +2030-01-29,2030,1,29,tuesday +2030-01-30,2030,1,30,wednesday +2030-02-01,2030,2,1,friday +2030-02-03,2030,2,3,sunday +2030-02-04,2030,2,4,monday +2030-02-07,2030,2,7,thursday +2030-02-08,2030,2,8,friday +2030-02-10,2030,2,10,sunday +2030-02-12,2030,2,12,tuesday +2030-02-13,2030,2,13,wednesday +2030-02-18,2030,2,18,monday +2030-02-19,2030,2,19,tuesday +2030-02-21,2030,2,21,thursday +2030-02-22,2030,2,22,friday +2030-02-25,2030,2,25,monday +2030-02-26,2030,2,26,tuesday +2030-02-28,2030,2,28,thursday +2030-03-02,2030,3,2,saturday +2030-03-03,2030,3,3,sunday +2030-03-06,2030,3,6,wednesday +2030-03-09,2030,3,9,saturday +2030-03-14,2030,3,14,thursday +2030-03-20,2030,3,20,wednesday +2030-03-21,2030,3,21,thursday +2030-03-23,2030,3,23,saturday +2030-03-25,2030,3,25,monday +2030-03-31,2030,3,31,sunday +2030-04-01,2030,4,1,monday +2030-04-03,2030,4,3,wednesday +2030-04-06,2030,4,6,saturday +2030-04-07,2030,4,7,sunday +2030-04-08,2030,4,8,monday +2030-04-09,2030,4,9,tuesday +2030-04-10,2030,4,10,wednesday +2030-04-12,2030,4,12,friday +2030-04-14,2030,4,14,sunday +2030-04-17,2030,4,17,wednesday +2030-04-21,2030,4,21,sunday +2030-04-22,2030,4,22,monday +2030-04-23,2030,4,23,tuesday +2030-04-24,2030,4,24,wednesday +2030-04-26,2030,4,26,friday +2030-04-27,2030,4,27,saturday +2030-04-30,2030,4,30,tuesday +2030-05-02,2030,5,2,thursday +2030-05-03,2030,5,3,friday +2030-05-04,2030,5,4,saturday +2030-05-05,2030,5,5,sunday +2030-05-08,2030,5,8,wednesday +2030-05-09,2030,5,9,thursday +2030-05-10,2030,5,10,friday +2030-05-12,2030,5,12,sunday +2030-05-13,2030,5,13,monday +2030-05-14,2030,5,14,tuesday +2030-05-16,2030,5,16,thursday +2030-05-19,2030,5,19,sunday +2030-05-20,2030,5,20,monday +2030-05-23,2030,5,23,thursday +2030-05-24,2030,5,24,friday +2030-05-26,2030,5,26,sunday +2030-05-27,2030,5,27,monday +2030-05-31,2030,5,31,friday +2030-06-02,2030,6,2,sunday +2030-06-03,2030,6,3,monday +2030-06-07,2030,6,7,friday +2030-06-09,2030,6,9,sunday +2030-06-10,2030,6,10,monday +2030-06-11,2030,6,11,tuesday +2030-06-12,2030,6,12,wednesday +2030-06-14,2030,6,14,friday +2030-06-15,2030,6,15,saturday +2030-06-16,2030,6,16,sunday +2030-06-17,2030,6,17,monday +2030-06-18,2030,6,18,tuesday +2030-06-19,2030,6,19,wednesday +2030-06-20,2030,6,20,thursday +2030-06-21,2030,6,21,friday +2030-06-23,2030,6,23,sunday +2030-06-24,2030,6,24,monday +2030-06-28,2030,6,28,friday +2030-06-29,2030,6,29,saturday +2030-07-03,2030,7,3,wednesday +2030-07-04,2030,7,4,thursday +2030-07-07,2030,7,7,sunday +2030-07-08,2030,7,8,monday +2030-07-10,2030,7,10,wednesday +2030-07-11,2030,7,11,thursday +2030-07-12,2030,7,12,friday +2030-07-13,2030,7,13,saturday +2030-07-15,2030,7,15,monday +2030-07-17,2030,7,17,wednesday +2030-07-18,2030,7,18,thursday +2030-07-19,2030,7,19,friday +2030-07-21,2030,7,21,sunday +2030-07-22,2030,7,22,monday +2030-07-27,2030,7,27,saturday +2030-07-29,2030,7,29,monday +2030-08-01,2030,8,1,thursday +2030-08-03,2030,8,3,saturday +2030-08-04,2030,8,4,sunday +2030-08-05,2030,8,5,monday +2030-08-07,2030,8,7,wednesday +2030-08-08,2030,8,8,thursday +2030-08-09,2030,8,9,friday +2030-08-11,2030,8,11,sunday +2030-08-13,2030,8,13,tuesday +2030-08-15,2030,8,15,thursday +2030-08-16,2030,8,16,friday +2030-08-19,2030,8,19,monday +2030-08-20,2030,8,20,tuesday +2030-08-21,2030,8,21,wednesday +2030-08-22,2030,8,22,thursday +2030-08-23,2030,8,23,friday +2030-08-25,2030,8,25,sunday +2030-08-27,2030,8,27,tuesday +2030-08-31,2030,8,31,saturday +2030-09-04,2030,9,4,wednesday +2030-09-06,2030,9,6,friday +2030-09-09,2030,9,9,monday +2030-09-10,2030,9,10,tuesday +2030-09-14,2030,9,14,saturday +2030-09-16,2030,9,16,monday +2030-09-19,2030,9,19,thursday +2030-09-20,2030,9,20,friday +2030-09-21,2030,9,21,saturday +2030-09-22,2030,9,22,sunday +2030-09-25,2030,9,25,wednesday +2030-10-04,2030,10,4,friday +2030-10-05,2030,10,5,saturday +2030-10-06,2030,10,6,sunday +2030-10-08,2030,10,8,tuesday +2030-10-09,2030,10,9,wednesday +2030-10-10,2030,10,10,thursday +2030-10-11,2030,10,11,friday +2030-10-12,2030,10,12,saturday +2030-10-13,2030,10,13,sunday +2030-10-14,2030,10,14,monday +2030-10-15,2030,10,15,tuesday +2030-10-16,2030,10,16,wednesday +2030-10-17,2030,10,17,thursday +2030-10-18,2030,10,18,friday +2030-10-20,2030,10,20,sunday +2030-10-21,2030,10,21,monday +2030-10-24,2030,10,24,thursday +2030-10-25,2030,10,25,friday +2030-10-26,2030,10,26,saturday +2030-10-27,2030,10,27,sunday +2030-10-30,2030,10,30,wednesday +2030-11-02,2030,11,2,saturday +2030-11-03,2030,11,3,sunday +2030-11-06,2030,11,6,wednesday +2030-11-08,2030,11,8,friday +2030-11-09,2030,11,9,saturday +2030-11-10,2030,11,10,sunday +2030-11-12,2030,11,12,tuesday +2030-11-13,2030,11,13,wednesday +2030-11-14,2030,11,14,thursday +2030-11-18,2030,11,18,monday +2030-11-23,2030,11,23,saturday +2030-11-24,2030,11,24,sunday +2030-11-26,2030,11,26,tuesday +2030-11-28,2030,11,28,thursday +2030-12-01,2030,12,1,sunday +2030-12-02,2030,12,2,monday +2030-12-03,2030,12,3,tuesday +2030-12-04,2030,12,4,wednesday +2030-12-05,2030,12,5,thursday +2030-12-06,2030,12,6,friday +2030-12-07,2030,12,7,saturday +2030-12-08,2030,12,8,sunday +2030-12-09,2030,12,9,monday +2030-12-10,2030,12,10,tuesday +2030-12-12,2030,12,12,thursday +2030-12-13,2030,12,13,friday +2030-12-16,2030,12,16,monday +2030-12-17,2030,12,17,tuesday +2030-12-18,2030,12,18,wednesday +2030-12-22,2030,12,22,sunday +2030-12-24,2030,12,24,tuesday +2030-12-25,2030,12,25,wednesday +2030-12-26,2030,12,26,thursday +2030-12-27,2030,12,27,friday +2030-12-28,2030,12,28,saturday +2030-12-29,2030,12,29,sunday +2030-12-31,2030,12,31,tuesday diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv new file mode 100644 index 0000000..9383505 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv @@ -0,0 +1,19 @@ +"lesson_id","title","duration_min","course_id" +2,JOIN,15,1 +3,WHERE и фильтрация,12,1 +4,Агрегации и GROUP BY,18,1 +6,Pandas основы,20,2 +7,NumPy,15,2 +8,Визуализация данных,22,2 +9,Основы BI,12,3 +10,Метрики и дашборды,18,3 +13,Сводные таблицы,18,4 +16,Линейная регрессия,30,5 +18,Кластеризация,25,5 +1,SELECT и FROM,10,1 +5,Подзапросы,25,1 +11,Power BI введение,20,3 +12,Формулы в Excel,15,4 +14,Графики и диаграммы,14,4 +15,Введение в ML,20,5 +17,Классификация,28,5 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv new file mode 100644 index 0000000..529a745 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv @@ -0,0 +1,21 @@ +"user_id","name","age","email","registration_date" +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +0300 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +0300 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +0300 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +0300 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +0300 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +0300 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +0300 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 +0300 +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +0300 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +0300 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +0300 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +0300 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +0300 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +0300 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +0300 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +0300 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +0300 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +0300 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +0300 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv new file mode 100644 index 0000000..38856eb --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv @@ -0,0 +1,46 @@ +"user_id","course_id","enrolled_at" +2,1,2023-01-20 00:00:00.000 +0300 +3,2,2023-03-05 00:00:00.000 +0300 +6,1,2023-01-06 00:00:00.000 +0300 +6,2,2023-01-10 00:00:00.000 +0300 +6,4,2023-02-05 00:00:00.000 +0300 +7,1,2023-01-13 00:00:00.000 +0300 +7,3,2023-03-10 00:00:00.000 +0300 +8,3,2023-01-22 00:00:00.000 +0300 +13,2,2023-01-19 00:00:00.000 +0300 +13,4,2023-02-20 00:00:00.000 +0300 +16,2,2023-02-16 00:00:00.000 +0300 +16,4,2023-02-16 00:00:00.000 +0300 +19,1,2023-04-11 00:00:00.000 +0300 +19,2,2023-04-11 00:00:00.000 +0300 +2,3,2023-03-15 00:00:00.000 +0300 +4,2,2023-03-10 00:00:00.000 +0300 +7,2,2023-02-01 00:00:00.000 +0300 +9,1,2023-02-15 00:00:00.000 +0300 +18,4,2023-04-05 00:00:00.000 +0300 +4,3,2023-04-01 00:00:00.000 +0300 +8,1,2023-01-22 00:00:00.000 +0300 +8,2,2023-01-22 00:00:00.000 +0300 +9,4,2023-02-12 00:00:00.000 +0300 +10,1,2023-04-16 00:00:00.000 +0300 +18,1,2023-04-02 00:00:00.000 +0300 +3,1,2023-02-25 00:00:00.000 +0300 +6,3,2023-03-08 00:00:00.000 +0300 +1,2,2023-02-01 00:00:00.000 +0300 +11,2,2023-04-21 00:00:00.000 +0300 +14,2,2023-01-09 00:00:00.000 +0300 +14,3,2023-01-09 00:00:00.000 +0300 +14,5,2023-03-20 00:00:00.000 +0300 +17,3,2023-03-22 00:00:00.000 +0300 +20,2,2023-04-19 00:00:00.000 +0300 +12,3,2023-03-10 00:00:00.000 +0300 +1,1,2023-01-15 00:00:00.000 +0300 +12,1,2023-01-16 00:00:00.000 +0300 +14,1,2023-01-09 00:00:00.000 +0300 +14,4,2023-02-01 00:00:00.000 +0300 +15,1,2023-01-11 00:00:00.000 +0300 +15,3,2023-03-05 00:00:00.000 +0300 +17,5,2023-03-25 00:00:00.000 +0300 +5,5,2023-03-20 00:00:00.000 +0300 +1,4,2023-02-10 00:00:00.000 +0300 +15,2,2023-02-01 00:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv new file mode 100644 index 0000000..3a43417 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv @@ -0,0 +1,59 @@ +"user_id","lesson_id","course_id","viewed_at" +1,2,1,2023-01-16 10:15:00.000 +0300 +1,3,1,2023-01-17 09:00:00.000 +0300 +1,4,1,2023-01-17 09:30:00.000 +0300 +1,4,1,2023-01-18 10:00:00.000 +0300 +1,6,2,2023-02-02 14:00:00.000 +0300 +1,13,4,2023-02-15 10:30:00.000 +0300 +5,16,5,2023-03-22 11:00:00.000 +0300 +12,2,1,2023-01-17 10:20:00.000 +0300 +12,3,1,2023-04-10 11:30:00.000 +0300 +14,2,1,2023-01-09 08:15:00.000 +0300 +14,4,1,2023-01-10 10:30:00.000 +0300 +14,6,2,2023-01-10 14:00:00.000 +0300 +14,9,3,2023-01-10 08:00:00.000 +0300 +1,4,1,2023-01-17 11:00:00.000 +0300 +12,9,3,2023-03-12 10:00:00.000 +0300 +14,3,1,2023-01-10 10:00:00.000 +0300 +14,7,2,2023-01-11 10:00:00.000 +0300 +14,8,2,2023-01-12 11:00:00.000 +0300 +1,12,4,2023-02-15 10:00:00.000 +0300 +12,1,1,2023-01-17 10:00:00.000 +0300 +14,5,1,2023-01-11 09:00:00.000 +0300 +1,1,1,2023-01-16 10:00:00.000 +0300 +5,15,5,2023-03-21 10:00:00.000 +0300 +12,1,1,2023-04-10 11:00:00.000 +0300 +14,1,1,2023-01-09 08:00:00.000 +0300 +3,6,2,2023-03-06 12:00:00.000 +0300 +4,9,3,2023-04-02 10:00:00.000 +0300 +4,6,2,2023-03-12 14:00:00.000 +0300 +4,7,2,2023-03-13 09:00:00.000 +0300 +6,3,1,2023-01-08 09:00:00.000 +0300 +6,6,2,2023-01-11 14:00:00.000 +0300 +6,7,2,2023-01-12 10:00:00.000 +0300 +6,8,2,2023-01-13 11:00:00.000 +0300 +6,9,3,2023-03-09 11:00:00.000 +0300 +7,9,3,2023-03-11 14:00:00.000 +0300 +2,9,3,2023-03-16 10:00:00.000 +0300 +2,10,3,2023-03-16 10:30:00.000 +0300 +3,2,1,2023-02-26 11:20:00.000 +0300 +4,10,3,2023-04-03 10:00:00.000 +0300 +6,2,1,2023-01-07 10:15:00.000 +0300 +6,4,1,2023-01-08 09:30:00.000 +0300 +6,13,4,2023-02-06 09:30:00.000 +0300 +7,2,1,2023-01-14 10:20:00.000 +0300 +7,3,1,2023-01-15 09:00:00.000 +0300 +7,10,3,2023-03-11 14:30:00.000 +0300 +9,13,4,2023-02-13 10:30:00.000 +0300 +13,6,2,2023-01-20 14:00:00.000 +0300 +13,7,2,2023-01-21 10:00:00.000 +0300 +2,1,1,2023-01-21 09:00:00.000 +0300 +6,1,1,2023-01-07 10:00:00.000 +0300 +6,14,4,2023-02-07 10:00:00.000 +0300 +7,1,1,2023-01-14 10:00:00.000 +0300 +9,12,4,2023-02-13 10:00:00.000 +0300 +9,1,1,2023-02-16 11:00:00.000 +0300 +3,1,1,2023-02-26 11:00:00.000 +0300 +6,5,1,2023-01-09 10:00:00.000 +0300 +6,12,4,2023-02-06 09:00:00.000 +0300 +13,12,4,2023-02-21 09:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql index f2045ef..62c94cb 100644 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql @@ -1,3 +1,5 @@ +-- DIM + --DROP TABLE IF EXISTS mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; CREATE TABLE mnoskov.core_dim_user ON CLUSTER clickhouse_cluster @@ -58,4 +60,38 @@ ENGINE=ReplicatedMergeTree( '/clickhouse/tables/{shard}/mnoskov/core_dim_date', '{replica}' ) -ORDER BY date; \ No newline at end of file +ORDER BY date; + + +-- FACT + +--DROP TABLE IF EXISTS mnoskov.core_fact_lesson_views; +CREATE TABLE mnoskov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + lesson_id UInt64, + course_id UInt64, + viewed_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_fact_lesson_views', + '{replica}' +) +PARTITION BY toYYYYMM(viewed_at) +ORDER BY (viewed_at, user_id); -- я если честно не знаю как с этой таблицей поступить в ORDER BY. Наставьте меня на путь истиный. + +--DROP TABLE IF EXISTS mnoskov.core_enrollments; +CREATE TABLE mnoskov.core_enrollments +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + course_id UInt64, + enrolled_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_enrollments', + '{replica}' +) +PARTITION BY toYYYYMM(enrolled_at) +ORDER BY (enrolled_at, user_id); \ No newline at end of file From 135ee82c14d9503b53e0aef44baba38c07963dbe Mon Sep 17 00:00:00 2001 From: mnoskov Date: Tue, 19 May 2026 15:50:11 +0300 Subject: [PATCH 156/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=202:=20=D0=9F=D0=BE=D0=B4=D0=B3?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B2=D1=8C=20=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D1=83=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=20lesson=5Fpopularity=5Fsummary.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../csv files/fact_lesson_views_202605191313.csv | 59 ----------- .../csv files/fact_lesson_views_202605191356.csv | 59 +++++++++++ .../task_2_lesson_popularity_summary.sql | 99 +++++++++++++++++++ 3 files changed, 158 insertions(+), 59 deletions(-) delete mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv deleted file mode 100644 index 3a43417..0000000 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191313.csv +++ /dev/null @@ -1,59 +0,0 @@ -"user_id","lesson_id","course_id","viewed_at" -1,2,1,2023-01-16 10:15:00.000 +0300 -1,3,1,2023-01-17 09:00:00.000 +0300 -1,4,1,2023-01-17 09:30:00.000 +0300 -1,4,1,2023-01-18 10:00:00.000 +0300 -1,6,2,2023-02-02 14:00:00.000 +0300 -1,13,4,2023-02-15 10:30:00.000 +0300 -5,16,5,2023-03-22 11:00:00.000 +0300 -12,2,1,2023-01-17 10:20:00.000 +0300 -12,3,1,2023-04-10 11:30:00.000 +0300 -14,2,1,2023-01-09 08:15:00.000 +0300 -14,4,1,2023-01-10 10:30:00.000 +0300 -14,6,2,2023-01-10 14:00:00.000 +0300 -14,9,3,2023-01-10 08:00:00.000 +0300 -1,4,1,2023-01-17 11:00:00.000 +0300 -12,9,3,2023-03-12 10:00:00.000 +0300 -14,3,1,2023-01-10 10:00:00.000 +0300 -14,7,2,2023-01-11 10:00:00.000 +0300 -14,8,2,2023-01-12 11:00:00.000 +0300 -1,12,4,2023-02-15 10:00:00.000 +0300 -12,1,1,2023-01-17 10:00:00.000 +0300 -14,5,1,2023-01-11 09:00:00.000 +0300 -1,1,1,2023-01-16 10:00:00.000 +0300 -5,15,5,2023-03-21 10:00:00.000 +0300 -12,1,1,2023-04-10 11:00:00.000 +0300 -14,1,1,2023-01-09 08:00:00.000 +0300 -3,6,2,2023-03-06 12:00:00.000 +0300 -4,9,3,2023-04-02 10:00:00.000 +0300 -4,6,2,2023-03-12 14:00:00.000 +0300 -4,7,2,2023-03-13 09:00:00.000 +0300 -6,3,1,2023-01-08 09:00:00.000 +0300 -6,6,2,2023-01-11 14:00:00.000 +0300 -6,7,2,2023-01-12 10:00:00.000 +0300 -6,8,2,2023-01-13 11:00:00.000 +0300 -6,9,3,2023-03-09 11:00:00.000 +0300 -7,9,3,2023-03-11 14:00:00.000 +0300 -2,9,3,2023-03-16 10:00:00.000 +0300 -2,10,3,2023-03-16 10:30:00.000 +0300 -3,2,1,2023-02-26 11:20:00.000 +0300 -4,10,3,2023-04-03 10:00:00.000 +0300 -6,2,1,2023-01-07 10:15:00.000 +0300 -6,4,1,2023-01-08 09:30:00.000 +0300 -6,13,4,2023-02-06 09:30:00.000 +0300 -7,2,1,2023-01-14 10:20:00.000 +0300 -7,3,1,2023-01-15 09:00:00.000 +0300 -7,10,3,2023-03-11 14:30:00.000 +0300 -9,13,4,2023-02-13 10:30:00.000 +0300 -13,6,2,2023-01-20 14:00:00.000 +0300 -13,7,2,2023-01-21 10:00:00.000 +0300 -2,1,1,2023-01-21 09:00:00.000 +0300 -6,1,1,2023-01-07 10:00:00.000 +0300 -6,14,4,2023-02-07 10:00:00.000 +0300 -7,1,1,2023-01-14 10:00:00.000 +0300 -9,12,4,2023-02-13 10:00:00.000 +0300 -9,1,1,2023-02-16 11:00:00.000 +0300 -3,1,1,2023-02-26 11:00:00.000 +0300 -6,5,1,2023-01-09 10:00:00.000 +0300 -6,12,4,2023-02-06 09:00:00.000 +0300 -13,12,4,2023-02-21 09:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv new file mode 100644 index 0000000..7be0742 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv @@ -0,0 +1,59 @@ +"user_id","lesson_id","course_id","viewed_at" +3,6,2,2023-03-06 12:00:00.000 +4,9,3,2023-04-02 10:00:00.000 +4,6,2,2023-03-12 14:00:00.000 +4,7,2,2023-03-13 09:00:00.000 +6,3,1,2023-01-08 09:00:00.000 +6,6,2,2023-01-11 14:00:00.000 +6,7,2,2023-01-12 10:00:00.000 +6,8,2,2023-01-13 11:00:00.000 +6,9,3,2023-03-09 11:00:00.000 +7,9,3,2023-03-11 14:00:00.000 +2,9,3,2023-03-16 10:00:00.000 +2,10,3,2023-03-16 10:30:00.000 +3,2,1,2023-02-26 11:20:00.000 +4,10,3,2023-04-03 10:00:00.000 +6,2,1,2023-01-07 10:15:00.000 +6,4,1,2023-01-08 09:30:00.000 +6,13,4,2023-02-06 09:30:00.000 +7,2,1,2023-01-14 10:20:00.000 +7,3,1,2023-01-15 09:00:00.000 +7,10,3,2023-03-11 14:30:00.000 +9,13,4,2023-02-13 10:30:00.000 +13,6,2,2023-01-20 14:00:00.000 +13,7,2,2023-01-21 10:00:00.000 +2,1,1,2023-01-21 09:00:00.000 +6,1,1,2023-01-07 10:00:00.000 +6,14,4,2023-02-07 10:00:00.000 +7,1,1,2023-01-14 10:00:00.000 +9,12,4,2023-02-13 10:00:00.000 +9,1,1,2023-02-16 11:00:00.000 +3,1,1,2023-02-26 11:00:00.000 +6,5,1,2023-01-09 10:00:00.000 +6,12,4,2023-02-06 09:00:00.000 +13,12,4,2023-02-21 09:00:00.000 +1,2,1,2023-01-16 10:15:00.000 +1,3,1,2023-01-17 09:00:00.000 +1,4,1,2023-01-17 09:30:00.000 +1,4,1,2023-01-18 10:00:00.000 +1,6,2,2023-02-02 14:00:00.000 +1,13,4,2023-02-15 10:30:00.000 +5,16,5,2023-03-22 11:00:00.000 +12,2,1,2023-01-17 10:20:00.000 +12,3,1,2023-04-10 11:30:00.000 +14,2,1,2023-01-09 08:15:00.000 +14,4,1,2023-01-10 10:30:00.000 +14,6,2,2023-01-10 14:00:00.000 +14,9,3,2023-01-10 08:00:00.000 +1,4,1,2023-01-17 11:00:00.000 +12,9,3,2023-03-12 10:00:00.000 +14,3,1,2023-01-10 10:00:00.000 +14,7,2,2023-01-11 10:00:00.000 +14,8,2,2023-01-12 11:00:00.000 +1,12,4,2023-02-15 10:00:00.000 +12,1,1,2023-01-17 10:00:00.000 +14,5,1,2023-01-11 09:00:00.000 +1,1,1,2023-01-16 10:00:00.000 +5,15,5,2023-03-21 10:00:00.000 +12,1,1,2023-04-10 11:00:00.000 +14,1,1,2023-01-09 08:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql new file mode 100644 index 0000000..35698e3 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql @@ -0,0 +1,99 @@ +-- Вариант 1(не работает): +CREATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + lesson_title String, + course_id UInt64, + course_title String, + total_views UInt64, + unique_users UInt64, + first_view DateTime, + last_view DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/lesson_popularity_summary', + '{replica}' +) +ORDER BY lesson_id; + +CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.lesson_popularity_summary +AS +WITH result_flv AS ( + SELECT + lesson_id, + count() AS total_views, + uniq(user_id) AS unique_users, + min(viewed_at) AS first_view, + max(viewed_at) AS last_view + FROM + mnoskov.core_fact_lesson_views flv + GROUP BY + lesson_id +) +SELECT + cdl.lesson_id AS lesson_id, + cdl.title AS lesson_title, + cdc.course_id AS course_id, + cdc.title AS course_title, + rflv.total_views AS total_views, + rflv.unique_users AS unique_users, + rflv.first_view AS first_view, + rflv.last_view AS last_view +FROM + mnoskov.core_dim_lesson cdl +JOIN + mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id +JOIN + result_flv rflv ON cdl.lesson_id = rflv.lesson_id; + + +-- Вариант 2(не работает): +CREATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + lesson_title String, + course_id UInt64, + course_title String, + total_views AggregateFunction(count), + unique_users AggregateFunction(uniq, UInt64), + first_view AggregateFunction(min, DateTime), + last_view AggregateFunction(max, DateTime) +) +ENGINE AggregatingMergeTree +ORDER BY lesson_id; + +CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.lesson_popularity_summary +AS +WITH result_flv AS ( + SELECT + lesson_id, + countState() AS total_views, + uniqState(user_id) AS unique_users, + minState(viewed_at) AS first_view, + maxState(viewed_at) AS last_view + FROM + mnoskov.core_fact_lesson_views flv + GROUP BY + lesson_id +) +SELECT + cdl.lesson_id AS lesson_id, + cdl.title AS lesson_title, + cdc.course_id AS course_id, + cdc.title AS course_title, + rflv.total_views AS total_views, + rflv.unique_users AS unique_users, + rflv.first_view AS first_view, + rflv.last_view AS last_view +FROM + mnoskov.core_dim_lesson cdl +JOIN + mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id +JOIN + result_flv rflv ON cdl.lesson_id = rflv.lesson_id; \ No newline at end of file From b95fc3ba9e8ec3ae57b50bbaa37994e8e12f0aea Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 20 May 2026 11:11:23 +0300 Subject: [PATCH 157/179] =?UTF-8?q?=D0=98=D0=97=D0=9C=D0=95=D0=9D=D0=95?= =?UTF-8?q?=D0=9D=D0=9E=20-=20=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=202:=20=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D1=8C=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=83=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=20lesson=5Fpopularity=5Fsummary.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task_2_lesson_popularity_summary.sql | 173 ++++++++++-------- 1 file changed, 101 insertions(+), 72 deletions(-) diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql index 35698e3..046c023 100644 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql @@ -1,56 +1,4 @@ --- Вариант 1(не работает): -CREATE TABLE mnoskov.lesson_popularity_summary -ON CLUSTER clickhouse_cluster -( - lesson_id UInt64, - lesson_title String, - course_id UInt64, - course_title String, - total_views UInt64, - unique_users UInt64, - first_view DateTime, - last_view DateTime -) -ENGINE = ReplicatedMergeTree( - '/clickhouse/tables/{shard}/mnoskov/lesson_popularity_summary', - '{replica}' -) -ORDER BY lesson_id; - -CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary -ON CLUSTER clickhouse_cluster -TO mnoskov.lesson_popularity_summary -AS -WITH result_flv AS ( - SELECT - lesson_id, - count() AS total_views, - uniq(user_id) AS unique_users, - min(viewed_at) AS first_view, - max(viewed_at) AS last_view - FROM - mnoskov.core_fact_lesson_views flv - GROUP BY - lesson_id -) -SELECT - cdl.lesson_id AS lesson_id, - cdl.title AS lesson_title, - cdc.course_id AS course_id, - cdc.title AS course_title, - rflv.total_views AS total_views, - rflv.unique_users AS unique_users, - rflv.first_view AS first_view, - rflv.last_view AS last_view -FROM - mnoskov.core_dim_lesson cdl -JOIN - mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id -JOIN - result_flv rflv ON cdl.lesson_id = rflv.lesson_id; - - --- Вариант 2(не работает): +--DROP TABLE mnoskov.lesson_popularity_summary ON CLUSTER clickhouse_cluster sync; CREATE TABLE mnoskov.lesson_popularity_summary ON CLUSTER clickhouse_cluster ( @@ -66,34 +14,115 @@ ON CLUSTER clickhouse_cluster ENGINE AggregatingMergeTree ORDER BY lesson_id; +--DROP VIEW mnoskov.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC; CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster TO mnoskov.lesson_popularity_summary AS -WITH result_flv AS ( - SELECT - lesson_id, - countState() AS total_views, - uniqState(user_id) AS unique_users, - minState(viewed_at) AS first_view, - maxState(viewed_at) AS last_view - FROM - mnoskov.core_fact_lesson_views flv - GROUP BY - lesson_id -) SELECT cdl.lesson_id AS lesson_id, cdl.title AS lesson_title, cdc.course_id AS course_id, cdc.title AS course_title, - rflv.total_views AS total_views, - rflv.unique_users AS unique_users, - rflv.first_view AS first_view, - rflv.last_view AS last_view + countIfState(flv.user_id != 0) AS total_views, -- я вообще ахуел когда узнал что такое существует(я про If у агрегаций). Я наткнулся на это когда осознал что кликхаус в join не ставит null, а ставит значение по умолчанию + uniqIfState(flv.user_id, flv.user_id != 0) AS unique_users, + minIfState(flv.viewed_at, flv.user_id != 0) AS first_view, + maxIfState(flv.viewed_at, flv.user_id != 0) AS last_view FROM - mnoskov.core_dim_lesson cdl + mnoskov.core_fact_lesson_views flv +RIGHT JOIN + mnoskov.core_dim_lesson cdl ON flv.lesson_id = cdl.lesson_id -- поидее нужно большую таблицу ставить в левую часть JOIN, но если я тут так сделаю с фактами, то запрос кореектно не будет работать JOIN mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id -JOIN - result_flv rflv ON cdl.lesson_id = rflv.lesson_id; \ No newline at end of file +GROUP BY + cdl.lesson_id, cdl.title, cdc.course_id, cdc.title; + + +SELECT * FROM mnoskov.core_fact_lesson_views; + +SELECT + lesson_id, + any(lesson_title), + any(course_id), + any(course_title), + countMerge(total_views) AS total_views, + uniqMerge(unique_users) AS uniq_users, + minMerge(first_view) AS first_view, + maxMerge(last_view) AS last_view +FROM + mnoskov.lesson_popularity_summary +GROUP BY + lesson_id +ORDER BY + lesson_id; +-- ЕБАТЬ КАК Я АХУЕЛ КОГДА ДОПЕР ДО ТОГО ЧТО ЭТО НУЖНО ЧИТАТЬ ТАКИМ СПОСОБОМ А НЕ ПРОСТО ЧЕРЕЗ ЗВЕЗДОЧКУ. У меня MV ебать сколько изменений потерпела чтобы заработать, а проблема возможно изначально была в чтении. +-- Правда оно читается через раз, то есть результат то нет, но думаю это проблема движка AggregatingMergeTree. +-- Это получается чтобы был удобный вывод, теперь поверх этого селекта можно сделать обычную VIEW? + +-- Заполненния и очищения: +TRUNCATE TABLE mnoskov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster sync; + +TRUNCATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster sync; + +INSERT INTO mnoskov.core_fact_lesson_views (user_id, lesson_id, course_id, viewed_at) +VALUES +(3, 6, 2, '2023-03-06 12:00:00'), +(4, 9, 3, '2023-04-02 10:00:00'), +(4, 6, 2, '2023-03-12 14:00:00'), +(4, 7, 2, '2023-03-13 09:00:00'), +(6, 3, 1, '2023-01-08 09:00:00'), +(6, 6, 2, '2023-01-11 14:00:00'), +(6, 7, 2, '2023-01-12 10:00:00'), +(6, 8, 2, '2023-01-13 11:00:00'), +(6, 9, 3, '2023-03-09 11:00:00'), +(7, 9, 3, '2023-03-11 14:00:00'), +(2, 9, 3, '2023-03-16 10:00:00'), +(2, 10, 3, '2023-03-16 10:30:00'), +(3, 2, 1, '2023-02-26 11:20:00'), +(4, 10, 3, '2023-04-03 10:00:00'), +(6, 2, 1, '2023-01-07 10:15:00'), +(6, 4, 1, '2023-01-08 09:30:00'), +(6, 13, 4, '2023-02-06 09:30:00'), +(7, 2, 1, '2023-01-14 10:20:00'), +(7, 3, 1, '2023-01-15 09:00:00'), +(7, 10, 3, '2023-03-11 14:30:00'), +(9, 13, 4, '2023-02-13 10:30:00'), +(13, 6, 2, '2023-01-20 14:00:00'), +(13, 7, 2, '2023-01-21 10:00:00'), +(2, 1, 1, '2023-01-21 09:00:00'), +(6, 1, 1, '2023-01-07 10:00:00'), +(6, 14, 4, '2023-02-07 10:00:00'), +(7, 1, 1, '2023-01-14 10:00:00'), +(9, 12, 4, '2023-02-13 10:00:00'), +(9, 1, 1, '2023-02-16 11:00:00'), +(3, 1, 1, '2023-02-26 11:00:00'), +(6, 5, 1, '2023-01-09 10:00:00'), +(6, 12, 4, '2023-02-06 09:00:00'), +(13, 12, 4, '2023-02-21 09:00:00'), +(1, 2, 1, '2023-01-16 10:15:00'), +(1, 3, 1, '2023-01-17 09:00:00'), +(1, 4, 1, '2023-01-17 09:30:00'), +(1, 4, 1, '2023-01-18 10:00:00'), +(1, 6, 2, '2023-02-02 14:00:00'), +(1, 13, 4, '2023-02-15 10:30:00'), +(5, 16, 5, '2023-03-22 11:00:00'), +(12, 2, 1, '2023-01-17 10:20:00'), +(12, 3, 1, '2023-04-10 11:30:00'), +(14, 2, 1, '2023-01-09 08:15:00'), +(14, 4, 1, '2023-01-10 10:30:00'), +(14, 6, 2, '2023-01-10 14:00:00'), +(14, 9, 3, '2023-01-10 08:00:00'), +(1, 4, 1, '2023-01-17 11:00:00'), +(12, 9, 3, '2023-03-12 10:00:00'), +(14, 3, 1, '2023-01-10 10:00:00'), +(14, 7, 2, '2023-01-11 10:00:00'), +(14, 8, 2, '2023-01-12 11:00:00'), +(1, 12, 4, '2023-02-15 10:00:00'), +(12, 1, 1, '2023-01-17 10:00:00'), +(14, 5, 1, '2023-01-11 09:00:00'), +(1, 1, 1, '2023-01-16 10:00:00'), +(5, 15, 5, '2023-03-21 10:00:00'), +(12, 1, 1, '2023-04-10 11:00:00'), +(14, 1, 1, '2023-01-09 08:00:00'); \ No newline at end of file From ba35f28721aa335c3a4d56cf417b72894c73d7ca Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 20 May 2026 11:47:19 +0300 Subject: [PATCH 158/179] =?UTF-8?q?Redact=20csv's=20for:=20=D0=9F=D0=B5?= =?UTF-8?q?=D1=82-=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82:=20=D0=92=D0=B8?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../csv files/{dim_course_202605191311.csv => dim_course_202605201139.csv} | 10 ++-- .../csv files/dim_user_202605191313.csv | 21 --------- .../csv files/dim_user_202605201142.csv | 21 +++++++++ .../csv files/enrollments_202605191313.csv | 46 ------------------- .../csv files/enrollments_202605201134.csv | 46 +++++++++++++++++++ 5 files changed, 72 insertions(+), 72 deletions(-) rename mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/{dim_course_202605191311.csv => dim_course_202605201139.csv} (56%) delete mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv delete mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv similarity index 56% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv rename to mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv index 0bae8f3..e998233 100644 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605191311.csv +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv @@ -1,6 +1,6 @@ "course_id","title","category","created_at" -2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +0300 -3,BI с нуля,business,2023-03-01 00:00:00.000 +0300 -4,Excel для аналитиков,business,2023-02-01 00:00:00.000 +0300 -1,SQL для начинающих,data,2023-01-01 00:00:00.000 +0300 -5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +0300 +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +3,BI с нуля,business,2023-03-01 00:00:00.000 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv deleted file mode 100644 index 529a745..0000000 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605191313.csv +++ /dev/null @@ -1,21 +0,0 @@ -"user_id","name","age","email","registration_date" -1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +0300 -5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +0300 -11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +0300 -12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +0300 -14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +0300 -15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +0300 -17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +0300 -20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 +0300 -2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +0300 -3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +0300 -4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +0300 -6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +0300 -7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +0300 -8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +0300 -9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +0300 -10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +0300 -13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +0300 -16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +0300 -18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +0300 -19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv new file mode 100644 index 0000000..a31ef6a --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv @@ -0,0 +1,21 @@ +"user_id","name","age","email","registration_date" +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv deleted file mode 100644 index 38856eb..0000000 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605191313.csv +++ /dev/null @@ -1,46 +0,0 @@ -"user_id","course_id","enrolled_at" -2,1,2023-01-20 00:00:00.000 +0300 -3,2,2023-03-05 00:00:00.000 +0300 -6,1,2023-01-06 00:00:00.000 +0300 -6,2,2023-01-10 00:00:00.000 +0300 -6,4,2023-02-05 00:00:00.000 +0300 -7,1,2023-01-13 00:00:00.000 +0300 -7,3,2023-03-10 00:00:00.000 +0300 -8,3,2023-01-22 00:00:00.000 +0300 -13,2,2023-01-19 00:00:00.000 +0300 -13,4,2023-02-20 00:00:00.000 +0300 -16,2,2023-02-16 00:00:00.000 +0300 -16,4,2023-02-16 00:00:00.000 +0300 -19,1,2023-04-11 00:00:00.000 +0300 -19,2,2023-04-11 00:00:00.000 +0300 -2,3,2023-03-15 00:00:00.000 +0300 -4,2,2023-03-10 00:00:00.000 +0300 -7,2,2023-02-01 00:00:00.000 +0300 -9,1,2023-02-15 00:00:00.000 +0300 -18,4,2023-04-05 00:00:00.000 +0300 -4,3,2023-04-01 00:00:00.000 +0300 -8,1,2023-01-22 00:00:00.000 +0300 -8,2,2023-01-22 00:00:00.000 +0300 -9,4,2023-02-12 00:00:00.000 +0300 -10,1,2023-04-16 00:00:00.000 +0300 -18,1,2023-04-02 00:00:00.000 +0300 -3,1,2023-02-25 00:00:00.000 +0300 -6,3,2023-03-08 00:00:00.000 +0300 -1,2,2023-02-01 00:00:00.000 +0300 -11,2,2023-04-21 00:00:00.000 +0300 -14,2,2023-01-09 00:00:00.000 +0300 -14,3,2023-01-09 00:00:00.000 +0300 -14,5,2023-03-20 00:00:00.000 +0300 -17,3,2023-03-22 00:00:00.000 +0300 -20,2,2023-04-19 00:00:00.000 +0300 -12,3,2023-03-10 00:00:00.000 +0300 -1,1,2023-01-15 00:00:00.000 +0300 -12,1,2023-01-16 00:00:00.000 +0300 -14,1,2023-01-09 00:00:00.000 +0300 -14,4,2023-02-01 00:00:00.000 +0300 -15,1,2023-01-11 00:00:00.000 +0300 -15,3,2023-03-05 00:00:00.000 +0300 -17,5,2023-03-25 00:00:00.000 +0300 -5,5,2023-03-20 00:00:00.000 +0300 -1,4,2023-02-10 00:00:00.000 +0300 -15,2,2023-02-01 00:00:00.000 +0300 diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv new file mode 100644 index 0000000..e3a32cd --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv @@ -0,0 +1,46 @@ +"user_id","enrolled_at","course_id" +2,2023-01-20 00:00:00.000,1 +3,2023-03-05 00:00:00.000,2 +6,2023-01-06 00:00:00.000,1 +6,2023-01-10 00:00:00.000,2 +6,2023-02-05 00:00:00.000,4 +7,2023-01-13 00:00:00.000,1 +7,2023-03-10 00:00:00.000,3 +8,2023-01-22 00:00:00.000,3 +13,2023-01-19 00:00:00.000,2 +13,2023-02-20 00:00:00.000,4 +16,2023-02-16 00:00:00.000,2 +16,2023-02-16 00:00:00.000,4 +19,2023-04-11 00:00:00.000,1 +19,2023-04-11 00:00:00.000,2 +2,2023-03-15 00:00:00.000,3 +4,2023-03-10 00:00:00.000,2 +7,2023-02-01 00:00:00.000,2 +9,2023-02-15 00:00:00.000,1 +18,2023-04-05 00:00:00.000,4 +4,2023-04-01 00:00:00.000,3 +8,2023-01-22 00:00:00.000,1 +8,2023-01-22 00:00:00.000,2 +9,2023-02-12 00:00:00.000,4 +10,2023-04-16 00:00:00.000,1 +18,2023-04-02 00:00:00.000,1 +3,2023-02-25 00:00:00.000,1 +6,2023-03-08 00:00:00.000,3 +1,2023-02-01 00:00:00.000,2 +11,2023-04-21 00:00:00.000,2 +14,2023-01-09 00:00:00.000,2 +14,2023-01-09 00:00:00.000,3 +14,2023-03-20 00:00:00.000,5 +17,2023-03-22 00:00:00.000,3 +20,2023-04-19 00:00:00.000,2 +12,2023-03-10 00:00:00.000,3 +1,2023-01-15 00:00:00.000,1 +12,2023-01-16 00:00:00.000,1 +14,2023-01-09 00:00:00.000,1 +14,2023-02-01 00:00:00.000,4 +15,2023-01-11 00:00:00.000,1 +15,2023-03-05 00:00:00.000,3 +17,2023-03-25 00:00:00.000,5 +5,2023-03-20 00:00:00.000,5 +1,2023-02-10 00:00:00.000,4 +15,2023-02-01 00:00:00.000,2 From 1da82b68771bff4441a6c9e72887027743804e74 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 20 May 2026 13:41:30 +0300 Subject: [PATCH 159/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=203:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=B9=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=83=20ina?= =?UTF-8?q?ctive=5Fusers=5Fsummary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task_3_inactive_users_summary.sql | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql new file mode 100644 index 0000000..95b7ac1 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql @@ -0,0 +1,75 @@ +--DROP TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.inactive_users_summary +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + name String, + email String, + age UInt8, + registered_courses_count UInt8, + registration_date DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/inactive_users_summary', + '{replica}' +) +ORDER BY user_id; + +--DROP VIEW mnoskov.mv_inactive_users_summary ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_inactive_users_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.inactive_users_summary +AS +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + e.registered_courses_count AS registered_courses_count, + u.registration_date AS registration_date +FROM + mnoskov.core_dim_user u +LEFT JOIN ( + SELECT + user_id, + count() AS registered_courses_count + FROM + mnoskov.core_enrollments + GROUP BY + user_id +) e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT lv.user_id FROM mnoskov.core_fact_lesson_views lv); -- я ломалася тут между NOT IN и NOT EXISTS, но решил оставить первый тк это уже витрины мы делаем, и NULL поидее быть не должно + + +-- Чтение очищение заполнение +SHOW TABLES FROM mnoskov; + +SELECT * FROM mnoskov.core_dim_user; +SELECT * FROM mnoskov.inactive_users_summary; + +TRUNCATE TABLE mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +TRUNCATE TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC; + +INSERT INTO mnoskov.core_dim_user (user_id, name, age, email, registration_date) +VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-09 23:00:00'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-04 23:00:00'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-19 23:00:00'), +(4, 'Diana', 28, 'diana@mail.com', '2023-02-28 23:00:00'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-09 23:00:00'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-04 23:00:00'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-11 23:00:00'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-19 23:00:00'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-09 23:00:00'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-14 23:00:00'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-19 23:00:00'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-14 23:00:00'), +(13, 'Mia', 26, 'mia@mail.com', '2023-01-17 23:00:00'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-07 23:00:00'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-09 23:00:00'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-14 23:00:00'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-19 23:00:00'), +(18, 'Rita', 27, 'rita@mail.com', '2023-03-31 23:00:00'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-09 23:00:00'), +(21, 'Tinanal', 22, 'tinanal@mail.com', '2023-04-19 23:02:00') +(22, 'Anal2', 24, 'tinanal222@mail.com', '2023-04-19 23:22:00'); \ No newline at end of file From 18cfc2a077e511b2fa2ff761a0a3129808a4cde0 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Wed, 20 May 2026 15:22:48 +0300 Subject: [PATCH 160/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=204:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=B9=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=83=20cou?= =?UTF-8?q?rse=5Fcompletion=5Frate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task_4_course_completion_rate.sql | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql new file mode 100644 index 0000000..5d53f29 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql @@ -0,0 +1,117 @@ +--DROP TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.course_completion_rate +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + user_name String, + course_id UInt64, + course_title String, + lessons_in_course UInt16, + lesson_viewed UInt16, + completion_rate Float32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id); + +--DROP VIEW mnoskov.mv_course_completion_rate ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_course_completion_rate +ON CLUSTER clickhouse_cluster +TO mnoskov.course_completion_rate +AS +SELECT + u.user_id AS user_id, + u.name AS user_name, + cl.course_id AS course_id, + cl.course_title AS course_title, + cl.lessons_in_course AS lessons_in_course, + lv.lesson_viewed AS lesson_viewed, + ifnull(round(toFloat32(ifnull(lv.lesson_viewed, 0)) / nullif(cl.lessons_in_course, 0), 2), 0) AS completion_rate -- вроде все логично +FROM + mnoskov.core_enrollments e +RIGHT JOIN + mnoskov.core_dim_user u ON e.user_id = u.user_id +JOIN ( + SELECT + c.course_id, + c.title AS course_title, + uniq(lesson_id) lessons_in_course + FROM + mnoskov.core_dim_course c + JOIN + mnoskov.core_dim_lesson l ON l.course_id = c.course_id + GROUP BY + c.course_id, c.title +) cl ON e.course_id = cl.course_id +LEFT JOIN ( + SELECT + user_id, + course_id, + uniq(lesson_id) AS lesson_viewed + FROM + mnoskov.core_fact_lesson_views + GROUP BY + user_id, course_id +) lv ON e.user_id = lv.user_id AND e.course_id = lv.course_id +ORDER BY + u.user_id, e.course_id; + +-- Чтение очищение заполнение +SHOW TABLES FROM mnoskov; + +SELECT * FROM mnoskov.core_enrollments; +SELECT * FROM mnoskov.course_completion_rate; + +TRUNCATE TABLE mnoskov.core_enrollments ON CLUSTER clickhouse_cluster sync; +TRUNCATE TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync; + +INSERT INTO mnoskov.core_enrollments +(user_id, course_id, enrolled_at) +VALUES +(6, 1, '2023-01-05 23:00:00'), +(14, 2, '2023-01-08 23:00:00'), +(14, 3, '2023-01-08 23:00:00'), +(14, 1, '2023-01-08 23:00:00'), +(6, 2, '2023-01-09 23:00:00'), +(15, 1, '2023-01-10 23:00:00'), +(7, 1, '2023-01-12 23:00:00'), +(1, 1, '2023-01-14 23:00:00'), +(12, 1, '2023-01-15 23:00:00'), +(13, 2, '2023-01-18 23:00:00'), +(2, 1, '2023-01-19 23:00:00'), +(8, 3, '2023-01-21 23:00:00'), +(8, 1, '2023-01-21 23:00:00'), +(8, 2, '2023-01-21 23:00:00'), +(1, 2, '2023-01-31 23:00:00'), +(7, 2, '2023-01-31 23:00:00'), +(14, 4, '2023-01-31 23:00:00'), +(15, 2, '2023-01-31 23:00:00'), +(6, 4, '2023-02-04 23:00:00'), +(1, 4, '2023-02-09 23:00:00'), +(9, 4, '2023-02-11 23:00:00'), +(9, 1, '2023-02-14 23:00:00'), +(16, 2, '2023-02-15 23:00:00'), +(16, 4, '2023-02-15 23:00:00'), +(13, 4, '2023-02-19 23:00:00'), +(3, 1, '2023-02-24 23:00:00'), +(3, 2, '2023-03-04 23:00:00'), +(15, 3, '2023-03-04 23:00:00'), +(6, 3, '2023-03-07 23:00:00'), +(4, 2, '2023-03-09 23:00:00'), +(7, 3, '2023-03-09 23:00:00'), +(12, 3, '2023-03-09 23:00:00'), +(2, 3, '2023-03-14 23:00:00'), +(5, 5, '2023-03-19 23:00:00'), +(14, 5, '2023-03-19 23:00:00'), +(17, 3, '2023-03-21 23:00:00'), +(17, 5, '2023-03-24 23:00:00'), +(4, 3, '2023-03-31 23:00:00'), +(18, 1, '2023-04-01 23:00:00'), +(18, 4, '2023-04-04 23:00:00'), +(19, 1, '2023-04-10 23:00:00'), +(19, 2, '2023-04-10 23:00:00'), +(10, 1, '2023-04-15 23:00:00'), +(20, 2, '2023-04-18 23:00:00'), +(11, 2, '2023-04-20 23:00:00'); \ No newline at end of file From 9a2834586e5982a7bfea0217c6fb93edd0ce3349 Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Thu, 21 May 2026 00:56:45 +0300 Subject: [PATCH 161/179] Add homework for JOIN --- vberetar/postgresql/sql_join.sql | 144 +++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 vberetar/postgresql/sql_join.sql diff --git a/vberetar/postgresql/sql_join.sql b/vberetar/postgresql/sql_join.sql new file mode 100644 index 0000000..e2b7c55 --- /dev/null +++ b/vberetar/postgresql/sql_join.sql @@ -0,0 +1,144 @@ +-- Задание 1 +SELECT o.id AS order_id, + o.order_date, + o.status, + u.name AS user_name, + u.country +FROM orders o +JOIN users u ON o.user_id = u.id; + +-- Ответ на вопросы +-- 1. Логичнее начать c orders, так как нам в первую очередь нужны все заказы, +-- а потом только пользователи которые их сделали. + +-- 2. Я думаю уместнее сделать LEFT. +-- Я пробовал сделать с INNER и LEFT и у меня не пропали строки. +-- Получается это из за того что у каждого заказа есть пользователь, не дуаю что должен создастся заказ без пользователя. + + +-- Задание 2 +SELECT o.id AS order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id +WHERE o.id = 101; + +-- Ответ на вопрос +-- Выдаст дублированный состав заказа с id 101, +-- получается он будет обращаться к каждаму заказу, но отображать продукты заказа с id 101 + + +-- Задание 3 +SELECT o.id AS order_id, + u.name AS name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM orders o +JOIN users u ON u.id = o.user_id +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id; + +-- Ответ на вопрос +-- 1. Появляется дубликат заказов потому что у 3 заказа есть несколько товаров, join превращает(размножает) их в несколько строк. +-- 2. Отличается тем что мы выводим все заказы с товарами а не один заказ. + + +-- Задание 4 +SELECT u.name AS user_name, + o.id AS order_id, + o.status +FROM users u +LEFT JOIN orders o ON o.user_id = u.id; + +-- Ответ на вопрос +-- INNER JOIN не выдаст пользователей без заказа. Только тех у кого есть пара с orders. + + +-- Задание 5 +SELECT u.id AS user_id, + u.name AS user_name +FROM users u +LEFT JOIN orders o ON o.user_id = u.id +WHERE o.id IS NULL; + +-- Ответ на вопрос +-- 1. Проверять IS NULL нужно по полю orders.id +-- 2. Мы должны в запросе получить всех пользователей, а потом в фильтре "узнать" есть ли у них заказы. + + +-- Задание 6 +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity +FROM orders o +JOIN users u ON u.id = o.user_id +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id +WHERE p.name ILIKE '%kindle%'; + +-- Ответ на вопрос +-- В заказе 2 есть только один товар с названием Kindle. + + +-- Задание 7 +SELECT p.name AS product_name, + oi.order_id, + oi.quantity +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id; + +-- Ответ на вопрос +-- 1. order_id будет NULL +-- 2. Если использовать INNER JOIN то он не покажет товар который не заказывали. + + +-- Задание 8 +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM products p +JOIN order_items oi ON oi.product_id = p.id +JOIN orders o ON o.id = oi.order_id +JOIN users u ON u.id = o.user_id +WHERE o.status = 'paid'; + +-- Ответ на вопрос +-- У меня получился тот же результат если написать вот так - JOIN orders o ON o.id = oi.order_id and o.status = 'paid' + + +-- Задание 9 +SELECT o.id AS order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM products p +JOIN order_items oi ON oi.product_id = p.id +JOIN orders o ON o.id = oi.order_id +JOIN users u ON u.id = o.user_id +WHERE u.name = 'Alice'; + +-- Ответ на вопрос +-- Если у пользователя нет ни одного заказа, то этот запрос вернет пустую строку. +-- Не вернет потому что нечего отобразить таблице, если нет заказов то и нет купленных товаров. +-- Но если прописать RIGHT JOIN users u ON u.id = o.user_id то отобразит все строки NULL. + + +-- Задание 10 +SELECT o.id AS order_id, + o.user_id, + o.status +FROM orders o +LEFT JOIN order_items oi ON oi.order_id = o.id +WHERE oi.order_id IS NULL; + +-- Ответ на вопрос +-- Такой запрос выдаст пустые строки. +-- INNER JOIN должен выдать строки только если есть "пара". From af3293dc4e4b1a93b8a9b6365f80f34d52476d77 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 21 May 2026 14:37:36 +0300 Subject: [PATCH 162/179] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=205:=20=D0=9D=D0=B0=D0=BF=D0=B8=D1=88=D0=B8=20SQL-=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20=D0=BA=20=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=BD=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql new file mode 100644 index 0000000..ebe46d6 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql @@ -0,0 +1,36 @@ +-- Топ 5 самых непросматреваемых уроков +SELECT + any(lesson_title), + any(course_title), + countMerge(total_views) AS total_views +FROM + mnoskov.lesson_popularity_summary +GROUP BY + lesson_id +ORDER BY + total_views +LIMIT 5; + + +-- Неактивные пользователи младше 26 +SELECT + name, + age, + email +FROM + mnoskov.inactive_users_summary +WHERE + age < 26; + + +-- 3 круса с самым низкми процентом завершения +SELECT + course_title, + avg(completion_rate) AS avg_completion_rate +FROM + mnoskov.course_completion_rate +GROUP BY + course_title +ORDER BY + avg_completion_rate +LIMIT 3; \ No newline at end of file From 7e41fd56857da0c0921f28f41775373b4bf174e0 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 21 May 2026 14:54:24 +0300 Subject: [PATCH 163/179] =?UTF-8?q?=D0=9F=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82:=20=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9.=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=206:=20=D0=A1=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BD=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=20?= =?UTF-8?q?Greenplum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt new file mode 100644 index 0000000..fa5fbd2 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt @@ -0,0 +1,2 @@ +Разница есть только в таблицах mv_lesson_popularity_summary +Где в Greenplum - NULL, в clickhouse - 0. Потому что clickhouser сам подставляет 0 вместо NULL. \ No newline at end of file From b613634f73367076b4ad47ac7bb1467eb700a081 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 21 May 2026 16:44:30 +0300 Subject: [PATCH 164/179] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20'Materialized=20View=20(MV)'=20-=20Clickhouse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice_materialized_view_MV.sql | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql diff --git a/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql b/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql new file mode 100644 index 0000000..bfb6cd2 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql @@ -0,0 +1,157 @@ +-- Задание 1 + +--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_events', + '{replica}' +) +ORDER BY ts; + +--DROP TABLE mnoskov.clean_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.clean_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/clean_events', + '{replica}' +) +ORDER BY ts; + +CREATE MATERIALIZED VIEW mnoskov.mv_clean +ON CLUSTER clickhouse_cluster +TO mnoskov.clean_events +AS +SELECT + user_id, + ts, + nullif(event_type, '') AS event_type -- это я сделать чтобы выводилось NULL а не '' - пустая строка. +FROM mnoskov.raw_events +WHERE user_id != 0 AND event_type IS NOT NULL +ORDER BY ts; + +-- Тесты: +SELECT * FROM raw_events; +SELECT * FROM mnoskov.mv_clean; + +TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +INSERT INTO mnoskov.raw_events +(user_id, ts, event_type) +VALUES +(0, '2024-07-17 10:00:00', 'click'), +(1, '2024-07-17 10:01:00', 'view'), +(2, '2024-07-17 10:02:00', NULL); + + +-- Задание 2 + +--raw_events +--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_events', + '{replica}' +) +ORDER BY ts; + +--click_events +--DROP TABLE mnoskov.click_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.click_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/click_events', + '{replica}' +) +ORDER BY ts; + +--login_events +--DROP TABLE mnoskov.login_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.login_events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/login_events1', + '{replica}' +) +ORDER BY ts; + +--purchase_events +--DROP TABLE mnoskov.purchase_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.purchase_events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/purchase_events1', + '{replica}' +) +ORDER BY ts; + +--mv_click +--DROP VIEW mnoskov.mv_click ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_click +ON CLUSTER clickhouse_cluster +TO mnoskov.click_events +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'click' +ORDER BY ts; + +--mv_login +--DROP VIEW mnoskov.mv_login ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_login +ON CLUSTER clickhouse_cluster +TO mnoskov.login_events1 +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'login' +ORDER BY ts; + +--mv_purchase +--DROP VIEW mnoskov.mv_purchase ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_purchase +ON CLUSTER clickhouse_cluster +TO mnoskov.purchase_events1 +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'purchase' +ORDER BY ts; + + +-- Тесты: +SELECT * FROM mnoskov.click_events; +SELECT * FROM mnoskov.login_events1; +SELECT * FROM mnoskov.purchase_events1; + +TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +INSERT INTO mnoskov.raw_events +(user_id, ts, event_type) +VALUES +(1, '2024-07-17 10:00:00', 'click'), +(2, '2024-07-17 10:01:00', 'login'), +(3, '2024-07-17 10:02:00', 'purchase'); \ No newline at end of file From 82d999c6f176ae9e6a5a8ebbb633eb7ff4ddc3af Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Fri, 22 May 2026 01:09:28 +0300 Subject: [PATCH 165/179] Add homework on SQL UNION --- vberetar/postgresql/sql_union.sql | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 vberetar/postgresql/sql_union.sql diff --git a/vberetar/postgresql/sql_union.sql b/vberetar/postgresql/sql_union.sql new file mode 100644 index 0000000..b71e744 --- /dev/null +++ b/vberetar/postgresql/sql_union.sql @@ -0,0 +1,49 @@ +-- Задание 1 +SELECT id, name, 'active' AS user_status +FROM users +WHERE is_active = TRUE +UNION ALL +SELECT id, name, 'inactive' AS user_status +FROM users +WHERE is_active = FALSE +ORDER BY name; + +-- Ответ на вопрос +-- 1. Количество строк не изменилось +-- 2. Предположу, что подача информации в таком виде более информативней(понятней). + + +-- Задание 2 +SELECT id AS order_id, user_id, order_date, 'old' AS period +FROM orders +WHERE order_date < '2024-01-01' +UNION ALL +SELECT id AS order_id, user_id, order_date, 'new' AS period +FROM orders +WHERE order_date >= '2024-01-01' +ORDER BY order_date, order_id; + +-- Ответ на вопрос +-- 1. В dbeaver в самой левой колонке отображается номерация строк, а снизу общее количество строк который выдал запрос. +-- На последней строке с old можно увидеть число в самой левой колонке, это и есть количество строк c old. Далее отнимаем общее кол-во строк от old и получаем кол-во new. +-- 2. Тяжело ответить на такой вопрос, не знаю. Если у меня были бы сомнения, я бы у новичка спросил. + + +-- Задание 3 +SELECT DISTINCT p.id AS product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM products p +JOIN order_items oi ON oi.product_id = p.id +UNION ALL +SELECT DISTINCT p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id +WHERE oi.product_id IS NULL +ORDER BY product_name; + +-- Ответ на вопрос +-- 1. Нам нужно чтобы товар(products) ни разу не заказывали(order_items), получается что мы проверяем есть ли ссылка в каком нибудь заказе на продукт. +-- 2. Дает сигнал бизнесу что товар не хотят покупать, возможно на него нужно сделать скидку или прорекламировать товар у блогера какого нибудь. From 4b403441c8df90363da952ea93f75491f1ffe9ae Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Sat, 23 May 2026 00:42:45 +0300 Subject: [PATCH 166/179] Add homework on SQL aggregation and grouping --- .../postgresql/sql_aggregation_grouping.sql | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 vberetar/postgresql/sql_aggregation_grouping.sql diff --git a/vberetar/postgresql/sql_aggregation_grouping.sql b/vberetar/postgresql/sql_aggregation_grouping.sql new file mode 100644 index 0000000..6140687 --- /dev/null +++ b/vberetar/postgresql/sql_aggregation_grouping.sql @@ -0,0 +1,105 @@ +-- Задание 1 +SELECT category, COUNT(*) AS products_count +FROM products +GROUP BY category +ORDER BY products_count DESC; + +-- Ответ на вопросы +-- 1. С ORDER BY гораздо удобней читать результат, без него данные отдаются в непонятном порядке. +-- 2. Для категории smartphone добавится +1 значение. + + +-- Задание 2 +SELECT order_id, + SUM(quantity) AS total_quantity, + SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items +GROUP BY order_id +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. Если убрать GROUP BY и order_id то запрос выдаст 1 строку со всеми суммами total_quantity и order_revenue. +-- Если убрать только GROUP BY и не убрать order_id то выдаст ошибку. Так как нельзя оставлять неагрегированные столбцы без GROUP BY. +-- 2. Если добавить product_id без добавления её в GROUP BY или агрегат то выдаст ошибку. + + +-- Задание 3 +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY p.category +ORDER BY category_revenue; + +-- Ответ на вопросы +-- 1. По задаче нам же нужно сгруппировать и посчитать сумму по категориям а не по id товарам. +-- 2. Если добавить p.name в SELECT без добавления её в GROUP BY или агрегат то выдаст ошибку. Нельзя вставлять неагрегированные столбцы без GROUP BY. + + +-- Задание 4 +SELECT oi.order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY oi.order_id +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. В этом задании мы получили сумму(выручку) по заказам оплаченным, а в прошлом задании мы получили сумму(выручку) по категориям товаров. +-- 2. Посчитать выручку, сколько заработала компания. Остальные заказы еще не оплачены. + + +-- Задание 5 +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY p.category +HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY category_revenue; + +-- Ответ на вопросы +-- 1. WHERE не принимает агрегирующие функции, можно сделать так - WHERE oi.quantity * oi.price_at_purchase > 1000 +-- 2. WHERE фильтрует до GROUP BY, а HAVING после GROUP BY. + + +-- Задание 6 +SELECT country, + COUNT(*) AS active_users_count +FROM users +WHERE is_active = TRUE +GROUP BY country +HAVING COUNT(*) > 2 +ORDER BY active_users_count DESC; + +-- Ответ на вопросы +-- 1. Предположу что отбрасывает неактивных пользователей сразу, перед групировкой и агрегаций. В дальнейшем может сэкономить на времени вычисления, особенно если данных будет много. +-- 2. Пустую таблицу без строк. + + +-- Задание 7 +SELECT p.name AS product_name, + SUM(oi.quantity) AS total_quantity_sold +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY product_name +ORDER BY total_quantity_sold DESC +LIMIT 3; + +-- Ответ на вопросы +-- 1. Тут мы выясняем какой товар самый ходовой и сколько его продается, а рейтинг по выручке показывает сколько зарабатывает фирма от продажи товара. +-- 2. В случаях когда, например, нужно выяснить какой товар хорошо продается чтоб его закупить на склад для продажи. + + +-- Задание 8 +SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items +GROUP BY order_id +HAVING SUM(quantity * price_at_purchase) < 100 +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. Есть товары дешевые и люди которые мало зарабатывают, ещё есть всякие скидки на ряд товаров. +-- 2. Я думаю проще увидеть отфильтрованные заказы, чем листать весь список с заказами. From b654a6eac09c85e1fa6afc472805795fd3a7d49b Mon Sep 17 00:00:00 2001 From: dmezentsev Date: Sun, 24 May 2026 11:32:46 +0300 Subject: [PATCH 167/179] add homework --- dmezentsev/postgresql/homework/CTE.sql | 195 +++++++++ dmezentsev/postgresql/homework/Explain.md | 375 ++++++++++++++++++ dmezentsev/postgresql/homework/Locking.md | 88 ++++ .../postgresql/homework/Logic_physic_joins.md | 307 ++++++++++++++ .../postgresql/homework/Optimization.md | 251 ++++++++++++ dmezentsev/postgresql/homework/Query_order.md | 328 +++++++++++++++ .../postgresql/homework/Transactions.md | 68 ++++ dmezentsev/postgresql/homework/Views.sql | 130 ++++++ .../postgresql/homework/Window_functions.sql | 209 ++++++++++ 9 files changed, 1951 insertions(+) create mode 100644 dmezentsev/postgresql/homework/CTE.sql create mode 100755 dmezentsev/postgresql/homework/Explain.md create mode 100755 dmezentsev/postgresql/homework/Locking.md create mode 100755 dmezentsev/postgresql/homework/Logic_physic_joins.md create mode 100755 dmezentsev/postgresql/homework/Optimization.md create mode 100755 dmezentsev/postgresql/homework/Query_order.md create mode 100755 dmezentsev/postgresql/homework/Transactions.md create mode 100755 dmezentsev/postgresql/homework/Views.sql create mode 100644 dmezentsev/postgresql/homework/Window_functions.sql diff --git a/dmezentsev/postgresql/homework/CTE.sql b/dmezentsev/postgresql/homework/CTE.sql new file mode 100644 index 0000000..8c99b87 --- /dev/null +++ b/dmezentsev/postgresql/homework/CTE.sql @@ -0,0 +1,195 @@ +-- Задача 1. Пользователи с заказами +SELECT id AS user_id +FROM dmezentsev.users +WHERE id IN (SELECT user_id + FROM dmezentsev.orders + ) +ORDER BY name; + +/* - Чем такой подход отличается от варианта с JOIN + DISTINCT по users? + + В случае с JOIN + DISTINCT мы бы сперва соединили таблицы, получили юзеров, + которые делали заказы, а потом SELECT DISTINCT отобрали бы id этих юзеров + + При подзапросе мы получаем условный список юзеров с заказами, и фильтрацией + отбираем их id в другой таблице + + - В каком случае IN (подзапрос) может быть более читаемым, чем JOIN? + +В случае, когда нужны поля первой таблицы, а подзапрос только как фильтр. +Меньше строк кода, джойна другой таблицы, подзапрос выигрывает по читаемости +*/ + +-- Задача 2. Пользователи без заказов (NOT EXISTS) + +SELECT id, + name, + email +FROM dmezentsev.users u +WHERE NOT EXISTS (SELECT 1 + FROM dmezentsev.orders o + WHERE o.user_id = u.id); + +/* - Почему для этой задачи NOT EXISTS предпочтительнее, + чем NOT IN (SELECT user_id ...)? + + Подзапрос бы выдал список user_id, по которым надо пройтись и найти соответствие + в другой таблице. EXISTS / NOT EXISTS ищет первое совпадение по user_id и потом + сразу переходит к другому user_id, тем самым сокращая время выполнения запроса + + - Как бизнес может использовать список таких пользователей? + + Может попробовать замотивировать их к первой покупке выдачей промокода/скидки/ + специального предложения + */ + +-- Задача 3. Сколько заказов у каждого пользователя (подзапрос в SELECT) + +SELECT u.name, + (SELECT count(*) + FROM dmezentsev.orders o + WHERE o.user_id = u.id) AS orders_count +FROM dmezentsev.users u +ORDER BY orders_count DESC, name; + +/* - Почему подзапрос в SELECT не может возвращать несколько строк? + +Потому что для каждого u.name он возвращает только одну строку - число заказов. +Если попытаться вернуть больше строк, то будет ошибка выполнения запроса + + +- Как изменится сложность запроса, если пользователей станет, например, 1 млн? + +Внешне запрос останется таким же, увеличится лишь время выполнения запроса + */ + +-- Задача 4. Сумма выручки по каждому пользователю (подзапрос или CTE) + +SELECT u.name, + t.total_revenue +FROM (SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.order_items oi ON oi.order_id = o.id + GROUP BY o.user_id) AS t +JOIN dmezentsev.users u ON t.user_id = u.id +ORDER BY total_revenue DESC; + +/* - Как проще читать такой запрос: когда шаг «посчитать выручку по пользователям» + вынесен отдельно, или когда всё написано в одном SELECT? + + Подзапросы читаются проще, логически понятны шаги получения нужных строк сперва + в подзапросе, а потом в основном запросе + + - Если эту «выручку по пользователям» придётся использовать в другом отчёте, + как избежать дублирования кода? + + В таком случае, лучше подзапрос вынести в отдельный CTE, чтобы потом + переиспользовать в коде + */ + +-- Задача 5. Топ‑3 товара по выручке (CTE) + +WITH products_total AS ( + SELECT product_id, + SUM(quantity * price_at_purchase) AS product_revenue + FROM dmezentsev.order_items + GROUP BY product_id +) + +SELECT p.name, + p.category, + pt.product_revenue +FROM dmezentsev.products p +JOIN products_total pt ON p.id = pt.product_id +ORDER BY product_revenue DESC +LIMIT 3; + +/* - Почему удобно сначала считать агрегаты по товарам, + а потом уже подтягивать имена/категории? + + Код становится более читаемым из-за разделения ответственности: + агрегат отдельно в CTE, основной запрос для предоставления всех данных отдельно + + - В чём отличие подхода через CTE от простого `GROUP BY` в одном `SELECT`? + + GROUP BY сделает запрос сложнее для понимания, в него нужно будет добавлять + поля для агрегации. CTE разделяет всю логику, делая код понятнее, к тому же + CTE можно переиспользовать + */ + +-- Задача 6. Активные пользователи с высокой суммой заказов + +WITH users_total AS ( + SELECT o.user_id, + SUM(quantity * price_at_purchase) AS total_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY o.user_id +) + +SELECT u.name, + u.email, + ut.total_revenue +FROM dmezentsev.users u +JOIN users_total ut ON u.id = ut.user_id +WHERE u.is_active + AND total_revenue > 1000; + +/* - Как бы ты назвал этот сегмент пользователей на языке продукта + (VIP, high‑spenders и т.п.)? + + Low - эконом + Medium - основные + High - премиум + VIP - топ + + Допустим, что этот сегмент пользователей был бы Medium + + - Что можно делать с таким сегментом: какие типы акций/коммуникаций? + + Medium сегмент - это лояльные клиенты, которые регулярно покупают, + но сумма покупок не дотягивает до High + Я бы предложил для такого сегмента введение накопительной системы, + дающей право на получение дополнительных скидок, введение системы + раннего доступа к закрытым распродажам + + */ + +-- Задача 7. «Подозрительно маленькие» оплаченные заказы + +WITH orders_total AS ( + SELECT oi.order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.order_items oi + INNER JOIN dmezentsev.orders o ON oi.order_id = o.id + WHERE o.status = 'paid' + GROUP BY oi.order_id +) +SELECT ot.order_id, + o.order_date, + ot.order_revenue +FROM orders_total ot +JOIN dmezentsev.orders o ON ot.order_id = o.id +WHERE ot.order_revenue < 100; + +/* - Какие реальные причины могут быть у таких заказов +(ошибки, тесты, злоупотребления промокодами)? + +Предположу, что причины могут быть разного вида: + Технические: + - Ошибка округления валют + - Использование нескольких промокодов одновременно + - Двойное списание бонусов + - Ошибки интеграции с платежными системами + + Человеческий фактор: + - Ручное редактирование заказа + +- На каком шаге выгоднее фильтровать по статусу paid: +внутри подзапроса или снаружи, и почему? + +Выгоднее фильровать внутри подзапроса, чтобы в подзапросе группировать +уже отфильтрованные данные. И уже в основном запросе джойнить, а меньше +строк для джойна ускорят запрос + */ diff --git a/dmezentsev/postgresql/homework/Explain.md b/dmezentsev/postgresql/homework/Explain.md new file mode 100755 index 0000000..bbd5452 --- /dev/null +++ b/dmezentsev/postgresql/homework/Explain.md @@ -0,0 +1,375 @@ +### Задача 1 — Читаем план + +#### Требования: + +Выполни этот запрос: + +```sql +EXPLAIN +SELECT name, email +FROM users +WHERE id = 1; +``` + +**1. Какой тип сканирования использует база?** + +Index Scan (индексное сканирование) + +**2. Что означает строка `Index Cond: (id = 1)`?** + +Index Cond (Index Condition) — это условие, которое применяется на уровне индекса (id = 1) + +**3. Почему база использует индекс, а не читает всю таблицу?** + +База выбирает индекс вместо полного сканирования таблицы, потому что id — это первичный ключ с индексом, что делает Index Scan значительно быстрее и дешевле, чем Seq Scan + +### Задача 2 — Seq Scan vs Index Scan + +#### Требования: + +Выполни оба запроса с EXPLAIN: + +```sql +-- Запрос А +EXPLAIN +SELECT * FROM users WHERE id = 2; + +-- Запрос Б +EXPLAIN +SELECT * FROM users WHERE age > 25; +``` + +**1. Почему в первом запросе используется Index Scan, а во втором может быть Seq Scan (если нет индекса на age)?** + +- Первый запрос (id = 2) использует Index Scan, потому что на id есть индекс первичного ключа. +- Второй запрос (age > 25) использует Seq Scan, потому что на age нет индекса + +### Задача 3 — Замеряем время + +#### Требования: + +Выполни запрос с EXPLAIN ANALYZE: + +```sql +EXPLAIN ANALYZE +SELECT name, price +FROM products +WHERE category = 'Электроника'; +``` + +**1. Запиши Execution Time** + +Execution Time: 0.699 ms + +**2. Определи тип сканирования (Seq Scan или Index Scan)** + +Seq Scan + +**3. Сколько строк вернул запрос?** + +rows=3 + +### Задача 4 — До и после индекса + +#### Требования: + +**Шаг 1:** Выполни запрос без индекса: + +```sql +EXPLAIN ANALYZE +SELECT * FROM users WHERE country = 'Россия'; +``` + +Запиши **Execution Time** и тип сканирования. + +Execution Time: 76.281 ms +Seq Scan + +**Шаг 2:** Создай индекс: + +```sql +CREATE INDEX idx_country ON users(country); +``` + +**Шаг 3:** Снова выполни тот же запрос с EXPLAIN ANALYZE. + +**1. Изменился ли тип сканирования?** + +Тип сканирования не изменился (слишком мало строк в таблице, чтобы оптимизатор использовал индекс) + +**2. Стал ли запрос быстрее? На сколько?** + +Execution Time: 0.018 ms +Время выполнения запроса после создания индекса стало гораздо меньше, на ~76 ms + +**3. Почему индекс помог?** + +Индекс поможет, потому что: + +- Уменьшит количество читаемых данных - читать только страницы с нужными значениями +- Использует структуру B-Tree +- Позволяет найти строки без сканирования всей таблицы + +### Задача 5 — Анализируем JOIN + +#### Требования: выполни запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия'; +``` + +**1. Какой тип JOIN использует база (Hash Join, Nested Loop, Merge Join)?** + +Hash Join, потому что таблицы маленькие, нет индексов — построить хеш-таблицу и просканировать +правую таблицу дешевле всего. + +**2. С какой таблицы база начала чтение?** + +orders + +**3. Сколько времени занял весь запрос?** + +Execution Time: 0.083 ms + +### Задача 6 — Почему медленно? + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT * +FROM users +WHERE LOWER(email) = 'alexey@mail.ru'; +``` + +**1. Выполни запрос и посмотри на план** + +Seq Scan on users (cost=0.00..1.15 rows=1 width=467) (actual time=0.012..0.012 rows=0 loops=1) + Filter: (lower((email)::text) = 'alexey@mail.ru'::text) + Rows Removed by Filter: 10 +Planning Time: 0.429 ms +Execution Time: 0.026 ms + +**2. Использует ли база индекс?** + +План показывает Seq Scan, а не Index Scan. + +**3. Почему НЕТ? (даже если индекс на email есть)** + +Применение функции LOWER() к колонке + +**4. Как исправить запрос, чтобы индекс заработал?** + +- Создать индекс с учетом применения функции LOWER() + +```sql +CREATE INDEX idx_users_email_lower ON users (LOWER(email)); +``` +- Использовать ILIKE +- Хранить нормализованный email + +### Задача 7 — Функции убивают индексы + +#### Сравни два запроса: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM users WHERE age + 5 > 30; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; +``` + +**1. Какой запрос работает быстрее?** + +Запрос Б работает быстрее: 0.016 ms против 0.043 ms + +**2. Почему первый запрос НЕ может использовать индекс на `age`?** + +Потому что к колонке age применена математическая операция (age + 5), индекс не используется + +**3. Как правильно переписать запрос А?** + +```sql +SELECT * FROM users WHERE age > 25; +``` + +### Задача 8 — Диагностика медленного запроса + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE EXTRACT(YEAR FROM u.created_at) = 2026 +GROUP BY u.name; +``` + +**1. Почему функция `EXTRACT()` — плохая идея?** + +Потому что EXTRACT(YEAR FROM created_at) — это функция, применённая к колонке, +индекс на created_at работать не будет. + +**2. Как переписать условие WHERE без функции?** + +```sql +WHERE u.created_at >= '2026-01-01' AND u.created_at < '2027-01-01' +``` + +**3. Будет ли использоваться индекс на `created_at` в текущем виде?** + +Индекс на created_at НЕ БУДЕТ использоваться + +### Задача 9 — SELECT * против конкретных колонок + +#### Сравни два запроса: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM products WHERE price > 50000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, price FROM products WHERE price > 50000; +``` + +**1. Какой запрос работает быстрее?** + +Запрос Б работает быстрее (0.017 ms против 0.051 ms) + +**2. Почему второй может быть эффективнее (даже если тип сканирования одинаковый)?** + +Меньше данных нужно прочитать с диска (две колонки против всех колонок таблицы) + +**3. В каких ситуациях разница будет особенно заметна?** + +- Большое количество строк +- Медленные диски (HDD) +- Высокая нагрузка (тысячи запросов в секунду) + +### Задача 10 — Оптимизируем JOIN с условием + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' AND p.category = 'Электроника'; +``` + +**1. Выполни запрос и посмотри на план** + +Nested Loop (cost=12.66..41.04 rows=1 width=440) (actual time=34.552..34.556 rows=0 loops=1) + Join Filter: (u.id = o.user_id) + Rows Removed by Join Filter: 5 + -> Seq Scan on users u (cost=0.00..1.12 rows=1 width=226) (actual time=0.012..0.015 rows=1 loops=1) + Filter: ((country)::text = 'RU'::text) + Rows Removed by Filter: 9 + -> Nested Loop (cost=12.66..39.84 rows=6 width=230) (actual time=34.510..34.536 rows=5 loops=1) + -> Hash Join (cost=12.51..38.61 rows=6 width=230) (actual time=34.094..34.105 rows=5 loops=1) + Hash Cond: (oi.product_id = p.id) + -> Seq Scan on order_items oi (cost=0.00..22.70 rows=1270 width=20) (actual time=33.943..33.945 rows=10 loops=1) + -> Hash (cost=12.50..12.50 rows=1 width=226) (actual time=0.047..0.048 rows=3 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 9kB + -> Seq Scan on products p (cost=0.00..12.50 rows=1 width=226) (actual time=0.022..0.027 rows=3 loops=1) + Filter: ((category)::text = 'Электроника'::text) + Rows Removed by Filter: 4 + -> Index Scan using orders_pkey on orders o (cost=0.15..0.21 rows=1 width=16) (actual time=0.084..0.084 rows=1 loops=5) + Index Cond: (id = oi.order_id) +Planning Time: 0.278 ms +Execution Time: 34.691 ms + +**2. В каком порядке база читает таблицы?** + +Порядок чтения таблиц: +1. users +2. products +3. order_items +4. Hash Join +5. orders +6. Nested Loop. + +**3. Сколько строк проходит через каждый JOIN?** + +1. users (1) +2. products (3) +3. order_items (10) +4. Hash Join (5) +5. orders (5) +6. Nested Loop (0) + +**4. Можно ли оптимизировать? (Подсказка: подумай про индексы на `country` и `category`)** + +- Создать индексы: idx_users_country, idx_products_category +- Обновить статистику: ANALYZE всех таблиц + +### Задача 11 — Найди и исправь + +#### Представь, что тебе дали код коллеги: + +```sql +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; +``` + +**1. Выполни этот запрос с EXPLAIN ANALYZE** + +Nested Loop (cost=0.00..22.35 rows=1 width=549) (actual time=0.034..0.035 rows=0 loops=1) + Join Filter: (u.id = o.user_id) + -> Seq Scan on users u (cost=0.00..1.20 rows=1 width=467) (actual time=0.033..0.033 rows=0 loops=1) + Filter: ((lower((name)::text) ~~ '%иван%'::text) AND ((age + 10) > 35)) + Rows Removed by Filter: 10 + -> Seq Scan on orders o (cost=0.00..21.10 rows=4 width=82) (never executed) + Filter: (EXTRACT(month FROM order_date) = '1'::numeric) +Planning Time: 0.235 ms +Execution Time: 0.045 ms + +**2. Найди **ВСЕ** проблемы, которые мешают оптимизации** + +- SELECT * - выбор всех колонок +- LOWER(u.name) — функция на колонке, не используется индекс на name +- LIKE '%иван%' — процент в начале означает "искать подстроку в любой позиции" +- EXTRACT(MONTH FROM o.order_date) = 1 — функция на колонке +- u.age + 10 > 35 — расчеты на колонке + +**3. Перепиши запрос правильно** + +```sql +SELECT u.id, u.name, u.age, u.email, u.country, o.id AS order_id, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.name ILIKE '%иван%' + AND o.order_date >= '2025-01-01' + AND o.order_date < '2025-02-01' + AND u.age > 25; +``` + +**4. Сравни Execution Time до и после** + +На малых данных разница незначительна: +0.036 ms против 0.045ms + +**5. Объясни, почему твой вариант лучше** + +- Перечисление нужных полей в SELECT +- Регистронезависимая фильтрация ILIKE +- Убраны функции и расчеты по колонкам, не позволяющие использовать индексы + +Все это в совокупности на больших таблицах ускорит время выполнения запроса diff --git a/dmezentsev/postgresql/homework/Locking.md b/dmezentsev/postgresql/homework/Locking.md new file mode 100755 index 0000000..88d921c --- /dev/null +++ b/dmezentsev/postgresql/homework/Locking.md @@ -0,0 +1,88 @@ +### Задание 1. Чтение и чтение + +#### Требования: + +- Используй таблицу `users` из примера (Alice и Bob). +- Сформулируй, что делают две одновременные сессии, каждая выполняет **`SELECT * FROM users WHERE id = 1;`**. + +Обе сессии читают данные пользователя Alice (id = 1) из таблицы users. Поскольку это обычные запросы на чтение, они не блокируют друг друга. + +**1. Почему оба запроса могут выполниться сразу, без ожидания?** + +Эти запросы выполняются параллельно, не дожидаясь друг друга, и мгновенно возвращают результат + +### Задание 2. Два `UPDATE` одного пользователя + +#### Требования: + +- Сценарий: две сессии одновременно пытаются поменять email у Alice (**`id = 1`**). +- Одна меняет на **`alice.new@example.com`**, другая — на **`alice.marketing@example.com`**. +- Опиши, что будет делать вторая сессия, пока первая транзакция не закончилась. + +1. Сессия 1 начинает транзакцию и выполняет UPDATE. PostgreSQL захватывает блокировку строки для id = 1. +2. Сессия 2 пытается выполнить свой UPDATE, не может выполнить UPDATE сразу — она должна ждать. +3. Сессия 2 переходит в состояние ожидания, пока сессия 1 не завершит свою транзакцию +4. Сессия 1 завершает транзакцию +5. Сессия 2 выполняет свой UPDATE уже поверх нового состояния строки. + +**1. Почему нельзя разрешить обоим UPDATE пройти одновременно?** + +Нельзя разрешить обоим UPDATE пройти одновременно, потому что это приведёт к потерянному обновлению, нарушению целостности данных + +**2. Какой email в итоге останется у Alice и почему порядок COMMIT важен?** + +Итоговый email зависит от порядка COMMIT — побеждает последняя зафиксированная транзакция + +Порядок COMMIT определяет, какое значение останется. + +### Задание 3. `SELECT ... FOR UPDATE` + +#### Требования: + +- Сценарий: +В Сессии 1 выполняется **`BEGIN; SELECT * FROM users WHERE id = 2 FOR UPDATE;`** и пользователь Bob «забронирован» для изменений. +- Придумай, что может сделать Сессия 2: + - что будет с `SELECT * FROM users WHERE id = 2;` + +Выполняется сразу — возвращает текущие данные Bob, не дожидаясь Сессии 1 + + - что будет с `UPDATE users SET country = 'FR' WHERE id = 2;` + +Сессия 2 ждёт — блокируется до тех пор, пока Сессия 1 не выполнит COMMIT или ROLLBACK + +**1. Почему **`SELECT`** без `FOR UPDATE` не блокируется, а `UPDATE` ждёт?** + +Чтение (без FOR UPDATE) — не изменяет данные + +**2. В каких задачах интернет‑магазина `SELECT ... FOR UPDATE` помогает избежать ошибок?** + +- Резервирование товара при оформлении заказа: без FOR UPDATE могло бы случиться, что два покупателя одновременно видят последний товар в наличии, оба оформляют заказ, и товар продаётся дважды +- Применение персональной скидки или купона: иначе юзер мог бы отправить два параллельных запроса и использовать один купон дважды +- Изменение цены товара во время оформления корзины: не допустит, когда юзер положил товар в корзину по одной цене, а к моменту оплаты цена выросла + +### Задание 4. Дедлок на двух пользователях + +#### Требования: + +- Возьми сценарий из теории: + - Сессия 1 сначала обновляет `country` у Alice, потом пытается обновить Bob; + - Сессия 2 сначала обновляет Bob, потом пытается обновить Alice. + +**1. Объясни своими словами, почему возникает дедлок и что сделает база с одной из транзакций.** + +Каждая транзакция ждёт ресурс, который удерживает другая транзакция. Ни одна из них не может продолжить выполнение. + +Когда deadlock обнаружен, база откатывает (ROLLBACK) одну из транзакций (ту, которая выполняется меньше или которая привела к дедлоку), чтобы разрешить конфликт + +**2. Как можно переписать код, чтобы дедлок не возникал *(подсказка: порядок обновления строк)*?** + +```sql +BEGIN; +UPDATE users SET country = 'CA' WHERE id = 1; +UPDATE users SET country = 'CA' WHERE id = 2; +COMMIT; +``` + +**3. Почему для интернет‑магазина это важно, если, например, массово меняют country многим пользователям скриптом?** + +Для интернет-магазина это критично при массовых обновлениях — без единого порядка тысячи дедлоков парализуют работу системы, вызывая откаты, падение производительности и несогласованные данные. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Logic_physic_joins.md b/dmezentsev/postgresql/homework/Logic_physic_joins.md new file mode 100755 index 0000000..2ccf34f --- /dev/null +++ b/dmezentsev/postgresql/homework/Logic_physic_joins.md @@ -0,0 +1,307 @@ +### Задача 1 — Когда какой метод? + +| **Ситуация** | **Какой JOIN выберет база?** | **Почему?** | +| --- | --- | --- | +| 10 пользователей, 1 млн заказов, есть индекс на orders.user_id | Nested Loop | Маленькая левая таблица (10 строк) + индекс на правой | +| 100 000 пользователей, 100 000 заказов, нет индексов | Hash Join | Обе таблицы средние, нет индексов | +| Обе таблицы отсортированы по ключу, есть индексы | Merge Join | Данные уже отсортированы — можно пройтись за один проход | +| Маленькая таблица (5 строк) соединяется с большой (1 млн строк), нет индекса | Hash Join | Hash Join просканирует большую таблицу 1 раз | + +### Задача 2 — Размер таблиц имеет значение + +#### Требования: + +Создай тестовые данные: + +```sql +-- Маленькая таблица (10 строк) +CREATE TABLE small_users AS +SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + +-- Большая таблица (100 000 строк) +CREATE TABLE big_orders AS +SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; +``` + +Теперь выполни: + +```sql +-- Вариант 1: Без индекса +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; + +-- Вариант 2: С индексом +CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); + +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +``` + +**1. Какой метод `JOIN` используется в каждом случае?** + +- Без индекса — Hash Join (строит хеш по маленькой таблице, сканирует большую) +- С индексом — Nested Loop (для каждого пользователя делает Index Scan в таблице заказов) + +**2. Почему с индексом база может выбрать `Nested Loop`?** + +Потому что левая таблица (small_users) очень маленькая (10 строк), а индекс на big_orders.user_id позволяет мгновенно находить заказы каждого пользователя + +**3. Насколько быстрее стало с индексом?** + +С индексом в 10-20 раз быстрее (~2.5 мс против ~45 мс). Index Scan читает только нужные строки (не все 100 000 заказов) + +### Задача 3 — Влияние сортировки + +#### Требования: + +Выполни три варианта: + +```sql +-- Вариант 1: Без ORDER BY +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: С ORDER BY по ключу соединения +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; + +-- Вариант 3: С ORDER BY по другому полю +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY o.order_date; +``` + +**1. Изменился ли тип `JOIN` во втором варианте?** + +Тип JOIN НЕ ИЗМЕНИЛСЯ — во всех вариантах используется Hash Join + +**2. Появился ли `Merge Join`?** + +Merge Join НЕ ПОЯВИЛСЯ — для Merge Join нужен индекс на orders.user_id, чтобы заказы были отсортированы по ключу соединения. + +**3. Почему `ORDER BY` может повлиять на выбор `JOIN`?** + +ORDER BY влияет на выбор JOIN ТОЛЬКО при наличии индексов на ключах соединения. Если бы индекс на orders.user_id существовал, ORDER BY по ключу соединения привёл бы к появлению Merge Join, который использует отсортированные данные + +### Задача 4 — Найди узкое место + +#### Требования: + +Дан медленный запрос: + +```sql +SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия'; +``` + +**1. Выполни с `EXPLAIN ANALYZE`** + +**2. Посмотри, какие типы `JOIN` используются** + +Все JOIN — Hash Join + +**3. Есть ли `Seq Scan`? Если да — где?** + +Да, есть Seq Scan на всех таблицах (таблицы очень маленькие (7-10 строк), и PostgreSQL считает, что читать всю таблицу последовательно дешевле, чем использовать индекс) + +**4. Предложи, какие индексы нужно создать** + +```sql +CREATE INDEX idx_users_country ON users(country); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_order_items_order_id ON order_items(order_id); +CREATE INDEX idx_order_items_product_id ON order_items(product_id); +``` + +- для фильтрации по стране +- для ускорения поиска заказов конкретного пользователя +- для ускорения поиска позиций заказа +- для ускорения поиска товара в позициях + +**5. Создай индексы и проверь, изменились ли типы `JOIN`** + +После создания индексов (на больших данных) типы JOIN изменятся с Hash Join на Nested Loop, а сканирование users станет Index Scan. + +### Задача 5 — Многоступенчатый JOIN + +```sql +EXPLAIN ANALYZE +SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +LEFT JOIN order_items oi ON o.id = oi.order_id +WHERE u.country = 'Россия' +GROUP BY u.name; +``` + +**1. Посмотри на план выполнения** + +**2. Какие методы `JOIN` используются на каждом уровне?** + +План выполнения показывает: оба JOIN — Hash Left Join, все таблицы читаются через Seq Scan (так как данные маленькие). + +**3. Почему для первого `JOIN` один метод, а для второго — другой?** + +Оба JOIN используют Hash Left Join, потому что нет индексов на внешних ключах, а таблицы маленькие, чтобы построить хеш-таблицы в памяти. + +**4. Сколько строк проходит через каждый `JOIN`?** + +1. После фильтра country = 'RU': users — 1 строка +2. После первого LEFT JOIN (users и orders) — 1 строка +3. После второго LEFT JOIN (orders и order_items) — 1 строка (у заказа 1 позиция) +4. Результат: 1 строка с агрегатами (orders_count=1, products_count=1) + +### Задача 6 — `LEFT JOIN` vs `INNER JOIN` + +#### Требования: + +Сравни два запроса: + +```sql +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +``` + +**1. Одинаковый ли тип физического JOIN используется?** + +Нет, типы JOIN разные: INNER JOIN использует Hash Join, LEFT JOIN использует Hash Left Join + +**2. Есть ли разница в Execution Time?** + +Небольшая, LEFT JOIN медленнее на ~0.003 мс + +**3. Почему LEFT JOIN может быть медленнее?** + +- Должен сохранять ВСЕ строки из левой таблицы (даже без соответствия) и добавлять NULL +- Возвращает больше строк + +### Задача 7 — Порядок таблиц в JOIN + +#### Требования: + +Сравни: + +```sql +-- Вариант 1: users JOIN orders +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: orders JOIN users +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM orders o +JOIN users u ON o.user_id = u.id; +``` + +**1. Изменился ли тип `JOIN`?** + +Тип JOIN НЕ ИЗМЕНИЛСЯ — в обоих случаях используется Hash Join + +**2. Изменился ли порядок чтения таблиц в плане?** + +Порядок чтения таблиц в плане НЕ ИЗМЕНИЛСЯ — оптимизатор в обоих случаях начал с orders (меньшей таблицы) + +**3. Одинаковое ли `Execution Time`?** + +Execution Time ОДИНАКОВОЕ, потому что планы идентичны + +**4. Почему оптимизатор может выбрать разный порядок?** + +Оптимизатор может выбрать разный порядок на основе COST: учитывает размер таблиц, фильтрацию, наличие индексов и тип JOIN + +### Задача 8 — Спровоцируй Merge Join + +#### Требования: + +Напиши запрос, который **гарантированно** использует Merge Join. + +**Подсказки:** +- Используй ORDER BY +- Убедись, что есть индексы на ключи соединения +- Обе таблицы должны быть достаточно большими + +Создал таблицы на 100 000 + строк с помощью ИИ, не стал сюда копировать + +```sql +EXPLAIN +SELECT u.id, u.name, o.id, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; +``` + +**Фишка в том, что Merge Join будет использоваться, если:** + +- Индекс на users.id (первичный ключ — есть) +- Индекс на orders.user_id (нужно создать) +- ORDER BY u.id, o.user_id — совпадает с ключами JOIN +- Достаточно большой размер таблиц + +### Задача 9 — Когда Nested Loop лучше Hash Join? + +#### Требования: + +Придумай ситуацию (размер таблиц, наличие индексов, условия), когда: + +**1. Nested Loop будет быстрее Hash Join** + +- Левая таблица очень маленькая (1-100 строк), Nested Loop сделает мало итераций +- Есть индекс на правой таблице по ключу JOIN +- Правая таблица очень большая (миллионы строк) + +**2. Hash Join будет быстрее Nested Loop** + +- Обе таблицы большие (тысячи+ строк) +- Нет индекса на правой таблице +- Достаточно памяти для хеш-таблицы + +**3. Создай тестовые данные и проверь свою гипотезу!** + +Создавал с помощью ИИ таблицы и принудительно отключал у оптимизатора варианты физического джойна (то есть сперва обе таблицы через Nested Loop, потом через Hash Join, затем так же другие две таблицы), вывел результаты в табличку: + +| Сценарий | Тип JOIN | Время | +|----------|----------|-------| +| маленькая + большая + индекс | Nested Loop | ~180 ms | +| маленькая + большая + индекс | Hash Join | ~400 ms | +| большая + большая + нет индекса | Hash Join | ~85 ms | +| большая + большая + нет индекса | Nested Loop | ~12500 ms | + +Гипотезы подтвердились: Nested Loop хорош для 'маленькая + большая + индекс', Hash Join хорош для 'большая + большая + нет индекса'. Но лучше всё-таки не отключать оптимизатору вариант выбора физического джойна))) \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Optimization.md b/dmezentsev/postgresql/homework/Optimization.md new file mode 100755 index 0000000..9ad8eb6 --- /dev/null +++ b/dmezentsev/postgresql/homework/Optimization.md @@ -0,0 +1,251 @@ +### Задача 1 — Одинаковые ли запросы? + +#### Требования: + +- Напиши, будут ли эти два запроса давать одинаковый результат? Объясни почему. + +```sql +-- Вариант А +SELECT name, age +FROM users +WHERE country = 'RU' AND age > 25; + +-- Вариант Б +SELECT name, age +FROM users +WHERE age > 25 AND country = 'RU'; +``` + +Оба запроса дадут абсолютно одинаковый результат. В обоих случаях будут выбраны те пользователи, у которых одновременно country = 'RU' И age > 25 + +**1. Какой из них будет работать быстрее?** + +Они будут работать с одинаковой скоростью — оптимизатор запросов сам анализирует условия и выбирает оптимальный порядок выполнения, игнорируя порядок в тексте запроса. + +### Задача 2 — JOIN в разном порядке + +#### Требования: + +Напиши два варианта запроса: сначала пользователи JOIN заказы, потом наоборот. + +**1. Будут ли результаты одинаковыми?** + +Если бы использовался INNER JOIN: результаты были бы идентичными + +**2. Твой вариант 1** + +```sql +SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; +``` + +**3. Твой вариант 2** + +```sql +SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status +FROM dmezentsev.orders o +LEFT JOIN dmezentsev.users u ON o.user_id = u.id; +``` + +**4. Какой таблицей оптимизатор начнёт чаще всего? Почему?** + +Оптимизатор PostgreSQL начинает с меньшей таблицы, потому что это снижает стоимость JOIN-операции + +### Задача 3 — Первое знакомство с EXPLAIN + +#### Требования: + +Выполни команду: + +```sql +EXPLAIN ANALYZE +SELECT name, email +FROM users +WHERE country = 'RU'; +``` + +**1. Какой тип сканирования использует база (Seq Scan, Index Scan)?** + +Seq Scan (последовательное сканирование) — база будет читать всю таблицу users подряд, одну строку за другой, проверяя условие country = 'RU'. + +**2. Сколько строк она прочитала?** + +База прочитала все 10 строк таблицы, проверила каждую, но вернула только 1. + +**3. Сколько времени это заняло?** + +0.025–0.050 ms + +### Задача 4 — Сравни два плана + +#### Требования: + +Выполни оба запроса с EXPLAIN ANALYZE: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * +FROM users +WHERE country = 'RU'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, email +FROM users +WHERE country = 'RU'; +``` + +**1. Почему второй запрос может быть быстрее первого?** + +Второй запрос (SELECT name, email) может быть быстрее первого (SELECT *), потому что читает меньше данных (только две колонки вместо всех). + +### Задача 5 — JOIN с EXPLAIN + +#### Требования: + +Посмотри план выполнения JOIN: + +```sql +EXPLAIN ANALYZE +SELECT u.name, o.id, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU'; +``` + +**1. С какой таблицы начала читать база? Почему именно с неё?** + +База начнёт читать с таблицы users, потому что у неё есть фильтр WHERE country = 'RU', который сильно уменьшает количество строк + +### Задача 6 — Найди узкое место + +#### Требования: + +У тебя есть запрос: + +```sql +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND LOWER(u.name) LIKE '%алекс%' +GROUP BY u.name; +``` + +**1. Найди как минимум 2 проблемы, которые замедляют запрос. Как их исправить?** + +- `LOWER(u.name)` — функция на колонке, которая также блокирует использование индекса и требует вычислений для каждой строки +- `LIKE '%алекс%'` не использует индекс, требуя полного сканирования + +**Исправить:** + +- использовать `ILIKE` без `LOWER` +- хранить уже нормализованные данные + +### Задача 7 — Оптимизируй JOIN + +#### Требования: + +Дан запрос: + +```sql +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия'; +``` + +**1. В каком порядке база будет читать таблицы? Почему? Можно ли как-то ускорить этот запрос?** + +База начнёт читать с таблицы users, применяя фильтр country = 'Россия' ('RU'), потому что это сильно уменьшает количество строк. + +**Ускорить можно:** + +- создать индексы +- использовать материализованные представления + +### Задача 8 — Сравни подходы + +#### Требования: + +Какой запрос будет работать быстрее и почему? + +```sql +-- Вариант А +SELECT name, price +FROM products +WHERE category = 'Электроника' + AND price > 50000; + +-- Вариант Б +SELECT * +FROM products +WHERE category = 'Электроника' + AND price > 50000; +``` + +**1. Запусти оба с EXPLAIN ANALYZE и сравни время выполнения.** + +Вариант А будет быстрее, меньше данных нужно прочитать + +- `SELECT name, price` читает 2 колонки (name VARCHAR(100), price DECIMAL) +- `SELECT *` читает все 5 колонок (id, name, category, price, in_stock) + +### Задача 9 — Найди и исправь + +#### Требования: + +Представь, что тебе дали код коллеги: + +```sql +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; +``` + +**1. Найди ВСЕ проблемы в этом запросе** + +- `EXTRACT(YEAR FROM created_at)` делает невозможным использование индекса +- `LOWER(country)` — функция на колонке, блокирующая использование индекса +- Вычисление в условии `age + 10 > 35` + +**2. Перепиши его правильно** + +```sql +SELECT id, name, email, country, age, created_at, is_active +FROM dmezentsev.users +WHERE created_at >= '2026-01-01' + AND created_at < '2027-01-01' + AND country = 'RU' + AND age > 25; +``` + +**3. Объясни, почему твой вариант лучше** + +Он лучше, потому что может использовать индексы, не выполняет лишних вычислений, возвращает только нужные колонки и фактически найдёт данные (с правильным значением страны). + +### Задача 10 — Исследуй реальные данные + +#### Требования: + +Создай таблицу с 10 000 записей пользователей (можно сгенерировать через функцию `generate_series`). + +Сравни время выполнения: + +```sql +-- Запрос 1 +SELECT * FROM users WHERE age > 25; + +-- Запрос 2 +SELECT name, email FROM users WHERE age > 25; +``` + +**1. Используй EXPLAIN ANALYZE и посмотри, насколько второй запрос быстрее первого на большом объёме данных.** + +Второй запрос (SELECT name, email) будет быстрее на 30-50% на 10 000 записях, потому что читает меньше данных (только 2 колонки вместо всех), ну и поэтому передаёт меньше байт по сети \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Query_order.md b/dmezentsev/postgresql/homework/Query_order.md new file mode 100755 index 0000000..d8ec3ca --- /dev/null +++ b/dmezentsev/postgresql/homework/Query_order.md @@ -0,0 +1,328 @@ +### Задача 1 — Запрос с группировкой + +```sql +SELECT country, COUNT(*) AS users_count +FROM users +WHERE is_active = true +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY users_count DESC; +``` + +**1. Когда применится `WHERE` — до или после `GROUP BY`?** + +WHERE применяется ДО GROUP BY + +**2. Когда применится `HAVING` — до или после `GROUP BY`?** + +HAVING применяется ПОСЛЕ GROUP BY + +**3. Можно ли использовать алиас `users_count` в `HAVING`?** + +HAVING выполняется до SELECT. Алиас users_count создаётся только на этапе SELECT. Поэтому на момент выполнения HAVING алиас ещё не существует. + +**4. Можно ли использовать алиас `users_count` в `ORDER BY`?** + +ORDER BY выполняется после SELECT. На этом этапе алиас users_count уже создан и доступен для использования. + +### Задача 2 — JOIN с фильтрацией + +```sql +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' AND o.status = 'completed' +ORDER BY o.order_date DESC; +``` + +**1. Опиши порядок выполнения своими словами (как будто объясняешь другу-новичку).** + +Порядок выполнения: +База сначала фильтрует пользователей по стране (WHERE u.country = 'Россия'), оставляя только пользователей из России. Затем соединяет (JOIN) этих пользователей с таблицей заказов, находя их заказы. После этого фильтрует заказы по статусу (WHERE o.status = 'completed'). Затем выбирает нужные колонки (SELECT): имя, дату заказа и статус заказа, и сортирует (ORDER BY). + +### Задача 3 — Найди ошибку + +Дан запрос: + +```sql +SELECT country, AVG(age) AS avg_age +FROM users +GROUP BY country +HAVING country = 'Россия'; +``` + +**1. Запрос работает, но написан **неэффективно**. Почему?** + +HAVING используется для фильтрации по колонке country, которая не является агрегированной. Для фильтрации по обычным колонкам (не результатам агрегации) предназначен WHERE. + +**2. Перепиши его правильно, используя WHERE.** + +```sql +SELECT country, AVG(age) AS avg_age +FROM users +WHERE country = 'Россия' +GROUP BY country; +``` + +**3. Объясни, почему твой вариант быстрее.** + +Потому что WHERE применяется до группировки: база сначала отфильтровывает только нужные строки (страна 'RU'), затем группирует и вычисляет средний возраст только для них. Это уменьшает объём данных для группировки, база может использовать индекс на country и не делает лишних вычислений + +### Задача 4 — Исправь запрос + +```sql +SELECT category, AVG(price) AS avg_price +FROM products +GROUP BY category +HAVING category = 'Электроника' AND AVG(price) > 20000; +``` + +**1. Перепиши запрос так, чтобы часть условий была в `WHERE`.** + +```sql +SELECT category, AVG(price) AS avg_price +FROM products +WHERE category = 'Электроника' +GROUP BY category +HAVING AVG(price) > 20000; +``` + +**2. Объясни, почему это быстрее.** + +Это быстрее, потому что WHERE отсекает ненужные категории до группировки + +**3. Нарисуй схему: сколько строк будет обработано в каждом случае.** + +Исходный запрос обрабатывает 4 группы (7 строк) — потом 4 вычисления средней цены по каждой категории. +Исправленный запрос фильтрует по условию — обрабатывает только 1 группу (3 строки) — 1 вычисление средней цены + +### Задача 5 — Исправь все ошибки + +```sql +SELECT + country, + COUNT(*) AS users_count +FROM users +WHERE users_count > 1 +GROUP BY country +ORDER BY country; +``` + +**1. Найди ошибку и исправь запрос.** + +В запросе используется WHERE users_count > 1, но алиас users_count создаётся в SELECT, который выполняется после WHERE. Кроме того, WHERE не может фильтровать по агрегатным функциям COUNT(*) + +```sql +SELECT country, COUNT(*) AS users_count +FROM users +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY country; +``` + +### Задача 6 — Полный цикл выполнения + +```sql +SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.is_active = true +GROUP BY u.country +HAVING COUNT(o.id) > 1 +ORDER BY orders_count DESC +LIMIT 3; +``` + +**1. Опиши ПОЛНЫЙ порядок выполнения (все 7 шагов) и объясни, что происходит на каждом.** + +- **FROM** — читаем таблицу users и готовим LEFT JOIN с orders +- **JOIN** — соединяем с заказами, у пользователей без заказов — NULL +- **WHERE** — фильтруем активных пользователей (is_active = true) +- **GROUP BY** — группируем по стране +- **HAVING** — оставляем только группы с количеством заказов > 1 +- **SELECT** — вычисляем COUNT(o.id) и AVG(o.id) для каждой группы +- **ORDER BY** — сортируем по убыванию orders_count +- **LIMIT** — берём первые 3 строки + +### Задача 7 — Оптимизация + +Сравни два запроса: + +```sql +-- Запрос А +SELECT category, COUNT(*) +FROM products +GROUP BY category +HAVING category = 'Электроника'; + +-- Запрос Б +SELECT category, COUNT(*) +FROM products +WHERE category = 'Электроника' +GROUP BY category; +``` + +**1. Какой запрос быстрее? Почему?** + +Запрос Б быстрее. Причина: фильтр WHERE category = 'Электроника' применяется до группировки + +**2. Нарисуй таблицу: на каком шаге сколько строк обрабатывается в каждом запросе.** + +| Показатель | Запрос А | Запрос Б | +|------------|----------|----------| +| Прочитано строк | 7 | 7 | +| Создано групп | 4 | 1 | +| Вычислено COUNT | 4 раза | 1 раз | + +**3. Выполни оба с `EXPLAIN ANALYZE` и сравни `Execution Time`.** + +У запроса А Rows Removed by Filter: 3 после группировки, у запроса Б фильтр применяется при сканировании таблицы + +### Задача 8 — Многоэтапная фильтрация + +```sql +SELECT + u.name, + COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' +GROUP BY u.name +HAVING COUNT(o.id) >= 2 +ORDER BY orders_count DESC; +``` + +**1. Когда применяются условия `WHERE`?** + +Условия WHERE применяются до группировки — фильтруют отдельные строки (пользователей и заказы) перед тем, как они попадут в GROUP BY + +**2. Когда применяется условие `HAVING`?** + +Условие HAVING применяется после группировки — фильтрует уже сформированные группы (пользователей) по результату агрегации (COUNT(o.id)). + +**3. В чём разница между фильтрацией по `o.status` в `WHERE` и по `COUNT(o.id)` в `HAVING`?** + +WHERE o.status = 'completed' фильтрует отдельные заказы до группировки и (из-за LEFT JOIN) отсекает пользователей без подходящих заказов. HAVING COUNT(o.id) >= 2 фильтрует пользователей после группировки по количеству их заказов. + +### Задача 9 — JOIN и ORDER BY + +```sql +SELECT + u.name, + p.name AS product_name, + p.price +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' +ORDER BY p.price DESC +LIMIT 5; +``` + +**1. На каком этапе применяется `ORDER BY`?** + +ORDER BY применяется на предпоследнем этапе — после всех JOIN, WHERE и SELECT, но до LIMIT. + +**2. Почему `LIMIT` идёт после `ORDER BY`, а не до?** + +LIMIT идёт после ORDER BY, потому что должен взять первые N строк уже отсортированного результата. + +**3. Что будет, если поменять их местами (технически невозможно, но подумай концептуально)?** + +Концептуально при LIMIT, а потом ORDER BY сначала взяли бы случайные строки (в физическом порядке), а потом их отсортировали бы + +### Задача 10 — Напиши запрос с нуля + +Нужно получить: + +- Список категорий товаров +- Количество товаров в каждой категории +- Среднюю цену товаров в категории +- Только те категории, где средняя цена > 15000 +- Только те категории, где товаров >= 2 +- Отсортировать по средней цене (от большей к меньшей) +- Взять только топ-3 + +**1. Напиши запрос** + +```sql +SELECT + category, + COUNT(*) AS products_count, + AVG(price) AS avg_price +FROM dmezentsev.products +GROUP BY category +HAVING AVG(price) > 15000 + AND COUNT(*) >= 2 +ORDER BY avg_price DESC +LIMIT 3; +``` + +**2. Опиши порядок выполнения** + +- **FROM** — читаем таблицу products (7 строк) +- **GROUP BY** — группируем по category (4 группы) +- **HAVING** — применяем оба условия (фильтруем группы) +- **SELECT** — вычисляем COUNT(*) и AVG(price) +- **ORDER BY** — сортируем по avg_price DESC +- **LIMIT** — берём первые 3 строки + +**3. Объясни, почему использовал `WHERE` или `HAVING` для каждого условия** + +В этом запросе нет условий WHERE, потому что нет фильтрации по отдельным строкам. Оба условия работают с результатами агрегации (AVG(price), COUNT(*)), поэтому фильтруются уже в HAVING + +**4. Выполни с `EXPLAIN ANALYZE` и посмотри план** + +``` +Seq Scan on products -- Читаем все 7 строк таблицы +HashAggregate -- Группируем по category (4 группы) +Filter: (avg(price) > 15000 AND count(*) >= 2) -- Применяем HAVING +Rows Removed by Filter: 3 -- 3 группы отброшены +Sort -- Сортируем результат по avg_price DESC +Limit -- Ограничиваем 3 строками +``` + +### Задача 11 — Подзапросы и порядок + +```sql +SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count +FROM users u +WHERE u.country = 'Россия' +ORDER BY orders_count DESC; +``` + +**1. Когда выполняется подзапрос в `SELECT`?** + +Подзапрос в SELECT выполняется для каждой строки после WHERE u.country = 'Россия', но до ORDER BY + +**2. Можно ли использовать алиас `orders_count` в `WHERE`?** + +Алиас orders_count нельзя использовать в WHERE, потому что WHERE выполняется до SELECT + +**3. Можно ли использовать его в `ORDER BY`?** + +Алиас orders_count можно использовать в ORDER BY, так как ORDER BY выполняется после SELECT. + +**4. Перепиши этот запрос через `JOIN`. Какой вариант быстрее? Проверь с `EXPLAIN ANALYZE`!** + +```sql +SELECT u.name, u.age, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' +GROUP BY u.id, u.name, u.age +ORDER BY orders_count DESC; +``` + +На маленьких данных разница незаметна. Предположу, что на больших таблицах JOIN быстрее, потому что подзапрос выполняется для каждого пользователя, а JOIN — за один проход. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Transactions.md b/dmezentsev/postgresql/homework/Transactions.md new file mode 100755 index 0000000..c767031 --- /dev/null +++ b/dmezentsev/postgresql/homework/Transactions.md @@ -0,0 +1,68 @@ +### Задание 1. Оформление заказа + +#### Требования: + +- Используй таблицы пет‑проекта: `users`, `orders`, `order_items`, `products`. + - Заказ: пользователь покупает два разных товара. + - Сформулируй транзакцию, которая за один раз: + - создаёт запись в `orders`; + - добавляет все позиции в `order_items`; + - уменьшает остаток товаров в `products`. + +**1. Что в данных пойдёт не так, если часть шагов выполнится, а часть нет?** + +- Создан заказ, но не добавлены товары: заказ без позиций. Пользователь думает, что оформил заказ, а товары не зарезервированы. +- Добавлены товары в order_items, но не создан заказ: записи о товарах висят без привязки к заказу + +**2. Почему оформление заказа — типичный пример «либо всё, либо ничего»?** + +Оформление заказа требует атомарности, потому что заказ, товары и складские остатки должны оставаться согласованными — иначе бизнес-логика нарушается (деньги списаны, а товара нет, или товар уехал, а заказ не оформлен). + +### Задание 2. Ошибка посередине + +#### Требования: + +- Сценарий: + - заказ создали; + - одну позицию заказа добавили; + - на второй позиции выяснилось, что товара нет на складе. +- Сформулируй, что должна сделать транзакция в такой ситуации (что должно сохраниться, а что откатиться). + +При оформлении заказа из двух позиций, когда на второй позиции выясняется отсутствие товара на складе, транзакция должна полностью откатить все изменения (выполнить ROLLBACK). + +**1. Какие «битые» заказы можно получить без отката (`ROLLBACK`)?** + +- Заказ без позиций — запись в orders есть, но в order_items пусто. +- Заказ с частичным наполнением — первая позиция добавлена, вторая нет. Заказ существует, но неполный. +- Заказ с некорректным статусом — статус created, но часть операций не выполнена. +- Заказ с нарушенной суммой — если считается общая сумма заказа по order_items, она не будет соответствовать ожидаемой (вторая позиция отсутствует, но пользователь мог уже оплатить полную стоимость). + +**2. Как такие ошибки повлияют на отчёты и витрины по выручке?** + +- Завышение количества заказов +- Искажение среднего чека +- Неверный расчет выручки + +### Задание 3. Один `UPDATE` как транзакция + +#### Требования: + +- Возьми простое действие в магазине: + - смена `email` пользователя в `users`; + - изменение цены товара в `products`. +- Объясни, почему даже такое действие должно выполняться как маленькая транзакция. + +Даже одиночный UPDATE выполняется как атомарная транзакция, что гарантирует «либо всё, либо ничего» + +**1. Что может случиться, если во время изменения цены пропадёт соединение с базой?** + +- Соединение потеряно до начала выполнения: + Команда не дошла до сервера — изменения не произошли. +- Соединение потеряно в процессе выполнения: + Транзакция автоматически откатывается (ROLLBACK). +- Соединение потеряно после успешного выполнения: + Транзакция уже зафиксирована (COMMIT выполнен). + +**2. Почему хорошо, что база сама откатывает незавершённые изменения?** + +Это хорошо, потому что защищает от повреждения данных, упрощает разработку и гарантирует соблюдение ограничений целостности без ручного вмешательства. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Views.sql b/dmezentsev/postgresql/homework/Views.sql new file mode 100755 index 0000000..8454a63 --- /dev/null +++ b/dmezentsev/postgresql/homework/Views.sql @@ -0,0 +1,130 @@ +-- Задание 1. Витрина выручки по категориям + +/* Требования: + +- Используй таблицы: `products`, `orders`, `order_items`. +- Учитывай только оплаченные заказы (`status = 'paid'`). +- Создай `MATERIALIZED VIEW` `mv_category_revenue` со столбцами: + - `category` + - `total_revenue` — суммарная выручка по категории (`quantity * price_at_purchase`) +- Отсортируй результат по `total_revenue` по убыванию при выборке. +*/ + +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM dmezentsev.products p +JOIN dmezentsev.orders_items oi ON p.id = oi.product_id +JOIN dmezentsev.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +ORDER BY total_revenue DESC + + +/* 1. Чем удобно иметь готовую витрину по категориям, вместо того чтобы каждый раз писать JOIN по трём таблицам? +- Производительность — данные уже агрегированы и лежат в одной материализованной таблице. Вместо сканирования +трёх таблиц и группировки, запрос читает готовую таблицу. +- Простота запросов — аналитик пишет SELECT * FROM mv_category_revenue, не вникая в схему и условия фильтрации. +*/ + + +-- Задание 2. Витрина активности пользователей + +/* Требования: + +- Используй таблицы: `users`, `orders`. +- Считай только оплаченные заказы. +- Создай MATERIALIZED VIEW **`mv_user_activity`** со столбцами: + - `user_id` + - `user_name` + - `orders_count` — сколько оплаченных заказов сделал пользователь +- В выборке покажи только тех, у кого `orders_count > 0`, отсортируй по `orders_count` по убыванию. +*/ + +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.user_id, + u.user_name, + COUNT(o.order_id) AS orders_count +FROM users u +JOIN orders o ON u.user_id = o.user_id +WHERE o.status = 'paid' +GROUP BY u.user_id, u.user_name +HAVING COUNT(o.order_id) > 0 +ORDER BY orders_count DESC; + +/* 1. Как по такой витрине найти «самых лояльных» клиентов? + +SELECT * FROM mv_user_activity LIMIT 3; + +(Предположил ради примера, что самые лояльные - это топ-3 клиента) + +2. Что будет, если считать активность не по всем заказам, а только за последние 30 дней +— как изменится смысл витрины? + +Смысл витрины изменится на отслеживание текущей (недавней) активности, а не за весь период времени в данных +*/ + + +-- Задание 3. Витрина выручки по дням + +/* Требования: + +- Используй таблицы: `orders`, `order_items`. +- Учитывай только оплаченные заказы. +- Создай `MATERIALIZED VIEW` **`mv_daily_revenue`** со столбцами: + - `order_date::date` как `sales_date` + - `daily_revenue` — суммарная выручка за день +- При выборке отсортируй по `sales_date` по возрастанию. +*/ + +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + SUM(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM orders o +JOIN order_items oi ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY o.order_date::date +ORDER BY sales_date ASC; + +/* 1. Как по такой витрине заметить «просадки» и «пики» продаж? + +- Построить линейный график зависимости daily_revenue от sales_date +- Использовать оконные функции для сравнения со скользящим средним (например, за 7 дней) +- Вычислить оконной функцией LAG и разницу с предыдущим днём, чтобы увидеть изменения. + +2. Что быстрее: считать выручку «на лету» из сырых таблиц или читать из `mv_daily_revenue` ? Почему? + +Чтение из mv_daily_revenue быстрее, потому что: +- В mv_daily_revenue меньше строк, так как данные агрегированы. Сырые таблицы могут содержать +миллионы записей — каждый день приходится заново агрегировать все данные. +*/ + +-- Задание 4. Обновление витрин + +/* Требования: + +- Для любой одной из созданных витрин (на выбор ученика) написать команду обновления с `REFRESH MATERIALIZED VIEW ...`. +*/ + +REFRESH MATERIALIZED VIEW mv_daily_revenue; + + +/* - Придумать расписание обновления витрины для двух сценариев: + - интернет‑магазин с несколькими заказами в день; + - крупный магазин с тысячами заказов в час. + +Интернет‑магазин с несколькими заказами в день — 1 раз в сутки, ночью +За день накапливается мало новых данных, достаточно обновляться раз в сутки + +Крупный магазин с тысячами заказов в час — каждые 30 минут +Обновление за 30 минут успевает до следующего цикла + +1. Чем грозит слишком редкий `REFRESH` для отчётов (какие ошибки могут появиться в анализе)? +- Устаревшие данные — угроза применения ошибочных мер. +- Пропуск аномалий — если витрина обновляется редко, аномалии можно не заметить до следующего REFRESH + +2. Почему нельзя обновлять витрины «каждую секунду», даже если хочется всегда свежие данные? +- Ресурсы БД — каждый REFRESH блокирует чтение и перестраивает всю витрину заново +- Проблемы с блокировками — если кто-то читает витрину во время REFRESH, запрос встанет в очередь или получит ошибку. +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Window_functions.sql b/dmezentsev/postgresql/homework/Window_functions.sql new file mode 100644 index 0000000..e2b773c --- /dev/null +++ b/dmezentsev/postgresql/homework/Window_functions.sql @@ -0,0 +1,209 @@ +-- Задача 1. Нумерация заказов пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + ROW_NUMBER() OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS order_number_for_user +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - В чём отличие такой нумерации от простой сортировки  +ORDER BY order_date без оконной функции? + +В случае с оконной функцией идет сортировка по order_date в рамках одного окна +(по пользователю), и каждому заказу пользователя присваивается свой порядковый +номер. При простой сортировке без оконной функции по дате сортировались бы +все заказы +*/ + +-- Задача 2. Накопительная выручка по пользователю + +WITH order_totals AS ( + SELECT o.user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.users u ON o.user_id = u.id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY o.user_id, u.name, o.id, o.order_date +) + +SELECT user_id, + user_name, + order_id, + order_date, + order_revenue, + SUM(order_revenue) OVER ( + PARTITION BY user_id + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_revenue_for_user +FROM order_totals; + +/* - Почему для накопительной суммы важно задавать рамку окна +(от «самого первого» до «текущего»), а не оставлять значения по умолчанию? + +По умолчанию RANGE BETWEEN ... Если у пользователя два заказа +в один и тот же день, то накопительная сумма посчитается так: + 1 января 100 руб Накопительная сумма: 300 + 1 января 200 руб Накопительная сумма: 300 + +Важно задавать явно ROWS BETWEEN..., чтобы накопительная сумма считалась +правильно: + 1 января 100 руб Накопительная сумма: 100 + 1 января 200 руб Накопительная сумма: 300 + +- Как по такой таблице понять, что пользователь начал тратить больше? + +Если накопительная сумма растет, то это указывает на то, что +пользователь с течением времени тратит больше +*/ + +-- Задача 3. Рейтинг пользователей по выручке + +WITH user_revenue AS ( + SELECT u.id AS user_id, + u.name AS user_name, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM dmezentsev.users u + JOIN dmezentsev.orders o ON u.id = o.user_id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY u.name, u.id +) + + SELECT user_id, + user_name, + total_revenue, + RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank, + DENSE_RANK() OVER (ORDER BY total_revenue DESC) AS revenue_dense_rank +FROM user_revenue; + +/* - Чем RANK отличается от DENSE_RANK в случае, когда +у двух пользователей одинаковая выручка? + +Допустим, у двух таких пользователей будет 1ое место у каждого по выручке. +При RANK у следующего будет 3ье место, при DENSE_RANK - 2ое. + +RANK пропускает следующие числовые позиции после одинаковых +значений (например: 1, 2, 2, 4), а DENSE_RANK не пропускает (например: 1, 2, 2, 3). + */ + +-- Задача 4. Первый и последний заказ пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + FIRST_VALUE(o.order_date) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS first_order_date, + LAST_VALUE(o.order_date) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Как можно использовать поля «первый заказ» и «последний заказ» +в сегментации клиентов? + +Для определения частоты покупок (разница между датами), выделения +новых или ушедших клиентов (например, клиенты без заказов за последние 3 месяца) + +- Почему для правильного LAST_VALUE важно задать рамку окна так, чтобы смотреть +на всю историю пользователя, а не только до текущей строки? + +Потому что по умолчанию рамка окна — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, +и LAST_VALUE показывает последнее значение в текущей рамке, т.е. текущую строку. +А нужно по всей рамке, от первой до последней даты у каждого пользователя + */ + +-- Задача 5. Заказы и количество заказов пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + COUNT(o.id) OVER (PARTITION BY u.id) AS orders_per_user +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Чем такой результат отличается от классического отчёта +«по пользователям» с GROUP BY user_id? + +При применении оконной функции строки не схлопываются, то есть если у +пользователя несколько заказов, то по каждому заказу будет строка с данными, +и в каждой строке будет общее число заказов + */ + +-- Задача 6. Сравнение суммы заказа с предыдущим + +WITH orders_total AS ( + SELECT u.id AS user_id, + o.id AS order_id, + u.name AS user_name, + o.order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.users u + JOIN dmezentsev.orders o ON u.id = o.user_id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY u.id, o.id, u.name, o.order_date +) + +SELECT user_id, + user_name, + order_id, + order_date, + order_revenue, + LAG(order_revenue) OVER ( + PARTITION BY user_id + ORDER BY order_date + ) AS prev_order_revenue +FROM orders_total; + +/* - Как по такому результату понять, +увеличивается или уменьшается средний чек с течением времени? + +Нужно сравнить order_revenue с prev_order_revenue для каждого заказа. +Если чаще order_revenue > prev_order_revenue — средний чек растёт. +Если чаще order_revenue < prev_order_revenue — средний чек падает. +sql + */ + +-- Задача 7. Категории с накопительной выручкой по времени + +WITH category_revenue AS ( + SELECT p.category, + DATE_TRUNC('day', o.order_date)::DATE AS order_date, + SUM(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM dmezentsev.order_items oi + JOIN dmezentsev.products p ON oi.product_id = p.id + JOIN dmezentsev.orders o ON oi.order_id = o.id + GROUP BY p.category, + DATE_TRUNC('day', o.order_date)::DATE +) + +SELECT category, + order_date, + daily_category_revenue, + SUM(daily_category_revenue) OVER ( + PARTITION BY category + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_category_revenue +FROM category_revenue; + +/* - Чем такой «накопительный» график по категории отличается по смыслу +от обычного ежедневного (без SUM OVER)? + +Обычный график daily_category_revenue показывает, какая выручка категории день ото дня. +Накопительный график running_category_revenue показывает общий рост +или падение категории за период собранных данных + */ \ No newline at end of file From 24bf412aebb837e2269a136d3f08dbba8e9f7f52 Mon Sep 17 00:00:00 2001 From: mnoskov Date: Mon, 25 May 2026 10:15:32 +0300 Subject: [PATCH 168/179] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B4=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8=20-=203:=20=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=B9=20=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D1=83=20inactive=5Fusers=5Fsumma?= =?UTF-8?q?ry=20&=20=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=205:=20?= =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=88=D0=B8=20SQL-=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D1=8B=20=D0=BA=20=D0=B2=D0=B8=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql | 4 ++-- .../Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql index 95b7ac1..25b333d 100644 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql @@ -6,7 +6,7 @@ ON CLUSTER clickhouse_cluster name String, email String, age UInt8, - registered_courses_count UInt8, + registered_courses_count UInt32, registration_date DateTime ) ENGINE = ReplicatedMergeTree( @@ -71,5 +71,5 @@ VALUES (17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-19 23:00:00'), (18, 'Rita', 27, 'rita@mail.com', '2023-03-31 23:00:00'), (19, 'Sam', 30, 'sam@gmail.com', '2023-04-09 23:00:00'), -(21, 'Tinanal', 22, 'tinanal@mail.com', '2023-04-19 23:02:00') +(21, 'Tinanal', 22, 'tinanal@mail.com', '2023-04-19 23:02:00'), (22, 'Anal2', 24, 'tinanal222@mail.com', '2023-04-19 23:22:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql index ebe46d6..f9b83f5 100644 --- a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql +++ b/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql @@ -1,7 +1,7 @@ -- Топ 5 самых непросматреваемых уроков SELECT - any(lesson_title), - any(course_title), + any(lesson_title) AS lesson_title, + any(course_title) AS course_title, countMerge(total_views) AS total_views FROM mnoskov.lesson_popularity_summary From 1bc7c5f3fee55d4d4f9f320aa9349cdf963ca05e Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Mon, 25 May 2026 19:14:57 +0300 Subject: [PATCH 169/179] Add homework on SQL Functions: dates, strings, logic --- .../postgresql/sql_dates_strings_logic.sql | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 vberetar/postgresql/sql_dates_strings_logic.sql diff --git a/vberetar/postgresql/sql_dates_strings_logic.sql b/vberetar/postgresql/sql_dates_strings_logic.sql new file mode 100644 index 0000000..6401815 --- /dev/null +++ b/vberetar/postgresql/sql_dates_strings_logic.sql @@ -0,0 +1,89 @@ +-- 1 Задача +SELECT name, + CAST(created_at AS date) AS created_date, + CURRENT_DATE - CAST(created_at AS date) AS days_since_registration +FROM users +WHERE created_at <= CURRENT_DATE - INTERVAL '180 days' +ORDER BY days_since_registration DESC; +-- Ответ на вопросы +-- 1. Возможно предложить старым пользователям скидки всякие или интересное предложение для удержания пользователям. Или можно проанализировать их заказы. +-- 2. Я предположу что удобней не марочится со временем а сразу работать с датой + + +-- 2 Задача +SELECT name, + LOWER(email) AS email_clean, + SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain +FROM users +WHERE email IS NOT NULL +ORDER BY domain, name; +-- Ответ на вопросы +-- 1. Приводим email к нижнему регистру для чистоты данных, +-- а зачем нужен отдельно домен не знаю, может есть какая нибудь важная разница в почтовых сервисах. +-- 2. Получим в колонке domain null. + + +-- 3 Задача +SELECT name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + END AS age_group +FROM users +ORDER BY age; +-- Ответ на вопросы +-- 1. Возможно данные и статистику быстрее и удобнее получить через sql а не через код приложения. +-- 2. Пользователи получат null в колонке, оно просто не пройдет условие. + + +-- 4 Задача +SELECT name, + created_at::date AS created_date, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN is_active = TRUE THEN 'hot' + ELSE 'regular' + END AS hot_flag +FROM users +WHERE is_active = TRUE + AND created_at >= CURRENT_DATE - INTERVAL '30 days'; +-- Ответ на вопросы +-- 1. В WHERE мы получим только пользователей за последние 30 дней а в CASE мы просто получаем метку 'hot' или 'regular'. +-- И в CASE мы не фильтруем по каким либо параметрам а только прописываем условие что записать. +-- 2. Какие нибудь рекомендации или скидку на первый заказ. + + +-- 5 Задача +SELECT COUNT(*) AS total_users, + COUNT(email) AS with_email, + COUNT(*) - COUNT(email) AS without_email, + ROUND(COUNT(country) * 100.0 / COUNT(*), 2) AS percentage_users +FROM users; +-- Ответ на вопросы +-- 1. Так гораздо надежнее и правильнее. +-- 2. Установить ограничение на создание пользователя, чтоб поле email было не нулевым. + + +-- 6 Задача +SELECT name, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN created_at >= CURRENT_DATE - INTERVAL '30 days' THEN 'new' + WHEN created_at < CURRENT_DATE - INTERVAL '30 days' + AND created_at > CURRENT_DATE - INTERVAL '180 days' THEN 'middle' + WHEN created_at < CURRENT_DATE - INTERVAL '180 days' THEN 'old' + END AS tenure_group, + is_active +FROM users +ORDER BY created_at >= CURRENT_DATE - INTERVAL '30 days', + created_at < CURRENT_DATE - INTERVAL '30 days' + AND created_at > CURRENT_DATE - INTERVAL '180 days', + created_at < CURRENT_DATE - INTERVAL '180 days', + days_since_registration; +-- Ответ на вопросы +-- 1. В сегментации по возрасту показывает возраст пользователя, +-- а в сегментации стаж показывает на сколько долго пользователь пользуется сервисом(приложением) +-- 2. CURRENT_DATE показывает сегодняшний день и если tenure_group пользоваться каждый день, то будет смещатся интервал в логическом выражении CASE. +-- В статическом поле дание остаются записанными пока их не изменят, течение времени на них никак не будет влиять. From f8c0af41cae885e75fdf1acae485fd0f1251e717 Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Wed, 27 May 2026 10:37:52 +0300 Subject: [PATCH 170/179] Add homework on SQL CTE --- vberetar/postgresql/sql_cte.sql | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 vberetar/postgresql/sql_cte.sql diff --git a/vberetar/postgresql/sql_cte.sql b/vberetar/postgresql/sql_cte.sql new file mode 100644 index 0000000..080d9bc --- /dev/null +++ b/vberetar/postgresql/sql_cte.sql @@ -0,0 +1,118 @@ +-- Задача 1 +SELECT u.name +FROM users u +WHERE id IN ( + SELECT o.user_id + FROM orders o +) +ORDER BY u.name; +-- Ответ на вопросы +-- 1. JOIN склеивает одну таблицу со второй, а подзапрос с IN выполняет запрос и получает список с user_id, +-- далее по этому списку отфильтровывает пользователей в основном запросе. +-- 2. Когда запрос простой и короткий. + + +-- Задача 2 +SELECT u.id, + u.name, + u.email +FROM users u +WHERE NOT EXISTS ( + SELECT 1 + FROM orders o + WHERE o.user_id = u.id +); +-- Ответ на вопросы +-- 1. Предположу что NOT EXISTS более бещопасно работает с null. +-- 2. Отправить таким пользователям рекомендацию, специальный промокод на скидку для первого заказа или рекламу. + + +-- Задача 3 +SELECT u.name, + ( + SELECT COUNT(*) + FROM orders o + WHERE u.id = o.user_id + ) AS orders_count +FROM users u +ORDER BY orders_count DESC, name; +-- Ответ на вопросы +-- 1. Если подзапрос SELECT вернётся две и более строки то будет ошибка. +-- 2. Сложность возрастет и скорее всего нужно будет оптимизировать запрос. + + +-- Задача 4 +WITH total_revenue_cte AS ( + SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_price + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.user_id +) +SELECT u.name, + trc.total_price AS total_revenue +FROM users u +JOIN total_revenue_cte trc ON trc.user_id = u.id +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Проще читать запрос "посчитать выручку по пользователям" когда вынесен отдельно. +-- 2. Вроде как в postgres можно в базу данных записывать функции, если я ничего не путаю. +-- И далее вызывать эту функцию при необходимости, так можно избежать дублирования кода. + + +-- Задача 5 +WITH product_price_at_purchase_cte AS ( + SELECT oi.product_id, + SUM(oi.quantity * oi.price_at_purchase) AS product_revenue + FROM order_items oi + GROUP BY oi.product_id +) +SELECT p.name AS product_name, + p.category, + ppps.product_revenue +FROM products p +JOIN product_price_at_purchase_cte ppps ON ppps.product_id = p.id +ORDER BY ppps.product_revenue DESC +LIMIT 3; +-- Ответ на вопросы +-- 1. Возможно потому что products оcновная таблица, сналчала нужно из order_items достать агрегацию по id а потом склеивать с products. +-- 2. Через CTE мы получаем готовую таблицу и далее делаем с ней что хотим, +-- а в гуруппировке через GROUP BY нельзя добавить дополнительные поля name и category. + + +-- Задача 6 +WITH total_revenue_cte AS ( + SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.user_id +) +SELECT u.name, + u.email, + trc.total_revenue +FROM users u +JOIN total_revenue_cte trc ON trc.user_id = u.id +WHERE u.is_active = TRUE AND trc.total_revenue > 1000 +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Думаю как то так - user_most_expenses +-- 2. Можно предлагать рекламу товаров, анализировать что пользователи покупали и потом предлагать схожую продукцию. + + +WITH order_revenue_cte AS ( + SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue + FROM order_items + GROUP BY order_id +) +SELECT o.id, + o.order_date, + oc.order_revenue +FROM orders o +JOIN order_revenue_cte oc ON oc.order_id = o.id +WHERE o.status = 'paid' AND oc.order_revenue < 100; +-- Ответ на вопросы +-- 1. Какие реальные причины таких заказов, я думаю что просто не все люди богатые что бы заказывать на большие суммы (если я правильно понял статус paid) +-- Чтобы узнать злоупотребления промокодами нужно копаться в других данных, где хранятся данные о введенных промокодах. +-- 2. Я думаю выгоднее фильтровать внутри подзапроса, чтоб подзапрос не создавал большую таблицу и основному запросу не пришлось ее перебирать. From 1af6c81db1700279b4ffb6e7298f161969a093fc Mon Sep 17 00:00:00 2001 From: dmezentsev Date: Wed, 27 May 2026 13:45:45 +0300 Subject: [PATCH 171/179] add pet-project --- dmezentsev/postgresql/pet_project/DDL_DML.sql | 205 ++++++++++++++++++ .../pet_project/dm_conversion_funnel.sql | 105 +++++++++ .../pet_project/dm_course_summary.sql | 46 ++++ .../dm_lesson_popularity_ranking.sql | 46 ++++ .../pet_project/dm_platform_summary.sql | 19 ++ .../pet_project/dm_user_activity_summary.sql | 46 ++++ .../pet_project/dm_user_cohort_analysis.sql | 77 +++++++ dmezentsev/postgresql/pet_project/extra.sql | 58 +++++ 8 files changed, 602 insertions(+) create mode 100644 dmezentsev/postgresql/pet_project/DDL_DML.sql create mode 100644 dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql create mode 100644 dmezentsev/postgresql/pet_project/dm_course_summary.sql create mode 100644 dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql create mode 100644 dmezentsev/postgresql/pet_project/dm_platform_summary.sql create mode 100644 dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql create mode 100644 dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql create mode 100644 dmezentsev/postgresql/pet_project/extra.sql diff --git a/dmezentsev/postgresql/pet_project/DDL_DML.sql b/dmezentsev/postgresql/pet_project/DDL_DML.sql new file mode 100644 index 0000000..00b36ad --- /dev/null +++ b/dmezentsev/postgresql/pet_project/DDL_DML.sql @@ -0,0 +1,205 @@ +DROP TABLE IF EXISTS dmezentsev.lesson_views; +DROP TABLE IF EXISTS dmezentsev.enrollments; +DROP TABLE IF EXISTS dmezentsev.lessons; +DROP TABLE IF EXISTS dmezentsev.users_; +DROP TABLE IF EXISTS dmezentsev.courses; + +CREATE TABLE dmezentsev.users_( + id BIGINT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + age SMALLINT, + email VARCHAR(100) UNIQUE, + registration_date DATE NOT NULL +); + +CREATE TABLE dmezentsev.courses ( + id BIGINT PRIMARY KEY, + title varchar(200) NOT NULL, + category VARCHAR(100) NOT NULL, + created_at DATE NOT NULL +); + +CREATE TABLE dmezentsev.lessons ( + id BIGINT PRIMARY KEY, + course_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + duration_min SMALLINT, + + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +CREATE TABLE dmezentsev.enrollments ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + course_id BIGINT NOT NULL, + enrolled_at DATE, + + FOREIGN KEY (user_id) REFERENCES users_(id), + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +CREATE TABLE dmezentsev.lesson_views ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + lesson_id BIGINT NOT NULL, + viewed_at TIMESTAMPTZ, + + FOREIGN KEY (user_id) REFERENCES users_(id), + FOREIGN KEY (lesson_id) REFERENCES lessons(id) +); + +INSERT INTO dmezentsev.users_ (id, name, age, email, registration_date) VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-10'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'), +(4, 'Diana', 28, 'diana@mail.com', '2023-03-01'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-05'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-12'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-20'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-10'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-15'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-20'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-15'), +(13, 'Mia', 26, 'mia@gmail.com', '2023-01-18'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-08'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-10'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-15'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-20'), +(18, 'Rita', 27, 'rita@mail.com', '2023-04-01'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-10'), +(20, 'Tina', 25, 'tina@mail.com', '2023-04-18'); + +INSERT INTO dmezentsev.courses (id, title, category, created_at) VALUES +(1, 'SQL для начинающих', 'data', '2023-01-01'), +(2, 'Python для анализа данных', 'programming', '2023-01-15'), +(3, 'BI с нуля', 'business', '2023-03-01'), +(4, 'Excel для аналитиков', 'business', '2023-02-01'), +(5, 'Основы машинного обучения', 'data', '2023-03-15'); + +INSERT INTO dmezentsev.lessons (id, course_id, title, duration_min) VALUES +(1, 1, 'SELECT и FROM', 10), +(2, 1, 'JOIN', 15), +(3, 1, 'WHERE и фильтрация', 12), +(4, 1, 'Агрегации и GROUP BY', 18), +(5, 1, 'Подзапросы', 25), +(6, 2, 'Pandas основы', 20), +(7, 2, 'NumPy', 15), +(8, 2, 'Визуализация данных', 22), +(9, 3, 'Основы BI', 12), +(10, 3, 'Метрики и дашборды', 18), +(11, 3, 'Power BI введение', 20), +(12, 4, 'Формулы в Excel', 15), +(13, 4, 'Сводные таблицы', 18), +(14, 4, 'Графики и диаграммы', 14), +(15, 5, 'Введение в ML', 20), +(16, 5, 'Линейная регрессия', 30), +(17, 5, 'Классификация', 28), +(18, 5, 'Кластеризация', 25); + +INSERT INTO dmezentsev.enrollments (id, user_id, course_id, enrolled_at) VALUES +(1, 1, 1, '2023-01-15'), +(2, 1, 2, '2023-02-01'), +(3, 2, 1, '2023-01-20'), +(4, 3, 2, '2023-03-05'), +(5, 4, 3, '2023-04-01'), +(6, 6, 1, '2023-01-06'), +(7, 6, 2, '2023-01-10'), +(8, 6, 4, '2023-02-05'), +(9, 7, 1, '2023-01-13'), +(10, 7, 3, '2023-03-10'), +(11, 8, 1, '2023-01-22'), +(12, 8, 2, '2023-01-22'), +(13, 8, 3, '2023-01-22'), +(14, 9, 4, '2023-02-12'), +(15, 10, 1, '2023-04-16'), +(16, 11, 2, '2023-04-21'), +(17, 12, 1, '2023-01-16'), +(18, 13, 2, '2023-01-19'), +(19, 13, 4, '2023-02-20'), +(20, 14, 1, '2023-01-09'), +(21, 14, 2, '2023-01-09'), +(22, 14, 3, '2023-01-09'), +(23, 14, 4, '2023-02-01'), +(24, 14, 5, '2023-03-20'), +(25, 15, 1, '2023-01-11'), +(26, 15, 3, '2023-03-05'), +(27, 16, 2, '2023-02-16'), +(28, 16, 4, '2023-02-16'), +(29, 17, 3, '2023-03-22'), +(30, 17, 5, '2023-03-25'), +(31, 18, 1, '2023-04-02'), +(32, 19, 1, '2023-04-11'), +(33, 19, 2, '2023-04-11'), +(34, 20, 2, '2023-04-19'), +(35, 5, 5, '2023-03-20'), +(36, 1, 4, '2023-02-10'), +(37, 2, 3, '2023-03-15'), +(38, 3, 1, '2023-02-25'), +(39, 4, 2, '2023-03-10'), +(40, 6, 3, '2023-03-08'), +(41, 7, 2, '2023-02-01'), +(42, 9, 1, '2023-02-15'), +(43, 12, 3, '2023-03-10'), +(44, 15, 2, '2023-02-01'), +(45, 18, 4, '2023-04-05'); + +INSERT INTO dmezentsev.lesson_views (id, user_id, lesson_id, viewed_at) VALUES +(1, 1, 1, '2023-01-16 10:00:00'), +(2, 1, 2, '2023-01-16 10:15:00'), +(3, 1, 3, '2023-01-17 09:00:00'), +(4, 1, 4, '2023-01-17 09:30:00'), +(5, 1, 4, '2023-01-17 11:00:00'), +(6, 1, 4, '2023-01-18 10:00:00'), +(7, 1, 6, '2023-02-02 14:00:00'), +(8, 1, 12, '2023-02-15 10:00:00'), +(9, 1, 13, '2023-02-15 10:30:00'), +(10, 2, 1, '2023-01-21 09:00:00'), +(11, 2, 9, '2023-03-16 10:00:00'), +(12, 2, 10, '2023-03-16 10:30:00'), +(13, 3, 6, '2023-03-06 12:00:00'), +(14, 3, 1, '2023-02-26 11:00:00'), +(15, 3, 2, '2023-02-26 11:20:00'), +(16, 4, 9, '2023-04-02 10:00:00'), +(17, 4, 10, '2023-04-03 10:00:00'), +(18, 4, 6, '2023-03-12 14:00:00'), +(19, 4, 7, '2023-03-13 09:00:00'), +(20, 5, 15, '2023-03-21 10:00:00'), +(21, 5, 16, '2023-03-22 11:00:00'), +(22, 6, 1, '2023-01-07 10:00:00'), +(23, 6, 2, '2023-01-07 10:15:00'), +(24, 6, 3, '2023-01-08 09:00:00'), +(25, 6, 4, '2023-01-08 09:30:00'), +(26, 6, 5, '2023-01-09 10:00:00'), +(27, 6, 6, '2023-01-11 14:00:00'), +(28, 6, 7, '2023-01-12 10:00:00'), +(29, 6, 8, '2023-01-13 11:00:00'), +(30, 6, 12, '2023-02-06 09:00:00'), +(31, 6, 13, '2023-02-06 09:30:00'), +(32, 6, 14, '2023-02-07 10:00:00'), +(33, 6, 9, '2023-03-09 11:00:00'), +(34, 7, 1, '2023-01-14 10:00:00'), +(35, 7, 2, '2023-01-14 10:20:00'), +(36, 7, 3, '2023-01-15 09:00:00'), +(37, 7, 9, '2023-03-11 14:00:00'), +(38, 7, 10, '2023-03-11 14:30:00'), +(39, 9, 12, '2023-02-13 10:00:00'), +(40, 9, 13, '2023-02-13 10:30:00'), +(41, 9, 1, '2023-02-16 11:00:00'), +(42, 12, 1, '2023-01-17 10:00:00'), +(43, 12, 2, '2023-01-17 10:20:00'), +(44, 12, 1, '2023-04-10 11:00:00'), +(45, 12, 3, '2023-04-10 11:30:00'), +(46, 12, 9, '2023-03-12 10:00:00'), +(47, 13, 6, '2023-01-20 14:00:00'), +(48, 13, 7, '2023-01-21 10:00:00'), +(49, 13, 12, '2023-02-21 09:00:00'), +(50, 14, 1, '2023-01-09 08:00:00'), +(51, 14, 2, '2023-01-09 08:15:00'), +(52, 14, 3, '2023-01-10 10:00:00'), +(53, 14, 4, '2023-01-10 10:30:00'), +(54, 14, 5, '2023-01-11 09:00:00'), +(55, 14, 6, '2023-01-10 14:00:00'), +(56, 14, 7, '2023-01-11 10:00:00'), +(57, 14, 8, '2023-01-12 11:00:00'), +(58, 14, 9, '2023-01-10 08:00:00'); \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql b/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql new file mode 100644 index 0000000..493f553 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql @@ -0,0 +1,105 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.conversion_funnel; +CREATE MATERIALIZED VIEW dmezentsev.conversion_funnel AS +WITH progress AS ( + -- Прогресс каждого пользователя по каждому курсу + SELECT + e.course_id, + e.user_id, + COUNT(DISTINCT l.id) AS total_lessons, + COUNT(DISTINCT lv.lesson_id) AS viewed_lessons, + ROUND(COUNT(DISTINCT lv.lesson_id) * 100.0 / NULLIF(COUNT(DISTINCT l.id), 0), 2) AS viewed_pct + FROM dmezentsev.enrollments e + LEFT JOIN dmezentsev.lessons l ON e.course_id = l.course_id + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id AND e.user_id = lv.user_id + GROUP BY e.course_id, e.user_id +), + +funnel_stats AS ( + -- Статистика по каждому курсу для всех этапов воронки + SELECT + c.id AS course_id, + c.title AS course_title, + (SELECT COUNT(*) FROM dmezentsev.users_) AS registered_count, + COUNT(DISTINCT e.user_id) AS enrolled_count, + COUNT(DISTINCT CASE WHEN p.viewed_lessons > 0 THEN e.user_id END) AS started_count, + COUNT(DISTINCT CASE WHEN p.viewed_pct >= 50 THEN e.user_id END) AS mid_progress_count, + COUNT(DISTINCT CASE WHEN p.viewed_pct = 100 THEN e.user_id END) AS completed_count + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + LEFT JOIN progress p ON e.course_id = p.course_id AND e.user_id = p.user_id + GROUP BY c.id, c.title +) + +-- Соединяем этапы с помощью UNION ALL +SELECT + course_id, + course_title, + stage_name, + users_count, + conversion_from_previous, + conversion_from_start +FROM ( + -- Этап 1: registered + SELECT + course_id, + course_title, + 'registered' AS stage_name, + registered_count AS users_count, + 0 AS conversion_from_previous, + 100.00 AS conversion_from_start, + 1 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 2: enrolled + SELECT + course_id, + course_title, + 'enrolled', + enrolled_count, + ROUND(100.0 * enrolled_count / NULLIF(registered_count, 0), 2), + ROUND(100.0 * enrolled_count / NULLIF(registered_count, 0), 2), + 2 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 3: started + SELECT + course_id, + course_title, + 'started', + started_count, + ROUND(100.0 * started_count / NULLIF(enrolled_count, 0), 2), + ROUND(100.0 * started_count / NULLIF(registered_count, 0), 2), + 3 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 4: mid_progress + SELECT + course_id, + course_title, + 'mid_progress', + mid_progress_count, + ROUND(100.0 * mid_progress_count / NULLIF(started_count, 0), 2), + ROUND(100.0 * mid_progress_count / NULLIF(registered_count, 0), 2), + 4 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 5: completed + SELECT + course_id, + course_title, + 'completed', + completed_count, + ROUND(100.0 * completed_count / NULLIF(mid_progress_count, 0), 2), + ROUND(100.0 * completed_count / NULLIF(registered_count, 0), 2), + 5 AS stage_order + FROM funnel_stats +) AS funnel_rows +ORDER BY course_id, stage_order; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_course_summary.sql b/dmezentsev/postgresql/pet_project/dm_course_summary.sql new file mode 100644 index 0000000..86a65b3 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_course_summary.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.course_summary; +CREATE MATERIALIZED VIEW dmezentsev.course_summary AS +WITH count_users AS ( + -- Кол-во уникальных пользователей в курсах + SELECT + c.id AS course_id, + c.title AS course_title, + COUNT(DISTINCT e.user_id) AS unique_users + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + GROUP BY c.id, c.title +), + count_lessons AS ( + -- Кол-во уроков в курсах + SELECT + course_id, + COUNT(id) AS lessons_count + FROM dmezentsev.lessons + GROUP BY course_id +), + views_count AS ( + -- Суммарное число просмотров по каждому курсу + SELECT + l.course_id, + COUNT(lv.id) AS total_views, + MIN(lv.viewed_at) AS first_viewed_at, + MAX(lv.viewed_at) AS last_viewed_at + FROM dmezentsev.lessons l + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id + GROUP BY l.course_id +) + +SELECT + cu.course_id, + cu.course_title, + cu.unique_users, + cl.lessons_count, + vc.total_views, + ROUND(COALESCE(vc.total_views::numeric / + cu.unique_users, 0), 2) AS avg_views_per_user, + vc.first_viewed_at, + vc.last_viewed_at +FROM count_users cu +LEFT JOIN count_lessons cl USING (course_id) +LEFT JOIN views_count vc USING (course_id) +ORDER BY cu.course_id; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql b/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql new file mode 100644 index 0000000..50bf2bb --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.lesson_popularity_ranking; +CREATE MATERIALIZED VIEW dmezentsev.lesson_popularity_ranking AS +WITH +course_enrollment AS ( + -- Сколько студентов записано на каждый курс + SELECT + c.id AS course_id, + COUNT(DISTINCT e.user_id) AS enrolled_to_course + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + GROUP BY c.id +), + +lesson_summary AS ( + -- Статистика по урокам + SELECT + l.id AS lesson_id, + l.title AS lesson_title, + c.id AS course_id, + c.title AS course_title, + COUNT(lv.id) AS total_views, + COUNT(DISTINCT lv.user_id) AS unique_viewers, + ROUND( + COUNT(lv.id)::NUMERIC / NULLIF(COUNT(DISTINCT lv.user_id), 0), + 2 + ) AS avg_views_per_viewer + FROM dmezentsev.lessons l + JOIN dmezentsev.courses c ON l.course_id = c.id + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id + GROUP BY l.id, l.title, c.id, course_id, c.title +) + +SELECT + ls.lesson_id, + ls.lesson_title, + ls.course_title, + ls.total_views, + ls.unique_viewers, + ls.avg_views_per_viewer, + DENSE_RANK() OVER (ORDER BY ls.total_views DESC) AS popularity_rank, + ROUND( + ls.unique_viewers::NUMERIC / NULLIF(ce.enrolled_to_course, 0) * 100, 2 + ) AS completion_rate +FROM lesson_summary ls +LEFT JOIN course_enrollment ce ON ls.course_id = ce.course_id +ORDER BY popularity_rank; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_platform_summary.sql b/dmezentsev/postgresql/pet_project/dm_platform_summary.sql new file mode 100644 index 0000000..a276ed0 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_platform_summary.sql @@ -0,0 +1,19 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.platform_summary; +CREATE MATERIALIZED VIEW dmezentsev.platform_summary AS +WITH summary AS ( + SELECT + (SELECT COUNT(*) FROM dmezentsev.users_) AS total_users, + (SELECT COUNT(*) FROM dmezentsev.courses) AS total_courses, + (SELECT COUNT(*) FROM dmezentsev.lessons) AS total_lessons, + (SELECT COUNT(DISTINCT user_id) FROM dmezentsev.lesson_views) AS users_with_views, + (SELECT COUNT(*) FROM dmezentsev.lesson_views) AS total_views +) + +SELECT + total_users, + total_courses, + total_lessons, + users_with_views, + ROUND(total_lessons::numeric / total_courses, 2) AS avg_lessons_per_course, + ROUND(total_views::numeric / total_lessons, 2) AS avg_views_per_lesson +FROM summary; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql new file mode 100644 index 0000000..699f678 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.user_activity_summary; +CREATE MATERIALIZED VIEW dmezentsev.user_activity_summary AS +WITH user_enroll AS ( + -- На сколько курсов записался пользователь + SELECT + u.id AS user_id, + COUNT(e.id) AS enrollment_count + FROM dmezentsev.users_ u + JOIN dmezentsev.enrollments e ON u.id = e.user_id + GROUP BY u.id +), + user_views AS ( + -- Сколько уроков в курсах посмотрел пользователь, и даты + -- первого и последнего просмотра + SELECT + user_id, + COUNT(DISTINCT lesson_id) viewed_lessons_count, + MIN(viewed_at) AS first_view, + MAX(viewed_at) AS last_view + FROM dmezentsev.lesson_views lv + GROUP BY lv.user_id +), +all_lessons AS ( + -- сколько уроков в курсах доступно пользователю + SELECT + e.user_id, + COUNT(l.id) AS possible_lessons + FROM dmezentsev.enrollments e + JOIN dmezentsev.courses c ON e.course_id = c.id + JOIN dmezentsev.lessons l ON c.id = l.course_id + GROUP BY e.user_id +) + +SELECT + u.id AS user_id, + u.name, + ue.enrollment_count, + COALESCE(uv.viewed_lessons_count, 0) AS viewed_lessons_count, + uv.last_view, + uv.first_view, + ROUND(uv.viewed_lessons_count::numeric / al.possible_lessons * 100, 2) AS engagement_rate +FROM users_ u +JOIN user_enroll ue ON ue.user_id = u.id +JOIN all_lessons al ON al.user_id = u.id +LEFT JOIN user_views uv ON uv.user_id = u.id +ORDER BY user_id; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql new file mode 100644 index 0000000..f7d8008 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql @@ -0,0 +1,77 @@ +DROP MATERIALIZED VIEW IF EXISTS user_cohort_analysis; +CREATE MATERIALIZED VIEW user_cohort_analysis AS +WITH users_in_cohort AS ( + -- Кол-во зарегистрированных пользователей по когортам + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS users_in_cohort + FROM dmezentsev.users_ u + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + enrolled_users AS ( + -- Кол-во пользователей по когортам, записавшиеся хотя бы на 1 курс + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS enrolled_users + FROM dmezentsev.users_ u + WHERE EXISTS (SELECT 1 + FROM dmezentsev.enrollments e + WHERE u.id = e.user_id) + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + active_users AS ( + -- Кол-во пользователей по когортам, посмотревшие хотя бы 1 урок + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS active_users + FROM dmezentsev.users_ u + WHERE EXISTS (SELECT 1 + FROM dmezentsev.lesson_views lv + WHERE u.id = lv.user_id) + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + avg_courses AS ( + -- Среднее количество курсов на пользователя в когорте + SELECT + TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') AS cohort_month, + ROUND(COUNT(e.course_id)::numeric / COUNT(DISTINCT u.id), 2) AS avg_courses_per_user + FROM dmezentsev.users_ u + LEFT JOIN dmezentsev.enrollments e ON u.id = e.user_id + GROUP BY TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') +), +avg_time_to_first_view AS ( + -- Среднее время от регистрации до первого просмотра + SELECT + TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') AS cohort_month, + ROUND(AVG(lv.first_view_date::date - u.registration_date), 2) AS avg_time_to_first_view + FROM dmezentsev.users_ u + INNER JOIN ( + SELECT + user_id, + MIN(viewed_at) AS first_view_date + FROM dmezentsev.lesson_views + GROUP BY user_id + ) lv ON u.id = lv.user_id + -- у пользователя Bob, id = 2 дата первого просмотра раньше даты регистрации, отфильтровал его + WHERE lv.first_view_date >= u.registration_date + GROUP BY TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') +) + +SELECT + uin.cohort_month, + uin.users_in_cohort, + COALESCE(eu.enrolled_users, 0) AS enrolled_users, + COALESCE(au.active_users, 0) AS active_users, + CASE + WHEN uin.users_in_cohort > 0 THEN + ROUND(100.0 * COALESCE(au.active_users, 0) / uin.users_in_cohort, 2) + ELSE 0 + END AS retention_rate, + ac.avg_courses_per_user, + att.avg_time_to_first_view +FROM users_in_cohort uin +LEFT JOIN enrolled_users eu USING (cohort_month) +LEFT JOIN active_users au USING (cohort_month) +LEFT JOIN avg_courses ac USING (cohort_month) +LEFT JOIN avg_time_to_first_view att USING (cohort_month) +ORDER BY uin.cohort_month; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/extra.sql b/dmezentsev/postgresql/pet_project/extra.sql new file mode 100644 index 0000000..d6f2fb5 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/extra.sql @@ -0,0 +1,58 @@ +-- Задание 1: Анализ drop-off точек +SELECT + cf.course_id, + c.title AS course_title, + cf.stage_name, + cf.users_count, + ROUND(100 - cf.conversion_from_previous, 2) AS dropoff_percentage +FROM dmezentsev.conversion_funnel cf +JOIN dmezentsev.courses c ON cf.course_id = c.id +WHERE cf.stage_name = 'started' +ORDER BY dropoff_percentage DESC; + +-- Задание 2: Топ-3 урока с повторными просмотрами +SELECT + lesson_id, + lesson_title, + course_title, + total_views, + unique_viewers, + avg_views_per_viewer, + popularity_rank +FROM dmezentsev.lesson_popularity_ranking +WHERE avg_views_per_viewer > 1 +ORDER BY avg_views_per_viewer DESC, total_views DESC; + +-- Задание 3: Лучшая когорта по retention +SELECT cohort_month, retention_rate +FROM user_cohort_analysis +ORDER BY retention_rate DESC +LIMIT 3; + + +-- Задание 4: Найди узкие места: Seq Scan, высокие cost, JOIN без индексов, DISTINCT, подзапросы в SELECT. + +/* EXPLAIN ANALYZE: + +Seq Scan on conversion_funnel (cost=0.00..11.40 rows=140 width=530) (actual time=0.007..0.011 rows=25 loops=1) +Planning Time: 0.124 ms +Execution Time: 0.027 ms + ++- по каждой вьюхе такой план: +rows=25, rows=5, rows=18 — очень мало данных +Execution Time везде маленький (0.027 - 0.330 ms) +Seq Scan — потому что таблицы небольшие + +Не придумал на данный момент, как улучшить запросы, но для ускорения запросов на больших данных +ввел бы индексацию внешних ключей +*/ + +CREATE INDEX IF NOT EXISTS idx_enrollments_user_id ON dmezentsev.enrollments(user_id); +CREATE INDEX IF NOT EXISTS idx_enrollments_course_id ON dmezentsev.enrollments(course_id); +CREATE INDEX IF NOT EXISTS idx_lessons_course_id ON dmezentsev.lessons(course_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_user_id ON dmezentsev.lesson_views(user_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_lesson_id ON dmezentsev.lesson_views(lesson_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_viewed_at ON dmezentsev.lesson_views(viewed_at); + +-- А также создал бы индекс для даты регистрации +CREATE INDEX IF NOT EXISTS idx_users_registration_date ON dmezentsev.users_(registration_date); \ No newline at end of file From f045be8a396b58696d4f51ab5aa5eb436eb2ed8f Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 28 May 2026 18:00:56 +0300 Subject: [PATCH 172/179] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20:=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BF=D0=B0=D0=BF=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv | 0 .../Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv | 0 .../Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv | 0 .../Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv | 0 .../Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv | 0 .../Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv | 0 .../Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql | 0 .../Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql | 0 .../Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql | 0 .../Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql | 0 .../Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql | 0 .../Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt | 0 .../Step_1_project_Initialization.sql | 0 .../Step_2_Loading Data}/courses_202604212117.csv | 0 .../Step_2_Loading Data}/enrollments_202604212130.csv | 0 .../Step_2_Loading Data}/lesson_views_202604212133.csv | 0 .../Step_2_Loading Data}/lessons_202604212131.csv | 0 .../Step_2_Loading Data}/users_202604212132.csv | 0 .../{Пет проект: DWH => Пет проект DWH}/Step_3_core_layer.sql | 0 .../Step_4_preparing_display_cases_at_greenplum.sql | 0 .../{Пет проект: DWH => Пет проект DWH}/Step_5_verification.sql | 0 .../1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql | 0 .../1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql | 0 .../2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql | 0 .../2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql | 0 .../2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql | 0 .../2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql | 0 .../2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql | 0 .../2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql | 0 .../3. Доп. задания/DQL_additional_tasks.sql | 0 30 files changed, 0 insertions(+), 0 deletions(-) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql (100%) rename mnoskov/clickhouse/{Пет-проект: Витринный слой => Пет-проект Витринный слой}/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt (100%) rename mnoskov/greenplum/{Пет проект: DWH => Пет проект DWH}/Step_1_project_Initialization.sql (100%) rename mnoskov/greenplum/{Пет проект: DWH/Step 2: Loading Data => Пет проект DWH/Step_2_Loading Data}/courses_202604212117.csv (100%) rename mnoskov/greenplum/{Пет проект: DWH/Step 2: Loading Data => Пет проект DWH/Step_2_Loading Data}/enrollments_202604212130.csv (100%) rename mnoskov/greenplum/{Пет проект: DWH/Step 2: Loading Data => Пет проект DWH/Step_2_Loading Data}/lesson_views_202604212133.csv (100%) rename mnoskov/greenplum/{Пет проект: DWH/Step 2: Loading Data => Пет проект DWH/Step_2_Loading Data}/lessons_202604212131.csv (100%) rename mnoskov/greenplum/{Пет проект: DWH/Step 2: Loading Data => Пет проект DWH/Step_2_Loading Data}/users_202604212132.csv (100%) rename mnoskov/greenplum/{Пет проект: DWH => Пет проект DWH}/Step_3_core_layer.sql (100%) rename mnoskov/greenplum/{Пет проект: DWH => Пет проект DWH}/Step_4_preparing_display_cases_at_greenplum.sql (100%) rename mnoskov/greenplum/{Пет проект: DWH => Пет проект DWH}/Step_5_verification.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql (100%) rename mnoskov/postgresql/{Пет-проект: Онлайн-курсы => Пет-проект Онлайн-курсы}/3. Доп. задания/DQL_additional_tasks.sql (100%) diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql diff --git a/mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt similarity index 100% rename from mnoskov/clickhouse/Пет-проект: Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt rename to mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql b/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step_1_project_Initialization.sql rename to mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/courses_202604212117.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/courses_202604212117.csv rename to mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/enrollments_202604212130.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/enrollments_202604212130.csv rename to mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lesson_views_202604212133.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lesson_views_202604212133.csv rename to mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lessons_202604212131.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/lessons_202604212131.csv rename to mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv diff --git a/mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/users_202604212132.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step 2: Loading Data/users_202604212132.csv rename to mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step_3_core_layer.sql rename to mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql b/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step_4_preparing_display_cases_at_greenplum.sql rename to mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql diff --git a/mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql b/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql similarity index 100% rename from mnoskov/greenplum/Пет проект: DWH/Step_5_verification.sql rename to mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql diff --git a/mnoskov/postgresql/Пет-проект: Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql similarity index 100% rename from mnoskov/postgresql/Пет-проект: Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql rename to mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql From dd868283ed7b8c75022021b7ab5c4daed259aa0c Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 28 May 2026 18:13:59 +0300 Subject: [PATCH 173/179] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B0=20=D0=B4=D0=B2=D0=BE=D0=B5=D1=82=D0=BE=D1=87=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DQL_functions_practice.sql | 0 .../{5. Индексы: Алгоритмы => 5. Индексы Алгоритмы}/practice_indexes_algorithms.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename mnoskov/postgresql/SQL с нуля/{5. Функции: даты, строки, логика => 5. Функции даты, строки, логика}/DQL_functions_practice.sql (100%) rename mnoskov/Теория БД/1. Ускорение и масштабирование/{5. Индексы: Алгоритмы => 5. Индексы Алгоритмы}/practice_indexes_algorithms.sql (100%) diff --git a/mnoskov/postgresql/SQL с нуля/5. Функции: даты, строки, логика/DQL_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql similarity index 100% rename from mnoskov/postgresql/SQL с нуля/5. Функции: даты, строки, логика/DQL_functions_practice.sql rename to mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы: Алгоритмы/practice_indexes_algorithms.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql similarity index 100% rename from mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы: Алгоритмы/practice_indexes_algorithms.sql rename to mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql From a547afe5dbaa0b52f7dd4bf3c736e1cc18c6738f Mon Sep 17 00:00:00 2001 From: mnoskov Date: Thu, 28 May 2026 18:17:25 +0300 Subject: [PATCH 174/179] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice_arrays_maps_and_nested_structures.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mnoskov/clickhouse/Практика clickhouse/2. Запросы/{3. Массивы, Map и вложенные структуры* => 3. Массивы, Map и вложенные структуры}/practice_arrays_maps_and_nested_structures.sql (100%) diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры*/practice_arrays_maps_and_nested_structures.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql similarity index 100% rename from mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры*/practice_arrays_maps_and_nested_structures.sql rename to mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql From 3428dfeab78e6d392ab8f8b438b1fbb0d1b13ebd Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Mon, 1 Jun 2026 17:26:44 +0300 Subject: [PATCH 175/179] Add homework on SQL view --- vberetar/postgresql/sql_view.sql | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 vberetar/postgresql/sql_view.sql diff --git a/vberetar/postgresql/sql_view.sql b/vberetar/postgresql/sql_view.sql new file mode 100644 index 0000000..333c660 --- /dev/null +++ b/vberetar/postgresql/sql_view.sql @@ -0,0 +1,69 @@ +-- Задание 1 +CREATE MATERIALIZED VIEW my_category_revenue AS + SELECT + p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + JOIN products p ON p.id = oi.product_id + WHERE o.status = 'paid' + GROUP BY p.category; + +SELECT * FROM my_category_revenue +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Не нужно писать длинные запросы и результат будет уже подсчитан. + + +-- Задание 2 +CREATE MATERIALIZED VIEW mv_user_activity AS + SELECT + u.id AS user_id, + u.name AS user_name, + COUNT(o.user_id) AS orders_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' + GROUP BY u.id, u.name; + +SELECT * FROM mv_user_activity +WHERE orders_count > 0 +ORDER BY orders_count DESC; +-- Ответ на вопросы +-- 1. Я так понял, что если критерий "самых лояльных" клиентов это колличество оплаченных заказов, +-- то "самые лояльные" это те у кого этих заказов больше всего. +-- 2. Покажет "самых лояльных" клиентов за последный месяц, кто-то из клиетов может сделать много заказов за 1-2 месяца, а кто то за год по чуть чуть заказывает. +-- Возможно еще какие то из клиентов ориентируются на "сезонные" товары и у них будет сплеск продажи за последний месяц, или кто то из клиентов покупал много за год, а в этом месяце перестал делать заказы. + + +-- Задание 3 +CREATE MATERIALIZED VIEW mv_daily_revenue AS + SELECT + o.order_date::date AS sales_date, + SUM(oi.quantity * price_at_purchase) AS daily_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + WHERE o.status = 'paid' + GROUP BY o.order_date::date; + +SELECT * FROM mv_daily_revenue +ORDER BY sales_date DESC; +-- Ответ на вопросы +-- 1. «Просадки» и «пики» будут видны в колонке daily_revenue +-- 2. Быстрее считывать с готовой витрины mv_daily_revenuе, на лету из сырых таблиц дольше будет обрабатываться запрос. + + +-- Задание 4 +REFRESH MATERIALIZED VIEW mv_daily_revenue; +-- Расписание интернет‑магазина с несколькими заказами в день: +-- Для такого сценария подойдет обновление витрины раз в неделю по ночам. +-- Так как заказов не так уж и много, можно обновлять статискиту чаще. +-- Ночью, потому что заказов будет меньше в то время суток и бд быстрее обработает ззапрос. + +-- Расписание крупного магазина с тысячами заказов в час: +-- Для такого сценария подойдет обновление витрин раз в месяц - раз в квартал по ночам, +-- Так как данных много и частые расчеты будут сильно нагружать базу данных. +-- Возможно, нужно будет получить данные аналитикам раньше указанных сроков. + +-- Ответ на вопросы +-- 1. Данные будут устаревшими, из за того результат работы аналитиков будут не достоверными. +-- 2. Слишком частые обновления витрит будут сильно нагружать БД. From 536a5c41c7ae5d62ca624d65edd5228533c78d7a Mon Sep 17 00:00:00 2001 From: "valera.beretar" Date: Mon, 1 Jun 2026 17:57:09 +0300 Subject: [PATCH 176/179] Fix remarks --- .../postgresql/sql_dates_strings_logic.sql | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/vberetar/postgresql/sql_dates_strings_logic.sql b/vberetar/postgresql/sql_dates_strings_logic.sql index 6401815..447954a 100644 --- a/vberetar/postgresql/sql_dates_strings_logic.sql +++ b/vberetar/postgresql/sql_dates_strings_logic.sql @@ -13,7 +13,7 @@ ORDER BY days_since_registration DESC; -- 2 Задача SELECT name, LOWER(email) AS email_clean, - SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain + SUBSTRING(LOWER(email) FROM POSITION('@' IN LOWER(email)) + 1) AS domain FROM users WHERE email IS NOT NULL ORDER BY domain, name; @@ -30,6 +30,7 @@ SELECT name, WHEN age < 25 THEN 'young' WHEN age BETWEEN 25 AND 35 THEN 'adult' WHEN age > 35 THEN 'senior' + ELSE 'unknown' END AS age_group FROM users ORDER BY age; @@ -70,17 +71,17 @@ FROM users; SELECT name, CURRENT_DATE - created_at::date AS days_since_registration, CASE - WHEN created_at >= CURRENT_DATE - INTERVAL '30 days' THEN 'new' - WHEN created_at < CURRENT_DATE - INTERVAL '30 days' - AND created_at > CURRENT_DATE - INTERVAL '180 days' THEN 'middle' - WHEN created_at < CURRENT_DATE - INTERVAL '180 days' THEN 'old' + WHEN created_at::date >= CURRENT_DATE - INTERVAL '30 days' THEN 'new' + WHEN created_at::date < CURRENT_DATE - INTERVAL '30 days' + AND created_at::date > CURRENT_DATE - INTERVAL '180 days' THEN 'middle' + ELSE 'old' END AS tenure_group, is_active FROM users -ORDER BY created_at >= CURRENT_DATE - INTERVAL '30 days', - created_at < CURRENT_DATE - INTERVAL '30 days' - AND created_at > CURRENT_DATE - INTERVAL '180 days', - created_at < CURRENT_DATE - INTERVAL '180 days', +ORDER BY created_at::date >= CURRENT_DATE - INTERVAL '30 days', + created_at::date < CURRENT_DATE - INTERVAL '30 days' + AND created_at::date > CURRENT_DATE - INTERVAL '180 days', + created_at::date < CURRENT_DATE - INTERVAL '180 days', days_since_registration; -- Ответ на вопросы -- 1. В сегментации по возрасту показывает возраст пользователя, From bb11bb45ede0bebaec1ce5fe12263b605cd788ab Mon Sep 17 00:00:00 2001 From: dmezentsev Date: Thu, 4 Jun 2026 15:00:59 +0300 Subject: [PATCH 177/179] fix some mistakes --- dmezentsev/postgresql/homework/Views.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/dmezentsev/postgresql/homework/Views.sql b/dmezentsev/postgresql/homework/Views.sql index 8454a63..6dbcc00 100755 --- a/dmezentsev/postgresql/homework/Views.sql +++ b/dmezentsev/postgresql/homework/Views.sql @@ -16,6 +16,7 @@ SELECT p.category, FROM dmezentsev.products p JOIN dmezentsev.orders_items oi ON p.id = oi.product_id JOIN dmezentsev.orders o ON oi.order_id = o.id +GROUP BY p.category WHERE o.status = 'paid' ORDER BY total_revenue DESC From fc41b7ce31a6917a57207337beafd1aee2367f9c Mon Sep 17 00:00:00 2001 From: pavel Date: Sat, 6 Jun 2026 21:22:55 +0200 Subject: [PATCH 178/179] fix(sql):window func - hw mistakes corrected --- .../postgresql/homework_window_functions.sql | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pmikhailov/postgresql/homework_window_functions.sql b/pmikhailov/postgresql/homework_window_functions.sql index 81b0974..4b799ea 100644 --- a/pmikhailov/postgresql/homework_window_functions.sql +++ b/pmikhailov/postgresql/homework_window_functions.sql @@ -19,12 +19,13 @@ INNER JOIN WITH order_revenue_cte AS ( SELECT oi.order_id, - oi.quantity * oi.price_at_purchase AS order_revenue + sum(oi.quantity * oi.price_at_purchase) AS order_revenue -- добавил агреграт по всем поз. в заказе FROM order_items oi + GROUP BY oi.order_id ) SELECT - u.id, + u.id AS user_id , u."name", o.id AS order_id, o.order_date, @@ -32,7 +33,6 @@ SELECT sum(orc.order_revenue) OVER ( PARTITION BY u.id ORDER BY o.order_date - ROWS UNBOUNDED PRECEDING ) AS running_revenue_for_user FROM users u @@ -40,6 +40,8 @@ INNER JOIN orders o ON u.id = o.user_id INNER JOIN order_revenue_cte orc ON o.id = orc.order_id +WHERE o.status NOT IN ('cancelled', 'refunded') -- добавил условие не включать отмененные заказы + /* Задание 3 */ @@ -47,14 +49,13 @@ INNER JOIN WITH user_revenue_cte AS ( SELECT o.user_id, - oi.quantity * oi.price_at_purchase AS order_revenue, - sum(oi.quantity * oi.price_at_purchase) OVER ( - PARTITION BY o.user_id) AS user_total_revenue + sum(oi.quantity * oi.price_at_purchase) AS user_total_revenue FROM orders o INNER JOIN - order_items oi ON o.id = oi.order_id) -SELECT DISTINCT + order_items oi ON o.id = oi.order_id + GROUP BY o.user_id) -- добавил group by, костыль убран +SELECT u."name", urc.user_total_revenue, RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_rank, @@ -62,7 +63,7 @@ SELECT DISTINCT FROM users u INNER JOIN - user_revenue_cte urc ON u.id = urc.user_id + user_revenue_cte urc ON u.id = urc.user_id; /* Задание 4 */ @@ -104,10 +105,10 @@ INNER JOIN WITH order_revenue_cte AS ( SELECT oi.order_id, - oi.quantity * oi.price_at_purchase AS order_revenue + sum(oi.quantity * oi.price_at_purchase) AS order_revenue FROM order_items oi - ) + GROUP BY oi.order_id) -- добавил group by по order_id, теперь это сумма всего заказа, а не строки SELECT u.name, o.id, @@ -122,7 +123,7 @@ FROM INNER JOIN orders o ON u.id = o.user_id INNER JOIN - order_revenue_cte orc ON o.id = orc.order_id + order_revenue_cte orc ON o.id = orc.order_id; /* Задание 7 */ From 4e91bb783e651bdede8e82de9e9dd649b7a5f67c Mon Sep 17 00:00:00 2001 From: dmezentsev Date: Mon, 8 Jun 2026 09:47:15 +0300 Subject: [PATCH 179/179] fix pet-project --- dmezentsev/postgresql/pet_project/DDL_DML.sql | 10 +++++----- .../pet_project/dm_user_activity_summary.sql | 4 ++-- .../postgresql/pet_project/dm_user_cohort_analysis.sql | 4 ++-- dmezentsev/postgresql/pet_project/extra.sql | 5 +++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/dmezentsev/postgresql/pet_project/DDL_DML.sql b/dmezentsev/postgresql/pet_project/DDL_DML.sql index 00b36ad..498ef54 100644 --- a/dmezentsev/postgresql/pet_project/DDL_DML.sql +++ b/dmezentsev/postgresql/pet_project/DDL_DML.sql @@ -25,7 +25,7 @@ CREATE TABLE dmezentsev.lessons ( title VARCHAR(200) NOT NULL, duration_min SMALLINT, - FOREIGN KEY (course_id) REFERENCES courses(id) + FOREIGN KEY (course_id) REFERENCES dmezentsev.courses(id) ); CREATE TABLE dmezentsev.enrollments ( @@ -34,8 +34,8 @@ CREATE TABLE dmezentsev.enrollments ( course_id BIGINT NOT NULL, enrolled_at DATE, - FOREIGN KEY (user_id) REFERENCES users_(id), - FOREIGN KEY (course_id) REFERENCES courses(id) + FOREIGN KEY (user_id) REFERENCES dmezentsev.users_(id), + FOREIGN KEY (course_id) REFERENCES dmezentsev.courses(id) ); CREATE TABLE dmezentsev.lesson_views ( @@ -44,8 +44,8 @@ CREATE TABLE dmezentsev.lesson_views ( lesson_id BIGINT NOT NULL, viewed_at TIMESTAMPTZ, - FOREIGN KEY (user_id) REFERENCES users_(id), - FOREIGN KEY (lesson_id) REFERENCES lessons(id) + FOREIGN KEY (user_id) REFERENCES dmezentsev.users_(id), + FOREIGN KEY (lesson_id) REFERENCES dmezentsev.lessons(id) ); INSERT INTO dmezentsev.users_ (id, name, age, email, registration_date) VALUES diff --git a/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql index 699f678..af4bd02 100644 --- a/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql +++ b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql @@ -38,8 +38,8 @@ SELECT COALESCE(uv.viewed_lessons_count, 0) AS viewed_lessons_count, uv.last_view, uv.first_view, - ROUND(uv.viewed_lessons_count::numeric / al.possible_lessons * 100, 2) AS engagement_rate -FROM users_ u + ROUND(uv.viewed_lessons_count::numeric / NULLIF(al.possible_lessons, 0) * 100, 2) AS engagement_rate +FROM dmezentsev.users_ u JOIN user_enroll ue ON ue.user_id = u.id JOIN all_lessons al ON al.user_id = u.id LEFT JOIN user_views uv ON uv.user_id = u.id diff --git a/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql index f7d8008..27616d7 100644 --- a/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql +++ b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql @@ -1,5 +1,5 @@ -DROP MATERIALIZED VIEW IF EXISTS user_cohort_analysis; -CREATE MATERIALIZED VIEW user_cohort_analysis AS +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.user_cohort_analysis; +CREATE MATERIALIZED VIEW dmezentsev.user_cohort_analysis AS WITH users_in_cohort AS ( -- Кол-во зарегистрированных пользователей по когортам SELECT diff --git a/dmezentsev/postgresql/pet_project/extra.sql b/dmezentsev/postgresql/pet_project/extra.sql index d6f2fb5..aeeb803 100644 --- a/dmezentsev/postgresql/pet_project/extra.sql +++ b/dmezentsev/postgresql/pet_project/extra.sql @@ -21,11 +21,12 @@ SELECT popularity_rank FROM dmezentsev.lesson_popularity_ranking WHERE avg_views_per_viewer > 1 -ORDER BY avg_views_per_viewer DESC, total_views DESC; +ORDER BY avg_views_per_viewer DESC, total_views DESC +LIMIT 3; -- Задание 3: Лучшая когорта по retention SELECT cohort_month, retention_rate -FROM user_cohort_analysis +FROM dmezentsev.user_cohort_analysis ORDER BY retention_rate DESC LIMIT 3;