{"id":218418,"date":"2022-01-25T09:46:09","date_gmt":"2022-01-25T08:46:09","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=big-data-tools&#038;p=218418"},"modified":"2022-11-30T13:49:20","modified_gmt":"2022-11-30T12:49:20","slug":"how-i-started-out-with-dbt","status":"publish","type":"big-data-tools","link":"https:\/\/blog.jetbrains.com\/zh-hans\/big-data-tools\/2022\/01\/25\/how-i-started-out-with-dbt","title":{"rendered":"How I started out with dbt\u00ae"},"content":{"rendered":"\n<p>For some time now, I\u2019ve noticed that <a href=\"https:\/\/www.getdbt.com\/\" target=\"_blank\" rel=\"noopener\">dbt<\/a>\u00ae is gaining popularity. I\u2019ve seen more questions and more success stories, so a couple of days ago I decided to&nbsp;try it out. But what exactly is dbt anyway?<\/p>\n\n\n\n<p>Here is the first phrase you can find in its <a href=\"https:\/\/docs.getdbt.com\/docs\/introduction\" target=\"_blank\" rel=\"noopener\">documentation<\/a>: \u201cdbt (data build tool) enables analytics engineers to transform data in their warehouses by simply writing select statements. dbt handles turn these select statements into tables and views.\u201d It sounds interesting, but maybe that\u2019s not entirely clear. Here\u2019s my interpretation: dbt is a half-declarative tool for describing transformations inside a warehouse.<\/p>\n\n\n\n<p>dbt doesn\u2019t perform any extractions or loads (as in ELT); it is only responsible for transformations.<\/p>\n\n\n\n<p>A remarkable fact about dbt: it uses 2 data engineering lingua franca: SQL and YAML.<\/p>\n\n\n\n<p>So, let&#8217;s get going!<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"installation\">Installation<\/h1>\n\n\n\n<p>As dbt Core is written in Python I would usually install it with <a href=\"https:\/\/pypa.github.io\/pipx\/\" target=\"_blank\" rel=\"noopener\">pipx<\/a>. But here is the catch: there are many<a href=\"https:\/\/docs.getdbt.com\/docs\/available-adapters\" target=\"_blank\" rel=\"noopener\"> different connectors<\/a> from dbt to other databases, so installing dbt in an isolated environment may not be that beneficial in this specific situation.<\/p>\n\n\n\n<p>An alternative way is to use Python virtualenv and install dbt there, but for the sake of simplicity, I won\u2019t describe it here. Thus, I\u2019ll install dbt for Postgres with:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">pip install \u2013-user dbt-postgres<\/pre>\n\n\n\n<p>This takes some time because there are many dependencies, but not too many. The next step is to create a project.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"creating-a-project\">Creating a project<\/h1>\n\n\n\n<p>Navigate to a directory where you want to create a project and call:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dbt init<\/pre>\n\n\n\n<p>During execution, this command asks several questions: the name of the project and id of the connector. I have only one connector \u2013 postgres \u2013 so I will select it by doing the following:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1490\" height=\"888\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-5.png\" alt=\"\" class=\"wp-image-218819\"\/><\/figure>\n\n\n\n<p>Given that I\u2019ve used <code>demo-project<\/code> as the project name, the following file structure will be generated:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">demo-project\n\u251c\u2500\u2500 analyses\n\u2502   \u2514\u2500\u2500 .gitkeep\n\u251c\u2500\u2500 data\n\u2502   \u2514\u2500\u2500 .gitkeep\n\u251c\u2500\u2500 dbt_project.yml\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 macros\n\u2502   \u2514\u2500\u2500 .gitkeep\n\u251c\u2500\u2500 models\n\u2502   \u2514\u2500\u2500 example\n\u2502       \u251c\u2500\u2500 my_first_dbt_model.sql\n\u2502       \u251c\u2500\u2500 my_second_dbt_model.sql\n\u2502       \u2514\u2500\u2500 schema.yml\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 snapshots\n\u2502   \u2514\u2500\u2500 .gitkeep\n\u2514\u2500\u2500 tests\n    \u2514\u2500\u2500 .gitkeep<\/pre>\n\n\n\n<p>Also, it will create (or update) the file <code>~\/.dbt\/profiles.yml<\/code>, where database connection settings are stored.<\/p>\n\n\n\n<p>As we can see, most of the project is just empty directories. This lets us understand what the most important parts are:<\/p>\n\n\n\n<ol><li><code>dbt_project.yml<\/code> is the description of the project\u2019s global settings.<\/li><li><code>models<\/code> is the directory with the description of models we will work with.<\/li><\/ol>\n\n\n\n<p>Now it\u2019s time to understand what the model is. A model is an atomic entity of dbt describing a model of our domain. It is just a single SQL file containing a single <code data-enlighter-language=\"sql\" class=\"EnlighterJSRAW\">SELECT<\/code> statement. Later on, it will be kept in our storage as a view, materialized view, or other related entity.<\/p>\n\n\n\n<p>Besides the SQL model, we should also have a schema.yml file. This file contains a declarative description of models, tests, and types. I\u2019ll elaborate on schema.yml later.<\/p>\n\n\n\n<p>Now it\u2019s time to write the first example.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"test-schema\">Test schema<\/h1>\n\n\n\n<p>Consider the following DDL:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">CREATE TABLE IF NOT EXISTS customer\n(\n   id         BIGSERIAL PRIMARY KEY,\n   email      TEXT                                   NOT NULL,\n   username   TEXT                                   NOT NULL,\n   activated  BOOLEAN                  DEFAULT FALSE,\n   created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS post\n(\n   id         BIGSERIAL PRIMARY KEY,\n   author     BIGINT                                 NOT NULL REFERENCES customer,\n   content    TEXT,\n   created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS comment\n(\n   id         BIGSERIAL PRIMARY KEY,\n   author     BIGINT                                 NOT NULL REFERENCES customer,\n   content    TEXT,\n   created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,\n   post       BIGINT                                 NOT NULL REFERENCES post\n);<\/pre>\n\n\n\n<p>It\u2019s a straightforward schema with three tables: <code>customer<\/code>, <code>comment<\/code>, and <code>post<\/code>. Each post has an author (customer), and each comment has an author (customer) and a post. If you prefer a visual representation of tables, here it is:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1227\" height=\"778\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-7.png\" alt=\"\" class=\"wp-image-218831\"\/><\/figure>\n\n\n\n<p>Just for the sake of realism, I added several not entirely necessary fields like <code>created_at<\/code>.<\/p>\n\n\n\n<p>Now I will put some data into my DB just to have something to test my model on.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">INSERT INTO customer (id, email, username, activated, created_at) VALUES (2, 'asm0dey@jetbrains.com', 'asm0dey', true, '2022-01-13 13:59:38.029016 +00:00');\nINSERT INTO customer (id, email, username, activated, created_at) VALUES (3, 'pavel.finkelshteyn@jetbrains.com', 'asm0dey', true, '2022-01-13 18:05:22.305746 +00:00');\nINSERT INTO comment (id, author, content, created_at, post) VALUES (1, 3, 'Test Comment', '2022-01-13 18:06:08.872014 +00:00', 1);\nINSERT INTO public.post (id, author, content, created_at) VALUES (1, 2, 'Hello, world!', '2022-01-13 13:59:59.911324 +00:00');<\/pre>\n\n\n\n<p>Two customers, one post, one comment \u2013 nothing fancy, but already enough to start my experiments.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"first-model\">First model<\/h1>\n\n\n\n<p>First off, I will define the most simple model possible: a user model. I delete everything from the models\/example directory and create a new file called <code>user.sql<\/code> there. Inside I write:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">SELECT * FROM customer<\/pre>\n\n\n\n<p>That\u2019s all. Of course, it won\u2019t be that easy in real systems, but just imagine that we have a more complex data structure. Now, it\u2019s time to define our view with its constraints in schema.yml and define what schema.yml is. Basically, <code>schema.yml<\/code> is the registry of all of our models in the current directory. Since all of the models are organized into a tree structure, descriptors can define some properties for underlying directories.<\/p>\n\n\n\n<p>Here is my description of the <code>user<\/code> model:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">version: 2\n\nmodels:\n- name: user\n  description: \"Same as customer, but with another name\"\n  columns:\n    - name: id\n      description: \"The primary key for this table\"\n      tests:\n        - unique\n        - not_null\n    - name: email\n      description: email\n      tests:\n        - unique\n        - not_null\n    - name: username\n      description: unique name of customer\n      tests:\n        - not_null\n        - unique<\/pre>\n\n\n\n<p>It\u2019s almost self-descriptive, but let\u2019s look at several interesting things:<\/p>\n\n\n\n<ol><li>Even though I select all fields from the <code>customer<\/code> table, I define only three fields in the schema. This demonstrates that they don\u2019t have to match each other. Of course, not enumerating all these fields leaves us prone to errors and is therefore not recommended.<\/li><li>The model itself and its columns have a field named <code>description<\/code>. This description will go into the documentation.<\/li><li>Each column may have field <code>tests<\/code>. I will run these tests to see if everything works as expected. Out of the box, dbt ships with four generic tests already defined: <code>unique<\/code>, <code>not_null<\/code>, <code>accepted_values<\/code>, and <code>relationships<\/code>. You can read more about them and custom tests in <a href=\"https:\/\/docs.getdbt.com\/docs\/building-a-dbt-project\/tests\" target=\"_blank\" rel=\"noopener\">docs<\/a>.<\/li><\/ol>\n\n\n\n<p>Now that I have this very basic model, I can create it in my DB with the <code data-enlighter-language=\"shell\" class=\"EnlighterJSRAW\">dbt run<\/code> command.<\/p>\n\n\n\n<p>This is how the output looks on my machine:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"488\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-8.png\" alt=\"\" class=\"wp-image-218844\"\/><\/figure>\n\n\n\n<p>What did this command do? It created a view named <code>user<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"401\" height=\"418\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-9.png\" alt=\"\" class=\"wp-image-218855\"\/><\/figure>\n\n\n\n<p>And its content precisely corresponds to our SQL query:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"1058\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-10.png\" alt=\"\" class=\"wp-image-218867\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"testing-model\">Testing model<\/h2>\n\n\n\n<p>Now, let\u2019s test if our model complies with the <code data-enlighter-language=\"shell\" class=\"EnlighterJSRAW\">dbt test<\/code> command (spoiler: they should fail).<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"916\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-4.png\" alt=\"\" class=\"wp-image-218808\"\/><\/figure>\n\n\n\n<p>What a surprise \u2013 I have a &#8220;nonunique&#8221; username! And you know what&#8217;s cool? It gives me the location of the script executed to perform the check, so let\u2019s open it. Here\u2019s how it looks:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">select\n  username as unique_field,\n  count(*) as n_records\n\nfrom \"demo\".\"public\".\"user\"\nwhere username is not null\ngroup by username\nhaving count(*) > 1<\/pre>\n\n\n\n<p>To check what\u2019s wrong, I can just hit <kbd>Ctrl<\/kbd>+<kbd>Enter<\/kbd> in my IDE and see problematic usernames. Here\u2019s how it looks for me:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"1058\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-11.png\" alt=\"\" class=\"wp-image-218878\"\/><\/figure>\n\n\n\n<p>Now I know what to do to fix it \u2013 I\u2019ll just update my username. <code>pasha.finkelshteyn<\/code> should do the trick.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"970\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-12.png\" alt=\"\" class=\"wp-image-218890\"\/><\/figure>\n\n\n\n<p>Tests passed!<\/p>\n\n\n\n<p>But now the question arises: what if I want to build a more complex model? A model on top of other models, for example?<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"second-model\">Second model<\/h1>\n\n\n\n<p>Of course, it\u2019s possible thanks to <a href=\"https:\/\/jinja.palletsprojects.com\/en\/3.0.x\/\" target=\"_blank\" rel=\"noopener\">jinja<\/a> templating in models.<\/p>\n\n\n\n<p>Jinja is a modern and powerful templating library used in all kinds of projects. There are such well-known tools as <a href=\"https:\/\/www.ansible.com\/\" target=\"_blank\" rel=\"noopener\">Ansible<\/a> and <a href=\"https:\/\/www.djangoproject.com\/\" target=\"_blank\" rel=\"noopener\">Django<\/a>. dbt does not usually utilize the full power of Jinja, but only a tiny subset of it (phew!). We need a dbt-specific function, called <code>ref<\/code>. It is almost clear from its name that it can refer to one model from another.<\/p>\n\n\n\n<p>Let\u2019s say, for example, that we want to view users with a count of posts they wrote. To perform this task, I\u2019ll create a model.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">WITH users AS (\n  SELECT u.id user_id, u.username username\n  FROM {{ ref('user') }} u\n)\nSELECT user_id,\n      username,\n      count(post.id) post_number\nFROM users,\n    post\nWHERE post.author = user_id\nGROUP BY user_id, username<\/pre>\n\n\n\n<p>Also, I register the model in the schema:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">- name: user_post_count\n  description: info about users and posts they wrote\n  columns:\n    - name: user_id\n      type: bigint\n      tests:\n        - unique\n        - not_null\n        - relationships:\n            to: ref('user')\n            field: id\n    - name: username\n      type: text\n      tests:\n        - unique\n        - not_null\n        - relationships:\n            to: ref('user')\n            field: username\n    - name: post_number\n      description: number of posts user wrote\n      type: int\n      tests:\n        - not_null<\/pre>\n\n\n\n<p>In both schema and model, you can see that I reference the already existing view with help of the ref function, and I hope that its usage is self-explanatory. It may be interesting to look at generated model code:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">WITH users AS (\n  SELECT u.id user_id, u.username username\n  FROM \"demo\".\"public\".\"user\" u\n)\nSELECT user_id,\n      username,\n      count(post.id) post_number\nFROM users,\n    post\nWHERE post.author = user_id\nGROUP BY user_id, username<\/pre>\n\n\n\n<p>The exciting thing here is that dbt itself substituted the reference with the current view location in the database.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"updating-transform-layer\">Updating transform layer<\/h1>\n\n\n\n<p>This time <code data-enlighter-language=\"shell\" class=\"EnlighterJSRAW\">dbt build<\/code> performed a humongous amount of work, checking and recreating two views and running 13 tests.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"1143\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-13.png\" alt=\"\" class=\"wp-image-218902\"\/><\/figure>\n\n\n\n<p>All the tests passed, so we have a new view (1) with a count of posts (2) for each user:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"1058\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-14.png\" alt=\"\" class=\"wp-image-218913\"\/><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"generating-docs-on-the-transform-layer\">Generating docs on the transform layer<\/h1>\n\n\n\n<p>The final thing I want to do is generate documentation on my current view. To accomplish this, I will run the <code data-enlighter-language=\"shell\" class=\"EnlighterJSRAW\">dbt docs serve<\/code> command.<\/p>\n\n\n\n<p>The generated documentation is just beautiful! For example, it contains:<\/p>\n\n\n\n<ul><li>List of columns in a view<\/li><\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1176\" height=\"400\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-17.png\" alt=\"\" class=\"wp-image-218967\"\/><\/figure>\n\n\n\n<ul><li>Dependencies<\/li><\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1176\" height=\"369\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-16.png\" alt=\"\" class=\"wp-image-218952\"\/><\/figure>\n\n\n\n<ul><li>Source and compiled code of model:<\/li><\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"868\" height=\"613\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2022\/01\/image-15.png\" alt=\"\" class=\"wp-image-218933\"\/><\/figure>\n\n\n\n<p>As well as lineage info, view diagram, and many more.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"conclusion\">Conclusion<\/h1>\n\n\n\n<p>Let\u2019s look at what we accomplished today:<\/p>\n\n\n\n<ol><li>Created a dbt project.<\/li><li>Described a couple of models in our exampletoy warehouse.<\/li><li>Generated docs on our transformation layer.<\/li><\/ol>\n\n\n\n<p>Quite impressive for a small manual, right?<\/p>\n\n\n\n<p>As you can see, dbt is an extremely easy-to-use tool for anybody familiar with SQL. It allows you to build, test, and document the transformation layer of the warehouse quickly.<\/p>\n\n\n\n<p>Of course, I\u2019ve only covered the essential aspects. I\u2019ve built only a couple of views and we didn\u2019t touch on topics like snapshots and incremental views. With that in mind, stay tuned to learn more!<\/p>\n\n\n\n<p>You can check the source code on my <a href=\"https:\/\/github.com\/asm0dey\/dbt-getting-started\" target=\"_blank\" rel=\"noopener\">GitHub<\/a>, and you\u2019re welcome to send me any feedback or improvement proposals.<\/p>\n\n\n\n<p class=\"has-small-font-size\"><a href=\"https:\/\/getdbt.com\" class=\"ek-link\" target=\"_blank\" rel=\"noopener\">dbt<\/a> Mark is a trademark of dbt Labs, Inc.<\/p>\n","protected":false},"author":1234,"featured_media":219559,"comment_status":"closed","ping_status":"closed","template":"","categories":[594],"tags":[588,6920,6918,201],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/big-data-tools\/218418"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/big-data-tools"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/types\/big-data-tools"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/users\/1234"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/comments?post=218418"}],"version-history":[{"count":10,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/big-data-tools\/218418\/revisions"}],"predecessor-version":[{"id":226603,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/big-data-tools\/218418\/revisions\/226603"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media\/219559"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=218418"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/categories?post=218418"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/tags?post=218418"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/cross-post-tag?post=218418"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}